Top.Mail.Ru

Дисплей на базе контроллера SSD1306. Библиотека для STM32.

Форум продолжает поставлять идеи для контента статей 👍 Как обещал, закупил дисплеи, естественно, они поблуждали по необъятным просторам в процессе доставки, но наконец-то прибыли. Так что сегодня изучаем, подключаем и создаем базовую библиотеку для OLED дисплея на базе контроллера SSD1306 для STM32.

Собственно, эти дисплеи поставляются чаще всего в виде законченных модулей:

Модуль на базе SSD1306.

Шлейф дисплея заведен на плату, содержащую необходимую обвязку и имеющую штыревой разъем с шагом 2.54 мм для взаимодействия с окружающей средой и внешним миром. В итоге это дает минимальный порог вхождения в работу с модулем, поскольку электрическая коммутация заключается лишь в подключении нескольких проводов. Соответственно, данные удобства даруют дисплеям на SSD1306 грандиозную популярность.

В плане классификации можно выделить две характеристики - это используемый для связи интерфейс и разрешение экрана. Начнем со второй. Сам по себе SSD1306 поддерживает максимальное разрешение 128*64 точек, модули же, в основном, бывают:

  • 128*64
  • 128*32
  • 96*16

Движемся далее... Контроллер поддерживает обмен данными с использованием следующих интерфейсов:

  • 8-bit 6800/8080 параллельный интерфейс.
  • 3-х/4-х проводной SPI.
  • I2C.

Выбор осуществляется подачей высокого или низкого уровня на выводы BS0, BS1 и BS2 контроллера SSD1306. Возможные комбинации выглядят так:

Конфигурация SSD1306.

Готовые модули изначально заточены под конкретный вариант. То есть плата содержит компоненты для активации того или иного интерфейса и, соответственно, на штыревой разъем идут сигналы именно выбранного интерфейса:

Варианты модулей.

И поскольку существуют разные варианты, то при покупке лучше заранее озадачиться проверкой параметров модуля. Иначе может оказаться, что куплен модуль с I2C, а устанавливаться он должен на плату/в устройство, где для дисплея выведен SPI.

Краткое лирическое отступление. Помимо прочего, возможны и дополнительные нерегламентированные сюрпризы. Например, подключаем дисплей, получаем изображение, которое смещено в сторону на несколько пикселей относительно запланированного. И естественным и логичным шагом будет проверка регистров, настроек, инициализации и т. д. А в реальности оказывается, что купленный модуль содержит не SSD1306, а контроллер SH1106, который, конечно, во многом идентичен, но далеко не во всем. Поэтому данную вероятность стоит держать в уме, если вдруг ПО работает не так как ожидается. Пишите в комментарии, либо на форум если что - будем разбираться.

Готовые модули поставляются обычно с I2C или SPI, поэтому чуть углубимся в данную тему.

Подключение по интерфейсу I2C.

Режим-рекордсмен в категории - минимальное количество линий связи. Помимо питания дисплея используются только сигналы I2C:

  • SDA
  • SCL

Адрес устройства на шине I2C формируется из битов:

Адрес устройства I2C.

Старшие 6 битов - фиксированы и имеют заранее определенные значения. Бит SA0 соответствует уровню сигнала на выводе SA0 контроллера SSD1306. Получаем возможные варианты адресов (x соответствует биту R/W, о нем чуть ниже):

  • 0b0111100x
  • 0b0111101x

Купив модуль, произведенный не самыми благоразумными разработчиками, легко можно столкнуться с тем, что нигде не указано, каким именно образом подключен этот вывод. В этом случае следует прозвонить цепь и найти, куда подтянут SA0. Собственно, для этого нужно знать, где отправная точка для поиска, а находится она здесь:

SSD1306 вывод SA0.

15-й контакт шлейфа - это вывод D/C контроллера, который в режиме I2C играет роль сигнала SA0.

Второй вариант - попробовать оба варианта адреса, тот который заработает соответствует актуальному подключению. Этот вариант не охватывает ситуацию, когда проблема в коде, и никакой адрес не сработает.

