CIU32 I2C — GPIO 模拟 I2C 主机

CIU32 CIU32F003_STDLib V1.0.3 GPIO 模拟 I2C 主机收发示例教学

I2C — GPIO 模拟 I2C 主机(GPIO_Simulate_I2C_Master)

固件库: CIU32F003_STDLib V1.0.3 芯片: CIU32F003(Cortex-M0) 源文件: CIU32/CIU32F003_STDLib_V1.0.3/Projects/I2C/GPIO_Simulate_I2C_Master/Source/


一、功能简介

本示例用普通 GPIO 引脚“模拟”出 I2C 主机时序(俗称 bit-bang / 软 I2C), 不依赖芯片内部的硬件 I2C 外设。程序依次完成:

  1. 作为主机向从机 0x5C 发送 8 字节数据;
  2. 再向同一从机 接收 8 字节数据。

通过这段代码,你能看清 I2C 每个比特是怎么靠“拉高/拉低 SCL、SDA + 延时”拼出来的。 注意:该实现不支持时钟延长(clock stretching),通信速率约 150Kbps。

二、硬件准备

  • CIU32F003 开发板
  • 一个 I2C 从设备(如 EEPROM、传感器),地址为 0x5C
  • 引脚(本示例):
信号 引脚
SCL PB3
SDA PB4
  • SCL、SDA 必须各接一个 4.7kΩ 上拉电阻到 VDD(开漏总线靠上拉拉高)
  • 主从设备共地

没有真实从机时,可先用“逻辑分析仪/示波器”观察 PB3/PB4 上的波形来验证时序是否正确。

三、完整代码

3.1 主流程 main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "main.h"

int main(void)
{
    system_clock_config();   /* 系统时钟 RCH 48MHz */
    gpio_init();             /* SCL/SDA 配置为开漏输出,默认高 */

    bsp_i2c_master_send();   /* 主机发送 8 字节给从机 0x5C */
    bsp_i2c_master_receive();/* 主机从从机 0x5C 接收 8 字节 */

    while (1) { }
}

3.2 引脚与延时 common.c(节选)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define I2C_PIN_PORT   GPIOB
#define SCL_PIN        GPIO_PIN_3
#define SDA_PIN        GPIO_PIN_4

/* GPIO 初始化:SCL/SDA 默认输出高,开漏,无上下拉(靠外部上拉电阻拉高) */
void gpio_init(void)
{
    std_gpio_init_t gpio_config = {0};
    std_rcc_gpio_clk_enable(RCC_PERIPH_CLK_GPIOB);

    std_gpio_set_pin(I2C_PIN_PORT, (SCL_PIN | SDA_PIN));  /* 先置高,避免起始毛刺 */

    gpio_config.pin         = (SCL_PIN | SDA_PIN);
    gpio_config.mode        = GPIO_MODE_OUTPUT;
    gpio_config.output_type = GPIO_OUTPUT_OPENDRAIN;  /* 开漏:只能拉低或释放(高) */
    gpio_config.pull        = GPIO_NOPULL;
    std_gpio_init(I2C_PIN_PORT, &gpio_config);
}

/* 简单软件延时,改 delay_time 可调通信速率 */
void i2c_delay_time(uint32_t delay_time)
{
    uint32_t delay_cnt;
    for (delay_cnt = delay_time; delay_cnt > 0; delay_cnt--);
}

3.3 底层时序 i2c_bsp.c(核心)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#define BUFF_SIZE     (8U)
#define SLAVE_ADDRESS (0x5C)
#define CLK_DELAY     (16U)   /* SCL 半周期延时 */
#define ACK_DELAY     (6U)    /* 采样 ACK 前的建立延时 */
#define USER_DELAY    (5U)    /* 地址匹配后的额外延时 */
#define REQ_WRITE     (0U)
#define REQ_READ      (1U)
#define RET_ACK       (0U)
#define RET_NACK      (1U)

uint8_t g_tx_buffer[BUFF_SIZE] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
uint8_t g_rx_buffer[BUFF_SIZE];

/* 起始条件:SCL 高时,SDA 由高变低 */
void bsp_i2c_generate_start(void)
{
    std_gpio_set_pin(I2C_PIN_PORT, (SCL_PIN | SDA_PIN));   /* SDA/SCL 先高 */
    std_gpio_set_pin_mode(I2C_PIN_PORT, SDA_PIN, GPIO_MODE_OUTPUT);
    i2c_delay_time(CLK_DELAY);
    std_gpio_reset_pin(I2C_PIN_PORT, SDA_PIN);             /* SDA 拉低 = 起始 */
    i2c_delay_time(CLK_DELAY);
    std_gpio_reset_pin(I2C_PIN_PORT, SCL_PIN);             /* 拉低 SCL,准备传数据 */
}

