第11章 显示外设驱动:I2C协议OLED屏、SPI协议LCD屏字符/图片/中文显示

前言

上一章我们全面掌握了USART串口通信的全场景开发,实现了STM32与上位机的数据交互,而显示外设则是嵌入式设备人机交互的核心载体,是调试日志、状态显示、菜单交互的必备组件。对应51单片机开发中,我们通常通过软件模拟I2C/SPI时序驱动OLED/LCD屏,存在时序精度差、CPU占用率高、刷新速度慢、代码移植性差的痛点;而STM32内置硬件I2C/SPI控制器,可自动生成标准通信时序,配合DMA实现零CPU占用的高速刷新,完美适配工业场景的显示需求。新手入门显示驱动普遍面临三大痛点:屏不亮无显示、画面乱码花屏、中文显示异常、硬件总线卡死。本章将严格遵循「先协议原理拆解,再驱动逻辑实现」的顺序,联动51单片机对应知识点,从底层到实操全面吃透I2C OLED与SPI LCD的驱动开发,完成字符、图片、中文全场景显示实战。

本章目录

  • 一、本章学习目标
  • 二、核心知识点
    • 2.1 工业显示外设选型与51单片机驱动方案核心对比
    • 2.2 I2C协议核心原理与SSD1306 OLED屏驱动逻辑
    • 2.3 SPI协议核心原理与ILI9341 LCD屏驱动逻辑
    • 2.4 字符/图片/中文显示底层原理与取模规则
    • 2.5 HAL库硬件I2C/SPI封装逻辑与核心API深度解析
  • 三、STM32CubeMX+Keil5保姆式实操:显示外设全场景驱动实战
    • 3.1 工程创建与基础配置
    • 3.2 I2C、SPI与GPIO图形化配置
    • 3.3 工程代码生成与驱动文件移植
    • 3.4 字符/中文/图片显示业务代码编写
    • 3.5 编译、烧录与效果验证
  • 四、保姆式排错指南
  • 五、我的踩坑记录
  • 六、课后小练习(附完整标准答案)
    • 6.1 基础巩固练习
    • 6.2 进阶实战练习
  • 七、核心知识点速记
  • 八、本章小结

一、本章学习目标

  1. 掌握I2C、SPI串行通信协议的核心时序与工作原理,对比51单片机软件模拟时序方案的核心差异,理解硬件总线的工程优势
  2. 吃透0.96寸SSD1306 OLED屏、2.4寸ILI9341 LCD屏的驱动架构与指令规则,能独立完成底层驱动的移植与修改
  3. 掌握ASCII字符、中文汉字、图片的取模规则与显示逻辑,联动C语言数组、指针、const关键字知识点,实现全类型内容的稳定显示
  4. 熟练使用HAL库硬件I2C/SPI完成OLED、LCD屏的驱动开发,实现滚动显示、数值实时刷新、图片显示等全场景功能,代码符合工业级规范
  5. 能独立排查屏不亮、乱码花屏、中文显示异常、硬件总线卡死等高频问题,掌握DMA高速刷屏的实现方法,为后续项目人机交互开发打下坚实基础

二、核心知识点

2.1 工业显示外设选型与51单片机驱动方案核心对比

嵌入式开发中最常用的两款显示外设为0.96寸I2C OLED屏2.4寸SPI LCD屏,二者的核心特性与适用场景如下:

显示外设 核心参数 通信接口 核心优势 工业适用场景
0.96寸OLED屏 128×64分辨率,单色,自发光,无需背光,宽视角,低功耗 I2C(2线) 接线极简、功耗极低、驱动简单、无需初始化GRAM,上电即可显示 设备状态指示、调试日志显示、小型低功耗设备、工业传感器节点
2.4寸LCD屏 240×320分辨率,16位彩色,TFT液晶,需背光驱动 SPI(4线+2控制线) 彩色显示、分辨率高、刷新速度快、可显示图片/视频/复杂界面 人机交互菜单、工业控制面板、数据可视化、智能设备交互界面

我们从51单片机的驱动方案出发,无缝衔接理解STM32硬件总线驱动的核心优势:

驱动方案 51单片机软件模拟时序 STM32硬件I2C/SPI驱动 对开发的核心影响
时序实现 手动翻转IO口,通过延时控制时序,精度差,受主频影响大 硬件控制器自动生成标准时序,精度高,不受软件干扰 软件模拟时序极易出现通信异常,不同主频的芯片需重新调整延时;硬件时序兼容性极强,代码可跨芯片移植
CPU占用 通信全程CPU需循环翻转IO,占用率100%,无法同步执行其他任务 仅需配置初始参数,数据收发全程硬件自动完成,配合DMA可实现零CPU占用 软件模拟方案刷新大屏时CPU完全卡死,无法同步执行采集、控制逻辑;硬件方案刷新期间CPU可正常执行核心业务,系统实时性大幅提升
通信速度 I2C最高约100Kbps,SPI最高约500Kbps,刷新速度极慢,大屏刷新需数百毫秒 I2C最高400Kbps,SPI最高18Mbps,刷新速度提升数十倍,2.4寸LCD全屏刷新仅需十几毫秒 软件模拟方案无法实现流畅的画面刷新、动画效果;硬件方案可实现高速刷屏、图片滑动、菜单切换等流畅交互
代码复杂度 需手动实现起始/停止信号、应答、时序同步,代码量大,逻辑复杂,易出bug HAL库已封装标准通信API,仅需调用收发函数即可完成通信,代码极简,稳定性强 软件模拟方案需开发者完全掌握协议底层时序,开发周期长;硬件方案可快速实现驱动开发,聚焦显示业务逻辑,开发效率大幅提升

2.2 I2C协议核心原理与SSD1306 OLED屏驱动逻辑

术语通俗解释:I2C全称集成电路总线,是一种两线式串行半双工通信总线,仅需SCL(串行时钟线)、SDA(串行数据线)两根线即可实现主设备与多个从设备之间的通信,类比一条公路上的主车与多辆从车,SCL是统一的行驶节拍,SDA是传输的货物,通过地址区分不同的从车。

