资讯动态

STM32F103 + 0.96 寸 I2C OLED 移植 U8g2:从 CubeMX 建工程到裁剪、显示与滑动动画

发布时间:2026/10/27 2:04:48 来源:尧图企业网站定制
STM32F103 0.96 寸 I2C OLED 移植 U8g2从 CubeMX 建工程到裁剪、显示与滑动动画本文记录一次在 STM32F103C8T6 上移植 U8g2 的完整过程涵盖 CubeMX 搭建框架、源码裁剪、HAL 回调对接以及滑动与位图动画的实现。硬件STM32F103C8T6 最小系统板 0.96 寸 SSD1306 OLED128×64I2C工具STM32CubeMX Keil MDK 5ARMCC V5.06u7 U8g2 源码一、CubeMX 搭 HAL 框架1.1 基本配置项目配置MCUSTM32F103C8TxRCCHSE 8 MHz 晶振PLL ×9 → 72 MHzDebugSerial WireSWDSYS TimebaseSysTick1.2 I2C1 配置OLED 用硬件 I2C1参数值I2C1I2CSCLPB6SDAPB7Speed ModeFast ModeClock Speed400 kHzAddressing Mode7-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);bufu8g2_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.c128×64 全缓冲需要 128 × 64 / 8 1024 字节显存只留一个分配函数uint8_t*u8g2_m_16_8_f(uint8_t*page_cnt){staticuint8_tbuf[1024];*page_cnt8;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_trdemo 标题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_idx0;break;caseU8X8_MSG_BYTE_SEND:while(arg_int--0){if(i2c_idxsizeof(i2c_buf))i2c_buf[i2c_idx]*p;p;}break;caseU8X8_MSG_BYTE_END_TRANSFER:addru8x8_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_ti0;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 位地址0x787 位 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_tickHAL_GetTick();last_draw_tick0;page0;while(1){nowHAL_GetTick();/* 约 30 ms 刷新一帧避免主循环无延时地反复刷整屏 */if((now-last_draw_tick)30U){last_draw_ticknow;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:page0;break;}}/* 每页显示 5 秒 */if((now-last_page_tick)5000U){last_page_ticknow;page;if(page4U)page0;}}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_ID20230001#defineSTUDENT_NAMEAS123voidU8G2_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_tx0,y0;staticint8_tdx1,dy1;staticuint32_tlast_move0;constchar*topLEFT/RIGHT;constchar*bottomUP/DOWN;int16_tw(int16_t)u8g2_GetStrWidth(u8g2,top);int16_tx_limit128-w;if(x_limit0)x_limit0;if((tick-last_move)50U)/* 每 50 ms 移动一步 */{last_movetick;/* 上面一行左右往返 */xdx*4;if(xx_limit){xx_limit;dx-1;}if(x0){x0;dx1;}/* 下面一行上下往返 */ydy*1;if(y18){y18;dy-1;}if(y0){y0;dy1;}}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)(40y),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_tframe0;/* 每 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/u8g2U8g2 官方文档含字体列表与 XBM 说明https://github.com/olikraus/u8g2/wikiSSD1306 数据手册STM32F103xx 参考手册RM0008

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