本教程描述了如何在带有 PIC24FJ 微控制器的小型 128x64 OLED 显示器上使 LVGL 工作。这里使用的 OLED 显示板具有 SH1106 驱动芯片,但代码几乎不需更改即可与 SSD1306 一起使用,并且应该很容易适应其他显示驱动程序。你应该首先阅读 LVGL 移植指南,因为本教程仅涵盖单色 OLED 显示器所需的额外注意事项。
显示内存组织#
SH1106 包含一个帧缓冲,排列成"页",这些页是水平的 8 像素条纹。每个页延伸到显示器的全宽(128 列)。对显示帧缓冲的写入通过选择列号和页号完成,然后发送包含每个 8 像素列的像素的数据字节。可以通过传输更多数据字节来发送任意数量的列,直到页的末尾。
配置 lv_conf.h#
第一个要设置的文件是 lv_conf.h。主要设置如下所示。由于 PIC 的资源有限,LV_MEM_SIZE 设置得相当小。LV_COLOR_DEPTH 设置为 1 位,因为显示器是单色的,LV_VDB_PX_BPP 设置为 1,以便 VDB 每个像素仅使用一位。VDB 大小设置为 2048 像素(256 字节)。诸如阴影和抗锯齿之类的花哨效果被禁用(它们在单色中没有意义),并且除了 "MONO" 之外的大多数主题都被关闭。
#define LV_MEM_SIZE (2U * 1024U) /*`lv_mem_alloc` 使用的内存大小(字节)(>= 2kB)*/
#define LV_HOR_RES (128)
#define LV_VER_RES (64)
#define LV_ANTIALIAS 0 /*1:启用抗锯齿*/
#define LV_COLOR_DEPTH 1 /*色深:1/8/16/32*/
#define USE_LV_SHADOW 0 /*1:启用阴影*/
#define USE_LV_GROUP 0 /*1:启用对象组(用于键盘)*/
#define USE_LV_GPU 0 /*1:启用 GPU 接口*/
#define USE_LV_REAL_DRAW 0 /*1:启用直接绘制到帧缓冲而不是 VDB 的函数(如果 LV_VDB_SIZE = 0 则需要)*/
#define USE_LV_FILESYSTEM 0 /*1:启用文件系统(图像可能需要)*/
#define USE_LV_LOG 0 /*启用/禁用日志模块*/
// 我关闭了所有其他主题
#define USE_LV_THEME_MONO 1 /*用于单色显示器的单色主题*/c显示驱动程序回调函数#
接下来,将 lv_porting/lv_port_disp_templ.c 复制到 lv_port_disp.c 并添加三个函数:
set_px_cb()- 每当 LVGL 想要将像素写入 VDB 时调用。处理将像素数据写入字节的正确格式以发送到显示器。对于接受垂直对齐像素的显示器(一个字节代表 8 像素垂直线),此回调函数正确地放置它们。flush_cb()- LVGL 使用此调用将区域 flush 到显示器。rounder_cb()- 确保发送到flush_cb()的区域始终是整数页,因为显示器使用页进行更新。大多数单色显示器仅接受至少 1 字节宽的数据,因此 LVGL 尝试刷新 3x5 像素的区域没有意义。此回调函数将缓冲区区域舍入到最近的页(垂直字节边界)。
set_px_cb() 回调函数#
此回调函数在传递的缓冲区中设置正确的位,使用 SH1106 使用的页排列。它接收 7 个参数:
disp_drv:显示驱动程序结构引用(在此实现中未使用)buf:我们应该更改像素的缓冲区(对在 LVGL 初始化期间指示的可能更大缓冲区的一部分的引用)buf_w:从buf开始的区域宽度,组成它的像素列数x和y:要更改的像素的坐标color:像素的值,0 或 1。当LV_COLOR_DEPTH为 1(单色)时,lv_color_t类型折叠为具有单个有效位字段的联合opa:颜色不透明度(在单色配置中未使用)
请注意,此实现反转颜色以实现所需的视觉效果:
#define BIT_SET(a,b) ((a) |= (1U<<(b)))
#define BIT_CLEAR(a,b) ((a) &= ~(1U<<(b)))
void set_px_cb(struct _disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
lv_color_t color, lv_opa_t opa) {
uint16_t byte_index = x + (( y>>3 ) * buf_w);
uint8_t bit_index = y & 0x7;
// == 0 反转,所以我们在黑色背景上得到蓝色
if ( color.full == 0 ) {
BIT_SET( buf[ byte_index ] , bit_index );
}
else {
BIT_CLEAR( buf[ byte_index ] , bit_index );
}
}cflush_cb() 回调函数#
此回调函数计算需要写入的页范围(row1 → row2),发送起始行和列的 OLED 命令,然后发送字节(每个 8 像素列一个)并结束传输。
它需要 3 个参数:
disp_drv:显示驱动程序结构引用(在此实现中未使用)area:要刷新的屏幕区域的坐标,打包在lv_area_t结构中(应该已被 rounder 回调函数舍入)color_p:包含要 flush 的实际数据的缓冲区。一个可以安全转换为uint8_t的lv_color_t数组,用于单色显示器
static void flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
uint8_t row1 = area->y1>>3;
uint8_t row2 = area->y2>>3;
uint8_t *buf = (uint8_t*) color_p;
for(uint8_t row = row1; row <= row2; row++) {
oledStartSend( row, area->x1 );
for(uint16_t x = area->x1; x <= area->x2; x++) {
oledByteOut(*buf);
buf++;
}
oledEndSend();
}
/* 重要!!!
* 通知图形库你已完成 flush */
lv_flush_ready();
}crounder_cb() 回调函数#
最简单的函数,此回调函数将区域的 y 限制增加到最近的页边界(8 的下限和上限倍数,或字节边界):
void rounder_cb(struct _disp_drv_t * disp_drv, lv_area_t *area) {
area->y1 = (area->y1 & (~0x7));
area->y2 = (area->y2 & (~0x7)) + 7;
}c连接回调函数#
将此代码添加到 lv_port_disp_init() 函数:
lv_refr_set_round_cb( round_cb );
disp_drv_p->vdb_wr = vdb_wr;
disp_drv_p->disp_flush = disp_flush;cOLED 通信函数#
最后一个底层任务是实现 OLED 通信函数。这些特定于使用的微控制器和接口。以下是使用 SPI 的 PIC24FJ 的实现。可以使用增强型 FIFO 或 DMA 来增强:
#define SPI_DC LATAbits.LATA1
#define SPI_DATA() SPI_DC = 1
#define SPI_COMMAND() SPI_DC = 0
void oledByteOut( uint8_t data )
{
SPI1BUF = data;
while(SPI1STATbits.SPIRBF == 0);
data = SPI1BUF; // 也必须读取
}
void oledCommand( uint8_t data )
{
SPI_COMMAND();
oledByteOut(data);
}
void oledStartSend( uint8_t row, uint8_t col ) {
oledCommand( 0xB0 | row);
// 转到第一列
col += 2; // SH1106 的 +2 - SSD1306 删除
oledCommand( 0x00 | (col & 0xF) ); // col LS 4 位
oledCommand( 0x10 | ((col>>4) & 0xF) ); // col MS 4 位
SPI_DATA();
}
void oledEndSend() {
}
void oledInit()
{
TRISBbits.TRISB7 = 0; RPOR3bits.RP7R = 9; // RP7 是 GB002 上的引脚 16,9 选择 SS1OUT 来驱动片选
SPI1CON1 = 0; // 清除 SPI1CON1 寄存器的内容
SPI1CON1bits.CKP = 1; // 时钟的空闲状态为高电平;活动状态为低电平
SPI1CON1bits.CKE = 0; // 串行输出数据在从空闲时钟状态到活动时钟状态的转换时更改
SPI1CON1bits.MSTEN = 1; // 启用主模式
// oLed 说 600ns -> 1.67Mhz,因此使用 4:1,4:1 = 1MHz(pg180)
// 测量的周期为 750ns,即 1.3MHz - 不知道为什么
SPI1CON1bits.PPRE = 2; // 主预分频器 3 = 1:1,2 = 4:1
SPI1CON1bits.SPRE = 5; // 次预分频器 5 = 4:1
SPI1STATbits.SPIEN = 1; // 启用 SPI 外设
}c初始化应用程序#
最后一步是在主应用程序中初始化显示器后选择 MONO 主题:
oledInit();
static lv_disp_buf_t disp_buf; /*显示缓冲区的描述符 */
static lv_disp_drv_t disp_drv; /*显示驱动程序的描述符*/
static lv_color_t gbuf[DISP_BUF_SIZE]; /*用作显示缓冲区的内存区域 */
lv_init();
lv_disp_buf_init(&disp_buf, gbuf, NULL, DISP_BUF_SIZE); /* DISP_BUF_SIZE 可以小于实际显示器 */
lv_port_disp_init(&disp_drv);
// 设置 mono 主题 - 也许这对于 1 位/像素来说是默认的?
// 色调似乎没有影响
lv_theme_mono_init(0 /* hue */,NULL /* 使用 LV_FONT_DEFAULT */);
lv_theme_set_current( lv_theme_get_mono() );c初始化显示缓冲区时,你可以使用任何适合内存约束的大小。LVGL 将使用此内存范围绘制要刷新的控件。如果控件不适合提供的范围,它将在多次传递中绘制和 flush。为了最大化性能,使用整个屏幕大小(以字节为单位,因此像素除以 8):
#define DISP_BUF_SIZE (128*64/8)c总结#
本教程涵盖了将 LVGL 移植到具有基于页的内存组织的单色 OLED 显示器的基本步骤。关键点是:
- 为 1 位色深和单色主题配置
lv_conf.h - 实现三个回调函数:
set_px_cb()、flush_cb()和rounder_cb() - 为特定硬件实现显示通信函数
- 初始化 LVGL 并激活 MONO 主题
相同的方法适用于 SH1106 和 SSD1306 驱动程序,只需进行最小的修改(主要是列偏移调整)。这个基础可以通过调整回调函数中的内存组织细节来适应其他单色显示驱动程序。
