深入理解I2C时序(以I2C实时时钟PCF8563为案例)


一、前言

硬件背景

  • GD32F407VET6
  • I2C实时时钟PCF8563

原理图

image-20241116194724707 image-20241116194750339

文档资料

二、恩智浦(NXP)I2C总线协议时序解析

参见NXP(恩智浦)I2C总线协议说明书和用户手册中的

**6 Electrical specifications and timing for I/O stages and bus lines **

**Table 11. Characteristics of the SDA and SCL bus lines for Standard, Fast, and Fast-mode Plus I2C-bus devices **

Overview

SDA/SCL总线特性

image-20241116195121046

时序定义

image-20241116202433638

I2C传输控制

I2C规定单次通信需要遵循如下两个控制

数据传输控制

在SCL为低时可以修改SDA;SCL为高时,SDA应该保持不变。

在SCL为低电平时,准备SDA(发送方);在SCL为高时,保持SDA(在此期间接送方会读取SDA)。

这一点在NXP(恩智浦)I2C总线协议说明书和用户手册中的 3.1.3 Data validity说明如下:

The data on the SDA line must be stable during the HIGH period of the clock. The HIGH or LOW state of the data line can only change when the clock signal on the SCL line is LOW (see Figure 4). One clock pulse is generated for each data bit transferred.

image-20241116214251742

起始/终止控制

在SCL为高时,拉低SDA(需要提前准备好SDA为高);在SCL为低时,拉高SDA(需要提前准备好SDA为低)。

可以发现为了区分数据传输控制,特意在SCL为高时操作SDA。

这一点在NXP(恩智浦)I2C总线协议说明书和用户手册中的 3.1.4 START and STOP conditions 说明如下:

All transactions begin with a START (S) and are terminated by a STOP (P) (see Figure 5). A HIGH to LOW transition on the SDA line while SCL is HIGH defines a START condition. A LOW to HIGH transition on the SDA line while SCL is HIGH defines a STOP condition.

image-20241116214423843

fSCL时钟线频率(frequency for SCL)

image-20241116201656138

该参数规定了I2C的SCL时钟线的频率,以Fast-mode为例,最大为400kHz。这意味着我们在操作SCL时,将SCL置0的时间+随后将SCL置1的时间之和不能小于 1/400kHz = 2.5us:

image-20241116201725124

以GD32F4为例,对应标准库函数 gpio_output_options_set中的 speed参数,即控制GPIO引脚输出高低电平时,能够达到的最大频率。

tHD;STA起始信号保持时间(HolD time for START condition)

image-20241116202607065

该参数规定了起始信号(START condition)需要保持的时间(在SCL为高电平时,将SDA由高拉低后需要保持的时间)

image-20241116202932000

这里有两个细节需要注意一下:

image-20241116204002371

  1. hold time (repeated) START condition:repeated表明第一个起始信号后的重复起始信号都需要遵循这个规则,例如在PCF8563中提到的 写地址,读数据模式中,在第一个S(起始信号)之后有一个 dummy write(指定后面连续读的起始寄存器地址),然后又有一个S,紧接着才是真正的连续读数据。这里第二个S就属于 repeated START condition
image-20241116203555124
  1. After this period, the first clock pulse is generated:暗示我们在这个tHD;STA周期(相当于I2C通信的准备阶段)过后,主设备应该生成第一个时钟脉冲开始传输数据。因此时序定义里也给出了如下示意图,暗示我们在延时tHD;STA之后,应该将SCL拉低(图中 1st clock cycle也进一步印证了这一点)。

    image-20241116204645958

tLOW/tHIGH时钟线高低电平周期控制(SCL low/high period)

image-20241116204955105

该参数规定了我们传输数据拉高拉低SCL时,其高低电平应该持续的时间,以Fast-mode为例,SCL低电平最低持续1.3us,高电平则最低0.6us

tSU;STA 起始信号建立(准备)时间(SetUp time for START)

image-20241116205306146

该参数规定了起始信号的建立(准备)时间,这个参数有点难理解。我们参照时序定义来看下:

image-20241116205502756

和tHD;STA对比来看,tHD;STA规定了起始信号的保持时间(SCL为高时,拉低SDA并保持)。

tSU;STA 则是用于重复起始信号的(每次通信至少对应一个START和STOP,连续的多次通信中紧接着前一次通信STOP之后的START可称为 repeated START,前面介绍的 写地址,连续读模式对应的START,START,STOP中第二个START也是如此)。

