【嵌入式 C 学习】Day25:I²C 基础通信、设备地址与 ACK/NACK,Day26:I²C 寄存器读写、Repeated START 与连续多字节读取
【嵌入式 C 学习】Day25:I²C 基础通信、设备地址与 ACK/NACK
一、前言
前两天学习了 SPI:
Day23:SPI 单字节全双工通信
Day24:SPI 寄存器读写协议
今天开始学习另一种常见的板级通信协议:I²C。
I²C 常用于连接:
温湿度传感器
EEPROM
RTC 实时时钟
OLED 显示屏
电流电压检测芯片
加速度计
陀螺仪
GPIO 扩展芯片
本次使用 C 语言模拟一个地址为 0x48 的 I²C 从机,实现:
START 和 STOP
7 位设备地址
读写地址字节
ACK 和 NACK
单字节写入
单字节读取
错误地址处理
错误通信顺序检查
通信次数统计
二、I²C 的两根信号线
I²C 主要使用两根线:
SCL:串行时钟线
SDA:串行数据线
SCL 通常由主机产生,用于提供通信时钟。
SDA 是双向数据线,可以完成:
主机向从机发送数据
从机向主机发送数据
由于发送和接收共用 SDA,I²C 通常属于半双工通信。
三、为什么 I²C 需要上拉电阻
I²C 的 SCL 和 SDA 通常采用开漏输出。
开漏输出只能:
主动把线路拉低
释放线路
不能像普通推挽 GPIO 一样主动输出高电平。
因此需要外部上拉电阻:
VCC
|
上拉电阻
|
主机 SDA ------+------ 从机 SDA
当所有设备都释放 SDA 时,上拉电阻使线路变为高电平。
当任意一个设备拉低 SDA 时,总线变为低电平。
所以:
输出 0:主动拉低
输出 1:释放总线
这种多个设备共用线路的逻辑称为:
线与
Wired-AND
四、I²C 总线空闲状态
总线空闲时:
SCL = HIGH
SDA = HIGH
此时没有设备拉低线路。
在本次软件模拟中,使用:
bus.started = 0U;
表示总线处于空闲状态。
五、START 起始信号
START 的定义是:
SCL 保持高电平时
SDA 从高电平变为低电平
简化时序:
SCL:──────── HIGH ────────
SDA:──── HIGH ──┐
└── LOW ──
↑
START
START 表示一段新的 I²C 事务开始。
程序接口:
int i2c_start(I2cBus *bus);
成功后:
started = 1
addressed = 0
start_count + 1
六、STOP 停止信号
STOP 的定义是:
SCL 保持高电平时
SDA 从低电平变为高电平
简化时序:
SCL:──────── HIGH ────────
SDA:──── LOW ───┐
└── HIGH ─
↑
STOP
STOP 表示当前事务结束,总线恢复为空闲状态。
程序接口:
int i2c_stop(I2cBus *bus);
成功后:
started = 0
addressed = 0
stop_count + 1
七、I²C 如何选择从机
SPI 通常使用不同的 CS 引脚选择从机:
CS_FLASH
CS_SENSOR
CS_DISPLAY
I²C 则通过设备地址选择从机。
例如:
温度传感器:0x48
EEPROM:0x50
RTC:0x68
多个设备可以共用同一组:
SCL
SDA
主机发送地址后,只有地址匹配的从机返回 ACK。
核心区别:
SPI 使用 CS 选择设备
I²C 使用设备地址选择设备
八、7 位设备地址
本次模拟设备地址:
#define I2C_DEVICE_DEFAULT_ADDRESS 0x48U
0x48 是一个 7 位设备地址。
7 位地址的数值范围为:
0x00~0x7F
需要注意,真实 I²C 规范中有部分保留地址,因此实际使用时还需要查看芯片数据手册。
九、地址字节的组成
I²C 设备地址是 7 位,但总线上发送的是 8 位地址字节。
结构:
bit7~bit1:7 位设备地址
bit0:读写方向位
即:
地址字节 = 7 位地址左移 1 位 | 读写方向
程序使用:
typedef enum
{
I2C_DIRECTION_WRITE = 0,
I2C_DIRECTION_READ = 1
} I2cDirection;
十、地址 0x48 的写地址字节
设备地址:
0x48
左移一位:
0x48 << 1 = 0x90
写方向为 0:
0x90 | 0 = 0x90
因此:
0x90
表示:
访问设备 0x48,并进行写操作
十一、地址 0x48 的读地址字节
设备地址左移:
0x48 << 1 = 0x90
读方向为 1:
0x90 | 1 = 0x91
因此:
0x91
表示:
访问设备 0x48,并进行读操作
需要区分:
0x48:7 位设备地址
0x90:写地址字节
0x91:读地址字节
程序中通常保存 0x48,发送时再生成 0x90 或 0x91。
十二、ACK 应答
ACK 全称:
Acknowledge
I²C 每发送完 8 位后,第 9 个时钟用于应答。
ACK 时,接收方拉低 SDA:
SDA = 0
表示:
当前字节已经正确接收
可以继续通信
程序中定义:
typedef enum
{
I2C_RESPONSE_ACK = 0,
I2C_RESPONSE_NACK = 1
} I2cResponse;
十三、NACK 非应答
NACK 时,接收方不拉低 SDA:
SDA = 1
NACK 可能表示:
没有设备响应该地址
从机拒绝接收数据
主机不再继续读取
通信过程发生异常
因此 NACK 不一定表示错误,需要结合出现的位置判断。
十四、单字节写入流程
本次向设备写入:
0xA5
完整流程:
START
↓
发送地址 0x48 + WRITE
↓
从机 ACK
↓
发送数据 0xA5
↓
从机 ACK
↓
STOP
总线上的字节:
地址字节:0x90
数据字节:0xA5
程序调用:
i2c_start(&bus);
i2c_send_address(
&bus,
&device,
0x48U,
I2C_DIRECTION_WRITE,
&response);
i2c_write_byte(
&bus,
&device,
0xA5U,
&response);
i2c_stop(&bus);
写入后:
stored_data = 0xA5
十五、单字节读取流程
本次从机准备返回:
0x5A
完整流程:
START
↓
发送地址 0x48 + READ
↓
从机 ACK
↓
从机发送 0x5A
↓
主机发送 NACK
↓
STOP
总线上的地址字节:
0x91
程序调用:
i2c_start(&bus);
i2c_send_address(
&bus,
&device,
0x48U,
I2C_DIRECTION_READ,
&response);
i2c_read_byte(
&bus,
&device,
&read_data,
I2C_RESPONSE_NACK);
i2c_stop(&bus);
十六、为什么单字节读取后发送 NACK
读取数据时:
从机是发送方
主机是接收方
如果主机还要继续读取,主机发送 ACK。
如果当前已经是最后一个字节,主机发送 NACK。
所以单字节读取时:
读取 0x5A
主机发送 NACK
STOP
这里的 NACK 表示:
数据已经收到
不再继续读取
属于正常协议行为。
十七、I²C 总线结构体
本次定义:
typedef struct
{
uint32_t clock_hz;
uint8_t started;
uint8_t addressed;
uint8_t current_address;
I2cDirection direction;
uint32_t start_count;
uint32_t stop_count;
uint32_t ack_count;
uint32_t nack_count;
uint32_t byte_count;
} I2cBus;
其中:
clock_hz:I²C 时钟频率
started:是否已经产生 START
addressed:是否有从机响应地址
current_address:当前 7 位地址
direction:当前读写方向
start_count:START 次数
stop_count:STOP 次数
ack_count:ACK 次数
nack_count:NACK 次数
byte_count:8 位字段数量
十八、模拟从机结构体
定义:
typedef struct
{
uint8_t address;
uint8_t stored_data;
uint8_t read_data;
} I2cDevice;
含义:
address:设备的 7 位地址
stored_data:保存主机写入的数据
read_data:从机准备返回的数据
初始化后:
address = 0x48
stored_data = 0x00
read_data = 0x5A
十九、函数返回值和 NACK 的区别
i2c_send_address() 同时具有:
函数返回值
response 参数
函数返回值表示:
参数是否正确
是否已经 START
方向是否合法
地址是否合法
response 表示协议层应答:
ACK
NACK
例如访问错误地址时:
地址字节正常发送
从机没有响应
response = NACK
函数仍返回成功
因此:
协议 NACK 不等于 C 函数执行失败
二十、错误地址测试
模拟从机地址:
0x48
主机故意访问:
0x50
写地址字节:
0x50 << 1 = 0xA0
因为没有地址为 0x50 的设备,所以得到 NACK:
Address 0x50 -> NACK
地址阶段收到 NACK 后,再继续写数据会被驱动拒绝。
设备原来的数据仍然保持:
0xA5
二十一、错误通信顺序测试
程序测试了以下错误:
没有 START 就发送地址
没有 START 就发送 STOP
事务中再次 START
没有发送地址就写数据
READ 方向调用写函数
WRITE 方向调用读函数
这些调用都被正确拒绝。
Day25 暂时不支持 Repeated START,所以下列代码第二次调用失败:
i2c_start(&bus);
i2c_start(&bus);
Day26 将专门学习 Repeated START。
二十二、实际运行结果
执行:
cd /root/Embedded_14Days/day25
make clean
make
make run
运行结果:
===== Day25 I2C Basic Communication Test =====
[I2C CONFIG] Clock = 100000 Hz
[DEVICE CONFIG] 7-bit address = 0x48
[DEVICE CONFIG] Read data = 0x5A
----- Test 1: Address Byte Build -----
[ADDRESS] 7-bit device address = 0x48
[ADDRESS] Write address byte = 0x90
[ADDRESS] Read address byte = 0x91
[CHECK] Address bytes are correct
[CHECK] Write byte bit0 = 0
[CHECK] Read byte bit0 = 1
----- Test 2: Single Byte Write -----
[I2C] START
[I2C] Address byte 0x90 -> ACK
[I2C] Write data 0xA5 -> ACK
[I2C] STOP
[DEVICE] Stored data = 0xA5
[CHECK] Single byte write verified
----- Test 3: Single Byte Read -----
[I2C] START
[I2C] Address byte 0x91 -> ACK
[I2C] Read data 0x5A
[I2C] Master response -> NACK
[I2C] STOP
[CHECK] Single byte read verified
----- Test 4: Wrong Device Address -----
[I2C] START
[I2C] Address 0x50 (byte 0xA0) -> NACK
[EXPECTED] Wrong address was not acknowledged
[EXPECTED ERROR] Data write rejected after address NACK
[I2C] STOP
[CHECK] Stored data unchanged: 0xA5
----- Test 5: Invalid Sequences -----
[EXPECTED ERROR] Address rejected before START
[EXPECTED ERROR] STOP rejected before START
[I2C TEST] First START succeeded
[EXPECTED ERROR] Repeated START rejected
[EXPECTED ERROR] Write rejected before address
[EXPECTED ERROR] Write rejected in READ direction
[EXPECTED ERROR] Read rejected in WRITE direction
[CHECK] Invalid sequence test completed
----- Test 6: Invalid Parameters -----
[EXPECTED ERROR] NULL bus rejected
[EXPECTED ERROR] Zero clock rejected
[EXPECTED ERROR] NULL device rejected
[EXPECTED ERROR] Invalid direction rejected
[EXPECTED ERROR] NULL address output rejected
----- Final I2C Status -----
[FINAL] START count = 3
[FINAL] STOP count = 3
[FINAL] ACK count = 3
[FINAL] NACK count = 2
[FINAL] Byte count = 5
[FINAL] Bus started = NO
[FINAL] Addressed = NO
[FINAL] Stored data = 0xA5
[FINAL CHECK] All I2C tests passed
===== I2C Test Finished =====
编译过程中没有出现:
warning
error
二十三、最终计数分析
单字节写入
START:1
STOP:1
地址字节:1
数据字节:1
ACK:2
单字节读取
START:1
STOP:1
地址字节:1
数据字节:1
ACK:1
NACK:1
错误地址
START:1
STOP:1
地址字节:1
NACK:1
最终:
START = 3
STOP = 3
ACK = 3
NACK = 2
BYTE = 5
ACK 和 NACK 使用第 9 个时钟周期,不属于新的 8 位数据字节,因此不计入 byte_count。
二十四、SPI 与 I²C 对比
| 项目 | SPI | I²C |
|---|---|---|
| 信号线 | SCK、MOSI、MISO、CS | SCL、SDA |
| 设备选择 | CS | 设备地址 |
| 数据方向 | MOSI/MISO 分开 | SDA 双向 |
| 通信方式 | 通常全双工 | 通常半双工 |
| 应答机制 | 由设备协议决定 | ACK/NACK |
| 上拉电阻 | 通常不需要 | 通常需要 |
| 多设备连接 | 每个从机通常需要 CS | 多设备共用两根线 |
| 通信速度 | 通常较高 | 通常较低 |
最重要的是:
SPI 使用 CS 选设备
I²C 使用地址选设备
二十五、今日收获
通过 Day25 学习,我掌握了:
- I²C 的基本特点;
- SCL 和 SDA 的作用;
- 开漏输出;
- 上拉电阻;
- 总线空闲状态;
- START 和 STOP;
- 7 位设备地址;
- 地址字节组成;
- 读写方向位;
0x48、0x90和0x91的区别;- ACK 和 NACK;
- 地址阶段应答;
- 写数据阶段应答;
- 单字节读取结束 NACK;
- 单字节写入流程;
- 单字节读取流程;
- 错误地址处理;
- 通信顺序检查;
- 函数失败和协议 NACK 的区别;
- I²C 状态与通信次数统计。
二十六、下一步计划
Day26 将继续学习:
I²C 寄存器读写协议
Repeated START
连续多字节读取
典型 I²C 寄存器读取流程:
START
设备地址 + WRITE
寄存器地址
Repeated START
设备地址 + READ
读取寄存器数据
NACK
STOP
重点理解:
为什么读取 I²C 寄存器时
通常需要先写寄存器地址
再切换到读取方向

