понеділок, 16 вересня 2019 р.

STM32: Бібліотека дисплею ILI9341 по інтерфейсу FSMC


Передмова

Бібліотека для роботи з дисплеєм 240х320 з чипом ILI9341 за допомоги інтерфейсу FSMC.

Колись придбав таку плату розробника з чипом STM32F407VET6 для дослідження 4хх серії. Плата якісна. Має на борту роз'єм для радіомодуля 2.4 ГГц NRF24L01, роз'єм для дисплею FSMC, модуль SD-CARD, тримач батарейки 3В для модуля RTC. Пам'ять FLASH. Два вбудованих світлодіода та дві користувацькі кнопки. Досить пристойний кандидат для серйозної розробки. Почнемо з дисплею.
STM32F407VET6 Black Board
До цієї плати продають сумісні по роз'єму дисплеї з резистивним тачскріном. Цей дисплей достатньо просто вставити "бутербродом" в роз'єм плати розробника.
3.2 inch TFT LCD Resistive Touch Screen 320 * 240 ILI9341 Display Module
Є варіант плати з дисплеєм в одним лотом на aliexpress.

Залізяччя

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

Розпіновка роз'ємів JTAG та TFT
Схема підключення TFT

CubeMX

Відкриваємо CubeMX або CubeIDE і налаштовуємо периферію:

  1. В розділі "Connectivity" обираємо "FSMC";
  2. В розділі "NOR Flash/PSRAM/SRAM/ROM/LCD 1" активуємо "NE1 Chip Select", обираємо "Memory type -> LCD Interface", "LCD Register Select -> A18" (відповідно до розпіновки роз'єму для дисплею, в мене сигнал RS дисплею під'єднано до PD13 мікроконтролеру. А ніжка PD13 активується для RS, коли обрати A18), "Data -> 16 bits";
  3. В розділі "Configurations -> NOR/PSRAM control" обираємо: "Memory type -> LCD Interface", "Bank -> Bank 1 NOR/PSRAM 1", "Write operation -> Enabled", "Extended mode -> Disabled". В розділі "NOR/PSRAM timing" обираємо: "Address setup time in HCLK clock cicles -> 1", "Data setup time in HCLK clock cycles -> 5", "Bus turn around time in HCLK clock cycles -> 0";
  4. Не забуваємо про підсвічування екрану. Відповідно до схеми це PB1. Встановимо режим GPIO_Output і дамо назву цій шпильці - "LCD_BL";
  5. Генеруємо проект. Даємо назву проекту, наприклад: "STM32F407VET6_Black_ILI9341". Вказуємо шлях до проекту та зберігаємо проект на диску.

Налаштування FSMC в CubeMX (для збільшення клацайте по світлині)

Підєднуємо бібліотеку до проекту

Запускаємо свій засіб розробки. Я використовую, або "CubeIDE for STM32", або "Atolic True Studio for STM32". Обидві IDE абсолютно безкоштовні. Відкриваємо проект згенерований CubeMX або CubeIDE і додаємо файли бібліотеки до проекту. Файли з розширенням *.h до теки INC проекту, а файли з розширенням *.c до теки SRC проекту. 
Бібліотека містить такі файли:
  • colors.h - визначення кольорів
  • fonts.h - структура шрифтів та перелік шрифтів
  • ili9341.h - визначення, макроси, прототипи функцій бібліотеки
  • image.h - структура для зображень, та перелік зображень
  • registers.h - визначення регістрів дисплею
  • STLogo.c - приклад картинки 240X240 для демонстрації
  • font12.c - шрифт 12 пікселів у висоту
  • font16.c - шрифт 16 пікселів у висоту
  • font20.c - шрифт 20 пікселів у висоту
  • font24.c - шрифт 24 пікселів у висоту
  • font8.c - шрифт 8 пікселів у висоту
  • ili9341.c - містить всі функції бібліотеки
  • example.c - містить демо-код, додати до файлу main.c свого проекту у відповідні секції коду.
Всі шрифти запозичив у ST, які знаходяться у репозиторії CubeMX за шляхом наприклад: "C:\Users\ваш_логін\STM32Cube\Repository\STM32Cube_FW_F4_V1.24.1\Utilities\Fonts".

Налаштування бібліотеки

Інтерфейс FSMC передбачає що, дисплей, який під'єднано до мікроконтролеру, буде "бачитись" як додаткова область пам'яті. І для надсилання команд і даних до дисплею достатньо ці команди і дані пересилати в певну ділянку додаткової пам'яті. Всі інші "ногодриги" шпильками зробить периферія FSMC мікроконтролеру. Залишилось тільки визначити за якими ж адресами надсилати команди і дані.
Звернемось до документації.
Розділ 36.4 External device address mapping:
Малюнок 435
Таблиці 216, 217

