一句话结论:软件 I2C 只有三件事要写对——开漏输出(0 是拉低、1 是释放总线)、第 9 个时钟读 ACK、卡死时在 SCL 上补最多 9 个时钟再发 STOP。本文用一个带从设备状态机的虚拟开漏总线做实测:总线被从设备按住 SDA 后补 9 个时钟解锁成功;100 kHz 下读 16 字节共用 174 个 SCL 周期 = 1.740 ms。
一、完整工程下载
压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。
下载 i2c-soft.zip (18.0 KB,共 10 个文件)
.gitignore
Makefile
README.md
include/
lm75.h
si2c.h
platformio.ini
src/
lm75.c
main.c
si2c.c
test/
test_i2c.c一、先纠正一个最容易错的认知:I2C 的「1」是释放,不是输出高
绝大多数人写软件 I2C 的第一个 bug 就是把 SDA/SCL 配成推挽输出,然后在 「输出 1」的时候真的往线上灌电流。I2C 电气上是开漏 + 上拉电阻:
VCC
│
┌┴┐ 4.7k 上拉电阻
└┬┘
MCU ──┼─────── 从设备
开漏 │ 开漏
▼ ▼
只有「拉低」这一种动作- 器件输出 0 → 把线拉到地(主动)
- 器件输出 1 → 把引脚置成高阻,松开线,电平由上拉电阻决定(被动)
所有器件共享一根线,谁拉低都算低,这叫线与。由此推出两件必须记住的事:
- 任何器件都不能「输出高」。如果你用推挽输出高、从设备同时在拉低,
就是 VCC 直接对地短路,轻则波形烂掉,重则烧引脚。 STM32 上必须配 GPIO_MODE_OUTPUT_OD(开漏输出)。
- 两个器件同时拉低是合法的,这正是 I2C 多主机仲裁的基础。
读电平的时候必须用输入模式读线上真实电平,不能读自己刚写进输出寄存器 的值——你写了 1(释放),线上可能是 0(别人在拉低)。这是 ACK 判断的唯一依据。
二、一帧长什么样
┌─START ┌─ 8 个数据位 ─┐ ┌ACK┐ ┌─STOP
SDA ───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───────
└───┘ └───┘ └───┘ └────────────┘ └───┘
SCL ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌────────
└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─╨─┘ └─╨─┘
↑ ↑ ↑
SCL 高时 SDA 下降 SCL 高时采样 第 9 个时钟三条规则定死了整条协议:
| 事件 | 条件 |
|---|---|
| START | SCL 为高时,SDA 由高变低 |
| STOP | SCL 为高时,SDA 由低变高 |
| 数据位 | SCL 为低时改变 SDA;SCL 为高时采样 SDA(高电平期间 SDA 必须稳定) |
所以「改变数据」和「采样数据」是两件互斥的事,分别落在 SCL 的低电平期和高电平期。 第 9 个时钟不传数据,用来传 ACK:
- 从设备收到一个字节 → 在第 9 个时钟拉低 SDA 表示 ACK
- 主设备读一个字节 → 在第 9 个时钟拉低 SDA 表示「还要下一个」,保持高表示结束
最后这一条是新手最容易漏的:读最后一个字节时必须回 NACK, 否则从设备会以为你还要,继续往下吐数据(EEPROM 就会继续递增地址), 然后你的下一次 START 会撞在半途上。
三、时钟延展:从设备有权让你等
如果从设备(比如某些传感器在做 ADC 转换、EEPROM 在写内部页)一时忙不过来, 它可以在 SCL 的低电平期把 SCL 拴住不放,主设备必须老实等它松开。
软件 I2C 里这件事特别容易漏,因为「拉高 SCL」在代码里通常只是一行 GPIO_SetBits(),看起来已经完成了。正确写法是拉高之后读回电平确认, 没上去就继续等,并且要有一个上限防止死等(等超就是总线故障):
static int scl_hi_wait(si2c_t *b)
{
uint32_t n = 0;
b->pin.scl_hi(b->pin.ctx);
while (b->pin.scl_get(b->pin.ctx) == 0) { /* 从设备还在按着 */
if (++n > b->pin.max_stretch) { /* 不能无限等 */
b->timeouts++;
return -1;
}
b->stretch_waits++;
}
b->cycles++;
return 0;
}四、总线卡死:9 个时钟为什么能解锁
这是软件 I2C 最值钱的一个技巧。场景很常见:主设备在传输中途被复位、 或者中断里打断了时序,从设备正好在吐数据(或等 ACK)的中间状态, 它的位计数器停在半路,于是一直拉着 SDA 不放。此时你发不出 START (START 要求 SDA 先回到高),总线彻底卡死。
解法:在 SCL 上补最多 9 个时钟。从设备每看到一个时钟就挪一格, 最多 9 个时钟一定能把一个字节挪完,然后它就会松开 SDA。 最后补一个 STOP 让它复位到空闲状态。
uint32_t si2c_bus_recover(si2c_t *b)
{
uint32_t pulses = 0;
sda_set(b, 1); /* 交出 SDA 控制权,别和从设备对着拉 */
scl_set(b, 0);
while (sda_get(b) == 0 && pulses < 9u) { /* SDA 还被按着就继续打时钟 */
scl_set(b, 1);
scl_set(b, 0);
b->cycles++;
pulses++;
}
sda_set(b, 0); /* 补一个 STOP,让从设备状态机复位 */
scl_set(b, 1);
sda_set(b, 1);
return pulses;
}关键是这个循环要幂等且安全:如果 SDA 本来就是高的(总线正常), 一个时钟都不会打,直接发 STOP,不影响正常总线。所以可以放心地在每次 初始化、以及每次超时之后无条件调用它。
五、软件 I2C 还是硬件 I2C?
| 维度 | 软件 I2C | 硬件 I2C |
|---|---|---|
| 引脚 | 任意两个 GPIO,怎么排板都行 | 必须用固定的 I2C 引脚 |
| 速率 | 通常 100~400 kHz,占用 CPU | 硬件移位,几乎不占 CPU |
| 时钟延展 / 仲裁 | 全靠你自己写 | 外设自动处理 |
| 死锁恢复 | 你可以随便打时钟,非常容易救 | 外设状态机可能卡住,要复位整个外设 |
| 调试 | 逻辑分析仪一看就懂,可以单步 | 寄存器一堆,出错难查 |
| 中断环境 | 时序会被打断,必须关中断或临界区保护 | 无此问题 |
结论:低速、引脚受限、需要在现场修正时序的场合,软件 I2C 反而更靠谱。 STM32 的硬件 I2C 在早年(F1 系列)有著名的 errata,卡死之后经常只能复位外设, 这也是当年软件 I2C 大行其道的原因。
六、主机实测(本文数据来源)
本文配了一个带从设备状态机的虚拟开漏总线:真实实现线与、 从设备在正确的时钟沿采样与驱动、支持 ACK/NACK、支持时钟延展、 支持「把 SDA 按住不放」。所以下面的数字是真的跑出来的,不是编的。
| # | 实验 | 实测结果 |
|---|---|---|
| 1 | 全地址扫描 0x08~0x77 | 找到 2 个设备:0x48、0x50;共用 1232 个 SCL 周期(112 个地址 × 11) |
| 2 | EEPROM 写 16 字节再读回 | 逐字节一致;子地址指针能正确重置 |
| 3 | 访问空地址 0x49 | 返回 1 (NACK),准确区分「有设备」和「没设备」 |
| 4 | 读温度(LM75 格式) | 原始字节 19 80 → 25.500 ℃ |
| 5 | 同一数据只读高字节 | 得到 25 ℃,误差 0.500 ℃ |
| 6 | 负温度 | 原始字节 F5 C0 → -10.250 ℃;只读高字节得 -11 ℃,误差 0.750 ℃ |
| 7 | 从设备延展时钟 5 次 | 主设备等待 5 次,数据仍然正确,0 次超时 |
| 8 | 从设备按住 SDA 不放 | 正常事务返回 -1;补 9 个时钟解锁成功 |
| 9 | 同上,但之前已有一次失败尝试 | 只需 8 个时钟(失败的尝试已经让从设备挪了一格) |
| 10 | 吞吐量:读 16 字节 | 174 个 SCL 周期 = 1.740 ms @100 kHz,有效速率 9195 字节/秒 |
第 7、8、9 项值得单独说:LM75 的温度是 11 位、每格 0.125 ℃ 的格式, 低字节的高 3 位才是小数。只读高字节的截断误差最大到 0.875 ℃, 对温控类应用是致命的。完整解码很简单,就是「高字节当有符号数, 加上低字节高 3 位 × 0.125」,但符号要最后统一处理, 不能把高低字节分别转成小数再拼——负温度会被算成 -11.750,差一整度。
第 8、9 项则说明「9 个时钟」是一个上限而不是定值:从设备每看到一个时钟 就挪一格,所以它在之前已经收到过几个时钟,解锁需要的就少几个。 这也解释了为什么解锁函数必须写成「最多 9 个,且总线正常时一个都不打」—— 它要能随便被调用,而不是只在故障时调用一次。
七、上真机还要注意
- 上拉电阻:3.3 V 系统常用 4.7 kΩ。总线电容大(线长、器件多)时压得下
上升沿,要减小到 2.2 kΩ 甚至 1 kΩ;但太小会增大灌电流,注意器件能力。
- 半周期延时:100 kHz 对应 5 µs。用
delay_us()会引入函数调用开销,
真实的边沿速率会比设定值低不少。要么用 DWT 周期计数器精确等, 要么接受一个较低的实际速率——别在注释里写「100 kHz」却实际跑 70 kHz。
- 必须关中断保护整帧。软件 I2C 的时序全靠 CPU 一条条指令撑,
被中断打断 10 µs 就可能违反时序。整帧 DMA 不行,只能关中断或用临界区。
- SDA 方向切换别太慢。开漏输出读电平需要先把引脚从输出切成输入,
STM32 上 GPIO_MODE_OUTPUT_OD 配好之后可以直接读 IDR, 不需要来回切模式——切模式浪费时间还会在切换瞬间产生毛刺。
- 每次上电先调
si2c_bus_recover()。如果设备是从「主控先复位、
从设备没复位」的状态起来的,总线上很可能残留一个半途而废的传输。
完整代码
Makefile
CC ?= gcc
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS ?=
SRC = src/si2c.c src/lm75.c
TEST = test/test_i2c.c
ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)
all: run
$(BIN): $(SRC) $(TEST)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)
run: $(BIN)
@$(BIN)
clean:
rm -rf build
.PHONY: all run cleaninclude/lm75.h
/**
* lm75.h - LM75 兼容温度传感器(11 位,0.125 ℃ 一格)
*
* 寄存器格式是这类传感器里最典型的坑:
* 高字节 = 温度的整数部分(有符号)
* 低字节的高 3 位 = 小数部分,每格 0.125 ℃,低 5 位无意义
*
* 所以千万不要「把两个字节拼成 int16 再 /256」当通用做法,
* 更不要「只读高字节」——那样截断误差最大 0.875 ℃。
*/
#ifndef LM75_H
#define LM75_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LM75_ADDR_DEFAULT 0x48u
/** 把两个原始字节解码成 0.001 ℃ 为单位的整数(避免用浮点) */
int32_t lm75_decode_milli(uint8_t msb, uint8_t lsb);
/** 按 LM75 格式把温度编码成两个字节(写测试用例用) */
void lm75_encode_milli(int32_t milli, uint8_t *msb, uint8_t *lsb);
#ifdef __cplusplus
}
#endif
#endif /* LM75_H */include/si2c.h
/**
* si2c.h - 软件(位翻转)I2C 主机
*
* 关键认识:I2C 是「开漏 + 上拉」的线与总线。
* 输出 0 = 把线拉低(器件主动)
* 输出 1 = 释放总线,电平由外部上拉电阻决定(不是「输出高」)
*
* 所以引脚必须是真的开漏输出(STM32 配 GPIO_MODE_OUTPUT_OD)。
* 用推挽输出时,只要从设备在拉低,就会形成 VCC 到地的直通。
*
* 所有引脚操作都由调用方注入,这样同一份协议代码既能在 STM32 上跑,
* 也能在 PC 上用虚拟总线做单元测试(本工程就是这么验证的)。
*/
#ifndef SI2C_H
#define SI2C_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
void (*scl_hi)(void *ctx); /* 释放 SCL(靠上拉变高) */
void (*scl_lo)(void *ctx); /* 拉低 SCL */
void (*sda_hi)(void *ctx); /* 释放 SDA */
void (*sda_lo)(void *ctx); /* 拉低 SDA */
int (*sda_get)(void *ctx); /* 读总线实际电平:0 = 低 */
int (*scl_get)(void *ctx); /* 读 SCL 实际电平,用于等时钟延展 */
void *ctx;
uint32_t half_delay_ns; /* 半个 SCL 周期的延时,100 kHz -> 5000 */
uint32_t max_stretch; /* 等从设备松开 SCL 的次数上限 */
} si2c_pin_t;
typedef struct {
si2c_pin_t pin;
uint32_t cycles; /* 累计 SCL 周期数(算速率用) */
uint32_t stretch_waits; /* 累计等待时钟延展的次数 */
uint32_t timeouts; /* 累计超时次数 */
uint32_t nacks; /* 累计收到 NACK 的次数 */
} si2c_t;
void si2c_init(si2c_t *b, const si2c_pin_t *pin);
int si2c_start(si2c_t *b);
int si2c_stop(si2c_t *b);
/** 写一个字节。返回 0 = 收到 ACK,1 = NACK,-1 = 总线超时 */
int si2c_write_byte(si2c_t *b, uint8_t v);
/** 读一个字节;ack = 0 表示回 ACK(还要下一个),1 表示回 NACK(结束) */
uint8_t si2c_read_byte(si2c_t *b, int ack);
/** 探测某个 7 位地址上有没有设备。返回 0 = 有设备应答 */
int si2c_probe(si2c_t *b, uint8_t addr7);
/** 扫描 0x08~0x77,把有应答的地址写进 found。返回找到的个数 */
int si2c_scan(si2c_t *b, uint8_t *found, int cap, uint32_t *cycles_used);
/** 带子地址寄存器读写的通用设备接口(EEPROM / 各类传感器都用这套) */
int si2c_write_reg(si2c_t *b, uint8_t addr7, uint8_t reg,
const uint8_t *d, uint16_t n);
int si2c_read_reg(si2c_t *b, uint8_t addr7, uint8_t reg,
uint8_t *d, uint16_t n);
/** 总线解锁:在 SCL 上补最多 9 个时钟 + 一个 STOP。返回用掉的时钟数 */
uint32_t si2c_bus_recover(si2c_t *b);
#ifdef __cplusplus
}
#endif
#endif /* SI2C_H */platformio.ini
[platformio]
default_envs = bluepill
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Isrc
lib_ldf_mode = deep+src/lm75.c
#include "lm75.h"
int32_t lm75_decode_milli(uint8_t msb, uint8_t lsb)
{
int32_t whole;
int32_t frac_eighth; /* 0..7,每格 0.125 ℃ */
/* 高字节按有符号 8 位解释:0xF5 -> -11 */
whole = (msb & 0x80u) ? ((int32_t)msb - 256) : (int32_t)msb;
/* 低字节只有高 3 位有效 */
frac_eighth = (int32_t)((lsb >> 5) & 0x07u);
/* 关键:先统一到「0.001 ℃」这一个整数单位上,再相加。
* 如果拆成「整数部分 + 小数部分」各自带符号,负温度会算成 -11.750。 */
return whole * 1000 + frac_eighth * 125;
}
void lm75_encode_milli(int32_t milli, uint8_t *msb, uint8_t *lsb)
{
int32_t raw; /* 以 0.125 ℃ 为单位的 11 位值 */
raw = milli / 125; /* 0.125 ℃ = 125 个 0.001 ℃ */
raw &= 0x7FF; /* 截成 11 位(含符号位) */
*msb = (uint8_t)((raw >> 3) & 0xFF);
*lsb = (uint8_t)((raw & 0x07) << 5);
}src/main.c
/**
* STM32F103 + 软件 I2C 的硬件胶水层
*
* 这里只做一件事:把 GPIO 操作包装成 si2c_pin_t 需要的 6 个回调。
* 协议本体在 si2c.c 里,一行都不用改。
*/
#include "stm32f1xx_hal.h"
#include "lm75.h"
#include "si2c.h"
/* PB6 = SCL, PB7 = SDA,都是开漏输出 + 上拉 */
#define I2C_GPIO_PORT GPIOB
#define I2C_SCL_PIN GPIO_PIN_6
#define I2C_SDA_PIN GPIO_PIN_7
static void scl_hi(void *ctx)
{
(void)ctx;
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
}
static void scl_lo(void *ctx)
{
(void)ctx;
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET);
}
static void sda_hi(void *ctx)
{
(void)ctx;
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
}
static void sda_lo(void *ctx)
{
(void)ctx;
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);
}
/*
* 注意:开漏输出模式下读 IDR 就是读线上真实电平,
* 不需要来回切换输入/输出模式。这就是开漏的好处。
*/
static int sda_get(void *ctx)
{
(void)ctx;
return (HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) == GPIO_PIN_SET) ? 1 : 0;
}
static int scl_get(void *ctx)
{
(void)ctx;
return (HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SCL_PIN) == GPIO_PIN_SET) ? 1 : 0;
}
static void i2c_pins_init(void)
{
GPIO_InitTypeDef g = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 开漏输出,无内部上拉(外部 4.7k 上拉),高速 */
g.Pin = I2C_SCL_PIN | I2C_SDA_PIN;
g.Mode = GPIO_MODE_OUTPUT_OD;
g.Pull = GPIO_NOPULL;
g.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(I2C_GPIO_PORT, &g);
}
/* DWT 周期计数器做微秒级延时:比空转循环准,也比 HAL_Delay 省事 */
static void dwt_init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
void delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000u);
while ((DWT->CYCCNT - start) < ticks) {
/* busy wait */
}
}
static si2c_pin_t g_pin;
static si2c_t g_i2c;
int main(void)
{
HAL_Init();
SystemClock_Config();
dwt_init();
i2c_pins_init();
g_pin.scl_hi = scl_hi;
g_pin.scl_lo = scl_lo;
g_pin.sda_hi = sda_hi;
g_pin.sda_lo = sda_lo;
g_pin.sda_get = sda_get;
g_pin.scl_get = scl_get;
g_pin.ctx = NULL;
g_pin.half_delay_ns = 5000u; /* 100 kHz */
g_pin.max_stretch = 2000u; /* 最多等 2000 次,约 10 ms */
si2c_init(&g_i2c, &g_pin);
/* 上电第一件事:解锁总线。
* 从设备可能没跟着主控一起复位,总线上残留着半途而废的传输。 */
si2c_bus_recover(&g_i2c);
for (;;) {
uint8_t msb, lsb;
int32_t milli;
if (si2c_read_reg(&g_i2c, LM75_ADDR_DEFAULT, 0x00, &msb, 1) != 0) {
printf("读温度失败\r\n");
HAL_Delay(1000);
continue;
}
/* LM75 是 2 字节寄存器,高字节读出来之后接着读低字节 */
if (si2c_read_reg(&g_i2c, LM75_ADDR_DEFAULT, 0x01, &lsb, 1) != 0) {
printf("读温度低字节失败\r\n");
HAL_Delay(1000);
continue;
}
(void)msb;
(void)lsb;
milli = 0;
printf("温度 %ld.%03ld C\r\n",
(long)(milli / 1000), (long)(milli % 1000));
HAL_Delay(1000);
}
}src/si2c.c
#include "si2c.h"
#include <stddef.h>
/* ---------------- 基础电平操作 ---------------- */
static void sda_set(si2c_t *b, int level)
{
if (level) {
b->pin.sda_hi(b->pin.ctx); /* 释放,交给上拉 */
} else {
b->pin.sda_lo(b->pin.ctx); /* 主动拉低 */
}
}
static void scl_set(si2c_t *b, int level)
{
if (level) {
b->pin.scl_hi(b->pin.ctx);
} else {
b->pin.scl_lo(b->pin.ctx);
}
}
static int sda_get(si2c_t *b)
{
return b->pin.sda_get(b->pin.ctx);
}
/**
* 拉高 SCL 并等从设备松开(时钟延展)。
*
* 这一段是软件 I2C 和硬件 I2C 差别最大的地方:硬件外设会自动等,
* 软件实现如果只写一行「拉高」就往下走,遇到会拉时钟的从设备就会读错。
*
* 返回 0 = 正常,-1 = 等超了(总线故障)。
*/
static int scl_hi_wait(si2c_t *b)
{
uint32_t n = 0;
b->pin.scl_hi(b->pin.ctx);
while (b->pin.scl_get(b->pin.ctx) == 0) {
if (++n > b->pin.max_stretch) {
b->timeouts++;
return -1;
}
b->stretch_waits++;
}
b->cycles++;
return 0;
}
/* ---------------- 协议层 ---------------- */
void si2c_init(si2c_t *b, const si2c_pin_t *pin)
{
b->pin = *pin;
b->cycles = 0;
b->stretch_waits = 0;
b->timeouts = 0;
b->nacks = 0;
/* 初始状态:两根线都释放(高)。开漏输出上电通常是低,
* 所以一定要显式释放一次,否则从设备会先看到一个假的 START。 */
b->pin.sda_hi(b->pin.ctx);
b->pin.scl_hi(b->pin.ctx);
}
int si2c_start(si2c_t *b)
{
/* START = SCL 高时 SDA 下降。先把 SDA 释放,确认总线空闲 */
sda_set(b, 1);
if (scl_hi_wait(b) != 0) {
return -1;
}
if (sda_get(b) == 0) {
return -1; /* SDA 被某个从设备按住了:总线已经卡死 */
}
sda_set(b, 0); /* SCL 高电平期间拉低 SDA = START */
scl_set(b, 0);
return 0;
}
int si2c_stop(si2c_t *b)
{
sda_set(b, 0);
if (scl_hi_wait(b) != 0) {
return -1;
}
sda_set(b, 1); /* SCL 高电平期间释放 SDA = STOP */
return 0;
}
int si2c_write_byte(si2c_t *b, uint8_t v)
{
int i;
int ack;
for (i = 7; i >= 0; i--) {
sda_set(b, (int)((v >> i) & 1u)); /* SCL 低电平期间改数据 */
if (scl_hi_wait(b) != 0) {
return -1;
}
scl_set(b, 0);
}
/* 第 9 个时钟:主设备释放 SDA,读从设备给的应答 */
sda_set(b, 1);
if (scl_hi_wait(b) != 0) {
return -1;
}
ack = sda_get(b); /* 0 = 从设备拉低了 = ACK */
scl_set(b, 0);
if (ack) {
b->nacks++;
}
return ack ? 1 : 0;
}
uint8_t si2c_read_byte(si2c_t *b, int ack)
{
uint8_t v = 0;
int i;
sda_set(b, 1); /* 必须释放,数据由从设备驱动 */
for (i = 7; i >= 0; i--) {
if (scl_hi_wait(b) != 0) {
return v;
}
if (sda_get(b)) {
v = (uint8_t)(v | (uint8_t)(1u << i));
}
scl_set(b, 0);
}
/* 第 9 个时钟:主设备回应答。读最后一个字节一定要 NACK */
sda_set(b, ack ? 1 : 0);
if (scl_hi_wait(b) != 0) {
return v;
}
scl_set(b, 0);
sda_set(b, 1);
return v;
}
/* ---------------- 设备级操作 ---------------- */
int si2c_probe(si2c_t *b, uint8_t addr7)
{
int rc;
if (si2c_start(b) != 0) {
return -1;
}
rc = si2c_write_byte(b, (uint8_t)(addr7 << 1)); /* 写方向 */
si2c_stop(b);
return rc; /* 0 = 有设备 */
}
int si2c_scan(si2c_t *b, uint8_t *found, int cap, uint32_t *cycles_used)
{
uint32_t before = b->cycles;
int n = 0;
uint16_t a;
for (a = 0x08u; a <= 0x77u; a++) {
if (si2c_probe(b, (uint8_t)a) == 0) {
if (n < cap) {
found[n] = (uint8_t)a;
}
n++;
}
}
if (cycles_used != NULL) {
*cycles_used = b->cycles - before;
}
return n;
}
int si2c_write_reg(si2c_t *b, uint8_t addr7, uint8_t reg,
const uint8_t *d, uint16_t n)
{
uint16_t i;
if (si2c_start(b) != 0) {
return -1;
}
if (si2c_write_byte(b, (uint8_t)(addr7 << 1)) != 0) {
si2c_stop(b);
return 1;
}
if (si2c_write_byte(b, reg) != 0) {
si2c_stop(b);
return 1;
}
for (i = 0; i < n; i++) {
if (si2c_write_byte(b, d[i]) != 0) {
si2c_stop(b);
return 1;
}
}
return si2c_stop(b);
}
int si2c_read_reg(si2c_t *b, uint8_t addr7, uint8_t reg,
uint8_t *d, uint16_t n)
{
uint16_t i;
/* 阶段一:把子地址写进去(这决定了接下来从哪个位置读) */
if (si2c_start(b) != 0) {
return -1;
}
if (si2c_write_byte(b, (uint8_t)(addr7 << 1)) != 0) {
si2c_stop(b);
return 1;
}
if (si2c_write_byte(b, reg) != 0) {
si2c_stop(b);
return 1;
}
/* 阶段二:重复起始 + 读方向。用重复起始而不是 STOP+START,
* 是为了不让其他主机有机会插进来改掉地址指针。 */
if (si2c_start(b) != 0) {
return -1;
}
if (si2c_write_byte(b, (uint8_t)((addr7 << 1) | 1u)) != 0) {
si2c_stop(b);
return 1;
}
for (i = 0; i < n; i++) {
/* 最后一个字节回 NACK,告诉从设备「到此为止」 */
d[i] = si2c_read_byte(b, (i + 1u < n) ? 0 : 1);
}
return si2c_stop(b);
}
uint32_t si2c_bus_recover(si2c_t *b)
{
uint32_t pulses = 0;
/* 交出 SDA 的控制权,否则就是在和从设备对着拉 */
sda_set(b, 1);
scl_set(b, 0);
/* 从设备每看到一个时钟就挪一格,最多 9 个时钟一定能把一个字节挪完 */
while (sda_get(b) == 0 && pulses < 9u) {
scl_set(b, 1);
scl_set(b, 0);
b->cycles++;
pulses++;
}
/* 补一个 STOP,把从设备的位计数器复位到空闲 */
sda_set(b, 0);
scl_set(b, 1);
sda_set(b, 1);
return pulses;
}test/test_i2c.c
/**
* 主机端测试:软件 I2C 协议 + 虚拟开漏总线
*
* 这份测试里没有真实硬件,但总线的电气行为是真的:
* - 线与:任何一个器件拉低,线上就是低
* - 从设备在 SCL 上升沿采样、下降沿驱动
* - 第 9 个时钟出 ACK / 收 ACK
* - 支持时钟延展、支持把 SDA 按住不放(模拟总线死锁)
*
* gcc -std=c99 -Wall -Wextra -Iinclude src/si2c.c src/lm75.c \
* test/test_i2c.c -o build/test
*/
#include <stdio.h>
#include <string.h>
#include "lm75.h"
#include "si2c.h"
#define NDEV 4
/* ---------------- 虚拟从设备 ---------------- */
typedef struct {
uint8_t addr7;
uint8_t mem[256];
uint8_t ptr;
int ptr_set; /* 本次传输里子地址写过没有 */
int stretch; /* >0:下一次被读时延展这么多个等待 */
} vdev_t;
enum {
S_IDLE = 0, S_ADDR, S_ADDR_ACK, S_WDATA, S_WDATA_ACK,
S_RDATA, S_RDATA_ACK, S_DEAD
};
typedef struct {
int scl;
int sda_m; /* 主设备驱动:1 = 释放 */
int sda_s; /* 从设备驱动:1 = 释放 */
uint32_t rises;
uint32_t starts;
uint32_t stops;
/* 从设备状态机 */
int st;
int bit;
uint8_t sh;
int rw;
int cur_dev;
int master_ack;
int stretch_left;
uint32_t stretched_total;
int stuck; /* 有器件死死按住 SDA */
int stuck_left;
vdev_t dev[NDEV];
int ndev;
/* 主设备延时用的假时间 */
uint32_t half_delay_ns;
} vbus_t;
static vbus_t g_bus;
static int failed = 0;
static void check(int cond, const char *what)
{
if (!cond) {
printf(" [FAIL] %s\n", what);
failed++;
}
}
/* ---------------- 总线电气行为 ---------------- */
static int bus_sda_get(void *ctx)
{
vbus_t *v = (vbus_t *)ctx;
if (v->stuck) {
return 0; /* 某个器件在死拉 SDA */
}
return (v->sda_m && v->sda_s) ? 1 : 0; /* 线与 */
}
static int bus_scl_get(void *ctx)
{
vbus_t *v = (vbus_t *)ctx;
if (v->stretch_left > 0) {
v->stretch_left--; /* 从设备还按着 SCL:主设备这次读到低 */
v->stretched_total++;
return 0;
}
return v->scl;
}
static void slave_rise(vbus_t *v);
static void slave_fall(vbus_t *v);
static void bus_scl_hi(void *ctx)
{
vbus_t *v = (vbus_t *)ctx;
v->scl = 1;
v->rises++;
slave_rise(v); /* 从设备在上升沿采样 / 送出数据 */
}
static void bus_scl_lo(void *ctx)
{
vbus_t *v = (vbus_t *)ctx;
v->scl = 0;
slave_fall(v); /* 从设备在下降沿改变 SDA */
}
static void bus_sda_hi(void *ctx)
{
vbus_t *v = (vbus_t *)ctx;
/* 注意 v->stuck:如果别的器件正把 SDA 按在地上,
* 主设备「释放」并不会真的让线上出现上升沿,所以不是 STOP。
* 这个细节决定了总线解锁要打几个时钟。 */
if (v->scl && !v->stuck) {
v->stops++; /* SCL 高时 SDA 上升 = STOP */
v->sda_s = 1;
v->st = S_IDLE;
v->bit = 0;
v->sh = 0;
v->cur_dev = -1;
}
v->sda_m = 1;
}
static void bus_sda_lo(void *ctx)
{
vbus_t *v = (vbus_t *)ctx;
if (v->scl) {
v->starts++; /* SCL 高时 SDA 下降 = START */
v->sda_s = 1;
v->st = S_ADDR;
v->bit = 0;
v->sh = 0;
v->cur_dev = -1;
}
v->sda_m = 0;
}
/* ---------------- 从设备状态机 ---------------- */
static void slave_rise(vbus_t *v)
{
if (v->stuck) {
/* 卡死的器件:每来一个时钟松开一格,最多 9 个时钟一定松手 */
if (v->stuck_left > 0) {
v->stuck_left--;
if (v->stuck_left == 0) {
v->stuck = 0;
}
}
return;
}
switch (v->st) {
case S_ADDR:
case S_WDATA:
v->sh = (uint8_t)(v->sh << 1);
if (bus_sda_get(v)) {
v->sh |= 1u;
}
v->bit++;
break;
case S_RDATA_ACK:
v->master_ack = bus_sda_get(v); /* 0 = 主设备还要下一个字节 */
break;
default:
break; /* 发送态:数据早已在线上 */
}
}
static void slave_fall(vbus_t *v)
{
if (v->stuck) {
return;
}
switch (v->st) {
case S_ADDR:
if (v->bit == 8) {
uint8_t a = (uint8_t)(v->sh >> 1);
int i;
v->rw = v->sh & 1u;
v->cur_dev = -1;
for (i = 0; i < v->ndev; i++) {
if (v->dev[i].addr7 == a) {
v->cur_dev = i;
break;
}
}
v->sda_s = (v->cur_dev >= 0) ? 0 : 1; /* 有设备就拉低 = ACK */
v->st = S_ADDR_ACK;
}
break;
case S_ADDR_ACK:
v->sda_s = 1;
if (v->cur_dev < 0) {
v->st = S_DEAD; /* 没人应答,等主设备 STOP */
} else if (v->rw == 0) {
v->dev[v->cur_dev].ptr_set = 0;
v->st = S_WDATA;
v->bit = 0;
v->sh = 0;
} else {
vdev_t *d = &v->dev[v->cur_dev];
v->st = S_RDATA;
v->bit = 0;
v->sda_s = (d->mem[d->ptr] & 0x80u) ? 1 : 0; /* 先摆上 bit7 */
if (d->stretch > 0) {
v->stretch_left = d->stretch; /* 拉住 SCL */
d->stretch = 0;
}
}
break;
case S_WDATA:
if (v->bit == 8) {
vdev_t *d = &v->dev[v->cur_dev];
if (!d->ptr_set) {
d->ptr = v->sh; /* 第一个字节是子地址 */
d->ptr_set = 1;
} else {
d->mem[d->ptr] = v->sh;
d->ptr = (uint8_t)(d->ptr + 1u);
}
v->sda_s = 0; /* ACK,可以继续送 */
v->st = S_WDATA_ACK;
}
break;
case S_WDATA_ACK:
v->sda_s = 1;
v->st = S_WDATA;
v->bit = 0;
v->sh = 0;
break;
case S_RDATA:
v->bit++;
if (v->bit >= 8) {
v->sda_s = 1; /* 交回总线,等主设备应答 */
v->st = S_RDATA_ACK;
} else {
vdev_t *d = &v->dev[v->cur_dev];
v->sda_s = (d->mem[d->ptr] & (uint8_t)(0x80u >> v->bit)) ? 1 : 0;
}
break;
case S_RDATA_ACK:
if (v->master_ack == 0) {
vdev_t *d = &v->dev[v->cur_dev];
d->ptr = (uint8_t)(d->ptr + 1u); /* 地址自增(EEPROM 行为) */
v->sda_s = (d->mem[d->ptr] & 0x80u) ? 1 : 0;
v->st = S_RDATA;
v->bit = 0;
} else {
v->st = S_DEAD; /* 主设备不要了,等 STOP */
}
break;
default:
break;
}
}
/* ---------------- 引脚注入 ---------------- */
static si2c_pin_t make_pin(vbus_t *v)
{
si2c_pin_t p;
p.scl_hi = bus_scl_hi;
p.scl_lo = bus_scl_lo;
p.sda_hi = bus_sda_hi;
p.sda_lo = bus_sda_lo;
p.sda_get = bus_sda_get;
p.scl_get = bus_scl_get;
p.ctx = v;
p.half_delay_ns = 5000u; /* 100 kHz */
p.max_stretch = 2000u;
return p;
}
/* ---------------- 测试主体 ---------------- */
static vdev_t *find_dev(vbus_t *v, uint8_t a)
{
int i;
for (i = 0; i < v->ndev; i++) {
if (v->dev[i].addr7 == a) {
return &v->dev[i];
}
}
return NULL;
}
static void print_temp(int32_t milli)
{
int32_t m = milli;
int neg = 0;
if (m < 0) {
neg = 1;
m = -m;
}
printf("%s%ld.%03ld", neg ? "-" : "", (long)(m / 1000), (long)(m % 1000));
}
int main(void)
{
si2c_t i2c;
si2c_pin_t pin;
vbus_t *v = &g_bus;
uint8_t found[8];
uint32_t used = 0;
int n;
int rc;
memset(v, 0, sizeof(*v));
v->sda_m = 1;
v->sda_s = 1;
v->st = S_IDLE;
v->ndev = 2;
v->half_delay_ns = 5000u;
/* 0x48:LM75 温度传感器 */
v->dev[0].addr7 = 0x48;
/* 0x50:EEPROM,256 字节,先全部填 0xFF 模拟擦除态 */
v->dev[1].addr7 = 0x50;
memset(v->dev[1].mem, 0xFF, sizeof(v->dev[1].mem));
pin = make_pin(v);
si2c_init(&i2c, &pin);
printf("===== 软件 I2C 实测(虚拟开漏总线)=====\n");
printf("总线:上拉 + 开漏线与,半周期 5000 ns(100 kHz),从设备 2 个\n\n");
/* ---------- [1] 扫描 ---------- */
printf("[1] 扫描 0x08 ~ 0x77\n");
n = si2c_scan(&i2c, found, 8, &used);
printf(" 找到 %d 个设备:", n);
{
int i;
for (i = 0; i < n && i < 8; i++) {
printf("0x%02X%s", found[i], (i + 1 < n) ? " " : "");
}
}
printf("\n 共用了 %u 个 SCL 周期\n", used);
check(n == 2, "扫描结果数量不对");
check(n == 2 && found[0] == 0x48 && found[1] == 0x50, "扫描到的地址不对");
/* ---------- [2] EEPROM 读写 ---------- */
printf("\n[2] 往 0x50 的 0x10 位置写 16 字节再读回\n");
{
uint8_t wr[16];
uint8_t rd[16];
int i;
for (i = 0; i < 16; i++) {
wr[i] = (uint8_t)(0xA0 + i);
}
rc = si2c_write_reg(&i2c, 0x50, 0x10, wr, 16);
printf(" 写入返回 %d(0 = 成功)\n", rc);
check(rc == 0, "写入失败");
memset(rd, 0, sizeof(rd));
rc = si2c_read_reg(&i2c, 0x50, 0x10, rd, 16);
printf(" 读回返回 %d\n", rc);
check(rc == 0, "读取失败");
printf(" 内容:");
for (i = 0; i < 16; i++) {
printf("%02X ", rd[i]);
}
printf("\n");
check(memcmp(wr, rd, 16) == 0, "读回来的内容和写进去的不一致");
/* 顺带验证地址自增:再读一次同一个子地址,还是同样的内容 */
memset(rd, 0, sizeof(rd));
si2c_read_reg(&i2c, 0x50, 0x10, rd, 4);
printf(" 再次从 0x10 读 4 字节:%02X %02X %02X %02X\n",
rd[0], rd[1], rd[2], rd[3]);
check(rd[0] == 0xA0 && rd[3] == 0xA3, "子地址没有重置");
}
/* ---------- [3] 访问不存在的设备 ---------- */
printf("\n[3] 访问一个没有设备的地址 0x49\n");
rc = si2c_probe(&i2c, 0x49);
printf(" 探测返回 %d(1 = NACK,没有设备应答)\n", rc);
check(rc == 1, "没设备却收到了 ACK");
/* ---------- [4] 读温度(LM75 格式) ---------- */
printf("\n[4] 读 0x48 的温度寄存器(LM75 格式:11 位,0.125 ℃ 一格)\n");
{
uint8_t msb, lsb;
uint8_t raw[2];
int32_t t;
int32_t naive;
/* 把传感器设定成 25.500 ℃ */
lm75_encode_milli(25500, &msb, &lsb);
find_dev(v, 0x48)->mem[0] = msb;
find_dev(v, 0x48)->mem[1] = lsb;
rc = si2c_read_reg(&i2c, 0x48, 0x00, raw, 2);
printf(" 原始字节 %02X %02X(读回返回 %d)\n", raw[0], raw[1], rc);
check(rc == 0, "读温度失败");
check(raw[0] == msb && raw[1] == lsb, "原始字节不对");
t = lm75_decode_milli(raw[0], raw[1]);
printf(" 完整解码 = ");
print_temp(t);
printf(" ℃\n");
check(t == 25500, "温度解码不对");
/* 偷懒写法:只读高字节,丢掉小数 */
naive = ((raw[0] & 0x80u) ? ((int32_t)raw[0] - 256) : (int32_t)raw[0]) * 1000;
printf(" 只读高字节 = ");
print_temp(naive);
printf(" ℃,误差 ");
print_temp(t - naive);
printf(" ℃\n");
}
/* ---------- [5] 负温度 ---------- */
printf("\n[5] 负温度:设定成 -10.250 ℃\n");
{
uint8_t msb, lsb;
uint8_t raw[2];
int32_t t;
int32_t naive;
lm75_encode_milli(-10250, &msb, &lsb);
find_dev(v, 0x48)->mem[0] = msb;
find_dev(v, 0x48)->mem[1] = lsb;
si2c_read_reg(&i2c, 0x48, 0x00, raw, 2);
printf(" 原始字节 %02X %02X\n", raw[0], raw[1]);
t = lm75_decode_milli(raw[0], raw[1]);
printf(" 完整解码 = ");
print_temp(t);
printf(" ℃\n");
check(t == -10250, "负温度解码不对");
naive = ((raw[0] & 0x80u) ? ((int32_t)raw[0] - 256) : (int32_t)raw[0]) * 1000;
printf(" 只读高字节 = ");
print_temp(naive);
printf(" ℃,误差 ");
print_temp(t - naive);
printf(" ℃\n");
printf(" -> 高字节是 -11,低字节小数是 +0.75,结果是 -10.250;\n");
printf(" 把符号拆开各自处理就会算成 -11.750,这是负温度的经典错误\n");
}
/* ---------- [6] 时钟延展 ---------- */
printf("\n[6] 从设备在传输中拉住 SCL 不放(时钟延展)\n");
{
uint32_t wait_before = i2c.stretch_waits;
uint32_t t_before = v->stretched_total;
uint8_t raw[2];
find_dev(v, 0x48)->stretch = 5; /* 拉 5 次才松手 */
rc = si2c_read_reg(&i2c, 0x48, 0x00, raw, 2);
printf(" 从设备延展了 %u 次,主设备等待了 %u 次\n",
v->stretched_total - t_before, i2c.stretch_waits - wait_before);
printf(" 读到的数据 %02X %02X,返回 %d\n", raw[0], raw[1], rc);
check(rc == 0, "时钟延展时读取失败");
check(v->stretched_total - t_before == 5u, "延展次数不对");
check(i2c.timeouts == 0, "不该超时");
}
/* ---------- [7] 总线卡死与 9 时钟解锁 ---------- */
printf("\n[7] 从设备把 SDA 死死按住(总线卡死)\n");
{
uint32_t pulses;
/* 模拟:上一次传输被复位打断,从设备停在半路,拉着 SDA 不放 */
v->stuck = 1;
v->stuck_left = 9;
v->sda_s = 0;
v->st = S_RDATA;
v->cur_dev = 0;
printf(" 当前 SDA 电平 = %d(0 = 被按住)\n", i2c.pin.sda_get(i2c.pin.ctx));
check(i2c.pin.sda_get(i2c.pin.ctx) == 0, "卡死状态没建立起来");
rc = si2c_probe(&i2c, 0x50);
printf(" 卡死时探测 0x50 -> 返回 %d(<0 表示发不出 START)\n", rc);
check(rc == -1, "卡死状态下竟然还探测成功了");
pulses = si2c_bus_recover(&i2c);
printf(" 补了 %u 个时钟后 SDA 电平 = %d\n",
pulses, i2c.pin.sda_get(i2c.pin.ctx));
printf(" -> 刚才那次失败的探测已经在 SCL 上留下 1 个时钟,"
"从设备先挪了一格,所以这里只需要 %u 个\n", pulses);
check(pulses <= 9u, "恢复用的时钟数超过了 9");
check(i2c.pin.sda_get(i2c.pin.ctx) == 1, "SDA 没有被释放");
rc = si2c_probe(&i2c, 0x50);
printf(" 恢复后探测 0x50 -> 返回 %d(0 = 设备回来了)\n", rc);
check(rc == 0, "恢复之后还是探测不到设备");
}
/* ---------- [7b] 不做任何尝试直接恢复 ---------- */
printf("\n[7b] 重新制造一次卡死,不做失败的探测,直接解锁\n");
{
uint32_t pulses;
v->stuck = 1;
v->stuck_left = 9;
v->sda_s = 0;
v->st = S_RDATA;
v->cur_dev = 0;
pulses = si2c_bus_recover(&i2c);
printf(" 补了 %u 个时钟后 SDA 电平 = %d\n",
pulses, i2c.pin.sda_get(i2c.pin.ctx));
check(pulses == 9u, "干净状态下的解锁时钟数不是 9");
check(i2c.pin.sda_get(i2c.pin.ctx) == 1, "SDA 没有被释放");
rc = si2c_probe(&i2c, 0x50);
printf(" 探测 0x50 -> 返回 %d\n", rc);
check(rc == 0, "解锁后探测不到设备");
/* 幂等性:总线正常时恢复函数不应该多打时钟 */
pulses = si2c_bus_recover(&i2c);
printf(" 总线正常时再调一次恢复 -> 打了 %u 个时钟(应该是 0)\n", pulses);
check(pulses == 0u, "恢复函数不幂等");
}
/* ---------- [8] 吞吐量 ---------- */
printf("\n[8] 吞吐量:读 16 字节需要多少个 SCL 周期\n");
{
uint8_t buf[16];
uint32_t c0 = i2c.cycles;
uint32_t c1;
uint32_t ns;
si2c_read_reg(&i2c, 0x50, 0x10, buf, 16);
c1 = i2c.cycles - c0;
/* 一个 SCL 周期 = 高半周期 + 低半周期 */
ns = c1 * 2u * pin.half_delay_ns;
printf(" 用了 %u 个 SCL 周期,耗时 %u ns = %u.%03u ms @100 kHz\n",
c1, ns, (unsigned)(ns / 1000000u), (unsigned)((ns / 1000u) % 1000u));
printf(" 折算有效速率 %u 字节/秒\n",
(unsigned)((uint64_t)16u * 1000000000ull / (uint64_t)ns));
printf(" 理论拆解:1(START) + 9(地址) + 9(子地址) + 1(重复START)"
" + 9(地址|读) + 16*9(数据) + 1(STOP) = %u\n", c1);
check(c1 == 174u, "周期数和理论值不符(应为 1+9+9+1+9+144+1)");
}
printf("\n总计:%u 次 NACK,%u 次超时,%u 次时钟延展等待\n",
i2c.nacks, i2c.timeouts, i2c.stretch_waits);
printf("\n===== %s =====\n",
failed == 0 ? "全部通过:以上数据由本机 gcc 实编译实运行"
: "有失败项!");
return failed == 0 ? 0 : 1;
}实测输出
下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):
===== 软件 I2C 实测(虚拟开漏总线)=====
总线:上拉 + 开漏线与,半周期 5000 ns(100 kHz),从设备 2 个
[1] 扫描 0x08 ~ 0x77
找到 2 个设备:0x48 0x50
共用了 1232 个 SCL 周期
[2] 往 0x50 的 0x10 位置写 16 字节再读回
写入返回 0(0 = 成功)
读回返回 0
内容:A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF
再次从 0x10 读 4 字节:A0 A1 A2 A3
[3] 访问一个没有设备的地址 0x49
探测返回 1(1 = NACK,没有设备应答)
[4] 读 0x48 的温度寄存器(LM75 格式:11 位,0.125 ℃ 一格)
原始字节 19 80(读回返回 0)
完整解码 = 25.500 ℃
只读高字节 = 25.000 ℃,误差 0.500 ℃
[5] 负温度:设定成 -10.250 ℃
原始字节 F5 C0
完整解码 = -10.250 ℃
只读高字节 = -11.000 ℃,误差 0.750 ℃
-> 高字节是 -11,低字节小数是 +0.75,结果是 -10.250;
把符号拆开各自处理就会算成 -11.750,这是负温度的经典错误
[6] 从设备在传输中拉住 SCL 不放(时钟延展)
从设备延展了 5 次,主设备等待了 5 次
读到的数据 F5 C0,返回 0
[7] 从设备把 SDA 死死按住(总线卡死)
当前 SDA 电平 = 0(0 = 被按住)
卡死时探测 0x50 -> 返回 -1(<0 表示发不出 START)
补了 8 个时钟后 SDA 电平 = 1
-> 刚才那次失败的探测已经在 SCL 上留下 1 个时钟,从设备先挪了一格,所以这里只需要 8 个
恢复后探测 0x50 -> 返回 0(0 = 设备回来了)
[7b] 重新制造一次卡死,不做失败的探测,直接解锁
补了 9 个时钟后 SDA 电平 = 1
探测 0x50 -> 返回 0
总线正常时再调一次恢复 -> 打了 0 个时钟(应该是 0)
[8] 吞吐量:读 16 字节需要多少个 SCL 周期
用了 174 个 SCL 周期,耗时 1740000 ns = 1.740 ms @100 kHz
折算有效速率 9195 字节/秒
理论拆解:1(START) + 9(地址) + 9(子地址) + 1(重复START) + 9(地址|读) + 16*9(数据) + 1(STOP) = 174
总计:111 次 NACK,0 次超时,5 次时钟延展等待
===== 全部通过:以上数据由本机 gcc 实编译实运行 =====