STM32F407 Урок 07. Работаем с OLED дисплеем SH1106 по шине SPI.
Здравствуйте, уважаемые читатели. Этот урок получился внеплановым, прошу прощения у тех кто ждет рассказ о Ka-Radio, следующая статья будет обязательно о нем. Пришел ко мне 1,3'' OLED экранчик по демократической цене около 200руб, первое, что бросилось в глаза - надпись SH1106 вместо SSD1306. Поиск в интернете прояснил, что это практически то-же самое, только оставлен единственный страничный режим адресации, да и тот ограничен одной строкой.
И так, настроим с самого начала, чтобы при использовании подобного дисплея в проектах - потом ссылаться на этот урок. Выберем в CubeMX наш STM32F407VE кристалл, обязательно включим режим отладки, выберем резонаторы 8MHz, частоту работы кристалла 168MHz от HSE. При желании можете включить USB как виртуальный COM и светодиоды для контроля прохождения проблемных точек кода, настройку можете посмотреть в обзоре и первом уроке.
Вы можете выбрать любой, а мне по душе SPI3. Выберем его мастером на передачу в DMA режиме и активируем прерывания, настройки будут такие :
Теперь настроим лапки управляющих сигналов DC (данные/команда), RESET (аппаратный сброс) и CS (выбор дисплея) :
Увеличим Heap и Stack в 2 раза и создадим проект для Atollic, и выберем его запуск. С вами создадим библиотеку, которую потом будем подключать к своим проектам. В левом окне раскроем папку Src нашего проекта и в меню выберем File->Source File, введем имя нашей библиотеки spi1106.c аналогично создадим File->Header File с именем spi1106.h. Последний перенесем из папки Src в Inc и откроем для редактирования, между #define ic1306_H_ и #endif /* ic1306_H_ */ определим короткие команды для управлением сигналами CD, RESET и CS и функцию инициализации экрана :
#define SPI1106_H_
#include "main.h"
void sh1106Init (uint8_t contrast, uint8_t bright, uint8_t mirror);
#define SH_Command HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET)
#define SH_Data HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET)
#define SH_ResHi HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_SET)
#define SH_ResLo HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_RESET)
#define SH_CsHi HAL_GPIO_WritePin(GPIOA, CS_Pin, GPIO_PIN_SET)
#define SH_CsLo HAL_GPIO_WritePin(GPIOA, CS_Pin, GPIO_PIN_RESET)
#endif /* SPI1106_H_ */
В файле spi1106.c начнем создавать наши функции, вначале приинклудим main.h, хэдер нашей библиотеки и выбранного канала SPI дисплея :
#include "main.h"
#include <spi1106.h>
extern SPI_HandleTypeDef hspi3;
Напишем функции пересылки кода команды и байта данных :
void SH1106_WC (uint8_t comm)
{
uint8_t temp[1];
SH_Command;
SH_CsLo;
temp[0]=comm;
HAL_SPI_Transmit(&hspi3,&temp,1,1);
SH_CsHi;
}
void SH1106_WD (uint8_t data)
{
uint8_t temp[1];
SH_Data;
SH_CsLo;
temp[0]=data;
HAL_SPI_Transmit(&hspi3,&temp,1,1);
SH_CsHi;
}
Функция инициализации будет выглядить следующим образом :
void sh1106Init (uint8_t contrast, uint8_t bright,uint8_t mirror)
{
SH_ResLo;
HAL_Delay(1);
SH_ResHi;
HAL_Delay(1);
SH1106_WC(0xAE); //display off
SH1106_WC(0xA8); //--set multiplex ratio(1 to 64)
SH1106_WC(0x3F); //
SH1106_WC(0x81); //--set contrast control register
SH1106_WC(contrast);
if (mirror) {SH1106_WC(0xA0);
SH1106_WC(0xC0);}
else {SH1106_WC(0xA1);
SH1106_WC(0xC8); }
SH1106_WC(0xDA);
SH1106_WC(0x12);
SH1106_WC(0xD3);
SH1106_WC(0x00);
SH1106_WC(0x40);
SH1106_WC(0xD9); //--set pre-charge period
SH1106_WC(bright);
SH1106_WC(0xAF); //--turn on SSD1306 panel
}
С ней, надеюсь все понятно, первый параметр - контрастность (0-255), второй - яркость (разбита на два полубайта, комбинации 0xX0 0x0X недопустимы), третий - ориентация дисплея (0/1). Для понимания работы дисплея и дальнейших функций вывода изображения советую почитать статью на Датагоре Визуализация для микроконтроллера. Часть 1. OLED дисплей 0.96" (128х64) на SSD1306 и переведенный на русский даташит SSD1306 на Microsin.net.
До бесконечного цикла в main.c проведем инициализацию дисплея :
/* USER CODE BEGIN 2 */
sh1106Init (40,0x22,0);
/* USER CODE END 2 */
На экране увидите графические узоры типа таких :
Таблица соединений :
SH1106 ---- STM32F407- GND ----- GND
- VDD ----- 3V3
- SCK ----- PC10
- SDA ----- PC12
- RES ----- PD0
- DC ------ PC11
- CS ------ PA15
В файле spi1106.h напишем определение сразу еще трех функций - очистки, печати мелким и средним шрифтом :
void sh1106Init (uint8_t contrast, uint8_t bright, uint8_t mirror);
void sh1106Clear(uint8_t start, uint8_t stop);
void sh1106SmallPrint(uint8_t posx, uint8_t posy, uint8_t *str);
void sh1106MediumPrint(uint8_t posx, uint8_t posy,uint8_t *str);
#endif /* SPI1106_H_ */
В функции очистки в файле spi1106.c выбор страницы с 0-ой по 7-ю осуществляется командой 0xB0...0xB7 и будет выглядеть так :
void sh1106Clear(uint8_t start, uint8_t stop)
{ uint32_t *adrclear;
uint32_t timep,timec;
uint8_t dt[128];
adrclear=(uint32_t *)dt;
for(uint8_t i=0;i<32;i++) {*adrclear++=0x00;}
for (uint8_t m = start; m <= stop; m++)
{
SH1106_WC(0xB0+m);
SH1106_WC(2);
SH1106_WC(0x10);
SH_Data;
SH_CsLo;
HAL_SPI_Transmit_DMA(&hspi3,dt,128);
timec=HAL_GetTick();
timep=timec+50;
while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timep))
{timec=HAL_GetTick();}
SH_CsHi;
}
}
До основного цикла main.c, после инициализации, вставим очистку :
/* USER CODE BEGIN 2 */
sh1106Init (40,0x22,0);
sh1106Clear(0,7);
/* USER CODE END 2 */
Можете поиграться с очиткой, задав заполнение своим узором, мы же двигаем далее, вставим в файл spi1106.c ближе к началу сперва маленький шрифт из файла DefaultFonts.c (в начале массива символов необходимо удалить ненужные нам 4-е служебных байта) и напишем функцию печати маленьким шрифтом :
void sh1106SmallPrint(uint8_t posx, uint8_t posy, uint8_t *str)
{
uint8_t dt[128];
uint16_t posfont, posscr;
uint32_t *adrclr;
uint16_t *adrdst,*adrsrc;
uint32_t timer,timec;
adrclr=(uint32_t *)&dt;
uint8_t code;
code=*str++;
for(uint8_t i=0;i<32;i++) { *adrclr++=0; }
posscr=posx*6;
while (code>31)
{
if(code==32) {posscr+=2;}
else
{posfont=6*(code-32);
adrdst=(uint16_t *)&dt[posscr];
adrsrc=(uint16_t *)&SmallFont[posfont];
*(adrdst++)=*(adrsrc++);
*(adrdst++)=*(adrsrc++);
*(adrdst++)=*(adrsrc++);
posscr+=6;
}
code=*str++;
if (posscr>122) break;
}
SH1106_WC(0xB0+posy);
SH1106_WC(2);
SH1106_WC(0x10);
SH_Data;
SH_CsLo;
HAL_SPI_Transmit_DMA(&hspi3,dt,128);
timec=HAL_GetTick();
timer=timec+50;
while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timer))
{timec=HAL_GetTick();}
SH_CsHi;
}
В main.c после очистки напечатаем приветствие :
sh1106Clear(0,7);
sh1106SmallPrint(0,0,(uint8_t *) "Hello SH1106");
/* USER CODE END 2 */
Затем, в билиотеке spi1106.c после мелкого шрифта вставим средний из того-же файла DefaultFonts.c (также удалите 4 служебных байта) и напишем функцию печати средним шрифтом :
void sh1106MediumPrint(uint8_t posx, uint8_t posy, uint8_t *str)
{ uint8_t dt[256];
uint16_t posfont, posscr;
uint32_t *adrdst, *adrsrc;
uint32_t timer,timec;
adrdst=(uint32_t *)&dt;
uint8_t code;
code=*str++;
for(uint8_t i=0;i<64;i++) { *adrdst++=0; }
posscr=posx*12;
while (code>31)
{posfont=24*(code-32);
adrsrc=(uint32_t *)&MediumFont[posfont];
adrdst=(uint32_t *)&dt[posscr];
*(adrdst++)=*(adrsrc++);
*(adrdst++)=*(adrsrc++);
*(adrdst++)=*(adrsrc++);
adrsrc=(uint32_t *)&MediumFont[posfont+12];
adrdst=(uint32_t *)&dt[posscr+128];
*(adrdst++)=*(adrsrc++);
*(adrdst++)=*(adrsrc++);
*(adrdst++)=*(adrsrc++);
code=*str++;
posscr+=12;
if (posscr>116) break;
}
SH1106_WC(0xB0+posy);
SH1106_WC(2);
SH1106_WC(0x10);
SH_Data;
SH_CsLo;
HAL_SPI_Transmit_DMA(&hspi3,dt,128);
timec=HAL_GetTick();
timer=timec+50;
while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timer))
{timec=HAL_GetTick();}
SH1106_WC(0xB0+posy+1);
SH1106_WC(2);
SH1106_WC(0x10);
SH_Data;
SH_CsLo;
HAL_SPI_Transmit_DMA(&hspi3,dt+128,128);
timec=HAL_GetTick();
timer=timec+50;
while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timer))
{timec=HAL_GetTick();}
SH_CsHi;
}
В main.c напечатаем средним шрифтом под мелким :
sh1106SmallPrint(0,0,(uint8_t *) "Hello SH1106_1234567890");
sh1106MediumPrint(0,1,(uint8_t *) "Hi SH1106");
sh1106MediumPrint(0,3,(uint8_t *) "Hello SH1106");
/* USER CODE END 2 */
Как видно, библиотека корректно обрезает строку, не налезая на следующую и не вылетая в HardFault при выходе за выделенный буфер. Пока писал эту библиотеку - нашел ошибку в старой, перекачайте для дисплея SSD1306 I2C новую версию.
Можно было написать еще функцию вывода больших символов и графические примитивы, но честно говоря мне рассмотренных пока хватает, оставим это на домашнее задание для вас.
А на последок померяем скорость обновления всего экрана, в главном цикле напишем такую программу :
/* Infinite loop */
/* USER CODE BEGIN WHILE */
uint8_t buf[128*8];
char str[32];
uint16_t count;
uint8_t x,y,b;
uint32_t timep,timec;
while (1)
{
count++;
b=count&0x07;
x=(count>>3)&0x7f;
y=(count>>10)&0x07;
buf[y*128+x]=buf[y*128+x]|(1<<b);
timec=HAL_GetTick();
for (uint8_t m = 0; m < 7; m++)
{SH1106_WC(0xB0+m);
SH1106_WC(2);
SH1106_WC(0x10);
SH_Data;
SH_CsLo;
HAL_SPI_Transmit_DMA(&hspi3,buf+m*128,128);
while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY))
{__NOP();}
SH_CsHi;
}
timep=HAL_GetTick();
sprintf(str, "%d", timep-timec);
sh1106SmallPrint(0,7,str);
/* USER CODE END WHILE */
По скорости заполнения - примерно одна строка в секунду и мелькающим в нижней строке то "0" то "1" можно сказать, что достигнута скорость обновления семи строк экрана менее 1ms, т.е. на нормальном языке более 1000fps. Конечно с учетом реальных задач построения изображения в буфере, будет медленнее, но результат впечатляет.
Как всегда, в разделе Файлы вы можете скачать архив библиотеки там же файл с русским шрифтом, а если Вам есть что добавить :
Адрес для контактов : imax9@narod.ru
Если вам понравились мои работы и вы желаете поддержать сайт - сделайте дотацию.
При копировании статьи – обязательна ссылка на авторство и источник. Без разрешения автора копирование запрещено.
© Максим Ильин 2020г.