新手也能学会的 STM32 OLED 开发:I2C 协议、字模生成、滑动动画一步到位

一、理论知识

在动手实践前,先理解以下核心概念,这对后续调试至关重要:

1.1 I2C协议基本原理

I2C是一种串行通信协议,仅需SDA(数据线)和SCL(时钟线)即可实现设备间通信。其核心特性包括:

  • 起始信号:SCL高电平时,SDA从高电平跳转为低电平;
  • 停止信号:SCL高电平时,SDA从低电平跳转为高电平;
  • 数据传输:每字节8位,传输完后跟随1位ACK/NACK响应位,通信速率有100kbps等标准模式;

1.2 0.96寸OLED屏工作原理

本次使用的是SSD1306驱动、128×64分辨率的I2C接口OLED屏,其I2C地址通常为0x780x7A。显示原理为自发光,每个像素对应1位数据(亮/暗),无需背光,功耗极低。操作上,需通过I2C写入指令后刷新显示。

1.3 U8g2图形库简介

U8g2是一款优秀的开源单色图形库,在GitHub上开源(地址:https://github.com/olikraus/u8g2)。它支持多种嵌入式平台(如STM32),功能简洁且强大,可实现:

  • 图形绘制(点、线、矩形、圆、椭圆等);
  • 文字显示(支持多种字体,可适配中文);
  • 分页显示、滑动显示等功能;

二、硬件准备与接线

  • 主控:STM32F103C8T6(最小系统板);

  • OLED屏:SSD1306驱动的0.96寸I2C OLED屏;

  • 接线方式

    OLED引脚 STM32引脚 说明
    VCC 3.3V 电源输入
    GND GND 接地
    SCL PB11 I2C时钟线
    SDA PB10 I2C数据线

三、工程搭建

在移植U8g2之前,需使用CubeMX配置STM32的部分功能:

3.1 RCC配置

开启外部晶振(HSE),选择“Crystal/Ceramic Resonator”。

请添加图片描述

3.2 SYS配置

调试接口选择“Serial Wire”。

02

3.3 I2C配置(OLED通信方式)

选择I2C外设(如I2C2),配置模式为“主模式”,时钟速度可设置为100kHz(标准模式)。

03

3.4 TIM配置(U8g2图形需要us级延迟)

配置一个通用定时器,用于生成精准的微秒级延迟,为U8g2的时序提供支撑。

04

3.5 时钟树配置

根据外部晶振频率(通常为8MHz或12MHz),配置系统时钟为72MHz(STM32F103C8T6的常用主频)。

05

四、U8g2移植

4.1 准备U8g2源码

U8g2的源码在GitHub上开源,进入源码地址后下载整个U8g2图形库的压缩包。

06

4.2 精简U8g2文件

U8g2支持多种显示驱动屏幕,源码中包含了各个驱动对应的文件。为减小整个工程的代码体积,在移植U8g2时,可删除一些无用的文件:

  • 无用驱动文件:这些文件在u8g2_drivers目录下,命名格式为u8x8_d_xxx.c,其中xxx表示具体的驱动芯片型号与屏幕分辨率组合。例如,对于使用SSD1306驱动芯片、分辨率为128×64的OLED屏幕,实际所需的驱动文件为u8x8_d_ssd1306_128x64_noname.c,其余与项目无关的驱动文件(如支持SH1106、SSD1327、SSD1351等芯片的.c文件)均可安全删除,以显著减小最终固件体积。
  • 精简u8g2_setup.c:该文件包含了所有显示设备的初始化函数(即u8g2_Setup_xxx系列函数)。由于本项目使用的是I2C接口的SSD1306 128×64 OLED屏幕,只需保留对应的初始化函数:u8g2_Setup_ssd1306_i2c_128x64_noname_f,其余函数可全部删除以简化程序。
  • 精简u8g2_memory.c:该文件定义了多种显存分配方案(以u8g2_Memory_xxx命名的函数)。由于我们选用的是u8g2_Setup_ssd1306_i2c_128x64_noname_f,其内部仅调用了u8g2_m_16_8_1这个函数,所以留下这个函数即可,其余显存分配函数可删除。

4.3 将精简后的U8g2添加到Keil工程

将上述精简后的U8g2文件(.c.h)添加到Keil的工程组文件夹中,在代码中包含对应的头文件(如u8g2.hu8x8.h),并确保编译路径正确。

请添加图片描述

五、功能实现与效果展示

5.1 U8g2 Demo例程

调用U8g2自带的图形绘制函数,实现基本图形显示:

#include "stm32_u8g2.h"//main.c函数
#include "test.h"
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_I2C2_Init();
  MX_TIM1_Init();
  u8g2_t u8g2;
  u8g2Init(&u8g2);	
  while (1)
  {
             u8g2_FirstPage(&u8g2);
       do
       {
				 u8g2DrawTest(&u8g2);
       } while (u8g2_NextPage(&u8g2));
  }
}