I2C协议核心时序规则
  1. 总线空闲状态:SCL、SDA均为高电平,上拉电阻保证总线空闲电平;
  2. 起始信号:SCL为高电平时,SDA从高电平跳变到低电平,启动一次通信;
  3. 数据传输:SCL为低电平时,SDA可改变数据;SCL为高电平时,SDA数据必须保持稳定,接收端在SCL高电平时采样数据,每次传输8位数据,高位在前;
  4. 应答信号:每传输8位数据后,接收端会在第9个时钟周期拉低SDA,发送应答信号ACK,确认数据接收成功;
  5. 停止信号:SCL为高电平时,SDA从低电平跳变到高电平,结束本次通信。
I2C从机地址规则

I2C总线上的每个从设备都有唯一的7位地址,第8位为读写位:0=写操作,1=读操作。工业常用0.96寸SSD1306 OLED屏的默认7位地址为0x3C,8位写地址为0x78,读地址为0x79,部分屏地址为0x3D,对应写地址0x7A

SSD1306 OLED屏驱动核心逻辑

OLED屏的驱动核心是通过I2C总线向SSD1306控制器发送命令数据

  • 命令 :配置OLED屏的工作模式,如显示开关、对比度、扫描方向、地址设置等,通过控制字节0x00标识;
  • 数据 :写入GRAM的显示内容,1个字节对应8个像素点,通过控制字节0x40标识;
  • 128×64分辨率的OLED屏分为8页(Page0~Page7),每页8行,128列,写入1个字节对应一页中同一列的8个像素点,实现点阵显示。

联动C语言知识点 :OLED的显示内容均以uint8_t类型的数组存储,通过指针遍历数组,批量写入GRAM,实现字符、图片的显示;字模数组需定义为const类型,存储在Flash中,节省宝贵的SRAM内存。

2.3 SPI协议核心原理与ILI9341 LCD屏驱动逻辑

术语通俗解释:SPI全称串行外设接口,是一种四线式串行全双工通信总线,通过SCK(串行时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选)四根线实现高速通信,类比主设备与从设备之间的专用高速通道,CS是通道开关,SCK是传输节拍,MOSI/MISO是双向数据通道,可同时收发数据。

SPI协议核心时序规则
  1. 片选控制:通信前拉低对应从设备的CS引脚,选中目标设备,通信结束后拉高CS引脚,释放总线;
  2. 4种工作模式 :由CPOL(时钟极性)与CPHA(时钟相位)组合决定,工业最常用模式0(CPOL=0,CPHA=0)
    • CPOL=0:总线空闲时SCK为低电平;
    • CPHA=0:在SCK的第一个跳变沿(上升沿)采样数据,第二个跳变沿(下降沿)更新数据;
  3. 全双工传输:每个SCK时钟周期,主设备通过MOSI发送1位数据,同时通过MISO接收1位数据,收发同步完成,传输效率远高于I2C。
ILI9341 LCD屏驱动核心逻辑

2.4寸ILI9341 LCD屏为240×320分辨率、16位RGB565彩色屏,驱动核心分为三部分:

  1. 控制引脚 :除了SPI总线的SCK、MOSI、CS引脚外,还需2个GPIO引脚:
    • DC(数据/命令)引脚:低电平表示传输的是命令,高电平表示传输的是显示数据;
    • RES(复位)引脚:低电平复位LCD控制器,上电后需执行硬件复位,保证控制器初始化正常;
  2. 初始化流程:上电复位后,通过SPI总线向ILI9341控制器发送初始化命令序列,配置像素格式、扫描方向、显示窗口、PLL时钟等参数,初始化完成后开启显示;
  3. 显示原理:LCD的每个像素点对应16位的RGB565数据(5位红+6位绿+5位蓝),240×320分辨率的全屏共需240×320×2=153600字节数据,通过SPI总线批量写入GRAM,即可实现画面显示。

联动C语言知识点 :LCD的图片、字模均以uint16_t类型的数组存储,通过C语言的循环、指针实现指定区域的批量写入,配合DMA可实现高速刷屏,无需CPU干预数据传输。

2.4 字符/图片/中文显示底层原理与取模规则

所有显示内容的本质都是点阵数据,通过取模软件将字符、汉字、图片转换为MCU可识别的十六进制数组,再写入显示控制器的GRAM,即可实现内容显示,这是所有显示驱动的核心基础。

1. ASCII字符显示原理

ASCII字符为8×16或6×12点阵,每个字符对应一个固定的点阵数组,通过ASCII码值偏移找到对应字符的点阵数据,写入对应坐标的GRAM,即可实现字符显示。常用取模格式:列行式、逆向、高位在前,与OLED/LCD的扫描方向匹配。

2. 中文汉字显示原理

中文显示的核心是GB2312编码与字模库:

  1. 每个中文汉字对应GB2312编码中的一个区位码,常用16×16或24×24点阵,每个汉字对应32字节或72字节的点阵数组;
  2. 字模库是按GB2312编码顺序排列的汉字点阵数组,通过汉字的GB2312编码计算偏移量,找到对应点阵数据,实现中文显示;
  3. 取模核心规则:必须与驱动代码的扫描顺序、字节序完全一致,否则会出现乱码、翻转、错位等问题。
3. 图片显示原理

图片显示的核心是将图片转换为对应分辨率的点阵数组:

  1. OLED单色图片:将图片转换为128×64分辨率的单色位图,取模生成uint8_t类型的数组,写入OLED的GRAM即可显示;
  2. LCD彩色图片:将图片转换为240×320分辨率的16位RGB565格式位图,取模生成uint16_t类型的数组,批量写入LCD的GRAM即可显示;
  3. 取模核心要求:图片分辨率必须与屏的显示区域匹配,色彩格式必须与驱动配置一致,否则会出现花屏、颜色错乱等问题。

2.5 HAL库硬件I2C/SPI封装逻辑与核心API深度解析

