硬件IIC主从机通信机制与工程实现——从原理->架构设计->代码实现---基于LCM32F067(Cortex M0内核)
4.6.2 如果主机打印 write err / read err
摘要
IIC(Inter-Integrated Circuit)总线是一种广泛应用于嵌入式系统中的同步串行通信协议,具有布线简单、支持多设备通信以及硬件资源消耗低等特点。本文基于 MCU 硬件 IIC 控制器,设计并实现了一种典型的寄存器映射型 IIC 主从通信系统。系统采用主机—从机结构,从机内部维护寄存器表,通过标准寄存器读写协议完成数据交互。文章首先分析 IIC 总线工作原理与通信时序,然后结合实际工程代码,对主机寄存器访问流程、从机中断驱动机制以及异常处理策略进行了系统分析。进一步通过通信状态机和代码流程图解释系统软件结构。实验结果表明,该系统能够稳定完成连续寄存器读写,并具有良好的可扩展性和工程应用价值。
1 引言
在现代嵌入式系统中,大量外围器件需要与主控制器进行通信,例如温度传感器、EEPROM 存储器、实时时钟以及各种数据采集芯片。为了降低系统复杂度,工业界广泛采用串行通信总线。IIC 总线因其结构简单、可靠性高而成为最常用的通信接口之一。
IIC 总线最初由 Philips 公司提出,其设计目标是实现芯片之间的短距离通信。与 SPI 等总线相比,IIC 仅需要两条信号线即可实现多设备通信,因此特别适合资源受限的嵌入式系统。
在实际工程中,大多数 IIC 设备都采用 寄存器映射通信模型。主机首先发送寄存器地址,然后进行数据读写操作。典型应用包括:
-
EEPROM 存储器
-
温度传感器
-
RTC 实时时钟
-
ADC/DAC 芯片
本文通过一个 MCU 硬件 IIC 控制器实现完整的主从通信系统,其中:
-
主机负责寄存器访问
-
从机维护寄存器表
-
通信遵循标准寄存器访问协议
该系统能够很好地模拟真实 IIC 外设的通信方式。
2 IIC 总线原理
2.1 IIC 总线结构
IIC 总线由两条信号线构成:
| 信号线 | 功能 |
|---|---|
| SCL | 时钟线 |
| SDA | 数据线 |
IIC 总线采用 开漏(Open-Drain)输出结构,所有设备共享总线,并通过上拉电阻维持高电平。
这种设计具有两个重要特点:
-
支持多设备并联
-
避免总线冲突
在代码实现中,GPIO 被配置为:
-
复用模式
-
开漏输出
-
上拉电阻
3 IIC 基本通信时序
3.1 IIC 总线基本时序