Так, дальше - бит R/W - отвечает за желаемое направление передачи данных. R/W = 1 - чтение, R/W = 0 - запись. У нас будет исключительно второй случай ✌️ Итоговые варианты адресов:

  • SA0 = 0, адрес для чтения данных - 0x79 (0b01111001), адрес для записи - 0x78 (0b01111000).
  • SA0 = 1, адрес для чтения данных - 0x7B (0b01111011), адрес для записи - 0x7A (0b01111010).

Кроме прочего, необходимо указать дисплею, что именно за данные мы отправляем - команду или данные пикселей для записи в GDDRAM (Graphic Display Data RAM) для последующего вывода на экран. В режиме I2C данная информация передается непосредственно через интерфейс (в отличие от SPI, см. ниже). Разбираем на примерах (SA0 здесь будет на земле). Отправка (запись) команд (служебные интерфейсные биты вроде старт и стоп-битов я опускаю):

Запись команд в SSD1306.

Отправка (запись) данных:

Запись данных в SSD1306.

Следующий за адресом байт Control Byte как раз и содержит информацию о назначении последующих данных. Значащими являются только 2 старших бита:

  • Co - Continuation bit - у нас он будет всегда нулевым.
  • D/CData / Command Selection bit - этот бит и дает дисплею понять, что мы ему передаем - команду или данные. "0" - команда, "1" - данные для записи в память GDDRAM SSD1306 (как на схемах).

Именно таким образом и будем осуществлять обменные процессы при использовании I2C. Идем дальше...

Подключение по интерфейсу SPI.

В данном случае имеем два варианта - 4-wire или 3-wire SPI. В первом случае задействованы линии:

  • CS - Chip Select - при отправке команд или данных дисплея на CS необходимо подать низкий уровень.
  • D/C - Data/Command - отвечает за назначение отправляемых данных. Если отправляем байт команды, то на D/C нужно выдать низкий уровень. Если передаем данные для записи в GDDRAM для последующего вывода на экран, то - высокий уровень.
  • SDIN (D1) - SPI MOSI.
  • SCLK (D0) - SPI CLK.

В целом, довольно обыденно )

В режиме 3-х проводного SPI мы лишаемся возможности управлять выводом D/C, что сулит нам неприятности в виде дополнительной подготовки отправляемых данных. В этом режиме вместо 8-ми бит данных необходимо передавать девять, старший из которых будет отвечать как раз за выбор данных/команды (D/C).

По итогу для 4-wire SPI формат посылок выглядит так:

SPI 4-wire.

А для 3-wire уже иначе:

SPI 3-wire.

С передачей данных разобрались, при создании библиотеки реализуем на практике. Да, у меня, кстати, дисплей 128*64 с I2C. В проекте локализуем как обычно нижний и верхний уровни отдельно, чтобы при необходимости максимально быстро и просто адаптировать под свой дисплей.

Уже прозвучало несколько раз про команды SSD1306, полное описание приводить не буду, в даташите хорошо все расписано, ограничусь перечнем:

Команды SSD1306
Set Lower Column Start Address for Page Addressing Mode (00h~0Fh)
Set Higher Column Start Address for Page Addressing Mode (10h~1Fh)
Set Memory Addressing Mode (20h)
Set Column Address (21h)
Set Page Address (22h)
Set Display Start Line (40h~7Fh)
Set Contrast Control for BANK0 (81h)
Set Segment Re-map (A0h/A1h)
Entire Display ON (A4h/A5h)
Set Normal/Inverse Display (A6h/A7h)
Set Multiplex Ratio (A8h)
Set Display ON/OFF (AEh/AFh)
Set Page Start Address for Page Addressing Mode (B0h~B7h)
Set COM Output Scan Direction (C0h/C8h)
Set Display Offset (D3h)

Единственное что, разберем формат, в котором приводится описание команд в документации. Допустим, на примере команды Continuous Horizontal Scroll Setup:

Разбор команды для SSD1306.

Красным обведены непосредственно байты команды. Каждая строка - это отправляемый байт, поэтому данная команда состоит из 7-ми отправляемых друг за другом байт. В зависимости от определенных битов меняются настройки контроллера. Первый байт может принимать два варианта значений:

  • 0x26 - для правой горизонтальной прокрутки.
  • 0x27 - для левой.

Отличие в одном бите, X0:

Второй байт - фиксирован - 0x00. Третий байт позволяет установить адрес стартовой страницы, значащими являются три младших бита. Возможные значения:

  • 0x00 - 0000 0000 - PAGE0.
  • 0x00 - 0000 0001 - PAGE1.
  • 0x00 - 0000 0010 - PAGE2.
  • и далее по аналогичной схеме.

И по аналогичной же схеме следующие байты команды:

Биты команды.

Двигаемся к наиболее интересному - формированию выводимых пикселей. Последовательность битов, каждому из которых соответствует один пиксель, хранится в памяти GDDRAM контроллера. Соответственно, размер данной области памяти равен 128*64 битов (по биту на пиксель, контроллер поддерживает максимальное разрешение 128*64 - все логично).

Память разделена на 8 страниц:

Структура памяти SSD1306.

И есть три возможных режима обновления изображения:

  1. Page addressing mode.
  2. Horizontal addressing mode.
  3. Vertical addressing mode.

Рассмотрим по очереди, Page addressing:

Page addressing mode.

При отправке данных запись начнется с области, которая соответствует 0-й странице и 0-й колонке (PAGE0, COL0). Далее номер активной колонки автоматически инкрементируется, что приведет к записи в область PAGE0, COL1. Аналогичным образом произойдет и для других колонок, по достижению же COL127 указатель снова перейдет на COL0 той же самой(!) страницы PAGE0.

Выбор области для записи происходит при помощи трех команд из списка, который мы рассмотрели, а именно:

  • Команды 0xB0 - 0xB7 устанавливают адрес страницы. 0xB0 - PAGE0, 0xB1 - PAGE1...0xB7 - PAGE7.
  • Команды 0x00 - 0x0F отвечают за младший полубайт номера колонки.
  • Команды 0x10 - 0x1F отвечают за старший полубайт номера колонки.

Естественно, как без небольшого наглядного примера - допустим, мы хотим записать данные в область 4-ой страницы (PAGE4), начиная с 75-ой колонки. 75 - это 0x4B. Для 0x4B - младший полубайт 0xB и старший полубайт 0x4, значит нужные команды:

  • 0xB4 (номер страницы)
  • 0x0B (младший полубайт номера колонки)
  • 0x14 (старший полубайт номера колонки)

В результате будут обновляться данные выбранной этими командами активной области. Переходим ко второму режиму - Horizontal addressing. Графически будет так:

Horizontal addressing mode.

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

И завершаем режимом - Vertical addressing:

Vertical addressing mode.

Суть уже и так видна из схемы, так что и не требуется ничего добавлять.

Те команды установки активной страницы и колонки, которые мы обсудили (0xB0 - 0xB7, 0x00 - 0x0F, 0x10 - 0x1F), используются только для режима page addressing. Для двух других механизм другой:

  • Команда 0x21 - для установки стартового и конечного номеров колонок, в которые будет производиться запись.
  • Команда 0x22 - для установки стартового и конечного номеров страниц, в которые будет производиться запись.

Данные команды предусматривают последовательную отправку 3-х байт, соответственно, для 0x21:

  • 0x21 - код команды.
  • 2-ой байт - начальный номер колонки.
  • 3-ий байт - конечный адрес колонки.

Для 0x22 - полностью аналогично. Возможные номера колонок ограничены значениями - 0...127, страниц - 0...7. Конечно, рассмотрим небольшой пример 🔎 Пусть в режиме horizontal addressing хотим обновлять область со 2-ой по 4-ю страницы с номерами колонок 20-50:

Выбор рабочей области SSD1306.

Итоговый сет необходимых команд будет таким:

  • 0x21, 0x14 (20), 0x32 (50)
  • 0x22, 0x02, 0x04

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

Обновление пикселей.

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

Библиотека для работы с SSD1306 для STM32.

Как раз теперь, наконец-то, перейдем к практической части. Но и тут придется еще обсудить предполагаемую концепцию работы. В общем-то, пойдем по "классическому" пути при работе с SSD1306.

Заключается он в том, что дисплей обновляется целиком. Первично мы имеем массив байт, в котором каждый из битов соответствует одному пикселю. Пикселей у нас (мой дисплей 128*64) - 8192. Таким образом, это трансформируется в 1024 байта. И для вывода чего-либо на дисплей мы сначала изменяем нужные биты в этом массиве, а затем весь(!) массив отправляем дисплею.

