STM32——OLED显示汉字
前言在使用 STM32 驱动 SSD1306 OLED 时很多新手都会遇到汉字错位、爱心旋转、图案乱码等问题而这些问题的根源往往不是硬件接线而是对 OLED 页寻址模式、PCtoLCD2002 取模规则、显示函数底层逻辑的不理解。今天我将基于你提供的完整工程从硬件配置、字模原理、驱动源码到显示函数逐行拆解带你吃透 16x16 汉字 / 图标显示的本质让你不仅能写出正确的代码还能自己定制任意图案告别 “玄学调参”。一、基础配置与原理1. 硬件与开发环境主控STM32F103标准库外设0.96 寸 I2C 接口 SSD1306 OLED 屏引脚分配PB6 SCL、PB7 SDA字模工具PCtoLCD2002寻址模式SSD1306 默认页寻址模式2. PCtoLCD2002 取模配置适配本驱动确定驱动唯一正确的取模方式任何一个参数改动都会导致图案错位参数配置含义点阵格式阴码数据位为 1 时点亮像素为 0 时熄灭取模方式列行式先按列从上到下取 8 个点组成 1 字节再向右取下一列取模走向逆向低位在前每列最上方的点对应字节的 bit0最下方对应 bit7点阵大小16×16单个汉字 / 图标占 16 行 ×16 列像素3. 16x16 字模存储规则SSD1306 屏幕的显存按 “页” 管理1 页对应 8 行像素因此 16×16 的图案需要分上下两部分存储上半部分第 0~7 行16 列每列 1 字节共 16 字节下半部分第 8~15 行16 列每列 1 字节共 16 字节✅ 因此任意一个 16x16 的汉字 / 图标固定占用 32 字节数组中按 “上 16 字节→下 16 字节” 的顺序排列。二、完整工程源码1 、oled.c 驱动文件#include stm32f10x.h #include oled.h #include tim.h #include codetab.h /** * brief I2C1 外设初始化PB6SCLPB7SDA * note 配置为复用开漏模式400kHz快速I2C通信 */ void I2C_Configuration(void) { I2C_InitTypeDef I2C_InitStructure; GPIO_InitTypeDef GPIO_Initstructure; // 开启GPIOB和I2C1外设时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 , ENABLE); // 配置PB6、PB7为复用开漏模式I2C必须使用开漏 GPIO_Initstructure.GPIO_Mode GPIO_Mode_AF_OD; GPIO_Initstructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_Initstructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_Initstructure); // 初始化I2C1 I2C_DeInit(I2C1); I2C_InitStructure.I2C_Ack I2C_Ack_Enable; // 使能应答 I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; // 7位地址 I2C_InitStructure.I2C_ClockSpeed 400000; // 400kHz快速模式 I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; // 占空比2:1 I2C_InitStructure.I2C_Mode I2C_Mode_I2C; // 标准I2C模式 I2C_InitStructure.I2C_OwnAddress1 0x30; // 主机地址可自定义 I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); } /** * brief I2C底层写一个字节含寄存器地址和数据 * param addr OLED寄存器地址0x00为命令0x40为数据 * param data 要发送的命令或数据 */ void I2C_WriteByte(uint8_t addr,uint8_t data) { while( I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) ); // 等待I2C总线空闲 I2C_GenerateSTART(I2C1, ENABLE); // 产生起始信号 while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) ); // 等待主机模式确认 I2C_Send7bitAddress(I2C1, OLED_ADDRESS, I2C_Direction_Transmitter); // 发送OLED设备地址 while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) ); I2C_SendData(I2C1, addr); // 写入寄存器地址命令/数据选择 while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED ) ); I2C_SendData(I2C1, data); // 写入实际数据 while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED ) ); I2C_GenerateSTOP(I2C1, ENABLE); // 产生停止信号 } /** * brief 向OLED写入命令 * param I2C_Command 要写入的命令字 */ void WriteCmd(unsigned char I2C_Command) { I2C_WriteByte(0x00,I2C_Command); // 0x00表示后续为命令 } /** * brief 向OLED写入显示数据像素点数据 * param I2C_Data 要写入的像素数据 */ void WriteData(unsigned char I2C_Data) { I2C_WriteByte(0x40,I2C_Data); // 0x40表示后续为显示数据 } /** * brief OLED初始化SSD1306标准配置序列 * note 配置为页寻址模式开启电荷泵设置对比度等 */ void OLED_Init(void) { delay_ms(100); // 等待OLED上电稳定 WriteCmd(0xAE); // 关闭显示 WriteCmd(0x20); // 设置内存地址模式 WriteCmd(0x02); // 选择页寻址模式本驱动核心 WriteCmd(0xb0); // 设置页起始地址 WriteCmd(0xc8); // COM扫描方向倒置 WriteCmd(0x00); // 列地址低4位 WriteCmd(0x10); // 列地址高4位 WriteCmd(0x40); // 显示起始行 WriteCmd(0x81); // 对比度设置 WriteCmd(0xff); // 亮度最大值 WriteCmd(0xa1); // 段地址映射左右翻转 WriteCmd(0xa6); // 正常显示模式非反色 WriteCmd(0xa8); // 多路复用比设置 WriteCmd(0x3F); // 1/64占空比 WriteCmd(0xa4); // 输出跟随RAM数据 WriteCmd(0xd3); // 显示偏移设置 WriteCmd(0x00); // 无偏移 WriteCmd(0xd5); // 时钟分频因子设置 WriteCmd(0xf0); // 分频比 WriteCmd(0xd9); // 预充电周期设置 WriteCmd(0x22); WriteCmd(0xda); // COM引脚硬件配置 WriteCmd(0x12); WriteCmd(0xdb); // VCOMH电压设置 WriteCmd(0x20); WriteCmd(0x8d); // 电荷泵设置 WriteCmd(0x14); // 开启电荷泵必须否则屏幕不亮 WriteCmd(0xaf); // 开启显示 } /** * brief 设置OLED光标位置页寻址模式专用 * param x 列坐标0~127 * param y 页号0~71页8行像素 */ void OLED_Setpos(unsigned char x,unsigned char y) { WriteCmd(0xb0 y); // 选择当前页0xb0~0xb7对应页0~7 WriteCmd((x0xf0)4|0x10); // 列地址高4位x的高4位 0x10前缀 WriteCmd(x 0x0F); // 列地址低4位x的低4位 } /** * brief 全屏填充指定数据亮屏/清屏 * param Fill_Data 填充数据0x00为全灭0xFF为全亮 */ void OLED_Fill(unsigned char Fill_Data) { unsigned char m,n; for(m0;m8;m) // 遍历所有8个页 { WriteCmd(0xb0m); // 设置当前页 WriteCmd(0x00); // 列地址低4位为0 WriteCmd(0x10); // 列地址高4位为0即从列0开始 for(n0;n128;n) // 写入128列数据 { WriteData(Fill_Data); } } } /** * brief 清屏全屏置0 */ void OLED_Close(void) { OLED_Fill(0x00); } /** * brief 开启OLED显示唤醒电荷泵和屏幕 */ void OLED_ON(void) { WriteCmd(0X8D); // 电荷泵设置 WriteCmd(0X14); // 开启电荷泵 WriteCmd(0XAF); // 开启显示 } /** * brief 关闭OLED显示休眠模式 */ void OLED_OFF(void) { WriteCmd(0X8D); // 电荷泵设置 WriteCmd(0X10); // 关闭电荷泵 WriteCmd(0XAE); // 关闭显示 } /********************************************************* * brief 16x16汉字/图标显示函数核心重点 * param x 起始列坐标0~127 * param y 起始页号0~7控制上下位置 * param N 字模在F16X16数组中的索引从0开始 * note 适配PCtoLCD2002列行式、逆向、阴码取模 *********************************************************/ void OLED_ShowCN(unsigned char x,unsigned char y,unsigned char N) { unsigned char wn 0; unsigned int addr 32*N; // 每个16x16图案固定32字节计算偏移地址 // 第一步写入上半部分上8行 OLED_Setpos(x,y); // 定位到起始列x、起始页y for(wn0;wn16;wn) // 循环写入16列数据每列1字节共16列 { WriteData(F16X16[addr]); // 写入当前列的像素数据 addr1; // 地址1准备写下一列 } // 第二步写入下半部分下8行 OLED_Setpos(x,y1); // 页号1切换到下一页下8行像素 for(wn0;wn16;wn) // 继续写入16列数据填满下8行 { WriteData(F16X16[addr]); addr1; } }2、核心函数OLED_ShowCN深度解析这是整个工程的灵魂也是最容易出错的地方我们拆成 5 步讲透每一行代码的作用。① 函数参数说明参数含义控制效果x起始列坐标0~127控制图案在屏幕上的左右位置y起始页号0~7控制图案在屏幕上的上下位置1 页 8 行像素N字模在数组中的索引选择要显示的图案从 0 开始编号②执行逻辑分步拆解步骤 1计算字模偏移地址unsigned int addr 32*N;原理每个 16x16 图案固定占用 32 字节通过索引N直接偏移精准定位到当前要显示的字模起始位置。示例N0爱心→ addr0N1李→ addr32避免数组越界和数据错位。步骤 2定位上半部分的起始坐标OLED_Setpos(x,y);调用OLED_Setpos函数将 OLED 的写入光标定位到x列、y页。此时后续写入的数据将从该位置开始依次向右填充 16 列对应上 8 行像素。步骤 3写入上半部分上 8 行数据for(wn0;wn16;wn) { WriteData(F16X16[addr]); addr1; }循环 16 次每次写入 1 个字节对应 1 列的 8 个像素。写入顺序第 x 列→第 x1 列→…→第 x15 列刚好填满 16 列、上 8 行的区域。数据来源F16X16[addr]到F16X16[addr15]即字模数组的前 16 字节。步骤 4切换到下半部分下 8 行OLED_Setpos(x,y1);关键操作页号y1因为上半部分已经用了第y页8 行像素下半部分必须用第y1页才能拼接成完整的 16 行像素。列坐标保持不变确保上下两部分对齐图案不会错位。步骤 5写入下半部分下 8 行数据for(wn0;wn16;wn) { WriteData(F16X16[addr]); addr1; }继续循环 16 次写入字模数组的后 16 字节填满下 8 行、16 列的区域。上下两部分拼接最终形成完整的 16×16 汉字 / 图标。2、 main.c 主函数#include stm32f10x.h #include main.h #include stdio.h #include sg90.h #include oled.h /** * brief 简易软件延时函数约1ms延时基于72MHz主频 * param time 延时毫秒数 */ void delay(uint16_t time) { uint16_t i 0; while(time --) { i 12000; while(i --); } } int main(void) { unsigned char i 0; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 中断优先级分组 // 初始化外设 I2C_Configuration(); OLED_Init(); delay(2000); // 等待OLED初始化稳定 // 屏幕测试全亮→全灭 OLED_Fill(0XFF); // 全屏点亮 delay(2000); OLED_Fill(0X00); // 全屏清屏 delay(2000); // 循环显示数组内的16x16图案从索引0开始依次排列 for(i0; i6; i) { // 每个图案占16列起始列依次16实现水平排列 OLED_ShowCN(22i*16, 0, i); } while(1) { // 主循环可添加其他任务 } }3、 codetab.h 字模数组文件#ifndef _CODETAB_H #define _CODETAB_H // 16x16字模数组每32字节为一个完整图案 unsigned char F16X16[] { // 爱心♥ 索引032字节 0x00,0xF8,0xFC,0xFE,0xFE,0xFC,0xF8,0xF0,0xF8,0xFC,0xFE,0xFE,0xFC,0xF8,0x00,0x00, 0x00,0x03,0x07,0x0F,0x1F,0x3F,0x7F,0xFF,0x7F,0x3F,0x1F,0x0F,0x07,0x03,0x00,0x00, // 汉字李 索引132字节 0x80,0x84,0x44,0x44,0x24,0x14,0x0C,0xFF,0x0C,0x14,0x24,0x44,0x44,0x84,0x80,0x00, 0x08,0x08,0x08,0x08,0x09,0x49,0x89,0x79,0x0D,0x0B,0x09,0x08,0x08,0x08,0x08,0x00, }; #endif三、工程使用规范与拓展新增图案只需在codetab.h中按 “32 字节一组” 的格式追加字模无需修改驱动代码。多字符排列多个图案并排显示时起始列x每次 16 即可刚好错开不重叠。修改图案位置调整y参数可以控制图案的上下位置例如y1会让图案显示在屏幕第 8~15 行。拓展功能可以在OLED_ShowCN的基础上封装字符串显示函数实现自动换行、居中显示等效果。