суботу, 26 листопада 2016 р.

Мій проект: Контроль напруги батарей

Передмова

Цей допис роблю для себе, як документацію на пристрій. Пристрій робився на замовлення в одному екземплярі. Як кому згодиться то дуже добре. В цьому проекті є: RTC, ADC, TIMER, LCD1602, keyboard, micromenu, SD-CARD SPI, UART. Можна подивитись як реалізовано для прикладу і використовувати в своїх проектах.

Призначення


Цей пристрій призначений для контролю поточної напруги +60В, -60В, -24В, на батареях, та у разі виходу напруги за межі норми сповістити користувача звуковою і світловою сигналізацією. Пристрій оснащений символьним дисплеем де виводиться інформація про напруги, поточний час з датою. Також пристрій записує на SD-Card всі події які відбуваються: поточні значення напруги, вихід напруги за межі норми, час коли користувач зреагував на аварійну ситуацію, та коли напруги повернулись в межі норми. Пристрій має розгалужене, зручне меню для гнучкого налаштування параметрів які зберігаються на флеш-карті.

Схема пристрою

Пристрій складається з двох плат на яких розташовані: дільник напруги, мікропроцесорний блок на STM32F103C8T6 (blue peel), дисплей на 2 рядки по 16 символів LCD1602, модуль SD-CARD, клавіатурний блок і блок сигналізації (звукова і світлова), та блок живлення з гальванічною розв'язкою на  5В (наприклад, від зарядного для смартфону).
Схема пристрою. Клацайте щоб збільшити.

Програмне забезпечення

Всі сирці проекту CubeMX + Atolic TrueStudio знаходяться за ланкою.

середу, 9 листопада 2016 р.

STM32: Бібліотека Delay (мілісекунди, мікросекунди) на таймерах


Передмова

Звісно, штучних затримок в програмі потрібно уникати. І часові затримки краще організовувати за допомоги переривань по таймерам з подальшої обробкою. Але в невеличких проектах і там де виконання програми не критичне до затримок, дуже зручно використовувати прості функції delay. Нещодавно почав використовувати драйвер HAL де є вже функція затримки в мілісекундах. Функції затримки в мікросекундах на жаль там немає. Та і прив'язуватись до якогось високорівневого драйверу в такій простій задачі, як затримки, теж немає сенсу. Вирішив створити бібліотеку delay для своїх потреб з затримками в мілісекундах і мікросекундах. Щоб відлік часу був точним, обов'язково організувати відлік часу на таймерах. Щоб була відв'язана від високорівневих драйверів HAL чи SPL і мала мінімум налаштувань. Щоб достатньо було вказати який використовувати для затримок таймер.

Бібліотека delay

Бібліотека "delay" складається з двох файлів: delay.h і delay.c

Має такі функції:
  • void delayInit(void) - функція вмикання потрібного таймеру для відліку пауз;
  • void delayDeInit(void) - функція вимикання таймеру, як відлік пауз вже не потрібен;
  • void delayMs(volatile uint32_t delay) - функція відліку затримки в мілісекундах, на вхід ціле число мілісекунд
  • void delayUs(volatile uint32_t delay) - функція відліку затримки в мікросекундах, на вхід ціле число мікросекунд
В файлі delay.h обрати який таймер буде вести відлік часу. Можна обрати з TIM1, TIM2, TIM3 та TIM4. Як потрібен інший таймер, дописати в файлі delay.c необхідний таймер по аналогії.
Частота для правильного відліку часу береться зі змінної SystemCoreClock, це якщо частота шини таймеру співпадає з SystemCoreClock (дивись малюнок). Червоним обведено ключові місця:
Однакові частоти
Бувають випадки що частоти не співпадають, як, наприклад, на іншому малюнку:
Частоти не однакові
Як видно з малюнку, частота SYSCLK - 32MHz, а шина таймера на шині APB1 периферії вже тактується 8MHz. І шина таймерів на шині периферії APB2 тактується 16MHz. Це все завдяки дільникам і множникам шин APB. Тому будьте уважні і задавайте правильні частоти в герцах для таймерів в файлі delay.h. 
Звичайно можна визначити частоту автоматично за допомоги функцій драйверу HAL, наприклад такими як:
HAL_RCC_GetSysClockFreq(), HAL_RCC_GetHCLKFreq(), HAL_RCC_GetPCLK1Freq(), HAL_RCC_GetPCLK2Freq(), тощо.
Але ж хотілось відійти від високорівневих драйверів. Напевно є спосіб автоматизувати визначення частоти таймеру і без HAL чи SPL, але я ще не дійшов до цього :-)