И по итогу, с одной стороны, это неоптимально, поскольку для отрисовки даже одного пикселя нужна отправка всех 1024 байт. С другой же стороны такой механизм более нагляден и прост и, соответственно, понижает порог вхождения в работу с дисплеем. Кроме того, скорости отправки данных ( =обновления дисплея) вполне хватает для адекватной работы даже с I2C. В общем, смотрите по конкретной задаче, которую решаете, не нужно строго следовать по описанному где бы то ни было пути, все нужно оценивать и принимать решение в соответствии с тем, что требуется сделать.

Подключение дисплея к STM32 (I2C версия):

  • I2C1 SDA (PB7) - SDA
  • I2C1 SCL (PB6) - SCL

Настройки периферии:

Настройки I2C в STM32CubeMx.

Переходим к развеиванию последних непонятных моментов, если они остались, путем практической реализации библиотеки для работы с SSD1306. Библиотека будет включать в себя 4 файла:

Библиотека для работы с SSD1306 для STM32.
  • ssd1306_interface.c, ssd1306_interface.h - файлы, инкапсулирующие в себе всю интерфейсную деятельность, связанную с передачей данных. В моем случае там будет передача по I2C, при использовании модулей с SPI нужно будет вносить изменения исключительно в данные файлы.
  • ssd1306.c, ssd1306.h - функции для отрисовки графических примитивов, вывода текста и т. д., не зависящие от используемого интерфейса. Сегодня начнем с вывода на дисплей прямоугольника. Как минимум, будет вторая статья с выводом текста, а дальше посмотрим.

Начнем с передачи команд (ssd1306_interface.c):

/*----------------------------------------------------------------------------*/
void SendCommand(uint8_t* data, uint8_t size)
{
  HAL_I2C_Mem_Write(&hi2c1, SSD1306_I2C_ADDRESS, SSD1306_I2C_CONTROL_BYTE_COMMAND,
                    1, data, size, SSD1306_I2C_TIMEOUT);
}



/*----------------------------------------------------------------------------*/

В качестве аргументов - указатель на байты команды и количество этих самых байт. Причем, обратите внимание, что отправка осуществляется при помощи HAL_I2C_Mem_Write(), а не HAL_I2C_Master_Transmit(). Это связано с тем форматом, который нам требуется - отправка фиксированного байта (в данном случае SSD1306_I2C_CONTROL_BYTE_COMMAND = 0x00), за которым следуют данные. Класть этот байт в начало массива передаваемых данных не слишком удобно, а вот HAL_I2C_Mem_Write() подойдет отлично. Так и поступаем.

В этот же файл добавляем передачу данных, суть процесса практически идентична:

/*----------------------------------------------------------------------------*/
void SendData(uint8_t *data, uint16_t size)
{
  HAL_I2C_Mem_Write_IT(&hi2c1, SSD1306_I2C_ADDRESS, SSD1306_I2C_CONTROL_BYTE_DATA, 
                       1, data, size);
  SSD1306_state = SSD1306_BUSY;
}



/*----------------------------------------------------------------------------*/

Но здесь уже отправка с использованием прерываний, поскольку каждый раз при обновлении экрана зависать на этом процессе вообще не вариант. Данные отправляются на передачу, мы же меняем значение флага SSD1306_state. Возможные варианты определены в ssd1306_interface.h:

typedef enum
{
  SSD1306_READY = 0x00,
  SSD1306_BUSY  = 0x01
} SSD1306_State;

Таким образом, перед отправкой следует проверять данный флаг, чтобы удостовериться, что предыдущая отправка успешно завершилась. А отслеживать успешность этой самой отправки мы будем при помощи прерывания по окончанию передачи. Определим соответствующий callback:

/*----------------------------------------------------------------------------*/
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
  SSD1306_state = SSD1306_READY;
}



/*----------------------------------------------------------------------------*/

И да, полный код файлов будет в конце статьи, как и готовый проект для STM32.

Собственно все - интерфейсная часть на этом завершена. Продолжаем в ssd1306.c. Следующим логичным шагом будет инициализация SSD1306:

/*----------------------------------------------------------------------------*/
void SSD1306_Init()
{   
  uint8_t data[3];
  
  // Set display off
  data[0] = 0xAE;
  SendCommand(data, 1);
  
  // Set oscillator frequency
  data[0] = 0xD5;
  data[1] = 0x80;
  SendCommand(data, 2);
  
  // Enable charge pump regulator
  data[0] = 0x8D;
  data[1] = 0x14;
  SendCommand(data, 2);

  // Set display start line
  data[0] = 0x40;
  SendCommand(data, 1);
  
  // Set segment remap
  data[0] = 0xA1;
  SendCommand(data, 1);
  
  // Set COM output scan direction
  data[0] = 0xC8;
  SendCommand(data, 1);
  
  // Set COM pins hardware configuration
  data[0] = 0xDA;
  data[1] = 0x12;
  SendCommand(data, 2);
  
  // Set MUX ratio
  data[0] = 0xA8;
  data[1] = 63;
  SendCommand(data, 2);
  
  // Set display offset
  data[0] = 0xD3;
  data[1] = 0;
  SendCommand(data, 2);
  
  // Set horizontal addressing mode
  data[0] = 0x20;
  data[1] = 0x00;
  SendCommand(data, 2);
  
  // Set column address
  data[0] = 0x21;
  data[1] = 0;
  data[2] = 127;
  SendCommand(data, 3);
  
  // Set page address
  data[0] = 0x22;
  data[1] = 0;
  data[2] = 7;
  SendCommand(data, 3);
  
  // Set contrast
  data[0] = 0x81;
  data[1] = 0x7F;
  SendCommand(data, 2);

  // Entire display on
  data[0] = 0xA4;
  SendCommand(data, 1);

  //Set normal display
  data[0] = 0xA6;
  SendCommand(data, 1);

  // Set display on
  data[0] = 0xAF;
  SendCommand(data, 1);
}



/*----------------------------------------------------------------------------*/

Для разнообразия я поместил описание в виде комментариев, так что дублировать в виде текста - смысла не имеет. Теперь займемся механизмом вывода данных на экран. Определяем в ssd1306.h помимо прочего размеры:

#define SSD1306_X_SIZE                                                  128
#define SSD1306_Y_SIZE                                                  64

#define SSD1306_BUFFER_SIZE                                             (SSD1306_X_SIZE *  SSD1306_Y_SIZE) / 8

И массив для хранения битов-пикселей:

static uint8_t pixelBuffer[SSD1306_BUFFER_SIZE];

Функция обновления изображения будет заключаться в отправке всего массива на SSD1306:

/*----------------------------------------------------------------------------*/
void SSD1306_UpdateScreen()
{  
  SendData(pixelBuffer, SSD1306_BUFFER_SIZE);
}



/*----------------------------------------------------------------------------*/

И сразу же реализуем очистку массива, а следом за ним и дисплея:

/*----------------------------------------------------------------------------*/
void SSD1306_ClearScreen()
{
  for (uint16_t i = 0; i < SSD1306_BUFFER_SIZE; i++)
  {
    pixelBuffer[i] = 0x00;
  }

  SSD1306_UpdateScreen();
}



/*----------------------------------------------------------------------------*/

При наших настройках "очищенный" пиксель - это нулевой бит, что мы и проделали с массивом. Вся остальные процессы, связанные с выводом какого-либо объекта на экран будут заключаться исключительно в изменении битов массива pixelBuffer[], а также в предварительной очистке через SSD1306_ClearScreen() и последующем обновлении через SSD1306_UpdateScreen(). Смотрим на практике - добавим функцию для рисования прямоугольника. При этом нам понадобится вспомогательная функция для установки в единицу бита массива, который соответствует координате (x, y) экрана:

/*----------------------------------------------------------------------------*/
static void SetPixel(uint8_t x, uint8_t y)
{
  pixelBuffer[x + (y / 8) * SSD1306_X_SIZE] |= (1 << (y % 8));
}



/*----------------------------------------------------------------------------*/

Выше мы рассмотрели как именно и в какой последовательности передаваемая информация попадает в память контроллера, а затем и на экран:

Биты GDDRAM.

Индекс байта массива определяется так: (x + (y / 8) * SSD1306_X_SIZE). И в этом байте нам нужно выставить бит с номером (y % 8). Допустим, нам нужно добраться до бита, который соответствует координатам x = 121 и y = 3. По этой формуле получаем:

  • Индекс массива = 121 + 0 * SSD1306_X_SIZE = 121. Проверяем по картинке - все верно.
  • Номер бита = 3 % 8 = 3.