В нас обрано Bank1 - NOR/PSRAM 1, А18, 16-біт. Відповідно до малюнку та таблиць - адреса для команд 0x60000000. Визначаємо адресу для даних: 1 << 18 = 0x40000 де 18 це A18. Плюс, так як в нас 16 бітна шина, то 0x40000 << 1 = 0x80000. Тепер складаємо ці дві адреси до купи: 0x60000000 + 0x80000 = 0x60080000 і отримуємо адресу для даних. Заносимо ці адреси до визначень у файл ili9341.h:
#define LCD_BASE0          ((uint32_t)0x60000000)
#define LCD_BASE1          ((uint32_t)0x60080000)
Це і всі налаштування.

Якщо ви в CubeMX не вказали назву шпильки для керування підсвіткою екрана, як було запропоновано "LCD_BL", то тоді потрібно тут поміняти:
#define LCD_BL_ON() HAL_GPIO_WritePin(LCD_BL_GPIO_Port, LCD_BL_Pin, GPIO_PIN_SET)
#define LCD_BL_OFF() HAL_GPIO_WritePin(LCD_BL_GPIO_Port, LCD_BL_Pin, GPIO_PIN_RESET)
На GPIOB замість LCD_BL_GPIO_Port та GPIO_PIN_1 замість LCD_BL_Pin. Порт і номер шпильки куди під'єднано підсвітку дисплея.

Демо-код

Перш за все вкладаємо до свого проекту директивою #include - бібліотеку ili9341:
/* USER CODE BEGIN Includes */
#include "ili9341.h"
/* USER CODE END Includes */
Оголошуємо власний дефайн функції "min" (потрібно для роботи демо):
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define min(a,b) (((a)<(b))?(a):(b))
/* USER CODE END PD */
Оголошуємо прототипи всіх демо-функцій:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void demoLCD(int i);
unsigned long testFillScreen();
unsigned long testText();
unsigned long testLines(uint16_t color);
unsigned long testFastLines(uint16_t color1, uint16_t color2);
unsigned long testRects(uint16_t color);
unsigned long testFilledRects(uint16_t color1, uint16_t color2);
unsigned long testFilledCircles(uint8_t radius, uint16_t color);
unsigned long testCircles(uint8_t radius, uint16_t color);
unsigned long testTriangles();
unsigned long testFilledTriangles();
unsigned long testRoundRects();
unsigned long testFilledRoundRects();
unsigned long testDrawImage();
/* USER CODE END PFP */
Викликаємо функцію ініціалізації дисплея і вмикаємо підсвічування дисплея. Оголошуємо допоміжну змінну "i":
/* USER CODE BEGIN 2 */
  LCD_BL_ON();
  lcdInit();
  int i = 0;
  /* USER CODE END 2 */
В безкінечному циклі викликаємо демо-функцію і збільшуємо змінну "i" на одиницю, щоб міняти орієнтування екрану за кожний прохід демо-функції:
 /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   demoLCD(i);
   i++;
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
Але не забуваємо, що нам потрібні ще функції, що входять до складу "demoLCD", прототипи яких ми же раніше оголосили. Її ставимо в кінці файла "main.c" в блок користувача 4:
/* USER CODE BEGIN 4 */
void demoLCD(int i)
{
 lcdSetOrientation(i % 4);

 uint32_t t = testFillScreen();
 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", t);
 HAL_Delay(2000);

 t = HAL_GetTick();
 lcdTest();
 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", HAL_GetTick() - t);
 HAL_Delay(2000);

 t = testText();
 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", t);
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testLines(COLOR_CYAN));
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testFastLines(COLOR_RED, COLOR_BLUE));
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testRects(COLOR_GREEN));
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testFilledRects(COLOR_YELLOW, COLOR_MAGENTA));
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testFilledCircles(10, COLOR_MAGENTA));
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testCircles(10, COLOR_WHITE));
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testTriangles());
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testFilledTriangles());
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testRoundRects());
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testFilledRoundRects());
 HAL_Delay(2000);

 lcdSetTextFont(&Font16);
 lcdSetCursor(0, lcdGetHeight() - lcdGetTextFont()->Height - 1);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("Time: %4lu ms", testDrawImage());
 HAL_Delay(2000);
}

unsigned long testFillScreen()
{
 unsigned long start = HAL_GetTick(), t = 0;
 lcdFillRGB(COLOR_BLACK);
 t += HAL_GetTick() - start;
 lcdSetCursor(0, 0);
 lcdSetTextFont(&Font24);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("BLACK");
 HAL_Delay(1000);
 start = HAL_GetTick();
 lcdFillRGB(COLOR_RED);
 t += HAL_GetTick() - start;
 lcdSetCursor(0, 0);
 lcdSetTextFont(&Font24);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("RED");
 HAL_Delay(1000);
 start = HAL_GetTick();
 lcdFillRGB(COLOR_GREEN);
 t += HAL_GetTick() - start;
 lcdSetCursor(0, 0);
 lcdSetTextFont(&Font24);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("GREEN");
 HAL_Delay(1000);
 start = HAL_GetTick();
 lcdFillRGB(COLOR_BLUE);
 t += HAL_GetTick() - start;
 lcdSetCursor(0, 0);
 lcdSetTextFont(&Font24);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdPrintf("BLUE");
 HAL_Delay(1000);
 start = HAL_GetTick();
 lcdFillRGB(COLOR_BLACK);
 return t += HAL_GetTick() - start;
}