delay.h


/*
 * my_delay.h
 *
 *  Created on: 20 груд. 2017 р.
 *      Author: Andriy
 */

#ifndef DELAY_H_
#define DELAY_H_

#include <stdint.h>

//Розкоментувати потрібний таймер для функцій затримки
#define TIMER1
// #define TIMER2
// #define TIMER3
// #define TIMER4

#ifdef TIMER1
#define CURRENT_TIMER               ((TIM_TypeDef *) TIM1)
#endif

#ifdef TIMER2
#define CURRENT_TIMER               ((TIM_TypeDef *) TIM2)
#endif

#ifdef TIMER3
#define CURRENT_TIMER               ((TIM_TypeDef *) TIM3)
#endif

#ifdef TIMER4
#define CURRENT_TIMER               ((TIM_TypeDef *) TIM4)
#endif

#define CURRENT_FREQ    SystemCoreClock

void delayInit(void);
void delayDeInit(void);
void delayMs(volatile uint32_t delay);
void delayUs(volatile uint32_t delay);

#endif /* DELAY_H_ */

delay.c


/*
 * delay.c
 *
 *  Created on: 20 груд. 2017 р.
 *      Author: Andriy
 */

#include <delay.h>
#include "stm32f1xx.h"

// Функція вмикання таймеру для потреб delay
void delayInit(void)
{
#ifdef TIMER1
 RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
#endif

#ifdef TIMER2
 RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
#endif

#ifdef TIMER3
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
#endif

#ifdef TIMER4
 RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
#endif
}

// Функція вимикання таймеру як немає потреби в delay
void delayDeInit(void)
{
#ifdef TIMER1
 RCC->APB2ENR &= ~RCC_APB2ENR_TIM1EN;
#endif

#ifdef TIMER2
 RCC->APB1ENR &= ~RCC_APB1ENR_TIM2EN;
#endif

#ifdef TIMER3
 RCC->APB1ENR &= ~RCC_APB1ENR_TIM3EN;
#endif

#ifdef TIMER4
 RCC->APB1ENR &= ~RCC_APB1ENR_TIM4EN;
#endif
}

//Функція формування затримки в мілісекундах
void delayMs(volatile uint32_t delay)
{
 CURRENT_TIMER->PSC = CURRENT_FREQ/1000-1; //Встановлюємо подрібнювач
 CURRENT_TIMER->ARR = delay; //встановлюємо значення переповнювання таймеру, а також і значення при якому генеруеться подія оновлення
 CURRENT_TIMER->EGR |= TIM_EGR_UG; //Генерируемо Подію оновлення для запису даних в регістри PSC і ARR
 CURRENT_TIMER->CR1 |= TIM_CR1_CEN|TIM_CR1_OPM; //Запускаемо таймер записом биту CEN і встановлюємо режим Одного проходу встановленням біту OPM
 while ((CURRENT_TIMER->CR1) & (TIM_CR1_CEN!=0)); //Виконуємо цикл поки рахує таймер до нуля
}

//Функція формування затримки в мікросекундах
void delayUs(volatile uint32_t delay)
{
    CURRENT_TIMER->PSC = CURRENT_FREQ/1000000-1; ///Встановлюємо подрібнювач
 CURRENT_TIMER->ARR = delay; //встановлюємо значення переповнювання таймеру, а також і значення при якому генеруеться подія оновлення
 CURRENT_TIMER->EGR |= TIM_EGR_UG; //Генерируемо Подію оновлення для запису даних в регістри PSC і ARR
 CURRENT_TIMER->CR1 |= TIM_CR1_CEN|TIM_CR1_OPM; //Запускаемо таймер записом биту CEN і встановлюємо режим Одного проходу встановленням біту OPM
 while ((CURRENT_TIMER->CR1) & (TIM_CR1_CEN!=0)); //Виконуємо цикл поки рахує таймер до нуля
}

