avatar🌌
零醇零醇の小屋

樱花树下,再续前缘

鹤望兰无凋零之时,比翼鸟永世长存

STM32 HAL库入门教程(八):软件模拟 I2C 驱动 0.96寸 OLED 屏幕 —— 避开硬件死锁大坑!

前面七篇教程里 我们手头的小蓝板经历了: 点灯、读按键、外部中断、PWM呼吸、串口发消息、定时器闹钟以及 ADC 测电压 但是不知道大家有没有发现一个大问题: 不管我们测出了什么好玩的数据 每次都必须拿一根 USB 线连着电脑 靠电脑上的串口助手才能看得到( 现实里的智能手环、电子秤、温度计 怎么可能天天屁股后面拖着一台电脑呢?

今天 我们就来给单片机装上一张真正属于它自己的“小脸蛋”——0.96 寸 I2C OLED 显示屏!(。・ω・。)ノ♡

先来聊聊今天的主角:淘宝上几块钱就能买到的 4 针 0.96 寸 OLED 屏幕 看图 图-1

它身上只有可怜兮兮的 4 根引脚:

  • “VCC”:接 3.3V(或者 5V)
  • “GND”:接地
  • “SCL”:I2C 时钟线
  • “SDA”:I2C 数据线

肯定有细心的小伙伴要问啦:“既然是 I2C 屏幕 为什么我们不用 STM32 自带的硬件 I2C 呢?” 这里要先给新手朋友们科普一个在嵌入式圈子里人尽皆知的大坑:( >﹏<。) “STM32F103 的硬件 I2C 内部设计有历史遗留的硬件缺陷!极其容易在等待标志位的时候莫名其妙死锁卡死!” 所以不管是做大创、打电赛还是做商业项目 共识基本都是:在 F103 上能用【软件模拟 I2C】 就绝对不用硬件 I2C! 而且模拟 I2C 还有一个巨大的爽点:板子上随便挑两个你看着顺眼的 GPIO 引脚都能驱动 自由度直接拉满喵~

再来聊聊今天的驱动代码来源—— 这里必须要大声致敬一下带无数人入门单片机的白月光老师【江协科技】! 本篇教程使用的驱动底层就是源自江协老师超经典的 0.96 寸 OLED 驱动库(向江科大致敬喵~ (✧ω✧))! 不过江协老师当年讲的是老款标准库(SPL) 很多刚用 CubeMX + HAL 库的新手移植的时候到处报错 所以今天我就把它完整适配成了【HAL 库版】 保证大家无脑放进工程就能直接跑!

等等!在动手接线之前 我们先来搞懂两个面试、考试和吹水最常被问到的核心硬核原理: 为什么 I2C 规定必须用开漏输出?以及它的通信时序到底是怎么走的?


零、 两个核心原理大揭秘 ​

1. 为什么 I2C 必须使用“开漏输出(Open-Drain)”? ​

还记得第二篇讲 GPIO 时我们科普过的“推挽输出 vs 开漏输出”吗? 推挽输出有强驱动能力 能主动输出 3.3V(高电平)和 0V(低电平); 而开漏输出只能输出 0V(接地) 想输出 3.3V 必须在外面挂一个“上拉电阻”拉上去!

那为什么 I2C 这么讲究 偏偏要用开漏输出呢?核心原因有两个:

  • 第一点:防止短路烧芯片(线与逻辑) I2C 是一根总线挂多个设备的(一主多从) 也就是说同一根 SDA 数据线上可能连着单片机、OLED 屏幕、温湿度传感器好几个设备! 如果是“推挽输出”:万一单片机想发 1(引脚接 3.3V电源) 此时屏幕正好在发 ACK 应答 0(引脚接地 GND) 两个引脚连在同一根线上 3.3V 电源和 GND 直接内部短路!瞬间产生大电流 芯片当场冒烟暴毙! 而“开漏输出 + 外部上拉”就聪明极了:所有设备都只能“拉低到地(0)” 或者“放手不干(由上拉电阻拉到 3.3V)” 任何一个人拉低 总线就是 0;只有所有人都放手 总线才是 1!这就叫“线与” 就算两个人同时抢着说话 也绝对不会短路烧芯片喵~

  • 第二点:实现真正的双向通信(随时可以接收数据) 因为是开漏输出 单片机只要输出 1(松开引脚) 这个引脚就会进入高阻浮空态 此时不用在代码里来回切换输入/输出模式 单片机直接去读取引脚电平 就能读到屏幕返回给我们的数据啦! (PS:你买的 4 针 OLED 模块背面其实已经焊接好了两颗 4.7k 的贴片上拉电阻 所以即便你配置成开漏模式 也能正常工作哦!)

2. 看懂 I2C 的通信节拍:四大时序 ​

看图 图0

I2C 只有两根线 它传数据靠的是极其严格的时序逻辑:

  • 起始信号(Start): SCL(时钟线)保持高电平时 SDA(数据线)突然从高电平跳变到低电平 代码对应:
c
void OLED_I2C_Start(void)
{
    OLED_W_SDA(1);
    OLED_W_SCL(1);
    OLED_W_SDA(0); // SCL高电平期间,SDA由高变低
    OLED_W_SCL(0);
}
  • 数据传输与有效性(Data Transfer): SCL 是节拍器 规定非常死: “SCL 为低电平期间 允许 SDA 改变电平(写下一位数据);” “SCL 为高电平期间 SDA 必须保持稳定绝对不能乱动!” 一个字节有 8 位(高位先行 MSB 比如从 0x80 开始判断) SCL 跳动 8 次 就传完了 8 个位!

  • 应答信号(ACK / NACK): 发完 8 个位后 主机在第 9 个时钟周期松开 SDA(交出话筒) 从机如果收到了 就会在第 9 个脉冲期间把 SDA 往下拉低(0) 表示:“收到啦 主人请继续讲喵~”(ACK); 如果没人理你 SDA 依然被电阻拉高(1) 那就是非应答(NACK) (注:在给 0.96寸 OLED 写命令和数据时 由于屏幕是被动接收 我们通常多发一个时钟把 ACK 忽略过去就行啦)

  • 停止信号(Stop): SCL(时钟线)保持高电平时 SDA(数据线)突然从低电平跳变到高电平! 代码对应:

c
void OLED_I2C_Stop(void)
{
    OLED_W_SDA(0);
    OLED_W_SCL(1);
    OLED_W_SDA(1); // SCL高电平期间,SDA由低变高
}

原理彻底打通啦!是不是感觉看底层代码像看漫画一样亲切了?(=´ω`=)


接线准备(这次我们挑两个很顺眼的引脚):

  • OLED 的 VCC -> 接单片机 3.3V
  • OLED 的 GND -> 接单片机 GND
  • OLED 的 SCL -> 接单片机 PB8
  • OLED 的 SDA -> 接单片机 PB9

接下来打开我们熟悉的 cubemx! 老规矩选好 c8t6 配好时钟和调试口 在右侧芯片图上 点击 PB8 和 PB9 统统配置为“GPIO_Output”! 在左侧 System Core -> GPIO 里 选中它俩: GPIO output level 选 High(默认拉高) GPIO mode 选 Output Open Drain(开漏输出!) Maximum output speed 选 High(高速)

图2

配置好工程 点击生成代码 打开 Keil 工程!

接下来就是最关键的“调库侠移植大法”啦! 我们需要三个小文件(我已经放在本篇配套源码的工程里啦):

  • OLED.c(屏幕控制核心驱动)
  • OLED.h(函数声明)
  • OLED_Font.h(字库)

把这三个文件复制到你工程的 Core/Src 和 Core/Inc 目录下 并在 Keil 左侧工程树里把 OLED.c 添加进来 什么?你说你不会添加?看图 5 和图 6: 图5图6

打开 OLED.c 可以看到最顶部有两行引脚宏定义: #define OLED_W_SCL(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, x) #define OLED_W_SDA(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, x) 如果你接的不是 PB8 和 PB9 只要在这里改成你自己接的引脚就好啦 简直比喝奶茶还简单喵~

图3

移植完毕!现在打开 main.c 在最上面引入头文件:

c
/* USER CODE BEGIN Includes */
#include "OLED.h"
/* USER CODE END Includes */

然后在 USER CODE BEGIN 2 里面 就可以爽快地调用江协老师封装好的全套 API 啦:

c
/* USER CODE BEGIN 2 */
OLED_Init(); // 初始化小屏幕(会自动清屏)

// 在第 1 行第 1 列显示字符串
OLED_ShowString(1, 1, "Hello World!");

// 在第 2 行第 1 列显示博客名
OLED_ShowString(2, 1, "reiatsushi.com");

// 在第 3 行第 1 列显示数字
OLED_ShowString(3, 1, "STM32 OK!");
/* USER CODE END 2 */
图4

编译 下载 烧录! 拔掉单片机跟电脑的所有连线 只要随便插一个充电宝或者电池给单片机供电 看一眼那块 0.96 寸的小玻璃片:

哇!黑底屏幕上 清晰锐利地亮起了白色的文字!(✧ω✧) 这一刻 单片机终于不用再被电脑的串口助手绑架 成为了一台真正独立运转的掌上小设备! 有了屏幕 以后我们上一篇测出来的 ADC 电压、第六篇的秒数 都可以直接实时在小屏幕上刷新显示啦!

什么?你说你想手写一个带有i2c的驱动程序?如果是OLED的话算了吧 可以试试写mpu6050 由于我手上没有mpu6050的芯片 以后再说啦!

完结撒花!(=^・ω・^=) 快去给你的小单片机画上第一句helloworld~


💻 本教程完整工程源码已开源:
👉 TomSorachan/stm32-hal-tutorials

STM32 HAL库入门教程(七):ADC 模数转换 —— 测电压与读取真实世界模拟量!