/* 停止条件:SCL 高时,SDA 由低变高 */
void bsp_i2c_generate_stop(void)
{
    std_gpio_reset_pin(I2C_PIN_PORT, (SCL_PIN | SDA_PIN)); /* SDA/SCL 先低 */
    std_gpio_set_pin_mode(I2C_PIN_PORT, SDA_PIN, GPIO_MODE_OUTPUT);
    i2c_delay_time(CLK_DELAY);
    std_gpio_set_pin(I2C_PIN_PORT, SCL_PIN);               /* SCL 拉高 */
    i2c_delay_time(CLK_DELAY);
    std_gpio_set_pin(I2C_PIN_PORT, SDA_PIN);               /* SDA 拉高 = 停止 */
}

/* 发送 1 字节(高位先),返回从机 ACK 状态 */
uint32_t bsp_i2c_master_send_byte(uint8_t send_data)
{
    uint32_t bit_cnt = 0, ack_type = 0;
    /* 设置 SDA 初始电平并切输出 */
    if (send_data & 0x80) std_gpio_set_pin(I2C_PIN_PORT, SDA_PIN);
    else                  std_gpio_reset_pin(I2C_PIN_PORT, SDA_PIN);
    std_gpio_set_pin_mode(I2C_PIN_PORT, SDA_PIN, GPIO_MODE_OUTPUT);

    for (bit_cnt = 0; bit_cnt < 8; bit_cnt++)   /* 逐位发送 */
    {
        if ((send_data << bit_cnt) & 0x80) std_gpio_set_pin(I2C_PIN_PORT, SDA_PIN);
        else                                 std_gpio_reset_pin(I2C_PIN_PORT, SDA_PIN);
        i2c_delay_time(CLK_DELAY);
        std_gpio_set_pin(I2C_PIN_PORT, SCL_PIN);   /* SCL 高,从机采样 */
        i2c_delay_time(CLK_DELAY);
        std_gpio_reset_pin(I2C_PIN_PORT, SCL_PIN); /* SCL 低,准备下一位 */
    }
    /* 第 9 个时钟读 ACK:SDA 设为输入,主机释放 */
    std_gpio_set_pin_mode(I2C_PIN_PORT, SDA_PIN, GPIO_MODE_INPUT);
    i2c_delay_time(CLK_DELAY);
    std_gpio_set_pin(I2C_PIN_PORT, SCL_PIN);
    i2c_delay_time(ACK_DELAY);
    ack_type = std_gpio_get_input_pin(I2C_PIN_PORT, SDA_PIN) ? RET_NACK : RET_ACK;
    std_gpio_reset_pin(I2C_PIN_PORT, SCL_PIN);
    i2c_delay_time(CLK_DELAY);
    return ack_type;
}

/* 接收 1 字节(高位先) */
uint8_t bsp_i2c_master_receive_byte(void)
{
    uint32_t bit_cnt = 0;
    uint8_t  receive_data = 0;
    std_gpio_set_pin_mode(I2C_PIN_PORT, SDA_PIN, GPIO_MODE_INPUT); /* 主机释放 SDA */
    for (bit_cnt = 0; bit_cnt < 8; bit_cnt++)
    {
        std_gpio_reset_pin(I2C_PIN_PORT, SCL_PIN);
        i2c_delay_time(CLK_DELAY);
        std_gpio_set_pin(I2C_PIN_PORT, SCL_PIN);  /* SCL 高,从机驱动 SDA */
        i2c_delay_time(CLK_DELAY);
        receive_data <<= 1;
        if (std_gpio_get_input_pin(I2C_PIN_PORT, SDA_PIN) == true)
            receive_data += 1;
    }
    std_gpio_reset_pin(I2C_PIN_PORT, SCL_PIN);
    i2c_delay_time(CLK_DELAY);
    return receive_data;
}

