news 2026/5/11 22:00:27

STM32——OLED显示汉字

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 外设初始化(PB6=SCL,PB7=SDA) * @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~7,1页=8行像素) */ void OLED_Setpos(unsigned char x,unsigned char y) { WriteCmd(0xb0 + y); // 选择当前页(0xb0~0xb7对应页0~7) WriteCmd((x&0xf0)>>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(m=0;m<8;m++) // 遍历所有8个页 { WriteCmd(0xb0+m); // 设置当前页 WriteCmd(0x00); // 列地址低4位为0 WriteCmd(0x10); // 列地址高4位为0,即从列0开始 for(n=0;n<128;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(wn=0;wn<16;wn++) // 循环写入16列数据(每列1字节,共16列) { WriteData(F16X16[addr]); // 写入当前列的像素数据 addr+=1; // 地址+1,准备写下一列 } // 第二步:写入下半部分(下8行) OLED_Setpos(x,y+1); // 页号+1,切换到下一页(下8行像素) for(wn=0;wn<16;wn++) // 继续写入16列数据,填满下8行 { WriteData(F16X16[addr]); addr+=1; } }

2、核心函数OLED_ShowCN深度解析

这是整个工程的灵魂,也是最容易出错的地方,我们拆成 5 步,讲透每一行代码的作用。

① 函数参数说明

参数含义控制效果
x起始列坐标(0~127)控制图案在屏幕上的左右位置
y起始页号(0~7)控制图案在屏幕上的上下位置(1 页 = 8 行像素)
N字模在数组中的索引选择要显示的图案(从 0 开始编号)

②执行逻辑分步拆解

步骤 1:计算字模偏移地址
unsigned int addr = 32*N;
  • 原理:每个 16x16 图案固定占用 32 字节,通过索引N直接偏移,精准定位到当前要显示的字模起始位置。
  • 示例:N=0(爱心)→ addr=0;N=1(李)→ addr=32,避免数组越界和数据错位。
步骤 2:定位上半部分的起始坐标
OLED_Setpos(x,y);
  • 调用OLED_Setpos函数,将 OLED 的写入光标定位到x列、y页
  • 此时,后续写入的数据将从该位置开始,依次向右填充 16 列,对应上 8 行像素。
步骤 3:写入上半部分(上 8 行)数据
for(wn=0;wn<16;wn++) { WriteData(F16X16[addr]); addr+=1; }
  • 循环 16 次,每次写入 1 个字节,对应 1 列的 8 个像素。
  • 写入顺序:第 x 列→第 x+1 列→…→第 x+15 列,刚好填满 16 列、上 8 行的区域。
  • 数据来源:F16X16[addr]F16X16[addr+15],即字模数组的前 16 字节。
步骤 4:切换到下半部分(下 8 行)
OLED_Setpos(x,y+1);
  • 关键操作:页号y+1,因为上半部分已经用了第y页(8 行像素),下半部分必须用第y+1页,才能拼接成完整的 16 行像素。
  • 列坐标保持不变,确保上下两部分对齐,图案不会错位。
步骤 5:写入下半部分(下 8 行)数据
for(wn=0;wn<16;wn++) { WriteData(F16X16[addr]); addr+=1; }
  • 继续循环 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(i=0; i<6; i++) { // 每个图案占16列,起始列依次+16,实现水平排列 OLED_ShowCN(22+i*16, 0, i); } while(1) { // 主循环,可添加其他任务 } }

3、 codetab.h 字模数组文件

#ifndef _CODETAB_H #define _CODETAB_H // 16x16字模数组,每32字节为一个完整图案 unsigned char F16X16[] = { // 爱心♥ 索引0(32字节) 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, // 汉字"李" 索引1(32字节) 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

三、工程使用规范与拓展

  1. 新增图案:只需在codetab.h中按 “32 字节一组” 的格式追加字模,无需修改驱动代码。
  2. 多字符排列:多个图案并排显示时,起始列x每次 + 16 即可,刚好错开不重叠。
  3. 修改图案位置:调整y参数可以控制图案的上下位置,例如y=1会让图案显示在屏幕第 8~15 行。
  4. 拓展功能:可以在OLED_ShowCN的基础上,封装字符串显示函数,实现自动换行、居中显示等效果。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/11 21:59:13

NoSQL

NoSQL&#xff08;Not Only SQL&#xff09; 是泛指非关系型数据库的统称&#xff0c;核心是放弃固定表结构、优先水平扩展 高可用 灵活 Schema&#xff0c;适合海量、高并发、非结构化 / 半结构化数据场景。一、核心特点&#xff08;vs 传统 SQL&#xff09;Schema 灵活&…

作者头像 李华
网站建设 2026/5/11 21:53:12

从数学原理到工程实践:最小二乘法的MATLAB拟合全解析

1. 最小二乘法的数学本质&#xff1a;从误差分析到最优解 当你面对一堆实验数据点&#xff0c;想要找到一条最能代表它们趋势的曲线时&#xff0c;最小二乘法就是你的最佳拍档。这个方法的核心思想其实非常直观——让所有数据点到拟合曲线的"距离"之和最小。这里的&q…

作者头像 李华
网站建设 2026/5/11 21:48:32

计算机毕业设计:Python医疗文本挖掘与可视化决策平台 Flask框架 随机森林 机器学习 疾病数据 智慧医疗 深度学习(建议收藏)✅

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ > &#x1f345;想要获取完整文章或者源码&#xff0c;或者代做&#xff0c;拉到文章底部即可与…

作者头像 李华
网站建设 2026/5/11 21:47:35

DHCP 服务器总结:概念、原理与实验详解

DHCP 服务器总结&#xff1a;概念、原理与实验详解 一、传统网络配置的痛点 在没有 DHCP 之前&#xff0c;每台计算机需要手动配置以下参数&#xff1a; IP 地址子网掩码默认网关DNS 服务器 手动配置存在诸多问题&#xff1a; 效率低下&#xff1a;大规模网络部署时&#xff0c…

作者头像 李华