基于STM32移植U8g2图形库,实现OLED图形、文字、动画显示(HAL库)

一、I2C协议基本原理

I2C(Inter-Integrated Circuit)协议是一种同步、半双工、串行通信总线协议,主要用于集成电路之间的低速通信。其基本原理可概括如下:

  1. 物理结构

    • 采用双线制:数据线(SDA)和时钟线(SCL)
    • 总线拓扑:支持多主多从结构,所有设备并联在总线上
    • 地址寻址:每个从设备有唯一地址(通常7位或10位)
  2. 通信机制

    • 起始条件:SCL高电平时SDA从高→低跳变
    • 停止条件:SCL高电平时SDA从低→高跳变
    • 数据有效性:SDA数据在SCL高电平时必须稳定
    • 字节传输:每字节后跟随应答位(ACK)或非应答位(NACK)
  3. 时序演示
SCL __/‾‾‾\__/‾‾‾\__/‾‾‾\__...
SDA __XXXXXXX_ACK_...
 

二、0.96 寸 OLED 屏工作原理

OLED(Organic Light-Emitting Diode),即有机发光二极管。其核心在于能够自发光的有机材料层。

0.96寸OLED屏幕通常具有128*64的分辨率,即由128列×64行共8192个独立的像素点构成一个矩阵,屏幕的驱动芯片(如SSD1306)接收来自微控制器(如Arduino、STM32)发送的图像数据,驱动芯片根据接收到的数据,精确地控制施加在每个像素点上的电压。施加电压的像素点发光,不施加电压(或施加反向电压)的像素点则不发光。通过控制每个像素点的亮灭(或灰度等级),就形成了我们看到的图像或文字。

三、U8g2 库简介

U8g2 是一个功能强大、开源的 单色图形显示库,它的核心目标是提供一套统一的接口,方便开发者驱动各种小型单色显示器,总计支持超过 200 种不同的显示控制器和屏幕组合

无论底层使用的是哪种显示控制器或通信接口(I²C, SPI, 并行 8位/6800/8080, UART, 甚至软件模拟),开发者只需使用 U8g2 提供的一套相同的函数来绘制图形和文字。这极大地简化了开发过程,提高了代码的可移植性。

它采用了 页面缓冲区(Page Buffer) 的概念。处理图形时,库会将屏幕分成若干页(通常是高度为 8 像素的块),每次只处理并传输一页的数据到显示屏。这种方式可以显著减少对微控制器内存(RAM)的需求,尤其是在处理高分辨率屏幕时。

四、CubeMX配置

首先我们要使用CubeMX生产一个模板工程,主要配置I2C通讯与定时器(us级延时)

五、U8g2移植

1.准备U8g2库文件

U8g2的源码是在GitHub上开源的,U8g2下载地址: https://github.com/olikraus/u8g2

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

2.精简U8g2库文件

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

2.1去掉无用的驱动文件

首先,类似 u8x8_d_xxx.c 命名的文件中包含 U8x8 的驱动兼容,文件名包括驱动的型号和屏幕分辨率,因此需要删除无用的驱动文件,只保留当前设备的驱动。例如,本次使用的是 128x64 的 SSD1306 屏幕,那么只需要保留 u8x8_ssd1306_128x64_noname.c 文件,删除其它类似的文件即可。删除其余驱动文件后:

2.2.精简u8g2_d_setup.c

本次使用的OLED是IIC接口,只留本次要用到u8g2_Setup_ssd1306_i2c_128x64_noname_f就好,其它的可以删掉或注释掉。这里建议使用vscode或其他好用的编辑器来搜索到这个函数,将其前后的其他函数删除。

2.3.精简u8g2_d_memory.c

用到的u8g2_Setup_ssd1306_i2c_128x64_noname_f函数中,只调用了u8g2_m_16_8_f这个函数,所以留下这个函数。

3.将精简后的U8g2库添加至Keil

我们将csrc文件夹移动到工程目录中

将精简后的U8g2库添加到keil

添加头文件

4.编写移植函数

新建目录U8g2_drv用于存放我们的移植代码

在Core中新建文件夹U8g2用于存放我们自己编写的代码

添加头文件路径

在tim.c中添加Tims_delay_us函数实现us级延时,在tim.h中声明

stm32_u8g2.c:

#include "stm32_u8g2.h"
#include "tim.h"
#include "i2c.h"
 
 
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
    static uint8_t buffer[128];
    static uint8_t buf_idx;
    uint8_t *data;
 
    switch (msg)
    {
    case U8X8_MSG_BYTE_INIT:
    {
        /* add your custom code to init i2c subsystem */
        MX_I2C2_Init(); //I2C初始化
    }
    break;
 
    case U8X8_MSG_BYTE_START_TRANSFER:
    {
        buf_idx = 0;
    }
    break;
 
    case U8X8_MSG_BYTE_SEND:
    {
        data = (uint8_t *)arg_ptr;
 
        while (arg_int > 0)
        {
            buffer[buf_idx++] = *data;
            data++;
            arg_int--;
        }
    }
    break;
 
    case U8X8_MSG_BYTE_END_TRANSFER:
    {
        if (HAL_I2C_Master_Transmit(&hi2c2, OLED_ADDRESS, buffer, buf_idx, 1000) != HAL_OK)
            return 0;
    }
    break;
 
    case U8X8_MSG_BYTE_SET_DC:
        break;
 
    default:
        return 0;
    }
 
    return 1;
}
 
 
 
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    switch (msg)
    {
    case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
        __NOP();
        break;
    case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
        for (uint16_t n = 0; n < 320; n++)
        {
            __NOP();
        }
        break;
    case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
        HAL_Delay(1);
        break;
    case U8X8_MSG_DELAY_I2C: // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
        Tims_delay_us(5);
        break;                    // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
    case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
        break;                    // arg_int=1: Input dir with pullup high for I2C clock pin
    case U8X8_MSG_GPIO_I2C_DATA:  // arg_int=0: Output low at I2C data pin
        break;                    // arg_int=1: Input dir with pullup high for I2C data pin
    case U8X8_MSG_GPIO_MENU_SELECT:
        u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
        break;
    case U8X8_MSG_GPIO_MENU_NEXT:
        u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
        break;
    case U8X8_MSG_GPIO_MENU_PREV:
        u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
        break;
    case U8X8_MSG_GPIO_MENU_HOME:
        u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
        break;
    default:
        u8x8_SetGPIOResult(u8x8, 1); // default return value
        break;
    }
    return 1;
}
 
//U8g2的初始化,需要调用下面这个u8g2_Setup_ssd1306_128x64_noname_f函数,该函数的4个参数含义:
//u8g2:传入的U8g2结构体
//U8G2_R0:默认使用U8G2_R0即可(用于配置屏幕是否要旋转)
//u8x8_byte_sw_i2c:使用软件IIC驱动,该函数由U8g2源码提供
//u8x8_gpio_and_delay:就是上面我们写的配置函数
 
void u8g2Init(u8g2_t *u8g2)
{
	u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay); // 初始化u8g2 结构体
	u8g2_InitDisplay(u8g2);                                                                       // 
	u8g2_SetPowerSave(u8g2, 0);                                                                   // 
	u8g2_ClearBuffer(u8g2);
}
 
 
void draw(u8g2_t *u8g2)
{
	u8g2_ClearBuffer(u8g2); 
	
    u8g2_SetFontMode(u8g2, 1); /*字体模式选择*/
    u8g2_SetFontDirection(u8g2, 0); /*字体方向选择*/
    u8g2_SetFont(u8g2, u8g2_font_inb24_mf); /*字库选择*/
    u8g2_DrawStr(u8g2, 0, 20, "U");
    
    u8g2_SetFontDirection(u8g2, 1);
    u8g2_SetFont(u8g2, u8g2_font_inb30_mn);
    u8g2_DrawStr(u8g2, 21,8,"8");
        
    u8g2_SetFontDirection(u8g2, 0);
    u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
    u8g2_DrawStr(u8g2, 51,30,"g");
    u8g2_DrawStr(u8g2, 67,30,"\xb2");
    
    u8g2_DrawHLine(u8g2, 2, 35, 47);
    u8g2_DrawHLine(u8g2, 3, 36, 47);
    u8g2_DrawVLine(u8g2, 45, 32, 12);
    u8g2_DrawVLine(u8g2, 46, 33, 12);
  
    u8g2_SetFont(u8g2, u8g2_font_4x6_tr);
    u8g2_DrawStr(u8g2, 1,54,"github.com/olikraus/u8g2");
		
	u8g2_SendBuffer(u8g2);
	HAL_Delay(1000);
}
 
//画点填充
void testDrawPixelToFillScreen(u8g2_t *u8g2)
{
  int t = 1000;
	u8g2_ClearBuffer(u8g2);
 
  for (int j = 0; j < 64; j++)
  {
    for (int i = 0; i < 128; i++)
    {
      u8g2_DrawPixel(u8g2,i, j);
    }
  }
  HAL_Delay(1000);
}

stm32_u8g2.h:

#ifndef __STM32_U8G2_H
#define __STM32_U8G2_H
 
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "u8g2.h"
/* USER CODE BEGIN Includes */
 