Як користуватись

Скопіюйте теку, або файли бібліотеки до теки свого проекту. Приєднайте до свого проекту теку, або файли бібліотеки. До головного файлу main.c додайте #include "delay.h". В файлі "delay.h" зазначте який таймер для відліку затримок будете використовувати. І при потребі, частоту шини таймерів в герцах. Коли потрібна затримка просто викличте одну із функцій delayMs(xxx) чи delayUs(xxx) - де xxx часова затримка в мілісекундах чи мікросекундах. Наприклад, delayMs(500) - означає почекаємо пів секунди. Та не забувайте перед викликом цих функцій увімкнути таймер викликом функції delayInit().

Архів з бібліотекою

Завантажити архів з бібліотекою - download

середу, 2 листопада 2016 р.

Мій проект: Лічильник подій 4x4 (поточні, попередні, різниця)


Передмова

Цей допис роблю для себе, як документацію на пристрій. Пристрій робився на замовлення в одному екземплярі. Як кому згодиться то дуже добре.

Призначення

Зовнішній вигляд готового пристрою
Лічильник подій 4х4 фіксує і показує на екрані поточні показники лічильників, різницю між поточними показами лічильників і попереднім зняттям показів лічильників. Показання лічильників виводяться на чотироьохрозрядний семисегментний індикатор по черзі.
Слугує заміною застарілим електромеханічним лічильникам, які вже використали свій ресурс і працюють з великою похибкою.

Як користуватись

Після зняття показів всіх лічильників, натиснути кнопку для фіксації, що значення лічильників занесено до журналу і розрахунок різниці ведеться вже з поточних показів лічильників.

Схеми блоків

Блок фіксації події, гальванічної розв'язки та антибрязкіт.
Схема блоків оптичної розв'язки і антибрязкіт
Мікроконтролерний блок STM32F103C8T6.
Схема мікроконтролерного блоку
Блок індикації і кнопки.
Схема блока індикації і кнопки
Блок живлення.
Схема блоку живлення
Архів зі схемами. Живлення пристрою від акумуляторів 24 Вольта великої ємності в режимі "буфер". Зарядний пристрій до акумуляторів живиться від гарантованої мережі 220В. Додаткового резервного живлення не потребує.

Програмне забезпечення

Початкова ініціалізація периферії згенерована програмою CubeMX. Програма написана мовою C в засобі розробки CooCox CoIDE 1.7. Архів з проектом.

  • mxconstants.h - константи, структури
  • main.c - головна програма
  • stm32f1xx_it.c - обробник переривань
Дещо з коду під спойлером.



Прототипи функцій.
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
static void DigitToPort (uint8_t digit); // виставляє в відповідні розряди індикатора числа-розряди.
static void ShowScreen(uint8_t* number, uint8_t number_of_digits, const uint16_t* pDigitPin); // Вивід числа на екран. Аргументи: масив з чисел-розрядів після функції SplitToDigit, та кількість розрядів.
static void SplitToDigit(uint32_t counter, uint8_t* digit, uint8_t number_of_digits); // число з лічильника розбиває на окремі розряди. вхідні аргументи: значення лічильника, масив для розрядів, кількість розрядів.
/* USER CODE END PFP */

Деякі змінні.
/* USER CODE BEGIN 2 */
uint8_t digit[NUM_DIGIT] = {}; // Масив для символів в кожен розряд індикатора окремо
static const uint16_t digitPin[] = {D0_Pin,D1_Pin,D2_Pin,D3_Pin};
/* USER CODE END 2 */

Функції для роботи 7-сегментного дисплею
//Функція відображення інформації на дисплей
static void ShowScreen(uint8_t* number, uint8_t number_of_digits, const uint16_t* pDigitPin)
{
 for(uint8_t i = 0; i < number_of_digits; i++)
 {
  IND_PORT -> BRR = (D0_Pin|D1_Pin|D2_Pin|D3_Pin);//Вимикаємо всі розряди
  IND_PORT -> BSRR = pDigitPin[i];//Вмикаємо потрібний розряд індикатора
  DigitToPort(*(number+i));//Виводимо цифру у потрібний розряд
  HAL_Delay(1);//Невеличка затримка. Хай цифра світиться якийсь час
 }
}


