четвер, 24 травня 2018 р.

STM32: Бібліотека для зручного надсилання інформації налагодження в порт UART

Передмова

В проектах на мікроконтролерах часто-густо потрібно десь виводити інформацію для налагодження (debug). Найпростіше і зручніше це зробити в порт UART. Тоді, коли в цьому є потреба, в певних ділянках коду, вставляємо надсилання потрібних повідомлень в порт UART, а на ПК в будь-якій термінальній програмі читаємо повідомлення, що надсилає нам мікроконтролер. Це можуть бути повідомлення про помилку, або данні телеметрії з сенсорів і датчиків, або якась службова інформація, тощо. 
Для зручного надсилання в порт UART текстової, числової інформації зручно використовувати бібліотеку "uart". За її допомоги можна надсилати як поодинокі символи, так і числа типу INT, або числа представлені в шістнадцятковому форматі HEX8, HEX16, HEX32, або ж цілий текстовий рядок, або передати цілий буфер даних. 
Всі функції бібліотеки вгледів тут. Функцію ініціалізації порту UART не перейняв, тому що я всі налаштування периферії виконую в програмі CubeMX for STM32.

CubeMX

  • Створіть новий проект, або відкрийте проект з яким вже працюєте і вам потрібна інформація налагодження в COM PORT
  • Увімкніть потрібний порт UART
  • Налаштуйте по необхідності цей порт. За замовчуванням швидкість порту 115200
  • Згенеруйте або оновіть проект, з увімкненим UART

Бібліотека UART

Скопіюйте файли бібліотеки до вашого проекту. Бібліотека містить два файли:
Файл uart.h до теки inc проекту, а файл uart.c до теки src вашого проекту.
В файлі uart.h пропишіть ім'я структури порту UART, що надав CubeMX. Наприклад, huart1, huart2, тощо. Та файл HAL драйверу відповідно до вашого мікроконтролера, наприклад, stm32f1xx_hal.h якщо використовуєте stm32f1 серії: 
#include "stm32f1xx_hal.h"
#define UART_PORT  huart1
Все інше залиште без змін.

Перейдемо до файлу main.c проекту, та підключимо бібліотеку UART:
/* USER CODE BEGIN Includes */
#include "uart.h"
/* USER CODE END Includes */

Ну і щось надрукуємо для прикладу. Рядок тексту, оголосимо змінну INT та надрукуємо її як в десятковому так і шістнадцятковому представленні:
/* USER CODE BEGIN 2 */
UART_SendStr("UART is OK!\n\r");
  int var = 12345;
  UART_SendInt(var);
  UART_SendStr("\n\r");
  UART_SendHex16(var);
  UART_SendStr("\n\r");
/* USER CODE END 2 */

Компілюємо, заливаємо і в термінальній програмі споглядаємо на результат. Звісно що порт UART мікроконтролеру має бути з'єднаний з ПК за допомоги UART to USB TTL конвертеру. Або якщо, наприклад, у вас плата розробника STM32F103RB Nucleo, то достатньо використовувати USART2 на мікроконтролері, данні будуть надсилатись в USB порт програматора ST-LINK що на борту плати. І додаткових дротів і конверторів не потрібно.

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

неділя, 20 травня 2018 р.

STM32: Бібліотека для роботи з EEPROM типу AT24XXX по шині I2C

update 01.06.2018: шановний ReAl з форуму replace допоміг зі створенням вже оновленої версії цієї бібліотеки. Обговорення тут. Йому велика дяка, всім іншим - користуйтесь на здоров'я.

Передмова

В проектах, які вже "виросли" за межі просто поблимати світлодіодами, іноді виникає потреба десь зберігати дані в енергонезалежній пам'яті. Це можуть бути як і дані налаштувань, так і дані телеметрії, тощо. Варіантів безліч. Звісно, можна замість зовнішньої EEPROM використати внутрішню FLASH пам'ять самого мікроконтролеру і там зберігати якісь дані. Але ця стаття саме про зовнішню енергонезалежну EEPROM типу AT24Cxx, як її під'єднати до мікроконтролеру, як записати і як прочитати дані.

Схема підключення

Мікросхеми типу AT24Cxx працюють по шині I2C і підключаються до мікроконтролеру як і будь-який інший пристрій з шиною I2C:
Схема підключення EEPROM до мікроконтролера
Схема типова для всіх мікросхем цієї серії. В схемі що на малюнку EEPROM має адресу 0x50 (A0, A1, A2 - GND), та є дозвіл на запис (HOLD - GND). Не забуваймо про підтяжку до плюс живлення (логічної 1) ліній SDA і SCL шини I2C.

Корисно ознайомитись з документацією на свій чип EEPROM в мене це AT24C512. А також по цій темі є дуже хороша стаття: "Робота з EEPROM пам'яттю 24CXX -- огляд". Зі схемами і подробицями роботи I2C. 

Бібліотека AT24Cxx

Бібліотеку взяв у Ben Brown допис STM32 I2C EEPROM with HAL. Переробив на мову C замість C++, додав одну функцію для визначення чи під'єднаний чип EEPROM, чи ні.
Бібліотека має два файли:
У файлі AT24Cxx.h під'єднуємо бібліотеку HAL для свого чипу, прописуємо порт I2C до якого під'єднаний EEPROM, адресу EEPROM на шині I2C та розмір сторінки пам'яті EEPROM.
Наприклад, EEPROM AT24C512 (512 сторінок по 128 байт) під'єднаний до STM32F103C8T6 до першого I2C1 з адресою 0x50 (A0, A1, A2 - GND), то значення пишемо такі:
#include "stm32f1xx_hal.h"
#define EEPROM_I2C  hi2c1
#define EEPROM_ADDRESS  0x50
#define EEPROM_PAGESIZE 128

