查看: 313|回复: 0

一文带你编写I2C通讯函数

[复制链接]
1 金钱 回复本帖可获得 1 金钱奖励! 每人限 1 次

该用户从未签到

10

主题

3

回帖

122

积分

版主

积分
122
QQ
发表于 2024-10-10 11:16:34 | 显示全部楼层 |阅读模式
本帖最后由 sjzwangjh 于 2024-10-10 11:44 编辑

    I2C是单片机使用中经常使用的一个通讯端口,很多外设的控制和数据输出,都是以I2C总线的形式挂接在MCU端口上的,下面我们一步一步开始I2C的编程,一条I2C总线上,必然有一个“主控器件”,称之为“主器件”,其它器件都是“从器件”,每个“从器件”有一个唯一的“7bit”地址,所以一个I2C总线上最多可以挂载127个“从器件”,AVR器件中,对“0”地址发送数据是“广播”功能,所有的器件都可以接收数据。
一. 认识I2C的时序
   I2C通讯只需要两根线,通常命名为SCL和SDA,SCL是通讯数据“同步时钟”,由“主器件”发出,SDA是IO数据,可以由“主器件”发送,也可以由“从器件”发送,由此I2C的时序共有2种时序,“主写时序”和“主读时序”
   由于SDA挂接在所有器件上,并且通讯时会出现“数据竞争”,正常情况下,“输出0”如果直接和“输出1”短路,则会出现“竞争状态,此时总线上会产生“大电流”,总线的电平”既不是高电平,也不是低电平“,取决于两个输出的”强者“,为了避免这种情况的发生,I2C总线规定,必须在SDA和SCK上挂接”上拉电阻“到VCC,”上拉电阻“的大小,取决于”传输速率“,传输速度越高,电阻取值越小。I2C总线只输出”低电平“,高电平由”上拉电阻“产生,从而避免了”数据竞争“的出现,从而实现I2C的”安全通讯“。具体时序如图1~图4所示。
   完整的通讯时序如下:

              图1 I2C总线设备连接图


           图2 I2C SCL和SDA的时序关系图



图3 I2C 开始通讯(START)和结束通讯(STOP)时序图


图4 I2C发送地址时序图

二. 编写程序
  1. 为了方便程序的移植,一般首先用宏定义实现端口的“基本控制”,然后在程序种用“宏定义”代替“具体的端口操作”,从而实现程序的可移植性,举例如下:
  // 预备宏定义,为了方便移植,初始定义“越少越好”,宏定义给我们提供了“凡是重复皆可宏”的使用方式,
#ifndef DEFINE_CONCAT                                                       //*  判断 下面的宏定义 在其它文件中是否出现过,确保 “只包含一次”
    #define DEFINE_CONCAT(a, b)             a##b                        //* 粘接给定的两个字符串
    #define DEFINE_CONCAT_EXPANDED(a, b)     DEFINE_CONCAT(a, b)            //* 鉴于宏定义不能嵌套,所以定义了第二级粘贴

    //*基于端口的宏定义,组合出
    #define DEFINE_DIRECT(name,index)         DEFINE_CONCAT_EXPANDED(TRIS, DEFINE_CONCAT(name, index))    //* 将 TRIS 粘接到 用户端口上
    #define DEFINE_UPLOAD(name,index)         DEFINE_CONCAT_EXPANDED(WPU, DEFINE_CONCAT(name, index))    //* 将 TRIS 粘接到 用户端口上
    #define DEFINE_DWLOAD(name,index)         DEFINE_CONCAT_EXPANDED(WPD, DEFINE_CONCAT(name, index))    //* 将 TRIS 粘接到 用户端口上
    #define DEFINE_IO(name, index)             DEFINE_CONCAT_EXPANDED(P, DEFINE_CONCAT(name, index)) //* 用于组装类似 PA4 的端口 Bit访问变量
    #define DEFINE_VALUE(name)                 DEFINE_CONCAT_EXPANDED(PORT, name) // PA端口
    #define    DEFINE_PSINK(name,index)        DEFINE_CONCAT_EXPANDED(PSINK,DEFINE_CONCAT(name, index))    // 组装PSINKA0
    #define    DEFINE_IO_MODE(index)            DEFINE_CONCAT(ANSEL,index)        // 组装ANSEL0~ANSEL7的字符串
#endif // DEFINE_CONCAT

// 基于以上宏定义,可以只定义出端口的基本属性即可,此部分可称为:硬件定义

//  I2C1 GPIO Configuration
//  PB6   ------> I2C1_SCL
//  PB7   ------> I2C1_SDA
#define        I2C1_SCL_PORT            A
#define     I2C1_SCL_BIT_INDEX      6

#define        I2C1_SDA_PORT            B
#define     I2C1_SDA_BIT_INDEX      7

#define        I2C1_START_DELAY_US        4
#define        I2C1_CLK_DELAY_US        1
#define        I2C1_ACK_DELAY_US        2

//基于I2C的基本IO口定义,可以扩展出如下定义
#define     SCL_DIR_SET_IN          DEFINE_DIRECT(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 1        // 设置SCL输入
#define     SCL_DIR_SET_OUT         DEFINE_DIRECT(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 0        // 设置SCL输出
#define        SCL_UPLOAD_SET_OPEN        DEFINE_UPLOAD(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 1        // 打开上拉
#define        SCL_UPLOAD_SET_CLOSE    DEFINE_UPLOAD(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 0        // 关闭上拉
#define        SCL_SET_0                    DEFINE_IO(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 0        // 设置端口值
#define        SCL_SET_1                    DEFINE_IO(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 1        // 设置端口值
#define        SCL_GET                       (DEFINE_IO(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX))        // 读取端口值

