[Перевод] Портируем графическую библиотеку U8G2 на STM32
В этом руководстве мы познакомимся со знаменитой графической библиотекой U8G2 для монохромных встроенных дисплеев. Эта библиотека поддерживает практически все типы монохромных графических дисплеев, и сегодня мы изучим, как портировать ее на STM32.
Вы можете найти более подробную информацию о библиотеке U8G2 на ее странице на GitHub. У библиотеки есть официальное руководство по портированию, которому мы и будем следовать в этом руководстве.
Согласно руководству по портированию, при переносе библиотеки на любой микроконтроллер нам необходимо внести всего два набора изменений:
«uC specific» GPIO and Delay колбек.
u8×8 коммуникационный колбек.
GPIO and Delay колбек
GPIO and Delay колбек отвечает за обработку всех сообщений, связанных с портами ввода/вывода и задержкой.
В Delay‑сообщениях может быть указана задержка, необходимая для отображения (обычно в миллисекундах), или задержка, необходимая для генерации тактовых сигналов SPI или I2C (в основном в нано или микросекундах), в случае реализации программного SPI или I2C.
В GPIO‑сообщениях содержится информация о настройке и сбросе пинов GPIO, используемых для сопряжения с дисплеем. Эти пины GPIO могут включать в себя пины данных (D0, D1, D2 и т. д. в параллельном режиме) или пины управления (CS RST DC в режиме SPI).
Коммуникационный колбек
Коммуникационный колбек (communication callback) предназначен для обработки сообщений, используемых во время связи с дисплеем. Эти сообщения включают начало передачи, отправку байта, завершение передачи и т. д.
В этом руководстве мы разберемся с различными методами коммуникации для взаимодействия с различными дисплеями. Мы рассмотрим, как применять аппаратный SPI для взаимодействия с 1.3″ Oled‑дисплеем на SH1106, аппаратный I2C для взаимодействия с 0.96″ Oled‑дисплеем на SSD1306 и программный SPI для взаимодействия с дисплеем на HX1230.
Подготовка
Сперва нам нужно скопировать файлы библиотеки в папку нашего проекта. Для этого скачайте архив u8g2.zip со страницы GitHub.

Затем извлеките файлы из архива. Найдите и скопируйте папку csrc из u8g2 и поместите ее в папку Drivers вашего проекта.

Далее откройте свойства проекта → C/C++ Build → Settings → GCC Compiler → Include Path и нажмите кнопку Add. Это позволит вам указать путь к папке, которую вы только что скопировали.

После этого нажмите на Workspace, найдите папку csrc внутри Drivers и нажмите OK.

Наконец, нажмите на Apply and Close, чтобы сохранить внесенные изменения.

1.3″ Oled-дисплей на SH1106
Давайте начнем с 1.3-дюймового SPI Oled‑дисплея на базе SH1106. Ниже представлено изображение, демонстрирующее конфигурацию CubeMX, необходимую для взаимодействия с этим дисплеем.

Я настроил SPI1 в режиме «Transmit Only Master». Поскольку нам не требуется получать данные с дисплея, этот режим подходит для нас больше всего.
Скорость передачи данных не должна быть слишком высокой, поэтому я установил ее на уровне примерно 3 Мбит/с. CPOL установлен на Low, а CPHA — на 1 Edge, что соответствует SPI Mode 0.
Пин PA5 сконфигурирован как SCK, а PA7 — как Mosi. Помимо них, я также настроил PA9 как RESET, PC7 — как DC и PB6 — как CS пин.
На следующем изображении представлено соединение между дисплеем и nucleo F446.