Бібліотека має всього три функції:
HAL_StatusTypeDef AT24Cxx_IsConnected(void);
HAL_StatusTypeDef AT24Cxx_ReadEEPROM(unsigned address, const void* src, unsigned len);
HAL_StatusTypeDef AT24Cxx_WriteEEPROM(unsigned address, const void* src, unsigned len);

Перша, це просто перевірка чи є відгук за адресою EEPROM на шині I2C, чи немає. Повертає успіх чи невдачу з переліку HAL_StatusTypeDef.


Друга, читання з EEPROM в RAM мікроконтролера. Приймає початкову адресу в межах адресного простору вашого чипу EEPROM звідки прочитуються збережені дані, вказівник на змінну куди прочитуються дані, розмір даних які читаються з EEPROM. Повертає успіх чи невдачу з переліку HAL_StatusTypeDef.

Третя, запис до EEPROM. Приймає початкову адресу в межах адресного простору вашого чипу EEPROM куди будуть зберігатись дані, вказівник на дані які потрібно зберегти, розмір даних для збереження. Повертає успіх чи невдачу з переліку HAL_StatusTypeDef.

Приклад роботи

Для відладки і виводу різної інформації дуже зручно використовувати порт UART мікроконтролеру який під'єднано до ПК через UART2USB перехідник. Скористаємось такою нагодою і в CubeMX крім шини I2C1 увімкнемо порт UART1. Та приєднаємо бібліотеку UART до проекту.

CubeMX

Запускаємо CubeMX, створюємо новий проект, обираємо свій мікроконтролер, та вмикаємо потрібну переферію:
Налаштування в CubeMX
Робимо як на світлині позначено червоним:
  1. Вмикаємо шину I2C1
  2. Вмикаємо тактування від зовнішнього кварцевого резонатору
  3. Вмикаємо налогодження по SerialWire
  4. Вмикаємо порт UART1
Налаштування всі за замовчуванням нічого не міняв. Зберігаємо проект, генеруємо код.

Демо-код

Відкриваємо чи експортуємо проект в своєму засобі розробки. Копіюємо в теку inc проекту файл AT24Cxx.h, а в теку src проекту копіюємо файл AT24Cxx.c. І в визначені CubeMX для користувача ділянки - додаємо такий код:

Під'єднуємо бібліотеки до проекту.
/* USER CODE BEGIN Includes */
#include "AT24Cxx.h"
#include "uart.h"
/* USER CODE END Includes */

Оголосимо структуру для прикладу:
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
typedef struct{
 int intVar;
 char str[256];
 uint8_t flag;
 uint16_t save;
}ExampleTypeDef;
/* USER CODE END PV */

Та зміну цієї структури:
/* USER CODE BEGIN 0 */
ExampleTypeDef example;
/* USER CODE END 0 */

І сам демо-код.
 /* USER CODE BEGIN 2 */

  UART_SendStr("UART is OK!\n\r");

  if(AT24Cxx_IsConnected() == HAL_OK)
  {
   UART_SendStr("EEPROM is OK!\n\r");
   UART_SendStr("Size data = ");
   UART_SendInt(sizeof(example));
   UART_SendStr("\n\r");

   if(AT24Cxx_ReadEEPROM(396, &example, sizeof(example)) == HAL_OK)
   {
    UART_SendStr("EEPROM read is OK!\n\r");

    if(example.save != 0xABCD)
    {
     example.flag = 1;
       example.intVar = 12345678;
       example.save = 0xABCD;
       strcpy(example.str, "Testing for save any data structure. Thank you, ReAl!!! The link http://replace.org.ua/topic/9430/");

       if(AT24Cxx_WriteEEPROM(396, &example, sizeof(example)) == HAL_OK)
     {
      UART_SendStr("EEPROM save is OK!\n\r");
     }
     else
     {
      UART_SendStr("EEPROM save is failed!\n\r");
     }
    }

    UART_SendInt(example.intVar);
    UART_SendStr("\n\r");
    UART_SendStr("0x");
    UART_SendHex16(example.save);
    UART_SendStr("\n\r");
    UART_SendStr(example.str);
    UART_SendStr("\n\r");
   }
   else
   {
    UART_SendStr("EEPROM read is failed!\n\r");
   }
  }
  else
  {
   UART_SendStr("EEPROM is not connected!\n\r");
  }
  /* USER CODE END 2 */
Тут в зміну example читаємо дані з EEPROM і якщо ще не було запису до EEPROM, то записуємо певну інформацію. Компілюємо, заливаємо до мікроконтролеру і в терміналі ПК спостерігаємо запис/читання даних.
Тепер можна зняти живлення з мікроконтролеру і знову подати його. В термінал надрукуються ті самі дані, що були збережені до EEPROM першого разу.

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

Завантажити архів з бібліотекою AT24Cxx.rar

четвер, 10 травня 2018 р.

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

Передмова

