前言
大家好,这里是 Hello_Embed 。前面我们学习的 UART 是 "异步传输"------ 两台设备通过发送引脚(TX)、接收引脚(RX)和共地端互连,能同时发送和接收(全双工)。但嵌入式系统中还有很多其他传输协议,今天要讲的 I2C 就和 UART 有很大不同:它是 "半双工" 的,因为只有一条数据线(SDA),发送时不能接收,接收时不能发送;同时它只用三条线(时钟线 SCL、数据线 SDA、地线 GND),更重要的是 ------I2C 总线支持多设备连接,总线上可以同时接多个传感器、显示屏等,这是它的一大优势。
为了直观理解 I2C 协议的工作方式,我们以 OLED 显示屏为例(OLED 常用 I2C 通信),通过 "在 OLED 上显示字符" 的案例,一步步揭开 I2C 传输的面纱。
一、先看效果:OLED 显示字符串的代码与现象
之前我们用过 OLED 显示字符串,代码很简单,先初始化、清屏,再调用打印函数:
c
int main(void)
{
OLED_Init();// 初始化OLED显示屏
OLED_Clear();// 清屏(清空屏幕显示内容)
// 在x=0、y=3的位置打印字符串
OLED_PrintString(0, 3, "Hello, embed!");
// 在x=0、y=5的位置打印另一个字符串
OLED_PrintString(0, 5, "Hello, world!");
while (1)
{
/* 主循环无需额外操作,显示内容已固定 */
}
}
烧录后屏幕显示正常,两行字符串清晰可见:
二、硬件框图:OLED 与 I2C 的连接逻辑
要理解 "为什么这段代码能让屏幕显示字符",先看 OLED 的硬件结构:
右侧是 OLED 模块,核心由两部分组成:
- 屏幕面板:负责发光显示;
- 主控芯片 SSD1306 :相当于 OLED 的 "大脑",屏幕通过引脚与它连接,我们的程序其实是和 SSD1306 通信,而不是直接控制屏幕。
工作逻辑很简单:
- 我们的程序通过 I2C 协议向 SSD1306 发送数据(比如 "Hello, embed!" 的点阵);
- 数据被存入 SSD1306 内部的 "显存"(一块专门存储显示数据的内存);
- SSD1306 会不断把显存中的数据刷新到屏幕上,于是我们就看到了字符。
三、程序流程:从 "打印字符串" 到 "I2C 传输" 的四层逻辑
上面的代码看似简单,背后其实经历了四层 "分工协作",就像公司完成一个项目:
1. 应用程序:"老板"------ 明确需求
对应 main.c
中的代码,是整个流程的起点,明确 "要显示什么内容、在哪里显示":
c
OLED_PrintString(0, 3, "Hello, embed!");
OLED_PrintString(0, 5, "Hello, world!");
这里的 0
是 x 坐标(横向位置),3
和 5
是 y 坐标(纵向位置),字符串就是要显示的内容 ------ 这一步只提需求,不关心 "如何显示"。
2. 库函数:"高管"------ 拆解任务
应用程序提了需求,接下来由 OLED_PrintString
函数(库函数)把任务拆解:字符串是由多个字符组成的,所以需要逐个字符处理。
跳转到 OLED_PrintString
的定义看看:
c
int OLED_PrintString(uint8_t x, uint8_t y, const char *str)
{
int i = 0;
// 循环取出字符串中的每个字符(直到遇到结束符'\0')
while (str[i])
{
// 调用OLED_PutChar显示单个字符
OLED_PutChar(x, y, str[i]);
x++; // 每个字符占1个x位置,所以x坐标+1
// 如果x超过屏幕最大横向位置(15),就换行(y+2),x重置为0
if(x > 15)
{
x = 0;
y += 2;
}
i++;
}
return i; // 返回打印的字符总数
}
这个函数的作用很明确:把字符串拆成单个字符(比如把 "Hello" 拆成 'H'、'e'、'l'、'l'、'o'),然后调用 OLED_PutChar
函数逐个显示 ------ 相当于 "高管" 把大任务拆成小任务,分给具体的执行者。
3. SSD1306 驱动:"开发"------ 执行具体操作
OLED_PutChar
函数是真正处理 "如何显示单个字符" 的,它需要:
- 确定字符在屏幕上的位置(通过 x、y 计算);
- 取出字符对应的点阵数据(字符由点阵组成,比如 'A' 是 8x16 的点阵);
- 把点阵数据发送给 SSD1306 芯片。
看OLED_PutChar
的代码:
c
void OLED_PutChar(uint8_t x, uint8_t y, char c)
{
uint8_t page = y; // y坐标对应SSD1306的"页"(纵向位置单位)
uint8_t col = x*8; // x坐标对应"列"(横向位置单位,每个字符占8列)
// 检查坐标是否超出屏幕范围(超出则不显示)
if (y > 7 || x > 15)
return;
// 第一步:设置显存地址(告诉SSD1306:接下来的数据要存在哪个位置)
OLED_SetPosition(page, col);
// 第二步:发送字符上半部分的8个字节点阵
OLED_WriteNBytes((uint8_t*)&ascii_font[c][0], 8);
// 同理,设置下一页地址,发送字符下半部分的8个字节点阵
OLED_SetPosition(page + 1, col);
OLED_WriteNBytes((uint8_t*)&ascii_font[c][8], 8);
}
关键在这两句:
OLED_SetPosition
:设置 SSD1306 显存的地址(就像告诉快递员 "东西要放在哪个货架");OLED_WriteNBytes
:发送字符的点阵数据(ascii_font
是存储所有 ASCII 字符点阵的数组,比如ascii_font['A']
就是 'A' 的点阵)。
这一步完全遵循 SSD1306 芯片手册的要求:必须先设置地址,再发送数据,且每个字符的 16 个点阵字节要分两次发送(每次 8 字节)。
4. I2C 控制器驱动(HAL 库):"跑腿"------ 物理层传输
OLED_WriteNBytes
函数最终会调用 STM32 的 I2C 控制器,通过硬件引脚(SCL、SDA)把数据真正发送出去。这一步由 HAL 库的 I2C 函数(比如 HAL_I2C_Master_Transmit
)完成,相当于 "跑腿的",负责把数据从 STM32 传到 SSD1306。
结尾
今天我们从 "OLED 显示字符" 这个案例入手,从上到下梳理了 I2C 协议的应用流程:应用程序提需求→库函数拆任务→SSD1306 驱动做具体配置→I2C 控制器完成物理传输。
下一篇我们会反过来,从下往上深入:先了解 I2C 协议的底层时序(SCL 和 SDA 线上的高低电平变化规律),再学习 STM32 的 I2C 控制器如何配置,最终理解 "点阵数据是如何通过两条线(SCL、SDA)从 STM32 传到 SSD1306 的"。
Hello_Embed 继续带你从现象到本质,吃透 I2C 协议,敬请期待~