调用库中自带的图形绘制函数,实现基本图形显示:

{
    //demo例程
    u8g2_DrawLine(u8g2, 0, 0, 127, 63);  // 斜线
    u8g2_DrawCircle(u8g2, 64, 32, 20, U8G2_DRAW_ALL);  // 圆形
    u8g2_DrawFrame(u8g2, 10, 10, 40, 20);  // 矩形框
    u8g2_SetFont(u8g2, u8g2_font_ncenB14_tr);  // 设置字体
    u8g2_DrawStr(u8g2, 20, 50, "U8g2 Demo");  // 字符串
}

效果展示

21d0ab2c40721c8493a50d77025d6f57

5.2 显示学号和名字

注意:这里加载中文字体(如u8g2_font_wqy12_t_gb2312)时,若字库太大编译器不支持,需自己取模再进行显示。
首先在字模生成器中取中文的编码。进入网站后输入需求文字,选择“逐行式”,取模即可。
将取得的编码写到代码中,主要代码如下:

// 自定义“张”字16x16点阵
const unsigned char liao[] U8X8_PROGMEM =  {0x00,0x01,0xfc,0x7f,0x04,0x00,0xf4,0x3e,0x94,0x24,0xa4,0x28,0x94,0x25,0x84,0x06,0x64,0x18,0x1c,0x61,0xc4,0x04,0x34,0x02,0x82,0x11,0x72,0x0c,0x81,0x03,0x70,0x00,};
// 自定义“凯”字16x16点阵
const unsigned char jia[] U8X8_PROGMEM = {0x10,0x02,0x10,0x02,0xd0,0x3f,0x08,0x02,0x08,0x02,0x0c,0x02,0xfc,0x7f,0x0a,0x00,0x09,0x02,0x08,0x02,0xc8,0x3f,0x08,0x02,0x08,0x02,0x08,0x02,0xf8,0x7f,0x08,0x00,};
void u8g2DrawTest(u8g2_t *u8g2)
{
   //显示自己的学号和名字
   u8g2_ClearBuffer(u8g2);
   u8g2_SetFont(u8g2, u8g2_font_ncenB12_tf); // 加粗字体
   u8g2_DrawStr(u8g2, 10, 14, "ID: 123456");  // 学号
   u8g2_DrawXBMP(u8g2, 10, 30, 16, 16, liao);   
   u8g2_DrawXBMP(u8g2, 26, 30, 16, 16, jia );   
   u8g2_DrawXBMP(u8g2, 42, 30, 16, 16, le );
}

效果展示

5.3 上下、左右滑动显示

通过逐帧更新坐标实现滑动,先进行上下滑动在进行左右滑动,主要代码如下:

static int16_t pos_x = 0;  // 水平偏移量
static intint16_t pos_x = 0;  // 水平滑动位置(初始在最左)
int16_t pos_x1=16;
int16_t pos_x2=32;
int16_t pos_y = 50;  // 垂直滑动位置(初始在最上)
int16_t pos_y1=44;
uint8_t state=0;
uint8_t slide_speed = 4;  // 滑动步长(越大越快)
void u8g2DrawTest(u8g2_t *u8g2)
{    //上下左右滑动显示
    if(state==0){
    pos_y -= slide_speed;  // 向上滑动
    pos_y1 -= slide_speed;
    }
    else if(state==1){
    pos_y += slide_speed;  //向下滑动
    pos_y1 += slide_speed;
    }
    else if(state==2){
    pos_x-=slide_speed;
    pos_x1-=slide_speed;
    pos_x2-=slide_speed;
    }
    else {
    pos_x+=slide_speed;
    pos_x1+=slide_speed;
    pos_x2+=slide_speed;
    }
    u8g2_ClearBuffer(u8g2);
    u8g2_SetFont(u8g2, u8g2_font_ncenB12_tf); // 加粗字体
    u8g2_DrawStr(u8g2, pos_x, pos_y1, "ID: 123456");  // 学号
    u8g2_DrawXBMP(u8g2, pos_x, pos_y, 16, 16, liao);   
    u8g2_DrawXBMP(u8g2, pos_x1, pos_y, 16, 16, jia );   
    u8g2_DrawXBMP(u8g2, pos_x2, pos_y, 16, 16, le );
        if (pos_y < 0&&state==0) {  // 最后一行初始y=40,滑出屏幕上边界(y<0)后重置
         state=1; 
    }
        if(pos_y>48&&state==1){
         state=2;
    }
        if(pos_x<-20&&state==2){
            state=3;
        }
    
16_t slide_speed = 4; // 滑动速度

void drawSlideScreen(u8g2_t *u8g2)
{
  u8g2_ClearBuffer(u8g2);
  u8g2_SetFont(u8g2, u8g2_font_6x10_tf);
  u8g2_DrawStr(u8g2, pos_x, 30, "这是一个左右滑动的示例文本");
  u8g2_SendBuffer(u8g2);
  
  pos_x += slide_speed;
  if (pos_x > 127 || pos_x < -100) // 超出范围后反向
  {
    slide_speed = -slide_speed;
  }
  HAL_Delay(50);
}

int main(void)
{
  // 初始化代码省略...
  while (1)
  {
    drawSlideScreen(&u8g2);
  }
}

效果展示

5.4 动态图案生成

将图案分解为多帧点阵,通过“帧切换”实现动画。以“火柴人跑步”为例(通过3帧实现):

  1. 生成帧数据:用取模软件生成三帧的数据,并保存为数组;
  2. 多帧切换实现动画
    uint8_t current_frame = 0;  // 当前帧索引
  const uint16_t frame_delay = 50;  // 每帧停留50ms

  while (1) {  // 无限循环播放动画
    u8g2_FirstPage(u8g2);  // 开始绘制
    do {
      u8g2_ClearBuffer(u8g2);  // 清屏(清除缓冲区)

      // 根据当前帧索引绘制对应图案
      switch (current_frame) {
        case 0:
          // 绘制第1帧,确保宽高与点阵实际尺寸一致
          u8g2_DrawXBMP(u8g2, 20, 10, 70, 64, tractor_bitmap);
          break;
        case 1:
          // 绘制第2帧
          u8g2_DrawXBMP(u8g2, 20, 10, 50, 64, tractor_bitmap1);
          break;
        case 2:
          // 绘制第3帧
          u8g2_DrawXBMP(u8g2, 20, 10, 62, 64, tractor_bitmap2);
          break;
      }

    } while (u8g2_NextPage(u8g2));  // 刷新屏幕,完成当前帧显示

    // 切换到下一帧(循环0→1→2→0)
    current_frame = (current_frame + 1) % 3;

    // 控制动画速度(每帧停留100ms)
    HAL_Delay(frame_delay);
  }
     u8g2_DrawXBMP(u8g2, 20, 10, 62, 64, tractor_bitmap2);
      break;
  }

} while (u8g2_NextPage(u8g2));  // 刷新屏幕,完成当前帧显示

