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

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


update 13.04.2019 - виправлено функцію виводу зображення на екран, та додано до демонстрації вивід зображення. Додав можливість роботи в режимі DMA (читай в статті як це зробити).

Передмова

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 - сирцевий файл бібліотеки
  • image.h - заголовний файл зображення для демки
Ці файли потрібно вкласти до вашого проекту. Файли з розширенням ".h" до теки "inc", а файли з розширенням ".c" до теки "src". Або всю теку з бібліотекою, але на деяких засобах розробки потрібно в налаштуваннях проекту прописати шлях до теки з бібліотекою.

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

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

  • //#define USE_DMA - коли I2C шина для передачі використовує DMA потрібно розкоментувати рядок
  • #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"
#include "image.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);
  /* USER CODE END 2 */
  • В циклі викликаємо по черзі функції для демонстрації:

/* USER CODE BEGIN WHILE */
  while (1)
  {
 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();
 HAL_Delay(2000);
 ssd1306_Clear();
 ssd1306_DrawBitmap(0, 0, 128, 64, stm32fan);
 ssd1306_UpdateScreen();
 HAL_Delay(2000);
 ssd1306_InvertDisplay();
 HAL_Delay(2000);
 ssd1306_NormalDisplay();
 ssd1306_Clear();
    /* USER CODE END WHILE */

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

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


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_DrawBitmap(uint8_t X, uint8_t Y, uint8_t W, uint8_t H, const uint8_t* pBMP); // вивід зображення
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); // очистка буферу дисплея

Робота бібліотеки з DMA

Якщо потрібно, для надсилання даних по шині I2C з DMA, то спочатку потрібно увімкнути і налаштувати режим DMA в програмі CubeMX:
Увімкнення режиму DMA
  1. Відкриваємо вкладку з налаштуваннями шини I2C
  2. Режим I2C
  3. Вкладка DMA Setting
  4. Кнопка ADD
  5. Обираємо I2C2_TX (по шині I2C ми тільки відправляємо байти TX, на прийом RX не вмикаємо режим DMA)
Обов'язково вмикаємо переривання "I2C2 event interrput", без цього працювати не буде:
Вмикаємо переривання
Генеруємо проект. В своїй IDE оновлюємо проект та розкоментуємо рядок "#define USE_DMA". Все готово. Можна компілювати та заливати в МК проект і милуватись демкою на своєму дисплеї.

Image2CPP

Для формування дампу даних зображення перейдіть за ланкою.

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

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

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

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

четвер, 1 лютого 2018 р.

STM32: керуємо зсувним регістром SN74HC595 по SPI

Передмова

Щоб спробувати роботу інтерфейсу SPI на простому прикладі я обрав мікросхему зсувного регістру SN74HC595, який приймає по послідовній шині байт і виставляє цей байт на виході вже у паралельному вигляді. Спектр застосування доволі широкий. Це і розширення GPIO, коли не вистачає ніжок на MCU для проекту, і робота з 7-ми сегментними дисплеями, і робота зі світлодіодними матрицями, тощо.

Мета

З метою ознайомлення з інтерфейсом SPI:
  • підключимо зсувний регістр SN74HC595 до STM32;
  • створимо проект в STM32CubeMX, налаштуємо периферію;
  • напишемо невеличкий код для демонстрації.

Налаштування периферії в STM32CubeIDE