Финишируем функцией вывода прямоугольника:

/*----------------------------------------------------------------------------*/
void SSD1306_DrawFilledRect(uint8_t xStart, uint8_t xEnd, uint8_t yStart, uint8_t yEnd)
{
  for (uint8_t i = xStart; i < xEnd; i++)
  {
    for (uint8_t j = yStart; j <  yEnd; j++)
    {
      SetPixel(i, j);
    }
  }
}



/*----------------------------------------------------------------------------*/

И на этом переходим к проверке осуществленного. В main.c добавляем демо-вывод красивейших полосок на экран с очисткой по окончанию свободного пространства:

while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  for (uint8_t  i = 0; i < 8; i++)
  {
    SSD1306_DrawFilledRect(i * 16, i * 16 + 8, 16, 48);
    SSD1306_UpdateScreen();
    while(SSD1306_IsReady() == 0);
    
    HAL_Delay(25);
  }
  
  SSD1306_ClearScreen();
  while(SSD1306_IsReady() == 0);
}

Тут мы используем еще одну функцию из ssd1306.c, оставшуюся в тени, а именно SSD1306_IsReady():

/*----------------------------------------------------------------------------*/
uint8_t SSD1306_IsReady()
{
  if (SSD1306_state == SSD1306_BUSY)
  {
    return 0;
  }
  
  return 1;
}



/*----------------------------------------------------------------------------*/

Анализируя SSD1306_state, функция определяет готовность контроллера к передаче новой информации для вывода на экран. В случае готовности функция возвращает "1", в противном случае "0".

Запускаем проект и в результате наблюдаем:

P. S. Несмотря на "официальную" максимально поддерживаемую частоту I2C, равную 400 KHz, мне удавалось значительно разогнать обменные процессы, а, соответственно, и обновление информации на дисплее. Но это уже совсем другая история...

/**
  ******************************************************************************
  * @file           : ssd1306.c
  * @brief          : SSD1306 driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "ssd1306.h"
#include "ssd1306_interface.h"



/* Declarations and definitions ----------------------------------------------*/

SSD1306_State SSD1306_state = SSD1306_READY;

static uint8_t pixelBuffer[SSD1306_BUFFER_SIZE];



/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
void SSD1306_Init()
{   
  uint8_t data[3];
  
  // Set display off
  data[0] = 0xAE;
  SendCommand(data, 1);
  
  // Set oscillator frequency
  data[0] = 0xD5;
  data[1] = 0x80;
  SendCommand(data, 2);
  
  // Enable charge pump regulator
  data[0] = 0x8D;
  data[1] = 0x14;
  SendCommand(data, 2);

  // Set display start line
  data[0] = 0x40;
  SendCommand(data, 1);
  
  // Set segment remap
  data[0] = 0xA1;
  SendCommand(data, 1);
  
  // Set COM output scan direction
  data[0] = 0xC8;
  SendCommand(data, 1);
  
  // Set COM pins hardware configuration
  data[0] = 0xDA;
  data[1] = 0x12;
  SendCommand(data, 2);
  
  // Set MUX ratio
  data[0] = 0xA8;
  data[1] = 63;
  SendCommand(data, 2);
  
  // Set display offset
  data[0] = 0xD3;
  data[1] = 0;
  SendCommand(data, 2);
  
  // Set horizontal addressing mode
  data[0] = 0x20;
  data[1] = 0x00;
  SendCommand(data, 2);
  
  // Set column address
  data[0] = 0x21;
  data[1] = 0;
  data[2] = 127;
  SendCommand(data, 3);
  
  // Set page address
  data[0] = 0x22;
  data[1] = 0;
  data[2] = 7;
  SendCommand(data, 3);
  
  // Set contrast
  data[0] = 0x81;
  data[1] = 0x7F;
  SendCommand(data, 2);

  // Entire display on
  data[0] = 0xA4;
  SendCommand(data, 1);

  //Set normal display
  data[0] = 0xA6;
  SendCommand(data, 1);

  // Set display on
  data[0] = 0xAF;
  SendCommand(data, 1);
}



/*----------------------------------------------------------------------------*/
void SSD1306_UpdateScreen()
{  
  SendData(pixelBuffer, SSD1306_BUFFER_SIZE);
}



