понеділок, 26 грудня 2016 р.

STM32: Годинник реального часу на stm32f1xx. Проблеми, рішення (частина перша)



За допомоги способу реалізації RTC, які висвітлені в цій статті, так і не вдалось враховувати дні, протягом яких MCU був у вимкненому стані, або режимі сну за допомоги HAL драйверу. Реалізація за допомоги CMSIS працює коректно. В другій частині цієї статті (продовженні) описано як вірішити проблему з датою, за допомоги невеличкої бібліотеки, де змінено деякі HAL функції роботи з RTC.

Передмова

В деяких проектах, виконаних на мікроконтролерах, є потреба в годиннику реального часу - RTC (real time clock). На мікроконтролерах STM32F1xx теж є RTC. Хоч і куций, але є і з ним можна працювати. Чому куций? Тому що час, який зберігається, а точніше продовжує йти - це звичайний 32-бітний лічильник секунд. І це все! Добра новина що, цей 32-бітний, лічильник знаходиться в домені BACKUP і продовжує додавати секунди навіть без основного живлення, якщо під'єднано до ніжки мікроконтролера VBAT - батарейку на 3 вольта. Розгляньмо підключення резервного живлення та способи реалізації RTC. Піддослідною платою розробника буде miniboard на чипові STM32F103C8T6. З підключеним дисплеєм WH1602 (HD44780). Бібліотека роботи з цим дисплеєм та його підключення у статті:
"STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780"

miniboard STM32F103C8T6

Схема підключення резервного живлення до VBAT

update 25.02.2019: Як виявилось, батарейку 3V, достатньо під'єднати до шпильки VBAT мікроконтролеру через діод D1, щоб основне живлення не потрапляло на батарейку. І це все, діод D2 можна не встановлювати, за бажанням. Коли мікроконтролер увімкнений, то і RTC живиться від загального живлення. Коли з мікроконтролеру зняти живлення, то RTC живиться від батарейки.

Щоб основне живлення не потрапляло на батарейку, а батарейка не живила всю схему, слід підключати, наприклад, по такій схемі:
Схема підключення резервного живлення VBAT

RTC за допомоги драйверу HAL

Реалізація RTC на HAL драйвері неповноцінна. А саме, лічильник RTC зберігає виключно секунди в межах доби. Дата зберігається просто в енергозалежній ОЗП і при вимкненні основного живлення значення дня, місяця і року скинуться до 01.01.00. З-за цього будильник можна встановити в межах доби. Звернемось до документації. В пункті 33.2.2 на сторінці 427 зазначено:
WARNING: Drivers RestrictionsRTC version used on STM32F1 families is version V1. All the features supported by V2
(other families) will be not supported on F1.
As on V2, main RTC features are managed by HW. But on F1, date feature is completely
managed by SW.


Then, there are some restrictions compared to other families:

  • Only format 24 hours supported in HAL (format 12 hours not supported)
  • Date is saved in SRAM. Then, when MCU is in STOP or STANDBY mode, date will be lost. User should implement a way to save date before entering in low power mode (an example is provided with firmware package based on backup registers)
  • Date is automatically updated each time a HAL_RTC_GetTime or HAL_RTC_GetDate is called. 
  • Alarm detection is limited to 1 day. It will expire only 1 time (no alarm repetition, need to program a new alarm) 
Програмна підтримка колодару. Та ще й так кострубато. З-за цього накладені обмеження в порівнянні з іншими серіями мікроконтролерів. А саме:
  • Тільки 24-годинний формат часу
  • Дата зберігається в ОЗП і втрачається при перезавантаженні, втраті живлення чи переході в режим очікування. Треба самому потурбуватись про збереження дати в backup регістри.
  • Сигнал тривоги обмежений однією добою. І потрібно наново встановлювати при потребі.
Зрозуміло. Так і зробимо. Потурбуємось про збереження дати в backup регістри самі.
Скористаємось нашим помічником CubeMX for STM32.

Налаштування і запуск RTC в CubeMX

Особливо налаштовувати нічого і не потрібно. Просто в пункті "Pinout" в гілці "RCC" пункт "Low Speed Clock (LSE)" виставте в "Crystal/Ceramic Resonator", а в гілці "RTC" активуйте пункт "Activate Clock Source". Активувати пункт "Activate Calendar" не потрібно. Бо тоді, при кожному новому увімкнені чи перезавантаженні мікроконтролера, буде встановлюватись дата, яку задали в налаштуваннях кожного разу. Зробіть як показано на малюнку:
RTC PinOut
Далі переходимо до вкладки "Clock Configuration" і обираємо зовнішній кварцовий резонатор як на малюнку:
RTC Clock Configuration
В пункті RTC Configuration нічого не змінював, залишив як є:
RTC Configuration
У вкладці "NVIC Settings" за потреби оберіть переривання по RTC.