对于重复起始信号而言,SCL可能是以低电平开始的(例如上一次通信将SCL拉低了),为了满足实现起始信号(拉低SDA)的前置条件(SCL为高,SDA为高),该参数规定了在拉低SDA之前,SDA应该在SCL为高期间保持高电平的时间。

起始/终止信号的实现

这里我们已经可以写出起始信号的伪代码了(所有延时以 Fast-mode 为例):

I2C_Start()

// 前置条件
SDA = HIGH
SCL = HIGH
// 前置条件建立(准备)时间
delay 0.6us

// 起始信号(SCL,SDA为高时,拉低SDA)
SDA = LOW
// 起始信号保持时间
delay 0.6

// 开始第一个时钟周期低电平阶段
SCL = 0;
delay 1.3us

对应的C语言实现如下:

#define I2C_SCL_PIN    GPIO_PIN_6
#define I2C_SDA_PIN    GPIO_PIN_7
#define I2C_GPIO_PORT  GPIOB
#define I2C_GPIO_RCU   RCU_GPIOB

#define SDA_HIGH() gpio_bit_set(I2C_GPIO_PORT, I2C_SDA_PIN)

#define SDA_LOW() gpio_bit_reset(I2C_GPIO_PORT, I2C_SDA_PIN)

#define SCL_HIGH() gpio_bit_set(I2C_GPIO_PORT, I2C_SCL_PIN)

#define SCL_LOW() gpio_bit_reset(I2C_GPIO_PORT, I2C_SCL_PIN)

void start() {
    SDA_HIGH();
    SCL_HIGH();
    delay_1us(1); //start setup
    SDA_LOW();
    delay_1us(1); //start hold

    SCL_LOW();
    delay_1us(1);
}

注意:这里第22行,并没有和伪代码中的1.3us保持一致,是因为在后续的数据传输中,置位SDA后是需要一个setup时延的(例如1us),无形之间延长了SCL低电平的时间(相当于变成为2us),详见后文分析。

tHD;DAT数据保持时间(HolD time for DATA)

image-20241116212042232

可以发现该参数对应两种条件: CBUS compatible controllersI2C-bus devices,我们这里只关心I2C,要求持续的最小时间为0,相当于不需要控制时延,暂时可以忽略该参数。

tSU;DAT数据建立(准备)时间(SetUp time for DATA)

image-20241116212548515

该参数规定了SDA建立(准备)时间,也即在SCL为低时,置位SDA为下一个要发送的数据后,到拉高SCL(以让接收方读取数据)之前应该保持的时间:

image-20241116214624147

我们每次发送数据都遵循如下流程

  • 拉低SCL,并延时tLOW
  • 准备SDA(下一次要发送的数据),并延时tSU;DAT
  • 拉高SCL,并延时/tHIGH

发送一个字节的实现

至此,我们可以写出发送一个字节的伪代码了(接着之前的 I2C_Start):

I2C_SendByte

uint8_t byte
// 需要循环8次,发送一个字节8个bit
for(i = 0 ; i < 8 ; i++) {
    // 此前I2C_Start的结尾以将SCL拉低,这里我们可以直接准备SDA
    SDA = (byte & (0x08 >> i)) ? HIGH : LOW
    delay 1us // SDA建立时间,这期间SCL仍未低电平,因此一共持续了2us
    
    SCL = HIGH
    delay 1us // SCL高电平周期
    
    SCL = LOW // 开启下一个时钟周期
    delay 1us // SCL低电平周期
}

对应GD32F4的C语言实现:

void start() {
    SDA_HIGH();
    SCL_HIGH();
    delay_1us(1); //start setup
    SDA_LOW();
    delay_1us(1); //start hold

    SCL_LOW();
    delay_1us(1);
}

void send_byte(uint8_t byte) {
    for (int i = 0; i < 8; ++i) {
        if (byte & (0x80 >> i)) {
            SDA_HIGH();
        } else {
            SDA_LOW();
        }
        delay_1us(1); // data set-up

        SCL_HIGH();
        delay_1us(1); // scl high

        SCL_LOW();
        delay_1us(1);
    }
}

tr / tf上升沿/下降沿时间(Rising/Falling edge)

image-20241116220318791

image-20241116220810270

这两个参数规定了SCL和SDA的上升沿(r-rising edge)、下降沿(f-falling edge)的时间控制。这个参数和GPIO对应电气特性中的压摆率(Slew Rate)。