/* 主机完整发送流程 */
void bsp_i2c_master_send(void)
{
    uint32_t ack_type, index;
    bsp_i2c_generate_start();
    ack_type = bsp_i2c_master_send_byte(SLAVE_ADDRESS | REQ_WRITE); /* 地址 + 写 */
    if (ack_type != RET_ACK) error_process();   /* 无 ACK 则异常处理 */
    i2c_delay_time(USER_DELAY);
    for (index = 0; index < BUFF_SIZE; index++)
    {
        ack_type = bsp_i2c_master_send_byte(g_tx_buffer[index]);
        if (ack_type != RET_ACK) error_process();
    }
    bsp_i2c_generate_stop();
}

/* 主机完整接收流程(倒数第 2 字节回 ACK,最后 1 字节回 NACK) */
void bsp_i2c_master_receive(void)
{
    uint32_t ack_type, index;
    bsp_i2c_generate_start();
    ack_type = bsp_i2c_master_send_byte(SLAVE_ADDRESS | REQ_READ);  /* 地址 + 读 */
    if (ack_type != RET_ACK) error_process();
    i2c_delay_time(USER_DELAY);
    for (index = 0; index < BUFF_SIZE - 1; index++)
    {
        g_rx_buffer[index] = bsp_i2c_master_receive_byte();
        bsp_i2c_generate_ack(RET_ACK);    /* 主机回 ACK,告诉从机继续发 */
    }
    g_rx_buffer[index] = bsp_i2c_master_receive_byte();
    bsp_i2c_generate_ack(RET_NACK);       /* 最后一字节回 NACK,结束 */
    bsp_i2c_generate_stop();
}

四、代码分段讲解

① 开漏 + 上拉 I2C 总线用开漏输出:MCU 只能把线“拉低”或“释放”。释放后靠外部上拉电阻把线拉高。 因此 SCL/SDA 必须配置为 GPIO_OUTPUT_OPENDRAIN,且板子上要有上拉电阻,否则总线永远为低、无法通信。

② 起始/停止条件

  • 起始:SCL 保持高,SDA 由高变低;
  • 停止:SCL 保持高,SDA 由低变高。 切换引脚方向前先固定电平,是为了避免产生误触发毛刺。

③ 字节发送与 ACK 每字节 8 位、高位先发;SCL 高电平时从机采样 SDA。第 9 个时钟由接收方把 SDA 拉低表示 ACK。 本例把 SDA 切回输入(主机释放)去读从机应答。

④ 接收时的 ACK/NACK 主机每收到一字节要回 ACK 让从机继续发;读到最后一字节改回 NACK,从机就知道“不用再发了”,随后主机发停止条件。

⑤ 速率控制 i2c_delay_time() 是纯软件延时,CLK_DELAY 等数值决定每位宽度(约 150Kbps)。 改这些宏即可调速率,但纯软件延时占用 CPU、且不支持时钟延长。

五、实验现象

  • 逻辑分析仪/示波器可见标准 I2C 波形:起始 → 0x5C+W → ACK → 8 字节 → 停止 → 起始 → 0x5C+R → ACK → 收 8 字节(末字节前 ACK、末字节 NACK)→ 停止;
  • 接真实从机且地址匹配时,g_rx_buffer 收到从机返回的数据。

六、常见问题

现象 可能原因 解决办法
总线一直为低 没加上拉电阻 / 引脚非开漏 SCL/SDA 加 4.7k 上拉,确认 GPIO_OUTPUT_OPENDRAIN
地址发完收不到 ACK 从机地址/接线错 确认 7 位地址,R/W 位由库自动拼接;检查 SDA/SCL 未接反
通信偶尔失败 速率过快 / 从机需时钟延长 增大 CLK_DELAY;本实现不支持 clock stretching
写完 EEPROM 读不到 从机内部写周期未结束 写后延时(如 5ms)再发起读

七、小结

软 I2C 的关键是理解“开漏 + 上拉 + 起始/停止 + 每字节 ACK”四件事:

  • 引脚必须开漏、总线必须有上拉;
  • 起始 = SCL 高时 SDA 下降,停止 = SCL 高时 SDA 上升;
  • 每字节后第 9 个时钟是应答位;
  • 接收最后一字节回 NACK 表示结束。

八、拓展应用

  • 把这套函数封装成 i2c_write(reg, buf, len) / i2c_read(),对接具体传感器;
  • 改用硬件 I2C 外设(见 I2C1_Slave_Poll / I2C1_Slave_Interrupt),释放 CPU 并支持时钟延长;
  • 加上超时判断,避免从机无响应时死等。
世界是你们
使用 Hugo 构建
主题 Stack 由 Jimmy 设计