/*----------------------------------------------------------------------------*/
void SSD1306_ClearScreen()
{
  for (uint16_t i = 0; i < SSD1306_BUFFER_SIZE; i++)
  {
    pixelBuffer[i] = 0x00;
  }

  SSD1306_UpdateScreen();
}



/*----------------------------------------------------------------------------*/
static void SetPixel(uint8_t x, uint8_t y)
{
  pixelBuffer[x + (y / 8) * SSD1306_X_SIZE] |= (1 << (y % 8));
}



/*----------------------------------------------------------------------------*/
void SSD1306_DrawFilledRect(uint8_t xStart, uint8_t xEnd, uint8_t yStart, uint8_t yEnd)
{
  for (uint8_t i = xStart; i < xEnd; i++)
  {
    for (uint8_t j = yStart; j <  yEnd; j++)
    {
      SetPixel(i, j);
    }
  }
}



/*----------------------------------------------------------------------------*/
uint8_t SSD1306_IsReady()
{
  if (SSD1306_state == SSD1306_BUSY)
  {
    return 0;
  }
  
  return 1;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : ssd1306.h
  * @brief          : SSD1306 driver header
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef SSD1306_H
#define SSD1306_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"



/* Declarations and definitions ----------------------------------------------*/

#define SSD1306_X_SIZE                                                  128
#define SSD1306_Y_SIZE                                                  64

#define SSD1306_BUFFER_SIZE                                             (SSD1306_X_SIZE *  SSD1306_Y_SIZE) / 8



/* Functions -----------------------------------------------------------------*/

extern void SSD1306_Init();
extern void SSD1306_ClearScreen();
extern void SSD1306_UpdateScreen();
extern void SSD1306_DrawFilledRect(uint8_t xStart, uint8_t xEnd, uint8_t yStart, uint8_t yEnd);
extern uint8_t SSD1306_IsReady();



#endif // #ifndef SSD1306_H
/**
  ******************************************************************************
  * @file           : ssd1306_interface.c
  * @brief          : SSD1306 driver interface part
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "ssd1306_interface.h"



/* Declarations and definitions ----------------------------------------------*/

extern I2C_HandleTypeDef hi2c1;
extern SSD1306_State SSD1306_state;



/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
void SendCommand(uint8_t* data, uint8_t size)
{
  HAL_I2C_Mem_Write(&hi2c1, SSD1306_I2C_ADDRESS, SSD1306_I2C_CONTROL_BYTE_COMMAND,
                    1, data, size, SSD1306_I2C_TIMEOUT);
}



/*----------------------------------------------------------------------------*/
void SendData(uint8_t *data, uint16_t size)
{
  HAL_I2C_Mem_Write_IT(&hi2c1, SSD1306_I2C_ADDRESS, SSD1306_I2C_CONTROL_BYTE_DATA, 
                       1, data, size);
  SSD1306_state = SSD1306_BUSY;
}



/*----------------------------------------------------------------------------*/
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
  SSD1306_state = SSD1306_READY;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : ssd1306_interface.h
  * @brief          : SSD1306 driver interface header
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef SSD1306_INTERFACE_H
#define SSD1306_INTERFACE_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"



/* Declarations and definitions ----------------------------------------------*/

#define SSD1306_I2C_TIMEOUT                                             100
#define SSD1306_I2C_ADDRESS                                             0x78
#define SSD1306_I2C_CONTROL_BYTE_COMMAND                                0x00
#define SSD1306_I2C_CONTROL_BYTE_DATA                                   0x40



typedef enum
{
  SSD1306_READY = 0x00,
  SSD1306_BUSY  = 0x01
} SSD1306_State;



/* Functions -----------------------------------------------------------------*/

extern void SendCommand(uint8_t* data, uint8_t size);
extern void SendData(uint8_t *data, uint16_t size);



#endif // #ifndef SSD1306_INTERFACE_H
/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "ssd1306.h"

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
I2C_HandleTypeDef hi2c1;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_I2C1_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C1_Init();
  /* USER CODE BEGIN 2 */
  SSD1306_Init();
  SSD1306_ClearScreen();
  while(SSD1306_IsReady() == 0);
    
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    for (uint8_t  i = 0; i < 8; i++)
    {
      SSD1306_DrawFilledRect(i * 16, i * 16 + 8, 16, 48);
      SSD1306_UpdateScreen();
      while(SSD1306_IsReady() == 0);
      
      HAL_Delay(25);
    }
    
    SSD1306_ClearScreen();
    while(SSD1306_IsReady() == 0);
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief I2C1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_I2C1_Init(void)
{

  /* USER CODE BEGIN I2C1_Init 0 */

  /* USER CODE END I2C1_Init 0 */

  /* USER CODE BEGIN I2C1_Init 1 */

  /* USER CODE END I2C1_Init 1 */
  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 400000;
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I2C1_Init 2 */

  /* USER CODE END I2C1_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

Ссылка на проект - MT_SSD1306_Example_1.

Подписаться
Уведомить о
guest

12 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Андрей
2 лет назад

А как же вариант SPI ? ))) И два (и более) дисплея с разным содержимым тоже интересно (например как "показометры" уровня аудиосигнала)...

Сергей Боков
1 год назад

Хорошая статья. Спасибо, а это SetPixel на ассемблере.

; -------------------------------------
; установка пикселя в массиве pixelBuffer
;
; Принимает:   @0 - координата X
;         @1 - координата Y
; -------------------------------------
.macro   SetPixel
      ldi      YL, low(pixelBuffer+@0 + (@1 / 8) * SSD1306_X_SIZE)         ; [x + (y / 8) * SSD1306_X_SIZE]
      ldi      YH, high(pixelBuffer+ @0 + (@1 / 8) * SSD1306_X_SIZE)      ;
      ldi      ACC, @1                                          ; (Y) -> ACC
      rcall   _SetPixel
.endm

; --------------------------------------------
; установка пикселя в массиве pixelBuffer
;
; Принимает:   Y      - индекс байта массива pixelBuffer
;         ACC   - координата y пикселя
; Возвращает: установленный пиксель в pixelBuffer
; --------------------------------------------
_setPixel:
         ldi      temp, 8
         rcall   div8u_c                  ; получить в temp2 остаток от деления ACC/8
         ld      ACC, Y                  ; (Y) -> ACC
         inc      temp2
         CLC                  ; C = 0
         push   temp2
set_bit:   ror      ACC
         DJNZR   temp2, set_bit
         SEC                  ; С = 1
         pop      temp2
set_bit_1:   rol      ACC
         DJNZR   temp2, set_bit_1
         st      Y, ACC                  ; ACC -> (Y)
         ret

; --------------------------------------------
; Деление 8-и разрядных целых беззнаковых чисел (AVR)

; Принимает: ACC  - делимое
;         temp - делитель
; Использует:   temp1 - счетчик цикла
;
; Возвращает: ACC  - результат
;         temp2 - остаток
;-----------------------------------------------

div8u_c:
      sub temp2,   temp2   ;очистить остаток и перенос
      ldi temp1,   9      ;инициализировать счетчик цикла
d8u_1: rol ACC         ;делимое/результат сдвинуть влево
      dec temp1          ;уменьшить на единицу счетчик цикла
      brne d8u_2       ;переход, если не ноль
      ret              ;выход из подпрограммы
d8u_2: 
      rol temp2         ;остаток сдвинуть влево
      sub temp2,   temp   ;остаток= остаток - делитель
      brcc d8u_3       ;если результат < 0
      add temp2,   temp   ;восстановить остаток
      clc               ;сбросить перенос для формирования результата
      rjmp d8u_1       ;иначе
d8u_3: 
      sec             ;установить перенос для формирования результата
      rjmp d8u_1       ;вернуться назад

11058
11058
1 год назад

Прошу помощи. Можете продемонстрировать вариант SPI 4-wire ?
Только начал изучать эту тему, тяжеловато дается

Серегй
Серегй
1 год назад

Отлично! Запустил на STM32F030F4P6, всё работает. Очень хочется увидеть продолжение темы с выводом текста на экран.

Эдуард
Ответ на комментарий  Серегй
1 год назад

Мне нравятся цветные дисплеи. С ними работать проще и стоят дешевле.

Сергей
Сергей
1 месяц назад

Как повернуть изображение на 180 градусов ?

Сергей
Сергей
Ответ на комментарий  Aveal
1 месяц назад

Спасибо.

12
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x