/* USER CODE END Includes */
 
 
 
/* USER CODE BEGIN Private defines */
 
/* USER CODE END Private defines */
#define u8         unsigned char  // ?unsigned char ????
#define MAX_LEN    128  //
#define OLED_ADDRESS  0x78 // oled
#define OLED_CMD   0x00  // 
#define OLED_DATA  0x40  // 
 
/* USER CODE BEGIN Prototypes */
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
void u8g2Init(u8g2_t *u8g2);
void draw(u8g2_t *u8g2);
void testDrawPixelToFillScreen(u8g2_t *u8g2);
 
#endif

六、功能实现与展示

1.U8g2 Demo 例程

调用官方测试函数draw,实现基本图形显示:

void draw(u8g2_t *u8g2)
{
	u8g2_ClearBuffer(u8g2); 
	
    u8g2_SetFontMode(u8g2, 1); /*字体模式选择*/
    u8g2_SetFontDirection(u8g2, 0); /*字体方向选择*/
    u8g2_SetFont(u8g2, u8g2_font_inb24_mf); /*字库选择*/
    u8g2_DrawStr(u8g2, 0, 20, "U");
    
    u8g2_SetFontDirection(u8g2, 1);
    u8g2_SetFont(u8g2, u8g2_font_inb30_mn);
    u8g2_DrawStr(u8g2, 21,8,"8");
        
    u8g2_SetFontDirection(u8g2, 0);
    u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
    u8g2_DrawStr(u8g2, 51,30,"g");
    u8g2_DrawStr(u8g2, 67,30,"\xb2");
    
    u8g2_DrawHLine(u8g2, 2, 35, 47);
    u8g2_DrawHLine(u8g2, 3, 36, 47);
    u8g2_DrawVLine(u8g2, 45, 32, 12);
    u8g2_DrawVLine(u8g2, 46, 33, 12);
  
    u8g2_SetFont(u8g2, u8g2_font_4x6_tr);
    u8g2_DrawStr(u8g2, 1,54,"github.com/olikraus/u8g2");
		
	u8g2_SendBuffer(u8g2);
	HAL_Delay(1000);
}

2.学号姓名展示


// “雷”
const unsigned char lei[] U8X8_PROGMEM =  {0x00,0x00,0xfc,0x1f,0x80,0x00,0xfe,0x7f,0x82,0x40,0xb9,0x2e,0x80,0x00,0xb8,0x0e,0x00,0x00,0xfc,0x1f,0x84,0x10,0x84,0x10,0xfc,0x1f,0x84,0x10,0x84,0x10,0xfc,0x1f,};
// “国”
const unsigned char guo[] U8X8_PROGMEM = {0x00,0x00,0xfe,0x3f,0x02,0x20,0x02,0x20,0xfa,0x2f,0x82,0x20,0x82,0x20,0xf2,0x27,0x82,0x20,0x82,0x22,0x82,0x24,0xfa,0x2f,0x02,0x20,0x02,0x20,0xfe,0x3f,0x02,0x20,};
// “铎”
const unsigned char ze[] U8X8_PROGMEM={0x08,0x00,0xc8,0x3f,0xbc,0x20,0x04,0x11,0x02,0x0a,0x3d,0x04,0x08,0x1b,0xc8,0x64,0x3f,0x04,0x88,0x3f,0x08,0x04,0x08,0x04,0xe8,0x7f,0x18,0x04,0x08,0x04,0x00,0x04,};
void u8g2Draw_IDname(u8g2_t *u8g2)
{
   //显示自己的学号和名字
   u8g2_ClearBuffer(u8g2);
   u8g2_SetFont(u8g2, u8g2_font_ncenB08_tf); // 加粗字体
   u8g2_DrawStr(u8g2, 10, 14, "ID: 632207030513");  // 学号
   u8g2_DrawXBMP(u8g2, 10, 30, 16, 16, lei);   
   u8g2_DrawXBMP(u8g2, 26, 30, 16, 16, guo );   
   u8g2_DrawXBMP(u8g2, 42, 30, 16, 16, ze );
}

3.学号姓名滑动

int16_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 u8g2Draw_Move(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: 632207030513");  // 学号
		u8g2_DrawXBMP(u8g2, pos_x, pos_y, 16, 16, lei);   
		u8g2_DrawXBMP(u8g2, pos_x1, pos_y, 16, 16, guo );   
		u8g2_DrawXBMP(u8g2, pos_x2, pos_y, 16, 16, ze );
        if (pos_y < 0&&state==0) { 
         state=1; 
    }
        if(pos_y>48&&state==1){
         state=2;
    }
        if(pos_x<-20&&state==2){
            state=3;
        }
}