HAL库将I2C/SPI的底层寄存器操作封装为标准化的API函数,无需手动模拟时序,仅需简单调用即可完成数据收发,大幅提升开发效率与代码稳定性。

1. 硬件I2C核心API
HAL库API函数 核心功能 适用场景
HAL_I2C_Init(I2C_HandleTypeDef *hi2c) I2C外设初始化,配置时钟频率、地址模式、自身地址 工程初始化阶段配置I2C参数
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout) 向I2C从设备的指定寄存器写入数据,自动处理起始、停止、应答时序 OLED屏命令/数据写入,核心驱动函数
HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout) 从I2C从设备的指定寄存器读取数据 读取OLED控制器状态、I2C传感器数据
HAL_I2C_Mem_Write_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size) DMA模式写入数据,全程零CPU占用 高速批量数据写入,OLED全屏刷新

OLED驱动核心调用示例

c 复制代码
// 向OLED写入命令
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100);
// 向OLED写入显示数据
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, data_buf, 128, 100);
2. 硬件SPI核心API
HAL库API函数 核心功能 适用场景
HAL_SPI_Init(SPI_HandleTypeDef *hspi) SPI外设初始化,配置模式、波特率、数据位宽、时钟极性/相位 工程初始化阶段配置SPI参数
HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) 轮询模式发送数据,阻塞式等待发送完成 LCD命令/短数据发送,简单场景
HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) 轮询模式接收数据 读取SPI设备寄存器、传感器数据
HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size) DMA模式发送数据,全程零CPU占用 LCD全屏刷新、图片显示,高速批量数据传输

LCD驱动核心调用示例

c 复制代码
// 写LCD命令:DC拉低,发送1字节命令
LCD_DC_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
// 写LCD数据:DC拉高,发送批量数据
LCD_DC_HIGH();
HAL_SPI_Transmit_DMA(&hspi1, pic_buf, 153600);

三、STM32CubeMX+Keil5保姆式实操:显示外设全场景驱动实战

本次实操适配STM32F103C8T6核心板,实现两大核心功能:① 0.96寸I2C OLED屏字符、中文、滚动显示;② 2.4寸SPI LCD屏字符、中文、图片显示,全程无跳步,零基础可零报错跟随完成。

硬件说明

显示外设 引脚分配 核心参数 接线说明
0.96寸OLED屏 SCL→PB6(I2C1_SCL)、SDA→PB7(I2C1_SDA) SSD1306控制器,128×64分辨率,I2C地址0x3C VCC接3.3V,GND接核心板GND,严禁接5V
2.4寸LCD屏 SCK→PA5(SPI1_SCK)、MOSI→PA7(SPI1_MOSI)、CS→PA4、DC→PA2、RES→PA3 ILI9341控制器,240×320分辨率,16位RGB565 VCC接5V,背光接3.3V,GND接核心板GND

3.1 工程创建与基础配置

  1. 打开STM32CubeMX,点击ACCESS TO MCU SELECTOR,搜索选择STM32F103C8T6,点击Start Project创建工程。
  2. 调试接口配置:点击左侧System Core -> SYS,Debug选项选择Serial Wire,开启SWD串行调试。
  3. 时钟配置:点击RCC,HSE选项选择Crystal/Ceramic Resonator(外部8MHz晶振);进入Clock Configuration选项卡,配置PLL倍频为x9,系统时钟设置为72MHz,I2C、SPI外设时钟均为72MHz,无红色错误提示。

3.2 I2C、SPI与GPIO图形化配置

  1. I2C1配置:
    • 点击左侧Connectivity -> I2C1,Mode选择I2C
    • 配置参数:I2C Speed Mode为Fast Mode,I2C Clock Speed为400 KHz,其余保持默认;
    • 引脚自动映射为PB6(I2C1_SCL)、PB7(I2C1_SDA),均为复用开漏输出模式,自动开启上拉。
  2. SPI1配置:
    • 点击左侧Connectivity -> SPI1,Mode选择Full-Duplex Master(全双工主机模式);
    • 配置参数:
      • Mode:Full-Duplex Master
      • Hardware NSS Signal:Disable(软件CS)
      • Baud Rate Prescaler:2分频,72MHz/2=36MHz,符合ILI9341最高时钟要求
      • Clock Polarity (CPOL):Low
      • Clock Phase (CPHA):1 Edge
      • Data Size:8 Bits
      • First Bit:MSB First
    • 引脚自动映射为PA5(SPI1_SCK)、PA6(SPI1_MISO)、PA7(SPI1_MOSI),均为复用推挽输出模式。
  3. GPIO配置:
    • PA2、PA3、PA4:均选择GPIO_Output,推挽输出、高速、默认高电平,User Label分别设为LCD_DCLCD_RESLCD_CS
  4. DMA配置(SPI1发送):
    • 点击SPI1配置界面的DMA Settings,点击Add添加DMA通道:
      • DMA Request:SPI1_TX
      • Channel:DMA1 Channel 3
      • Direction:Memory To Peripheral
      • Priority:High
      • Mode:Normal
      • Data Width:Byte,Memory地址自增,Peripheral地址不增
  5. NVIC配置:
    • 点击左侧System Core -> NVIC,优先级分组选择Priority Group 2
    • 勾选I2C1 event interruptSPI1 global interruptDMA1 channel3 global interrupt,抢占优先级均设为1。

3.3 工程代码生成与驱动文件移植

  1. 工程生成配置:进入Project Manager,设置全英文无空格的工程名与保存路径,Toolchain/IDE选择MDK-ARM V5;进入Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheralKeep User Code when re-generating,点击GENERATE CODE生成工程,完成后点击Open Project打开Keil5工程。
  2. 驱动文件移植:
    • 在工程中新建oled.coled.hlcd.clcd.hfont.h五个文件,添加到工程中;
    • font.h文件中存放ASCII字模、中文GB2312字模、图片点阵数组,取模格式严格匹配驱动代码;
    • 驱动代码核心逻辑见下文,所有代码均适配STM32F103C8T6,可直接复制使用。