Це продовження статті "STM32: Годинник реального часу на stm32f1xx. Проблеми, рішення (частина перша)". В першій частині описано проблеми RTC на STM32F1xx серії та невдалі спроби подолати їх за допомоги HAL драйверу. Проблема полягає в тому, що при використанні HAL_RTC - лічильник RTC, який рахує секунди, функціями HAL драйверу, "обрізається" в межах доби. Максимально 86400 секунд. А дата зберігається окремо просто в RAM пам'яті мікроконтролера. Тому при вимкнені мікроконтролеру чи перехід в сон, втрачається дата RTC. Можна записати дату в backup регістри, але це не врятує, якщо мікроконтролер буде без живлення при переході на нову дату. Тому на серії STM32F1xx єдиний вихід правильної роботи RTC це враховувати в лічильнику RTC секунди, як часу так і дати - разом, а за допомоги розрахунків видобувати вже поточну час і дату. Ну і якщо вже використовуємо HAL драйвера та не створювати дублікат бібліотеки HAL_RTC, я просто створив невеличку бібліотеку зі зміненими деякими функціями:
HAL_StatusTypeDef mRTC_GetTime(RTC_HandleTypeDef* hrtc, RTC_TimeTypeDef* sTime, uint32_t Format);
HAL_StatusTypeDef  mRTC_GetDate(RTC_HandleTypeDef* hrtc, RTC_DateTypeDef* sDate, uint32_t Format);
HAL_StatusTypeDef  mRTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
HAL_StatusTypeDef  mRTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
HAL_StatusTypeDef mRTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format);
HAL_StatusTypeDef mRTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format);
Це функції встановлення/взяття часу, та встановелння/взяття дати, а також встановлення будильнику.

Щоб коректно задавати час, та брати час слід використовувати саме ці функції, а не аналогічні функції, які надаються HAL драйверами.

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

Всі налаштування RTC в CubeMX залишаються без змін і не мають ніяких особливостей. Як налаштувати роботу RTC можна переглянути в попередній статті, все абсолютно так само в одноіменному розділі "Налаштування і запуск RTC в CubeMX"
Та не забудьте увімкнути переривання від RTC раз на секунду:
Увімкнути переривання від RTC раз на секунду
Це потрібно для демонстрації роботи RTC.

Додаткова бібліотека RTC з модифікованими функціями

Додаткова бібліотека mRTC з модіфікованими функціями містить два файли:
В цій бібліотеці є змінені функції аналогічні функціям HAL драйверу, так і додаткові для зручності використання.

Список всіх функцій додаткової бібліотеки mRTC:
#define TIME_COUNTER(hrtc) mRTC_ReadCounter(hrtc) // повертає значення лічильнику часу
#define ALARM_COUNTER(hrtc) mRTC_ReadAlarmCounter(hrtc) // повертає значення лічильнику будильника
#define SECOND(hrtc, format) mRTC_GetSecond(hrtc, format) // повертає секунди
#define MINUTE(hrtc, format) mRTC_GetMinute(hrtc, format) // повертає хвилини
#define HOUR(hrtc, format) mRTC_GetHour(hrtc, format) // повертає години
#define WEEKDAY(hrtc) mRTC_GetWeekDay(hrtc) // повертає день тижня
#define DAY(hrtc, format) mRTC_GetDay(hrtc, format) // повертає день
#define MONTH(hrtc, format) mRTC_GetMonth(hrtc, format) // повертає місяць
#define YEAR(hrtc, format) mRTC_GetYear(hrtc, format) // повертає рік

void mRTC_Begin(RTC_HandleTypeDef* hrtc); // оновлює дату в буфері, викликати раз після увімкнення MCU
uint8_t mRTC_GetSecond(RTC_HandleTypeDef* hrtc, uint32_t Format); // повертає секунди
uint8_t mRTC_GetMinute(RTC_HandleTypeDef* hrtc, uint32_t Format); // повертає хвилини
uint8_t mRTC_GetHour(RTC_HandleTypeDef* hrtc, uint32_t Format); // повертає години
uint8_t mRTC_GetWeekDay(RTC_HandleTypeDef* hrtc); // повертає день тижня
uint8_t mRTC_GetDay(RTC_HandleTypeDef* hrtc, uint32_t Format); // повертає день
uint8_t mRTC_GetMonth(RTC_HandleTypeDef* hrtc, uint32_t Format); // повертає місяць
uint16_t mRTC_GetYear(RTC_HandleTypeDef* hrtc, uint32_t Format); // повертає рік

HAL_StatusTypeDef mRTC_GetTime(RTC_HandleTypeDef* hrtc, RTC_TimeTypeDef* sTime, uint32_t Format); // повертає час в структуру часу
HAL_StatusTypeDef mRTC_GetDate(RTC_HandleTypeDef* hrtc, RTC_DateTypeDef* sDate, uint32_t Format); // повертає дату в структуру дати
HAL_StatusTypeDef mRTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format); // задає час з структури часу
HAL_StatusTypeDef mRTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format); // задає дату з структури дати

HAL_StatusTypeDef mRTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format); // задає час будильника в межах доби
HAL_StatusTypeDef mRTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format); // задає час будильника в межах доби з перериваннями
На початку програми, після увімкнення MCU потрібно один раз викликати функцію mRTC_Begin для оновлення дати в буфері. Щоб не вираховувати кожного разу день тижня, день, місяць і рік у функціях: mRTC_GetWeekDay, mRTC_GetDay, mRTC_GetMonth, mRTC_GetYear, та mRTC_GetDate - значення беруться просто з буферу. Кожного разу коли викликається функція mRTC_GetTime перевіряється чи не настав новий день? І якщо настав, то автоматично оновлюється дата в буфері. Якщо користуватись тільки функціями mRTC_GetTime та mRTC_GetDate про зміну дня не потрібно турбуватись. Якщо функція mRTC_GetTime викликається не регулярно і може настати момент переходу дня фактично, а функція mRTC_GetTime не викликалась, то перед викликом функції mRTC_GetDate потрібно викликати функцію mRTC_Begin щоб оновити дату в буфері. Також, якщо користуватись функціями взяття окремо годин (mRTC_GetHour), хвилин (mRTC_GetMinute), секунд (mRTC_GetSecond), дня (mRTC_GetDay), місяця (mRTC_GetMonth), року (mRTC_GetYear) і дня тижня (mRTC_GetWeekDay), то теж потрібно самому контролювати вчасне оновлення дати в буфері викликом функції mRTC_Begin.

