news 2026/4/14 19:13:24

从底层驱动到图形显示:SH1107 OLED屏的代码实现与优化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从底层驱动到图形显示:SH1107 OLED屏的代码实现与优化实践

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); }

这个函数有三个关键点容易出错:

  1. 页地址计算:0xB0是基准值,加上y偏移量。但要注意y不能超过15(0x0F)
  2. 列地址高4位:需要右移4位后与0x10进行或运算
  3. 列地址低4位:直接取x的低4位

在项目实践中,我对这个函数做了两点优化:

  1. 增加边界检查,防止坐标越界导致显示异常
  2. 添加显式类型转换,避免某些编译器下的隐式转换问题

优化后的版本:

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); }

后来发现这种实现有两个问题:

  1. 固定延时效率低下,影响刷新率
  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英文字符,取模时要注意:

  1. 字宽设为8,字高设为16
  2. 每字符实际生成16字节数据(2页x8字节)
  3. 第一页显示上半部分,第二页显示下半部分

3.2 字符显示函数改进

基础字符显示函数存在几个效率问题:

  1. 每次显示字符都要重新设置坐标
  2. 没有利用页地址模式的自动列递增特性
  3. 字体切换不够灵活

优化后的实现增加了以下特性:

  1. 支持字符间距调整
  2. 自动换行处理
  3. 多种字体混合显示
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图片显示函数虽然简单,但存在明显缺陷:

  1. 没有边界检查
  2. 无法局部更新
  3. 内存占用高

改进方案采用分块加载技术:

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 动态刷新优化

要实现流畅的动画效果,需要特别注意:

  1. 双缓冲技术:在内存中维护两个显示缓冲区
  2. 差异刷新:只更新发生变化的部分区域
  3. 定时同步:控制刷新率在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 显示缓存管理

高效的缓存管理可以大幅提升性能:

  1. 按页组织缓存结构
  2. 使用位操作快速修改像素
  3. 实现区域更新标记
#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时最常遇到的三个问题:

  1. 屏幕无任何显示
  • 检查电源电压(通常需要3.3V)
  • 确认I2C地址(通常是0x3C或0x3D)
  • 验证复位信号时序
  1. 显示内容错位
  • 检查页地址和列地址设置
  • 确认屏幕实际分辨率
  • 验证字模数据的排列方式
  1. 显示闪烁或残影
  • 优化刷新时序
  • 增加显示缓冲区
  • 调整对比度设置(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实现了多级菜单系统。关键实现要点:

  1. 菜单数据结构设计
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;
  1. 菜单渲染优化
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); }
  1. 用户输入处理
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的刷新率。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 19:05:12

Android 14以太网适配实战:新API解析与framework-connectivity-t编译排错指南

1. Android 14以太网适配的核心挑战 最近在给客户做Android 14系统移植时&#xff0c;遇到了以太网功能适配的棘手问题。相比Android 12及更早版本&#xff0c;Android 14在网络架构上做了大刀阔斧的改革&#xff0c;特别是以太网管理这块&#xff0c;简直像是换了一套全新的玩…

作者头像 李华
网站建设 2026/4/14 19:04:45

深度学习超参数、验证集与偏差-方差权衡(十八)

1. 定位导航 前几篇我们解决了"如何训练一个模型"。但实际项目中真正决定成败的,往往不是模型本身,而是 怎么调参 和 怎么评估。本篇覆盖: 超参数的本质(与参数的区别) 训练集 / 验证集 / 测试集三分法 K 折交叉验证(小数据救命稻草) 点估计、偏差、方差的统…

作者头像 李华
网站建设 2026/4/14 19:03:27

GEO数据挖掘避坑指南:从国内镜像源选择到表达矩阵提取(R语言版)

GEO数据挖掘实战&#xff1a;从镜像加速到表达矩阵的R语言高效处理 每次打开GEO数据库&#xff0c;就像走进了一个巨大的基因表达数据超市——货架上摆满了从癌症研究到神经退行性疾病的各类数据集。但当你兴奋地选中心仪的数据集准备下载时&#xff0c;却常常被缓慢的下载速度…

作者头像 李华
网站建设 2026/4/14 19:02:44

逻辑电平-秋招笔试题目记录

逻辑电平-秋招笔试题目记录记录秋招过程中遇到的选择题, 便于复习与总结.第 1 题 【题目】3.3V及以下的逻辑电平被称为低电压逻辑电平, 如: LVTTL电平(正确)A. 正确B. 错误 【答案】 A 【解析】 3.3V及以下逻辑电平被称低电压逻辑(Low Voltage Logic), 更具体一点, 3.3V及以下通…

作者头像 李华