OLED核心驱动代码(oled.c)
c 复制代码
#include "oled.h"
#include "i2c.h"
#include "font.h"

// OLED写命令
void OLED_Write_Cmd(uint8_t cmd)
{
  HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 100);
}

// OLED写数据
void OLED_Write_Data(uint8_t *data, uint16_t len)
{
  HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, data, len, 100);
}

// 设置光标位置
void OLED_Set_Pos(uint8_t x, uint8_t y)
{
  OLED_Write_Cmd(0xb0 + y);
  OLED_Write_Cmd((x & 0x0f) | 0x00);
  OLED_Write_Cmd(((x & 0xf0) >> 4) | 0x10);
}

// OLED清屏
void OLED_Clear(void)
{
  uint8_t buf[128] = {0};
  for(uint8_t y=0; y<8; y++)
  {
    OLED_Set_Pos(0, y);
    OLED_Write_Data(buf, 128);
  }
}

// OLED初始化
void OLED_Init(void)
{
  HAL_Delay(100);
  OLED_Write_Cmd(0xAE); // 关闭显示
  OLED_Write_Cmd(0xd5); // 设置时钟分频因子
  OLED_Write_Cmd(0x80);
  OLED_Write_Cmd(0xa8); // 设置驱动路数
  OLED_Write_Cmd(0x3f); // 1/64 duty
  OLED_Write_Cmd(0xd3); // 设置显示偏移
  OLED_Write_Cmd(0x00); // 无偏移
  OLED_Write_Cmd(0x40); // 设置显示开始行
  OLED_Write_Cmd(0x8d); // 电荷泵设置
  OLED_Write_Cmd(0x14); // 开启电荷泵
  OLED_Write_Cmd(0x20); // 内存地址模式
  OLED_Write_Cmd(0x02); // 页地址模式
  OLED_Write_Cmd(0xa1); // 段重定义设置,bit0=1,列127对应SEG0
  OLED_Write_Cmd(0xc8); // COM扫描方向,从COM[N-1]到COM0
  OLED_Write_Cmd(0xda); // 设置COM硬件引脚配置
  OLED_Write_Cmd(0x12);
  OLED_Write_Cmd(0x81); // 对比度设置
  OLED_Write_Cmd(0xcf);
  OLED_Write_Cmd(0xd9); // 设置预充电周期
  OLED_Write_Cmd(0xf1);
  OLED_Write_Cmd(0xdb); // 设置VCOMH取消选择级别
  OLED_Write_Cmd(0x30);
  OLED_Write_Cmd(0xa4); // 全局显示开启,bit0=1,关闭全局显示
  OLED_Write_Cmd(0xa6); // 设置显示方式,bit0=0,正常显示
  OLED_Write_Cmd(0xAF); // 开启显示
  OLED_Clear();
}

// 显示ASCII字符
void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t ch)
{
  uint8_t i = 0;
  ch = ch - ' '; // 偏移到空格开始
  OLED_Set_Pos(x, y);
  OLED_Write_Data(&asc2_1608[ch*16], 8);
  OLED_Set_Pos(x, y+1);
  OLED_Write_Data(&asc2_1608[ch*16+8], 8);
}

// 显示字符串
void OLED_Show_String(uint8_t x, uint8_t y, char *str)
{
  while(*str)
  {
    OLED_Show_Char(x, y, *str);
    x += 8;
    if(x > 120)
    {
      x = 0;
      y += 2;
    }
    str++;
  }
}

// 显示16×16中文汉字
void OLED_Show_Chinese(uint8_t x, uint8_t y, uint8_t index)
{
  OLED_Set_Pos(x, y);
  OLED_Write_Data(&hz_1616[index*32], 16);
  OLED_Set_Pos(x, y+1);
  OLED_Write_Data(&hz_1616[index*32+16], 16);
}

// 滚动显示
void OLED_Scroll_Left(uint8_t start_page, uint8_t end_page, uint8_t speed)
{
  OLED_Write_Cmd(0x2E); // 关闭滚动
  OLED_Write_Cmd(0x27); // 向左滚动
  OLED_Write_Cmd(0x00); // 虚拟字节
  OLED_Write_Cmd(start_page); // 起始页
  OLED_Write_Cmd(speed); // 滚动速度
  OLED_Write_Cmd(end_page); // 结束页
  OLED_Write_Cmd(0x00); // 虚拟字节
  OLED_Write_Cmd(0xFF);
  OLED_Write_Cmd(0x2F); // 开启滚动
}
LCD核心驱动代码(lcd.c)
c 复制代码
#include "lcd.h"
#include "spi.h"
#include "font.h"
#include "gpio.h"

// LCD写命令
void LCD_Write_Cmd(uint8_t cmd)
{
  LCD_CS_LOW();
  LCD_DC_LOW();
  HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
  LCD_CS_HIGH();
}

// LCD写数据
void LCD_Write_Data(uint8_t *data, uint16_t len)
{
  LCD_CS_LOW();
  LCD_DC_HIGH();
  HAL_SPI_Transmit(&hspi1, data, len, 1000);
  LCD_CS_HIGH();
}

// LCD写16位数据
void LCD_Write_Data16(uint16_t data)
{
  uint8_t buf[2] = {data>>8, data&0xFF};
  LCD_CS_LOW();
  LCD_DC_HIGH();
  HAL_SPI_Transmit(&hspi1, buf, 2, 100);
  LCD_CS_HIGH();
}

// 设置显示窗口
void LCD_Set_Window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
  LCD_Write_Cmd(0x2A); // 列地址设置
  LCD_Write_Data16(x0);
  LCD_Write_Data16(x1);
  LCD_Write_Cmd(0x2B); // 行地址设置
  LCD_Write_Data16(y0);
  LCD_Write_Data16(y1);
  LCD_Write_Cmd(0x2C); // 写入GRAM
}

