5.2 I2C与SPI:接传感器


文档摘要

5.2 I2C 与 SPI:接传感器 串口打通了芯片与人的对话,这一节打通芯片与器件的对话。I2C 用两根线挂起一整排低速器件,SPI 用四根线喂饱高速外设,它们覆盖了嵌入式世界里九成以上的外接需求——传感器、EEPROM、屏幕、Flash、RTC。本节以 I2C 为主角把总线协议的通用骨架(寻址、应答、时序)拆开看一遍,再以寄存器级代码读一颗真实传感器,SPI 作为对照收尾。 I2C:两根线的组织学 I2C(Inter-Integrated Circuit)只有 SCL(时钟)与 SDA(数据)两根线,却支持一主多从。它能这么省,靠三件设计: 其一,开漏 + 上拉。所有设备的 SCL/SDA 都是开漏输出(3.

5.2 I2C 与 SPI:接传感器

串口打通了芯片与人的对话,这一节打通芯片与器件的对话。I2C 用两根线挂起一整排低速器件,SPI 用四根线喂饱高速外设,它们覆盖了嵌入式世界里九成以上的外接需求——传感器、EEPROM、屏幕、Flash、RTC。本节以 I2C 为主角把总线协议的通用骨架(寻址、应答、时序)拆开看一遍,再以寄存器级代码读一颗真实传感器,SPI 作为对照收尾。

I2C:两根线的组织学

I2C(Inter-Integrated Circuit)只有 SCL(时钟)与 SDA(数据)两根线,却支持一主多从。它能这么省,靠三件设计:

其一,开漏 + 上拉。所有设备的 SCL/SDA 都是开漏输出(3.3 节讲过开漏只能拉低),线被上拉电阻拽着保持高电平,任何设备想表达"0"就拉低。这解决了两个问题:多设备同时驱动不打架(线与逻辑),且从机可以故意拉低 SDA 拖慢时钟(时钟伸展)。代价是速度受限——上拉电阻与线电容组成的 RC 回路让边沿爬得慢,标准模式 100 kHz、快速模式 400 kHz,再往上要特殊驱动。

其二,地址寻址。每个从机有固定地址(7 位,芯片手册给定),主机每次开嗓先广播地址,被点名的从机应答,其余装聋。于是两根线理论上可挂 112 个器件(部分地址保留),同类器件靠地址引脚区分。

其三,应答机制。每传完一个字节,接收方必须在第 9 个时钟把 SDA 拉低一拍表示 ACK。主机发地址后有从机应答、发数据后有接收方应答;主机读数据时轮到主机应答,读完最后一个字节主机回 NACK(不应答)示意"收完了"。应答是 I2C 调试的第一线索——地址无应答,八成是接线、地址错或器件没上电。

一次完整的"主机写寄存器地址再读两字节"时序如下,这是绝大多数传感器的标准读法:

START → 器件地址+写 → ACK → 寄存器地址 → ACK → RESTART → 器件地址+读 → ACK → 数据高字节 → ACK(主机) → 数据低字节 → NACK → STOP

图 5-3:I2C 读传感器的完整时序

图 5-3:I2C 读传感器的完整时序

寄存器级 I2C 读传感器:事件驱动的耐心活

STM32 的 I2C 外设是典型的事件驱动状态机:每完成一个阶段置一个事件标志(SB 地址已发、ADDR 地址已应答、RXNE 收到字节),程序按事件顺序推进。读光照传感器 BH1750 的原始值(地址 0x23,读两字节):

#include "stm32f10x.h" #include <stdint.h> #define BH1750_ADDR_WRITE (0x23 << 1) /* 7 位地址左移 1 位,最低位 0 表示写 */ #define BH1750_ADDR_READ ((0x23 << 1) | 1) /* 最低位 1 表示读 */ /* I2C1 复用在 PB6=SCL PB7=SDA,配成复用开漏(CNF=11)*/ void i2c1_init_100k(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN; RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; GPIOB->CRL &= ~(0xFFUL << 24); GPIOB->CRL |= (0xEFUL << 24); /* PB6 PB7:复用开漏 + 50 MHz */ I2C1->CR2 = 36; /* APB1 36 MHz,供硬件算时钟控制 */ I2C1->CCR = 180; /* 36 MHz / (2 × 180) = 100 kHz */ I2C1->TRISE = 37; /* 最大上升时间,规范要求 */ I2C1->CR1 |= I2C_CR1_PE; /* 外设使能 */ } /* 读 BH1750 两字节:返回 0 成功 */ int bh1750_read(uint16_t *lux_raw) { uint32_t timeout; I2C1->CR1 |= I2C_CR1_START; /* 发 START */ while (!(I2C1->SR1 & I2C_SR1_SB)) { if (++timeout > 20000) return -1; } (void)I2C1->SR1; /* 读 SR1 清 SB 标志 */ I2C1->DR = BH1750_ADDR_WRITE; /* 器件地址+写 */ while (!(I2C1->SR1 & I2C_SR1_ADDR)) { if (++timeout > 40000) return -1; } (void)I2C1->SR1; (void)I2C1->SR2; /* 读 SR1+SR2 清 ADDR */ I2C1->DR = 0x10; /* 命令:连续高分辨率模式 */ while (!(I2C1->SR1 & I2C_SR1_TXE) || !(I2C1->SR1 & I2C_SR1_BTF)) { } I2C1->CR1 |= I2C_CR1_START; /* RESTART,掉头读 */ while (!(I2C1->SR1 & I2C_SR1_SB)) { } (void)I2C1->SR1; I2C1->DR = BH1750_ADDR_READ; while (!(I2C1->SR1 & I2C_SR1_ADDR)) { } (void)I2C1->SR1; (void)I2C1->SR2; I2C1->CR1 &= ~I2C_CR1_ACK; /* 只收最后字节前关应答 */ while (!(I2C1->SR1 & I2C_SR1_RXNE)) { } uint8_t hi = (uint8_t)I2C1->DR; /* 高字节 */ I2C1->CR1 |= I2C_CR1_ACK | I2C_CR1_STOP; /* 应答 + STOP */ while (!(I2C1->SR1 & I2C_SR1_RXNE)) { } uint8_t lo = (uint8_t)I2C1->DR; /* 低字节 */ *lux_raw = (uint16_t)((hi << 8) | lo); return 0; } /* 换算与打印:BH1750 原始值除以 1.2 得勒克斯 */ // uint16_t raw; if (bh1750_read(&raw) == 0) // printf("light = %d lux\r\n", (int)(raw / 1.2f));

这段代码的每一个"等标志"都对应时序图的一个阶段,对照着读一遍,I2C 的运行图景就立起来了。同时它也暴露了裸寄存器 I2C 的费劲之处:标志顺序繁琐、超时处理啰嗦、锁死恢复要九步舞(总线被从机拉死时,手动发九个时钟脉冲解锁是行业黑话级的经典操作)。工程上我会毫不犹豫选厂商库或成熟驱动做 I2C——寄存器层的价值是让你在总线卡死时知道卡在哪个阶段,而不是让你每次手写这套。ESP32 的 i2c_master 驱动同样把细节封装成配置结构体,调用形式几乎相同。

SPI:四根线的速度担当

SPI 用主从模式甩掉了寻址开销:SCK 时钟由主机提供,MOSI/MISO 两根数据线全双工对发,每个从机独享一根片选 CS——主机拉低谁的 CS 就跟谁说话,地址寻址的成本换成了一根物理线。没有应答机制、没有地址广播,硬件简单到极致,换来的是轻松几十 MHz 的速率,TFT 屏、SD 卡、外部 Flash 这些吞吐大户清一色用 SPI。

SPI 的唯一玄学是模式匹配:CPOL(空闲时时钟电平)与 CPHA(第几个边沿采样)组合成四种模式,器件手册都会标明自己吃哪一种,配错了读出的数据整体错位(比如 0x55 变 0xAA),症状极像接线错。看一眼模式表的记忆法:

模式 CPOL CPHA 采样时机 常见器件举例
0 0 0 第一个边沿(上升)采样 SD 卡、多数传感器
1 0 1 第二个边沿(下降)采样 部分旧款 ADC
2 1 0 第一个边沿(下降)采样 少见
3 1 1 第二个边沿(上升)采样 很多 TFT 屏控制器

ESP32 对此的抽象更省心:SPI 主驱动直接问你要 spi_bus_initialize 的时钟极性相位参数,手册标"SPI Mode 3"就原样填 CPOL=1 CPHA=1。三种接口选型到此可以收拢成一句话:**人和模组说话用 UART,低速传感器一群用 I2C,高速大流量器件用 SPI**;资源够时它们可以同时在线,各自挂各自的设备,互不打扰。

至此芯片已经能说、能听、能读传感器。但它还在"一次只做一件事"的裸机循环里打转——下一章引入 FreeRTOS,让芯片学会分身:多任务并行、消息排队、资源互斥,那些通信与传感的数据流将各得其所。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U