基于STM32F103与U8g2库的OLED图形显示
1.打开STM32CubeMX,点击File->。2.在芯片选择器中,在搜索框输入,然后在下方列表中选择,点击。RCCI2C1I2C1 ModeI2CHCLKProjectMDK-ARM V5Code.uvprojxProjectcsrc5.C/C++...New LinecsrcU8g2库需要依赖您实现的底层函数。我们需要创建一个新文件来实现它。在Keil工程中,右键->,选择,命名为。用同样的
基于STM32F103与U8g2库的OLED图形显示
一、 器材与软件环境
- 硬件:
- STM32F103C8T6核心板 x1
- 0.96寸OLED显示屏 (SSD1306驱动, I2C接口) x1
- ST-LINK/V2调试下载器 x1
- 杜邦线 若干
- 软件:
- STM32CubeMX
- Keil MDK-ARM (Keil 5)
二、 原理简述
- I2C协议: 一种由Philips公司开发的两线式串行总线,用于连接微控制器及其外围设备。它由SDA(串行数据线) 和SCL(串行时钟线) 构成,通过地址寻址方式实现主从设备间的通信。本实验中,STM32作为主设备,OLED屏作为从设备。
- OLED显示原理: 0.96寸OLED屏像素为128x64,每个像素点自发光,无需背光。它通过I2C接收STM32发送的命令和数据来控制每个像素点的亮灭。显示汉字或图片的本质是向OLED的GDDRAM(图形显示数据RAM)写入对应的点阵数据。
- U8g2库: 一个用于嵌入式设备的单色图形库,支持多种显示器控制器和单片机平台。它封装了底层驱动,提供了大量高层API(如绘制点、线、圆、字符、汉字、位图等),极大简化了图形界面开发。
三、 步骤
第一部分:硬件连接与STM32CubeMX工程创建
步骤 1:硬件连接
将OLED屏与STM32F103C8T6按照下表用杜邦线连接:
| OLED屏引脚 | STM32F103C8T6引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
| SCL | PB6 | I2C1时钟线 |
| SDA | PB7 | I2C1数据线 |
步骤 2:使用STM32CubeMX创建工程并配置时钟和I2C
1.打开STM32CubeMX,点击 File -> New Project。
2.在芯片选择器中,在搜索框输入 STM32F103C8,然后在下方列表中选择 STM32F103C8Tx,点击 Start Project。
3.配置时钟源:
- 在
Pinout & Configuration标签页下,左侧目录找到RCC。 - 将
High Speed Clock (HSE)设置为Crystal/Ceramic Resonator。

4.配置I2C1:
- 在左侧目录找到
I2C1。 - 将
I2C1 Mode设置为I2C。 - 此时,右侧的原理图上,PB6和PB7会自动被配置为I2C1的SCL和SDA。

5.配置时钟树:
- 点击上方
Clock Configuration标签。 - 为了获得较好的性能,我们将系统时钟配置到最高72MHz。找到HSE,输入8(因为外部晶振通常是8MHz),然后在
System Clock Mux处选择PLLCLK,最后在右侧的HCLK输入72,并按下回车。软件会自动完成PLL的倍频配置。

6.生成工程代码:
- 点击
Project Manager标签。 - 在
Project子标签下,设置Project Name。 - 将
Toolchain / IDE设置为MDK-ARM V5。 - 在
Code Generator子标签下,推荐选择Copy all used libraries into the project folder以便于代码管理。 - 点击右上角的
GENERATE CODE按钮,生成Keil工程文件。

第二部分:获取并移植U8g2库
步骤 3:下载U8g2库源码
- 访问U8g2的官方GitHub仓库:https://github.com/olikraus/u8g2
- 点击
Code->Download ZIP,将整个库下载到本地并解压。

步骤 4:将U8g2文件添加到Keil工程
- 打开上一步由CubeMX生成的Keil工程文件 (
.uvprojx)。 - 在Keil的
Project窗口中,右键点击Source Group 1(或您的组名),选择Add Existing Files to Group 'Source Group 1'...。 - 在弹出的文件浏览器中,导航到您解压的U8g2库的
csrc文件夹。这个文件夹包含了所有C源码文件。 - 我们需要添加以下关键文件:
u8x8_d_ssd1306_128x64_noname.c(可以删除除了这个驱动文件以外的其他u8x8_d_…文件)u8g2_d_setup.c(u8g2_d_ssd1306_128x64_noname_f函数所在的文件且文件中可以只留下这个函数)u8g2_d_memory.c(u8g2_m_16_8_f函数所在的文件且文件中可以只留下这个函数)u8g2_d_lcd.cu8g2_ll_hvline.cu8g2_buffer.cu8g2_font.cu8g2_hvline.cu8g2_selection_list.cu8g2_circle.cu8g2_line.cu8g2_polygon.cu8g2_bezier.cu8g2_box.cu8g2_cleardisplay.c(这个文件可能不存在,如果没有,请添加u8g2_clear_display.c)u8g2_setup.cu8g2_fonts.c