Код
Как я уже упоминал ранее, нам необходимо настроить GPIO and Delay и коммуникационный колбеки. Ниже приведена функция GPIO and Delay колбека.
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_DELAY_MILLI:
HAL_Delay(arg_int);
break;
case U8X8_MSG_GPIO_CS:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, arg_int);
break;
case U8X8_MSG_GPIO_DC:
HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, arg_int);
break;
case U8X8_MSG_GPIO_RESET:
HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, arg_int);
break;
}
return 1;
}
Шаблон для этого колбека описан в самом руководстве по портированию. Здесь мы определим только те сообщения, которые нам необходимы для дисплея, использующего аппаратный SPI.
U8×8_MSG_DELAY_MILLI используется для определения задержки в миллисекундах. В данном случае мы будем использовать функцию HAL_Delay, чтобы указать задержку в миллисекундах. Параметр arg_int представляет собой количество миллисекунд.
U8×8_MSG_GPIO_CS/DC/RESET используются для установки или сброса соответствующих пинов. В этом случае параметр arg_int может быть либо 1 (установка пина), либо 0 (сброс пина). Мы просто вызовем функцию GPIO_WritePin, чтобы установить или сбросить соответствующий пин.
Теперь перейдем к определению коммуникационного колбека.
uint8_t u8x8_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_BYTE_SET_DC:
HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, arg_int);
break;
case U8X8_MSG_BYTE_SEND:
HAL_SPI_Transmit(&hspi1, (uint8_t *)arg_ptr, arg_int, 1000);
break;
case U8X8_MSG_BYTE_START_TRANSFER:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
break;
case U8X8_MSG_BYTE_END_TRANSFER:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
break;
}
return 1;
}
Шаблон для этого колбека также описан в руководстве по портированию. Ниже представлен список сообщений, которые будут в нем использоваться.
U8×8_MSG_BYTE_SET_DC используется для установки или сброса пина DC. В этом случае параметр arg_int может быть либо 1 (установка пина), либо 0 (сброс пина). Для выполнения этой операции мы можем воспользоваться функцией GPIO_WritePin.
U8×8_MSG_BYTE_START_TRANSFER используется для инициирования передачи данных через SPI. Мы можем использовать это сообщение для включения SPI Slave устройства (дисплея), сбросив пин CS.
U8×8_MSG_BYTE_END_TRANSFER используется для завершения передачи данных через SPI. Мы можем использовать это сообщение, чтобы отключить SPI Slave (дисплей), установив пин CS.
U8×8_MSG_BYTE_SEND используется для отправки данных через SPI. Параметр arg_ptr содержит данные, подлежащие передаче, а arg_int — их количество в байтах. Для передачи требуемого количества байтов мы можем вызвать функцию HAL_SPI_Transmit.
Ниже приведена функция main.
#include "u8g2.h"
u8g2_t myDisplay;
int main()
{
...
u8g2_Setup_sh1106_128x64_noname_f(&myDisplay, U8G2_R0, u8x8_spi, u8x8_gpio_and_delay); // Инициализация u8g2-структуры
u8g2_InitDisplay(&myDisplay); // Отправка последовательности инициализации на дисплей, после чего дисплей переходит в спящий режим
u8g2_SetPowerSave(&myDisplay, 0); // Пробуждение дисплея
u8g2_ClearDisplay(&myDisplay);
u8g2_SetFont(&myDisplay, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&myDisplay, 0,15,"Hello world");
u8g2_DrawCircle(&myDisplay, 60, 30, 10, U8G2_DRAW_ALL);
u8g2_SendBuffer(&myDisplay);
while (1) {}
}
Подробную последовательность запуска библиотеки вы можете найти здесь (в документации). Сначала нам нужно определить u8g2-структуру, которую я назвал myDisplay.
Внутри функции main
мы начинаем с настройки дисплея. Функция u8g2_Setup_sh1106_128×64_noname_f принимает следующие параметры:
myDisplay — это u8g2-структура, которую мы определили ранее.
U8G2_R0 — это значение для ротации. Более подробную информацию о ротации вы можете найти здесь.
u8×8_spi — это процедура передачи байтов. Мы уже определили этот (коммуникационный) колбек.
u8×8_gpio_and_delay — это процедура задержки и GPIO. Мы также уже определили этот колбек.
Функция настройки u8g2_Setup_sh1106_128×64_noname_f имеет в конце букву «f». Также можно встретить функции настройки с »1» и »2» в конце. Эти цифры в основном означают
После настройки дисплея мы инициализируем его, а затем пробуждаем, отключая режим энергосбережения.
Затем я вывожу строку («Hello world») в верхней части дисплея и рисую круг в нижней части дисплея. Рекомендую вам ознакомиться со справочным руководством, чтобы найти дополнительные функции отрисовки, которые вы можете использовать.
В конце мы отправляем буфер на дисплей, чтобы данные буфера могли быть выведены на экране.
Результат
Ниже представлено изображение, которое демонстрирует результат выполнения приведенного выше кода.

