Здравствуйте, уважаемые читатели. В своих разработках на микроконтроллерах STM32, для вывода осмысленной информации, я пользуюсь OLED дисплеями на чипе SSD1306. В последний раз пришел ко мне 1,3" дисплей по демократичной цене — около 200руб. Первое, что бросилось в глаза — надпись SH1106 вместо SSD1306, поиск в интернете прояснил, что это практически тоже самое, только оставлен единственный страничный режим адресации, да и тот ограничен одной строкой. Как с ним работать я и постараюсь объяснить вам в этой публикации.
Где-то с год назад мне стало не хватать возможностей синей пилюли (STM32F103) и была заказана китайская плата разработчика STM32F407VE. Для отладки, часто, двух светодиодов не хватает, поэтому в каждом проекте для вывода информации подключаю OLED SSD1306 по шине I2C, в который влюбился еще со времен Arduino. Так как графику я на него не вывожу, в основном числа и текст, а размер готовых библиотек и их содержание поражало мое воображение, была написана небольшая библиотечка, которую я немного адаптировал под SH1106 и хочу поделится с вами процессом ее написания. Дисплей приехал 7pin SPI:
Плата разработчика у меня такая, но ничего вам не помешает подключить к другой, хоть на STM32F103, для чего HAL и был придуман (разве не так ?):
Выберем в CubeMX наш STM32F407VE кристалл, обязательно включим режим отладки, иначе потом перешивать придется через UART1. В Clock Configuration выберем резонаторы 8MHz и зададим частоту работы кристалла 168MHz от HSE. При желании можете сконфигурировать на выход ножки PA6 и PA7, к которым подключены светодиоды D2 и D3 (загораются подачей лог «0») для контроля прохождения проблемных точек кода:
На плате SPI1 выведен на колодку NRF24L01 и к нему у меня подключен ESP-PSRAM64H, для дисплея остался SPI3. Выберем его мастером на передачу в DMA режиме и активируем прерывания, настройки будут такие :
Теперь настроим ножки управляющих сигналов DC (данные/команда), RESET (аппаратный сброс) и CS (выбор дисплея) :
Таблица соединений :
SH1106 — STM32F407- GND — GND
- VDD — 3V3
- SCK — PC10
- SDA — PC12
- RES — PD0
- DC — PC11
- CS — PA15
Увеличим 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;
Напишем функции пересылки кода команды через SPI при помощи библиотеки HAL:
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 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.
До бесконечного цикла в main.c проведем инициализацию дисплея :
/* USER CODE BEGIN 2 */
sh1106Init (40,0x22,0);
/* USER CODE END 2 */
На экране увидите графические узоры типа таких :
В файле spi1106.h напишем определение сразу еще трех функций — очистки, печати мелким и средним шрифтом :
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);
В функции очистки в файле 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, после инициализации, вставим очистку :
sh1106Clear(0,7);
Можете поиграться с очисткой, задав заполнение своим узором, мы же двигаем далее, вставим в файл spi1106.c ближе к началу сперва маленький шрифт из файла DefaultFonts.c (взял из какой-то ардуиновской библиотеки). Особенностью данного шрифта является, то что он вертикально ориентирован (как раз под структуру отображения байта дисплеями SSD1306/SH1106). В начале массива символов необходимо удалить ненужные нам 4-е служебных байта. Весь фонт вы найдете в архиве библиотеки, в качестве примера, приведу лишь начало:
const uint8_t SmallFont[] = // Шрифт SmallFont
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 001) 0x20=032 пробел
0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, // 002) 0x21=033 !
0x00, 0x00, 0x07, 0x00, 0x07, 0x00, // 003) 0x22=034 "
0x00, 0x14, 0x7F, 0x14, 0x7F, 0x14, // 004) 0x23=035 #
0x00, 0x24, 0x2A, 0x7F, 0x2A, 0x12, // 005) 0x24=036 $
0x00, 0x23, 0x13, 0x08, 0x64, 0x62, // 006) 0x25=037 %
0x00, 0x36, 0x49, 0x55, 0x22, 0x50, // 007) 0x26=038 &
0x00, 0x00, 0x05, 0x03, 0x00, 0x00, // 008) 0x27=039 '
0x00, 0x00, 0x1C, 0x22, 0x41, 0x00, // 009) 0x28=040 (
0x00, 0x00, 0x41, 0x22, 0x1C, 0x00, // 010) 0x29=041 )
0x00, 0x14, 0x08, 0x3E, 0x08, 0x14, // 011) 0x2A=042 *
0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, // 012) 0x2B=043 +
0x00, 0x00, 0x00, 0xA0, 0x60, 0x00, // 013) 0x2C=044 ,
0x00, 0x08, 0x08, 0x08, 0x08, 0x08, // 014) 0x2D=045-
0x00, 0x00, 0x60, 0x60, 0x00, 0x00, // 015) 0x2E=046 .
0x00, 0x20, 0x10, 0x08, 0x04, 0x02, // 016) 0x2F=047 /
//
0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E, // 017) 0x30=048 0
0x00, 0x00, 0x42, 0x7F, 0x40, 0x00, // 018) 0x31=049 1
0x00, 0x42, 0x61, 0x51, 0x49, 0x46, // 019) 0x32=050 2
0x00, 0x21, 0x41, 0x45, 0x4B, 0x31, // 020) 0x33=051 3
0x00, 0x18, 0x14, 0x12, 0x7F, 0x10, // 021) 0x34=052 4
0x00, 0x27, 0x45, 0x45, 0x45, 0x39, // 022) 0x35=053 5
0x00, 0x3C, 0x4A, 0x49, 0x49, 0x30, // 023) 0x36=054 6
0x00, 0x01, 0x71, 0x09, 0x05, 0x03, // 024) 0x37=055 7
0x00, 0x36, 0x49, 0x49, 0x49, 0x36, // 025) 0x38=056 8
0x00, 0x06, 0x49, 0x49, 0x29, 0x1E, // 026) 0x39=057 9
0x00, 0x00, 0x36, 0x36, 0x00, 0x00, // 027) 0x3A=058 :
0x00, 0x00, 0x56, 0x36, 0x00, 0x00, // 028) 0x3B=059 ;
0x00, 0x08, 0x14, 0x22, 0x41, 0x00, // 029) 0x3C=060 <
0x00, 0x14, 0x14, 0x14, 0x14, 0x14, // 030) 0x3D=061 =
0x00, 0x00, 0x41, 0x22, 0x14, 0x08, // 031) 0x3E=062 >
0x00, 0x02, 0x01, 0x51, 0x09, 0x06, // 032) 0x3F=063 ?
//
0x00, 0x32, 0x49, 0x59, 0x51, 0x3E, // 033) 0x40=064 @
0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C, // 034) 0x41=065 A
0x00, 0x7F, 0x49, 0x49, 0x49, 0x36, // 035) 0x42=066 B
// ...
}
Напишем функцию печати маленьким шрифтом высотой одна страница (ширина 6, высота 8 точек), где posx знакоместо, кратное 6 пикселям дисплея, а posy номер страницы (0 — верхняя, 7 — нижняя):
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;
}
Затем, в библиотеке spi1106.c после мелкого шрифта вставим средний (высота 16, ширина 12 пикселей) из того же файла DefaultFonts.c (также удалите 4 служебных байта в начале массива) и напишем функцию печати средним шрифтом высотой две страницы, где posx знакоместо, кратное уже 12 пикселям дисплея, а posy аналогично предыдущей функции номер страницы:
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");
Как видно, библиотека корректно обрезает строку, не налезая на следующую и не вылетая в HardFault при выходе за выделенный буфер. Можно было написать еще функцию вывода больших символов и графические примитивы, но честно говоря, мне рассмотренных пока хватает, оставим это на домашнее задание для вас.
И в завершение определим скорость обновления всего экрана, в главном цикле напишем такую программу, которая заполняет по одному пикселю буфер экрана и затем обновляет 7 страниц и печатает на последней строке время в миллисекундах :
/* 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. Конечно с учетом реальных задач построения изображения в буфере, будет медленнее, но результат впечатляет. Да и задержку ожидания можно вставить до пересылки по SPI и распаралелить задачи отрисовки МК и перекидывания по SPI в режиме DMA.
Надеюсь данная публикация будет полезна вам для изучения алгоритма работы с данным дисплеем.
AVI-crak
Наглядный пример того — как не нужно делать.
Уровень железа: инициализация интерфейсов, простые функции записи/чтения для регистров жк экрана. Этот слой всегда уникальный, как для мк, так и для жк экрана. Не нужно надеяться на халл, имеет смысл написать в быстром cmsis варианте — с прямым обращением к регистрам. Но даже этот слой можно поделить на две части, чтобы потом меньше напрягаться.
Отдельно: запись одной цветной точки, запись прямоугольника, очистка цветом, работа со слоями, простейшая математика. Нет регистров, нет абстракций — у вас набор функций для доступа к жк от железного уровня. Впрочем жк экраны бывают разными, большими и маленькими. Этот слой будет отличаться для экранов с внутренней и внешней видеопамятью. Просто два набора функций.
Сверху: печать текста, линии, кривые, векторная графика, и так далее. Вот это уже можно написать всего один раз, и использовать во всех своих проектах.
При таком подходе замена жк экрана на другую модель занимает максимум пол часа — написать несколько функций уровня железа. А иногда они уже готовые попадаются в интернете — достаточно просто переименовать для своих целей имена функций.
imax9 Автор
Не хочется разжигать холивар HAL vs CMSIS. Умею работать с тем и другим. Хотелось бы услышать поконкретней, какая именно функция HAL является критической по скорости чтобы ее переписывать на регистры?
AVI-crak
Пальпация слона с целью обнаружения болезни — весьма затратное предприятие, он толстый везде. Но саму проблему я указал более чем точно — тотальное разделение.
На отдельном слое работы с примитивами — HAL и CMSIS должен отсутствовать!!! У вас для этого есть нижний слой — набор функций работы с железом.
Чтобы добавить дополнительный слой графики — достаточно написать новый примитив.
Чтобы печатать текст — нужна отдельная функция которая печатает символы. Текст всегда одинаковый, а вот символы (формат хранения) бывают разными.
Сколько времени у вас займет переписывание кода для возможности печатать разным шрифтом, добавить слой графики, сменить жк экран, сменить мк???
imax9 Автор
>Пальпация слона с целью обнаружения болезни — весьма затратное предприятие, он толстый везде.
Вот тут, я с Вами согласен, избыточность HAL иногда просто поражает.
>Но саму проблему я указал более чем точно — тотальное разделение.
Это не проблема, прочитайте внимательней, целью написать не универсальную библиотеку для дисплея, а вывод цифр, текста, псевдографики (при желании) как можно быстрее и меньше это занимало.
>На отдельном слое работы с примитивами — HAL и CMSIS должен отсутствовать!!! У вас для этого есть нижний слой — набор функций работы с железом.
Т.е. Вы предлагаете рисовать все по точкам и на низком уровне перебрасывать массив? Если бы я хотел написать универсальную библиотеку под разные дисплеи это бы имело смысл.
>Сколько времени у вас займет переписывание кода для возможности печатать разным шрифтом, добавить слой графики, сменить жк экран, сменить мк???
Тут я совсем перестал вас понимать, возможно, уровня моего просветления не хватает. Вы хотите сказать что переход на другой процессор на CMSIS легче и быстрее чем HAL? Про слои тоже уже звучит как мантра.
AVI-crak
Пять простых функций уровня железа:
Одиночная команда без данных, команда с фиксированным количеством данных (два байта), команда с данными переменной длинны, только данные переменной длинны, поднять CS. Последнюю команду можно не использовать, но придётся добавить параметр в две предыдущих. Вот здесь можно использовать дма, и простейшую машину состояний — чтобы не отдавить себе хвост.
Примитивы эксплуатируют команды самого дисплея, буквально командуют дисплеем. При этом графика получается самая простая: нарисовать точку, заполнить прямоугольник, и очистить цветом и так далее. Это позволяет менять дисплеи — переписывая немногочисленные примитивы. Либо использовать уровень абстракции под все дисплеи (почти), благо механика у них практически одинаковая. У вас будет набор #define с абстрактными командами дисплея, куда нужно вписать уже реальные команды. Этот процесс можно немного упростить, так как уже есть библиотеки сразу на много дисплеев. Они используют именно такой принцип.
Верхний уровень — это уже полноценная графика. То самое — что можно не изобретать заново, а просто взять готовое из чужих проектов. У вас будут связанные *.с и *.h файлы, при этом подстановка будет видеть только подключённые файлы — а не весь CMSIS, HAL, вместе с командами дисплея прямо из майна. Между прочим это очень удобно.
GCC потом всё это заоптимизирует, и просто выкинет лишнее.
Насчёт быстрого перехода всё очень просто — переписывается или изменяется только небольшая часть из всего имеющегося. Причём очень малая часть!!! Но с вашим подходом придётся переписывать буквально всё.