// число з лічильника розбиває на окремі розряди. вхідні аргументи: значення лічильника, масив для розрядів, кількість розрядів.
static void SplitToDigit(uint32_t counter, uint8_t* digit, uint8_t number_of_digits)
{
 uint32_t tmp = counter;

 for (uint8_t i = 0; i < number_of_digits; i++)
 {
  *(digit+i) = tmp % 10;
  tmp = tmp / 10;
 }

}


//Функція виставляє в порт потрібну цифру
static void DigitToPort (uint8_t digit)
{
 static const uint8_t digitsp[]={DIG0,DIG1,DIG2,DIG3,DIG4,DIG5,DIG6,DIG7,DIG8,DIG9,SEG_G_Pin,LITA,~DIG8,LITP,LITH,LITPE}; //оголошуємо масив з можливими варіантами символами на індикатор
 IND_PORT -> ODR &= ~DIG8; //Вимикаємо всі сегменти
 IND_PORT -> ODR |= digitsp[digit]; //Запалюємо потрібні
}

Виклик функції для виводу інформації на дисплей. Або в безкінечному циклі, або по перериванням.
ShowScreen(digit,sizeof(digit)/sizeof(digit[0]),digitPin);




Оголошуємо масиви. Створюємо структуру і оголошуємо масив структур.
bool flag_button[NUM_COUNTER] = {}; // Масив з прапорцями стану отримання імпульсу на лічильник

typedef struct
{
 uint32_t current;
 uint32_t previous;
}CounterTypeDef;

CounterTypeDef AI_Counter[NUM_COUNTER] = {}; // Масив з кількістью структур для лічильників

static const uint16_t ButtonPin[] = {AI_COUNTER1_Pin,AI_COUNTER2_Pin,AI_COUNTER3_Pin,AI_COUNTER4_Pin,
                            AI_COUNTER5_Pin,AI_COUNTER6_Pin,AI_COUNTER7_Pin,AI_COUNTER8_Pin,
                            AI_COUNTER9_Pin,AI_COUNTER10_Pin,AI_COUNTER11_Pin,AI_COUNTER12_Pin,
                            AI_COUNTER13_Pin,AI_COUNTER14_Pin,AI_COUNTER15_Pin,AI_COUNTER16_Pin};

static GPIO_TypeDef* ButtonPort[] = {AI_COUNTER1_GPIO_Port,AI_COUNTER2_GPIO_Port,AI_COUNTER3_GPIO_Port,AI_COUNTER4_GPIO_Port,
                            AI_COUNTER5_GPIO_Port,AI_COUNTER6_GPIO_Port,AI_COUNTER7_GPIO_Port,AI_COUNTER8_GPIO_Port,
                            AI_COUNTER9_GPIO_Port,AI_COUNTER10_GPIO_Port,AI_COUNTER11_GPIO_Port,AI_COUNTER12_GPIO_Port,
                            AI_COUNTER13_GPIO_Port,AI_COUNTER14_GPIO_Port,AI_COUNTER15_GPIO_Port,AI_COUNTER16_GPIO_Port};

В циклі опитуємо входи на подію. Якщо прапорець "flag_bounce" в true.
if (flag_bounce)
 {
  flag_bounce = false;
    for (uint8_t i = 0; i < NUM_COUNTER; i++)
     {
       if (!HAL_GPIO_ReadPin(ButtonPort[i],ButtonPin[i]) && !flag_button[i])
     {
           flag_button[i] = true;
           ++AI_Counter[i].current;
     }
     else if(HAL_GPIO_ReadPin(ButtonPort[i],ButtonPin[i]) && flag_button[i])
          {
           flag_button[i] = false;
          }
     }
   }

Увімкнено два таймери. Таймер TIM2 в перериванні скидає прапорець "flag_lock_screen" для зміни інформації на екрані що 3 секунди. Таймер TIM4 в перериванні встановлює прапорець "flag_bounce" для дозволу опитування входів на подію через кожні 25 мілісекунд (програмний антибрязкіт).