Как вы можете видеть, на дисплее отображается строка «Hello World», а также кружок в нижней части экрана.
Библиотека прекрасно работает с OLED‑дисплеем SH1106 с использованием аппаратного SPI.
0.96″ I2C OLED-дисплей на SSD1306
Теперь мы познакомимся с 0.96-дюймовым I2C OLED‑дисплеем на базе SSD1306. Ниже представлено изображение, демонстрирующее конфигурацию CubeMX, необходимую для взаимодействия с этим дисплеем.

Я настроил I2C в Fast Mode, чтобы тактовую частоту I2C можно было установить на 400 кГц. Поскольку OLED‑дисплей SSD1306 использует высокоскоростную синхронизацию I2C, быстрый режим является обязательным.
Пины PB8 и PB9 сконфигурированы как пины SCL и SDA.
На следующем рисунке представлено соединение между OLED‑дисплеем и F446.

Код
Нам нужно снова настроить коммуникационный и GPIO and Delay колбеки. Ниже приведена функция GPIO and Delay колбека.
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_DELAY_MILLI:
HAL_Delay(arg_int);
break;
}
return 1;
}
Шаблон для этого колбека приведен в самом руководстве по портированию. Здесь мы определим только те сообщения, которые нам нужны для дисплея, использующего аппаратный SPI.
U8×8_MSG_DELAY_MILLI вызывается для определения задержки в миллисекундах. Здесь мы просто вызовем HAL_Delay, чтобы указать задержку в миллисекундах. Параметр arg_int — это количество миллисекунд.
Поскольку мы не задействуем другие пины, такие как CS, DC и RESET, нам не требуется определять сообщения для них.
Теперь мы можем перейти к определению коммуникационного колбека:
uint8_t u8x8_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will никогда не отправляет более 32 байт между START_TRANSFER и END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch(msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
HAL_I2C_Master_Transmit(&hi2c1, 0x78, buffer, buf_idx, 1000);
break;
default:
return 0;
}
return 1;
}
Шаблон для этого обратного вызова также определен в руководстве по портированию. Это отдельный колбек, определенный для случаев, когда для передачи данных по I2C используется только одна функция.
Ниже представлен список сообщений, которые используются в этом колбеке:
U8×8_MSG_BYTE_SEND используется для отправки данных. В этом случае параметр arg_ptr содержит данные, подлежащие передаче, а arg_int — количество байтов этих данных. Здесь мы скопируем данные из arg_ptr в buffer.
U8×8_MSG_BYTE_START_TRANSFER используется для запуска передачи данных через SPI. Здесь мы просто сбросим индекс буфера (buf_indx), чтобы новые данные могли быть скопированы с самого начала буфера.
U8×8_MSG_BYTE_END_TRANSFER используется для завершения передачи данных через SPI. Здесь мы фактически передадим данные через I2C. Мы вызовем функцию HAL_I2C_Master_Transmit для передачи данных на дисплей. 0×78 — это slave‑адрес SSD1306, buffer — данные, которые мы хотим передать, а buf_indx — количество байтов данных.
Ниже представлена функция main
:
#include "u8g2.h"
u8g2_t myDisplay;
int main()
{
...
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&myDisplay, U8G2_R0, u8x8_i2c, u8x8_gpio_and_delay);
u8g2_InitDisplay(&myDisplay); // эта функция отправляет последовательность инициализации на дисплей, после чего он переходит в спящий режим
u8g2_SetPowerSave(&myDisplay, 0); // пробуждает дисплей
u8g2_ClearDisplay(&myDisplay);
u8g2_SetFont(&myDisplay, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&myDisplay, 0,15,"Hello world");
u8g2_DrawCircle(&myDisplay, 60, 30, 10, U8G2_DRAW_ALL);
u8g2_SendBuffer(&myDisplay);
while (1) {}
}
Вы можете найти подробную инструкцию по запуску библиотеки в документации. Прежде всего, необходимо определить u8g2-структуру, которую я назвал myDisplay.
Внутри функции main
мы начнем с настройки дисплея. Функция u8g2_Setup_ssd1306_i2c_128×64_noname_f принимает следующие параметры:
myDisplay — это структура u8g2, которую мы определили ранее.
U8G2_R0 — это значение для ротации. Более подробную информацию о ротации вы можете найти здесь.
u8×8_i2c — это процедура передачи байтов. Мы уже определили этот (коммуникационный) колбек.
u8×8_gpio_and_delay — это процедура задержки и GPIO. Мы также уже определили этот колбек.
После настройки дисплея мы инициализируем его, а затем пробуждаем, отключая режим энергосбережения.
Затем я вывожу строку («Hello world») в верхней части дисплея и рисую круг в нижней части дисплея. Рекомендую вам ознакомиться со справочным руководством, чтобы найти дополнительные функции отрисовки, которые вы можете использовать.
В конце мы отправляем буфер на дисплей, чтобы данные буфера могли быть выведены на экране.
Результат
Ниже представлено изображение, которое демонстрирует результат выполнения приведенного выше кода.