Створюємо новий проект в STM32CubeMX і налаштовуємо периферію як показано на малюнку:
Налаштування проекту

  1. Обираємо SPI і налаштовуємо як "Transmit only master", так як все одно будемо тільки передавати байти;
  2. Обираємо "Serial Wire" для програмування MCU за допомоги ST-Link (не обов'язково);
  3. Налаштовуємо одину шпильку на вихід GPIO_OUTPUT (яку вам зручно) і називаємо її "CS_HC595" вибір кристалу.
Генеруємо проект, даємо назву, наприклад "STM32F103RB_SPI_SN74HC595N". Переходимо до збірки схеми.

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

Так як в SN74HC595N шпильки називаються по своєму, то я наведу вам відповідність SPI сигналів до виводів SN74HC595N:
  • STM32: PB11 (CS) - 74HC595: 12 (RCLK) Storage register
  • STM32: PB15 (MOSI) - 74HC595: 14 (SER) Serial data input
  • STM32: PB13 (SCK) - 74HC595: 11 ( SRCLK) Shift register
Все інше схематично на макеті:

Макетна схема для Nucleo-F103RB

Макетна схема для STM32F103C8 (Blue Pill)

Все налаштовано і зібрано? Залишилось написати демо-код.

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

Відкриваємо файл "main.c". У визначених програмою STM32CubeIDE ділянках для користувача пишемо такий код:

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  for (int i = 0; i < 256; ++i)
	  {
		HAL_GPIO_WritePin(CS_HC595_GPIO_Port, CS_HC595_Pin, GPIO_PIN_RESET);
		HAL_SPI_Transmit(&hspi1, (uint8_t*) &i, 1, HAL_MAX_DELAY);
		HAL_GPIO_WritePin(CS_HC595_GPIO_Port, CS_HC595_Pin, GPIO_PIN_SET);
		HAL_Delay(150);
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Це простий цикл від 0 до 255, або від 0x00h до 0xFFh у шістнадцятковому виді. В цьому циклі ми по SPI з MCU шлемо один байт ітерації циклу до зсувного регістру кожні 150 мілісекунд. Простими словами, байт посланий  по послідовній шині, зсувний регістр прийме і виставить на паралельну шину вже у двійковому форматі. І будемо спостерігати як перебираються цифри від 0 до 255 у двійковому форматі.
Можете проекспериментувати з надсиланням байтів і зробити, наприклад, вогник що біжить зліва праворуч та повертається з право ліворуч. Шляхом зсуву бітів у циклі.

Наприклад, вогник що біжить:
/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  for (int i = 0; i < 8; ++i)
	  {
		int tmp = 1 << i;
		HAL_GPIO_WritePin(CS_HC595_GPIO_Port, CS_HC595_Pin, GPIO_PIN_RESET);
		HAL_SPI_Transmit(&hspi1, (uint8_t*) &tmp, 1, HAL_MAX_DELAY);
		HAL_GPIO_WritePin(CS_HC595_GPIO_Port, CS_HC595_Pin, GPIO_PIN_SET);
		HAL_Delay(150);
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
Або, тінь що біжить:
/* USER CODE BEGIN WHILE */
  while (1)
  {
	  for (int i = 0; i < 8; ++i)
	  {
		int tmp = ~(1 << i);
		HAL_GPIO_WritePin(CS_HC595_GPIO_Port, CS_HC595_Pin, GPIO_PIN_RESET);
		HAL_SPI_Transmit(&hspi1, (uint8_t*) &tmp, 1, HAL_MAX_DELAY);
		HAL_GPIO_WritePin(CS_HC595_GPIO_Port, CS_HC595_Pin, GPIO_PIN_SET);
		HAL_Delay(150);
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Відео-посібник



пʼятниця, 26 січня 2018 р.

STM32: Бібліотека сенсору освітлення TSL2561 шина I2C

Передмова

Переробив бібліотеку з SparkFun TSL2561 library що для Arduino IDE на бібліотеку для STM32 використовуючи HAL Driver в роботі з I2C, все інше стандартний код Сі.

Датчик світла TSL2561 є світло-цифровими перетворювачем, який перетворює інтенсивність світла в цифровий вид. Керування по шині I2C TSL2561. Пристрій поєднує в собі один широкосмуговий фотодіод (видимий плюс інфрачервоний) та один інфрачервоний фотодіод на єдиній інтегрованій схемі CMOS. Два інтегрованих АЦП перетворюють фотодіодні потоки до цифрового виду. Ці цифрові данні можуть бути введені в мікропроцесор де вже визначається освітленість (рівень освітлення) у люксах використовуючи емпіричну формулу для наближення реакції людини на очі.

Що потрібно


  • Плата STM32F103RB Nucleo;
  • Сенсор TSL2561;
  • два резистори 10К для підтяжки шини I2C;
  • чотири дроти для з'єднання.

Зручність цієї плати в тім, що данні з сенсору можна передавати до термінальної програми на ПК по USART2 штатним засобом, без додаткового обладнання і дротів. Приєднавши до ПК плату STM32F103RB Nucleo USB шнурком отримуєте ST-LinkV2, Flash disk, UART (COM port) в одному флаконі. 
Сенсор освітлення приєднаємо до шини I2C2 MCU. Під'єднаємо два резистори для підтяжки SDA і SCL до логічної 1 (+Vcc живлення). І напишемо програму яка буде виводити в послідовний порт USART2 данні про освітлення.

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

Схема підключення сенсора по I2C
На малюнку зображений сенсор BMP180, але це не має значення, головне показано як підключити до I2C шини. З TSL2561 все так само.

Створюємо проект в CubeMX

Створюємо новий проект в програмі CubeMX, обираємо свій MCU, назначаємо потрібні інтерфейси - SYS, I2C, USART, як на малюнку:
Налаштування шпильок
Даємо ім'я проекту, наприклад "STM32F103RB_I2C_UART_TSL2561", обираємо засіб розробки, наприклад "Atolic True Studio", та генеруємо код. З CubeMX це все.

Додаємо бібліотеку до проекту і пишемо тестову програму

Відкриваємо/експортуємо підготовлений, програмою CubeMX, проект в своєму засобі розробки.

Бібліотеку TSL2561 можна завантажити тут. Просто додайте заголовний файл бібліотеки "tsl2561.h" до теки "inc" проекту, а сирцевий файл бібліотеки "tsl2561.с" додайте до теки "src" проекту. З додаванням бібліотеки це все.

Відкриваємо головний файл проекту "main.c". І пишемо там такий код, у визначених ділянках для користувача програмою CubeMX.

Додаємо потрібні бібліотеки:
/* USER CODE BEGIN Includes */
#include "tsl2561.h"
#include <string.h>
#include <stdlib.h>
/* USER CODE END Includes */

Оголошуємо прототип функції друку текстових рядків до USART:
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
void print(char string[]);
/* USER CODE END PV */

Оголошуємо декілька змінних:
/* USER CODE BEGIN 1 */
 bool gain;        // Gain setting, 0 = X1, 1 = X16;
 unsigned int ms;  // Integration ("shutter") time in milliseconds
  /* USER CODE END 1 */

Друкуємо в послідовний порт USART текст, читаємо ID сенсору, ініціалізуємо сенсор та вмикаємо його:
/* USER CODE BEGIN 2 */
  print("\r\nUSART2 is OK\r\n");

  unsigned char ID;

  if (TSL2561_getID(&ID))
  {
   char string[2];
   sprintf(string, "%x", ID);

      print("Got factory ID: 0X");
      print(", should be 0X5X\r\n");
  }
  else
  {
     print("error get ID\r\n");
  }

  gain = 0;
  unsigned char time = 2;

  TSL2561_setTiming_ms(gain, time, &ms);
  TSL2561_setPowerUp();
  /* USER CODE END 2 */

В безкінечному циклі раз на секунду читаємо значення регістрів сенсору і вираховуємо значення "lux", та друкуємо все в послідовний порт:
/* USER CODE BEGIN WHILE */
  while (1)
  {
   char string[100];
   unsigned int data0, data1;

   if (TSL2561_getData(&data0, &data1))
   {
    double lux;    // Resulting lux value
    bool good;  // True if neither sensor is saturated

    // Perform lux calculation:
    good = TSL2561_getLux(gain, ms, data0, data1, &lux);

    // Print out the results:
    print("Data0: ");
    sprintf(string, "%d", data0);
    print(string);
    print(" -> ");
    print("Data1: ");
    sprintf(string, "%d", data1);
    print(string);
    print(" -> ");
    print("Lux: ");
    itoa((int) lux, string, 10);
    print(string);
    if (good) print(" (good)\r\n"); else print(" (BAD)\r\n");
  }
  else
  {
   // getData() returned false because of an I2C error, inform the user.
   print("error get lux\r\n");

  }
   HAL_Delay(1000);
  /* USER CODE END WHILE */

Не забуваймо додати саму функцію "print", прототип якої ми оголосили на початку програми:

/* USER CODE BEGIN 4 */
void print(char string[])
{
 HAL_UART_Transmit(&huart2, (uint8_t*) string, strlen(string), 100);
}
/* USER CODE END 4 */

Результат

Компілюємо програму, заливаємо готову прошивку до MCU, в термінальній програмі на ПК спостерігаємо результат:
Приклад роботи

Файли

понеділок, 22 січня 2018 р.

STM32: Сканер пристроїв на I2C шині (оновлено 24.12.2020)

update 24.12.2020: статтю оновлено до сучасних реалій. Додано відео-посібник.

Передмова

Розбирався я з шиною I2C, точніше хотів під'єднати сенсор освітлення TSL2561, який працює по шині I2C. І щоб зрозуміти, як тим сенсором скористуватись, довелось розбиратись з шиною I2C. Поки розбирався, вивчав, як воно все то працює і як наслідок написав невеличку програмку для сканування адрес пристроїв які "висять" на шині I2C. Зручно перевіряти адреси пристроїв, бо не завжди впевнений в правильності адреси, або переконатись чи взагалі, той, чи інший пристрій, відгукується на запит. Бо був випадок. Ніяк не міг запустити на іншому проекті пов'язаному з ESP8266 сенсор si7021. Як виявилось, він взагалі не реагує на запити. Більш за все бракований, або зіпсовався, коли припаював дроти до нього.

Створюємо проект в STM32CubeIDE

Створюємо новий проект в STM32CubeIDE. Обираємо свій MCU. Позначаємо потрібні піни. Я використовую MCU STM32F103C8T6, тож приклад буде для нього, але це не принципово. У вас може бути будь який інший MCU STM32.

Налаштування проекту
  1. Оберемо шину: I2C1 (або I2C2);
  2. Назначимо, що тактуватись будемо від зовнішнього кварцу: Crystal/Ceramic Resonator (не обов'язково, можна залишити як є, тактуватись буде, в цьому випадку, від внутрішнього генератора мікроконтролера);
  3. Оберемо пристрій Debug по якому заливаємо прошивку та налагоджуємо програму: Serial Wire;
  4. Оберемо послідовну шину: USART2 (можна інший, який зручно вам).
З налаштуваннями це все. Можна генерувати проект "Ctrl+S".

Пишемо програму

Відкриваємо в дереві проекту файл "main.c". В визначених, для користувача/розробника, ділянках додаємо такий код.

Додаємо до проекту бібліотеку "stdio.h", вона нам згодиться для друку повідомлень в Serial Monitor за допомоги функції "printf()":
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

Робимо редирект потоку до UART за допомоги переназначення системної функції "write":
/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len)
{
	HAL_UART_Transmit(&huart2, (uint8_t *) ptr, len, HAL_MAX_DELAY);
	return len;
}
/* USER CODE END 0 */

Тепер сама програма сканеру шини I2C:
/* USER CODE BEGIN WHILE */
  while (1)
  {
	  printf("\r\nStart scanner I2C bus\r\n");

	  for (int i = 0; i < 128; ++i)
	  {
		uint16_t address = i << 1;

		if (HAL_I2C_IsDeviceReady(&hi2c1, address, 1, HAL_MAX_DELAY) == HAL_OK)
		{
			printf("Address: 0x%X\r\n", address >> 1);
		}
	  }

	  printf("End test I2C bus\r\n");
	  HAL_Delay(5000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Компілюємо, заливаємо. Підключаємо до шини I2C MCU ваші пристрої I2C, та конвертер USART2USB до зазначеного USART по такій схемі (шпильки MCU мають відповідати назначеним шпилькам в STM32CubeIDE):
Схема підключення

В будь якій термінальній програмі слухаємо COM порт до якого підключений USART2USB конвертер.  Подаємо живлення на MCU і в термінальній програмі бачимо результат сканування:

Результат роботи програми

Я використовую вбудований в STM32CubeIDE термінал. "З коробки" Serial Monitor не встановлено, потрібно встановити додатково з репозиторію. Як це зробити, описано в статті.

Відео-посібник



пʼятниця, 22 грудня 2017 р.

STM32 Cube Programmer для програмування мікроконтролерів STM32


STM32CUBEPROG

Напередодні свят компанія STMicroelectronics дарує своїм прихильникам нову програму для прошивання мікроконтролерів STM32CubeProgrammer.

STM32CubeProgrammer (STM32CUBEPROG) - це все-в-одному, мультиопераційне програмне забезпечення для програмування мікроконтролерів STM32.

Ця програма надає просте у використанні та ефективне середовище для читання, запису та перевірки пам'яті пристрою через інтерфейс налагодження (JTAG і SWD) та інтерфейс завантажувача (UART та USB).

Програма STM32CubeProgrammer пропонує широкий спектр можливостей для програмування внутрішніх пам'яті мікроконтролерів STM32 (таких як Flash, RAM та OTP), а також зовнішніх пам'яті.

Програма STM32CubeProgrammer також дозволяє програмувати та завантажувати параметри, перевіряти програмування вмісту та автоматизувати програмування мікроконтролерів за допомогою сценаріїв.

Програма STM32CubeProgrammer має графічний інтерфейс GUI (графічний інтерфейс користувача) та CLI (інтерфейс командного рядка).

Головні особливості

  • Стирання, запис, перегляди та перевірка вмісту пристрою флеш-пам'яті
  • Підтримка Motorola S19, Intel HEX, ELF та двійкові формати bin
  • Підтримує інтерфейси налагодження та завантажувача:
  • Плата налагодження ST-LINK (JTAG / SWD)
  • Інтерфейси завантажувача UART та USB DFU
  • Програми, стирає та перевіряє зовнішню пам'ять, приклади для зовнішніх флеш-завантажувачів допомагають користувачам розробляти завантажувачі для певної зовнішньої пам'яті
  • Автоматизує програмування STM32 (стирання, перевірка, програмування, налаштування параметрів байтів)
  • Дозволяє програмувати пам'ять OTP
  • Підтримує програмування та налаштування параметрів байтів
  • Пропонує інтерфейс командного рядка для автоматизації через сценарії
  • Оновлення прошивки ST-LINK
  • Вмикає безпечне створення вбудованого програмного забезпечення за допомогою інструмента Trimted Package Creator STM32
  • Підтримка декількох ОС: Windows ® , Linux ® , macOS ®

Зображення програми




середа, 22 листопада 2017 р.

STM32: Керування світлодіодом по UART через термінальну програму на ПК


update 08.01.2021: оновив статтю + додав відео-посібник.

Передмова

Сам по собі чип STM32 дуже цікавий та самодостатній, але якщо він має зв'язок з зовнішнім світом, то сфери його застосування розширюються, та стають цікавішими. Спробуємо взаємодіяти з зовнішнім світом. В цьому прикладі, це буде плата NUCLEO-F103RB (у вас може бути інша плата, принцип залишається) та персональний комп'ютер. З'єднаємо їх за допомоги послідовного інтерфейсу UART або COM порту. Це буде проста програма, яка приймає команди від комп'ютера по послідовному порту і відповідно до команд реагує на них. В нашому прикладі, це буде увімкнення та вимкнення світлодіоду, що на борту плати STM32. 

Налаштування периферії

Запускаємо програму STM32CubeIDE, обираємо свою плату або чип, та починаємо налаштовувати периферію. Якщо було обрано, як в мене, плату NUCLEO-F103RB, то помічник налаштування периферії сам запропонує ініціалізувати периферію в режимі за замовчанням. Погоджуємось і нам залишається тільки додатково увімкнути глобальні переривання для USART2, та додати канал DMA для RX USART2:
Налаштування периферії


USART2 порт, який під'єднаний до програматора ST-Link V2-1 на платі NUCLEO. А програматор з'єднаний з комп'ютером USB шнурком і бачиться комп'ютером, як диск на 156 кБ (вірно для NUCLEO плат), та віртуальний COM порт, який виділить ваша операційна система:

Диск та COM порт програматора

Це дозволить ознайомитись з роботою UART без додаткового обладнання та дротів. 

Вкладку "Clock Configuration" програми STM32CubeMX залишив без змін. Хай буде як є. Перейдемо до вкладки "Configuration", та в зоні "Connectivity" натиснемо на "USART2" перейдемо до налаштувань USART2. У вкладці "Parametr Setting" залишаємо як є:

Налаштування параметрів USART2

  • Baud Rate - 115200 Bits/s
  • Word Length - 8 Bits
  • Parity - NONE
  • Stop Bits - 1
  • Data Direction - Receive and Transmit
  • Over Sampling - 16 Samples

Перейдемо до вкладки "NVIC Setting" та дозволимо переривання від USART2:

Налаштування глобального переривання USART2

А щоб код був "легшим", та не завантажувати MCU будемо для передачі та прийому даних використовувати технологію DMA. Для цього перейдемо до вкладки "DMA Setting". Тиснемо кнопку "Add", обираємо "Select" -> USART2_RX і повторюємо це саме для USART2_TX. А так як в нашому прикладі RX (прийом) будемо слухати завжди, то режим для прийому оберемо, як "circular" (круговий). Прийняли пакет даних - знову готові приймати.

Налаштування DMA

Все інше залишаємо як є. І можна генерувати проект.

Пишемо програму

Відкриваємо, чи імпортуємо, до свого засобу розробки проект і для перевірки пробуємо скласти (компілювати) проект. Як все без помилок, продовжимо далі.

В принципі, додати пару рядків і програма готова до прийому чи передаванню даних. Але є одне "але". Якщо ми передаємо дані, то ми знаємо наперед кількість байтів які хочемо передати і явно вказуємо це HAL функції. А от з прийманням даних не все так гладенько. Бо як будемо приймати "пакети" даних певної, постійної довжини, то теж все гладенько і просто. Вказав, спеціально навченій HAL функції, довжину прийомного буферу і все. А коли це будуть якісь команди різної довжини, чи дані різної довжини? Що тоді робити? Як знати що партнер закінчив свою передачу, а нам треба обробляти прийняте? 

Один з виходів з цієї ситуації це прийом по одному байту, та складання прийнятих байт в окремий буфер і аналіз прийнятих байт. І коли буде прийнятий байт '\n' (кінець рядку), чи '\r' (ENTER), то це буде означати, що передачу партнер закінчив і можна обробляти весь рядок прийнятих байтів на прийомній стороні.

Рішення цієї проблеми я вгледів тут. І ми скористаємось вже готовим рішенням та додаймо до свого проекту. В засобі розробки відкриваємо файл main.с і додаємо рядок:

/* USER CODE BEGIN PD */
#define MAXCLISTRING          100 // Biggest string the user will type
/* USER CODE END PD */

Це максимальна довжина рядку який можливо прийняти за один раз. Тут все.

Додамо трішки зминних:

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
uint8_t rxBuffer = '\000'; // where we store that one character that just came in
uint8_t rxString[MAXCLISTRING]; // where we build our string from characters coming in
int rxindex = 0; // index for going though rxString
/* USER CODE END PV */

Далі додаємо прототип функцій на початку, та редірект функції "_write" для друку повідомлень за допомоги функції "printf":

/* USER CODE BEGIN PFP */
void executeSerialCommand(uint8_t* string);

int _write(int file, char *ptr, int len)
{
	HAL_UART_Transmit(&huart2, (uint8_t*) ptr, len, HAL_MAX_DELAY);
	return len;
}
/* USER CODE END PFP */

В другу секцію коду для розробника додаємо передачу одного рядку тексту для перевірки передачі по UART2, та ініціацію прийому по UART2:

/* USER CODE BEGIN 2 */
  printf("\r\nReceive UART DMA started!\r\n");
  HAL_UART_Receive_DMA(&huart2, &rxBuffer, 1);
  /* USER CODE END 2 */

Безкінечний цикл залишається пустим:

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}

І в секцію чотири для розробника додаємо функцію, прототип якої ми додали на початку програми плюс callback функцію складання команди з прийнятих байтів:

/* USER CODE BEGIN 4 */
void executeSerialCommand(uint8_t* string)
{
	printf("\r\nConsole > ");

	if (strcmp ((char*) string, "at") == 0)
	{
		printf("OK\r\n");
	}
	else if (strcmp((char*) string, "on") == 0)
	{
		HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, ENABLE);
		printf("LED is ON\r\n");
	}
	else if (strcmp((char*) string, "off") == 0)
	{
		HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, DISABLE);
		printf("LED is OFF\r\n");
	}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	int i = 0;

	printf((char*) &rxBuffer); // Echo the character that caused this callback so the user can see what they are typing

	if (rxBuffer == 8 || rxBuffer == 127) // If Backspace or del
	{
		printf(" \b"); // "\b space \b" clears the terminal character. Remember we just echoced a \b so don't need another one here, just space and \b
		rxindex--;
		if (rxindex < 0) rxindex = 0;
	}
	else if (rxBuffer == '\n' || rxBuffer == '\r') // If Enter
	{
		executeSerialCommand(rxString);
		rxString[rxindex] = 0;
		rxindex = 0;
		for (i = 0; i < MAXCLISTRING; i++) rxString[i] = 0; // Clear the string buffer
	}
	else
	{
		rxString[rxindex] = rxBuffer; // Add that character to the string
		rxindex++;
		if (rxindex > MAXCLISTRING) // User typing too much, we can't have commands that big
		{
			rxindex = 0;
			for (i = 0; i < MAXCLISTRING; i++) rxString[i] = 0; // Clear the string buffer
		}
	}
}
/* USER CODE END 4 */
  • Функція printf друкує текстові рядки до терміналу
  • Функція executeSerialCommand обробляє прийняті команди (додайте сюди свої команди по аналогії)
  • Функція HAL_UART_RxCpltCallback аналізує кожен прийнятий байт і якщо не кінець рядку, не натиснута клавіша ENTER, чи не переповнився прийомний буфер, то всі прийняті байти складаються до буферу. Як прийом закінчено то буфер звільняється, а прийнятий рядок передається до функції executeSerialCommand.

Перевіряємо роботу програми

Компілюємо проект, якщо все було зроблено вірно, то проект збереться без помилок. Та заливаємо зкомпільовану програму до MCU. Щоб перевірити роботу програми і UARTу, треба на комп'ютері запустити якусь термінальну програму, наприклад Tera Term. Та вказати з яким COM портом і на якій швидкості програма буде працювати. В моєму випадку це буде COM6 і швидкість 115200. 

Налаштування порту

Щоб перевірити програму натисніть кнопку скидання на своїй платі розробника при запущеній програмі терміналу на комп'ютері. В терміналі з'явиться напис "Transmit UART DMA is OK!". Далі в терміналі тисніть клавішу ENTER - з'явиться запрошення для вводу команди "MyHomeIoT>". Наберіть великими літерами "AT" і натисніть ENTER і ваш MCU відповість "OK". Наберіть команду "on" маленькими літерами та завершіть введення ENTER і ваш MCU увімкне світлодіод на платі розробника. Дайте команду "off" і MCU вимкне світлодіод. Ось результат описаної роботи:


Приклад роботи програми

Відео-посібник

В відео більш детально і розгорнуто розглянуто приклад взаємодії мікроконтролера з терміналом. Оформлено окремим файлом (невеличка бібліотечка), розглянуто приклад як самому зробити callback функцію для своїх потреб. Файли з кодом по мотивам відео - тут.