Зеркало сайта: vivacious-stockings-frog.cyclic.app

STM32F103C8 без HAL и SPL: Работа с монохромными дисплеями STE2007 и SSD1306

разделы: STM32 , дата: 13 апреля 2023г.

В продолжение темы работы с дисплеями, в данной статье рассматривается работа с монохромными дисплеями от телефона Nokia 1202 и OLED дисплеем SSD1306. Первый дисплей продавался на али пару лет назад, и сейчас снят с продажи. Тем не менее, на его примере рассмотрим использование USART интерфейса в качестве 9-битного SPI, а так же коснемся вопроса конвертации шрифтов (пока растровых) для использования в своих проектах. В исходниках можно будет найти полные кодовые наборы в 256 символов конвертированных растрового шрифта 8х8 (кириллица) и 8х16 (латиница).

Про OLED дисплей SSD1306 достаточно сложно написать что-либо новое, по нему есть куча материалов в сети. Но мне показалось, что это не повод, что бы не упоминать его вообще.

Все примеры с скомпилированными прошивками можно скачать с портала GitLab по ссылке: https://gitlab.com/flank1er/stm32_bare_metal

Содержание:

I. SPI дисплей HX1230 (Nokia 1202)

  1. Общий обзор дисплея Nokia 1202
  2. Программный 9-битный протокол SPI
  3. Синхронный USART интерфейс в режиме 9-битного SPI
  4. Шрифты для дисплея Nokia 1202

II. OLED дисплей SSD1306 на I2C интерфейсе

  1. Дисплей SSD1306

1) Общий обзор дисплея Nokia 1202

Первый дисплей который я буду рассматривать - это монохромный графический дисплей от телефонов Nokia 1202/1203/1212/1280. Этот дисплей продавался на али несколько лет назад в качестве: "улучшенной замены дисплея Nokia 5110" в виде модуля HX1230. Больше он не продается, поэтому данная глава скорее теоретическая. "Голый" дисплей (не модуль), пока еще можно купить на али, но за цену в 300р лучше, наверное, взять TFT, или монохромный ST7567S. В свое время, модуль HX1230 продавался по цене дисплея Nokia 5110.

Дисплей на мой взгляд любопытный. При работе с ним проявляются проблемы, с которыми мы будем сталкиваться на других, более сложных дисплеях. И эти проблемы мне проще будет показать на данном дисплее, который по характеристикам не сильно ушел от Nokia 5110 и у большинства, наверное, ассоциируется с чем-то очень простым.

Главная проблема - это шрифты. Их откуда-то надо брать или как-то делать. Вторая проблема - это "нокиевский" 9-битный SPI протокол работы с дисплеем, который аппаратно не поддерживается микроконтроллерами STM32F103xx, поэтому придется "колхозить" его из USART. Но давайте по порядку.

Имеем монохромный дисплей от телефонов Nokia 1202/1203/1280, на контроллере STE2007. По габаритам дисплей похож на Nokia 5110:

Дисплей от 5110 выглядит массивнее, но он имеет широкие поля вокруг экранной области. Дисплей Nokia 1202 имеет более плотную картинку. На фото ниже, у дисплея 5110 можно даже разглядеть пикселизацию. Ну и сравните число горизонтальных линий, и расстояние между ними у одного дисплея и у другого. Паттерн заливки при этом один:

Графическая емкость дисплея Nokia 1202 больше - 96x68 пикселей, против 84x48 пикселей на Nokia 5110. Дисплей имеет "мертвую" зону - это половина нижней (девятой) строки. За счет этого, размер его экранной памяти равен не (96*68)/8, а (96*72)/8=864.

Разъем подключения модуля сделан совместимым с 5110, при этом пин D/C не используется в Nokia 1202. Он передается 9-м (старшим) битом по SPI-протоколу.

Подсветка дисплея Nokia 2102 включается подачей питания 3.3 Вольт на ножку BL. Сама подсветка более равномерная и приятная на вид чем у дисплея от Nokia 5110. У дисплея 5110 с синей подсветкой фотографии получаются лучше, чем он выглядит на самом деле.

2) Программный 9-битный протокол SPI

Давайте приступим к написанию кода. Для работы с дисплеем нам нужен будет 9-битный программный SPI протокол. В качестве отправной точки возьмем пример инициализации дисплея Nokia 5110: https://gitlab.com/flank1er/stm32_bare_metal/-/tree/master/17_pcd8544_init