Как вы можете видеть, на дисплее отображается строка «Hello World», а также кружок в нижней части экрана.
Библиотека прекрасно работает с OLED‑дисплеем SSD1306 с использованием аппаратного I2C.
Графический LCD-дисплей на HX1230
В этом разделе мы рассмотрим пример использования графического LCD‑дисплея на базе HX1230. Этот дисплей несколько отличается от тех, с которыми мы работали ранее. Ниже представлено изображение, иллюстрирующее формат инструкций для данного дисплея:

Как видно на изображении выше, HX1230 использует 9-битный формат инструкций (1D/C + 8 на данные). SPI STM32 поддерживает передачу данных как в 8-, так и в 16-битном формате. Мы конечно можем использовать 9-битную передачу с аппаратным SPI, однако это требует написания значительного количества кода для отслеживания количества переданных и оставшихся битов.
Вместо этого мы можем воспользоваться программным SPI и использовать готовые коммуникационные колбеки для выполнения 9-битной передачи. Это наглядно продемонстрировано на следующем рисунке:

Теперь, когда мы собираемся использовать программный SPI, нам необходимо настроить пины тактов и данных в качестве выходных в CubeMX.

Я настроил пины PA5 (SCK), PA7 (SDA), PA9 (RESET), PC7 (DC) и PB6 (CS) в качестве выходных.
Перейдите в конфигурацию GPIO и установите для этих выводов скорость Very High.
Ниже представлено изображение, демонстрирующее соединение между дисплеем и микроконтроллером nucleo F446.