Демонстраційна програма

  • За допомоги CubeMX створіть проект з налаштуваннями RTC, як зазначено вище у розділі "Налаштування і запуск RTC в CubeMX". Дайте йому ім'я, та згенеруйте код ініціалізації для свого засобу розробки.
  • Відкрийте чи імпортуйте проект створений програмою CubeMX у своєму засобі розробки. В мене це Atolic TrueStudio, професійний засіб розробки абсолютно безкоштовний.
  • Додайте файли бібліотеки до свого проекту. Файли з розширенням ".h" до теки "inc", а файли з розшиернням ".c" до теки "src"
  • Відрийте файл "main.c" і в зазначені для користувача ділянки коду додаємо демонстраційний код.
Приєднуємо до програми бібліотеку "mRTC":
/* USER CODE BEGIN Includes */
#include "m_rtc.h"
/* USER CODE END Includes */

Оголошуємо глобальні змінні та структури часу і дати:
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
uint8_t aShowTime[12] = {0};
uint8_t aShowDate[12] = {0};

RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
/* USER CODE END PV */

Оголошуємо прототипи функцій показу часу/дати та встановлення часу/дати:

/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void Show_RTC_Calendar(void);
void setDataTime(void);
/* USER CODE END PFP */

Викликаємо функції: увімкнути переривання RTC кожної секунди, оновити в буфері значення дати, показати дату і час.
  /* USER CODE BEGIN 2 */
  HAL_RTCEx_SetSecond_IT(&hrtc);
  // setDataTime(); // якщо потрібно встановити початковий час і дату, то розкоментуйте рядок
  mRTC_Begin(&hrtc);
  Show_RTC_Calendar();
  /* USER CODE END 2 */

Та не забуваємо написати самі функції в користувацькому блоці 4:
/* USER CODE BEGIN 4 */
void HAL_RTCEx_RTCEventCallback (RTC_HandleTypeDef* hrtc)
{
 Show_RTC_Calendar();
}

void Show_RTC_Calendar(void)
{
 /* Note: need to convert in decimal value in using __LL_RTC_CONVERT_BCD2BIN helper macro */
 /* Display time Format : hh:mm:ss */
 mRTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
 sprintf((char*)aShowTime,"%.2d:%.2d:%.2d", sTime.Hours, sTime.Minutes, sTime.Seconds);

 /* Display date Format : mm-dd-yy */
 mRTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
 sprintf((char*)aShowDate,"%.2d-%.2d-%.2d-%.2d", sDate.Date, sDate.Month, sDate.Year, sDate.WeekDay);
}