За время прошедшее с предыдущей статьи, я успел обзавестись логическим анализатором, и поэтому картинок в статье станет больше ;)

Скачиваем упомянутый пример :

$ git clone git@gitlab.com:flank1er/stm32_bare_metal.git

Переходим в каталог проекта:

$ cd stm32_bare_metal/17_pcd8544_init/

Очищаем проект:

$ make clean

В модуле "main.c", я вместо строки "pcd8544_init();" написал:

    for (;;) {
        pcd8544_init();
        delay_ms(300);
    }

Микроконтроллер я перевел в режим тактирования от HSI - 8 МГц закомментировав для этого в файле "asm/init.s" следующую строку:

//  bl  SystemInit

После этого я собрал проект, и залил полученную прошивку в микроконтроллер STM32F103C8T6. Подключил к нему дисплей Nokia 5110 и запустил логический анализатор, который показал такую картинку:

В данном случае мы видим последовательность команд инициализации дисплея - 0x21, 0x14, 0xB6, 0x04, 0x20, 0x0C:

void pcd8544_init() {
    // hardware reset
    gpio_reset(GPIOB,RST);
    gpio_set(GPIOB,RST);
    // init routine
    chip_select_enable();
    pcd8544_send(LCD_C, 0x21);  // LCD Extended Commands.
    pcd8544_send(LCD_C, 0x14);  // LCD bias mode 1:48. //0x13
    pcd8544_send(LCD_C, 0xB6);  // Set LCD Vop (Contrast).
    pcd8544_send(LCD_C, 0x04);  // Set Temp coefficent. //0x04
    pcd8544_send(LCD_C, 0x20);  // LCD Basic Commands
    pcd8544_send(LCD_C, 0x0C);  // LCD in normal mode.
    //pcd8544_send(LCD_C, 0x0d);  // inverse mode
    chip_select_disable();
    gpio_reset(GPIOB,BL);           // enable blacklight
    isPower=true;
    pcd8544_fill_fb(0x0);
    pcd8544_display_fb();
}

В проекте используется аппаратный SPI, нам же нужен программный. Поэтому убираем из Makefile строку:

DEF += -DHW_SPI

пересобираем проект и перезаливаем прошивку. Картинка немного поменяется:

Можно видеть, что скорость интерфейса снизилась более чем в два раза. Если в первом случае процедура инициализации занимала ~50 мкс, то сейчас время увеличилось до 130 мкс. Но тем не менее, SPI интерфейс работает корректно.

Переименуем проект из "17_pcd8544_init" в "28_ste2007_init". Вместо дисплея Nokia 5110 установим дисплей Nokia 1202 (предварительно отключив питание от микроконтроллера). Контакт на "D/C" можно оставить пустым, или как есть, мы им пользоваться больше не будем.

В модуле "pcd8544.c", функцию инициализации дисплея: "pcd8544_init()" приведем к следующему виду.

void pcd8544_init() {
    // hardware reset
    gpio_reset(GPIOB,RST);
    gpio_set(GPIOB,RST);
    // init routine
    chip_select_enable();
    pcd8544_send(LCD_C, 0xE2);  // reset
    delay_ms(1);
    pcd8544_send(LCD_C, 0xA4);  // power save off
    pcd8544_send(LCD_C, 0x2F);  // power control set
    pcd8544_send(LCD_C, 0xB0);  // set page address
    pcd8544_send(LCD_C, 0x10);  // set col=0 upper 3 bits
    pcd8544_send(LCD_C, 0x00);  // set col=0 lower 4 bits
    pcd8544_send(LCD_C, 0x40);  // set row 0
    pcd8544_send(LCD_C, 0xAF);  // dispaly on
    //pcd8544_send(LCD_C, 0x0d);  // inverse mode
    chip_select_disable();
    delay_ms(10);
//    gpio_reset(GPIOB,BL);           // enable blacklight
    isPower=true;
    pcd8544_fill_fb(0xAA);
    pcd8544_display_fb();
}

Последовательность инициализации я нашел поиском в интернете, в даташит на ste2007 на я не заглядывал.

Хочу обратить внимание, что время задержек в "delay_ms()" выставленны с учетом того, что микроконтроллер работает на частоте в пять раз ниже номинальной. т.е. они поделены на пять.