[!NOTE]

该参数可以在MCU数据手册与电气特性(Electrical characteristics )相关的章节中可以找到(AC characteristics

tSU;STO终止信号建立(准备)时间(SetUp time for STOP)

image-20241116221510344

由于终止信号(拉高SDA)是有前置条件的(SCL为高,SDA为低),该参数规定了在拉高SDA之前,SDA在SCL为高期间保持低电平的时间:

image-20241116222328620

tBUF总线释放时间(BUs Free time)

image-20241116222447598

该参数规定了在一个终止信号(P)和下一个起始信号(S)之间,总线应该被释放(SCL/SDA保持高电平)的时间:

image-20241116222631166

终止信号的实现

至此,我们可以实现终止信号的伪代码了:

I2C_Stop

// 前置条件:SDA为低,SCL为高
// 此前I2C_SendByte的结尾已将SCL拉低,这里可以直接准备SDA
SDA = 0
delay 1us // 为了确保SCL低电平周期大于1.3us,这里补充一个1us
SCL = HIGH
delay 1us // SCL高电平周期
delay 1us // 补充1us,确保>总线释放时间1.3us

对应C语言实现:

void stop() {
    SDA_LOW();
    SCL_HIGH();
    delay_1us(1); //stop setup
    SDA_HIGH();
    delay_1us(2); //bus free time between a STOP and START condition
}

tVD;DAT/tVD;ACK数据/ACK有效时间(Valid time for DATA/ACK)

image-20241116223438554

个人理解这两个参数是在接收的场景下使用的,在作为接收方释放SDA(拉高SDA)之后,发送方会通过拉高/拉低SDA来准备ACK/数据比特,然后我们需要等待tVD;DAT/tVD;ACK这样一个时间确保发送方准备好SDA了,然后我们再拉高SCL(让发送方保持SDA),并读取SDA。

接收ACK/数据字节代码实现

至此,我们可以实现接收ack和数据字节的代码了:

#define SDA_IN() gpio_mode_set(I2C_GPIO_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, I2C_SDA_PIN)
#define SDA_OUT() gpio_mode_set(I2C_GPIO_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, I2C_SDA_PIN)
#define SDA_READ() gpio_input_bit_get(I2C_GPIO_PORT, I2C_SDA_PIN)

bool wait_ack() {
    bool success = false;
    SDA_HIGH(); // release bus
    SCL_HIGH();
    delay_1us(1); // scl high

    SDA_IN();
    success = SDA_READ() == RESET; // read ack
    SDA_OUT();

    SCL_LOW();
    delay_1us(1); // scl low
    return success;
}

void receive_byte(uint8_t *buf) {
    SDA_IN();
    for (int i = 0; i < 8; ++i) {
        SCL_HIGH(); // slave keep data
        delay_1us(1);

        // read data
        (*buf) <<= 1;
        if(SDA_READ() == SET) {
            (*buf) |= 0x01;
        }

        SCL_LOW(); // let slave prepare next data
        delay_1us(1);
    }
    SDA_OUT();
}

三、I2C完整代码

hal_i2c_soft.h

//
// Created by 86157 on 2024/11/16.
//

#ifndef HAL_I2C_SOFT_H
#define HAL_I2C_SOFT_H

#include "gd32f4xx.h"

void hal_i2c_soft_init();
bool hal_i2c_soft_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t size);
bool hal_i2c_soft_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *buf, uint16_t size);
void hal_i2c_soft_test();

#endif //HAL_I2C_SOFT_H

hal_i2c_soft.c

//
// Created by 86157 on 2024/11/16.
//
#include <stdbool.h>
#include "hal_i2c_soft.h"
#include "systick.h"
#include "logger.h"

#define I2C_SCL_PIN    GPIO_PIN_6
#define I2C_SDA_PIN    GPIO_PIN_7
#define I2C_GPIO_PORT  GPIOB
#define I2C_GPIO_RCU   RCU_GPIOB

#define SDA_HIGH() gpio_bit_set(I2C_GPIO_PORT, I2C_SDA_PIN)

#define SDA_LOW() gpio_bit_reset(I2C_GPIO_PORT, I2C_SDA_PIN)

#define SCL_HIGH() gpio_bit_set(I2C_GPIO_PORT, I2C_SCL_PIN)

#define SCL_LOW() gpio_bit_reset(I2C_GPIO_PORT, I2C_SCL_PIN)