void setDataTime(void)
{
 sTime.Hours = 21;
 sTime.Minutes = 37;
   sTime.Seconds = 00;

   if(mRTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
   {
  Error_Handler();
   }

   sDate.Date = 9;
   sDate.Month = 5;
   sDate.Year = 18;

   if(mRTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
   {
    Error_Handler();
   }
}
/* USER CODE END 4 */

В такому варіанті кожної секунди буде викликатись функція Show_RTC_Calendar і в масивах aShowTime та aShowDate будуть знаходитись текстові рядки з актуальним часом і датою. А вже цей текст з датою і часом виводьте - чи то на дисплей, чи то в послідовний порт. На ваш розсуд.

RTC Alarm

Також можна встановити будильник за допомоги функцій mRTC_SetAlarm або mRTC_SetAlarm_IT значення лічильника RTC_Alarm встановлюється в межах доби, якщо поточний час більший за значення часу будильника і якщо значення будильника менше значення поточного часу, то будильник встановиться на наступну добу.

Значення лічильника RTC_Alarm встановлюється коректним і по досягненю лічильника RTC значення RTC_Alarm будильника скидаєтсья. Але чомусь самого переривання по Alarm не відбувається. Хоча все робив як в прикладі, що йде з HAL драйверами. Запустити будильник мені так і не вдалось: як з рідними HAL драйвером RTC, як за допомоги CMSIS,  так не виходило і за допомоги STD_Periph драйверів, ну і звісно і зараз не вийшло.
Можливо я просто не розумію як працює цей alarm і не правильно користуюсь ним. Як хто має позитивний досвід використання RTC_Alarm на STM32F1xx серії - поділіться цим зі мною.

RTC LL

Зараз в додачу до HAL драйверів зя'вились LL драйвер (сучасний нащадок Std_Periph). Пробував запустити RTC на STM32F1xx серії за допомоги LL драйверу. То також повне фіаско. В лічильнику RTC додається одна секунда аж раз на 30 реальних секунд. В кого є позитивний досвід використання RTC за допомоги LL драйверу, теж, будь ласка, відгукніться.

Бібліотека mRTC

четвер, 26 квітня 2018 р.

STM32: Бібліотека для зручної роботи з PCF857x по I2C

Передмова

Для розширення I/O портів існують різні мікросхеми для різних шин/протоколів. Наприклад мікросхема SN74HC595N з керуванням по SPI, про яку вже була в мене стаття. Або мікросхема PCF8574 з керуваннням по I2C, про яку і буде ця стаття. Існують і інші I/O Expander як 8 бітні, так і 16 бітні. Заглиблюватись в протоколи не будемо, в мережі достатньо якісної літератури по цій темі. Зосередимось на самому розширювачі портів вводу виводу. Писати и читати стан ніжок у PCF8574 будемо по шині I2C за допомоги HAL Driver. І щоб зручно було керувати розширювачем портів PCF8574 створив бібліотеку з необхідним набором функцій. За основу бібліотеки взяв готову бібліотеку PCF8574_ESP для ESP8266 та переніс її на нашу платформу STM32, а також переробив з мови C++ на Ci. В цій статті ознайомимось з роботою I/O Expander PCF8574 в режимі: 4 біти читаємо - 4 біти пишемо. Тобто, під'єднаємо до PCF8574 чотири кнопки і чотири світлодіоди. За допомоги кнопок будемо керувати станом світлодіодів.

Що потрібно

Схема

Зберіть макет за такою схемою та під'єднайте до шини I2C мікроконтролера. Резистори R7 - R10 можна не встановлювати. 
Схема підключення PCF8574 4-входи 4-виходи
Або як вам зручнише, зберіть макет по цьому малюнку
Макет Expander I/O PCF857

CubeMX for STM32

Запускаємо CubeMX, створюємо новий проект, обираємо свій чип STM32. У вкладці "Pinout" робимо такі налаштування шпильок:
  • RCC - Crystal/Ceramic resonator
  • SYS - Serial Wire
  • обираємо порт I2C до якого буде підключено дисплей. Я обрав I2C1
  • обираємо якийсь pin де будемо "ловити" подію по перериваннях від сигналу INT мікросхеми PCF8574. Я обрав PB5, як GPIO_EXTI5 і дав йому мітку PCF8574_INT
Налаштування периферії
Перейдемо до вкладки "Clock Configuration" та налаштуємо тактування мікроконтролеру як зазначено червоним на малюнку:
Наалаштування тактування
У вкладці "Configuration" можна налаштувати порт I2C. На чипові STM32F1xx все працює за замовчуванням на частоті 400кГц. Можна залишити як є. Налаштуємо наш PB5, щоб "ловити" переривання:
  • GPIO mode - external interrput mode with falling edge trigger detection (реагуємо на фронт що спадає)
  • GPIO Pull-Up/Pull-Down - Pull-up (з підтяжкою до живлення, логічної 1)
  • USER Label - PCF8574_INT
Налаштування переривань на PB5
Та в розділі NVIC увімкнемо переривання від PB5:
Увімкнемо переривання
Можна генерувати код програмою CubeMXforSTM32 для совго засобу розробки. В мене це Atolic TrueStudio, професійний засіб розробки абсолютно безкоштовний.

Підключення бібліотеки та демокод

Бібліотека складається з таких файлів:
  • pcf857x.h - початкові налаштування, визначення, прототипи
  • pcf857x.c - функції бібліотеки
Ці файли потрібно вкласти до вашого проекту. Файли з розширенням ".h" до теки "inc", а файли з розшиернням ".c" до теки "src". Або всю теку з бібліотекою, але на деяких засобах розробки потрібно в налаштуваннях проекту прописати шлях до теки з бібліотекою.
Відкриємо в зазобі розробки файл pcf857x.h і налаштуємо бібліотеку для правильної роботи з розширювачем портів:
  • #define STM32F1XX - вказати серію свого чипу
  • #define STM32_I2C_PORT hi2c1 - вказати порт I2C до якого під'єднано PCF8574 
  • #define PCF857x_ADDRESS 0x38 - вказати адресу PCF8574 на шині I2C (відповідно до схеми це буде адреса 0x38
Тепер напишемо демо програмку, щоб оцінити роботу розширювача портів. Відкриваємо файл "main.c" в засобі розробки і в визначених програмою CubeMX місцях для користувача, напишемо такий код:
  • Вкладемо заголовний файл з бібліотекою до нашого коду
/* USER CODE BEGIN Includes */
#include "pcf857x.h"
/* USER CODE END Includes */
  • Напишемо код для демонстрації. Запалимо всі вогники, через пару секунд загасимо всі вогники, зсунемо вогники ліворуч і праворуч та покрутимо вогник праворуч і ліворуч
  /* USER CODE BEGIN 2 */
  if(pcf857x_Init(0xFF, false) != PCF857x_OK)
  {
   while(1);
  }

  pcf857x_ResetInterruptPin();
  
  pcf857x_ToggleAll();
  HAL_Delay(2000);
  pcf857x_ToggleAll();
  HAL_Delay(2000);

  for (int i = 0; i < 4; ++i)
  {
   pcf857x_ShiftLeft(1);
   HAL_Delay(200);
  }

  for (int i = 0; i < 4; ++i)
  {
    pcf857x_ShiftRight(1);
    HAL_Delay(200);
  }

  pcf857x_Write(0, 0);
  HAL_Delay(200);
  for (int i = 0; i < 3; ++i)
  {
  pcf857x_RotateLeft(1);
  HAL_Delay(200);
  }

  for (int i = 0; i < 3; ++i)
  {
  pcf857x_RotateRight(1);
  HAL_Delay(200);
  }

  HAL_Delay(200);
  pcf857x_Write8(0xFF);  
  /* USER CODE END 2 */

Але це не все. В нашій схемі є ще кнопки. І можна відслідковувати стан натискання кожної кнопки і якимсь чином реагувати на це. Можна просто в лоб, в безкінечному циклі читати стан ніжок на expander PCF8574. Але це не розсудливо. Ми постійно читаємо шину I2C, а потім визначаємо було натискання кнопки чи ні, незалежно від того, чи дійсно кнопку натискали, чи нічого взагалі не відбувається. По суті даремно витрачаємо ресурси мікроконтролера і без будь якої користі ганяємо дані по шині I2C.

То що ж робити? На самому початку в програмі CubeMX ми задіяли ще одину шпильку від розширювача з назвою "INT". Шпилька "INT" виставляє логічний нуль, до наступного читання, коли відбулись якісь зміни на шпильках P0 - P7 розширювача портів PCF8574. А "ловити" цей "0" будемо на PB5 мікроконтролеру, який ми налаштували в режим переривань, підтягнули до "1", та "ловитимемо" саме фронт що спадає.

Весь потрібний початковий код з налаштувань периферії зробив за нас CubeMX, то нам залишається тільки написати обробник переривань. Для зручності в драйверах HAL є функція зворотнього виклику "HAL_GPIO_EXTI_Callback". Ця функція викликається кожного переривання і на вхід цій функції дається на якій шпильці відбулась подія.
  • Додамо до коду обробник переривань
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void HAL_GPIO_EXTI_Callback (uint16_t GPIO_Pin)
{
 if(GPIO_Pin == PCF8574_INT_Pin)
 {
  for (int i = 0; i < 4; ++i)
  {
   if(!pcf857x_Read(i + 4))
   {
    pcf857x_Toggle(i);
   }
  }
 }
}
/* USER CODE END PFP */
  • Безкінечний цикл залишається "пустим", а вся "магія" по перемиканню стану вогників працює за допомоги апаратного переривання, та невеличкого коду в обробнику переривань.
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
  • Тепер можна компілювати проект, заливати до мікроконтролера та милуватись роботою вогників і реакцією на натискання кнопок

Список функцій бібліотеки


PCF857x_TypeDef pcf857x_Init(uint16_t value_init, bool is8575); // Початкова ініціалізація pcf857x. На вхід число, початковий стан P0 - P7 та false якщо pcf8574 або true якщо pcf8575
uint8_t pcf857x_Read8(void); // Читання 8 бітів з P0 - P7
uint16_t pcf857x_Read16(void); // Читання 16 бітів з P0 - P15
bool pcf857x_Read(uint8_t pin); // Читання шпильки pin на pcf857x

PCF857x_TypeDef pcf857x_Write8(uint8_t value); // Запис 8 бітів до P0 - P7. Повертає код помилки.
PCF857x_TypeDef pcf857x_Write16(uint16_t value); // Запис 16 бітів до P0 - P15. Повертає код помилки.
PCF857x_TypeDef pcf857x_Write(uint8_t pin, bool value); // Запис значення до шпильки pin. Повертає код помилки.

PCF857x_TypeDef pcf857x_Toggle(uint8_t pin); // Поміняти стан шпильки pin. Повертає код помилки.
PCF857x_TypeDef pcf857x_ToggleAll(void); // Поміняти стан всіх шпильок. Повертає код помилки.
PCF857x_TypeDef pcf857x_ShiftRight(uint8_t n); // Зсув праворуч на n бітів. Повертає код помилки.
PCF857x_TypeDef pcf857x_ShiftLeft(uint8_t n); // Зсув ліворуч на n бітів. Повертає код помилки.
PCF857x_TypeDef pcf857x_RotateRight(uint8_t n); // Обертання праворуч на n бітів. Повертає код помилки.
PCF857x_TypeDef pcf857x_RotateLeft(uint8_t n); // Обертання ліворуч на n бітів. Повертає код помилки.
PCF857x_TypeDef pcf857x_ResetInterruptPin(void); // Скидання шпильки INT в початковий стан. Повертає код помилки.
PCF857x_TypeDef pcf857x_GetLastError(void); // Повертає останній код помилки.

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

Бібліотека для зручної роботи з розширювачем портів PCF857X

субота, 14 квітня 2018 р.

STM32: Бібліотека OLED Display SSD1306 по I2C


Передмова

OLED Display 0.96'' SSD1306
Один із поширених дисплеїв для поробок на мікроконтролерах є OLED Display 0.96'' з контролером SSD1306. Невеличкий розмір, проста схема підключення, великий контраст зображення, доступна ціна, роблять цей дисплей бажаним в своїх невеличких вбудованих проектах на мікроконтролерах. З успіхом цей дисплей використовую разом з чипом ESP8266, для якого є вибір з декількох бібліотек. А ось для мікроконтролеру STM32 повноцінної бібліотеки не знайшов. Тому вирішено портувати бібліотеку для OLED SSD1306 з Arduino подібних засобів писаних на C++, до наших, STM32-ушних потреб, використовуючи мову Сі та HAL бібліотеки. По суті, вся робота, це переробка функцій бібліотек з C++ на Сі, а те що стосується апаратної частини це два рядка коду - виклик HAL-овських функцій для роботи з I2C протоколом.

CubeMX for STM32

Запускаємо CubeMX, створюємо новий проект, обираємо свій чип STM32. У вкладці "Pinout" робимо такі налаштування шпильок:
  1. RCC - Crystal/Ceramic resonator
  2. SYS - Serial Wire
  3. обираємо порт I2C до якого буде підключено дисплей, я обрав I2C1
Налаштування в шпильок
Перейдемо до вкладки "Clock Configuration" та налаштуємо тактування мікроконтролеру як зазначено червоним на малюнку:
Наалаштування тактування
У вкладці "Configuration" можна налаштувати порт I2C. На чипові STM32F1xx все працює за замовчуванням на частоті 400кГц. А от на чипові STM32F0xx є більше можливостей в налаштуваннях I2C і вдалось запустити дисплей на частоті шини I2C аж 1000кГц:
Налаштування I2C для STM32F030F4
Але ця бібліотека "заважка" для чипу STM32F030F4, бо займає майже всь пам'ять мікроконтролера. Хоча можна видалити з бібліотеки непотрібні функції і цілком комфортно використовувати її і на таких молодших серіях MCU.

З CubeMX це все. Зберігаємо проект, надаємо ім'я проекту, генеруємо код для свого засобу розробки, та запускаємо свій засіб розробки. В мене це Atolic TrueStudio for STM32 - професійний засіб розробки і абсолютно безкоштовний.

Підключення бібліотеки до проекту та демокод

Бібліотека складається з таких файлів:
  • fonts.h - заголовний файл для трьох шрифтів
  • ssd1306_defines.h - налаштування для роботи дисплею
  • ssd1306.h - заголовний файл бібліотеки
  • font.c - три шрифта розміром 7х10, 11х18, 16х26
  • ssd1306.c - сирцевий файл бібліотеки
Ці файли потрібно вкласти до вашого проекту. Файли з розширенням ".h" до теки "inc", а файли з розшиернням ".c" до теки "src". Або всю теку з бібліотекою, але на деяких засобах розробки потрібно в налаштуваннях проекту прописати шлях до теки з бібліотекою.

Сподіваюсь з підключенням до проекту бібліотеки у вас все вийшло.

Відкриємо в зазобі розробки файл ssd1306_defines.h і налаштуємо бібліотеку для правильної роботи з дисплеєм:

  • #define STM32F1XX - вказати серію свого чипа
  • #define STM32_I2C_PORT hi2c1 - вказати порт I2C до якого під'єднано дисплей
  • #define SSD1306_ADDRESS 0x3C - адреса дисплею на шині I2C
  • #define SSD1306_128X64 - тип дисплею 128Х64 або 128Х32

Тепер напишемо демо програмку, щоб оцінити роботу дисплею. Відкриваємо файл "main.c" в засобі розробки і в визначених програмою CubeMX місцях для користувача, напишемо такий код:
  • Вкладемо заголовний файл з бібліотекою до нашого коду
/* USER CODE BEGIN Includes */
#include "ssd1306.h"
/* USER CODE END Includes */
  • Напишемо декілька функцій для демонстрації можливостей бібліотеки
/* USER CODE BEGIN 0 */
// Adapted from Adafruit_SSD1306
void drawLines()
{
  for (int16_t i = 0; i < ssd1306_GetWidth(); i += 4)
  {
    ssd1306_DrawLine(0, 0, i, ssd1306_GetHeight() - 1);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  for (int16_t i = 0; i < ssd1306_GetHeight(); i += 4)
  {
    ssd1306_DrawLine(0, 0, ssd1306_GetWidth() - 1, i);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  HAL_Delay(250);

  ssd1306_Clear();
  for (int16_t i = 0; i < ssd1306_GetWidth(); i += 4)
  {
   ssd1306_DrawLine(0, ssd1306_GetHeight() - 1, i, 0);
   ssd1306_UpdateScreen();
   HAL_Delay(10);
  }
  for (int16_t i = ssd1306_GetHeight() - 1; i >= 0; i -= 4)
  {
   ssd1306_DrawLine(0, ssd1306_GetHeight() - 1, ssd1306_GetWidth() - 1, i);
   ssd1306_UpdateScreen();
   HAL_Delay(10);
  }
  HAL_Delay(250);
  ssd1306_Clear();
  for (int16_t i = ssd1306_GetWidth() - 1; i >= 0; i -= 4)
  {
    ssd1306_DrawLine(ssd1306_GetWidth() - 1, ssd1306_GetHeight() - 1, i, 0);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  for (int16_t i = ssd1306_GetHeight() - 1; i >= 0; i -= 4)
  {
    ssd1306_DrawLine(ssd1306_GetWidth() - 1, ssd1306_GetHeight() - 1, 0, i);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  HAL_Delay(250);
  ssd1306_Clear();
  for (int16_t i = 0; i < ssd1306_GetHeight(); i += 4)
  {
    ssd1306_DrawLine(ssd1306_GetWidth() - 1, 0, 0, i);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  for (int16_t i = 0; i < ssd1306_GetWidth(); i += 4)
  {
    ssd1306_DrawLine(ssd1306_GetWidth() - 1, 0, i, ssd1306_GetHeight() - 1);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  HAL_Delay(250);
}

// Adapted from Adafruit_SSD1306
void drawRect(void)
{
  for (int16_t i = 0; i < ssd1306_GetHeight() / 2; i += 2)
  {
    ssd1306_DrawRect(i, i, ssd1306_GetWidth() - 2 * i, ssd1306_GetHeight() - 2 * i);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
}

// Adapted from Adafruit_SSD1306
void fillRect(void) {
  uint8_t color = 1;
  for (int16_t i = 0; i < ssd1306_GetHeight() / 2; i += 3)
  {
    ssd1306_SetColor((color % 2 == 0) ? Black : White); // alternate colors
    ssd1306_FillRect(i, i, ssd1306_GetWidth() - i * 2, ssd1306_GetHeight() - i * 2);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
    color++;
  }
  // Reset back to WHITE
  ssd1306_SetColor(White);
}

// Adapted from Adafruit_SSD1306
void drawCircle(void)
{
  for (int16_t i = 0; i < ssd1306_GetHeight(); i += 2)
  {
    ssd1306_DrawCircle(ssd1306_GetWidth() / 2, ssd1306_GetHeight() / 2, i);
    ssd1306_UpdateScreen();
    HAL_Delay(10);
  }
  HAL_Delay(1000);
  ssd1306_Clear();

  // This will draw the part of the circel in quadrant 1
  // Quadrants are numberd like this:
  //   0010 | 0001
  //  ------|-----
  //   0100 | 1000
  //
  ssd1306_DrawCircleQuads(ssd1306_GetWidth() / 2, ssd1306_GetHeight() / 2, ssd1306_GetHeight() / 4, 0b00000001);
  ssd1306_UpdateScreen();
  HAL_Delay(200);
  ssd1306_DrawCircleQuads(ssd1306_GetWidth() / 2, ssd1306_GetHeight() / 2, ssd1306_GetHeight() / 4, 0b00000011);
  ssd1306_UpdateScreen();
  HAL_Delay(200);
  ssd1306_DrawCircleQuads(ssd1306_GetWidth() / 2, ssd1306_GetHeight() / 2, ssd1306_GetHeight() / 4, 0b00000111);
  ssd1306_UpdateScreen();
  HAL_Delay(200);
  ssd1306_DrawCircleQuads(ssd1306_GetWidth() / 2, ssd1306_GetHeight() / 2, ssd1306_GetHeight() / 4, 0b00001111);
  ssd1306_UpdateScreen();
}

void drawProgressBarDemo(int counter)
{
 char str[128];
  // draw the progress bar
  ssd1306_DrawProgressBar(0, 32, 120, 10, counter);

  // draw the percentage as String
  ssd1306_SetCursor(64, 15);
  sprintf(str, "%i%%", counter);
  ssd1306_WriteString(str, Font_7x10);
  ssd1306_UpdateScreen();
}
/* USER CODE END 0 */
  • Зробимо ініціалізацію дисплею і викличемо по черзі демонстраційні функції
/* USER CODE BEGIN 2 */
  ssd1306_Init();
  ssd1306_FlipScreenVertically();
  ssd1306_Clear();
  ssd1306_SetColor(White);

  drawLines();
  HAL_Delay(1000);
  ssd1306_Clear();

  drawRect();
  HAL_Delay(1000);
  ssd1306_Clear();

  fillRect();
  HAL_Delay(1000);
  ssd1306_Clear();

  drawCircle();
  HAL_Delay(1000);
  ssd1306_Clear();

  for(int i = 0; i < 100; i++)
  {
   drawProgressBarDemo(i);
   HAL_Delay(25);
   ssd1306_Clear();
  }

  ssd1306_DrawRect(0, 0, ssd1306_GetWidth(), ssd1306_GetHeight());
  ssd1306_SetCursor(8, 20);
  ssd1306_WriteString("SSD1306", Font_16x26);
  ssd1306_UpdateScreen();
  ssd1306_Clear();
  /* USER CODE END 2 */

  • Тепер можна компілювати, заливати до мікроконтролера та милуватись роботою дисплею

Список функцій бібліотеки


uint16_t ssd1306_GetWidth(void); // повертає ширину екрану в пікселях
uint16_t ssd1306_GetHeight(void); // повертає висоту екрану в пікселях
SSD1306_COLOR ssd1306_GetColor(void); // повертає поточний колір
void ssd1306_SetColor(SSD1306_COLOR color); // задаємо поточний колір
uint8_t ssd1306_Init(void); // початкова ініціалізація дисплею
void ssd1306_Fill(); // заповнення екрану поточним кольором, аналог функції clear
void ssd1306_UpdateScreen(void); // оновлення зображення екрану
void ssd1306_DrawPixel(uint8_t x, uint8_t y); // малювання пікселю в заданих координатах
void ssd1306_DrawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1); // малювання лінії по заданих координатах
void ssd1306_DrawVerticalLine(int16_t x, int16_t y, int16_t length); // малювання вертикальної лінії по заданих координатах
void ssd1306_DrawHorizontalLine(int16_t x, int16_t y, int16_t length); // малювання горизонтальної лінії по заданих координатах
void ssd1306_DrawRect(int16_t x, int16_t y, int16_t width, int16_t height); // малювання прямокутника по заданих координатах
void ssd1306_DrawTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3); // малювання трьохкутника по заданим координатах
void ssd1306_FillRect(int16_t xMove, int16_t yMove, int16_t width, int16_t height); // малювання зафарбованого прямокутника по заданим координатам 
void ssd1306_DrawCircle(int16_t x0, int16_t y0, int16_t radius); // малювання кола по заданим координатам
void ssd1306_FillCircle(int16_t x0, int16_t y0, int16_t radius); // малювання зафарбованого кола по заданим координатам
void ssd1306_DrawCircleQuads(int16_t x0, int16_t y0, int16_t radius, uint8_t quads); // малювання сектора кола по заданим координатам
void ssd1306_DrawProgressBar(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t progress); // малювання прогресбару по заданим координатам і наскільки заповнений у відсотках
void ssd1306_Image(uint8_t *img, uint8_t frame, uint8_t x, uint8_t y); // вивід зображення
char ssd1306_WriteChar(char ch, FontDef Font); // друк окремого символу
char ssd1306_WriteString(char* str, FontDef Font); // друк текстового рядку
void ssd1306_SetCursor(uint8_t x, uint8_t y); // встановлення координат курсору в пікселях
void ssd1306_DisplayOn(void); // увімкнути дисплей
void ssd1306_DisplayOff(void); // вимкнути дисплей
void ssd1306_InvertDisplay(void); // інверсія дисплею
void ssd1306_NormalDisplay(void); // нормалізація дисплею
void ssd1306_ResetOrientation(void); // скидання оріентації дисплею
void ssd1306_FlipScreenVertically(void); // перегорнути зображення дисплею по вертикалі
void ssd1306_MirrorScreen(void); // дзеркалювання зображення дисплею
void ssd1306_Clear(void); // очистка буферу дисплея

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

Бібліотека для роботи з дисплеєм OLED Display 0.96'' SSD1306

Відео приклад роботи демо-коду

Посилання на джерела натхнення