Administrator
发布于 2026-09-25 / 0 阅读
0
0

软件 I2C 实战:开漏总线、时钟延展、扫描与 9 时钟解锁

一句话结论:软件 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 → 把引脚置成高阻,松开线,电平由上拉电阻决定(被动)

所有器件共享一根线,谁拉低都算低,这叫线与。由此推出两件必须记住的事:

  1. 任何器件都不能「输出高」。如果你用推挽输出高、从设备同时在拉低,

就是 VCC 直接对地短路,轻则波形烂掉,重则烧引脚。 STM32 上必须配 GPIO_MODE_OUTPUT_OD(开漏输出)。

  1. 两个器件同时拉低是合法的,这正是 I2C 多主机仲裁的基础。

读电平的时候必须用输入模式读线上真实电平,不能读自己刚写进输出寄存器 的值——你写了 1(释放),线上可能是 0(别人在拉低)。这是 ACK 判断的唯一依据。

二、一帧长什么样

       ┌─START                 ┌─ 8 个数据位 ─┐ ┌ACK┐        ┌─STOP
SDA ───┐   ┌───┐   ┌───┐   ┌───┐            ┌───┐   ┌───────
       └───┘   └───┘   └───┘   └────────────┘   └───┘
SCL ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌────────
      └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─╨─┘ └─╨─┘
       ↑                        ↑              ↑
   SCL 高时 SDA 下降          SCL 高时采样    第 9 个时钟

三条规则定死了整条协议:

事件条件
STARTSCL 为高时,SDA 由高变低
STOPSCL 为高时,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)
2EEPROM 写 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 个,且总线正常时一个都不打」—— 它要能随便被调用,而不是只在故障时调用一次。

七、上真机还要注意

  1. 上拉电阻:3.3 V 系统常用 4.7 kΩ。总线电容大(线长、器件多)时压得下

上升沿,要减小到 2.2 kΩ 甚至 1 kΩ;但太小会增大灌电流,注意器件能力。

  1. 半周期延时:100 kHz 对应 5 µs。用 delay_us() 会引入函数调用开销,

真实的边沿速率会比设定值低不少。要么用 DWT 周期计数器精确等, 要么接受一个较低的实际速率——别在注释里写「100 kHz」却实际跑 70 kHz。

  1. 必须关中断保护整帧。软件 I2C 的时序全靠 CPU 一条条指令撑,

被中断打断 10 µs 就可能违反时序。整帧 DMA 不行,只能关中断或用临界区。

  1. SDA 方向切换别太慢。开漏输出读电平需要先把引脚从输出切成输入,

STM32 上 GPIO_MODE_OUTPUT_OD 配好之后可以直接读 IDR, 不需要来回切模式——切模式浪费时间还会在切换瞬间产生毛刺。

  1. 每次上电先调 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 clean

include/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 实编译实运行 =====

评论