Функцию pcd8544_send() следует привести к такому виду:

void pcd8544_send(uint8_t dc, uint8_t data)
{

#ifdef HW_SPI
   SPI1->DR=data;
   while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY));
#else
    // 9-th bit
    if (dc == LCD_D)
        gpio_set(GPIOA,DIN);
    else
        gpio_reset(GPIOA,DIN);

    // Clock Signal
    gpio_set(GPIOA,CLK);
    gpio_reset(GPIOA,CLK);
    // send data bits
   for (uint8_t i=0; i<8; i++)
   {
      if (data & 0x80)
          gpio_set(GPIOA,DIN);
      else
          gpio_reset(GPIOA,DIN);

      data=(data<<1);
      // Set Clock Signal
      gpio_set(GPIOA,CLK);
      gpio_reset(GPIOA,CLK);
   }
#endif
}

Здесь мы просто вместо линии DC "дергаем" DIN/MOSI линию. Плюсом, добавляем еще один клок.

Еще надо будет поменять размер буфера:

//#define LCD_LEN   (uint16_t)((LCD_X * LCD_Y) / 8)
#define LCD_LEN 864

А функция заливки будет выглядеть так:

void pcd8544_display_fb() {
    chip_select_enable();
    for(uint16_t i=0;i<LCD_LEN; i++)
        pcd8544_send(LCD_D,fb[i]);

    pcd8544_send(LCD_C, 0xB0);  // set page address
    pcd8544_send(LCD_C, 0x10);  // set col=0 upper 3 bits
    pcd8544_send(LCD_C, 0x00);  // set col=0 lower 4 bits
    pcd8544_send(LCD_C, 0x40);  // set row 0

    chip_select_disable();
}

Главный цикл, пусть будет таким:

    for (;;) {
        pcd8544_init();
        delay_ms(500);
    }

Пересобираем прошивку, и загружаем ее в микроконтроллер. Смотрим что у нас происходит на шине:

Это последовательность инициализации дисплея, здесь все нормально. А здесь у нас начало передачи данных. Здесь тоже нет ошибок, идут 9-битные данные:

При подключении дисплея должна появиться такая картинка:

Экран будет "мигать", т.е. очищаться и снова заполняться горизонтальными линиями.

Далее следовало бы провести рефакторинг, переименовать все "pcd8544" на "ste2007", переписать все функции на новый дисплей. Но я предлагаю заняться чем-то поинтереснее.

3) Синхронный USART интерфейс в режиме 9-битного SPI

Часто можно прочитать, что синхронный UART протокол по сути является SPI протоколом. Мысль интересная, USART в STM32 может работать в режиме передачи 8 и 9 бит. Нам же как раз нужен 9-битный синхронный протокол, почему бы не попробовать?

Я реализовал данный вариант протокола и должен сказать, что такой вариант SPI протокола все-же несколько отличается от "чистого" SPI протокола. Что бы показать все наглядно, я буду приводить скрины с логического анализатора. Без него я бы, пожалуй, никогда не настроил USART в режиме 9-битного SPI, т.к. данная задача оказалась совсем нетривиальной. Проблема еще в том, что нет гарантий, что ваше устройство "проглотит" такой протокол.

    Для начала несколько тезисов:
  1. Скорость. Не факт, что она будет выше, чем у программной реализации, т.к. USART сравнительно медленный прокол, в максимуме дающий скорость 4 MBit. Я использовал интерфейс USART2 который висит на "медленной" шине APB1 и его скорость в максимуме оказалась всего 2 MBit. Это на уровне программной реализации SPI. Но USART1, по идее, должен работать в два раза быстрее.
  2. USART посылает данные младшим битом вперед (LSB), в то время как SPI посылает данные старшм битом вперед (MSB). Изменить порядок передачи данных через USART в STM32f103xx нельзя. Поэтому приходится делать это самим.
  3. USART перед передачей формирует стартовый бит и "стоп-бит" по завершению передачи фрейма. Если передачу стоп-бита я сумел отключить в настройках, то стартовый бит оказался неотключаемым. К счастью, "клок" (импульс тактирования) по не нему не отрабатывает, но время на шине он все-равно занимает, в результате чего скорость передачи такая, какая она есть.

Т.к. нам потребуется использовать USART2, то потребуется поменять схему подключения дисплея к микроконтроллеру STM32F103C8xx:

PA4 (USART2_TX) - CLK
PA2 (USART2_TX) - DIN
PA5       -       CS
PB4       -       RST

На прежнем месте осталась лишь линия RST.

В блоке инициализации периферии и GPIO, вместо включения SPI1 поставим включение интерфейса USART2

#ifdef HW_SPI
    RCC->APB1ENR |= RCC_APB1Periph_USART2;          // enable USART2
#endif

А инициализация GPIO порта A теперь будет как-то так:

    // Configure Port A. PA5_CS
    GPIOA->CRL &= ~(uint32_t)(0xf<<20);             // clear mode for PA5   (CS)
    GPIOA->CRL |= (uint32_t)(0x3<<20);              // for PA5 set  PushPull mode 50MHz (RST)
#ifdef HW_SPI
    // USART2 PA2_TX, PA4_CLK
    GPIOA->CRL &= ~(uint32_t)(0xf<<8);              // enable Alterentive mode, 50MHz
    GPIOA->CRL |=  (uint32_t)(0xb<<8);              // for PA2 = USART2_TX
    GPIOA->CRL &= ~(uint32_t)(0xf<<16);             // enable Alterentive mode, 50MHz
    GPIOA->CRL |=  (uint32_t)(0xb<<16);             // for PA4 = USART2_CLK
#else
    // USART2 PA2_TX, PA4_CLK
    GPIOA->CRL &= ~(uint32_t)(0xf<<8);              // enable PushPull mode, 50MHz
    GPIOA->CRL |=  (uint32_t)(0x3<<8);              // for PA2 = USART2_TX
    GPIOA->CRL &= ~(uint32_t)(0xf<<16);             // enable PushPull mode, 50MHz
    GPIOA->CRL |=  (uint32_t)(0x3<<16);             // for PA4 = USART2_CLK
#endif

В блоке инициализации USART2, в регистре USART2_CR1 нам надо будет включить флаги: UE, M, TE:

флаг M как раз включает 9-битную передачу данных.

В регистре USART2_CR2 нужно будет включить флаги: CLK_EN, LBCL и STOP присвоить единицу (1):

CLK_EN включает ножку тактирования, LBCL включает тактирование для последнего бита данных, а STOP=1 подавляет передачу STOP бита(по крайней мере я так понял по картинке с логического анализатора).

В коде, настройка интерфейса будет выглядеть как-то так:

    // --- UART2 setup ----
#ifdef HW_SPI
    //USART2->BRR = 0x271;  // 115200 baud
    USART2->BRR = 0x12;     // 2MBaud
    USART2->CR2 |= (USART_CR2_CLKEN| USART_CR2_STOP_1 | USART_CR2_LBCL); 
    USART2->CR1 |= (USART_CR1_UE_Set |USART_CR1_M| USART_Mode_Tx);
#endif

Для отправки данных изобретем такую функцию:

void  usart2_send(uint8_t dc,uint8_t data){
    uint8_t a=(lookup[data&0b1111] << 4) | lookup[data>>4];
    uint16_t d=(uint16_t)a;
    d=d<<1;
    if (dc == 0x01) d|=(uint16_t)0x1;
    while(!(USART2->SR & USART_FLAG_TXE)) {
        __asm__ volatile ("nop\n");
    }
    USART2->DR=d;
}

главный цикл пусть будет пока таким:

    for (;;) {
        pcd8544_send(0x0,0xa4);   // power save off
        pcd8544_send(0x0,0x2f);   // power control set
        pcd8544_send(0x0,0xB0);   // set page address
        pcd8544_send(0x0,0x10);   // set col=0 upper 3 bits
        pcd8544_send(0x0,0x00);   // set col=0 lower 4 bits
        pcd8544_send(0x0,0x40);   // set row 0
        pcd8544_send(0x0,0xAF);   // dispaly on

        pcd8544_send(0x1,0xaa);

        delay_ms(500);
    }

Теперь если посмотреть на шину логическим анализатором, то увидем следующее:

Во-первых здесь четко виден СТАРТ-бит, который посылается перед каждым фреймом. Особено четко его видно на передаче значения "0x1AA" где первой передается единичка. Отсюда идет значительное расстояние между фреймами, Ни общая скорость где-то на уровне ручной перечи азбукой Морзе, =7.5 мс.

Я попытался выставить максимальное значение скорости порта USART2, и при тактовой частоте микроконтроллера 8 МГц, у меня выдало 0.2 мс на передачу восьми байт:

Если сравнить с программной реализацией, скорости будут практически равными:

К сожалению, несмотря на то, что судя по картинке с логического анализатора, с форматом передачи данных через USART2 было все нормально, дисплей такой протокол не принял. Т.к. у меня уже был некоторый опыт решения траблов с ST7789, я аналогично попробовал вставить в код передачи фрейма, работу с CS линией:

void  usart2_send(uint8_t dc,uint8_t data){
    uint8_t a=(lookup[data&0b1111] << 4) | lookup[data>>4];
    uint16_t d=(uint16_t)a;
    d=d<<1;
    if (dc == 0x01)
        d|=(uint16_t)0x1;
    chip_select_enable();
    while(!(USART2->SR & USART_FLAG_TXE)) {
        __asm__ volatile ("nop\n");
    }

    USART2->DR=d;

    while(!(USART2->SR & USART_FLAG_TC)) {
        __asm__ volatile ("nop\n");
    }
    chip_select_disable();
}

И в этот раз дисплей это уже "проглотил". Логический анализатор показал такую картинку на шине:

Здесь видны огромные интервалы между фреймами, которые, как я понимаю, формируются двумя циклами ожидания. Но логический анализатор показывал, что без них по шине передавалась полная ерунда, поэтому они необходимы.

Осталось посмотреть, будет ли это работать на максимальных частотах микроконтроллера STM32F103C8xx.

Логический анализатор показал время передачи семи байтов бит-бангом равной 47 мкс.

А время передачи через USART равное 55 мкс:

Получается, что bit-bang немного даже более эффективен, чем аппаратный USART. Но мы можем использовать USART1 который будет работать в два раза быстрее и совместно с USART мы можем использовать DMA, хотя в случае данного дисплея, это конечно же бессмыслено.

Функция отправки данных на дисплей Nokia 1202 в конечном итоге приняла следующий вид:

void pcd8544_send(uint8_t dc, uint8_t data)
{
#ifdef HW_SPI
    uint8_t a=(lookup[data&0b1111] << 4) | lookup[data>>4];
    uint16_t d=(uint16_t)a;
    d=d<<1;
    if (dc == 0x01)
        d|=(uint16_t)0x1;
    chip_select_enable();
    while(!(USART2->SR & USART_FLAG_TXE)) {
        __asm__ volatile ("nop\n");
    }

    USART2->DR=d;

    while(!(USART2->SR & USART_FLAG_TC)) {
        __asm__ volatile ("nop\n");
    }
    chip_select_disable();
#else
    // 9-th bit
    if (dc == LCD_D)
        gpio_set(GPIOA,DIN);
    else
        gpio_reset(GPIOA,DIN);

    // Clock Signal
    gpio_set(GPIOA,CLK);
    gpio_reset(GPIOA,CLK);
    // send data bits
   for (uint8_t i=0; i<8; i++)
   {
      if (data & 0x80)
          gpio_set(GPIOA,DIN);
      else
          gpio_reset(GPIOA,DIN);

      data=(data<<1);
      // Set Clock Signal
      gpio_set(GPIOA,CLK);
      gpio_reset(GPIOA,CLK);
   }
#endif
}

Название функции, конечно же, следовало бы поменять.

4) Шрифты для дисплея Nokia 1202

Мы добрались до главной темы статьи, т.е. до проблем с шрифтами для дисплеев отличных от Nokia 5110. Проблема заключается в том, что их фактически нет. Каждый, кто добирается до использования дисплея отличного от Nokia 5110, первым делом ищут шрифты в интернете, и далее довольствуется тем, что найдут. Шрифты на самом деле есть конечно, это шрифт 5х7 от Nokia 5110, который используют везде где можно, остальные шрифты не содержат кириллицы. В частности, есть неплохой жидко-кристаллический шрифт 8х16 для цифр. В принципе, их комбинация выглядит неплохо, судите сами:


Фото взято с ресурса: https://red-resistor.ru

Но пока это единственный вариант который у нас есть. Что бы разжиться шрифтам, можно попытаться что-то cконвертировать из векторных TTF шрифтов, но сделать мелкий шрифт из векторного не так просто.