// LCD清屏
void LCD_Clear(uint16_t color)
{
  uint32_t pixel_num = 240*320;
  LCD_Set_Window(0, 0, 239, 319);
  LCD_CS_LOW();
  LCD_DC_HIGH();
  for(uint32_t i=0; i<pixel_num; i++)
  {
    LCD_Write_Data16(color);
  }
  LCD_CS_HIGH();
}

// LCD初始化
void LCD_Init(void)
{
  LCD_RES_LOW();
  HAL_Delay(100);
  LCD_RES_HIGH();
  HAL_Delay(100);

  LCD_Write_Cmd(0xCF);
  LCD_Write_Data16(0x00);
  LCD_Write_Data16(0xC1);
  LCD_Write_Data16(0X30);
  LCD_Write_Cmd(0xED);
  LCD_Write_Data16(0x64);
  LCD_Write_Data16(0x03);
  LCD_Write_Data16(0X12);
  LCD_Write_Data16(0X81);
  LCD_Write_Cmd(0xE8);
  LCD_Write_Data16(0x85);
  LCD_Write_Data16(0x00);
  LCD_Write_Data16(0x78);
  LCD_Write_Cmd(0xCB);
  LCD_Write_Data16(0x39);
  LCD_Write_Data16(0x2C);
  LCD_Write_Data16(0x00);
  LCD_Write_Data16(0x34);
  LCD_Write_Data16(0x02);
  LCD_Write_Cmd(0xF7);
  LCD_Write_Data16(0x20);
  LCD_Write_Cmd(0xEA);
  LCD_Write_Data16(0x00);
  LCD_Write_Data16(0x00);
  LCD_Write_Cmd(0xC0); // 功率控制
  LCD_Write_Data16(0x23); // VRH[5:0]
  LCD_Write_Cmd(0xC1); // 功率控制
  LCD_Write_Data16(0x10); // SAP[2:0];BT[3:0]
  LCD_Write_Cmd(0xC5); // VCM控制
  LCD_Write_Data16(0x3E);
  LCD_Write_Data16(0x28);
  LCD_Write_Cmd(0xC7); // VCM控制2
  LCD_Write_Data16(0x86);
  LCD_Write_Cmd(0x36); // 内存访问控制
  LCD_Write_Data16(0x00); // 扫描方向
  LCD_Write_Cmd(0x3A); // 像素格式
  LCD_Write_Data16(0x55); // 16位RGB565
  LCD_Write_Cmd(0xB1); // 帧速率控制
  LCD_Write_Data16(0x00);
  LCD_Write_Data16(0x18);
  LCD_Write_Cmd(0xB6); // 显示功能控制
  LCD_Write_Data16(0x08);
  LCD_Write_Data16(0x82);
  LCD_Write_Data16(0x27);
  LCD_Write_Cmd(0xF2); // 3伽马功能关闭
  LCD_Write_Data16(0x00);
  LCD_Write_Cmd(0x26); // 伽马曲线设置
  LCD_Write_Data16(0x01);
  LCD_Write_Cmd(0xE0); // 正伽马校正
  uint8_t gamma_p[] = {0x1F,0x1A,0x18,0x0A,0x0F,0x06,0x45,0X87,0x32,0x0A,0x07,0x02,0x07,0x05,0x00};
  LCD_Write_Data(gamma_p, 15);
  LCD_Write_Cmd(0XE1); // 负伽马校正
  uint8_t gamma_n[] = {0x00,0x25,0x27,0x05,0x10,0x09,0x3A,0x78,0x4D,0x05,0x18,0x0D,0x38,0x3A,0x1F};
  LCD_Write_Data(gamma_n, 15);
  LCD_Write_Cmd(0x11); // 退出睡眠
  HAL_Delay(120);
  LCD_Write_Cmd(0x29); // 开启显示
  LCD_Clear(0xFFFF); // 清屏白色
}

// 显示ASCII字符
void LCD_Show_Char(uint16_t x, uint16_t y, uint8_t ch, uint16_t color, uint16_t bg_color)
{
  ch = ch - ' ';
  LCD_Set_Window(x, y, x+7, y+15);
  for(uint8_t i=0; i<16; i++)
  {
    uint8_t temp = asc2_1608[ch*16+i];
    for(uint8_t j=0; j<8; j++)
    {
      if(temp & (0x80>>j))
        LCD_Write_Data16(color);
      else
        LCD_Write_Data16(bg_color);
    }
  }
}

// 显示16×16中文
void LCD_Show_Chinese(uint16_t x, uint16_t y, uint8_t index, uint16_t color, uint16_t bg_color)
{
  LCD_Set_Window(x, y, x+15, y+15);
  for(uint8_t i=0; i<32; i++)
  {
    uint8_t temp = hz_1616[index*32+i];
    for(uint8_t j=0; j<8; j++)
    {
      if(temp & (0x80>>j))
        LCD_Write_Data16(color);
      else
        LCD_Write_Data16(bg_color);
    }
  }
}

// 显示240×320全屏图片
void LCD_Show_Pic(uint16_t *pic_buf)
{
  LCD_Set_Window(0, 0, 239, 319);
  LCD_CS_LOW();
  LCD_DC_HIGH();
  HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)pic_buf, 240*320*2);
  LCD_CS_HIGH();
}

3.4 字符/中文/图片显示业务代码编写

在main.c文件中编写业务代码,所有代码必须写在用户代码区,避免重新生成时被覆盖。

c 复制代码
/* USER CODE BEGIN Includes */
#include "oled.h"
#include "lcd.h"
#include "font.h"
/* USER CODE END Includes */

