简介:这份资源是为STM32F103开发者准备的OLED显示功能代码包,面向嵌入式入门及中级学习者,解决OLED屏幕驱动与基础图形、波形绘制的快速上手问题。资源包内共2个文件,分别为oled.c源码与oled.h头文件,代码结构简洁,可直接移植到基于HAL或LL库的工程中使用。压缩包大小仅5KB,轻量易读,适合作为学习SPI/I2C接口驱动、坐标映射及像素绘制的参考资料。目前已有815人学习,凭借清晰的函数分层与波形显示示例,开发者可快速实现文本显示、直线绘制及ADC数据波形可视化。代码预留了功能扩展空间,便于结合具体OLED分辨率与刷新率进行调优,是理解嵌入式图形界面和实时数据展示的实用入门素材。
1. 看到“STM32F103 OLED 显示函数”这个包,先想清楚驱动代码为什么不是复制粘贴完事
一个带 OLED 显示函数的 STM32F103 工程包,解压后一般就是OLED.c、OLED.h、字模数组和一张连线图。有人直接拖进自己工程,结果不是白屏就是程序跑飞。问题多半不在文件本身,而在你复制的“OLED 连线函数”和你板子的引脚、时钟、GPIO 模式不匹配。OLED 驱动本质是两件事:用 IIC 把字节送到 SSD1306 控制器,再把像素按页写入显存。接线错了是白屏,初始化序列错了是花屏,命令字和数据字搞混是乱码,刷新策略不对就是“波形像慢镜头”。这篇沿着 IIC 连线、SSD1306 初始化、波形显示这条线展开,把接线和参数讲细,让“显示函数”真正变成你能改、能查、能迁移的代码。
2. OLED 连线函数背后:IIC 通信协议 OLED 的接线、上拉与电平时序
2.1 IIC 通信协议 OLED 的引脚定义:SCL/SDA 该接哪、上拉为什么省不了
市面上常见的 0.96 寸 OLED 模块,IIC 模式引出四根线:VCC、GND、SCL、SDA。这里的 OLED 通信协议 IIC 只有两根信号线,时钟线 SCL 由主机(STM32F103)产生,数据线 SDA 双向传输。SSD1306 的 IIC 地址默认是 0x78(写地址),读地址 0x79,但实际驱动里我们只写不读。
接线表如下:
| OLED 引脚 | STM32F103 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 部分模块标 5V,长期使用建议仍接 3.3V |
| GND | GND | 共地必须接,否则时序参考电平不一致 |
| SCL | PB6 / 任意推挽或开漏 IO | 建议选带 5V 容忍且不占用调试口的引脚 |
| SDA | PB7 | 与 SCL 同组,方便代码统一操作 |
注意 F103 最小系统板的 3.3V 通常由 AMS1117 从 5V 转出,OLED 全屏点亮时电流约 15–25mA,对板载 LDO 来说压力不大,但如果你同时驱动多个外设,建议用万用表量一下 VCC 对地电压是否掉到 3.0V 以下。
模块上一般已经焊了 4.7kΩ 上拉电阻到 VCC,所以 SCL 和 SDA 不需要再外接上拉。若你用的是裸屏或自己画的转接板,必须在两根线上各加一个 4.7kΩ 上拉。这是因为 IIC 协议要求引脚在空闲时为高电平,开漏输出配合上拉才能正确产生高电平。
2.2 模拟 IIC 的连线函数实现:起始信号、结束信号与字节电平
STM32F103 有硬件 IIC 外设,但硬件 IIC 在标准外设库时代调试比较麻烦,工程里最常见的是 GPIO 模拟 IIC。模拟的好处是引脚随便换,时序亲眼可见,而且没有硬件 IIC 的忙标志卡死问题。用 PB6/PB7 模拟,代码量很小:
#define OLED_SCL_PORT GPIOB #define OLED_SCL_PIN GPIO_Pin_6 #define OLED_SDA_PORT GPIOB #define OLED_SDA_PIN GPIO_Pin_7 #define OLED_SCL_High() OLED_SCL_PORT->BSRR = OLED_SCL_PIN #define OLED_SCL_Low() OLED_SCL_PORT->BRR = OLED_SCL_PIN #define OLED_SDA_High() OLED_SDA_PORT->BSRR = OLED_SDA_PIN #define OLED_SDA_Low() OLED_SDA_PORT->BRR = OLED_SDA_PIN void OLED_IIC_Delay(void) { uint8_t i = 4; while (i--); } void OLED_IIC_Start(void) { OLED_SDA_High(); OLED_SCL_High(); OLED_IIC_Delay(); OLED_SDA_Low(); // SCL 高电平期间 SDA 拉低,产生起始信号 OLED_IIC_Delay(); OLED_SCL_Low(); // 拉低 SCL,为发送数据做准备 } void OLED_IIC_Stop(void) { OLED_SDA_Low(); OLED_SCL_High(); OLED_IIC_Delay(); OLED_SDA_High(); // SCL 高电平期间 SDA 拉高,产生停止信号 } void OLED_IIC_SendByte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { if (dat & 0x80) OLED_SDA_High(); else OLED_SDA_Low(); dat <<= 1; OLED_IIC_Delay(); OLED_SCL_High(); // SCL 高电平期间数据必须稳定 OLED_IIC_Delay(); OLED_SCL_Low(); OLED_IIC_Delay(); } }这段代码里最关键的是时序:起始信号要求 SCL 在高电平时把 SDA 拉低,字节发送要求 SCL 上升沿之前把 SDA 电平摆好,数据在 SCL 高电平期间保持不变。OLED_IIC_Delay是软件延时,F103 主频 72MHz 时循环 4 次大约产生 1–2μs 的延时,正好把 IIC 时钟压到 400kHz 以内。
写字节时不需要检测 ACK。SSD1306 在收到每个字节后会拉低 SDA 作为应答,但很多 OLED 模块的应答引脚没有真正接出来,你如果写代码去等 ACK 超时,程序就会卡死在 IIC 发送函数里。这也是“显示函数加进工程就卡死”的一个隐藏原因,工程包里现成的 IIC 发送函数一般不会带 ACK 等待,别自己加上。
2.3 stm32f103 上 HAL 库与标准库的 GPIO 初始化差异
标准外设库 v3.5 和 HAL 库的 GPIO 初始化写法不同,但开漏输出、上拉、50MHz 速度这三项要一致。用标准库写:
GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);如果工程是用 STM32CubeMX 生成的 HAL 库工程,则对应写成:
GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull = GPIO_PULLUP; // 模块没上拉时依赖内部上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);模式必须选开漏输出,不能选推挽输出。推挽输出会把 SDA 强制拉高或拉低,如果 OLED 模块同时也在驱动 SDA,两个输出源打架,轻则波形畸变,重则引脚发热甚至损坏。内部上拉只是兜底,模块自带 4.7k 上拉时Pull选GPIO_PULLUP也没有副作用。
3. OLED 显示函数的封装顺序:从 SSD1306 初始化到汉字与字符串
3.1 SSD1306 初始化命令序列:先开电荷泵,再谈点亮 oled 屏幕全流程
点亮 oled 屏幕全流程的第一步不是写像素,而是给 SSD1306 发初始化序列。OLED 驱动芯片上电后默认处于关闭状态,必须按顺序设置时钟、复用比、电荷泵、对比度,最后用0xAF打开显示。初始化命令用前面写的OLED_IIC_SendByte发送,但要注意每个命令前面要加一个控制字节:0x00表示后面跟的是命令,0x40表示后面跟的是显示数据。
void OLED_WriteCmd(uint8_t cmd) { OLED_IIC_Start(); OLED_IIC_SendByte(0x78); // 器件地址 + 写标志 OLED_IIC_SendByte(0x00); // 控制字节:后续是命令 OLED_IIC_SendByte(cmd); OLED_IIC_Stop(); }初始化命令序列可以放在一个数组里,逐个发送:
static const uint8_t OLED_INIT_CMDS[] = { 0xAE, // 关闭显示,初始化期间保持黑屏 0xD5, 0x80, // 显示时钟分频,0x80 是常见默认值 0xA8, 0x3F, // 复用比 1/64,128x64 屏固定值 0xD3, 0x00, // 显示偏移 0 0x40, // 显示起始行 0 0x8D, 0x14, // 电荷泵开启,0x14 必须设置,否则白屏 0x20, 0x00, // 水平地址模式 0xA1, // 段重映射,配合 0xC8 修正左右镜像 0xC8, // 扫描方向从下到上 0xDA, 0x12, // COM 引脚配置,128x64 用 0x12 0x81, 0xCF, // 对比度,0x00~0xFF,0xCF 偏亮 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 电压 0xA4, // 从显存内容显示 0xA6, // 正常显示,0xA7 是反色 0xAF // 打开显示 }; void OLED_Init(void) { uint8_t i; OLED_GPIO_Init(); OLED_IIC_Delay(); for (i = 0; i < sizeof(OLED_INIT_CMDS); i++) OLED_WriteCmd(OLED_INIT_CMDS[i]); }初始化序列里最容易出错的是0x8D 0x14这一组。SSD1306 内部电荷泵默认关闭,如果不通过命令打开,屏幕连背光都不亮。另外0xA1和0xC8要配对出现,单独设置会导致显示内容左右或上下镜像。
3.2 显示函数分层:画点、字符、字符串与 OLED 显示汉字
显示函数不要全部堆在main.c里,常见的做法是分三层:底层是 IIC 写字节,中间层是 SSD1306 命令和显存操作,上层才是字符、字符串、汉字、波形这些业务显示函数。先在内存里维护一个 8 行 × 128 列的显存数组:
static uint8_t OLED_Buffer[8][128]; // 8 页,每页 128 字节,对应 128x64 void OLED_Refresh(void) { uint8_t page, col; for (page = 0; page < 8; page++) { OLED_WriteCmd(0xB0 + page); // 设置页地址 OLED_WriteCmd(0x00); // 列地址低 4 位 OLED_WriteCmd(0x10); // 列地址高 4 位 OLED_IIC_Start(); OLED_IIC_SendByte(0x78); OLED_IIC_SendByte(0x40); // 控制字节:后续是数据 for (col = 0; col < 128; col++) OLED_IIC_SendByte(OLED_Buffer[page][col]); OLED_IIC_Stop(); } }显示字符时先取字模,6×8 字体每个字符占 6 个字节,每字节对应一列,低位在上:
void OLED_ShowChar(uint8_t x, uint8_t y, char ch) { uint8_t i; if (x > 127 || y > 7 || ch < ' ') return; for (i = 0; i < 6; i++) OLED_Buffer[y][x + i] = OLED_Font6x8[ch - ' '][i]; }y 坐标是页号,范围 0~7,对应屏幕从上到下的 8 个 8 像素高的条带。OLED 显示汉字用 16×16 点阵,每个汉字占 32 字节,一页放不下,需要跨两页写入:
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index) { uint8_t i; if (x > 127 || y > 6) return; for (i = 0; i < 16; i++) OLED_Buffer[y][x + i] = OLED_Font16[index][i]; // 上半部分 for (i = 0; i < 16; i++) OLED_Buffer[y + 1][x + i] = OLED_Font16[index][16 + i]; // 下半部分 }字符串函数就是循环调用OLED_ShowChar,遇到\n换行即可。这个分层的意义在于:应用层永远只操作OLED_Buffer,不关心底层是 IIC 还是 SPI,也不关心引脚接在 PB6 还是 PC13。字模数据用取模软件生成,取模方式选“列行式、字节倒序”,和上面代码的字节位序保持一致。
3.3 加了 OLED 函数就卡死:排查 IIC 等待、时钟与复位
很多人在工程里加了 OLED 显示函数后程序卡死在初始化位置,常见的现象和排查方向如下:
| 现象 | 可能原因 | 检查方法 |
|---|---|---|
卡死在OLED_Init | GPIO 时钟没打开或引脚被其他外设占用 | 检查 RCC 时钟使能,确认 PB6/PB7 没接按键或串口 |
| 卡死在发数据循环 | IIC 发送函数里加了 ACK 等待 | 删掉 ACK 相关代码,OLED 模块不回 ACK 很常见 |
| 屏幕亮但全屏雪花 | 初始化序列缺了0xA4或显存未清 | 初始化后全屏写 0 再刷新一次 |
| 程序能跑但 OLED 没反应 | SCL/SDA 接反或控制字节写错 | 用逻辑分析仪看 SCL/SDA 波形,确认命令字是 0x00 |
| 按键等外设同时失灵 | PB6/PB7 被复用成其他功能 | F103 的 PB6/PB7 默认是 I2C1,检查是否被重映射 |
矩阵按键在 OLED 上没反应,多半不是 OLED 的问题,而是按键扫描和 OLED 共用了同一个引脚。F103 的 PB6/PB7 也是 I2C1 引脚,如果你用标准库开启了 I2C1 外设,GPIO 模拟函数就会和硬件外设抢引脚控制权。拿到工程包后第一步不是编译,而是全局搜索GPIO_Pin_6和GPIO_Pin_7,确认没有第二个外设同时操作它们。
4. STM32F103 波形显示:ADC 采样、DMA 搬运与 OLED 滚动映射
4.1 波形显示的三段式结构:采样、映射、刷新
OLED 上做波形显示,核心不是“画线”而是“数据链路”。完整结构是:ADC 采集模拟信号 → DMA 搬运到内存 → OLED 把采样点按列刷新。很多人的误区在于用HAL_Delay控制采样间隔或直接在 ADC 中断里刷屏,导致采样时间被 IIC 刷新阻塞。stm32f103 的 ADC 是 12 位,输入范围 0~3.3V,采集一个 0~3.3V 的正弦波信号时,采样值范围是 0~4095,需要映射到 OLED 的 0~63 像素高度。用 DAC 输出正玄波作为测试信号是最方便的,F103 的 DAC 通道输出接 ADC 输入,不需要外接信号发生器。
映射公式一句话讲清:
#define WAVE_TO_Y(val) (63 - ((val) * 60 >> 12))12 位最大值 4095,乘以 60 再右移 12 位,结果范围是 0~59,留出上下各 2 像素的边距。63 -是为了把高电平显示在上面,和示波器习惯一致。这个映射宏放在头文件里,采样位数变了只改这一处。
4.2 用定时器触发 ADC + DMA,避免波形采集拖慢 OLED 刷新
采集阶段如果直接在while(1)里轮询 ADC,OLED 刷新时采集就会断档。常见做法是定时器触发 ADC 转换,DMA 自动把结果搬运到缓冲区,CPU 只在 DMA 传输一半和完成时拿到通知。这里用 TIM2 的通道 2 触发 ADC1,DMA1 通道 1 搬运:
#define ADC_SAMPLE_NUM 64 uint16_t adc_buf[ADC_SAMPLE_NUM]; volatile uint8_t dma_half_done = 0; void Waveform_ADC_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; TIM_TimeBaseInitTypeDef tim; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_AIN; // PA0 做模拟输入 GPIO_Init(GPIOA, &gpio); dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_MemoryBaseAddr = (uint32_t)adc_buf; dma.DMA_DIR = DMA_DIR_PeripheralSRC; dma.DMA_BufferSize = ADC_SAMPLE_NUM; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; dma.DMA_Priority = DMA_Priority_High; dma.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &dma); adc.ADC_Mode = ADC_Mode_Independent; adc.ADC_ScanConvMode = DISABLE; adc.ADC_ContinuousConvMode = DISABLE; // 由定时器触发,不连续转换 adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2; // TIM2 通道 2 触发 adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); tim.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Period = 1000 - 1; // 1MHz / 1000 = 1kHz 触发频率 tim.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tim); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); // 更新事件作为触发源 DMA_ITConfig(DMA1_IT_TC1, ENABLE); // 传输完成中断 NVIC_EnableIRQ(DMA1_Channel1_IRQn); TIM_Cmd(TIM2, ENABLE); }注意 TIM2 的更新事件需要作为触发源输出,实际触发 ADC 的是 TIM2 的 TRGO,不是 PWM 输出。采样频率由TIM_Period决定,这里 1kHz 对应每毫秒采一个点。DMA 工作在循环模式,缓冲区采满 64 个点后自动从头开始,传输完成中断里只翻转标志位:
void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC1)) { dma_half_done = 1; DMA_ClearITPendingBit(DMA1_IT_TC1); } }如果要做多路捕获,把ADC_NbrOfChannel改成 2,在ADC_RegularChannelConfig里配置两个通道,DMA 缓冲区大小翻倍,数据按通道交替排列。stm32f103 多路 ADC 采集常见的坑是 DMA 只搬了一个通道的数据,原因多半是忘了开扫描模式。
4.3 OLED 列滚动刷新与采样率换算
波形显示不能每帧全屏清除再重画,那样闪烁会非常明显。常见做法是整屏显存向左滚动一列,新采样点画在最后一列。显存按字节组织,向左滚动一列等价于按位右移:
void OLED_ScrollLeft(void) { uint8_t page, col; uint8_t carry = 0; for (page = 0; page < 8; page++) { for (col = 0; col < 128; col++) { uint8_t val = OLED_Buffer[page][col]; uint8_t new_carry = (val & 0x01) ? 0x80 : 0; // 本字节最低位移到下一字节 OLED_Buffer[page][col] = (val >> 1) | carry; carry = new_carry; } } }这里carry在页内循环时传递,但页与页之间不传递,因为不同页对应屏幕不同高度的 8 行,水平滚动时进位只发生在同一页内。每次 DMA 传输完成,把 adc_buf 里的 64 个点依次画到缓冲区的最后一列,然后整体左移:
void Waveform_UpdateDisplay(void) { uint8_t i; static uint8_t col = 0; if (!dma_half_done) return; dma_half_done = 0; for (i = 0; i < ADC_SAMPLE_NUM; i++) { uint8_t y = WAVE_TO_Y(adc_buf[i]); OLED_Buffer[y / 8][col] |= (1 << (y % 8)); col++; if (col >= 128) { OLED_ScrollLeft(); col = 127; // 下一轮继续画最后一列 } } OLED_Refresh(); }OLED_Refresh会把整个 8×128 显存刷到屏幕,软件 IIC 下大约需要 20–30ms,所以波形刷新率理论极限在 30Hz 左右。ADC 采样率可以很高,但显示端跟不上时,波形看到的是“抽稀”后的结果。采样率和横向时间窗口的关系如下:
| 采样频率 | 128 列对应时间窗 | 可观察信号频率建议 |
|---|---|---|
| 1kHz | 128ms | 低于 100Hz |
| 2kHz | 64ms | 低于 200Hz |
| 5kHz | 25.6ms | 低于 500Hz |
| 10kHz | 12.8ms | 低于 1kHz |
这个表格的前提是每采样点占一列。如果采样率提高但列数不变,每个像素列代表的时间变短,波形横向被拉伸。要让低频信号完整显示,需要降低采样率或把多个采样点做峰值保持后再画一列,否则波形会看起来“断断续续”。
5. 验证与进阶:逻辑分析仪看时序、标准库到 HAL 库迁移的落地细节
5.1 用逻辑分析仪验证 IIC 时序与初始化序列
OLED 不上电、花屏、乱码时,先别怀疑芯片型号,把逻辑分析仪的通道 0 接 SCL、通道 1 接 SDA、地线接 GND,采样率设 1MHz 以上,抓一段初始化波形。重点看三处:起始信号是不是 SCL 高电平时 SDA 拉低;地址字节是不是 0x78;命令前有没有 0x00 控制字节。如果波形上能看到 0x78 后面跟着 0x00、0xAE,说明 IIC 层已经通了,问题在初始化命令本身。如果 SDA 上数据是 0x40 开头的,说明你把数据字节当命令发出去了,屏幕会显示乱码。抓一次全屏刷新还能数出0xB0到0xB7共 8 次页切换,次数不对说明刷新不完整。
5.2 从标准库到 HAL 库迁移 OLED 代码的注意点
F103 的工程越来越多用 CubeMX 生成 HAL 库,迁移 OLED 代码时不要直接复制标准库的 GPIO 操作。标准库用GPIO_SetBits和GPIO_ResetBits,HAL 库用HAL_GPIO_WritePin,替换关系如下:
// 标准库 GPIO_SetBits(GPIOB, GPIO_Pin_6); GPIO_ResetBits(GPIOB, GPIO_Pin_7); // HAL 库 HAL_GPIO_WritePin(GPIOB, GPIO_Pin_6, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_Pin_7, GPIO_PIN_RESET);如果 OLED 代码文件里到处是GPIOB->BSRR,那恭喜你,这套代码本身就是寄存器操作,标准库和 HAL 库都能用。DMA 中断在 HAL 库里有自己的处理流程,标准库的DMA1_Channel1_IRQHandler写法在 HAL 库里仍能直接用中断函数名,但如果同时用了HAL_DMA_IRQHandler,注意不要把同一个 DMA 通道回调函数覆盖掉。迁移后第一件事是打开 DMA 中断优先级分组NVIC_PriorityGroup_2,否则中断不响应。
5.3 让 OLED 波形显示变成可复用的驱动模块
把代码按驱动层、硬件抽象层、应用层拆开,后续换屏或换单片机只改一层。驱动层就是OLED_WriteCmd、OLED_Refresh和底层 IIC,硬件抽象层负责引脚宏定义和 GPIO 初始化,应用层只调用OLED_ShowString、OLED_ShowChinese、Waveform_UpdateDisplay。如果换 0.91 寸 128×32 屏,就把OLED_Buffer[8][128]改成OLED_Buffer[4][128],页循环上限从 8 改成 4,初始化命令里0xA8 0x3F改成0xA8 0x1F。如果你的板子只有 SPI 接口的 OLED,驱动层换成 SPI 发送函数,OLED_Buffer的组织方式可以完全不变。判断驱动函数写没写对的快速方法是显示一个十字线:四条边各画一条线,如果上下左右都对得上,说明段重映射和扫描方向设置正确;如果左右反了,把0xA1改成0xA0,上下反了把0xC8改成0xC0。把这条验证步骤写进项目的 README,下次换屏不用再从头查一遍。
本文还有配套的精品资源,点击获取