На самом деле растровые шрифты есть и их много, они использовались в компьютерах 80-х годов, когда преобладал интерфейс текстовой строки. Они до сих пор используются в Linux в текстовом режиме и моей первой мыслью было сконвертировать именно их. В настоящее время растровые шрифты для текстового режима входят в состав пакета "kbd". Там есть в том числе и кириллица. Скриншоты с шрифтами можно посмотреть здесь: https://adeverteuil.github.io/linux-console-fonts-screenshots/

Если отсечь наборы не содержащию кириллицу, и выбрать размер 8х8, то все шрифты будут выглядеть примерно одинаково. Поэтому я просто выбрал набор с DOS-кодировкой CP866/IBM866 из-за того, что шрифт 5х7 у меня также был в этой кодировке. Выглядит шрифт так:

Вроде бы неплохо выглядит, но тут есть один нюанс. Ширина глифов этого шрифта равна 7 пикселям против 5 у шрифта 5х7, т.о. межбуквенный интервал в этом шрифте сопоставляет всего один пиксел. На больших мониторах это смотрелось нормально, но на меленьком ЖК-дисплее текст может выглядеть чересчур плотным. Ну и вообще, сам шрифт выглядит как жирный/Bold.

Так же мое внимание привлек шрифт "Cybercafe":

Шрифт не содержит кириллицу, и его размер 8х16, т.е. это двухстрочный шрифт. Но он выглядит необычно и стильно на мой взгляд.

Первый шрифт был в формате PSF, шрифт Cybercafe в формате FNT. В обоих форматах пиксели упакованы в байты по восемь, как и в шрифтах для дисплея Nokia 5110, но только здесь выборка идет по строкам, а не по столбцам. В случае шрифта Cybercafe, кодирование буквы "D" будет таким:

Шестнадцатеричным редактором находим эту букву в файле шрифта:

Мне требовалось перекодировать шрифт вертикальными столбцами, в следующем порядке:

В итоге для буквы "D" должна получиться следующая последовательность:

0xfc, 0xfc, 0x4,  0x4,  0xc,  0xf8, 0xf0, 0x0,  0x1f, 0x1f, 0x10, 0x10, 0x18, 0xf,  0x7,  0x0,

FNT - это RAW формат, который содержит кодированные символы без всякого заголовка. PSF - это формат с аналогичным кодированием, но он содержит заголовок 32 байта, в котором указн размер шрифта. Сам способ кодирования одинаков для обоих форматов, я его только что описал. В итоге, за вечер я написал небольшую утилиту для переобразования файлов шрифтов, и в результате получил два заголовочных файла с полным набором глифов (т.е. 256).

Далее мне потребовались увеличенные в два раза цифры шрифты Cybercafe. Простое масштабирование в два раза меня не устраивало, т.к. глифы имеют большое пустое пространство снизу и сверху. Поэтому цифры пришлось самому.

Для этого мне потребовалась специальная программа и выбор пал на MatrixFont:

Программа предназначена для конвертации TTF шрифтов в матричные, но можно рисовать и свои. И минусов программы я бы назвал следующее: TTF-шрифт можно открыть только системный, открыть его как файл нельзя, требуется устанавливать его в систему. Второе. Свой шрифт нужно сохранять с расширением "rhf", иначе программа его потом не откроет. Сама программа с этим расширением сохранять не предлагает, фиг догадаешься. Третье. Порядок кодирования символов у нее тоже своеобразный:

Немного подумав, я изменил порядок выборки байт в своей программе на "через три", и на этом моя проблема была решена.

Из плюсов программы, я бы назвал: она не разу не вылетела, пока я с ней работал, и она без проблем запустилась на WindowXP в виртуалке.

Давайте посмотрим, что в итоге получилось. В начале я протестировал "жирный" шрифт на дисплее Nokia 5110. На фотографии снизу можно увидеть текст выведенный шрифтом 5х7, а надпись "Привет Мир" была выведенна шрифтом 8х8:

А здесь уже весь текст кроме инвертированной строки "testtesttest" выведен шрифтом 8х8:

Давайте перейдем к дисплею Nokia 1202, текстовый экран с шрифтом 5x7 будет выглядеть так:

Дисплей имеет 8 рабочих строк текста, но за счет этого текст выглядит более мелко. Шрифт 8х8 уже выглядит более солидно, но буквы как-будто немного приплюснуты:

Увеличенные в три раза цифры шрифта 8х8 выглядят так:

Кубично, я бы сказал.

