четвер, 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 з 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

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

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