// 切换到下一帧(循环0→1→2→0)
current_frame = (current_frame + 1) % 3;

// 控制动画速度(每帧停留100ms)
HAL_Delay(frame_delay);

效果展示

六、底层逻辑分析与I2C引脚波形采集

6.1 用Keil虚拟逻辑分析仪采集I2C的SDA和SCL波形

当使用Keil的虚拟逻辑分析仪采集I2C的SDA和SCL波形时,在正常通信情况下可以观察到符合I2C协议规范的波形:

  • 起始信号(Start):当SCL为高电平时,SDA从高电平跳转为低电平(一个下降沿);
  • 数据传输:在SCL的每个高电平期间,SDA保持稳定电平(代表每一位数据),每传输完1字节数据后,会有一个ACK/NACK电平(从机拉低SDA表示ACK,保持高电平表示NACK);
  • 停止信号(Stop):当SCL为高电平时,SDA从低电平跳转为高电平(一个上升沿);
  • SCL时钟:由主机产生,高电平和低电平持续时间由通信速率决定。

实物验证波形如下

0012

6.2 若Keil虚拟逻辑分析仪无法采集到符合上述特征的I2C波形,可采用以下替代方法:

  • 1. 硬件逻辑分析仪/示波器
    这是最直接有效的方法。将逻辑分析仪或示波器的探头连接到SDA和SCL引脚(注意共地),可以实时采集真实的I2C电平变化。

    • 优势:能稳定采集真实的硬件状态,不受软件模拟的限制;
    • 劣势:需购置硬件设备,成本较高;
    • 适用场景:适合排查硬件连接问题(如上拉电阻缺失、电平不匹配、信号干扰等)。
  • 2. I2C协议分析仪/调试器
    在调试环境中可通过外接I2C协议分析仪(如I2C调试器模块),将其串联在I2C总线上,实时解析I2C外设的寄存器状态、数据收发情况。

    • 优势:能解析I2C的地址、数据、ACK/NACK等,直接判断I2C协议是否合规;
    • 适用场景:适合排查通信逻辑问题(如地址错误、数据位序错误等)。
  • 3. GPIO模拟与串口打印
    如果使用软件模拟I2C的IO控制逻辑,可以在代码中插入串口打印,将每一步的SDA、SCL电平、发送/接收的数据输出到串口助手,以此分析I2C通信过程。

七、心得体会

本开发以 STM32F103C8T6 为主控、0.96 寸 SSD1306 OLED 屏为显示载体,完成从基础搭建到功能落地的全流程。前期明确 I2C 协议核心规则、OLED 屏特性及 U8g2 库功能,按 “OLED 的 SCL 接 PB11、SDA 接 PB10” 完成硬件接线;通过 CubeMX 配置 RCC、SYS、I2C 等功能,精简 U8g2 库,并移植到 Keil 工程,筑牢开发基础。

功能实现上,调用 U8g2 函数完成基础图形绘制,借助字模生成器实现中文与学号显示,通过偏移量控制实现滑动效果,以 “多帧切换” 完成动态图案动画;调试阶段优先用 Keil 虚拟逻辑分析仪验证 I2C 波形,辅以硬件分析仪、串口打印等方案排查问题。整体流程覆盖原理、配置、开发与调试,为嵌入式 OLED 显示开发提供简洁高效的参考方案。

这次 STM32 驱动 OLED 的开发,让我真切感受到嵌入式开发 “细节即关键”。从 I2C 配置错误导致的黑屏,到 U8g2 库冗余文件引发的编译问题,每一次排查都需紧扣底层原理 —— 直到用逻辑分析仪看到合规的波形、精简库文件后程序跑通,才懂 “按需精简” 与 “协议匹配” 的重要性。最终看到汉字和动画正常显示时,更明白:嵌入式开发的成长,就藏在每一次解决问题的顿悟里。

八、仓库地址

https://gitee.com/zhang-kaikaikaikai/oled.git

九、参考博客

https://blog.csdn.net/black_sneak/article/details/126312657

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