滑动

4.动画效果--小人起跳


// 动画状态
typedef enum {
    JUMP_UP,
    JUMP_DOWN,
    ON_GROUND
} JumpState;

// 小人结构体
typedef struct {
    int16_t x, y;           // 位置
    int16_t base_y;         // 地面基准位置
    int8_t jump_height;     // 跳跃高度
    JumpState state;        // 跳跃状态
    uint8_t frame;          // 动画帧
    int8_t velocity;        // 跳跃速度
} Character;

Character player;
uint32_t last_animation_time = 0;
const uint32_t ANIMATION_INTERVAL = 80; // 动画帧间隔

void JumpingMan_Init(void)
{
    player.x = 30;
    player.base_y = 45;     // 地面位置
    player.y = player.base_y;
    player.jump_height = 20;
    player.state = ON_GROUND;
    player.frame = 0;
    player.velocity = 0;
    
    last_animation_time = HAL_GetTick();
}
void Update_JumpingMan(void)
{
    uint32_t current_time = HAL_GetTick();
    
    if (current_time - last_animation_time < ANIMATION_INTERVAL) {
        return;
    }
    
    last_animation_time = current_time;
    
    // 更新动画帧
    player.frame = (player.frame + 1) % 4;
    
    // 状态机处理跳跃物理
    switch (player.state) {
        case ON_GROUND:
            // 在地面上等待,然后起跳
            if (player.frame % 4 == 0) {
                player.state = JUMP_UP;
                player.velocity = -4; // 向上的初速度
            }
            break;
            
        case JUMP_UP:
            player.y += player.velocity;
            player.velocity += 1; // 重力加速度
            
            // 到达最高点或开始下落
            if (player.velocity >= 0) {
                player.state = JUMP_DOWN;
            }
            break;
            
        case JUMP_DOWN:
            player.y += player.velocity;
            player.velocity += 1; // 重力加速度
            
            // 落地检测
            if (player.y >= player.base_y) {
                player.y = player.base_y;
                player.state = ON_GROUND;
                player.velocity = 0;
            }
            break;
    }
}
void Draw_Character(u8g2_t *u8g2)
{
    uint8_t body_height = 15;
    uint8_t head_radius = 4;
    
    // 绘制头部
    u8g2_DrawDisc(u8g2, player.x, player.y - body_height - head_radius, head_radius, U8G2_DRAW_ALL);
    
    // 绘制身体
    u8g2_DrawVLine(u8g2, player.x, player.y - body_height, body_height);
    
    // 绘制手臂 - 根据跳跃状态有不同的摆动
    if (player.state == JUMP_UP) {
        u8g2_DrawLine(u8g2, player.x, player.y - body_height + 5, 
                      player.x - 8, player.y - body_height - 2);
        u8g2_DrawLine(u8g2, player.x, player.y - body_height + 5, 
                      player.x + 8, player.y - body_height - 2);
    } else if (player.state == JUMP_DOWN) {
        u8g2_DrawLine(u8g2, player.x, player.y - body_height + 5, 
                      player.x - 6, player.y - body_height + 10);
        u8g2_DrawLine(u8g2, player.x, player.y - body_height + 5, 
                      player.x + 6, player.y - body_height + 10);
    } else {
        // 站立时的手臂
        u8g2_DrawLine(u8g2, player.x, player.y - body_height + 5, 
                      player.x - 6, player.y - body_height + 3);
        u8g2_DrawLine(u8g2, player.x, player.y - body_height + 5, 
                      player.x + 6, player.y - body_height + 3);
    }
    
    // 绘制腿部 - 根据动画帧有走路效果
    int8_t leg_offset = (player.frame % 2) * 2 - 1; // -1 或 1
    
    if (player.state == ON_GROUND) {
        // 站立时的腿部动画
        u8g2_DrawLine(u8g2, player.x, player.y, 
                      player.x - 4, player.y + 8 + leg_offset);
        u8g2_DrawLine(u8g2, player.x, player.y, 
                      player.x + 4, player.y + 8 - leg_offset);
    } else {
        // 跳跃时的腿部
        u8g2_DrawLine(u8g2, player.x, player.y, 
                      player.x - 4, player.y + 6);
        u8g2_DrawLine(u8g2, player.x, player.y, 
                      player.x + 4, player.y + 6);
    }
}