/* USER CODE BEGIN 0 */
// DMA发送完成回调,释放片选
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if(hspi->Instance == SPI1)
  {
    LCD_CS_HIGH();
  }
}
/* USER CODE END 0 */

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_I2C1_Init();
  MX_SPI1_Init();

  /* USER CODE BEGIN 2 */
  // OLED与LCD初始化
  OLED_Init();
  LCD_Init();
  
  // OLED显示内容
  OLED_Show_String(0, 0, "STM32 OLED Test");
  OLED_Show_Chinese(0, 2, 0); // 中
  OLED_Show_Chinese(16, 2, 1); // 文
  OLED_Show_Chinese(32, 2, 2); // 显
  OLED_Show_Chinese(48, 2, 3); // 示
  OLED_Show_String(0, 4, "Author: STM32");
  OLED_Scroll_Left(0, 7, 0x07); // 全屏向左滚动
  
  // LCD显示内容
  LCD_Clear(0x0000); // 清屏黑色
  LCD_Show_Chinese(10, 10, 0, 0xF800, 0x0000); // 红色中文
  LCD_Show_Chinese(30, 10, 1, 0x07E0, 0x0000); // 绿色中文
  LCD_Show_Chinese(50, 10, 2, 0x001F, 0x0000); // 蓝色中文
  LCD_Show_Chinese(70, 10, 3, 0xFFFF, 0x0000); // 白色中文
  LCD_Show_String(10, 40, "STM32 LCD Test", 0xFFFF, 0x0000);
  // 显示全屏图片
  LCD_Show_Pic(gImage_test);
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
}

3.5 编译、烧录与效果验证

  1. 点击Keil顶部Build按钮(F7快捷键)编译工程,底部提示0 Error(s), 0 Warning(s),说明编译成功。
  2. 仿真器配置:点击魔法棒图标(Alt+F7快捷键),Debug选项卡选择ST-Link Debugger,Settings中确认Port为SW,能识别到芯片;Flash Download选项卡勾选Reset and Run,点击OK保存。
  3. 硬件接线核心注意事项:OLED屏必须接3.3V,严禁接5V;LCD屏VCC接5V,背光接3.3V;所有外设GND必须与核心板GND共地。
  4. 效果验证:
    • 上电后OLED屏正常点亮,显示字符串、中文汉字,全屏向左滚动;
    • LCD屏正常点亮,显示彩色中文、字符串,最终显示全屏测试图片,无花屏、乱码;
    • SPI DMA刷屏全程无CPU占用,系统可同步执行其他任务。

四、保姆式排错指南

异常现象/报错信息 核心根因 一步到位解决方法
OLED屏完全不亮,无任何显示 1. 接线错误,SCL/SDA接反、VCC/GND接反;2. I2C地址错误,0x3C与0x3D不匹配;3. I2C时钟配置错误,超过400KHz;4. 屏硬件损坏,或供电电压错误 1. 严格核对接线:SCL→PB6、SDA→PB7,VCC接3.3V,GND共地;2. 更换I2C写地址0x78或0x7A,匹配屏的硬件地址;3. 配置I2C为Fast Mode,400KHz及以下;4. 更换屏测试,确认硬件无损坏
OLED屏点亮,但显示乱码、错位、翻转 1. 取模格式与驱动代码不匹配,字节序、扫描方向错误;2. 初始化命令错误,扫描方向、地址模式配置错误;3. 显示坐标超出屏的范围,数据写入错位 1. 严格匹配取模格式:列行式、逆向、高位在前,与驱动代码一致;2. 核对初始化命令,配置正确的段重定义与COM扫描方向,解决翻转问题;3. 限制显示坐标在0≤x≤127、0≤y≤7范围内
LCD屏白屏/黑屏,无任何显示 1. 接线错误,SPI引脚接反、DC/RES/CS引脚配置错误;2. 初始化命令序列错误,控制器未正常初始化;3. SPI模式配置错误,CPOL/CPHA与屏不匹配;4. 背光引脚未接,背光未开启 1. 严格核对接线:SCK→PA5、MOSI→PA7、DC→PA2、RES→PA3、CS→PA4,GND共地;2. 核对ILI9341初始化命令序列,确保时序与参数正确;3. 配置SPI为模式0,CPOL=0、CPHA=0,MSB在前;4. 背光引脚接3.3V,确保背光开启
LCD屏显示花屏、颜色错乱、图片错位 1. 取模格式错误,RGB565字节序与驱动不匹配;2. 显示窗口配置错误,行列地址超出屏的范围;3. SPI通信速度过快,数据传输错误;4. CS引脚控制错误,通信期间CS未持续拉低 1. 取模时选择RGB565、16位色、高位在前,与驱动代码匹配;2. 配置显示窗口时,列地址0239,行地址0319,禁止超出范围;3. 降低SPI波特率,改为4分频或8分频,提升通信稳定性;4. 通信全程拉低CS引脚,通信结束后再拉高
中文显示乱码,无法显示正确汉字 1. 汉字GB2312编码与字模库的索引不匹配;2. 取模的点阵大小与驱动代码的显示尺寸不匹配;3. 字模数组未定义为const,导致内存溢出;4. 汉字索引计算错误,偏移量不对 1. 确保取模的汉字编码为GB2312,与字模库的排列顺序一致;2. 16×16汉字必须匹配16×16的显示函数,禁止混用不同尺寸的字模;3. 字模数组必须加const修饰,存储在Flash中,避免SRAM溢出;4. 严格按汉字在字模库中的顺序设置索引,确保偏移量正确
硬件I2C通信卡死,程序进入死循环 1. I2C总线出现死锁,从设备拉低SDA线无法释放;2. 通信超时时间设置过短,未完成传输就强制退出;3. 未开启I2C事件中断,或中断优先级配置错误 1. 增加I2C总线死锁恢复代码,模拟9个时钟周期释放SDA线;2. 增加通信超时时间,设置为100ms以上;3. 开启I2C事件中断,配置合理的抢占优先级,避免中断被屏蔽
DMA刷屏时程序卡死,进入HardFault 1. 图片数组定义为局部变量,栈空间溢出;2. 数组长度与传输长度不匹配,内存越界;3. DMA传输完成后未释放CS引脚,导致后续SPI通信异常 1. 图片数组必须定义为全局const数组,存储在Flash中,禁止使用局部数组;2. 严格核对传输长度为240×320×2字节,与数组长度一致;3. DMA传输完成回调中拉高CS引脚,释放SPI总线