IIC 通信由以下几个基本元素构成:
1 起始条件(START)
当 SCL 为高电平时 SDA 从高变低,表示通信开始。
2 停止条件(STOP)
当 SCL 为高电平时 SDA 从低变高,表示通信结束。
3 数据传输
每个数据字节包含 8 位数据。
4 应答信号
第 9 个时钟周期用于 ACK。
-
SDA = 0 → ACK
-
SDA = 1 → NACK
4 Hardware_IIC 说明文档
4.1 文件说明
本目录现在包含两份 IIC 示例源码:
Master_IIC.cSlaver_IIC.c
两者配套使用,实现的是一个最小可用的“寄存器表型”I2C 主从通信模型。
此工程中的主机和从机的源代码从百度网盘中获取:
链接: https://pan.baidu.com/s/1q-m_srDCGYbLuIwx9PgKyg?pwd=2p45 提取码: 2p45
这个模型的特点是:
- 主机先写 1 个寄存器地址。
- 如果后面继续写数据,则从该地址开始顺序写入从机寄存器表。
- 如果后面改为读操作,则从该地址开始顺序读取从机寄存器表。
- 从机内部维护一个
RegisterMap[]数组,主机看到的“寄存器”本质上就是这个数组。
4.2 当前工程协议约定
4.2.1 从机地址
- 当前从机地址:
0x28 - 地址类型:7 位地址
这个地址已经避开了 I2C 保留地址区,适合工程使用。
4.2.2 从机寄存器空间
从机内部定义了:
RegisterMap[32]
也就是提供了 32 个逻辑寄存器,地址范围为:
0x00 ~ 0x1F
4.2.3 主机演示访问范围
当前主机示例每次都访问:
- 起始寄存器:
0x00 - 连续长度:
8字节
因此主机每轮的动作是:
- 向从机
0x28的0x00开始位置写入 8 字节。 - 再从从机
0x28的0x00开始位置读回 8 字节。 - 通过串口打印读回结果。
4.3 主机侧工作原理
4.3.1 主机初始化做了什么
Master_IIC.c 中主机初始化主要做了以下几件事:
- 打开
GPIOA/GPIOB/USART0/I2C0时钟。 - 把
PA9/PA10配置为 I2C 复用功能。 - 把 I2C 引脚输出类型改为开漏
GPIO_OType_OD。 - 初始化 USART0,作为调试串口。
- 初始化 I2C0 为主机模式。
- 设置 I2C 速率为
100kHz。 - 显式打开
RESTEN,允许主机发送 repeated START。
这里最关键的一点是 repeated START,因为“写寄存器地址后立即读数据”正是大多数寄存器型器件的标准访问方式。
4.3.2 主机写寄存器流程
主机函数:
I2C_MasterWriteRegisters()
对应的总线时序是:
START
SlaveAddr + W
RegisterAddr
Data0
Data1
...
DataN
STOP
函数内部逻辑如下:
- 先等待 I2C 总线空闲。
- 把目标从机地址写入
TAR。 - 向
DATACMD先写 1 个寄存器地址字节。 - 再顺序写入数据字节。
- 最后 1 个字节附加
STOP位。 - 等待硬件真正发送完成并返回空闲。
4.3.3 主机读寄存器流程
主机函数:
I2C_MasterReadRegisters()
对应总线时序是:
START
SlaveAddr + W
RegisterAddr
RESTART
SlaveAddr + R
Read0
Read1
...
ReadN
NACK
STOP
要注意,这个芯片的 I2C 读不是“直接读寄存器就能拿到总线数据”,而是:
- 先向
DATACMD写“读命令”。 - 硬件收到该命令后才去总线上发起读。
- 数据回来后放进 RX FIFO。
- CPU 再从 RX FIFO 里把数据取出来。
所以代码里的本质动作是:
- 先发 1 个写命令,把寄存器地址送给从机。
- 再发若干个
CMD=1的读命令。 - 第一条读命令带
RESTART。 - 最后一条读命令带
STOP。 - 再从 RX FIFO 逐字节取走读回数据。
4.3.4 主机为什么要做超时和 ABRT 检查
示例版最容易出问题的地方,是所有等待都可能无限死循环。
工程版里加入了:
I2C_TIMEOUT_COUNTI2C_MasterWaitStatus()I2C_MasterCheckAbort()I2C_MasterAbortTransfer()
这样做的意义是:
- 如果从机没应答,不会永远卡死。
- 如果地址或数据被 NACK,可以通过
TX_ABRT_SOURCE看到具体原因。 - 如果总线状态异常,可以主动请求 ABORT,减少死锁概率。
4.4 从机侧工作原理
4.4.1 从机初始化做了什么
Slaver_IIC.c 中从机初始化做了以下配置:
- 初始化 I2C0 为纯从机模式。
- 设置从机地址为
0x28。 - 使用 7 位地址格式。
- 打开
RX_FULL / RD_REQ / RX_DONE / STOP_DET / TX_ABRT / RX_OVER中断。 - 设置 FIFO 阈值为 0。
- 允许 RX FIFO 满时保持总线。
这些配置使得从机更像真实外设:
- 写操作能及时取数。
- 读操作能及时准备回送数据。
- 异常路径有状态可查。
4.4.2 从机如何识别“寄存器地址字节”
从机使用了手册里的 FDB 机制。
手册定义:
- 在从机接收模式下,地址后的第 1 个数据字节,
DATACMD.FDB = 1
工程版代码把这个字节解释为:
- 当前寄存器地址
也就是:
主机写:
START + 0x28(W) + 0x05 + 0xAA + 0xBB + STOP
从机理解为:
0x05 是寄存器地址
0xAA 写入寄存器 0x05
0xBB 写入寄存器 0x06
这就是寄存器型设备的典型行为。
4.4.3 从机写事务如何处理
从机接收写事务时,ISR 中首先不断把 RX FIFO 清空:
while (RFNF)
读 1 个字节
交给 Slave_ProcessReceivedByte()
Slave_ProcessReceivedByte() 内部规则如下:
- 如果
FDB=1,把这个字节当成寄存器地址。 - 如果
FDB=0,把这个字节写入当前寄存器。 - 每写 1 个数据字节,当前寄存器地址自动递增。
- 同时把前 8 个数据保存到
LastWriteShadow[],便于主循环打印。
当总线收到 STOP 时:
- ISR 进入
STOP_DET分支。 - 把这笔写事务长度保存到
LastWriteLength。 - 把
WriteFrameReady置 1。 - 主循环看到这个标志后,再统一打印。
所以对从机来说,真正的“一帧写事务结束”判据是:
STOP_DET
而不是某个固定长度,也不是“读到了某个特殊字节值”。
4.4.4 从机读事务如何处理
当主机改为读从机时,硬件会产生:
RD_REQ
这表示:
- 主机正在向从机请求数据;
- 如果从机 TX FIFO 里没有数据,硬件会保持 SCL 为低,等待软件装数。
工程版中,ISR 在 RD_REQ 分支里会做:
- 如果是本轮读事务的第一次请求,记录
TxStartRegister = CurrentRegister。 - 调用
Slave_FillTxFifo()。 - 把当前寄存器开始的内容顺序装入 TX FIFO。
- 清除
RD_REQ,让硬件释放时钟继续发送。
也就是说,从机发送的数据来源于:
RegisterMap[CurrentRegister]RegisterMap[CurrentRegister + 1]RegisterMap[CurrentRegister + 2]- ...
4.4.5 为什么正常读完也可能出现 TX_ABRT
这一点最容易被误解。
在这个 I2C 控制器里,从机发数据时,如果:
- 预先装入了多于主机真正需要的字节;
- 主机在读完最后 1 个字节后返回 NACK;
- 硬件会把 TX FIFO 中剩余未发字节 flush 掉;
- 同时置位
TX_ABRT
所以:
TX_ABRT不一定意味着“真正错误”- 很可能只是“主机正常读完,本次剩余待发数据被控制器清空”
工程版因此专门做了 Slave_OnTxAbort():
- 读取
TX_ABRT_SOURCE - 读取
TX_FLUSH_CNT - 计算主机真正读走了多少字节
- 把
CurrentRegister推进到下次应该继续读的位置 - 对“正常 flush 型结束”不重复当成严重错误上报
这一步非常关键,否则从机寄存器读指针会乱。
4.5 主从交互完整流程
4.5.1 写事务流程
以主机写 8 字节到从机 0x00 开始为例:
主机:
START
0x28 + W
0x00
0x10
0x11
0x12
0x13
0x14
0x15
0x16
0x17
STOP
从机内部流程:
- 收到地址匹配后进入从机接收状态。
- 收到首字节
0x00,且FDB=1:- 解释成寄存器地址;
CurrentRegister = 0x00
- 收到后续 8 个普通字节:
0x10 -> RegisterMap[0x00]0x11 -> RegisterMap[0x01]- ...
0x17 -> RegisterMap[0x07]
- 收到
STOP_DET:- 保存本次写入长度;
- 通知主循环打印。
主循环最后会打印类似:
write reg 0x00 len 0x08 data 10 11 12 13 14 15 16 17
4.5.2 读事务流程
以主机从从机 0x00 开始读 8 字节为例:
主机:
START
0x28 + W
0x00
RESTART
0x28 + R
读 8 字节
NACK
STOP
从机内部流程:
- 主机先用写方向发
0x00:- 从机收到首字节
0x00; CurrentRegister = 0x00
- 从机收到首字节
- 主机发 repeated START,再切换为读方向。
- 从机收到
RD_REQ:- 以
CurrentRegister为起点; - 往 TX FIFO 填
RegisterMap[0x00]开始的数据。
- 以
- 主机连续读走 8 字节。
- 最后一个字节后主机返回 NACK,并发 STOP。
- 如果 TX FIFO 还有剩余预装数据,控制器 flush 它们并触发
TX_ABRT。 - 从机在
Slave_OnTxAbort()中根据 flush 数量推算:- 主机真正拿走了多少字节;
- 下次寄存器读指针应推进到哪里。
4.6 调试时建议关注的现象
4.6.1 串口输出
主机串口应周期性看到:
read: xx xx xx xx xx xx xx xx
从机串口应周期性看到:
write reg 0x00 len 0x08 data xx xx xx xx xx xx xx xx
4.6.2 如果主机打印 write err / read err
优先检查:
- 主从地址是否一致;
- 上拉是否存在;
- SCL/SDA 是否交叉或接反;
- 两边 I2C 速率是否合理;
Last_AbortSource中是否出现7ADDR_NOACK / TXDATA_NOACK类来源。
4.6.3 如果从机打印 slave err flags
优先检查:
- 是否发生
RX_OVER; - 主机是否发得太快,但从机 ISR 处理不过来;
- 是否存在异常时钟、线缆太长或波形质量差的问题。
5. 总结
当前这套代码的核心思想可以概括为两点:
- 主机按“寄存器地址 + 连续数据”的方式访问从机。
- 从机把所有读写都映射到内部
RegisterMap[],并用中断驱动收发。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)