Код ініціалізації HAL

Генеруємо проект з кодом ініціалізації CubeMX. Відкривайте свій засіб розробки. Для ініціалізації годинника реального часу CubeMX генерує такі рядки до файлу main.c:
Структура з налаштуваннями RTC.
/* Private variables ---------------------------------------------------------*/
RTC_HandleTypeDef hrtc;
Прототип функції ініціалізації RTC.
/* Private function prototypes -----------------------------------------------*/
static void MX_RTC_Init(void);
В головну функцію main вставляє рядок виклику функції ініціалізації RTC.
 /* Initialize all configured peripherals */
  MX_RTC_Init();
І саму функцію ініціалізації RTC вже після main.
/* RTC init function */

static void MX_RTC_Init(void)
{

    /*Initialize RTC Only*/

  hrtc.Instance = RTC;
  hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
  hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
  if (HAL_RTC_Init(&hrtc) != HAL_OK)
  {
    Error_Handler();
  }

}
Простіше не буває.
Якщо вам потрібно, щоб були переривання раз на секунду від RTC і ви зробили відповідну позначку в CubeMX у вкладці "NVIC Settings", то в файл stm32f1xx_it.c буде розміщено обробник переривань:
/**
* @brief This function handles RTC global interrupt.
*/
void RTC_IRQHandler(void)
{
  /* USER CODE BEGIN RTC_IRQn 0 */

  /* USER CODE END RTC_IRQn 0 */
  HAL_RTCEx_RTCIRQHandler(&hrtc);
  /* USER CODE BEGIN RTC_IRQn 1 */
  
  /* USER CODE END RTC_IRQn 1 */
}
Але переривання відбуватись не будуть поки в основному файлі main.c не дозволити ці переривання функцією HAL_RTCEx_SetSecond_IT(&hrtc); в якості параметру вказівник на структуру RTC. Наприклад, розташуйте цей рядок в користувацькому блоці 2.
 /* USER CODE BEGIN 2 */

  HAL_RTCEx_SetSecond_IT(&hrtc);

 /* USER CODE END 2 */
Після виконання цього рядка мікроконтролером, кожної секунди мікроконтролер буде звертатись до функції обробника переривань RTC "RTC_IRQHandler", який розташований в файлі stm32f1xx_it.c.
Якщо потрібна корекція часу то можна скористатись функцією HAL_RTCEx_SetSmoothCalib, детально про цю функцію дивись у документації, про корректировку докладно тут.

Це все. Після цих маніпуляцій в 32-бітний регістр RTC кожну секунду буде додаватись одиниця. А при вимкнені основного живлення домен RTC буде живитись від резервного живлення, якщо ви його під'єднали. І годинник буде продовжувати працювати.
Але ж треба задати поточну дату і час, та показати дату і час на дисплей. А також не забуваймо, що нам самим треба потурбуватись про збереження дати в backup регістри. Ну що ж - почнемо!

Функція встановлення дати і часу

Щоб оперувати датою і часом, потрібно спочатку оголосити структури дати та часу:
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;

Тепер можна встановлювати дату і час. Вхідні данні функції це рік, місяць, день, день тижня, години, хвилини, секунди та формат часу. Функція нічого не повертає:
static void setDataAndTime(uint8_t year, uint8_t month, uint8_t day, uint8_t weekday, 
             uint8_t hour, uint8_t min,  uint8_t sec, uint32_t format)
{
    /**Initialize RTC and set the Time and Date**/
  sTime.Hours = hour;
  sTime.Minutes = min;
  sTime.Seconds = sec;

  if (HAL_RTC_SetTime(&hrtc, &sTime, format) != HAL_OK)
  {
    Error_Handler();
  }

  sDate.WeekDay = weekday;
  sDate.Month = month;
  sDate.Date = day;
  sDate.Year = year;

  if (HAL_RTC_SetDate(&hrtc, &sDate, format) != HAL_OK)
  {
    Error_Handler();
  }

}

Функція збереження дати до backup регістрів

Так як для збереження дати потрібно 32 біти, а backup регістри мають по 16 біт, то нам потрібно для збереження дати аж два backup регістри. На вхід функція отримує в які саме регістри потрібно зберегти дату. Нічого не повертає:
static void writeDateToBackup(uint32_t bkp_reg1, uint32_t bkp_reg2)
{
  HAL_RTC_GetDate(&hrtc, &sDate, FORMAT_BCD);
  HAL_RTCEx_BKUPWrite(&hrtc, bkp_reg2, (sDate.WeekDay << 8) | (sDate.Year));
  HAL_RTCEx_BKUPWrite(&hrtc, bkp_reg1, (sDate.Date << 8) | (sDate.Month));
}