5.添加头文件路径:
- 在Keil中,点击魔术棒按钮
Options for Target。 - 选择
C/C++标签。 - 在
Include Paths一栏,点击末尾的...按钮。 - 点击
New Line按钮 (文件夹带*号图标),然后添加U8g2库的csrc文件夹路径。

步骤 5:编写硬件层驱动函数 (HAL库适配)
U8g2库需要依赖您实现的底层gpio_and_delay函数。我们需要创建一个新文件来实现它。
-
在Keil工程中,右键
Source Group 1->Add New Item to Group 'Source Group 1',选择C File (.c),命名为u8g2_port.c。用同样的方法添加对应的头文件u8g2_port.h。 -
在
u8g2_port.h中写入以下内容:#ifndef U8G2_PORT_H #define U8G2_PORT_H #include "stm32f1xx_hal.h" // 确保包含HAL库头文件 #include "u8g2.h" // 包含U8g2库头文件 // 声明一个外部变量,用于I2C句柄,我们将在main.c中定义它 extern I2C_HandleTypeDef hi2c1; // 声明U8g2的初始化函数 uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr); uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr); #endif -
在
u8g2_port.c中写入以下内容:#include "u8g2_port.h" // GPIO和延时函数 uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr) { switch (msg) { case U8X8_MSG_GPIO_AND_DELAY_INIT: // 初始化,被u8g2.begin()调用 // 这里可以放置一些初始化的代码,但HAL库已经通过CubeMX初始化好了,通常为空。 break; case U8X8_MSG_DELAY_MILLI: // 延时毫秒命令 HAL_Delay(arg_int); // 调用HAL库的延时函数 break; case U8X8_MSG_DELAY_10MICRO: // 延时10微秒命令 (对于I2C通常不需要) for (uint16_t n = 0; n < 80; n++) // 粗略的10us延时循环,根据CPU频率调整 { __NOP(); } break; case U8X8_MSG_DELAY_100NANO: // 延时100纳秒命令 (对于I2C通常不需要) __NOP(); // 执行一个空操作,大约几十纳秒 break; default: return 0; // 消息未知,返回0表示失败 } return 1; // 消息处理成功 } // I2C字节传输函数 uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { static uint8_t buffer[32]; // U8g2将会分块传输数据,32字节是常见的块大小 static uint8_t buf_idx; uint8_t *data; switch (msg) { case U8X8_MSG_BYTE_SEND: // 发送数据,arg_ptr指向要发送的数据,arg_int是数据长度 data = (uint8_t *)arg_ptr; while (arg_int > 0) { buffer[buf_idx++] = *data; data++; arg_int--; } break; case U8X8_MSG_BYTE_INIT: // 初始化,这里不需要做什么 break; case U8X8_MSG_BYTE_SET_DC: // 设置数据/命令引脚,对于硬件I2C,这个信息通常包含在地址中,所以这里什么都不做。 break; case U8X8_MSG_BYTE_START_TRANSFER: // 开始传输 buf_idx = 0; break; case U8X8_MSG_BYTE_END_TRANSFER: // 结束传输,将缓冲区的数据通过HAL_I2C_Master_Transmit发送出去 // 0x78 是SSD1306的I2C地址(7位地址左移一位后为0x3C << 1 = 0x78) if (HAL_I2C_Master_Transmit(&hi2c1, 0x78, buffer, buf_idx, HAL_MAX_DELAY) != HAL_OK) { return 0; // 传输失败 } break; default: return 0; } return 1; } -
将
u8g2_port.c添加到Keil工程的Source Group 1中。
第三部分:编写主程序,实现显示功能
步骤 6:修改主程序 main.c
-
在
main.c的/* USER CODE BEGIN Includes */区域,包含头文件:/* USER CODE BEGIN Includes */ #include "u8g2.h" #include "u8g2_port.h" /* USER CODE END Includes */ -
在
/* USER CODE BEGIN PV */区域,定义一个U8g2对象:/* USER CODE BEGIN PV */ u8g2_t u8g2; // 定义一个U8g2结构体 /* USER CODE END PV */ -
在
/* USER CODE BEGIN 2 */区域,初始化U8g2库:/* USER CODE BEGIN 2 */ // 初始化U8g2库,配置为SSD1306驱动,128x64分辨率,硬件I2C u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_stm32_gpio_and_delay); u8g2_InitDisplay(&u8g2); // 发送初始化序列到显示器 u8g2_SetPowerSave(&u8g2, 0); // 唤醒显示器 u8g2_ClearBuffer(&u8g2); // 清除内部缓冲区 // 显示欢迎信息或测试图形 u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr); // 设置字体 u8g2_DrawStr(&u8g2, 0, 20, "U8g2 Init OK!"); // 在坐标(0,20)绘制字符串 u8g2_SendBuffer(&u8g2); // 将缓冲区内容发送到显示器 HAL_Delay(2000); // 延时2秒 /* USER CODE END 2 */ -
在
/* USER CODE BEGIN 3 */,实现显示。1:显示qq和id
/* USER CODE BEGIN 3 */ u8g2_ClearBuffer(&u8g2); // 清除缓冲区 u8g2_SetFont(&u8g2, u8g2_font_unifont_t_chinese2); u8g2_DrawUTF8(&u8g2, 0, 20, "QQ:361***717"); u8g2_DrawUTF8(&u8g2, 0, 40, "ID:CYUE18"); u8g2_SendBuffer(&u8g2); HAL_Delay(3000); // 显示3秒2:上下滑动显示 (使用第一页缓冲技术)
//2:上下滑动显示 u8g2_SetFont(&u8g2, u8g2_font_6x10_tf); for (int pos = 64; pos >= -64; pos--) { // 从下往上滑动 u8g2_ClearBuffer(&u8g2); u8g2_DrawStr(&u8g2, 0, pos, "QQ:3614789717"); u8g2_DrawStr(&u8g2, 0, pos+20, "Line 2 of the text."); u8g2_SendBuffer(&u8g2); HAL_Delay(50); // 控制滑动速度 } HAL_Delay(1000);任务 3:显示一个简单图案并添加动态效果 (这里是移动的小球)
// 3:显示动态图案 int ball_x = 20; int ball_y = 20; int ball_vx = 2; int ball_vy = 2; int ball_radius = 5; for (int i = 0; i < 150; i++) { u8g2_ClearBuffer(&u8g2); // 绘制边框 u8g2_DrawFrame(&u8g2, 0, 0, 128, 64); // 绘制背景网格线(可选) for (int x = 20; x < 128; x += 20) { u8g2_DrawVLine(&u8g2, x, 0, 64); } for (int y = 16; y < 64; y += 16) { u8g2_DrawHLine(&u8g2, 0, y, 128); } // 绘制移动的小球 u8g2_DrawDisc(&u8g2, ball_x, ball_y, ball_radius, U8G2_DRAW_ALL); // 在小球中心画个点,方便观察 u8g2_DrawPixel(&u8g2, ball_x, ball_y); // 显示简单的帧数信息(不使用snprintf) u8g2_SetFont(&u8g2, u8g2_font_5x7_tf); // 方法1:直接绘制数字(简单可靠) u8g2_DrawStr(&u8g2, 100, 10, "F:"); u8g2_SetCursor(&u8g2, 110, 10); u8g2_PrintU8(&u8g2, i % 100); // U8g2自带的数字打印函数 // 方法2:如果需要显示位置,使用简单条件显示 if (ball_vx > 0) { u8g2_DrawStr(&u8g2, 90, 20, "Right"); } else { u8g2_DrawStr(&u8g2, 90, 20, "Left"); } if (ball_vy > 0) { u8g2_DrawStr(&u8g2, 90, 30, "Down"); } else { u8g2_DrawStr(&u8g2, 90, 30, "Up"); } // 更新小球位置 ball_x += ball_vx; ball_y += ball_vy; // 改进的边界碰撞检测(考虑小球半径) if (ball_x <= ball_radius) { ball_vx = -ball_vx; ball_x = ball_radius; // 防止卡在左边界 } if (ball_x >= 128 - ball_radius) { ball_vx = -ball_vx; ball_x = 128 - ball_radius; // 防止卡在右边界 } if (ball_y <= ball_radius) { ball_vy = -ball_vy; ball_y = ball_radius; // 防止卡在上边界 } if (ball_y >= 64 - ball_radius) { ball_vy = -ball_vy; ball_y = 64 - ball_radius; // 防止卡在下边界 } u8g2_SendBuffer(&u8g2); HAL_Delay(35); // 控制动画帧率 }
步骤 7:编译与下载
-
在Keil中,点击
Rebuild(F7) 按钮编译整个工程。确保0 Errors, 0 Warnings。 -
将ST-LINK通过USB连接电脑,并与STM32板子正确连接。
-
点击
Load(F8) 按钮将程序下载到STM32中。 -
下载完成后,系统会自动复位运行。观察OLED屏幕的显示效果。



1. I2C波形分析
如果使用Keil的逻辑分析仪采集I2C的SDA波形,应该能看到标准的I2C通信时序:
- 起始条件:SCL高电平时SDA由高变低
- 设备地址:7位从设备地址(0x3C) + 读写位(0)
- 数据帧:多个8位数据字节
- 停止条件:SCL高电平时SDA由低变高
2. 替代分析方法
如果无法采集真实I2C信号,可以采用以下方法:
- 软件模拟I2C:使用GPIO模拟I2C时序,便于调试和观测
- 串口打印调试:在关键代码位置添加串口输出,分析程序执行流程
- 逻辑分析仪:使用外部逻辑分析仪捕获和分析I2C信号
- 示波器观测:使用数字示波器观察SDA和SCL的波形
更多推荐


所有评论(0)