五、我的踩坑记录

  1. 踩坑现象 :第一次驱动OLED屏,接线、配置都检查了,屏就是不亮,I2C通信一直返回超时,折腾了很久都没解决。
    底层原因 :我想当然地认为OLED屏的I2C地址是0x3C,写地址是0x3C<<1=0x78,但我手里的屏地址是0x3D,写地址应该是0x7A,地址不匹配,从设备不会应答,I2C通信一直超时。同时我把OLED屏的VCC接到了5V上,这款屏只支持3.3V供电,差点把屏烧了。
    最终解决方案:将OLED屏的VCC接到3.3V,把I2C写地址改为0x7A,重新烧录后屏立即点亮,显示完全正常。后来养成习惯,拿到新屏先查手册确认地址和供电电压,避免再踩这个坑。

  2. 踩坑现象 :OLED屏能点亮,但显示的字符全是乱码,上下翻转,左右错位,完全看不出内容。
    底层原因 :我用取模软件生成的字模是行列式、顺向的,而驱动代码用的是列行式、逆向的取模格式,字节序完全不匹配,写入GRAM的数据全是错的,自然显示乱码。同时初始化命令里的扫描方向配置反了,导致画面上下翻转。
    最终解决方案:重新设置取模软件,格式改为列行式、逆向、高位在前,和驱动代码完全匹配,同时修改初始化命令,配置正确的段重定义和COM扫描方向,重新生成字模后,显示完全正常,无错位、无翻转。

  3. 踩坑现象 :LCD屏能点亮,但显示中文和图片时,颜色完全不对,红色变成了蓝色,蓝色变成了红色,画面花屏。
    底层原因 :取模软件生成的RGB565数据是低位在前,而驱动代码是高位在前,字节序反了,导致RGB颜色分量完全错位,颜色错乱。同时SPI的波特率配置为2分频36MHz,超过了我手里这款屏的最大SPI时钟,数据传输出现错误,导致花屏。
    最终解决方案:取模时选择高位在前的RGB565格式,同时将SPI波特率改为4分频18MHz,符合屏的规格要求,重新生成图片数组后,颜色显示完全正确,无花屏、无错位。

  4. 踩坑现象 :把字模数组直接定义成了普通的全局数组,结果编译时提示SRAM内存溢出,程序无法下载,甚至下载后芯片直接卡死。
    底层原因 :16×16的中文汉字库有几百个汉字,每个汉字32字节,加上图片数组,总大小超过了STM32F103C8T6的20KB SRAM,直接导致内存溢出。普通数组默认存储在SRAM中,而const数组会存储在64KB的Flash中,我完全忽略了这一点。
    最终解决方案:所有字模、图片数组全部加上const修饰,存储在Flash中,编译后内存占用大幅降低,不再溢出,程序正常运行,芯片也不会卡死了。


六、课后小练习(附完整标准答案)

6.1 基础巩固练习

练习1:在OLED屏上实现ADC采集电压的实时刷新显示,每秒更新一次。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
uint16_t adc_val = 0;
float voltage = 0.0f;
char buf[32] = {0};
/* USER CODE END PV */

/* USER CODE BEGIN WHILE */
while (1)
{
  // 采集ADC值
  HAL_ADC_Start(&hadc1);
  HAL_ADC_PollForConversion(&hadc1, 100);
  adc_val = HAL_ADC_GetValue(&hadc1);
  HAL_ADC_Stop(&hadc1);
  // 计算电压
  voltage = (float)adc_val / 4095.0f * 3.3f;
  // 格式化字符串
  sprintf(buf, "Voltage:%.2fV", voltage);
  // 清除对应行,显示新内容
  OLED_Set_Pos(0, 6);
  OLED_Write_Data((uint8_t *)"                ", 16);
  OLED_Show_String(0, 6, buf);
  // 每秒更新一次
  HAL_Delay(1000);
}
/* USER CODE END WHILE */
练习2:在LCD屏上实现24位色深的彩虹渐变背景,叠加显示字符串。

标准答案

c 复制代码
// 彩虹渐变背景绘制函数
void LCD_Draw_Rainbow(void)
{
  for(uint16_t y=0; y<320; y++)
  {
    // 按行生成渐变颜色
    uint8_t r = (y * 255) / 320;
    uint8_t g = 255 - (y * 255) / 320;
    uint8_t b = 128;
    // 转换为RGB565
    uint16_t color = ((r>>3)<<11) | ((g>>2)<<5) | (b>>3);
    // 绘制整行
    LCD_Set_Window(0, y, 239, y);
    for(uint16_t x=0; x<240; x++)
    {
      LCD_Write_Data16(color);
    }
  }
}

// 主函数调用
/* USER CODE BEGIN 2 */
LCD_Init();
LCD_Draw_Rainbow();
LCD_Show_String(40, 150, "Rainbow Test", 0x0000, 0xFFFF);
/* USER CODE END 2 */
练习3:实现OLED屏的指定区域清屏,避免全屏清屏导致的闪烁。

标准答案

c 复制代码
// 指定区域清屏函数
void OLED_Clear_Area(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1)
{
  uint8_t buf[128] = {0};
  for(uint8_t y=y0; y<=y1; y++)
  {
    OLED_Set_Pos(x0, y);
    OLED_Write_Data(buf, x1 - x0 + 1);
  }
}

// 调用示例:清除第2行,0~127列
OLED_Clear_Area(0, 2, 127, 3);

6.2 进阶实战练习

练习1:基于SPI DMA实现LCD图片滑动切换效果,实现左右滑动动画。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
// 两张240×320的图片数组
extern const uint16_t gImage_pic1[];
extern const uint16_t gImage_pic2[];
uint8_t slide_offset = 0;
uint8_t slide_dir = 1; // 1右滑,-1左滑
/* USER CODE END PV */