Функція відновлення дати з backup регістрів

І ще нам потрібна функція яка буде читати записану дату з backup регістрів, заповнювати структуру дати і встановлювати її як поточну. На вхід функція отримує з яких саме регістрів потрібно відновити дату. Нічого не повертає:
static void readBackupToDate(uint32_t bkp_reg1, uint32_t bkp_reg2)
{
 sDate.Month = HAL_RTCEx_BKUPRead(&hrtc,bkp_reg1) & 0x00ff;
 sDate.Date = HAL_RTCEx_BKUPRead(&hrtc,bkp_reg1) >> 8;
 sDate.Year = HAL_RTCEx_BKUPRead(&hrtc,bkp_reg2) & 0x00ff;
 sDate.WeekDay =HAL_RTCEx_BKUPRead(&hrtc,bkp_reg2) >> 8;
 HAL_RTC_SetDate(&hrtc, &sDate, FORMAT_BCD);
}

Функція перевірки нового дня

Ця функція потрібна щоб перевірити чи настав новий день. Як настав, то повертає "false", як той самий день, то повертає "true". 
static bool checkNewDay(uint32_t bkp_reg1)
{
 HAL_RTC_GetDate(&hrtc, &sDate, FORMAT_BCD);
 if(sDate.Date != HAL_RTCEx_BKUPRead(&hrtc,bkp_reg1) >> 8)
 {
  return false;
 } else {
  return true;
 }
}
За допомоги цієї функції можна оновити при зміні дня данні про дату в backup регістри.

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

Всю необхідну роботу по ініціалізації та запуску нашого RTC виконав CubeMX, залишилось всім цим скористатись.
Алгоритм буде приблизно такий:
  • Після ініціалізації RTC перевіряємо чи в backup регістрах, відведених для зберігання дати, щось є крім нуля? Як нуль, то можна встановити якусь початкову дату.
  • Як не нуль, то запам'ятовуємо значення RTC лічильника в тимчасову змінну, тому що виклик функції запису збереженої дати зменшить значення RTC лічильника в межах доби. І час, коли MCU знаходився у відключеному стані - втратиться. Структуру дати заповнюємо даними з backup регістрів де зберігається дата перед вимкненням пристрою. Відновлюємо значення RTC лічильника з тимчасової змінної та викликаємо функцію HAL_RTC_GetTime або HAL_RTC_GetDate які приведуть час до стану "в межах доби", а якщо MCU був відключений більше ніж добу, то "пропущений" час додасця до дати автоматично;
  • Далі вже можна показувати час та дату на дисплей, спочатку викликавши функцію HAL_RTC_GetTime та функцію HAL_RTC_GetDate, а вже потім організувати вивід на дисплей отриманих даних;
  • Не забуваємо хоч раз на початку нової доби викликати функцію checkNewDay, щоб перевірити чи не настав новий день і зберігати до backup регістрів нове значення дати.
Ось приблизний приклад як це може виглядати програмно:
/* USER CODE BEGIN 2 */
  lcdInit(); // ініціалізація дисплею
  uint8_t aShowTime[16] = {0};