Подключение осуществляется аналогично тому, как это происходит и в случае SPI. На изображении показан LCD‑дисплей Nokia 5110, но схема остается неизменной и для HX1230. Единственное отличие заключается в отсутствии пина DC в HX1230, поэтому на схеме он не подключен.
Код
Мы опять начнем с функции GPIO and Delay колбека.
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_DELAY_NANO: // Задержка arg_int * 1 наносекунда
asm("NOP");
break;
case U8X8_MSG_DELAY_100NANO: // Задержка arg_int * 100 наносекунд
for (int i=0; i<30; i++)asm("NOP");
break;
case U8X8_MSG_DELAY_MILLI:
HAL_Delay(arg_int);
break;
case U8X8_MSG_GPIO_SPI_DATA:
HAL_GPIO_WritePin(SDA_GPIO_Port, SDA_Pin, arg_int);
break;
case U8X8_MSG_GPIO_SPI_CLOCK:
HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, arg_int);
break;
case U8X8_MSG_GPIO_CS:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, arg_int);
break;
case U8X8_MSG_GPIO_RESET:
HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, arg_int);
break;
}
return 1;
}
Шаблон для этого колбека приведен в самом руководстве по портированию. Здесь мы определим только те сообщения, которые нам нужны для дисплея, использующего программный SPI.
U8×8_MSG_DELAY_NANO используется для генерации задержки в наносекундах. Это необходимо для генерации тактовых сигналов SPI. Хотя нам и не нужна точность до наносекунды, она должна быть достаточно малой, чтобы обеспечить быстрые такты. Я просто выполняю здесь »No operation».
U8×8_MSG_DELAY_100NANO снова может быть использован для генерации тактовых сигналов. Здесь я также выполняю »No operation», но в цикле
for
.U8×8_MSG_DELAY_MILLI используется для определения задержки в миллисекундах. В данном случае мы будем использовать функцию HAL_Delay, чтобы указать задержку в миллисекундах. Параметр arg_int представляет собой количество миллисекунд.
U8×8_MSG_GPIO_SPI_DATA и U8×8_MSG_GPIO_SPI_CLOCK используются для установки или сброса выводов данных и синхронизации, что необходимо для генерации транзакции SPI. В этом случае параметр arg_int может быть либо 1 (установка пина), либо 0 (сброс пина). Мы просто вызовем функцию GPIO_WritePin, чтобы установить или сбросить соответствующий пин.
U8×8_MSG_GPIO_CS / RESET используются для установки или сброса соответствующих пинов. В этом случае параметр arg_int может быть либо 1 (установка пина), либо 0 (сброс пина). Мы просто вызовем функцию GPIO_WritePin, чтобы установить или сбросить соответствующий пин.
Ниже представлена функция main
:
#include "u8g2.h"
u8g2_t myDisplay;
int main()
{
...
u8g2_Setup_hx1230_96x68_f(&myDisplay, U8G2_R0, u8x8_byte_3wire_sw_spi, u8x8_gpio_and_delay);
u8g2_InitDisplay(&myDisplay); // Отправляем последовательность инициализации на дисплей. После этого дисплей переходит в спящий режим
u8g2_SetPowerSave(&myDisplay, 0); // Пробуждаем дисплей
u8g2_ClearDisplay(&myDisplay);
u8g2_SetFont(&myDisplay, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&myDisplay, 0,15,"Hello world");
u8g2_DrawCircle(&myDisplay, 60, 30, 10, U8G2_DRAW_ALL);
u8g2_SendBuffer(&myDisplay);
while (1) {}
}
Опять же, подробную последовательность запуска библиотеки вы можете найти здесь. Сначала нам нужно определить u8g2-структуру, которую я назвал myDisplay.
Внутри функции main
мы начинаем с настройки дисплея. Функция u8g2_Setup_hx1230_96×68_f принимает следующие параметры:
myDisplay — это u8g2-структура, которую мы определили ранее.
U8G2_R0 — это значение для ротации. Более подробную информацию о ротации вы можете найти здесь.
u8×8_byte_3wire_sw_spi — это процедура передачи байтов. Мы используем программный SPI, поэтому здесь мы будем использовать готовый коммуникационный колбек.
u8×8_gpio_and_delay — это процедура задержки и GPIO. Мы также уже определили этот колбек.
После настройки дисплея мы инициализируем его, а затем пробуждаем, отключая режим энергосбережения.
Затем я вывожу строку («Hello world») в верхней части дисплея и рисую круг в нижней части дисплея. Рекомендую вам ознакомиться со справочным руководством, чтобы найти дополнительные функции отрисовки, которые вы можете использовать.
Наконец, мы отправляем буфер на дисплей, чтобы данные буфера могли быть выведены на экране.
Результат
Ниже представлено изображение, демонстрирующее результат выполнения нашего кода.

Как вы можете видеть, на дисплее отображается строка «Hello World», а также кружок в нижней части экрана.
Эта библиотека прекрасно подходит для LCD‑дисплея HX1230, позволяя осуществлять управление через программный SPI.
Для тех, кто хочет углубить свои знания в области электроники и электротехники, мы подготовили специализированный вебинар. Вы узнаете, как использовать АЦП и ЦАП в приложениях на микроконтроллерах на практике, какие ошибки встречаются чаще всего и как их избежать. Присоединяйтесь к нам 14 апреля.
Подробнее про электротехнику и разработку для них приложений вы можете узнать в рамках онлайн-курсов от практикующих экспертов отрасли. Подробности в каталоге.
Habrahabr.ru прочитано 6841 раз