【嵌入式 C 学习】Day26:I²C 寄存器读写、Repeated START 与连续多字节读取
一、前言
Day25 学习了 I²C 基础通信,包括:
SCL 和 SDA
START 与 STOP
7 位设备地址
读写方向位
ACK 与 NACK
单字节写入
单字节读取
但真实的 I²C 传感器通常不仅包含一个数据字节,而是包含多个内部寄存器,例如:
设备 ID
状态寄存器
温度数据
配置寄存器
故障标志
中断状态
主机不仅要知道设备地址,还要告诉设备:
我要访问哪个寄存器
因此,Day26 使用 C 语言模拟了一个带寄存器的 I²C 温度传感器,实现:
设备寄存器表
寄存器指针
Repeated START
单寄存器读取
配置寄存器写入
连续多字节读取
寄存器指针自动递增
温度高低字节组合
只读寄存器保护
非法寄存器地址保护
总线忙状态保护
事务失败恢复
二、为什么直接发送读地址还不够
假设 I²C 设备地址是:
0x48
主机发送:
START
地址 0x48 + READ
只能告诉设备:
我要从地址为 0x48 的设备读取数据
但设备内部有多个寄存器:
0x00:设备 ID
0x01:状态
0x02:温度高字节
0x03:温度低字节
0x04:配置
从机无法仅根据读地址判断主机想读取哪个寄存器。
因此,读取内部寄存器时通常要先发送寄存器地址。
三、典型 I²C 寄存器读取流程
读取设备内部寄存器的典型流程是:
START
↓
设备地址 + WRITE
↓
从机 ACK
↓
发送寄存器地址
↓
从机 ACK
↓
Repeated START
↓
设备地址 + READ
↓
从机 ACK
↓
读取寄存器数据
↓
主机 NACK
↓
STOP
例如读取设备 ID 寄存器 0x00:
START
TX 0x90
RX ACK
TX 0x00
RX ACK
Repeated START
TX 0x91
RX ACK
RX 0x42
TX NACK
STOP
其中:
0x90:地址 0x48 + WRITE
0x00:设备 ID 寄存器地址
0x91:地址 0x48 + READ
0x42:设备 ID 数据
四、为什么先使用 WRITE 方向
读取寄存器之前,主机需要先把寄存器地址发送给从机。
例如:
0x02
表示主机准备读取:
TEMP_HIGH
虽然最终目标是读取数据,但发送寄存器地址这一步仍然是:
主机向从机发送数据
因此必须先使用:
设备地址 + WRITE
五、什么是寄存器指针
设备内部定义:
uint8_t register_pointer;
uint8_t register_pointer_valid;
register_pointer 表示当前准备访问的寄存器位置。
例如主机发送:
0x02
设备保存:
device->register_pointer = 0x02U;
device->register_pointer_valid = 1U;
之后进入 READ 方向时,从机从:
device->registers[
device->register_pointer]
返回数据。
此时读取结果是:
registers[0x02] = 0x09
六、为什么需要 register_pointer_valid
设备刚完成初始化时,主机还没有指定寄存器地址。
此时:
register_pointer_valid = 0U;
表示:
当前寄存器指针不能用于读取
主机发送合法寄存器地址后:
register_pointer_valid = 1U;
这样可以阻止错误流程:
没有设置寄存器地址
直接从设备读取数据
否则设备无法判断应该返回哪个寄存器。
七、什么是 Repeated START
Repeated START 中文称为:
重复起始信号
普通 START 发生在总线空闲时:
started = 0
Repeated START 发生在事务尚未通过 STOP 结束时:
started = 1
Day26 新增接口:
int i2c_repeated_start(
I2cBus *bus);
调用成功后:
started 保持为 1
addressed 清零
current_address 清零
等待重新发送设备地址
repeated_start_count 加 1
八、普通 START 与 Repeated START 的区别
普通 START:
总线原来处于空闲状态
开始一个全新的事务
Repeated START:
当前事务尚未结束
主机不发送 STOP
直接重新产生 START
调用条件分别是:
i2c_start(&bus);
要求:
started = 0
而:
i2c_repeated_start(&bus);
要求:
started = 1
九、为什么 Repeated START 后要重新发送地址
在寄存器读取第一阶段:
START
设备地址 + WRITE
寄存器地址
此时总线方向为:
WRITE
接下来主机准备读取数据,因此需要切换为:
READ
Repeated START 后必须重新发送:
设备地址 + READ
因此代码会清除旧状态:
bus->addressed = 0U;
bus->current_address = 0U;
然后重新调用:
i2c_send_address(
bus,
device,
address,
I2C_DIRECTION_READ,
&response);
十、为什么不用 STOP 再 START
也可以设计为:
START
设备地址 + WRITE
寄存器地址
STOP
START
设备地址 + READ
数据
STOP
但更常见的是:
START
设备地址 + WRITE
寄存器地址
Repeated START
设备地址 + READ
数据
STOP
Repeated START 的优势:
整个寄存器读取保持为一个连续事务
中间不会释放总线
其他主机不能插入通信
寄存器指针状态不容易被破坏
符合许多设备数据手册规定的时序
十一、本次模拟的设备寄存器
Day26 模拟一个地址为 0x48 的温度传感器。
| 地址 | 寄存器 | 初始值 | 权限 |
|---|---|---|---|
0x00 |
DEVICE_ID | 0x42 |
只读 |
0x01 |
STATUS | 0x01 |
只读 |
0x02 |
TEMP_HIGH | 0x09 |
只读 |
0x03 |
TEMP_LOW | 0xE6 |
只读 |
0x04 |
CONFIG | 0x00 |
可读写 |
代码定义:
#define I2C_DEVICE_REG_ID 0x00U
#define I2C_DEVICE_REG_STATUS 0x01U
#define I2C_DEVICE_REG_TEMP_HIGH 0x02U
#define I2C_DEVICE_REG_TEMP_LOW 0x03U
#define I2C_DEVICE_REG_CONFIG 0x04U
十二、设备 ID 读取
设备 ID:
#define I2C_DEVICE_EXPECTED_ID 0x42U
读取流程:
START
0x90
ACK
0x00
ACK
Repeated START
0x91
ACK
0x42
NACK
STOP
程序调用:
i2c_register_device_read_register(
&bus,
&device,
I2C_DEVICE_REG_ID,
&device_id);
运行结果:
[DEVICE] ID register = 0x42
[CHECK] Device ID matched
设备 ID 常用于验证:
设备地址是否正确
通信是否正常
芯片型号是否匹配
驱动是否访问了正确设备
十三、状态寄存器读取
状态寄存器:
0x01
本次定义 bit0 为温度数据就绪标志:
#define I2C_DEVICE_STATUS_READY 0x01U
检查代码:
if((status &
I2C_DEVICE_STATUS_READY) != 0U)
{
/* 温度数据已经准备完成 */
}
运行结果:
[DEVICE] Status register = 0x01
[CHECK] Temperature data is ready
十四、配置寄存器写入流程
CONFIG 寄存器地址:
0x04
写入数据:
0xA5
完整流程:
START
↓
设备地址 + WRITE
↓
ACK
↓
寄存器地址 0x04
↓
ACK
↓
配置数据 0xA5
↓
ACK
↓
STOP
总线上的三个字节:
0x90
0x04
0xA5
程序调用:
i2c_register_device_write_register(
&bus,
&device,
I2C_DEVICE_REG_CONFIG,
0xA5U);
运行结果:
[CONFIG] Before write = 0x00
[CONFIG] Write value = 0xA5
[CONFIG] After write = 0xA5
[CHECK] Configuration write verified
十五、为什么写事务完成后才更新寄存器
程序不是在收到数据 0xA5 后立即修改寄存器,而是在完整事务成功后才执行:
device->registers[
register_address] =
register_value;
需要保证以下步骤全部成功:
START 成功
设备地址发送成功
地址 ACK
寄存器地址发送成功
寄存器地址 ACK
数据发送成功
数据 ACK
STOP 成功
这样可以避免:
I²C 传输失败
但软件中的配置寄存器已经被错误修改
十六、连续多字节读取
温度原始值占两个寄存器:
0x02:TEMP_HIGH
0x03:TEMP_LOW
连续读取过程:
START
↓
地址 + WRITE
↓
寄存器地址 0x02
↓
Repeated START
↓
地址 + READ
↓
读取高字节 0x09
↓
主机 ACK
↓
读取低字节 0xE6
↓
主机 NACK
↓
STOP
程序调用:
i2c_register_device_read_temperature(
&bus,
&device,
&temperature_raw,
&temperature_c);
十七、中间字节为什么发送 ACK
读取第一个温度字节:
TEMP_HIGH = 0x09
由于后面还需要继续读取:
TEMP_LOW
所以主机发送:
ACK
表示:
这个字节已经收到
请继续发送下一个字节
十八、最后一个字节为什么发送 NACK
读取第二个字节:
TEMP_LOW = 0xE6
它是最后一个字节,所以主机发送:
NACK
表示:
当前字节已经收到
本次读取结束
不要再继续发送
之后主机发送 STOP。
因此:
中间字节后发送 ACK
最后字节后发送 NACK
十九、连续读取判断代码
核心逻辑:
if((index + 1U) < length)
{
master_response =
I2C_RESPONSE_ACK;
}
else
{
master_response =
I2C_RESPONSE_NACK;
}
假设:
length = 2
第一次循环:
index = 0
index + 1 = 1
1 < 2
发送 ACK
第二次循环:
index = 1
index + 1 = 2
2 < 2 不成立
发送 NACK
二十、寄存器指针自动递增
读取第一个字节前:
register_pointer = 0x02
设备返回:
registers[0x02] = 0x09
之后执行:
device->register_pointer++;
此时:
register_pointer = 0x03
下一次读取返回:
registers[0x03] = 0xE6
这种机制称为:
Auto Increment
寄存器地址自动递增
真实设备是否支持自动递增,需要查看芯片数据手册。
二十一、温度高低字节拆分
默认温度原始值:
#define I2C_DEVICE_DEFAULT_TEMP_RAW 2534U
十六进制:
2534 = 0x09E6
高字节提取:
(uint8_t)(
(temperature_raw >> 8) &
0x00FFU);
结果:
0x09
低字节提取:
(uint8_t)(
temperature_raw &
0x00FFU);
结果:
0xE6
二十二、温度高低字节组合
读取结果:
high_byte = 0x09
low_byte = 0xE6
组合代码:
temperature_raw =
(uint16_t)(
((uint16_t)high_byte << 8) |
(uint16_t)low_byte);
计算:
0x09 << 8 = 0x0900
0x0900 | 0x00E6 = 0x09E6
得到:
temperature_raw = 2534
二十三、温度换算
本次规定:
一个原始单位 = 0.01℃
代码:
temperature_c =
(float)temperature_raw /
100.0f;
计算:
2534 ÷ 100.0 = 25.34℃
运行结果:
[TEMPERATURE] High byte = 0x09
[TEMPERATURE] Low byte = 0xE6
[TEMPERATURE] Raw value = 2534
[TEMPERATURE] Celsius = 25.34 C
二十四、I²C 设备结构体设计
Day26 定义:
typedef struct
{
I2cDevice bus_device;
uint8_t registers[
I2C_DEVICE_REGISTER_COUNT];
uint8_t register_pointer;
uint8_t register_pointer_valid;
uint32_t register_read_count;
uint32_t register_write_count;
} I2cRegisterDevice;
各成员作用:
bus_device:
复用基础 I²C 地址和单字节通信功能
registers:
模拟设备内部寄存器
register_pointer:
当前寄存器位置
register_pointer_valid:
寄存器指针是否有效
register_read_count:
成功读取的寄存器数量
register_write_count:
成功写入的寄存器数量
二十五、为什么复用 Day25 的 I2cDevice
Day25 已经定义:
typedef struct
{
uint8_t address;
uint8_t stored_data;
uint8_t read_data;
} I2cDevice;
它已经能够完成:
设备地址匹配
地址 ACK/NACK
单字节发送
单字节接收
Day26 通过:
I2cDevice bus_device;
复用这些能力,再增加寄存器功能。
这样不需要把底层地址和 ACK/NACK 逻辑重新写一遍。
二十六、单寄存器读取复用连续读取接口
单寄存器读取函数:
int i2c_register_device_read_register(
I2cBus *bus,
I2cRegisterDevice *device,
uint8_t register_address,
uint8_t *register_value);
内部直接调用:
return i2c_register_device_read_registers(
bus,
device,
register_address,
register_value,
1U);
也就是:
单寄存器读取
等于
连续读取一个字节
这种写法减少了重复代码。
二十七、连续读取范围检查
当前合法寄存器范围:
0x00~0x04
例如从 0x03 开始读取 3 个字节:
0x03
0x04
0x05
其中 0x05 越界。
程序计算剩余寄存器数量:
remaining_registers =
I2C_DEVICE_REGISTER_COUNT -
start_register;
然后判断:
if(length > remaining_registers)
{
return 0;
}
非法操作在产生 START 前被拒绝,因此不会改变通信计数。
二十八、只读寄存器保护
当前只有:
CONFIG,地址 0x04
允许写入。
权限检查:
int i2c_device_register_is_writable(
uint8_t register_address)
{
if(register_address ==
I2C_DEVICE_REG_CONFIG)
{
return 1;
}
return 0;
}
以下寄存器不能写:
DEVICE_ID
STATUS
TEMP_HIGH
TEMP_LOW
程序尝试向 DEVICE_ID 写入 0x99 时,操作被拒绝。
设备 ID 仍然保持:
0x42
二十九、非法操作保护
本次测试了:
访问非法寄存器 0x20
连续读取越界
写入只读 DEVICE_ID
读取长度为 0
总线忙时开始寄存器读取
运行结果:
[EXPECTED ERROR] Invalid register 0x20 rejected
[EXPECTED ERROR] Out-of-range continuous read rejected
[EXPECTED ERROR] Read-only device ID write rejected
[EXPECTED ERROR] Zero-length read rejected
[EXPECTED ERROR] Register read rejected while bus busy
三十、失败操作为什么不改变计数
参数、地址和权限都在 START 前检查。
因此失败操作不会:
产生 START
产生 Repeated START
产生 STOP
增加 ACK 或 NACK
增加字节计数
增加寄存器读写计数
修改设备寄存器
运行结果:
[CHECK] All counters unchanged
三十一、总线忙状态保护
完整设备事务要求总线起始状态为空闲:
if(bus->started != 0U)
{
return 0;
}
测试中先手动调用:
i2c_start(&test_bus);
模拟总线正在被其他操作占用。
此时设备读取被拒绝:
[EXPECTED ERROR] Register read rejected while bus busy
最后调用 STOP 恢复总线空闲。
三十二、失败事务恢复
设备层定义内部函数:
static void
i2c_register_device_abort_transaction(
I2cBus *bus,
I2cRegisterDevice *device);
当事务执行到一半失败时:
将寄存器指针设为无效
如果事务已经开始,则尝试发送 STOP
目的:
避免总线一直处于忙状态
避免后续事务全部失败
避免继续使用错误寄存器指针
三十三、为什么辅助函数使用 static
定义:
static void
i2c_register_device_abort_transaction(...);
static 表示该函数只能在:
i2c_device.c
内部使用。
它属于设备驱动内部实现,不需要暴露给:
main.c
其他模块
三十四、软件分层
Day26 将 I²C 工程分为三层:
main.c
应用测试层
↓
i2c_device.c
设备寄存器协议层
↓
i2c.c
I²C 总线层
main.c
负责:
初始化总线和设备
执行设备测试
打印结果
检查最终统计
i2c_device.c
负责:
设备寄存器表
寄存器地址
寄存器指针
寄存器读写权限
Repeated START 读取流程
连续多字节读取
温度处理
i2c.c
负责:
START
Repeated START
STOP
设备地址发送
读写方向
ACK/NACK
单字节收发
总线状态和统计
三十五、实际运行结果
执行:
cd /root/Embedded_14Days/day26
make clean
make
make run
运行结果:
===== Day26 I2C Register Protocol Test =====
[I2C CONFIG] Clock = 100000 Hz
[DEVICE CONFIG] 7-bit address = 0x48
[DEVICE CONFIG] Register count = 5
[DEVICE CONFIG] Expected ID = 0x42
----- Test 1: Repeated START State -----
[EXPECTED ERROR] Repeated START rejected on idle bus
[I2C] Normal START generated
[I2C] Repeated START generated
[STATE] Bus started = YES
[STATE] Addressed = NO
[CHECK] Repeated START state is correct
[I2C] STOP generated
[CHECK] Temporary bus returned to idle
----- Test 2: Device Information -----
[DEVICE] ID register = 0x42
[CHECK] Device ID matched
[DEVICE] Status register = 0x01
[CHECK] Temperature data is ready
[COUNT] Register reads = 2
[COUNT] Repeated START = 2
----- Test 3: Continuous Temperature Read -----
[TEMPERATURE] High byte = 0x09
[TEMPERATURE] Low byte = 0xE6
[TEMPERATURE] Raw value = 2534
[TEMPERATURE] Celsius = 25.34 C
[CHECK] Temperature value matched
[COUNT] Register reads = 4
[COUNT] ACK count = 10
[COUNT] NACK count = 3
----- Test 4: Configuration Register -----
[CONFIG] Before write = 0x00
[CONFIG] Write value = 0xA5
[CONFIG] After write = 0xA5
[CHECK] Configuration write verified
[COUNT] Register reads = 6
[COUNT] Register writes = 1
----- Test 5: Invalid Register Operations -----
[EXPECTED ERROR] Invalid register 0x20 rejected
[EXPECTED ERROR] Out-of-range continuous read rejected
[EXPECTED ERROR] Read-only device ID write rejected
[EXPECTED ERROR] Zero-length read rejected
[CHECK] Device ID remains 0x42
[CHECK] All counters unchanged
----- Test 6: Busy Bus Protection -----
[I2C] Temporary bus marked busy
[EXPECTED ERROR] Register read rejected while bus busy
[I2C] Temporary bus released
[CHECK] Busy bus protection verified
----- Final I2C Register Status -----
[FINAL] START count = 6
[FINAL] Repeated START count = 5
[FINAL] STOP count = 6
[FINAL] ACK count = 19
[FINAL] NACK count = 5
[FINAL] Byte count = 24
[FINAL] Register reads = 6
[FINAL] Register writes = 1
[FINAL] Device selected = NO
[FINAL] Bus started = NO
[FINAL] Config register = 0xA5
[FINAL CHECK] All I2C register tests passed
===== I2C Register Test Finished =====
编译过程中没有出现:
warning
error
三十六、通信计数分析
主总线共执行:
5 次寄存器读取事务
1 次寄存器写入事务
所以:
普通 START = 6
STOP = 6
五次读取都需要 Repeated START:
Repeated START = 5
配置寄存器写入不需要切换方向,因此不使用 Repeated START。
三十七、为什么 Byte count 是 24
四次单寄存器读取,每次包含:
写地址字节
寄存器地址
读地址字节
寄存器数据
所以:
4 × 4 = 16
一次温度连续读取包含:
写地址字节
寄存器地址
读地址字节
温度高字节
温度低字节
所以:
5
一次配置寄存器写入包含:
写地址字节
寄存器地址
配置数据
所以:
3
总计:
16 + 5 + 3 = 24
三十八、为什么 ACK count 是 19
四次单寄存器读取,每次有:
写地址 ACK
寄存器地址 ACK
读地址 ACK
所以:
4 × 3 = 12
一次温度连续读取:
写地址 ACK
寄存器地址 ACK
读地址 ACK
高字节后的主机 ACK
所以:
4
一次配置写入:
写地址 ACK
寄存器地址 ACK
配置数据 ACK
所以:
3
总计:
12 + 4 + 3 = 19
三十九、为什么 NACK count 是 5
共有五次读取事务:
读取 DEVICE_ID
读取 STATUS
读取温度
读取写入前 CONFIG
读取写入后 CONFIG
每次读取最后一个字节后,主机都发送 NACK。
所以:
NACK count = 5
这些 NACK 都表示正常结束读取。
四十、Day25 与 Day26 的区别
Day25 重点:
设备地址
单字节写入
单字节读取
ACK 和 NACK
基本流程:
START
Address
Data
STOP
Day26 重点:
内部寄存器
寄存器指针
Repeated START
连续读取
寄存器权限
典型读取流程:
START
Address + WRITE
Register Address
Repeated START
Address + READ
Data
STOP
四十一、SPI 与 I²C 寄存器读取对比
SPI 寄存器读取:
CS LOW
发送读命令和寄存器地址
发送 Dummy Byte
读取寄存器数据
CS HIGH
I²C 寄存器读取:
START
设备地址 + WRITE
寄存器地址
Repeated START
设备地址 + READ
读取寄存器数据
STOP
SPI 通过:
CS
选择设备。
I²C 通过:
设备地址
选择设备。
四十二、今日收获
通过 Day26 学习,我掌握了:
- I²C 设备内部寄存器;
- 寄存器地址;
- 寄存器指针;
- 寄存器指针有效状态;
- 普通 START 和 Repeated START;
- Repeated START 的使用条件;
- Repeated START 后重新发送设备地址;
- 先写寄存器地址再读取数据;
- 单寄存器读取;
- 配置寄存器写入;
- 连续多字节读取;
- 中间字节 ACK;
- 最后字节 NACK;
- 寄存器地址自动递增;
- 温度高低字节拆分;
- 温度高低字节组合;
- 温度值换算;
- 只读与可写寄存器;
- 非法寄存器保护;
- 连续读取越界保护;
- 零长度读取保护;
- 总线忙状态保护;
- 失败事务恢复;
- 完整事务成功后再修改寄存器;
- 应用层、设备层和总线层分层;
- 通信事务数与寄存器读取数的区别;
- START、Repeated START、STOP、ACK、NACK 和字节计数。
四十三、下一步计划
下一天将学习:
中断基础与事件驱动
计划内容:
轮询和中断的区别
中断请求
中断使能
中断标志
中断服务函数 ISR
volatile
共享变量
中断计数
按键中断模拟
UART 接收中断模拟
主循环事件处理
中断中只做必要工作
重点理解:
中断负责快速响应和记录事件
主循环负责处理耗时任务

更多推荐


所有评论(0)