// Після увімкнення, пробудження, скидання, потрібно викликати цю функцію  
HAL_RTC_WaitForSynchro(&hrtc);  
// Перевіряємо чи дата вже була збережена чи ні
  if (HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1) == 0)
  {
   // як ні, то задамо якусь початкову дату і час
   setDataAndTime(0x17,RTC_MONTH_JANUARY,0x14,RTC_WEEKDAY_SATURDAY,0x21,0x48,0x00,FORMAT_BCD);
   // і запишемо до backup регістрів дату
   writeDateToBackup(RTC_BKP_DR1,RTC_BKP_DR2);
  } else {
 // якщо вже годинник встановлено, то...
 // запам'ятовуємо кількість секунд з лічильника RTC
 uint32_t timeCounter = (RTC->CNTH << 16) | RTC->CNTL;
 // відновимо з backup регістрів дату. Від лічильника RTC віднімиться час що виходить за межі доби
 readBackupToDate(RTC_BKP_DR1,RTC_BKP_DR2);
 // відновимо попереднє, збережене значення лічильника RTC
 RTC->CNTH = timeCounter>>16;
 RTC->CNTL = timeCounter;
 // Після запису до регістру CNT потрібно викликати цю функцію
 HAL_RTC_WaitForSynchro(&hrtc);
 // виклик цієї функції врахує пропущені дні що був MCU у вимкненому стані
 HAL_RTC_GetTime(&hrtc, &sTime, FORMAT_BIN);
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   // отримуємо поточні час і дату
   HAL_RTC_GetTime(&hrtc, &sTime, FORMAT_BIN);
   HAL_RTC_GetDate(&hrtc, &sDate, FORMAT_BIN);

   // виводимо дату і час на дисплей
   sprintf((char*)aShowTime,"\t%02d:%02d:%02d",sTime.Hours, sTime.Minutes, sTime.Seconds);
   lcdGoto(LCD_2nd_LINE,0);
   lcdPuts((char*)aShowTime);

   sprintf((char*)aShowTime,"\t%02d/%02d/%02d",sDate.Date,sDate.Month,sDate.Year);
   lcdGoto(LCD_1st_LINE,0);
   lcdPuts((char*)aShowTime);

// перевіримо чи настав інший день
   if(!checkNewDay(RTC_BKP_DR1))
   {
    // як так, то запишемо до backup регістрів нову дату
    writeDateToBackup(RTC_BKP_DR1,RTC_BKP_DR2);
   }
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
В прикладі все враховується, час не втрачається при вимкнені пристрою, якщо під'єднано резервне живлення, як показано на схемі на початку статті. А дата залишається тією що була на момент відключення живлення, хоча і мають додатись пропущені дні. Поки не зміг "побороти" проблему.

Це тільки приклад для демонстрації роботи RTC. Функції можна вдосконалити чи переробити на свій лад. Показав сам принцип.

Висновок: Повноцінно користуватись можна. Недолік - займання аж двох BACKUP регістрів, які можуть знадобитись для більш важливіших цілей. Дата зберігається, але дні коли пристрій був у "відключці" - втрачаються. Краще реалізувати свою бібліотеку реалізації RTC з колодаром. 

Налаштування і запуск RTC за допомоги CMSIS драйверу

Щоб обійтись без HAL для RTC можна скористатись стандартним драйвером CMSIS. Це стандартна бібліотека, у ній визначено/оголошено всі імена регістрів мікроконтролера. Тому немає потреби задавати їх адреси в пам'яті, постійно зазираючи до технічної документації по мікроконтролеру. Не містить програмного коду як такого.
Ось варіант ініціалізації на CMSIS:
static void mRTCInit(void)
{
 if ((RCC->BDCR & RCC_BDCR_RTCEN) != RCC_BDCR_RTCEN)        // Перевірка роботи годинника, якщо не увімкнені, то ініціалізувати
   {
    RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN;  // Увімкнути тактування PWR та Backup
    PWR->CR |= PWR_CR_DBP;                                  // Дозволити доступ до Backup області
    RCC->BDCR |= RCC_BDCR_BDRST;                            // Скинути Backup область
    RCC->BDCR &= ~RCC_BDCR_BDRST;
    RCC->BDCR |= RCC_BDCR_RTCEN | RCC_BDCR_RTCSEL_LSE;      // Обрати LSE джерело (кварц 32768) і подати тактування
    RCC->BDCR |= RCC_BDCR_LSEON;                            // Увімкнути LSE
    while ((RCC->BDCR & RCC_BDCR_LSEON) != RCC_BDCR_LSEON){}// Дочекатись увімкнення
    BKP->RTCCR |= 0;                                        // Калібрування RTC значення від 0 до 0x7F
    while (!(RTC->CRL & RTC_CRL_RTOFF));                    // Перевірити чи закінчились зміни регістрів RTC
    RTC->CRL  |=  RTC_CRL_CNF;                              // Дозволити запис до RTC
    RTC->PRLL  = 0x7FFF;                                    // Налаштувати дільник на 32768 (32767+1)
    RTC->CRL  &=  ~RTC_CRL_CNF;                             // Заборонити запис до RTC
    while (!(RTC->CRL & RTC_CRL_RTOFF));                    // Дочекатись кінця запису
    RTC->CRL &= (uint16_t)~RTC_CRL_RSF;                     // Синхронизувати RTC
    while((RTC->CRL & RTC_CRL_RSF) != RTC_CRL_RSF){}        // Дочекатись синхронізації
    PWR->CR &= ~PWR_CR_DBP;                                 // Заборонити доступ до Backup області
   }
}
Текст функції докладно доповнений коментарями, додаткового пояснення не потребує.
Після виклику функції mRTCInit, яку ми самі створили, годинник реального часу почне додавати одиницю в 32-бітний регістр RTC раз на секунду.

Реалізація колодару

Напишемо всі необхідні функції для реалізації колодару.

Задавання і отримання значення 32-бітного лічильника RTC

Функція отримання значення 32-бітного лічильника RTC:
static uint32_t mRTCGetCounter(void)                           // Отримати значення лічильника
{
          return  (uint32_t)((RTC->CNTH << 16) | RTC->CNTL);
}
Параметрів немає. Повертає значення 32-бітного лічильника.

Функція встановлення нового значення 32-бітного лічильника RTC:
static void mRTCSetCounter(uint32_t count)                     // Записати нове значення лічильника
{
  RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN;   // Увімкнути тактування PWR і Backup
  PWR->CR |= PWR_CR_DBP;                                    // Дозволити доступ до Backup області
  while (!(RTC->CRL & RTC_CRL_RTOFF));                      // Перевірити чи закінчились зміни регистрів RTC
  RTC->CRL |= RTC_CRL_CNF;                                  // Дозволити запис в регістри RTC
  RTC->CNTH = count>>16;                                    // Записати нове значення годинникового лічильника
  RTC->CNTL = count;
  RTC->CRL &= ~RTC_CRL_CNF;                                 // Заборонити запис в регістри RTC
  while (!(RTC->CRL & RTC_CRL_RTOFF));                      // Дочекатись кінця запису
  PWR->CR &= ~PWR_CR_DBP;                                   // Заборонити доступ до Backup області
}
Параметри: нове значення лічильника. Нічого не повертає.

Отримувати і задавати значення RTC лічильника ми вже можемо, залишилось ще перетворити значення лічильника в дату і час, та перетворення з дати і часу в значення лічильника.
Тут є два варіанти. Скористатись Unix Time, початок відліку часу ведеться від 01.01.1970 року і 32-бітного лічильника вистачить аж до 19 січня 2038 року, 03:14:07 UTC. Або вести початок відліку з 01.01.2001 року, що значно відтермінує час до виникнення переповнення лічильника.
Далі покажу дві реалізації перетворення значення лічильника в дату і час та навпаки. Одна реалізація колодару буде рахувати секунди від 01.01.1970. Інша реалізація колодару початок відліку почне від 01.01.2001. Обидва колодари реалізуємо у вигляді окремих бібліотек.

Рахуємо від 1 січня 1970 року

Взяв тут цю готову реалізацію, але алгоритми перетворення загальновідомі, на основі них і реалізовані перетворення.

Файл "UnixTime.h" де оголошена структура зберігання дати і часу та оголошені прототипи функцій:
/***************************************************************************
 * Ця бібліотека призначена для роботи з RTC STM32F1xx за допомоги UnixTime*
 * дозволяє перетворювати з лічильника в колодар (день, місяць, рік)    *
 * і час (години, хвилини, секунди), і навпаки       *
 ***************************************************************************/
#ifndef __UNIXTIME_H
#define __UNIXTIME_H
 
 #define SECOND_A_DAY 86400

typedef struct
 {
   int year;
   char mon;
   char mday;
   char hour;
   char min;
   char sec;
   char wday;
 } unixColodar;

void counterToColodar (unsigned long counter, unixColodar * unixTime);
unsigned long colodarToCounter (unixColodar * unixTime);

#endif

Файл "UnixTime.c" де розташовані самі функції перетворення:
#include "UnixTime.h"

void counterToColodar (unsigned long counter, unixColodar * unixTime)
{
 unsigned long a;
 char b;
 char c;
 char d;
 unsigned long time;

 time = counter%SECOND_A_DAY;
 a = ((counter+43200)/(SECOND_A_DAY>>1)) + (2440587<<1) + 1;
 a>>=1;
 unixTime->wday = a%7;
 a+=32044;
 b=(4*a+3)/146097;
 a=a-(146097*b)/4;
 c=(4*a+3)/1461;
 a=a-(1461*c)/4;
 d=(5*a+2)/153;
 unixTime->mday=a-(153*d+2)/5+1;
 unixTime->mon=d+3-12*(d/10);
 unixTime->year=100*b+c-4800+(d/10);
 unixTime->hour=time/3600;
 unixTime->min=(time%3600)/60;
 unixTime->sec=(time%3600)%60;
}

unsigned long colodarToCounter (unixColodar * unixTime)
{
 char a;
 int y;
 char m;
 unsigned long Uday;
 unsigned long time;

 a=((14-unixTime->mon)/12);
 y=unixTime->year+4800-a;
 m=unixTime->mon+(12*a)-3;
 Uday=(unixTime->mday+((153*m+2)/5)+365*y+(y/4)-(y/100)+(y/400)-32045)-2440588;
 time=Uday*SECOND_A_DAY;
 time+=unixTime->sec+unixTime->min*60+unixTime->hour*3600;
 return time;
}
Функція counterToColodar, перетворює значення лічильника в дату і час. На вхід параметри: значення лічильника і вказівник на структуру дати і часу. Нічого не повертає.
Функція colodarToCounter, перетворює дату і час в значення лічильника. На вхід параметри:
вказівник на структуру дати і часу. Повертає значення лічильника.
Завантажити архів бібліотеки UnixTime.rar

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

Додайте файли "UnixTime.h" та "UnixTime.c" до свого проекту. На початку головного файлу "main.c" приєднайте бібліотеку:
#include "UnixTime.h"
Оголосіть змінну типу unixColodar:
unixColodar unixTime;
Та додамо до головної програми "main.c" функцію зручного встановлення часу і дати:
static uint32_t colodarSetting(int year, char month, char day, char hour, char min, char sec)
{
 unixTime.year=year;
 unixTime.mon=month;
 unixTime.mday=day;
 unixTime.hour=hour;
 unixTime.min=min;
 unixTime.sec=sec;
 return colodarToCounter(&unixTime);
}
Щоб встановити дату і час, потрібно викликати цю функцію з параметрами часу і дати, а поверне функція вже готове значення 32-бітного лічильника RTC. Отриманий результат можна внести до лічильника RTC викликавши функцію mRTCSetCounter.

Наприклад, щоб встановити дату 23 грудня 2016 року, а час 21 годину 15 хвилин 00 секунд, треба зробити так:
mRTCSetCounter(colodarSetting(2016,12,23,21,15,0));
Цей рядок можна додати до функції ініціалізації RTC mRTCInit і тоді лічильник часу встановиться початковими часом і датою тільки при першому запуску RTC.

Щоб отримати поточну дату і час треба викликати функцію counterToColodar:
counterToColodar(mRTCGetCounter(), &unixTime);
Параметри: значення лічильника RTC, які повертає функція mRTCGetCounter, та вказівник на структуру дати і часу, яку заповнить поточними датою і часом функція counterToColodar.
Структура даних дати і часу заповнені, тепер можна отриману дату і час показати на дисплей. Це вже залежить на який дисплей ви будете виводити інформацію. Наприклад, якщо на дисплей типу WH1602 з бібліотекою hd44780. Підключення і робота з бібліотекою в статті: "STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780", то вивід дати і часу на дисплей можуть виглядати так:
counterToColodar(mRTCGetCounter(), &unixTime);
lcdGoto(LCD_1st_LINE,0);
lcdNtos((uint32_t) unixTime.mday, 2);
lcdGoto(LCD_1st_LINE,3);
lcdNtos((uint32_t) unixTime.mon, 2);
lcdGoto(LCD_1st_LINE,6);
lcdNtos((uint32_t) unixTime.year, 4);
lcdGoto(LCD_2nd_LINE,0);
lcdNtos((uint32_t) unixTime.hour, 2);
lcdGoto(LCD_2nd_LINE,3);
lcdNtos((uint32_t) unixTime.min, 2);
lcdGoto(LCD_2nd_LINE,6);
lcdNtos((uint32_t) unixTime.sec, 2);

Рахуємо від 1 січня 2001 року

Це просто ще одна реалізація колодару, як приклад. Взято тут. В цьому варіанті ще додано віднімання днів, що минули з 1 січня 1970 року по 1 січня 2001 року.
Файл "fTimeJD.h" де оголошено константу, структуру і прототипи функцій.
#ifndef FTIMEJD_H_
#define FTIMEJD_H_

#include "stm32f1xx.h"

#define JD0 2451911 // днів до 01 січня 2001 ПН

typedef struct{
  uint16_t year;
  uint8_t month;
  uint8_t day;
  uint8_t hour;
  uint8_t minute;
  uint8_t second;
} ftime_t;

// функція перетворення григоріанської дати і часу в значення лічильника
uint32_t FtimeToCounter(ftime_t * ftime);
// функція перетворення значення лічильника в григоріанську дату і час
void CounterToFtime(uint32_t counter,ftime_t * ftime);

#endif /* FTIMEJD_H_ */

Файл "fTimeJD.c" з функціями бібліотеки.
#include "fTimeJD.h"

// функція перетворення григоріанської дати і часу в значення лічильника
uint32_t FtimeToCounter(ftime_t * ftime)
{
  uint8_t a;
  uint16_t y;
  uint8_t m;
  uint32_t JDN;

  // Обчислення необхідних коефіцієнтів
  a=(14-ftime->month)/12;
  y=ftime->year+4800-a;
  m=ftime->month+(12*a)-3;
  // Обчислюємо значення поточного Юліанського дня
  JDN=ftime->day;
  JDN+=(153*m+2)/5;
  JDN+=365*y;
  JDN+=y/4;
  JDN+=-y/100;
  JDN+=y/400;
  JDN+=-32045;
  JDN+=-JD0;    // приберемо дні які минули до 1 січня 2001 року
  JDN*=86400;        // перетворимо дні в секунди
  JDN+=(ftime->hour*3600);  // і додамо секунди поточного дня
  JDN+=(ftime->minute*60);
  JDN+=(ftime->second);
  // повертаємо кількість секунд з опівночі 1 січня 2001 року
  return JDN;
}

// функція перетворення значення лічильника в григоріанску дату і час
void CounterToFtime(uint32_t counter,ftime_t * ftime)
{
  uint32_t ace;
  uint8_t b;
  uint8_t d;
  uint8_t m;

  ace=(counter/86400)+32044+JD0;
  b=(4*ace+3)/146097;
  ace=ace-((146097*b)/4);
  d=(4*ace+3)/1461;
  ace=ace-((1461*d)/4);
  m=(5*ace+2)/153;
  ftime->day=ace-((153*m+2)/5)+1;
  ftime->month=m+3-(12*(m/10));
  ftime->year=100*b+d-4800+(m/10);
  ftime->hour=(counter/3600)%24;
  ftime->minute=(counter/60)%60;
  ftime->second=(counter%60);
}
Функція CounterToFtime, перетворює значення лічильника в дату і час. На вхід параметри: значення лічильника і вказівник на структуру дати і часу. Нічого не повертає.
Функція FtimeToCounter, перетворює дату і час в значення лічильника. На вхід параметри:
вказівник на структуру дати і часу. Повертає значення лічильника.
Завантажити архів бібліотеки fTimeJD.rar

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

Додайте файли "fTimeJD.h" та "fTimeJD.c" до свого проекту. На початку головного файлу "main.c" приєднайте бібліотеку:
#include "fTimeJD.h"
Оголосіть змінну типу ftime_t:
ftime_t jdTime;
Та додамо до головної програми "main.c" функцію зручного встановлення часу і дати:
static uint32_t colodarSetting(int year, char month, char day, char hour, char min, char sec)
{
  jdTime.year = year;
  jdTime.month = month;
  jdTime.day = day;
  jdTime.hour = hour;
  jdTime.minute = min;
  jdTime.second = sec;
  return FtimeToCounter(&jdTime);
}
Щоб встановити дату і час, потрібно викликати цю функцію з параметрами часу і дати, а поверне функція вже готове значення 32-бітного лічильника RTC. Отриманий результат можна внести до лічильника RTC викликавши функцію mRTCSetCounter.

Наприклад, щоб встановити дату 23 грудня 2016 року, а час 21 годину 15 хвилин 00 секунд, треба зробити так:
mRTCSetCounter(colodarSetting(2016,12,23,21,15,0));
Цей рядок можна додати до функції ініціалізації RTC mRTCInit і тоді лічильник часу встановиться початковими часом і датою тільки при першому запуску RTC.

Щоб отримати поточну дату і час треба викликати функцію CounterToFtime:
CounterToFtime(mRTCGetCounter(), &jdTime);
Параметри: значення лічильника RTC, які повертає функція mRTCGetCounter, та вказівник на структуру дати і часу, яку заповнить поточними датою і часом функція CounterToFtime.
Структура даних дати і часу заповнені, тепер можна отриману дату і час показати на дисплей. З дисплеєм і бібліотекою, як в попередньому прикладі, вивід дати і часу на дисплей можуть виглядати ще й так:
uint8_t aShowTime[20] = {0}; 

CounterToFtime(mRTCGetCounter(), &jdTime);

sprintf((char*)aShowTime,"%02d:%02d:%02d",jdTime.hour, jdTime.minute, jdTime.second);
lcdGoto(LCD_2nd_LINE,0);
lcdPuts((char*)aShowTime);

sprintf((char*)aShowTime,"%02d/%02d/%02d",jdTime.day,jdTime.month,jdTime.year);
lcdGoto(LCD_1st_LINE,0);
lcdPuts((char*)aShowTime); 

Деякі зауваження

  • Перший запуск годинника реального часу може тривати досить довго, аж до 1 секунди. Подальші увімкнення, як годинник запущений, будуть швидкими.
  • Хоч і в функціях перетворення часу і дати в секунди та навпаки, багато операцій ділення і множення, та на STM32 ці операції хардварні і виконуються дуже швидко. Перетворення з лічильника секунд в дату і час триває менш ніж одна мілісекунда.
  • Інформацію про будильник не надавав. Поки не було необхідності. Можливо згодом додам.
  • плата розробника mini board STM32F103C8T6 продається з роз'ємами які не впаяні до плати. Треба їх самостійно впаяти. Будьте обережні  і добре промийте спиртом плату після паяння. Часто-густо по причині забрудненості плати годинник реального часу RTC просто не запускається, або плин секунд мікроконтролера не відповідає плину секунд в реальності. Похибка може бути дуже значною. Можливо буде потреба пропаяти годинникові кварц і конденсатори. Або навіть підібрати конденсатори що в обв'язці годинникового кварцу.
  • щодо вживання слова Колодар замість Календар. Слово календар походить від латинського calendarium - боргова книжка. Я з вами не боргову книжку налаштовував і запускав, а відлік часу. Коло-дар  - це коло, яке дарує нам наше Сонце і по ньому ведеться відлік часу. Так буде правильно.

середу, 14 грудня 2016 р.

STM32: Кирилічні символи на WH1602, HD4470


Передмова

В статті "Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780" ми розглянули як під'єднати до мікроконтролера дисплей WH1602 з контролером HD44780 та під'єднати бібліотеку hd44780 для роботи з цим дисплеєм. Також мається в статті і демопрограма, яка показує наочно можливості бібліотеки.
Все це чудово, але в своїх проектах хочеться виводити інформацію (пункти меню, параметри) все ж таки соєю мовою, а не англійською. Бо з латинськими символами в WH1602 немає жодних проблем, а от з кирилічними символами трапився пекельний облом. Або їх, кирилічних символів, в таблиці символів дисплею взагалі немає. Або є, але розташовані в тій таблиці без прив'язки до якогось стандарту кодування.

Як викручуватись?

Варіант з відсутньою кирилицею відкидаємо, бо потрібно "колгоспити" з символами користувача, які потрібно попередньо завантажувати в пам'ять дисплею і їх використовувати. Але це дуже марнотратно. Вже краще англійською. А от варіант з наявними символами кирилиці, хоч і не по стандарту, вже таки можна більш-менш комфортно використовувати.
Таблиця символів WH1602
Коли є в таблиці кирилиця, то є два варіанти. Один - це в програмному коді створити два масиви з таблицею win-1251 і відповідні коди символів дисплею WH1602 в інший масив. І на льоту це все перетворювати в людський вигляд.
Інший варіант - це скористатись зовнішньою програмою для конвертування тексту до сумісного з дисплеем.
Розглянемо варіант з зовнішньою програмою, бо конвертувати силами самого мікроконтролера займе як і пам'ять, так і ресурси по обчисленню. Хоча при сучасних параметрах мікроконтролерів цим можна знехтувати. Як на мене, варіант з зовнішньою утилітою по конвертуванню, набагато зручніше.

Конвертер символів знакосинтезуючих індикаторів

Ось таку назву має стаття де я знайшов цю корисну утиліту. Завантажуємо архів з програмою. Після розпакування запускаємо і маємо форму з трьома полями.
конвертор символів
В верхньому полі, що ліворуч, пишемо текст який хочемо бачити на дисплеї, а в верхньому, що праворуч, отримуємо те що ми маємо вставити в код своєї програми. В полі, що внизу, отримуємо код символів тексту, який можна використовувати для масивів в своїй програмі. 
На жаль китайські інженери не потурбувались про такі літери як "Іі, Єє, Її, Ґґ", та все одно це вже щось чим нічого. Можна вже якось "викрутитись" за допомоги користувацьких символів, що завантажуються до пам'яті дисплею.

STM32: Як зняти захист на читання пам'яті мікроконтролера

Передмова

Часто-густо використовую мікроконтролерну міні платку з чипом STM32F103C8T6. Дуже зручно використовувати в якихось поштучних проектах-замовленнях. Замовляю на EBAY чи ALIEXPRESS. Платка приходить вже з прошитою простою програмою мікроконтролерного "Hello World" - блимання набортним світлодіодом. Підключаєш живлення до плати і блимає світлодіод - плата в робочому стані. І заливаєш потім свою прошивку.
Mini Board STM32F103CT6
Але одного разу залити прошивку в нещодавно придбані плати не вдалось. Хоча світлодіод блимав, як зазвичай. Прошивач обурювався помилкою - не можу прочитати флеш! "No read flash". Спробував інший прошивач - те саме. Запідозрив що щось не те і згадав, що на мікроконтролерах можна захищати свої прошивки від читання (авторскі права і все таке). Раз можна встановити захист, значить можна і зняти. В сенсі не прочитати код прошивки з метою крадіжки інтелектуальної праці, а з метою залити свою програму до мікроконтролера. Звісно, що при знятті захисту, попередня прошивка затирається.

Знімаємо захист на читання

Запускаємо ST-LINK Utility, завантажити можна на сайті виробника - ST-LINK Utility. Тицяємо мишею по меню "Target -> Option Bytes", або тиснемо зразу "CTRL+B". І в пункті "Read Out Protection" обираємо "Disable".
Protection Disable
Потім тиснемо кнопку "Apply" - пам'ять мікроконтролера очиститься від захищеної прошивки і ви зможете заливати свою. Якщо буде потреба захистити свою прошивку від посягань, то в пункті "Read Out Protection" обрати - "Enable".

суботу, 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