#define SDA_IN() gpio_mode_set(I2C_GPIO_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, I2C_SDA_PIN)
#define SDA_OUT() gpio_mode_set(I2C_GPIO_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, I2C_SDA_PIN)
#define SDA_READ() gpio_input_bit_get(I2C_GPIO_PORT, I2C_SDA_PIN)

void hal_i2c_soft_init() {
    // 使能GPIOB时钟
    rcu_periph_clock_enable(I2C_GPIO_RCU);

    // 配置PB6(SCL)和PB7(SDA)为开漏输出模式
    gpio_mode_set(I2C_GPIO_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, I2C_SCL_PIN | I2C_SDA_PIN);
    gpio_output_options_set(I2C_GPIO_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C_SCL_PIN | I2C_SDA_PIN);

    // 将SCL和SDA线拉高
    gpio_bit_set(I2C_GPIO_PORT, I2C_SCL_PIN | I2C_SDA_PIN);
}

static void start() {
    SDA_HIGH();
    SCL_HIGH();
    delay_1us(1); //start setup
    SDA_LOW();
    delay_1us(1); //start hold

    SCL_LOW();
    delay_1us(1);
}

static void stop() {
    SDA_LOW();
    SCL_HIGH();
    delay_1us(1); //stop setup
    SDA_HIGH();
    delay_1us(2); //bus free time between a STOP and START condition
}

static bool wait_ack() {
    bool success = false;
    SDA_HIGH(); // release bus
    SCL_HIGH();
    delay_1us(1); // scl high

    SDA_IN();
    success = SDA_READ() == RESET; // read ack
    SDA_OUT();

    SCL_LOW();
    delay_1us(1); // scl low
    return success;
}

static void send_ack(bool ack) {
    // set sda to reponse ack/nack
    if (ack) {
        SDA_LOW();
    } else {
        SDA_HIGH();
    }
    delay_1us(1); // sda setup

    SCL_HIGH(); // let slave read ack
    delay_1us(1);

    SCL_LOW();
    delay_1us(1); // reset scl for next operation
}

static void send_byte(uint8_t byte) {
    for (int i = 0; i < 8; ++i) {
        if (byte & (0x80 >> i)) {
            SDA_HIGH();
        } else {
            SDA_LOW();
        }
        delay_1us(1); // data set-up

        SCL_HIGH();
        delay_1us(1); // scl high

        SCL_LOW();
        delay_1us(1);
    }
}

static void receive_byte(uint8_t *buf) {
    SDA_IN();
    for (int i = 0; i < 8; ++i) {
        SCL_HIGH(); // slave keep data
        delay_1us(1);

        // read data
        (*buf) <<= 1;
        if(SDA_READ() == SET) {
            (*buf) |= 0x01;
        }

        SCL_LOW(); // let slave prepare next data
        delay_1us(1);
    }
    SDA_OUT();
}

static bool start_for_write(uint8_t dev_addr, uint8_t reg_addr) {
    start();

    send_byte(dev_addr);
    if (!wait_ack()) {
        stop();
        LOG_DEBUG("write dev_addr failed")
        return false;
    }

    send_byte(reg_addr);
    if (!wait_ack()) {
        stop();
        LOG_DEBUG("write reg_addr failed")
        return false;
    }
    return true;
}

static bool start_for_read(uint8_t dev_addr) {
    start();
    send_byte(dev_addr | 0x01); // read from slave
    if (!wait_ack()) {
        stop();
        LOG_DEBUG("write dev_addr for read failed")
        return false;
    }
    return true;
}

bool hal_i2c_soft_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t size) {
    if (!start_for_write(dev_addr, reg_addr)) {
        return false;
    }
    for (uint16_t i = 0; i < size; ++i) {
        send_byte(*data);
        if (!wait_ack()) {
            stop();
            LOG_DEBUG("write data[%d] failed", *data)
            return false;
        }
        data++;
    }
    stop();
    return true;
}

bool hal_i2c_soft_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *buf, uint16_t size) {
    // dummy write for register address
    if (!start_for_write(dev_addr, reg_addr)) {
        return false;
    }
    // continuous read
    if (!start_for_read(dev_addr)) {
        return false;
    }
    for (uint16_t i = 0; i < size - 1; ++i) {
        receive_byte(buf++);
        send_ack(true);
    }
    receive_byte(buf);
    send_ack(false);

    stop();
    return true;
}


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录