Текст выведенный шрифтами 5х7 и 8х8:

Масштабированный текст:

Еще:

Шрифт Cybercafe - это отдельный разговор, т.к. он двухстрочный. На дисплее Nokia 1202 он выглядит так:

Напоминаю, что кириллицы он не содержит.

В итоге, на комбинации "бесплатно-скачанных" шрифтов и самостоятельно конвертированных, я составил два варианта интерфейса FM-приемника:

Стоило ли это таких усилий, судите сами.

5) Дисплей SSD1306

Далее у нас на обзоре широко распространенный, бюджетный, монохромный OLED дисплей на контроллере SDD1306. Данный контроллер был разработан где-то в 2010-м году, т.е. он будет поновее нежели STE2007 или PCD8544.

Контроллер может работать от SPI или I2C интерфейса, и иметь матрицу с разрешением 128х32 или 128х64 пикселей. У меня на руках имелись дисплеи с разрешением 128х32 пкс на I2C интерфейсе. Их я и буду рассматривать.

Для тех, кто уже освоил работу с монохромными дисплеям типа Nokia 5110, разобраться с SSD1306 не составит труда, там все то же самое, заморочки касаются только I2C интерфейса и процедуры инициализации.

Про данные дисплеи не писал только ленивый, поэтому я опушу их описание, и перейду сразу к нюансом программирования.

У меня на руках имелось два дисплея разрешения 128х32, один узкий с синими пикселами, другой, условно говоря, "квадратный" с желтой первой строкой. Дисплеи небольшие по размерам, и для вывода читаемого текста на узкий дисплей требовался двухстрочный шрифт 8х16. Я не хотел добавлять еще один шрифт в проект, поэтому экспериментировал на "квадратном" варианте, который нормально мог отображать шрифт высотой в 8 пикселей.

Дисплеи могут иметь I2C адрес 0x3C или 0x3D. Я читал, что китайцы дисплеи с разрешением 128x64 делают с I2C адресом 0x3D, а с разрешением 128x32 с I2C адресом 0х3С. Тем не менее, сканером I2C следует обязательно проверить какой реально адрес вашего дисплея:

Конфигурирование I2C для работы с дисплеем SSD1306 состоит из трех шагов: а) включение тактирования I2C модуля; б) настройка GPIO PB6 и PB7 для альтернативного режима; в) настройка I2C модуля микроконтроллера.

Дисплей SDD1306 может работать на "быстрой" I2C шине (Fast Mode) с частотой 400 кГц. Конфигурирование I2C модуля в режим Fast Mode выполняется следующим образом:

    // --- I2C setup ----
    disable_i2c;
    I2C1->CR2 &= I2C_CR2_FREQ_Reset;    //=0xffc0
    I2C1->CR2 |= 36;                    // set FREQ = APB1= 36MHz
    // 100 kHz
/*    I2C1->CCR = 180;                    // 100 KHz
    I2C1->TRISE = 37;
*/  // 400 kHz (fast mode I2C)
    I2C1->CCR = I2C_CCR_FS|45;   // 400 KHz
    I2C1->TRISE = 12;            // (SDA and SCL rise time)/TPCL1 + 1 = 300ns/28ns +1 = 12 for Fast Mode (FM)
 

Контроллер SDD1306 может работать с тремя видами I2C сессий:

1. Команда. В этом случае сначала передается START, затем I2C адрес, затем НОЛЬ, и затем уже команда в виде ОДНОГО байта, затем STOP. Даже если команда имеет параметры, то они передаются в формате следующей команды. Пример передачи последовательности команд можно наблюдать на картинке ниже:

2. Передача одного байта данных. Все делается как и предыдущем случае, но вместо нуля после старта, передается число 0x40. Этот формат мы использовать не будем.

3. Передача массива данных. В этом случае, после передачи числа 0x40 передается массив данных, и после завершения передачи, формируется СТОП на линии. Начало передачи массива можно увидеть на картинке ниже:

Инициализация дисплея - это последовательность передачи 25 байт в режиме команды. При отладке это можно делать вот так:

