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

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

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

25 коментарів:

  1. Здравствуйте, не могу запустить функцию void ssd1306_Image(uint8_t *img, uint8_t frame, uint8_t x, uint8_t y); // вивід зображення, остальные вроде работают.
    Создал массив uint8_t test [] = {данные}; с картинкой при помощи LCDAssistant, вставил в функцию, но ничего не отображается. И не понимаю что за переменная "frame". Пробовал менять переменную на const unsigned char test [] = {};, все равно ничего не выходит.

    ВідповістиВидалити
    Відповіді
    1. На жаль я не перевіряв цю функцію. Закінчу з драйвером для ST7735, все перевірю і гляну на цю проблему.

      Видалити
    2. Может она и работает, я просто не пойму как использовать, на экране даже мусора нет. И в интернете примеров тоже не нашел. В Main.c функцию по разному объявлял, для проверки что не зависает контроллер использовал рабочие функции. Отладчик функцию читает, но я не умею с ним работать, только начал заниматься баловством с МК. Буфер в таком виде uint8_t test [] = {0x00, 0x1C, 0x36, 0xC3.....0x36, 0xC3};.
      Функция в таком void ssd1306_Image(test, 128, 0, 0); Про Frame выше писал, но значение ставил по количеству данных в массиве. После функции ssd1306_UpdateScreen();, но все тихо. Буду очень благодарен за помощь.

      Видалити
    3. Можливо цей допис допоможе розібратись?
      https://forum.arduino.cc/index.php?topic=434348.0
      Там зазначено що біти для растрового зображення мають бути навпаки: наприклад 1000101 має бути 1010001
      А frame номер кадру в масиві кадрів, для анімації, як кадр один, то напевно має бути там 0. Ось приклад з frame:
      http://forum.arduino.cc/index.php?topic=375985.0

      Видалити
    4. Не получилось с родной функцией вывести изображение, нашел рабочую из другого примера, она с начала мусор выводила и оказалось пиксели по вертикали выстраиваются.
      https://github.com/LonelyWolf/stm32/tree/master/SSD1306 LCD_DrawBitmap();

      Почему родная функция не выводит изображение не понял.

      Видалити
    5. Добре. Я трішки пізніше розберусь що до чого і оновлю бібліотеку. Можливо додам режим DMA.

      Видалити
  2. Відповіді
    1. Шановний Віталій.
      Оновив статтю і драйвер.
      Будь ласка до ознайомлення.

      Видалити
    2. Большое спасибо! Попробую в ближайшее время!

      Видалити
  3. Спасибо за помощь.Хотел бы скромно отблагодарить за труд,скиньте номер карточки при возможности.

    ВідповістиВидалити
    Відповіді
    1. Будь ласка. РаДію, що допоміг вам. Вгорі є кнопка "Підтримати проект". Там буде форма від "Monobank", або 5168757319219764, Гончаренко Андрій.

      Видалити
  4. Создание Веб-сервера разложите нам по полочкам,пожалуйста!

    ВідповістиВидалити
    Відповіді
    1. Дякую за таку довіру. Але я ще не досвіду з мережевими технологіями на STM32. Хоча ця тема мені цікава.

      Видалити
  5. Отличная и довольно детально обдуманная работа.
    Но исходник, библиотеки, который вы, выложили на droupbox, никоим образом скачать нельзя, ибо защищен от копирования!

    ВідповістиВидалити
    Відповіді
    1. Скористайтесь ланкою на GitHub https://github.com/taburyak/STM32_OLED_SSD1306_HAL_DMA
      Щодо DropBox скарг не було. По можливості перевірю.

      Видалити
  6. Будьласка хелп. помилка undefined reference to `Font_7x10'

    ВідповістиВидалити
  7. Мало інформації. Ваш код допоміг би. В свій код додали #include "ssd1306.h"?

    ВідповістиВидалити
  8. Здравствуйте!
    SPL библиотеку надо подключать?

    ВідповістиВидалити
  9. Добрый день!А что за резисторы возле дисплея на фото?

    ВідповістиВидалити
  10. Доброго дня! Все працює, поки не додаю DMA. DMA на TX додав, переривання увімкнув, рядок розкоментував. На єкрані - нічого. Без DMA все працює. Може я щось забув?

    P.S. Я прибрав додаткові функції типу drawCircle і демо скоротив тільки до ініціалізації і малювання бітмапи - без DMA малює.
    Дякую!

    P.P.S
    #elif SSD1306_128X32
    прийшлося замінити на
    #elif defined(SSD1306_128X32)
    інакше компілятор сперечався)

    ВідповістиВидалити
    Відповіді
    1. Пройшовся дебаггером.
      Першу команду ініціалізації дисплея (ssd1306_WriteCommand(DISPLAYOFF)) проходить норм, навіть повертає HAL_OK. Але після виходу з цієї функції одразу випадає у void HardFault_Handler(void)...

      Видалити
  11. Вітаю!
    Не можу під'єднати 1.3" OLED Waveshare SH1106 до STM32F4DISCOVERY по I2C. Один раз вдалося запустити тест, потім втратив проект більше нічого не виходить. Перепробував все. Наштовхніть на думку. Я початківець, використовую Куб.
    Дякую. Валерій. м.Кропивницький.

    ВідповістиВидалити
  12. чи може бути таке, що на STM32F103C8T6 існує глюк з шиною ?
    пробував і2с1 - взагалі мовчить. Включив і2с2 - шина запрацювала, але подивився в логічний аналізатор - там постійно тільки 0х78 посилає (це адресса, яка написана на моєму ssd1306). Почитав, є певна еррата з цього приводу. Але з нею в мене чіп зависає.
    (HAL + CubeIDE 1.5.0 under ubuntu)

    ВідповістиВидалити
  13. Спробував цей проект без DMA на STM32F411 (чорна таблетка). Придбав на arduino.ua https://arduino.ua/prod3985-otladochnaya-plata-stm32f411-neraspayannaya
    Використав CubeIDE без інших засобів розробки. Все працює! Щиро дякую!

    ВідповістиВидалити