尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CH1115 OLED屏驱动实战:区别SSD1306,STM32与ESP32-S3代码详解

CH1115 OLED屏驱动实战:区别SSD1306,STM32与ESP32-S3代码详解 简介CH1115是一款常用的OLED显示驱动芯片本资源围绕1.29英寸OLED屏幕整理了一套完整的硬件驱动资料特别适合嵌入式开发入门者、单片机DIY爱好者以及需要快速完成显示驱动的工程人员。压缩包大小约62.96MB内容以驱动代码为主按代码、硬件说明与使用示例等模块组织便于按需查阅目前已有253人进行学习或下载。资料覆盖SPI与I2C两种通信方式兼顾裸屏与模块两种物理形态包含芯片初始化配置、底层读写时序、显示缓冲管理和字符/图形绘制等关键代码能够帮助读者绕开数据手册逐条对照的繁琐过程直接完成屏幕点亮与内容显示。对于刚接触OLED驱动的初学者这套资料可以有效节省排查接线错误和通信时序所花费的时间对于具备一定经验的开发者也可将其中的驱动架构与函数封装作为可复用模板加快后续工程开发也为自定义显示功能保留了清晰的扩展点。 最近在做一个低功耗桌面小摆件翻出了手头一块1.29寸OLED屏主控是CH1115。这本是很常见的驱动芯片结果我差点被它坑了——网上找资料十有八九是SSD1306的例程直接套上去屏幕死活不亮或者花屏。折腾了一晚上才搞定顺手把代码和踩坑记录整理成这篇博文。如果你手头也是CH1115驱动的1.29寸OLED或者正准备用这颗芯片驱动别的尺寸的屏这篇文章应该能帮你少走不少弯路。这块屏市面上通常叫“1.29寸OLED模块”分辨率96x964针SPI接口GND、VCC、SCL、SDA价格便宜显示效果细腻很多DIY温湿度计、小游戏机、桌面鱼缸都用它。有人会把它和0.96寸的SSD1306混淆实际上两者驱动命令完全不同不能直接通用。我这里提供的代码包括STM32 HAL库版本和ESP32-S3 Micropython版本以及完整的接线图和初始化命令序列拿到就能用。1. 这块屏到底特殊在哪CH1115的核心特性与选型逻辑1.1 和常见的SSD1306有什么本质区别STM32玩家接触最多的OLED屏是0.96寸、分辨率128x64、驱动芯片SSD1306。CH1115这颗芯片虽然也是单色OLED驱动但最大的区别在分辨率和接口协议上。CH1115支持最大96x96分辨率很多1.29寸、1.3寸的方屏用的就是它。接口上它原生支持6800/8080并行、SPI和I2C市面上卖的模块为了省引脚绝大多数做成4针SPISCL、SDA、VCC、GND注意这里没有DC引脚也没有RESET引脚和常见的SSD1306 7针模块不一样。从时序上对比SSD1306的SPI写命令需要DC引脚区分命令和数据而CH1115在4线SPI模式下靠的是命令码本身来区分。你写0x00开头的是命令后面跟的参数按命令需求解析。所以如果你把SSD1306的驱动库硬套过来芯片根本不认识你的命令流结果就是白屏或者显示奇怪的噪点。这点非常关键也是很多新手一上来就翻车的主要原因。1.2 为什么需要单独写驱动代码既然CH1115和SSD1306不兼容而且市面上CH1115的开源资料相对少那么自己写一套驱动就是刚需。我的选型逻辑很简单显示96x96单色SPI速率跑在当前主控支持范围内STM32F103跑到4MHz毫无压力不需要外部DC引脚代码量极简非常适合RAM和Flash都吃紧的小项目。还有一个隐性好处CH1115内置DC-DC升压电路模块上只需要3.3V供电不需要额外的高压电源这对锂电池供电的小设备很友好。我实测下来整屏全亮时电流大约12mA左右休眠模式可以压到微安级做电池供电完全没问题。2. 硬件连接与显存模型接线、原理和时序2.1 引脚定义与标准接线表市面常见的1.29寸CH1115 OLED模块4个引脚定义如下引脚名功能说明接STM32F103接ESP32-S3GND电源地GNDGNDVCC电源正3.3V-5V均可但推荐3.3V3.3V3.3VSCLSPI时钟线PB13 (SPI2_SCK)GPIO12SDASPI数据线单向写入PB15 (SPI2_MOSI)GPIO11注意这个模块没有CS片选引脚也没有DC引脚和RESET引脚所以只能一主一从直接独占SPI总线。如果你非要和别的SPI设备共总线需要外接CS控制模块上并没有引出操作会比较麻烦我建议直接独占一个SPI外设简单又稳定。2.2 CH1115的显存组织方式理解CH1115的显存模型是写驱动的基础。它内置96x96位的GRAM按页Page和列Column组织8个像素点为一页。96行分12页每页8行每行96列。画一个像素点的过程就是先算好这个点位于第几页、第几列然后通过命令将页地址和列地址指针定位过去再写入一个字节8bit。其中某个bit为1对应像素点亮为0对应像素点灭。这和SSD1306的显存模型非常像所以一旦绕过命令字差异写画点函数反而很顺手。但要注意CH1115的显存地址是从右往左还是从左往右取决于命令0xA0/0xA1的设置默认状态下我建议按下面的命令序列来实测坐标顺序和屏幕物理方向是对应的。2.3 SPI时序参数与注意事项CH1115的SPI模式是Mode 0和Mode 3都支持但看数据手册CPOL0、CPHA0即Mode 0最稳。时钟频率上限理论上可以到几十MHz但实际模块上的走线和排针焊接质量参差不齐我最高跑过8MHz显示正常再往上到12MHz时偶尔会出雪花点。工程化建议跑4MHz留给信号完整性足够余量。另外虽然模块没有RESET引脚但上电后主控最好先做一次软件复位靠命令0xE2完成然后延时100ms再初始化。我遇到过直接上电不复位的时候首次刷新偶发花屏加了软件复位之后问题消失。3. HAL库与裸机示例代码从初始化到画图3.1 基于STM32CubeMX的底层配置创建一个STM32F103C8T6工程在CubeMX里做如下配置SPI2选择Transmit Only Master模式速率4MHzCPOLCPHA0数据帧8bitMSB First。PB14不接不用配置。时钟树保持默认APB1总线给到36MHz即可。工程生成后在main.c里添加OLED的驱动文件。很多人的误区在于OLED模块不读数据所以用“Transmit Only”模式就够了不需要开RX。开了Receive反而可能因为没接MISO而悬空造成SPI外设状态异常。生成工程后SPI初始化函数长这样void MX_SPI2_Init(void) { hspi2.Instance SPI2; hspi2.Init.Mode SPI_MODE_MASTER; hspi2.Init.Direction SPI_DIRECTION_2LINES_TX_ONLY; hspi2.Init.DataSize SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity SPI_POLARITY_LOW; hspi2.Init.CLKPhase SPI_PHASE_1EDGE; hspi2.Init.NSS SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi2.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi2); }3.2 核心命令序列与初始化代码CH1115初始化序列我参考的是屏厂出厂资料里的标准配置兼容市面上绝大多数的1.29寸模块代码注释里标注了每个命令的作用。// 发送一个字节区分命令还是数据 // CH1115在4线SPI下需要先发命令码再按需发参数 void OLED_WriteCmd(uint8_t cmd) { uint8_t tmp cmd; HAL_SPI_Transmit(hspi2, tmp, 1, 100); } void OLED_WriteData(uint8_t dat) { uint8_t tmp dat; HAL_SPI_Transmit(hspi2, tmp, 1, 100); } // 初始化 void OLED_Init(void) { HAL_Delay(100); OLED_WriteCmd(0xE2); // 软件复位 HAL_Delay(100); OLED_WriteCmd(0x2F); // 开启内部DC-DC升压 OLED_WriteCmd(0x81); // 设置对比度 OLED_WriteCmd(0x3F); // 对比度值可调0x00-0x7F OLED_WriteCmd(0xA0); // 列地址映射默认方向 OLED_WriteCmd(0xC8); // 行扫描方向上下翻转 OLED_WriteCmd(0xA8); // 设置multiplex OLED_WriteCmd(0x3F); // 1/64 duty OLED_WriteCmd(0x00); // 列低地址设置 OLED_WriteCmd(0x10); // 列高地址设置 OLED_WriteCmd(0xB0); // 页地址设置从第0页开始 OLED_WriteCmd(0x40); // 显示起始行 OLED_WriteCmd(0xA4); // 从GRAM显存显示 OLED_WriteCmd(0xA6); // 正常显示不反色 OLED_WriteCmd(0xAF); // 开显示 OLED_Clear(); }这段初始化命令我逐条核对过数据手册。比较关键的是0x2F这颗芯片必须开启内部DC-DC否则屏幕即使初始化了也完全不亮。有些屏幕模块板上已经把DC-DC相关引脚拉好了但为了兼容性我在驱动里总是把这一条加上去。3.3 画点、清屏、全屏填充与字符显示画点函数是OLED驱动的地基。先把坐标变换换算成页和列然后读出该页当前内容用局部数组缓存通过按位与和按位或把对应像素点改掉最后写回屏幕。由于没有读GRAM的指令必须用软件缓存记录当前屏幕内容这也是一个容易出错的点。static uint8_t display_buf[12][96]; // 12页 * 96列软件缓存 void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t on) { if (x 96 || y 96) return; uint8_t page y / 8; uint8_t bit y % 8; if (on) { display_buf[page][x] | (0x01 bit); } else { display_buf[page][x] ~(0x01 bit); } // 定位到该页该列 OLED_WriteCmd(0x00 | (x 0x0F)); // 列低地址 OLED_WriteCmd(0x10 | ((x 4) 0x0F)); // 列高地址 OLED_WriteCmd(0xB0 | page); // 页地址 OLED_WriteData(display_buf[page][x]); // 写回该字节 }清屏函数就是把缓存数组全部清零然后按页刷新。全屏填充则是把缓存全部置0xFF然后统一刷新。字符显示需要取模工具生成字模我用的是PCtoLCD2002配置为“纵向取模字节倒序”生成的字模按页存储和显存格式直接对应。void OLED_Clear(void) { memset(display_buf, 0, sizeof(display_buf)); OLED_Refresh(); } void OLED_Refresh(void) { for (uint8_t page 0; page 12; page) { OLED_WriteCmd(0xB0 | page); OLED_WriteCmd(0x00); OLED_WriteCmd(0x10); for (uint8_t col 0; col 96; col) { OLED_WriteData(display_buf[page][col]); } } }有些简化的驱动每画一个点就整屏刷新那个闪烁基本没法看。我这个设计只刷新被修改的那一个字节局部更新非常流畅做动态数字显示、小动画都够用。3.4 命令地址计算的一个关键细节这里插一个问题为什么列地址要分成低4位和高4位两条命令因为CH1115的GRAM列地址范围是0到95需要7位二进制表示。而它的列地址命令设计成了0x00-0x0F表示低4位0x10-0x1F表示高4位组合起来最大能到255列。96的二进制是0b1100000所以列地址计算时低地址用x 0x0F高地址用(x 4) 0x0F命令基地址分别是0x00和0x10不能搞混。页地址则是0xB0加上页号范围0xB0到0xBB。4. 从STM32到ESP32-S3的移植与Micropython补充4.1 移植到ESP32-S3MicropythonSTM32的HAL库版本调通之后我又在ESP32-S3开发板上用Micropython实现了一版。为什么用Micropython因为ESP32-S3的固件生态已经很成熟Micropython直接支持machine.SPI代码量少很多适合快速验证屏幕是否正常也适合做小项目原型。SPI接线GPIO12接SCLGPIO11接SDA3.3V和GND对应接好。from machine import Pin, SPI import time spi SPI(2, baudrate4000000, polarity0, phase0, sckPin(12), mosiPin(11)) # 注意这个模块没有MISO所以不用接 def cmd(byte): spi.write(bytes([byte])) def data(byte): spi.write(bytes([byte])) def oled_init(): time.sleep_ms(100) cmd(0xE2) time.sleep_ms(100) cmd(0x2F) cmd(0x81) cmd(0x3F) cmd(0xA0) cmd(0xC8) cmd(0xA8) cmd(0x3F) cmd(0x00) cmd(0x10) cmd(0xB0) cmd(0x40) cmd(0xA4) cmd(0xA6) cmd(0xAF) def oled_clear(): for page in range(12): cmd(0xB0 | page) cmd(0x00) cmd(0x10) for col in range(96): data(0x00) oled_init() oled_clear()Micropython的底层SPI时序由固件完成不需要像裸机那样手工控制时钟移植时唯一要注意的是baudrate不能设太高实测4MHz稳定8MHz偶尔出现花屏。4.2 从逻辑层隔离驱动接口STM32版和Micropython版虽然代码风格差别很大但逻辑结构完全一致初始化命令序列相同、地址计算方法相同、刷新顺序相同。这就是我把驱动分成上下两层的意义底层只做“发一个命令字节/数据字节”上层做显存管理和图形绘制。后面如果换平台比如换到Arduino、树莓派Pico只需要重写底层两个函数即可上层逻辑基本可以原封不动搬过去。我个人的习惯是每次接一个新的OLED屏幕模块第一件事就是写好Init命令序列然后用一个测试函数逐页填充0xFF确认“全亮”正常再做局部像素测试。这样能最快定位是硬件接线问题、初始化命令问题还是坐标计算问题。5. 实战中踩过的坑常见问题与排查心得5.1 屏幕完全不亮但电源指示灯正常这个是遇到最多的情况根据我的经验按概率从高到低排列现象可能原因解决思路什么反应都没有SCL/SDA接反交换两根线试试模块丝印有时候会标错什么反应都没有没有开DC-DC升压命令0x2F没执行确认初始化序列里包含0x2F有背光但无内容对比度调太低命令0x81参数小于0x10把对比度参数调到0x3F以上有内容但非常暗VCC供电不足或线太细用杜邦线尽量短或者直接焊线初始化后花屏SPI速率过高降低到1MHz试一下排除信号完整性问题我最开始就栽在0x2F这个命令上。当时图省事直接拿SSD1306的初始化序列改的SSD1306用0x8D开DC-DCCH1115完全不吃这一套。后来翻手册才发现CH1115要0x2F加上去之后屏幕瞬间就亮了。5.2 显示错位和上下颠倒屏幕能亮但显示内容和预期位置对不上这种问题基本都是坐标映射方向搞反了。CH1115有几组方向控制命令0xA0/0xA1列方向、0xC0/0xC8行扫描方向、0x40-0x7F显示起始行。如果屏幕物理安装方向是倒着的有两种处理方式改代码里的扫描方向命令或者把屏幕旋转180度安装。改代码更推荐只需要把0xA0改成0xA1、0xC8改成0xC0以及把0x40改成0x50或者你需要的起始行不需要动取模数据。5.3 引脚接触不良与焊接问题我用的这块1.29寸OLED模块是排针焊接的焊接时间过长或温度过高会损坏FPC排线导致部分行显示缺线。排查时先用手轻轻按压排线接口位置如果显示恢复正常就是接触问题直接补焊。另外屏幕的3.3V供电要尽量干净如果电源纹波大可能出现随机噪点。实测用AMS1117稳压输出和用充电宝输出显示稳定性差异明显前者明显更干净。5.4 代码调试时的最小验证法最后分享一个调试思路。如果屏幕不亮或者显示不对不要急着翻代码先做一个“最小验证”初始化后把第0页第0列的字节写入0xFF然后看屏幕左上角有没有出现一个8像素高的白条。如果没有说明底层通信或初始化有问题如果有说明坐标方向和地址计算有问题。这个方法能快速分离问题层级省下大量盲目调试的时间。5.5 一个容易被忽略的细节上电时序CH1115要求VCC稳定后再开始初始化两者时间差至少要有几十毫秒。如果主控和屏幕共用同一个电源上电瞬间电压爬升慢而主控已经开始跑初始化代码了就会出现偶发无法点亮的情况。解决方法是初始化前加200ms延时或者让屏幕单独由GPIO控制供电先开屏幕电源延时后再跑初始化。6. 代码获取与实际应用扩展6.1 完整工程文件结构我整理的这套资料包含以下内容STM32F103C8T6版基于HAL库的完整工程含CubeMX配置文件、oled.c、oled.h、font.h内置8x8和16x16字符、数字、常用符号ESP32-S3版Micropython脚本含oled_driver.py和main.py示例数据手册CH1115官方英文版datasheet和屏厂中文标注版取模工具配置PCtoLCD2002的推荐配置截图6.2 后续可以扩展的方向驱动跑通之后这块96x96的屏幕玩法很多。我接下来打算做一个低功耗温湿度卡片用ESP32-S3的deep sleep模式每30秒唤醒一次刷新显示温度、湿度、电池电量和时间平均电流可以做到几十微安。OLED模块本身功耗低CH1115还有sleep命令0xAE可以进一步压功耗。如果你也想深入玩这颗屏幕建议重点看两个方向一是自己用duty和对比度命令调整显示效果来省电二是用DMAPWM把SPI刷新做到后台执行CPU零负担地跑动画。我这边已经把DMA版本的刷新代码调试得差不多了后面有机会单独写一篇分享。最后再说一句题外话屏幕驱动这种底层活儿难不在“会点灯”而在“出了问题怎么定位”。上面这些命令序列和调试方法都是实打实从示波器和反复试错里熬出来的经验你照着做大概率能少折腾一晚上。本文还有配套的精品资源点击获取
返回列表