// 滑动显示函数
void LCD_Slide_Show(const uint16_t *pic1, const uint16_t *pic2, uint8_t offset)
{
  for(uint16_t y=0; y<320; y++)
  {
    LCD_Set_Window(0, y, 239, y);
    LCD_CS_LOW();
    LCD_DC_HIGH();
    // 显示pic2的offset列
    HAL_SPI_Transmit(&hspi1, (uint8_t *)&pic2[y*240 + 240 - offset], offset*2, 100);
    // 显示pic1的剩余列
    HAL_SPI_Transmit(&hspi1, (uint8_t *)&pic1[y*240], (240 - offset)*2, 100);
    LCD_CS_HIGH();
  }
}

// 主循环滑动动画
/* USER CODE BEGIN WHILE */
while (1)
{
  if(slide_dir == 1)
  {
    slide_offset += 8;
    if(slide_offset >= 240)
    {
      slide_offset = 240;
      slide_dir = -1;
      HAL_Delay(1000);
    }
  }
  else
  {
    slide_offset -= 8;
    if(slide_offset == 0)
    {
      slide_dir = 1;
      HAL_Delay(1000);
    }
  }
  LCD_Slide_Show(gImage_pic1, gImage_pic2, slide_offset);
  HAL_Delay(20);
}
/* USER CODE END WHILE */
练习2:实现OLED屏的多级菜单界面,支持按键上下选择、确认进入。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
// 菜单列表
char *menu_list[] = {"1. Voltage Check", "2. Temp Monitor", "3. System Set", "4. About"};
uint8_t menu_num = sizeof(menu_list)/sizeof(char *);
uint8_t current_sel = 0; // 当前选中项
uint8_t key_state = 0;
/* USER CODE END PV */

// 菜单刷新函数
void OLED_Menu_Refresh(void)
{
  OLED_Clear();
  for(uint8_t i=0; i<menu_num; i++)
  {
    if(i == current_sel)
    {
      OLED_Show_String(0, i*2, ">"); // 选中箭头
    }
    OLED_Show_String(8, i*2, menu_list[i]);
  }
}

// 主循环按键处理
/* USER CODE BEGIN WHILE */
OLED_Menu_Refresh();
while (1)
{
  // 上键:PA0
  if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
  {
    HAL_Delay(20);
    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
    {
      if(current_sel > 0) current_sel--;
      OLED_Menu_Refresh();
      while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);
    }
  }
  // 下键:PA1
  if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
  {
    HAL_Delay(20);
    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
    {
      if(current_sel < menu_num-1) current_sel++;
      OLED_Menu_Refresh();
      while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);
    }
  }
  HAL_Delay(10);
}
/* USER CODE END WHILE */

七、核心知识点速记

  1. I2C是两线半双工总线,仅需SCL、SDA两根线,通过7位地址区分从设备,OLED屏常用地址0x3C,写地址0x78,通信速度最高400Kbps。
  2. SPI是四线全双工总线,SCK、MOSI、MISO、CS四根线,工业常用模式0(CPOL=0、CPHA=0),通信速度远高于I2C,适合LCD大屏高速刷新。
  3. 显示驱动的核心是点阵数据,所有字符、汉字、图片都需要通过取模软件转换为十六进制数组,取模格式必须与驱动代码的扫描顺序、字节序完全一致,否则会出现乱码。
  4. 字模、图片数组必须加const修饰,存储在Flash中,避免占用宝贵的SRAM内存,防止内存溢出。
  5. OLED屏驱动分为命令与数据,0x00标识命令,0x40标识数据,128×64屏分为8页,每页8行,1个字节对应同一列的8个像素。
  6. LCD屏驱动通过DC引脚区分命令与数据,低电平为命令,高电平为数据,240×320分辨率16位色全屏共153600字节数据,配合DMA可实现零CPU占用高速刷屏。
  7. 中文显示的核心是GB2312编码与字模库,通过汉字的区位码计算字模数组的偏移量,常用16×16点阵,每个汉字对应32字节数据。
  8. 硬件I2C必须处理总线死锁问题,通信时增加超时机制,避免程序卡死;SPI通信必须严格控制CS引脚,通信期间拉低,结束后拉高。
  9. 显示内容实时刷新时,禁止全屏清屏重绘,仅刷新变化的区域,避免屏幕闪烁,提升显示流畅度。

八、本章小结

本章我们深入拆解了I2C、SPI串行通信协议的核心时序,对比51单片机软件模拟时序方案的核心差异,吃透了SSD1306 OLED屏与ILI9341 LCD屏的驱动逻辑,掌握了字符、中文、图片的取模规则与显示实现,完成了HAL库硬件I2C/SPI的全场景驱动开发,解决了屏不亮、乱码花屏、总线卡死等高频问题。本章是I2C、SPI协议的实战应用,下一章我们将深入拆解I2C通信协议的底层时序与主从机通信逻辑,完成AT24C02、MPU6050等工业传感器的驱动开发,全面掌握I2C总线的工业级应用。

相关推荐
_李小白2 小时前
【AI大模型学习笔记之平台篇】第五篇:Trae常用模型介绍与性能对比
人工智能·笔记·学习
jason成都2 小时前
IoT 设备监控系统实战:基于 EMQX 的 MQTT 连接监控与数据格式指纹识别
开发语言·python
铭毅天下2 小时前
EasySearch Rules 规则语法速查手册
开发语言·前端·javascript·ecmascript
信创DevOps先锋2 小时前
Gitee:中国开发者生态的数字化转型基石与创新加速器
运维·gitee·devops
承渊政道2 小时前
【优选算法】(实战体会位运算的逻辑思维)
数据结构·c++·笔记·学习·算法·leetcode·visual studio
YMWM_2 小时前
print(f“{s!r}“)解释
开发语言·r语言
愤豆2 小时前
05-Java语言核心-语法特性--模块化系统详解
java·开发语言·python
bksczm2 小时前
文件流(fstream)
java·开发语言
NGC_66112 小时前
Java 线程池阻塞队列与拒绝策略
java·开发语言