#define     SDA_DIR_SET_IN          DEFINE_DIRECT(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 1        // 设置SDA输入
#define     SDA_DIR_SET_OUT         DEFINE_DIRECT(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 0        // 设置SDA输出
#define        SDA_UPLOAD_SET_OPEN        DEFINE_UPLOAD(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 1        // 打开上拉
#define        SDA_UPLOAD_SET_CLOSE    DEFINE_UPLOAD(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 0        // 关闭上拉
#define        SDA_SET_0                    DEFINE_IO(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 0        // 设置端口值
#define        SDA_SET_1                    DEFINE_IO(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 1        // 设置端口值
#define        SDA_GET                       (DEFINE_IO(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX))        // 读取端口值

//进而扩展出编程中使用的通用“宏”IO操作函数
#define        IIC_INIT        SCL_DIR_SET_IN;SCL_SET_0;SDA_DIR_SET_IN;SDA_SET_0        // 端口初始化,都设置为输入,在上拉的作用下输出高电平
#define     IIC_SCL_1        SCL_DIR_SET_IN
#define     IIC_SCL_0        SCL_DIR_SET_OUT
#define     IIC_SDA_1        SDA_DIR_SET_IN
#define     IIC_SDA_0        SDA_DIR_SET_OUT
#define     READ_SDA         SDA_GET

// I2C通讯通常只需要7个函数,列举如下:
//初始化IIC
void IIC_Init(void)
{                        
    IIC_INIT;
}

//产生IIC起始信号
void IIC_Start(void)
{
    SDA_DIR_SET_OUT;     //sda线输出
    IIC_SDA_1;            
    IIC_SCL_1;
    delay_us(I2C1_START_DELAY_US);
     IIC_SDA_0;      //START:when CLK is high,DATA change form high to low
    delay_us(I2C1_START_DELAY_US);
    IIC_SCL_0;      //钳住I2C总线,准备发送或接收数据
}

//产生IIC停止信号
void IIC_Stop(void)
{
    SDA_DIR_SET_OUT;      //sda线输出
    IIC_SCL_0;
    IIC_SDA_0;
     delay_us(I2C1_START_DELAY_US);
    IIC_SCL_1;      //STOP:when CLK is high DATA change form low to high
     delay_us(I2C1_START_DELAY_US);
    IIC_SDA_1;      //发送I2C总线结束信号                                
}

//等待应答信号到来
//返回值:0,接收应答失败
//        1,接收应答成功
uint8_t IIC_Wait_Ack(void)
{
    uint8_t ucErrTime=0;
    SDA_DIR_SET_IN;      //SDA设置为输入  
    IIC_SDA_1;delay_us(I2C1_CLK_DELAY_US);      
    IIC_SCL_1;delay_us(I2C1_CLK_DELAY_US);     
    while(READ_SDA)
    {
        ucErrTime++;
        if(ucErrTime>250)
        { // 超时
            IIC_Stop();
            return 0;  // 读取超时,返回0
        }
    }
    IIC_SCL_0;//时钟输出0        
    return 1;  // 正常返回1
}

//产生ACK应答
void IIC_Ack(void)
{
    IIC_SCL_0;
    SDA_DIR_SET_OUT;
    IIC_SDA_0;
    delay_us(I2C1_ACK_DELAY_US);
    IIC_SCL_1;
    delay_us(I2C1_ACK_DELAY_US);
    IIC_SCL_0;
}

//不产生ACK应答            
void IIC_NAck(void)
{
    IIC_SCL_0;
    SDA_DIR_SET_OUT;
    IIC_SDA_1;
    delay_us(I2C1_ACK_DELAY_US);
    IIC_SCL_1;
    delay_us(I2C1_ACK_DELAY_US);
    IIC_SCL_0;
}

//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答              
uint8_t IIC_Send_Byte(uint8_t txd)
{                        
    uint8_t t;   
    SDA_DIR_SET_OUT;         
    IIC_SCL_0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {
        if(txd & 0x80) IIC_SDA_1;
        else IIC_SDA_0;
        txd<<=1;      
        delay_us(I2C1_ACK_DELAY_US);   //对TEA5767这三个延时都是必须的
        IIC_SCL_1;
        delay_us(I2C1_ACK_DELAY_US);
        IIC_SCL_0;   
        delay_us(I2C1_ACK_DELAY_US);
    }

    return IIC_Wait_Ack();
}

// IIC读1个字节,
// ack=1时,发送ACK,
// ack=0,发送nACK   
uint8_t IIC_Read_Byte(uint8_t ack)
{
    uint8_t i,receive=0;
    SDA_DIR_SET_IN;//SDA设置为输入
    for(i=0;i<8;i++ )
    {
        IIC_SCL_0;
        delay_us(I2C1_ACK_DELAY_US);
        IIC_SCL_1;
        receive<<=1;
        if(READ_SDA)receive++;   
        delay_us(I2C1_CLK_DELAY_US);
    }                     
    if (!ack)
        IIC_NAck();//发送nACK
    else
        IIC_Ack(); //发送ACK   
    return receive;
}





本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

友情链接:

返回顶部 返回列表