1. SH1107 OLED屏基础解析
第一次接触SH1107驱动的OLED屏时,我被它独特的页地址模式搞得一头雾水。这种1.3寸的小屏幕虽然分辨率只有64x128,但要想完全掌握它的显示原理,得从最底层的寄存器操作开始理解。SH1107芯片最大支持128x128的矩阵面板,但我们常见的配置是64列x128行,对应16页(Page0-Page15),每页8行。
页地址模式(0x20命令设置)是SH1107最常用的工作方式。在这个模式下,数据写入后列地址指针会自动递增,但页地址保持不变。想象一下写字时的场景:你从左到右写满一行后,必须手动把笔移到下一行的开头才能继续写——这就是页地址模式的工作逻辑。具体到代码实现,我们需要先设置目标页地址(0xB0-0xBF),再分别设置列地址的高4位(0x10命令)和低4位(0x00命令)。
实际项目中我遇到一个典型问题:明明发送了正确的数据,屏幕上却显示错位。后来发现是列地址设置出了问题——SH1107的列地址范围是00H-7FH,但我的屏幕实际只有64列(00H-3FH)。如果错误设置了超出范围的列地址,数据就会被写入"看不见"的区域。这个坑让我花了整整一个下午调试,所以特别提醒新手要注意自己屏幕的实际分辨率。
2. 底层驱动函数实现
2.1 坐标设置函数优化
基础的OLED_Set_Pos函数虽然只有几行代码,但藏着不少玄机。原始实现是这样的:
void OLED_Set_Pos(unsigned char x, unsigned char y) { OLED_WR_Byte(0xb0+y, OLED_CMD); OLED_WR_Byte(((x&0xf0)>>4)|0x10, OLED_CMD); OLED_WR_Byte(x&0x0f, OLED_CMD); }这个函数有三个关键点容易出错:
- 页地址计算:0xB0是基准值,加上y偏移量。但要注意y不能超过15(0x0F)
- 列地址高4位:需要右移4位后与0x10进行或运算
- 列地址低4位:直接取x的低4位
在项目实践中,我对这个函数做了两点优化:
- 增加边界检查,防止坐标越界导致显示异常
- 添加显式类型转换,避免某些编译器下的隐式转换问题
优化后的版本:
void OLED_Set_Pos(uint8_t x, uint8_t y) { if(y > 15) y = 15; if(x > 63) x = 63; // 根据实际屏幕分辨率调整 OLED_WR_Byte(0xB0 | (y & 0x0F), OLED_CMD); OLED_WR_Byte(0x10 | ((x >> 4) & 0x03), OLED_CMD); // 高4位 OLED_WR_Byte(x & 0x0F, OLED_CMD); // 低4位 }2.2 数据写入时序优化
SH1107的数据写入时序对显示效果影响很大。最初我使用简单的延时方式:
void OLED_WR_Byte(uint8_t dat, uint8_t cmd) { I2C_Start(); I2C_WriteByte(0x78); // 设备地址 I2C_WriteByte(cmd ? 0x00 : 0x40); // 命令/数据标志 I2C_WriteByte(dat); I2C_Stop(); delay_us(2); }后来发现这种实现有两个问题:
- 固定延时效率低下,影响刷新率
- 没有检查ACK应答,可能导致数据丢失
改进后的版本加入了硬件I2C的超时检测:
#define I2C_TIMEOUT 1000 void OLED_WR_Byte(uint8_t dat, uint8_t cmd) { uint32_t timeout = I2C_TIMEOUT; while(I2C_GetFlagStatus(I2C_FLAG_BUSY) && timeout--); I2C_GenerateSTART(ENABLE); timeout = I2C_TIMEOUT; while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT) && timeout--); I2C_Send7bitAddress(0x78, I2C_Direction_Transmitter); timeout = I2C_TIMEOUT; while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) && timeout--); I2C_SendData(cmd ? 0x00 : 0x40); timeout = I2C_TIMEOUT; while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED) && timeout--); I2C_SendData(dat); timeout = I2C_TIMEOUT; while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED) && timeout--); I2C_GenerateSTOP(ENABLE); }3. 字符显示的实现与优化
3.1 字符取模原理
显示字符前需要先获取字模数据。PCtoLCD2002是最常用的取模软件,但它的设置项很容易让人困惑。经过多次尝试,我总结出最佳配置:
- 字模排列方式:逐列式
- 取模走向:逆向(低位在前)
- 输出数制:十六进制
- 自定义格式:去掉逗号和0x前缀
对于8x16英文字符,取模时要注意:
- 字宽设为8,字高设为16
- 每字符实际生成16字节数据(2页x8字节)
- 第一页显示上半部分,第二页显示下半部分
3.2 字符显示函数改进
基础字符显示函数存在几个效率问题:
- 每次显示字符都要重新设置坐标
- 没有利用页地址模式的自动列递增特性
- 字体切换不够灵活
优化后的实现增加了以下特性:
- 支持字符间距调整
- 自动换行处理
- 多种字体混合显示
typedef struct { const uint8_t *font_table; uint8_t width; uint8_t height; uint8_t spacing; } FontDef; FontDef Font_6x8 = {F6x8, 6, 8, 1}; FontDef Font_8x16 = {F8X16, 8, 16, 1}; void OLED_ShowChar(uint8_t x, uint8_t y, char ch, FontDef font) { uint8_t i, page_end = y + (font.height / 8); if(x > 63 || y > 15) return; for(uint8_t page = y; page < page_end; page++) { OLED_Set_Pos(x, page); uint16_t offset = (ch - 32) * font.height + (page - y) * font.width; for(i = 0; i < font.width; i++) { OLED_WR_Byte(font.font_table[offset + i], OLED_DATA); } } }4. 高级图形显示技术
4.1 图片显示优化
原始的BMP图片显示函数虽然简单,但存在明显缺陷:
- 没有边界检查
- 无法局部更新
- 内存占用高
改进方案采用分块加载技术:
void OLED_DrawBMP(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, const uint8_t *bmp) { uint16_t j = 0; uint8_t x, y, width = x1 - x0; for(y = y0; y < y1; y++) { OLED_Set_Pos(x0, y); for(x = x0; x < x1; x++) { OLED_WR_Byte(bmp[j++], OLED_DATA); // 每传输64字节加入短暂延时 if((j % 64) == 0) delay_us(10); } } }4.2 动态刷新优化
要实现流畅的动画效果,需要特别注意:
- 双缓冲技术:在内存中维护两个显示缓冲区
- 差异刷新:只更新发生变化的部分区域
- 定时同步:控制刷新率在30-60fps之间
实现代码框架:
uint8_t buffer1[1024], buffer2[1024]; uint8_t *front_buffer = buffer1; uint8_t *back_buffer = buffer2; void OLED_Refresh(void) { static uint8_t dirty_pages = 0xFF; // 初始全部刷新 for(uint8_t page = 0; page < 16; page++) { if(dirty_pages & (1 << page)) { OLED_Set_Pos(0, page); for(uint8_t col = 0; col < 64; col++) { uint16_t addr = page * 64 + col; OLED_WR_Byte(front_buffer[addr], OLED_DATA); } } } dirty_pages = 0; } void OLED_SwapBuffers(void) { uint8_t *temp = front_buffer; front_buffer = back_buffer; back_buffer = temp; dirty_pages = 0xFF; // 标记所有页需要刷新 }5. 性能优化实战技巧
5.1 通信速率优化
SH1107支持400kHz的I2C高速模式,但需要硬件支持。在STM32上配置示例:
void I2C_Configuration(void) { I2C_InitTypeDef I2C_InitStruct; I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStruct.I2C_OwnAddress1 = 0x00; I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStruct.I2C_ClockSpeed = 400000; // 400kHz I2C_Init(I2C1, &I2C_InitStruct); I2C_Cmd(I2C1, ENABLE); }5.2 显示缓存管理
高效的缓存管理可以大幅提升性能:
- 按页组织缓存结构
- 使用位操作快速修改像素
- 实现区域更新标记
#define PAGE_SIZE 64 #define TOTAL_PAGES 16 typedef struct { uint8_t data[PAGE_SIZE]; bool dirty; } OLED_Page; OLED_Page oled_pages[TOTAL_PAGES]; void OLED_DrawPixel(uint8_t x, uint8_t y, bool set) { if(x >= 64 || y >= 128) return; uint8_t page = y / 8; uint8_t bit_mask = 1 << (y % 8); if(set) { oled_pages[page].data[x] |= bit_mask; } else { oled_pages[page].data[x] &= ~bit_mask; } oled_pages[page].dirty = true; } void OLED_RefreshPartial(void) { for(uint8_t page = 0; page < TOTAL_PAGES; page++) { if(oled_pages[page].dirty) { OLED_Set_Pos(0, page); for(uint8_t col = 0; col < PAGE_SIZE; col++) { OLED_WR_Byte(oled_pages[page].data[col], OLED_DATA); } oled_pages[page].dirty = false; } } }6. 常见问题排查
调试SH1107时最常遇到的三个问题:
- 屏幕无任何显示
- 检查电源电压(通常需要3.3V)
- 确认I2C地址(通常是0x3C或0x3D)
- 验证复位信号时序
- 显示内容错位
- 检查页地址和列地址设置
- 确认屏幕实际分辨率
- 验证字模数据的排列方式
- 显示闪烁或残影
- 优化刷新时序
- 增加显示缓冲区
- 调整对比度设置(0x81命令)
一个实用的调试技巧:先实现一个简单的测试图案显示函数,可以快速验证硬件连接和基础功能:
void OLED_TestPattern(void) { for(uint8_t page = 0; page < 16; page++) { OLED_Set_Pos(0, page); for(uint8_t col = 0; col < 64; col++) { uint8_t pattern = (page << 4) | (col & 0x0F); OLED_WR_Byte(pattern, OLED_DATA); } } }7. 项目实战应用
在智能家居项目中,我使用SH1107实现了多级菜单系统。关键实现要点:
- 菜单数据结构设计
typedef struct { const char *title; const MenuItem *items; uint8_t item_count; int8_t selected; } Menu; typedef struct { const char *text; void (*action)(void); const Menu *submenu; } MenuItem;- 菜单渲染优化
void OLED_DrawMenu(const Menu *menu) { uint8_t start_item = 0; // 计算可见区域 if(menu->selected > 3) { start_item = menu->selected - 3; } // 绘制菜单项 for(uint8_t i = 0; i < 6 && (start_item + i) < menu->item_count; i++) { uint8_t y = i * 2; bool selected = (start_item + i) == menu->selected; if(selected) { OLED_FillRect(0, y*8, 64, 16, true); OLED_ShowStr(2, y, menu->items[start_item + i].text, &Font_6x8, false); } else { OLED_ShowStr(2, y, menu->items[start_item + i].text, &Font_6x8, true); } } // 绘制滚动条 OLED_DrawScrollBar(60, 0, 48, menu->selected, menu->item_count); }- 用户输入处理
void Menu_HandleInput(Menu *menu, InputEvent event) { switch(event) { case INPUT_UP: if(menu->selected > 0) menu->selected--; break; case INPUT_DOWN: if(menu->selected < menu->item_count - 1) menu->selected++; break; case INPUT_SELECT: if(menu->items[menu->selected].action) { menu->items[menu->selected].action(); } else if(menu->items[menu->selected].submenu) { current_menu = menu->items[menu->selected].submenu; } break; case INPUT_BACK: current_menu = parent_menu; break; } }这套驱动代码经过三个量产项目验证,在STM32F103和ESP32平台上均稳定运行。最关键的优化点是实现了局部刷新和双缓冲,使得菜单操作非常流畅,即使在低端MCU上也能达到30fps的刷新率。