///
/*
uint8_t ssd1306_init(void)
{
    uint8_t ret;

    ret=ssd1306_send_command(0xAE); // Set display OFF

    ret&=ssd1306_send_command(0xD5); // Set Display Clock Divide Ratio / OSC Frequency
    ret&=ssd1306_send_command(0x80); // Display Clock Divide Ratio / OSC Frequency

    ret&=ssd1306_send_command(0xA8); // Set Multiplex Ratio
    ret&=ssd1306_send_command(0x1F); // Multiplex Ratio for 128x32 (32-1)

    ret&=ssd1306_send_command(0xD3); // Set Display Offset
    ret&=ssd1306_send_command(0x00); // Display Offset

    ret&=ssd1306_send_command(0x0); // Set Display Start Line

    ret&=ssd1306_send_command(0x8D); // Set Charge Pump
    ret&=ssd1306_send_command(0x14); // Charge Pump (0x10 External, 0x14 Internal DC/DC)

    ret&=ssd1306_send_command(0x20); // Set Memory Mode
    ret&=ssd1306_send_command(0x00); // like ks0108

    //ret&=ssd1306_send_command(0xA0); // Horiontal Mirror
    ret&=ssd1306_send_command(0xA1); // Set Segment Re-Map
    ret&=ssd1306_send_command(0xC8); // Set Com Output Scan Direction

    ret&=ssd1306_send_command(0xDA); // Set COM Hardware Configuration
    ret&=ssd1306_send_command(0x02); // COM Hardware Configuration

    ret&=ssd1306_send_command(0x81); // Set Contrast
    ret&=ssd1306_send_command(0x8F); // Contrast

    ret&=ssd1306_send_command(0xD9); // Set Pre-Charge Period
    ret&=ssd1306_send_command(0xF1); // Set Pre-Charge Period (0x22 External, 0xF1 Internal)

    ret&=ssd1306_send_command(0xDB); // Set VCOMH Deselect Level
    ret&=ssd1306_send_command(0x40); // VCOMH Deselect Level

    ret&=ssd1306_send_command(0xA4); // Set all pixels OFF
    ret&=ssd1306_send_command(0xA6); // Set display not inverted
    //ret&=ssd1306_send_command(0x2E); // Deactivate Scroll
    ret&=ssd1306_send_command(0xAF); // Set display On

    return ret;
}

*/

После успешной инициализации, на дисплее должны появиться хаотически расположенные точки, как-то так:

Но лично я предпочитаю выполнять инициализацию следующим образом:

#define ssd1306_init_len 25

const uint8_t  ssd1306_init_str[ssd1306_init_len] =
        {0xAE,0xD5,0x80,0xA8,0x1F,0xD3,0x00,0x00,0x8D,0x14,0x20,0x00,
         0xA1,0xC8,0xDA,0x02,0x81,0x8F,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF};


uint8_t ssd1306_init() {
    uint8_t ret=SSD1306_OK;
    for(uint8_t i=0; i<ssd1306_init_len; i++) {
        i2c_send_cmd(ssd1306_init_str[i]);
    }

    delay_ms(20);
    ssd1306_clean();
    ssd1306_refresh();
    return ret;
}

В данном случае дисплей сразу очищается и готов к работе.

Функция заливки дисплея содержимым буфера выглядит так:

void ssd1306_refresh() {
    i2c_send_cmd(0x22); // Page Start Address
    i2c_send_cmd(0x0);
    i2c_send_cmd(0xFF);
    i2c_send_cmd(0x21);  //Column start address
    i2c_send_cmd(0x0);
    i2c_send_cmd(127);

    enable_i2c;
    if (init_i2c((SSD1306_I2C_ADDRESS<<1),0x40,NOLAST) == 0 ) {
       for(uint16_t i=0; i < ssd1306_buf_size; i++) {
           I2C1->DR=ssd1306_buf[i];
           while(!(I2C1->SR1 & I2C_FLAG_BTF));     // wait BFT
       }
       stop_i2c;
    }
    disable_i2c;
}

Здесь, перед заливкой передаются команды для установки положения адреса данных (курсора).

Работа с I2C типовая, затрагивать ее на хочу, кому интересно, смотрите код в репозитории.

Для демонстрации дисплея я создал тестовый экран шрифтом 5x7:

И шрифтом 8x8:

Еще я сделал счетчик на крупных цифрах "cybercafe". Выглядит так:

Передача восьми байт на частоте шины I2C в 100 кГц занимает 724 мкс (смотрите скрин с логического анализатора выше), а при частоте 400 кГц - 278 мкс:

Полностью запись экрана на 400 кГц занимает 18 мс:

Такой вот дисплей.