unsigned long testText()
{
 lcdFillRGB(COLOR_BLACK);
 unsigned long start = HAL_GetTick();
 lcdSetCursor(0, 0);
 lcdSetTextColor(COLOR_WHITE, COLOR_BLACK);
 lcdSetTextFont(&Font8);
 lcdPrintf("Hello World!\r\n");
 lcdSetTextColor(COLOR_YELLOW, COLOR_BLACK);
 lcdSetTextFont(&Font12);
 lcdPrintf("%i\r\n", 1234567890);
 lcdSetTextColor(COLOR_RED, COLOR_BLACK);
 lcdSetTextFont(&Font16);
 lcdPrintf("%#X\r\n", 0xDEADBEEF);
 lcdPrintf("\r\n");
 lcdSetTextColor(COLOR_GREEN, COLOR_BLACK);
 lcdSetTextFont(&Font20);
 lcdPrintf("Groop\r\n");
 lcdSetTextFont(&Font12);
 lcdPrintf("I implore thee,\r\n");
 lcdSetTextFont(&Font12);
 lcdPrintf("my foonting turlingdromes.\r\n");
 lcdPrintf("And hooptiously drangle me\r\n");
 lcdPrintf("with crinkly bindlewurdles,\r\n");
 lcdPrintf("Or I will rend thee\r\n");
 lcdPrintf("in the gobberwarts\r\n");
 lcdPrintf("with my blurglecruncheon,\r\n");
 lcdPrintf("see if I don't!\r\n");
 return HAL_GetTick() - start;
}

