I2C

最近移植 RT-Thread + FM24CL64 ,发送地址报错 NACK;排查下来,软硬件都有小问题;趁此机会,整理下I2C相关。

文末有简单的模拟 I2C 代码,单纯为了深入理解 I2C 协议,注释仅为个人理解。


I2C协议的灵魂,【上拉电阻】 和 【开漏输出】;由此带来 “线与” 特性,可实现多主、仲裁等。


FM24CL64,地址0xA0,读出指定位置(就从起始位置读起吧 0x0000)后的16字节数据。

 那么读的逻辑就是...,好不好用不知道,对比上图很合理。

uint8_t byte[16] = {0};

i2c_start(); i2c_send_byte(
0xA0); i2c_wait_ack(); i2c_send_byte(0x00); i2c_wait_ack(); i2c_send_byte(0x00); i2c_wait_ack(); i2c_start(); i2c_send_byte(0xA1); i2c_wait_ack(); for (i = 0; i < 16; i++) { byte[i] = i2c_read_byte(); if (15 != i) i2c_ack(); else i2c_nack(); } i2c_stop();

模拟 I2C

#include <stdint.h>
#include "stm32f1xx_hal.h"

#define SCL_H()     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET)
#define SCL_L()     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET)
#define SDA_H()     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET)
#define SDA_L()     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET)
#define SCL_R()     HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6)
#define SDA_R()     HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7)

void fm24cl64_read(void);

/*
 * Mr.Zhen,BlackGloves
 * 2021.12.03
**/
void udelay(uint8_t us)
{
    int i;
    for (i = 18*us; i > 0; i--) ;
}

void i2c_start(void)
{
    SCL_H();
    SDA_H();
    udelay(2);

    SDA_L();
    udelay(3);
}

void i2c_stop(void)
{
    SCL_H();
    SDA_L();
    udelay(2);

    SDA_H();
    udelay(3);
}

void i2c_send_byte(uint8_t byte)
{
    uint8_t i;

    // MSB
    SCL_L();
    for (i = 0; i < 8; i++)
    {
        if (byte & 0x80)
            SDA_H();
        else
            SDA_L();
        udelay(5);

        SCL_H();
        udelay(5);
        SCL_L();        //紧跟 SCL_H, byte 发送过程中,防止让出SCL后被其他设备占用.

        if (i == 7)
        {
            SDA_H();    //最后 1bit Master 释放 SDA SCL
            SCL_H();    //顺序很重要,先SDA后SCL;
                        //假设最后 1bit SDA=L, 先释放SCL的可能后果是,其他设备虽占用总线,但SDA_H()无效
        }

        byte <<= 1;
    }
}

uint8_t i2c_read_byte(void)
{
    uint8_t i;
    uint8_t val = 0;

    /* MSB */
    for (i = 0; i < 8; i++)
    {
        val <<= 1;
        SCL_H();        //Master 让出总线, slave 占有总线,并将数据输出到 SDA, 完成后 slave 让出总线;
        udelay(5);
        if (SDA_R())
            val++;
        SCL_L();        //master 占有总线, slave 得到消息(Master已获取上一bit数据)后释放 SDA, 并准备下一个bit的数据,
                        //待拿到总线控制权后第一时间将数据输出到 SDA
        if (i != 7)     //master 第8个时钟后半段的低电平期间, 要抉择是 ack 还是 nack, 提前输出到 SDA.
            udelay(5);  //这样, master第9个时钟起始的上升沿就可以判断出 SDA 的状态
                        //slave 根据ack / nack 进行后续操作, 这样可充分利用SCL时钟.
    }

    return val;
}

uint8_t i2c_wait_ack(void)
{
    uint8_t ret;

    SDA_H();
    SCL_H();    //确保 master 让出总线; i2c_send_byte 最后1bit会释放总线,这2行也可考虑删除.

    udelay(5);  //给足 slave 反应时间, 等待 ack
    if (SDA_R())
        ret = 1;
    else
        ret = 0; // ACK

    SCL_L();    //1.时序; 2.通知 slave,释放 SDA; 3.master 占有总线,准备发送数据;

    return ret;
}

void i2c_ack(void)
{
    SDA_L();    //接收完1byte数据后,第8个时钟后半段的低电平期间,拉低SDA,准备ACK信号.
    udelay(5);

    SCL_H();    //SCL上升沿,SDA=L
    udelay(5);
    SCL_L();
    udelay(5);  //第9个周期完

    SDA_H();
    SCL_H();    //master 释放 SDA SCL
}

void i2c_nack(void)
{
    SDA_H();    //假设 master 中途提前发送 nack, 那么 slave 不再准备数据;
    udelay(5);  //后面 master 读到的全是1, SDA高阻态么.

    SCL_H();
    udelay(5);
    SCL_L();
    udelay(5);

    SDA_H();
    SCL_H();
}

void fm24cl64_read(void)
{
    int i;
    uint8_t byte[16] = {0};

    if (SDA_R() && SCL_R())
    {
        i2c_start();
        i2c_send_byte(0xA0);
        i2c_wait_ack();
        i2c_send_byte(0x00);
        i2c_wait_ack();
        i2c_send_byte(0x00);
        i2c_wait_ack();
        i2c_start();
        i2c_send_byte(0xA1);
        i2c_wait_ack();
        for (i = 0; i < 16; i++)
        {
            byte[i] = i2c_read_byte();
            if (i != 15)
                i2c_ack();
            else
                i2c_nack();
        }
        i2c_stop();
    }
}

 

posted @ 2021-12-03 10:46  【黑手套】  阅读(114)  评论(0编辑  收藏  举报