STM32F103 + 0.96 寸 I2C OLED 移植 U8g2:从 CubeMX 建工程到裁剪、显示与滑动动画
本文记录一次在 STM32F103C8T6 上移植 U8g2 的完整过程,涵盖 CubeMX 搭建框架、源码裁剪、HAL 回调对接,以及滑动与位图动画的实现。
硬件:STM32F103C8T6 最小系统板 + 0.96 寸 SSD1306 OLED(128×64,I2C)
工具:STM32CubeMX + Keil MDK 5(ARMCC V5.06u7)+ U8g2 源码
一、CubeMX 搭 HAL 框架
1.1 基本配置
| 项目 | 配置 |
|---|---|
| MCU | STM32F103C8Tx |
| RCC | HSE 8 MHz 晶振,PLL ×9 → 72 MHz |
| Debug | Serial Wire(SWD) |
| SYS Timebase | SysTick |
1.2 I2C1 配置
OLED 用硬件 I2C1:
| 参数 | 值 |
|---|---|
| I2C1 | I2C |
| SCL | PB6 |
| SDA | PB7 |
| Speed Mode | Fast Mode |
| Clock Speed | 400 kHz |
| Addressing Mode | 7-bit |
二、U8g2 源码裁剪
2.1 需要哪些文件
U8g2 的源码位于官方仓库的csrc目录,但并非全部都需要,可按功能分为三类:
必须保留(绘图核心)
u8g2.h u8x8.h u8g2_setup.c u8g2_buffer.c u8g2_font.c u8g2_box.c u8g2_line.c u8g2_hvline.c u8g2_ll_hvline.c u8g2_bitmap.c u8g2_circle.c u8g2_polygon.c u8g2_arc.c u8x8_setup.c u8x8_display.c u8x8_byte.c u8x8_cad.c u8x8_gpio.c u8x8_8x8.c u8x8_string.c u8x8_u8toa.c u8x8_u16toa.c u8x8_d_ssd1306_128x64_noname.c被内部引用、链接时会报未定义符号的(容易漏)
u8g2_intersection.c /* u8g2_setup.c 引用 u8g2_IsIntersection */ u8g2_kerning.c /* u8g2_font.c 引用 u8g2_GetKerning */ u8x8_capture.c /* u8g2_buffer.c 引用 u8x8_capture_* */ u8x8_debounce.c /* u8x8_message.c 引用 u8x8_GetMenuEvent */u8g2_d_setup.c /* 所有驱动的初始化函数,几百个 */ u8g2_d_memory.c /* 所有分辨率的显存分配函数 */ u8g2_fonts.c /* 37.9 MB 字体数据 */2.2 裁u8g2_d_setup.c
只保留 SSD1306 128×64 I2C 全缓冲模式这一个函数:
uint8_tu8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t*u8g2,constu8g2_cb_t*rotation,u8x8_msg_cb byte_cb,u8x8_msg_cb gpio_and_delay_cb){uint8_ttile_buf_height;uint8_t*buf;u8g2_SetupDisplay(u8g2,u8x8_d_ssd1306_128x64_noname,u8x8_cad_ssd13xx_fast_i2c,byte_cb,gpio_and_delay_cb);buf=u8g2_m_16_8_f(&tile_buf_height);u8g2_SetupBuffer(u8g2,buf,tile_buf_height,u8g2_ll_hvline_vertical_top_lsb,rotation);return1;}2.3 裁u8g2_d_memory.c
128×64 全缓冲需要 128 × 64 / 8 =1024 字节显存,只留一个分配函数:
uint8_t*u8g2_m_16_8_f(uint8_t*page_cnt){staticuint8_tbuf[1024];*page_cnt=8;returnbuf;}2.4 裁u8g2_fonts.c
这是体积大头。做法是只从 37.9 MB 的原文件里切出实际用到的字体定义。
本次用到 5 个字体:
| 字体 | 用途 |
|---|---|
u8g2_font_6x10_tf | 小号注释文字 |
u8g2_font_6x12_tf | 标签文字 |
u8g2_font_7x14B_tf | 加粗正文 |
u8g2_font_ncenB14_tr | demo 标题 |
u8g2_font_logisoso24_tn | 大号学号数字 |
实操方法:不要在 IDE 里打开这个文件(会卡死)。用命令行按字体名定位行号再切片,例如先
findstr /N "u8g2_font_6x10_tf"找到起始行,再截取到下一条字体定义之前。
2.5 裁剪结果
Total RO Size (Code + RO Data) 20084 ( 19.61 kB) Total RW Size (RW Data + ZI Data) 3088 ( 3.02 kB) Total ROM Size 20120 ( 19.65 kB)三、把 U8g2 接到 HAL 上:两个回调
U8g2 不直接操作寄存器,它通过两个回调函数与底层通信:
u8x8_msg_cb byte_cb;/* 字节怎么发出去 */u8x8_msg_cb gpio_and_delay_cb;/* 延时和 GPIO 怎么控制 */这是整个移植的唯一接口层,写对这两个函数,U8g2 就跑起来了。
3.1 字节传输回调
U8g2 发数据的过程是:START_TRANSFER→ 若干次SEND→END_TRANSFER。
思路是把这一包数据先攒进缓冲区,最后一次HAL_I2C_Master_Transmit发出去,减少 I2C 启动次数。
/* SSD1306 整页刷新为 1 字节控制字 + 128 字节数据,故留 160 字节余量 */staticuint8_ti2c_buf[160];staticuint8_ti2c_idx;uint8_tu8x8_byte_stm32_hw_i2c(u8x8_t*u8x8,uint8_tmsg,uint8_targ_int,void*arg_ptr){uint8_t*p=(uint8_t*)arg_ptr;uint16_taddr;switch(msg){caseU8X8_MSG_BYTE_INIT:/* 硬件 I2C 已由 CubeMX 的 MX_I2C1_Init() 初始化 */break;caseU8X8_MSG_BYTE_START_TRANSFER:i2c_idx=0;break;caseU8X8_MSG_BYTE_SEND:while(arg_int-->0){if(i2c_idx<sizeof(i2c_buf))i2c_buf[i2c_idx++]=*p;p++;}break;caseU8X8_MSG_BYTE_END_TRANSFER:addr=u8x8_GetI2CAddress(u8x8);if(HAL_I2C_Master_Transmit(&hi2c1,addr,i2c_buf,i2c_idx,100)!=HAL_OK)return0;break;caseU8X8_MSG_BYTE_SET_DC:/* I2C 接口没有 DC 引脚 */break;default:return0;}return1;}3.2 延时与 GPIO 回调
uint8_tu8x8_gpio_and_delay_stm32(u8x8_t*u8x8,uint8_tmsg,uint8_targ_int,void*arg_ptr){(void)u8x8;(void)arg_ptr;switch(msg){caseU8X8_MSG_GPIO_AND_DELAY_INIT:break;caseU8X8_MSG_DELAY_MILLI:HAL_Delay(arg_int);break;caseU8X8_MSG_DELAY_10MICRO:/* 72 MHz 下的粗略微秒延时,U8g2 仅用于初始化间隔 */for(volatileuint32_ti=0;i<(uint32_t)arg_int*180U;i++)__NOP();break;caseU8X8_MSG_DELAY_100NANO:caseU8X8_MSG_DELAY_NANO:__NOP();break;caseU8X8_MSG_GPIO_RESET:/* 模块无 RES 引脚时忽略;有 RES 时在此控制 */break;caseU8X8_MSG_GPIO_I2C_CLOCK:caseU8X8_MSG_GPIO_I2C_DATA:break;default:return0;}return1;}4.3 初始化
u8g2_tu8g2;voidU8G2_Init(void){u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0,u8x8_byte_stm32_hw_i2c,u8x8_gpio_and_delay_stm32);/* 0.96 寸 SSD1306 常见 8 位地址:0x78(7 位 0x3C) */u8x8_SetI2CAddress(&u8g2.u8x8,0x78);u8g2_InitDisplay(&u8g2);u8g2_SetPowerSave(&u8g2,0);u8g2_ClearBuffer(&u8g2);u8g2_SendBuffer(&u8g2);}_f后缀表示full frame buffer(全缓冲)模式:1024 字节显存全在 RAM 里,所有绘图操作只改内存,调u8g2_SendBuffer()才一次性刷到屏幕。
四、显示功能的实现
4.1 页面调度
主循环用两个时间戳,一个控刷新率,一个控翻页:
uint32_tlast_page_tick;uint32_tlast_draw_tick;uint8_tpage;last_page_tick=HAL_GetTick();last_draw_tick=0;page=0;while(1){now=HAL_GetTick();/* 约 30 ms 刷新一帧,避免主循环无延时地反复刷整屏 */if((now-last_draw_tick)>=30U){last_draw_tick=now;switch(page){case0:U8G2_ShowI2CTest();break;case1:U8G2_ShowDemo();break;case2:U8G2_ShowIDName();break;case3:U8G2_ShowScroll(now);break;case4:U8G2_ShowAvatar(now);break;default:page=0;break;}}/* 每页显示 5 秒 */if((now-last_page_tick)>=5000U){last_page_tick=now;page++;if(page>4U)page=0;}}5.2 U8g2 demo 页
voidU8G2_ShowDemo(void){u8g2_ClearBuffer(&u8g2);u8g2_SetFont(&u8g2,u8g2_font_ncenB14_tr);u8g2_DrawStr(&u8g2,0,16,"U8g2 Demo");u8g2_DrawFrame(&u8g2,0,22,128,40);u8g2_DrawCircle(&u8g2,24,42,12,U8G2_DRAW_ALL);u8g2_DrawDisc(&u8g2,24,42,6,U8G2_DRAW_ALL);u8g2_DrawLine(&u8g2,45,30,120,55);u8g2_DrawLine(&u8g2,120,30,45,55);u8g2_SetFont(&u8g2,u8g2_font_6x10_tf);u8g2_DrawStr(&u8g2,45,44,"STM32F103");u8g2_DrawStr(&u8g2,45,56,"I2C OLED");u8g2_SendBuffer(&u8g2);}这一页把基础绘图 API 都用了一遍:DrawStr文字、DrawFrame空心矩形、DrawCircle空心圆、DrawDisc实心圆、DrawLine直线。
5.3 学号与昵称
#defineSTUDENT_ID"20230001"#defineSTUDENT_NAME"AS123"voidU8G2_ShowIDName(void){u8g2_ClearBuffer(&u8g2);u8g2_SetFont(&u8g2,u8g2_font_6x12_tf);u8g2_DrawStr(&u8g2,0,12,"Student ID:");u8g2_SetFont(&u8g2,u8g2_font_logisoso24_tn);u8g2_DrawStr(&u8g2,0,40,STUDENT_ID);u8g2_SetFont(&u8g2,u8g2_font_7x14B_tf);u8g2_DrawStr(&u8g2,0,60,"Name: "STUDENT_NAME);u8g2_SendBuffer(&u8g2);}用宏定义个人信息,改数据不用动逻辑。学号用 24 像素高的logisoso24,视觉重心明确。
关于中文名:本次全部用 ASCII 字体。想在 64 KB Flash 里显示中文,只能做子集字体——用 U8g2 官方的字体构造工具,只生成姓名那几个汉字,体积可控在几百字节。
5.4 滑动显示
voidU8G2_ShowScroll(uint32_ttick){staticint16_tx=0,y=0;staticint8_tdx=1,dy=1;staticuint32_tlast_move=0;constchar*top="LEFT/RIGHT";constchar*bottom="UP/DOWN";int16_tw=(int16_t)u8g2_GetStrWidth(&u8g2,top);int16_tx_limit=128-w;if(x_limit<0)x_limit=0;if((tick-last_move)>=50U)/* 每 50 ms 移动一步 */{last_move=tick;/* 上面一行:左右往返 */x+=dx*4;if(x>=x_limit){x=x_limit;dx=-1;}if(x<=0){x=0;dx=1;}/* 下面一行:上下往返 */y+=dy*1;if(y>=18){y=18;dy=-1;}if(y<=0){y=0;dy=1;}}u8g2_ClearBuffer(&u8g2);u8g2_SetFont(&u8g2,u8g2_font_7x14B_tf);u8g2_DrawStr(&u8g2,(u8g2_uint_t)x,20,top);u8g2_DrawStr(&u8g2,0,(u8g2_uint_t)(40+y),bottom);u8g2_DrawFrame(&u8g2,0,0,128,64);u8g2_SendBuffer(&u8g2);}设计上的两个考虑:
- 左右和上下分别用两行独立演示,不要合成一条斜线移动,否则两种方向混在一起,看不出各自的效果
- 边界用回弹而不是取模:
x到x_limit就反向,x到 0 再反向,这样才是"往返",不是"瞬移回起点"
x_limit = 128 - w用实际字宽算,换字体、换文案都不用改代码。
5.5 图片 + 动态效果
头像用 XBM 位图,两帧切换实现眨眼:
/* XBM 格式:每个字节低位在左,按行从上到下,每行 4 字节 */staticconstuint8_tavatar_frame0[]={/* 睁眼 32x32 */};staticconstuint8_tavatar_frame1[]={/* 闭眼 32x32 */};voidU8G2_ShowAvatar(uint32_ttick){staticuint8_tframe=0;/* 每 700 ms 里闭眼 180 ms */frame=((tick%700)<180)?1:0;u8g2_ClearBuffer(&u8g2);u8g2_DrawXBMP(&u8g2,48,4,32,32,frame?avatar_frame1:avatar_frame0);u8g2_SetFont(&u8g2,u8g2_font_6x10_tf);u8g2_DrawStr(&u8g2,30,48,"MY AVATAR");u8g2_DrawFrame(&u8g2,8,54,112,6);u8g2_DrawBox(&u8g2,9,55,(u8g2_uint_t)((tick/20)%110),4);u8g2_SendBuffer(&u8g2);}动态效果有两处:
- 眨眼:两帧位图按 700 ms 周期切换,180 ms 用闭眼帧,看起来就是自然眨眼
- 进度条:底部用
(tick / 20) % 110算出宽度,从 0 涨到 110 再回绕,形成循环扫描条
XBM 格式的坑:u8g2_DrawXBMP要求的是每行按字节对齐、低位在左。很多人用在线工具转出来的图左右镜像或者上下颠倒,就是字节位序或扫描方向搞错了。32×32 的位图,每行正好 4 字节,总共 128 字节一帧,很好核对。
六、最终效果
功能对照
| 要求 | 对应页面 | 实现方式 |
|---|---|---|
| U8g2 demo 例程 | 第 1 页 | 文字、矩形、圆、实心圆、直线 |
| 学号 + 昵称 | 第 2 页 | 三种字体混排 |
| 上下、左右滑动 | 第 3 页 | 两行独立往返,边界回弹 |
| 图案 + 动态 | 第 4 页 | 32×32 双帧眨眼 + 进度条 |
上电后每 5 秒自动切页,25 秒一轮循环。
六、视频演示
video_20261025_223921
参考资料
- U8g2 官方仓库:
https://github.com/olikraus/u8g2 - U8g2 官方文档(含字体列表与 XBM 说明):
https://github.com/olikraus/u8g2/wiki - SSD1306 数据手册
- STM32F103xx 参考手册(RM0008)