unsigned long testLines(uint16_t color)
{
  unsigned long start, t;
  int           x1, y1, x2, y2,
                w = lcdGetWidth(),
                h = lcdGetHeight();

  lcdFillRGB(COLOR_BLACK);

  x1 = y1 = 0;
  y2    = h - 1;
  start = HAL_GetTick();
  for(x2 = 0; x2 < w; x2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  x2 = w - 1;
  for(y2 = 0; y2 < h; y2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  t = HAL_GetTick() - start; // fillScreen doesn't count against timing

  HAL_Delay(1000);
  lcdFillRGB(COLOR_BLACK);

  x1 = w - 1;
  y1 = 0;
  y2 = h - 1;

  start = HAL_GetTick();

  for(x2 = 0; x2 < w; x2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  x2 = 0;
  for(y2 = 0; y2 < h; y2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  t += HAL_GetTick() - start;

  HAL_Delay(1000);
  lcdFillRGB(COLOR_BLACK);

  x1 = 0;
  y1 = h - 1;
  y2 = 0;
  start = HAL_GetTick();

  for(x2 = 0; x2 < w; x2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  x2 = w - 1;
  for(y2 = 0; y2 < h; y2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  t += HAL_GetTick() - start;

  HAL_Delay(1000);
  lcdFillRGB(COLOR_BLACK);

  x1 = w - 1;
  y1 = h - 1;
  y2 = 0;

  start = HAL_GetTick();

  for(x2 = 0; x2 < w; x2 += 6) lcdDrawLine(x1, y1, x2, y2, color);
  x2 = 0;
  for(y2 = 0; y2 < h; y2 += 6) lcdDrawLine(x1, y1, x2, y2, color);

  return t += HAL_GetTick() - start;
}

unsigned long testFastLines(uint16_t color1, uint16_t color2)
{
  unsigned long start;
  int x, y, w = lcdGetWidth(), h = lcdGetHeight();

  lcdFillRGB(COLOR_BLACK);
  start = HAL_GetTick();
  for(y = 0; y < h; y += 5) lcdDrawHLine(0, w, y, color1);
  for(x = 0; x < w; x += 5) lcdDrawVLine(x, 0, h, color2);

  return HAL_GetTick() - start;
}

unsigned long testRects(uint16_t color)
{
  unsigned long start;
  int n, i, i2,
      cx = lcdGetWidth()  / 2,
      cy = lcdGetHeight() / 2;

  lcdFillRGB(COLOR_BLACK);
  n = min(lcdGetWidth(), lcdGetHeight());
  start = HAL_GetTick();
  for(i = 2; i < n; i += 6)
  {
    i2 = i / 2;
    lcdDrawRect(cx - i2, cy - i2, i, i, color);
  }

  return HAL_GetTick() - start;
}

unsigned long testFilledRects(uint16_t color1, uint16_t color2)
{
  unsigned long start, t = 0;
  int n, i, i2,
      cx = lcdGetWidth() / 2 - 1,
      cy = lcdGetHeight() / 2 - 1;

  lcdFillRGB(COLOR_BLACK);
  n = min(lcdGetWidth(), lcdGetHeight());

  for(i = n; i > 0; i -= 6)
  {
    i2 = i / 2;
    start = HAL_GetTick();
    lcdFillRect(cx-i2, cy-i2, i, i, color1);
    t    += HAL_GetTick() - start;
    // Outlines are not included in timing results
    lcdDrawRect(cx-i2, cy-i2, i, i, color1);
  }

  return t;
}

unsigned long testFilledCircles(uint8_t radius, uint16_t color)
{
  unsigned long start;
  int x, y, w = lcdGetWidth(), h = lcdGetHeight(), r2 = radius * 2;

  lcdFillRGB(COLOR_BLACK);
  start = HAL_GetTick();
  for(x = radius; x < w; x += r2)
  {
    for(y = radius; y < h; y += r2)
    {
      lcdFillCircle(x, y, radius, color);
    }
  }

  return HAL_GetTick() - start;
}

unsigned long testCircles(uint8_t radius, uint16_t color)
{
  unsigned long start;
  int x, y, r2 = radius * 2,
      w = lcdGetWidth()  + radius,
      h = lcdGetHeight() + radius;

  // Screen is not cleared for this one -- this is
  // intentional and does not affect the reported time.
  start = HAL_GetTick();
  for(x = 0; x < w; x += r2)
  {
    for(y = 0; y < h; y += r2)
    {
      lcdDrawCircle(x, y, radius, color);
    }
  }

  return HAL_GetTick() - start;
}

unsigned long testTriangles()
{
  unsigned long start;
  int n, i, cx = lcdGetWidth() / 2 - 1,
            cy = lcdGetHeight() / 2 - 1;

  lcdFillRGB(COLOR_BLACK);
  n = min(cx, cy);
  start = HAL_GetTick();
  for(i = 0; i < n; i += 5)
  {
    lcdDrawTriangle(
      cx    , cy - i, // peak
      cx - i, cy + i, // bottom left
      cx + i, cy + i, // bottom right
      lcdColor565(i, i, i));
  }

  return HAL_GetTick() - start;
}

unsigned long testFilledTriangles()
{
  unsigned long start, t = 0;
  int i, cx = lcdGetWidth() / 2 - 1,
         cy = lcdGetHeight() / 2 - 1;

  lcdFillRGB(COLOR_BLACK);
  for(i = min(cx, cy); i > 10; i -= 5)
  {
    start = HAL_GetTick();
    lcdFillTriangle(cx, cy - i, cx - i, cy + i, cx + i, cy + i, lcdColor565(0, i*10, i*10));
    t += HAL_GetTick() - start;
    lcdFillTriangle(cx, cy - i, cx - i, cy + i, cx + i, cy + i, lcdColor565(i*10, i*10, 0));
  }

  return t;
}

unsigned long testRoundRects()
{
  unsigned long start;
  int w, i, i2,
      cx = lcdGetWidth() / 2 - 1,
      cy = lcdGetHeight() / 2 - 1;

  lcdFillRGB(COLOR_BLACK);
  w = lcdGetWidth(), lcdGetHeight();
  start = HAL_GetTick();
  for(i = 0; i < w; i += 6)
  {
    i2 = i / 2;
    lcdDrawRoundRect(cx-i2, cy-i2, i, i, i/8, lcdColor565(i, 0, 0));
  }

  return HAL_GetTick() - start;
}

unsigned long testFilledRoundRects()
{
  unsigned long start;
  int i, i2,
      cx = lcdGetWidth()  / 2 - 1,
      cy = lcdGetHeight() / 2 - 1;

  lcdFillRGB(COLOR_BLACK);
  start = HAL_GetTick();
  for(i = min(lcdGetWidth(), lcdGetHeight()); i > 20; i -=6 )
  {
    i2 = i / 2;
    lcdFillRoundRect(cx - i2, cy - i2, i, i, i / 8, lcdColor565(0, i, 0));
  }

  return HAL_GetTick() - start;
}

unsigned long testDrawImage()
{
 unsigned long start;

 lcdFillRGB(COLOR_BLACK);
 start = HAL_GetTick();
 if (lcdGetOrientation() == LCD_ORIENTATION_LANDSCAPE || lcdGetOrientation() == LCD_ORIENTATION_LANDSCAPE_MIRROR)
 {
  lcdDrawImage((lcdGetWidth() - bmSTLogo.xSize) / 2, 0, &bmSTLogo);
 }
 else
 {
  lcdDrawImage(0, (lcdGetHeight() - bmSTLogo.ySize) / 2, &bmSTLogo);
 }
 return HAL_GetTick() - start;
}
/* USER CODE END 4 */
Компілюємо проект. Заливаємо до пам'яті мікроконтролера і спостерігаємо за демонстрацією.

Файли

Відео-демострація




вівторок, 4 червня 2019 р.

STM32: Бібліотека для 7-сегментного 8 розрядного дисплею на MAX7219 по SPI шині


Передмова

Мабуть самий з найпоширеніших дисплеїв в радіоаматорскій практиці є семисегментний світлодіодний дисплей. Це й простота і дешевизна. Але й є недоліки. Це інформаційне обмеження щодо варіантів символів, які можна зобразити і велика кількість ніжок на мікроконтролері (7 або 8 ніжок для сегментів плюс на кожен розряд по 1 ніжці мікоконтролера).

За допомоги мікросхеми MAX7219 можна усунути недолік з великою кількістю ніжок. За допомоги однієї мікросхеми MAX7219 можна керувати 8 розрядним 7-ми сегментним дисплеєм, плюс режим декодування цифр, плюс регулювання яскравості свічення дисплею, плюс не потрібно турбуватись про динамічну індикацію. А данні до мікросхеми надсилати по шині SPI, яка обмежена швидкістю в 10 Мбіт/сек.

В цій статті розглянемо невеличку бібліотеку для зручного друку інформації на семисегментний дисплей з мікросхемою MAX7219:
  • Очищення дисплею
  • Встановлення рівню яскравості
  • Надсилання даних на MAX7219
  • Вмикання/вимикання дисплею
  • Встановлення/скидання режиму декодування
  • Друк цифр (символів) в заданому розряді
  • Друк чисел типу INT та FLOAT

Залізяччя

Для прикладу буду використовувати такі матеріали:

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

Обираємо свій мікроконтролер або дошку. Та налаштовуємо проект.
Налаштування проекту в Cube
  1. Тиснемо RCC -> High Speed Clock (HSE) -> Crystal/Ceramic Resonator
  2. SYS -> Debug -> Serial Wire
  3. Тиснемо SPI1
  4. Обираємо шпильку Chip Select -> наприклад PB0, обираємо режим GPIO_OUTPUT, та називаємо її CS_MAX7219
  5. Обираємо режим для SPI1 -> Transmit only master
  6. Слідкуємо щоб Baud Rate не перевищував 10 MBits/sec, якщо перевищує збільшіть Prescaler (for Baud Rate)
Генеруємо проект та переходимо до підключення дисплейного модуля до мікроконтролера, завантаження бібліотеки та написання демо-коду.

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

STM32F103C8T6 Дисплейний модуль
Vcc 3.3V
Vcc
GND
GND
PA7 SPI1_MOSI
DIN
PB0 CS_MAX7219
CS
PA5 SPI1_SCK
CLK

Бібліотека MAX7219

Завантажити бібліотеку можна з GitHub. Бібліотека складається з двох файлів: max7219.h та max7219.c та має такі функції:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void max7219_Init(uint8_t intensivity);
void max7219_SetIntensivity(uint8_t intensivity);
void max7219_Clean(void);
void max7219_SendData(uint8_t addr, uint8_t data);
void max7219_Turn_On(void);
void max7219_Turn_Off(void);
void max7219_Decode_On(void);
void max7219_Decode_Off(void);
void max7219_PrintDigit(MAX7219_Digits position, MAX7219_Numeric numeric, bool point);
MAX7219_Digits max7219_PrintItos(MAX7219_Digits position, int value);
MAX7219_Digits max7219_PrintNtos(MAX7219_Digits position, uint32_t value, uint8_t n);
MAX7219_Digits max7219_PrintFtos(MAX7219_Digits position, float value, uint8_t n);
  1. Ініціалізація дисплею. На вхід "рівень яскравості" (0x0 - 0xF);
  2. Встановлення рівня яскравості. На вхід "рівень яскравості" (0x0 - 0xF);
  3. Очищення дисплею;
  4. Надсилання в шину SPI до MAX7219 даних. На вхід: адреса, дані;
  5. Увімкнення дисплею;
  6. Вимкнення дисплею;
  7. Увімкнути режим декодування;
  8. Вимкнути режим декодування;
  9. Друк цифри (символу). На вхід: позиція (1 - 8), цифра (0 - 9), точка (true/false);
  10. Друк числа типу INT. На вхід: стартова позиція числа на дисплеї, число INT. На виході: поточна позиція курсору;
  11. Друк числа типу INT з фіксованою кількістю розрядів. На вхід: стартова позиція числа на дисплеї, число INT, кількість розрядів. На виході: поточна позиція курсору;
  12. Друк числа типу FLOAT. На вхід: стартова позиція числа на дисплеї, число FLOAT, кількість знаків після коми (не більше 4-х). На виході: поточна позиція курсору;

Демо-код

Відкриваємо свій засіб розробки та експортуємо, згенерований CubeMX або CubeIDE, код. 
Копіюємо файли бібліотеки до проекту. Файли з розширенням *.h до теки INC, файли з розширенням *.c до теки SRC.
Відкриваємо файл max7219.h та налаштовуємо бібліотеку:
  • Кількість розрядів #define NUMBER_OF_DIGITS 8
  • Порт SPI до якого приєднано дисплейний модуль #define SPI_PORT hspi1
Відкриваємо файл main.c і в секції USER CODE Includes вкладаємо нашу бібліотеку:
/* USER CODE BEGIN Includes */
#include "max7219.h"
/* USER CODE END Includes */

В користувацьку секцію 2 додаємо ініціалізацію дисплею з встановленим рівнем яскравості 7 і вмикаємо режим декодування цифр:
/* USER CODE BEGIN 2 */
  max7219_Init(7);
  max7219_Decode_On();
  /* USER CODE END 2 */

В безкінечному циклі друкуємо від'ємне число Pi. Та слово HELP. Чекаємо 1.5 секунди, очищаємо дисплей та друкуємо число 765 на 4-ри розряди (додається незначащий нуль), та ціле число 321. Знову чекаємо 1.5 секунди і все з початку.
/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   max7219_Clean();
   max7219_PrintFtos(DIGIT_8, -3.14, 2);
   max7219_PrintDigit(DIGIT_4, LETTER_H, false);
   max7219_PrintDigit(DIGIT_3, LETTER_E, false);
   max7219_PrintDigit(DIGIT_2, LETTER_L, false);
   max7219_PrintDigit(DIGIT_1, LETTER_P, false);
   HAL_Delay(1500);
   max7219_Clean();
   max7219_PrintNtos(DIGIT_8, 765, 4);
   max7219_PrintItos(DIGIT_3, 321);
   HAL_Delay(1500);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
Щоб друкувати довільні символи, то треба вимкнути режим декодування цифр і в масив static uint8_t SYMBOLS[] файлу max7219.c додати ці символи. А в нумерований список MAX7219_Numeric в файлі max7219.h додати позначення і порядковий номер в масиві - нового символу.

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

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

субота, 11 травня 2019 р.

STM32: Бібліотека OneWire і Dallas Temperature HAL по UART з DMA

update 04.01.2021: повністю оновлена стаття, переписана бібліотека, приклади, додано відео.

update 10.12.2020: завдяки коментаторам додав корективи. Зазначив, що для UART на якому будуть сенсори, крім DMA, потрібно ще увімкнути глобальні переривання "USART global interrupt". Та в файлі OneWire.c поміняв місцями функції:
HAL_UART_Receive_DMA(&HUARTx, ow_buf, num_bits);
HAL_UART_Transmit_DMA(&HUARTx, ow_buf, num_bits);

Первіша має бути функція приймання. Сподіваюсь це допоможе. Бо в мене і так і так все нормально працювало.

Передмова

Макет

Бібліотека OneWire створена за мотивами статей "Stm32 + 1-wire + DMA (продолжение)" та "STM32 + 1-wire. Поиск устройств". А бібліотека Dallas Temperature клон бібліотеки "DallasTemperature" для Arduino. За винятком: не працює з "Alarm" - не розібрався, не реалізована робота з паразитним живленням датчиків, та без перевірки на всі нулі функцією "IsAllZeros". У файлі "dallastemperature.c" в функції DT_IsConnected_ScratchPad закоментована перевірка "IsAllZeros". В іншому все працює коректно.

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

Запускаємо CubeIDE, обираємо свій мікроконтролер, або дошку. Даємо назву проекту, та налаштовуємо проект:
Налаштування проекту
  1. RCC - тактуємось від зовнішнього кварцу "Crystal/Ceramic resonator";
  2. SYS - відладка по "Serial Wire";
  3. USART1 або USART3 - обираємо режим "Single Wire (Half-Duplex)" та додаємо канали DMA як для прийому так і для передачі, а також вмикаємо глобальні переривання "USART global interrupt";
  4. USART2 - за замовчуванням, для передачі даних температури на послідовний порт комп'ютера.
Генеруємо проект, та додаємо файли бібліотеки до проекту. OneWire.h і DallasTemperature.h до теки "inc" проекту, а файли OneWire.с і DallasTemperature.с до теки "src" проекту.

В файлі DallasTemperature.h за замовчуванням, на лінії 1-Wire йде пошук, максимум 5 сенсорів. Змінити можна в рядку:

#define ONEWIRE_MAX_DEVICES	5

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

Схема підключення стандартна:
Схема підключення

Живлення, земля, дата + підтяжка до живлення через резистор 4.7 - 5.6 кОм. Ніжка "DQ" до USART1 (PA9) або USART3 (PC10) .

Демо-код

Відкриваємо файл проекту main.c та додаємо заголовні файли бібліотек до коду:
/* USER CODE BEGIN Includes */
#include "OneWire.h"
#include "DallasTemperature.h"
/* USER CODE END Includes */

Додаємо функцію відправки тексту по USART2 та функцію друкування адреси сенсорів:
/* Private user code ---------------------------------------------------------*/
/* 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;
}

// function to print a device address
void printAddress(CurrentDeviceAddress deviceAddress)
{
  for (uint8_t i = 0; i < 8; i++)
  {
	  printf("0x%02X ", deviceAddress[i]);
  }
}
/* USER CODE END 0 */

Приклад розрахований для двох DS18B20 на одній лінії USART3. Як у вас один сенсор, видаліть код для другого сенсора з індексом 1. Та якщо у вас лінія 1-wire не на USART3, то зробіть відповідні зміни в демо-коді.

Оголошуємо структури для OneWire та для DallasTemperature:
/* USER CODE BEGIN PV */
OneWire_HandleTypeDef ow;
DallasTemperature_HandleTypeDef dt;
/* USER CODE END PV */

В секцію 2 додаємо перевірку чи на лінії є якісь сенсори 1-Wire, взнаємо кількість підключених до лінії сенсорів, задаємо роздільну здатність сенсорів, та прочитаємо їхню адресу і поточну роздільну здатність:
/* USER CODE BEGIN 2 */
  printf("[%8lu] Debug UART2 is OK!\r\n", HAL_GetTick());

  OW_Begin(&ow, &huart3);

  if(OW_Reset(&ow) == OW_OK)
  {
	  printf("[%8lu] OneWire devices are present :)\r\n", HAL_GetTick());
  }
  else
  {
	  printf("[%8lu] OneWire no devices :(\r\n", HAL_GetTick());
  }

  DT_SetOneWire(&dt, &ow);

  // arrays to hold device address
  CurrentDeviceAddress insideThermometer;

  // locate devices on the bus
  printf("[%8lu] Locating devices...", HAL_GetTick());

  DT_Begin(&dt);

  printf("[%8lu] Found %d devices.\r\n", HAL_GetTick(), DT_GetDeviceCount(&dt));

  if (!DT_GetAddress(&dt, insideThermometer, 0))
	  printf("[%8lu] Unable to find address for Device 0\r\n", HAL_GetTick());

  printf("[%8lu] Device 0 Address: ", HAL_GetTick());
  printAddress(insideThermometer);
  printf("\r\n");

  // set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
  DT_SetResolution(&dt, insideThermometer, 12, true);

  printf("[%8lu] Device 0 Resolution: %d\r\n", HAL_GetTick(), DT_GetResolution(&dt, insideThermometer));

  if (!DT_GetAddress(&dt, insideThermometer, 1))
	  printf("[%8lu] Unable to find address for Device 1\r\n", HAL_GetTick());

  printf("[%8lu] Device 1 Address: ", HAL_GetTick());
  printAddress(insideThermometer);
  printf("\r\n");

  // set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
  DT_SetResolution(&dt, insideThermometer, 12, true);

  printf("[%8lu] Device 1 Resolution: %d\r\n", HAL_GetTick(), DT_GetResolution(&dt, insideThermometer));
  /* USER CODE END 2 */

Приклад розрахований для двох DS18B20 на одній лінії. Як у вас один сенсор, видаліть код для другого сенсора з індексом 1.

А в безкінечному циклі раз на 2 секунди будемо опитувати сенсори і виводити в послідовний порт отриману температуру:

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	// call DT_RequestTemperatures(&dt) to issue a global temperature
	// request to all devices on the bus
	printf("[%8lu] Requesting temperatures...", HAL_GetTick());
	DT_RequestTemperatures(&dt); // Send the command to get temperatures
	printf("\r\n[%8lu] DONE\r\n", HAL_GetTick());
	// After we got the temperatures, we can print them here.
	// We use the function ByIndex, and as an example get the temperature from the first sensor only.
	printf("[%8lu] Temperature for the device 1 (index 0) is: %.2f\r\n", HAL_GetTick(), DT_GetTempCByIndex(&dt, 0));
	printf("[%8lu] Temperature for the device 2 (index 1) is: %.2f\r\n", HAL_GetTick(), DT_GetTempCByIndex(&dt, 1));
	HAL_Delay(2000);
    /* USER CODE END WHILE */

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

Щоб функція "printf" могла виводити значення типу "float" потрібно відповідно налаштувати проект в CubeIDE, а саме додати ключ "-u _printf_float":

Налаштування друку float

І для Atolic TrueStudio обрати "NewLib standart":

Налаштування для друку float

Компілюємо, заливаємо. Запускаємо будь-яку термінальну програму і спостерігаємо:

Приклад роботи двох датчиків на лінії

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

Текст коду з відео:

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "OneWire.h"
#include "DallasTemperature.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
OneWire_HandleTypeDef ow1;
DallasTemperature_HandleTypeDef dt1;
OneWire_HandleTypeDef ow2;
DallasTemperature_HandleTypeDef dt2;
/* USER CODE END PV */

/* Private user code ---------------------------------------------------------*/
/* 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;
}

// function to print a device address
void printAddress(CurrentDeviceAddress deviceAddress)
{
  for (uint8_t i = 0; i < 8; i++)
  {
	  printf("0x%02X ", deviceAddress[i]);
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
printf("[%8lu] Debug UART2 is OK!\r\n", HAL_GetTick());

OW_Begin(&ow1, &huart1);
OW_Begin(&ow2, &huart3);

if(OW_Reset(&ow1) == OW_OK)
{
  printf("[%8lu] OneWire 1 line devices are present :)\r\n", HAL_GetTick());
}
else
{
  printf("[%8lu] OneWire 1 line no devices :(\r\n", HAL_GetTick());
}

if(OW_Reset(&ow2) == OW_OK)
{
  printf("[%8lu] OneWire 2 line devices are present :)\r\n", HAL_GetTick());
}
else
{
  printf("[%8lu] OneWire 2 line no devices :(\r\n", HAL_GetTick());
}

DT_SetOneWire(&dt1, &ow1);
DT_SetOneWire(&dt2, &ow2);

// arrays to hold device address
CurrentDeviceAddress insideThermometer;

// locate devices on the bus
printf("[%8lu] 1-line Locating devices...\r\n", HAL_GetTick());
DT_Begin(&dt1);
uint8_t numDevOneLine = DT_GetDeviceCount(&dt1);
printf("[%8lu] 1-line Found %d devices.\r\n", HAL_GetTick(), numDevOneLine);

printf("[%8lu] 2-line Locating devices...\r\n", HAL_GetTick());
DT_Begin(&dt2);
uint8_t numDevTwoLine = DT_GetDeviceCount(&dt2);
printf("[%8lu] 2-line Found %d devices.\r\n", HAL_GetTick(), numDevTwoLine);

for (int i = 0; i < numDevOneLine; ++i)
{
	if (!DT_GetAddress(&dt1, insideThermometer, i))
		  printf("[%8lu] 1-line: Unable to find address for Device %d\r\n", HAL_GetTick(), i);
	printf("[%8lu] 1-line: Device %d Address: ", HAL_GetTick(), i);
	printAddress(insideThermometer);
	printf("\r\n");
	// set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
	DT_SetResolution(&dt1, insideThermometer, 12, true);
	printf("[%8lu] 1-line: Device %d Resolution: %d\r\n", HAL_GetTick(), i, DT_GetResolution(&dt1, insideThermometer));
}

for (int i = 0; i < numDevTwoLine; ++i)
{
	if (!DT_GetAddress(&dt2, insideThermometer, i))
		printf("[%8lu] 2-line: Unable to find address for Device %d\r\n", HAL_GetTick(), i);
	printf("[%8lu] 2-line: Device %d Address: ", HAL_GetTick(), i);
	printAddress(insideThermometer);
	printf("\r\n");
	// set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
	DT_SetResolution(&dt2, insideThermometer, 12, true);
	printf("[%8lu] 2-line: Device %d Resolution: %d\r\n", HAL_GetTick(), i, DT_GetResolution(&dt2, insideThermometer));
}
/* USER CODE END 2 */

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	// call sensors.requestTemperatures() to issue a global temperature
	// request to all devices on the bus
	printf("[%8lu] 1-line: Requesting temperatures...", HAL_GetTick());
	DT_RequestTemperatures(&dt1); // Send the command to get temperatures
	printf("\r\n[%8lu] 1-line: DONE\r\n", HAL_GetTick());

	printf("[%8lu] 2-line: Requesting temperatures...", HAL_GetTick());
	DT_RequestTemperatures(&dt2); // Send the command to get temperatures
	printf("\r\n[%8lu] 2-line: DONE\r\n", HAL_GetTick());

	for (int i = 0; i < numDevOneLine; ++i)
	{
		// After we got the temperatures, we can print them here.
		// We use the function ByIndex, and as an example get the temperature from the first sensor only.
		printf("[%8lu] 1-line: Temperature for the device %d (index %d) is: %.2f\r\n", HAL_GetTick(), i, i, DT_GetTempCByIndex(&dt1, i));
	}

	for (int i = 0; i < numDevTwoLine; ++i)
	{
		// After we got the temperatures, we can print them here.
		// We use the function ByIndex, and as an example get the temperature from the first sensor only.
		printf("[%8lu] 2-line: Temperature for the device %d (index %d) is: %.2f\r\n", HAL_GetTick(), i, i, DT_GetTempCByIndex(&dt2, i));
	}

	HAL_Delay(DT_MillisToWaitForConversion(DT_GetAllResolution(&dt1)));
    /* USER CODE END WHILE */

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