void Draw_Scene(u8g2_t *u8g2)
{
    u8g2_ClearBuffer(u8g2);
    
    // 设置字体
    u8g2_SetFont(u8g2, u8g2_font_6x10_tf);
    
    // 绘制标题
    u8g2_DrawStr(u8g2, 15, 10, "Jumping Man!");
    
    // 绘制地面
    u8g2_DrawHLine(u8g2, 0, player.base_y + 10, u8g2_GetDisplayWidth(u8g2));
    
    // 绘制云朵
    u8g2_DrawDisc(u8g2, 100, 20, 4, U8G2_DRAW_ALL);
    u8g2_DrawDisc(u8g2, 105, 18, 3, U8G2_DRAW_ALL);
    u8g2_DrawDisc(u8g2, 95, 18, 3, U8G2_DRAW_ALL);
    
    // 绘制太阳
    u8g2_DrawDisc(u8g2, 110, 15, 6, U8G2_DRAW_ALL);
    
    // 绘制小人
    Draw_Character(u8g2);
    
    // 显示跳跃状态
    const char* state_text = "";
    switch (player.state) {
        case JUMP_UP: state_text = "UP"; break;
        case JUMP_DOWN: state_text = "DOWN"; break;
        case ON_GROUND: state_text = "GROUND"; break;
    }
    u8g2_DrawStr(u8g2, 80, 60, state_text);
    
    u8g2_SendBuffer(u8g2);
}

动画

5.主函数

将上述函数在主函数中调用即可实现以上功能

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C2_Init();
  MX_TIM1_Init();
  /* USER CODE BEGIN 2 */
	u8g2_t u8g2;
  u8g2Init(&u8g2);
	JumpingMan_Init();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		
		Update_JumpingMan();
		Draw_Scene(&u8g2);
//			 u8g2_FirstPage(&u8g2);
//       do
//       {
//				 u8g2Draw_Move(&u8g2);
//       } while (u8g2_NextPage(&u8g2));
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

七.虚拟逻辑分析仪去采集 I2C 引脚SDA的波形

当使用 Keil 的虚拟逻辑分析仪采集 I2C 的 SDA 引脚波形时,正常情况下应该呈现出符合 I2C 协议规范的波形特征:

起始信号(Start):当 SCL 为高电平时,SDA 从高电平跳转为低电平(一个下降沿)。
数据传输:在 SCL 的每个高电平期间,SDA 保持稳定的电平状态,代表传输的一位数据(高电平为 1,低电平为 0)。每传输 8 位数据后,会有一个 ACK/NACK 位,从机拉低 SDA 表示 ACK,保持高电平表示 NACK。
停止信号(Stop):当 SCL 为高电平时,SDA 从低电平跳转为高电平(一个上升沿)。
空闲状态:当 I2C 总线空闲时,SDA 和 SCL 都保持高电平(由上拉电阻维持)。
仿真实验结果如下:

这里没有出现 I2C 波形,可以考虑以下替代方法

1.使用物理逻辑分析仪或示波器

连接真实的逻辑分析仪(如 Saleae Logic Analyzer)或数字示波器到目标硬件的 I2C 信号线(SCL 和 SDA)。这些工具可以直接捕获实时波形,并提供详细的分析功能(如协议解码)。

  • 优势:高精度、实时数据,适用于硬件级调试。
  • 注意事项:确保探头带宽足够(通常 >1 MHz),并注意信号接地以避免噪声。

2.I2C 外设寄存器监控

在 Keil 仿真环境中,可以通过 “Peripherals → I2Cx”(如 I2C2)查看 I2C 外设的寄存器状态,间接验证 I2C 通信过程:

SR1 寄存器:检查SB位(起始信号发送成功)、ADDR位(地址发送成功)、TXE位(发送缓冲区空)、RXNE位(接收缓冲区非空)等。
SR2 寄存器:检查MSL位(主机模式)、BUSY位(总线忙)、ACK位(是否收到应答)等。
如果这些寄存器的状态符合预期(如发送地址后ADDR置位,且ACK有效),说明 I2C 协议在软件层面已经正确执行,问题可能出在虚拟逻辑分析仪的配置或硬件上。

如图所示,寄存器的状态是正常的,且符合IIC通讯

八、总结

本文详细介绍了I2C协议原理及0.96寸OLED屏的驱动方法。首先阐述I2C协议的双线制结构、地址寻址机制和通信时序,说明OLED自发光的像素控制特性。重点讲解U8g2图形库的移植过程,包括驱动文件精简、硬件I2C接口实现和延时函数配置。通过STM32CubeMX生成工程模板,完成I2C和定时器初始化,实现字符显示、图形绘制和动画效果等功能演示。文章还探讨了虚拟逻辑分析仪采集I2C波形的方法,为嵌入式显示开发提供完整解决方案。实验结果表明该方法能有效驱动OLED屏,具有实用参考价值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值