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的基础上,封装字符串显示函数,实现自动换行、居中显示等效果。
相关推荐
狮驼岭的小钻风1 小时前
单片机启动流程与 .s 文件详解
单片机·嵌入式硬件
金色光环1 小时前
【DSP学习笔记】 F28335中断系统理解-基于普中DSP28335开发攻略
笔记·单片机·学习·dsp开发
iCxhust1 小时前
8086/8088单板机VSCode集中环境开发编译(第二版整理)
ide·vscode·嵌入式硬件·编辑器·嵌入式·微机原理·8086最小系统
时光の尘1 小时前
【嵌入式大厂面经】·IIC常见考点(持续更新中···)
arm开发·单片机·嵌入式硬件·mcu·物联网·iot
三佛科技-187366133972 小时前
AIP7550GD893.TR是什么芯片?200mA/30V低压差线性稳压器芯片分析
单片机·嵌入式硬件
高翔·权衡之境2 小时前
主题3:天线与耦合——近场与远场
网络·嵌入式硬件·物联网·软件工程·信息与通信
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 7 天:PCB电路板基础、走线、焊盘、贴片/直插
单片机·嵌入式硬件
飞凌嵌入式2 小时前
飞凌嵌入式率先推出RK3572核心板 | 新一代八核AIoT平台,新品强势来袭!
科技·嵌入式硬件·嵌入式
LCG元3 小时前
STM32实战:基于STM32F103的Modbus RTU通信(从机实现)
stm32·单片机·嵌入式硬件