十三.I2C使用2——主从机程序编写


在前面一章我们已经铺垫了I2C的使用流程,下面我们就按照I2C的通讯流程写对应的代码,这个流程应该严格按照参考手册给出的定义

上面两幅图就是I2C通讯的流程

master代码流程

I2C的代码流程比较复杂,我们一个个函数来说

初始化

首先是初始化

void i2c_init(I2C_Type *base)
{
    base->I2CR &= ~(1<<7);      //disable I2C
    base->IFDR= 0x15;          //640分频,clk=103.125KHz
    base->I2CR |= (1<<7);       //I2C Enable
}

初始化里只是设置了个分频器,我们使用的时钟源是66MHz,选择640分频,速率为103.125KHz,设置分频器前要将I2C停止,设置完成后一定要使能I2C,其他寄存器操作时有些是要求I2C使能的。

产生Start流程

在完成初始化后,数据可以有主机先行发送。发送数据时,要先检查I2C的BUSY标志位是否为0(I2C_I2SR[IBB]),标志位为0时才能发送数据。发送第一组数据时要将从机的地址写入I2C_I2DR寄存器,从机地址是7位的,地址后面要跟一个读写标志。

/**
 * @brief 生成start信号
 * 
 * @param base I2C结构体
 * @param address 从机地址
 * @param direction 方向
 * @return unsigned char 
 */
unsigned char i2c_master_start(I2C_Type *base, 
                                unsigned char address,
                                enum i2c_direction direction)
{
    if((base->I2SR) & (1<<5)){      //IBB,I2C忙标志位
            return 1;               //IBB=1时,I2C忙,返回1
    } 
    
    //设置为主机模式
    base->I2CR |= ((1<<5) | (1<<4));  //[5]MSTA master mode,[4]MXT 发送模式
    //产生Start信号
    base->I2DR = ((unsigned int)address <<1) | ((direction== kI2C_Read)?1 :0);  //address位高7位为地址,低位为读写标志
    return 0;                       //无异常,返回0
}

函数中先判定是否为总线忙,如果总线忙跳出函数,抛出异常,如果正常就将设备设置成master、发送模式,将数据寄存器写入从机地址。注意地址左移1位,最低位留给读写标志位,用了个3元运算符(A?1:2,当A为True时值为1,否则为2)。

I2C Stop

master结束通讯,要发送一个Stop标志。

//产生stop信号
unsigned char i2c_master_stop(I2C_Type *base)
{
    unsigned short timeout = 0xffff;                //超时等待值
    
    //清除I2CRbit5:3
    base->I2CR &= ~(7<<3);                          //MSTA=0 Slave模式、MTX=0 接收模式 TXAK=0 发送NoACK 

    //等待I2C空闲
    while(base->I2SR &(1<<5))                       //BUSY位
    {
        timeout--;
        if(timeout ==0)
        return I2C_STATUS_TIMEOUT;                  //返回超时异常
    }
    return I2C_STATUS_OK;
}

这个Stop标志的产生要求我不太明白为什么要把I2C设置成slave模式。按照手册31.5.4 Generation of Stop章节说的,停止标志的产生要在数据倒数第二个Byte时master告诉从机最后一个byte发送完成后跟一个NoACK信号

重新发送Start

在数据传输中,如果master还需要占据总线,他需要重新发个Start信号,这个Start标志后还需要跟一个地址,这个地址可以是设备地址也可以是寄存器地址,restart信号和start信号之间是不能有Stop信号的。

//重复发送Start信号
unsigned char i2c_master_repeated_start(I2C_Type *base, 
                                unsigned char address,
                                enum i2c_direction direction)
{

    //检查I2C是否空闲或在从机模式下
    if(((base->I2SR & (1<<5)) && ((base->I2CR) &(1<<5))) == 0 )     //I2SR[5]:IBB 总线忙标志 I2CR[5]:MSTA master模式
        {return 1;}  //总线空闲模式无法重新生成Restart slave模式也无法生产Restart信号,跳出  

    base->I2CR |=(1<<4) |(1<<2);    //I2CR[4]MTX=1发送模式  [2]产生Repeat Start
    base->I2DR = ((unsigned int)address <<1) | ((direction== kI2C_Read)?1 :0);  //address位高7位为地址,低位为读写标志

    return I2C_STATUS_OK;
}

ReStart的流程和Start差不多,都是先判定是否总线忙,当总线不在空闲状态且I2C为master模式下,重新生成Start信号,信号是发送一个地址和读写标志。前面的if判断比较绕,就是在IBB位不能为0或MSTA不能为0,就是I2C不能是空闲或设备不是Slave模式,否则跳出

异常处理

这里的异常处理主要是通过I2SR寄存器的状态判定两种异常状态:仲裁丢失和无应答

/**
 * @brief 检查并处理异常
 * 
 * @param base I2C
 * @param state 状态(I2SR寄存器)
 * @return unsigned char 
 */
unsigned char i2c_check_and_clear_error(I2C_Type *base,unsigned int state)
{   
    //先检查是否为仲裁丢失错误
    if(state & (1<<4))              //state值为I2SR,bit[4]:IAL仲裁丢失
    {
        base->I2SR &= ~(1<<4);      //清除IAL异常
        base->I2CR &= ~(1<<7);      //禁止I2C
        base->I2CR |= (1<<7);       //使能I2C
        return I2C_STATUS_ARBITRATIONLOST;  //抛出异常:仲裁丢失
    }

    else if(state & (1<<0))         //I2SR[0]RXAK 1时收到NoACK信号
    {
        return I2C_STATUS_NAK;      //抛出异常:无应答
    }

    return I2C_STATUS_OK;           //返回正常
}

仲裁丢失时需要重新使能I2C,抛出异常状态(宏在头文件里定义了),无应答时不用重启I2C,直接抛异常。

数据发送

数据发送要按照本章节最开始那个流程图去进行,先看代码

/**
 * @brief I2C,master发送数据
 * 
 * @param base I2C结构体
 * @param buf  待发送数据
 * @param size 发送数据大小
 */
void i2c_master_write(I2C_Type *base,const unsigned char *buf,unsigned int size)
{
    while(!(base->I2SR & (1<<7)));  //ICF,等待传输完成,1时数据传输中,0时跳出循环

    base->I2SR &= ~(1<<1);          //IIF=0,清除中断

    base->I2CR |= (1<<4);           //MTX=1设置为发送

    while(size--){
        base->I2DR = *buf ++ ;          //将待写入的值写入寄存器
        while(!(base->I2SR & (1<<1)));  //等待传输完成,这里用到时IIT,没有用传输完成标志位(bit7)
        base->I2SR &= ~(1<<1);          //清除标志

        /*检查ACK*/
        if(i2c_check_and_clear_error(base,base->I2SR))
            break;                      //无异常时状态字为I2C_STATUS_OK=0,否则跳出循环
    }

    base->I2SR &= ~(1<<1);              //清除标志
    i2c_master_stop(base);              //停止传输
}

这里有个BUG:在发送过程中等待过程,按照手册上所说要检查I2SR的bit7

当该位为1的时候说明传输完成。但是按照官方的驱动,是通过检查bit[1],即IIF来实现的。这个不知道是为什么,如果检查ICF标志I2C通讯无法进行。但开始有个等待传输的过程又是检查的ICF。官方给出的硬件驱动就是这样写的整个流程就是

  1. 等待I2C上一发送过程结束
  2. 清除IIF中断标志
  3. 设置MTX为传输
  4. 将要发送到数据依次写入I2DR寄存器,写入后等待发送完毕标志
  5. 所有数据发送完毕,清除IIF标志
  6. 发送Stop标志停止传输

数据读取

数据读取的流程和发送基本一致,但是要判定是否是倒数第二个Byte的数据,因为I2C协议要求读数据为8bit数据加1bit的ACK标志,但最后一个Byte的数据后要跟的是NoACK,并且主机要在读取倒数第二个Byte数据时告诉从机下一组数据准备发个NoACK。

/**
 * @brief 读数据
 * 
 * @param base I2C结构体
 * @param buf  读取数据写入的缓存
 * @param size 读取数据大小
 */
void i2c_master_read(I2C_Type *base,unsigned char *buf,unsigned size){
    volatile uint8_t dummy = 0;         //假读数据
    dummy++;                            //防止编译报错,具体原因不详

    while(!(base->I2SR & (1<<7)));      //ICF,1时数据传输中,0时跳出循环

    base->I2SR &= ~(1<<1);              //IIF=0,清除中断标志

    base->I2CR &= ~((1<<4)|(1<<3));     //[4]MTX=0:接收数据 [3]TXAK=0 8位数据后加1位ACK响应

    if(size==1)                         //发送数据大小为1
        base->I2CR |= (1<<3);           //发送NoACK

    dummy = base->I2DR;                 //假读(个人觉得通知从机NoACK必须在读倒数第二个数据时发送,所以假读一次)

    while(size--){
        while(!(base->I2SR & (1<<1)));      //IIF是否为0?等待传输完成
        base->I2SR &= ~(1<<1);              //IIF=0,清除标志位
        if(size==0){                        //最后一个数据读取完毕
            i2c_master_stop(base);          //主机发送Stop标志
        }
        if(size==1){                        //倒数第二个数据
            base->I2CR |= (1<<3);           //NoACK通知 
        }

        *buf++ = base->I2DR;                //读取信息
    }
}

程序里做了个假读的流程,我估计原因就是如果读取数据长度为1时要假装读一下,以便从机发送NoACK。读取的流程和写的流程基本一样,也是通过IIF判定是否在读写操作。但是这里没有做异常检查和处理,我估计是因为要读操作时前面必须有个写操作,如果无响应就会在写的流程中出现异常。

I2C通讯函数

教程里最后把发送和接受的功能集成到了一个函数中,这个函数是最后我们调用的接口。

/*
 * @description    : I2C数据传输,包括读和写
 * @param - base: 要使用的IIC
 * @param - xfer: 传输结构体
 * @return         : 传输结果,0 成功,其他值 失败;
 */
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer)
{
    unsigned char ret = 0;
    enum i2c_direction direction = xfer->direction;    

    base->I2SR &= ~((1 << 1) | (1 << 4));            /* 清除标志位 */

    /* 等待传输完成 */
    while(!((base->I2SR >> 7) & 0X1)){}; 

    /* 如果是读的话,要先发送寄存器地址,所以要先将方向改为写 */
    if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read))
    {
        direction = kI2C_Write;
    }

    ret = i2c_master_start(base, xfer->slaveAddress, direction); /* 发送开始信号 */
    if(ret)
    {    
        return ret;
    }

    while(!(base->I2SR & (1 << 1))){};            /* 等待传输完成 */

    ret = i2c_check_and_clear_error(base, base->I2SR);    /* 检查是否出现传输错误 */
    if(ret)
    {
          i2c_master_stop(base);                         /* 发送出错,发送停止信号 */
        return ret;
    }
    
    /* 发送寄存器地址 */
    if(xfer->subaddressSize)
    {
        do
        {
            base->I2SR &= ~(1 << 1);            /* 清除标志位 */
            xfer->subaddressSize--;                /* 地址长度减一 */
            
            base->I2DR =  ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //向I2DR寄存器写入子地址
  
            while(!(base->I2SR & (1 << 1)));      /* 等待传输完成 */

            /* 检查是否有错误发生 */
            ret = i2c_check_and_clear_error(base, base->I2SR);
            if(ret)
            {
                 i2c_master_stop(base);                 /* 发送停止信号 */
                 return ret;
            }  
        } while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));

        if(xfer->direction == kI2C_Read)         /* 读取数据 */
        {
            base->I2SR &= ~(1 << 1);            /* 清除中断挂起位 */
            i2c_master_repeated_start(base, xfer->slaveAddress, kI2C_Read); /* 发送重复开始信号和从机地址 */
            while(!(base->I2SR & (1 << 1))){};/* 等待传输完成 */

            /* 检查是否有错误发生 */
            ret = i2c_check_and_clear_error(base, base->I2SR);
            if(ret)
            {
                 ret = I2C_STATUS_ADDRNAK;
                i2c_master_stop(base);         /* 发送停止信号 */
                return ret;  
            }                         
        }
    }    

    /* 发送数据 */
    if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0))
    {
        i2c_master_write(base, xfer->data, xfer->dataSize);
    }

    /* 读取数据 */
    if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0))
    {
           i2c_master_read(base, xfer->data, xfer->dataSize);
    }
    return 0;    
}

这个函数就比较复杂了,有时间的话可以分析一下,这里就不再讲了,最后把文件路径、头文件和最终代码都放出来

文件夹和前面的硬件驱动路径一样,在bsp文件夹下放了两个文件

 头文件 

/**
 * @file bsp_i2c.h
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2022-01-21
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#ifndef __BSP_I2C_H
#define __BSP_I2C_H

#include "imx6ul.h"

#define I2C_STATUS_OK                 (0)
#define I2C_STATUS_BUSY               (1)
#define I2C_STATUS_IDLE               (2)
#define I2C_STATUS_NAK                (3)
#define I2C_STATUS_ARBITRATIONLOST    (4)
#define I2C_STATUS_TIMEOUT            (5)
#define I2C_STATUS_ADDRNAK            (6)


enum i2c_direction
{
    kI2C_Write = 0x0,
    kI2C_Read = 0x1,
};

/*
 * I2Cmaster传输结构体
 */
struct i2c_transfer
{
    unsigned char slaveAddress;          /* 7位从机地址             */
    enum i2c_direction direction;         /* 传输方向             */
    unsigned int subaddress;               /* 寄存器地址            */
    unsigned char subaddressSize;        /* 寄存器地址长度             */
    unsigned char *volatile data;        /* 数据缓冲区             */
    volatile unsigned int dataSize;      /* 数据缓冲区长度             */
};


void i2c_init(I2C_Type *base);

unsigned char i2c_master_start(I2C_Type *base, 
                                unsigned char address,
                                enum i2c_direction direction);

unsigned char i2c_master_repeated_start(I2C_Type *base, 
                                unsigned char address,
                                enum i2c_direction direction);

unsigned char i2c_master_stop(I2C_Type *base);

unsigned char i2c_check_and_clear_error(I2C_Type *base,unsigned int state);

unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer);
#endif
bsp_i2c.h

c文件

#include "bsp_i2c.h"

void i2c_init(I2C_Type *base)
{
    
    base->I2CR &= ~(1<<7);  //disable I2C

    base->IFDR=0x15;   //640分频,clk=103.125KHz

    base->I2CR |= (1<<7);   //I2C Enable

}

/**
 * @brief 生成start信号
 * 
 * @param base I2C结构体
 * @param address 从机地址
 * @param direction 方向
 * @return unsigned char 
 */
unsigned char i2c_master_start(I2C_Type *base, 
                                unsigned char address,
                                enum i2c_direction direction)
{
    if((base->I2SR) & (1<<5)){      //IBB,I2C忙标志位
            return 1;               //IBB=1时,I2C忙,返回1
    } 
    
    //设置为主机模式
    base->I2CR |= ((1<<5) | (1<<4));  //[5]MSTA master mode,[4]MXT 发送模式
    //产生Start信号
    base->I2DR = ((unsigned int)address <<1) | ((direction== kI2C_Read)?1 :0);  //address位高7位为地址,低位为读写标志
    return 0;                       //无异常,返回0
}


//产生stop信号
unsigned char i2c_master_stop(I2C_Type *base)
{
    unsigned short timeout = 0xffff;                //超时等待值
    
    //清除I2CRbit5:3
    base->I2CR &= ~(7<<3);                          //MSTA=0 Slave模式、MTX=0 接收模式 TXAK=0 发送NoACK 

    //等待I2C空闲
    while(base->I2SR &(1<<5))                       //BUSY位
    {
        timeout--;
        if(timeout ==0)
        return I2C_STATUS_TIMEOUT;                  //返回超时异常
    }
    return I2C_STATUS_OK;
}

//重复发送Start信号
unsigned char i2c_master_repeated_start(I2C_Type *base, 
                                unsigned char address,
                                enum i2c_direction direction)
{

    //检查I2C是否忙或在从机模式下
    if(((base->I2SR & (1<<5)) && ((base->I2CR) &(1<<5))) == 0 )     //I2SR[5]:IBB 总线忙标志 I2CR[5]:MSTA master模式
        {return 1;}  //总线空闲模式无法重新发送start信号,slave模式也无法生成Restart信号,跳出  

    base->I2CR |=(1<<4) |(1<<2);    //I2CR[4]MTX=1发送模式  [2]产生Repeat Start
    base->I2DR = ((unsigned int)address <<1) | ((direction== kI2C_Read)?1 :0);  //address位高7位为地址,低位为读写标志

    return I2C_STATUS_OK;
}

/**
 * @brief 检查并处理异常
 * 
 * @param base I2C
 * @param state 状态(I2SR寄存器)
 * @return unsigned char 
 */
unsigned char i2c_check_and_clear_error(I2C_Type *base,unsigned int state)
{   
    //先检查是否为仲裁丢失错误
    if(state & (1<<4))              //state值为I2SR,bit[4]:IAL仲裁丢失
    {
        base->I2SR &= ~(1<<4);      //清除IAL异常
        base->I2CR &= ~(1<<7);      //禁止I2C
        base->I2CR |= (1<<7);       //使能I2C
        return I2C_STATUS_ARBITRATIONLOST;  //抛出异常:仲裁丢失
    }

    else if(state & (1<<0))         //I2SR[0]RXAK 1时收到NoACK信号
    {
        return I2C_STATUS_NAK;      //抛出异常:无应答
    }

    return I2C_STATUS_OK;           //返回正常
}

//发送和接受按要求是要判定传输完毕标志位,但是根据官方文档是判定的IIF,也就是中断标志位
/*发送数据*/
/**
 * @brief I2C,master发送数据
 * 
 * @param base I2C结构体
 * @param buf  待发送数据
 * @param size 发送数据大小
 */
void i2c_master_write(I2C_Type *base,const unsigned char *buf,unsigned int size)
{
    while(!(base->I2SR & (1<<7)));  //ICF,1时数据传输中,0时跳出循环

    base->I2SR &= ~(1<<1);          //IIF=0,清除中断

    base->I2CR |= (1<<4);           //MTX=1设置为发送

    while(size--){
        base->I2DR = *buf ++ ;          //将待写入的值写入寄存器
        while(!(base->I2SR & (1<<1)));  //等待传输完成,这里用到时IIT,没有用传输完成标志位(bit7)
        base->I2SR &= ~(1<<1);          //清除标志

        /*检查ACK*/
        if(i2c_check_and_clear_error(base,base->I2SR))
            break;                      //无异常时状态字为I2C_STATUS_OK=0,否则跳出循环
    }

    base->I2SR &= ~(1<<1);              //清除标志
    i2c_master_stop(base);              //停止传输
}


/**
 * @brief 读数据
 * 
 * @param base I2C结构体
 * @param buf  读取数据写入的缓存
 * @param size 读取数据大小
 */
void i2c_master_read(I2C_Type *base,unsigned char *buf,unsigned size){
    volatile uint8_t dummy = 0;         //假读数据
    dummy++;                            //防止编译报错,具体原因不详

    while(!(base->I2SR & (1<<7)));      //ICF,1时数据传输中,0时跳出循环

    base->I2SR &= ~(1<<1);              //IIF=0,清除中断标志

    base->I2CR &= ~((1<<4)|(1<<3));     //[4]MTX=0:接收数据 [3]TXAK=0 8位数据后加1位ACK响应

    if(size==1)                         //发送数据大小为1
        base->I2CR |= (1<<3);           //发送NoACK

    dummy = base->I2DR;                 //假读(个人觉得通知从机NoACK必须在读倒数第二个数据时发送,所以假读一次)

    while(size--){
        while(!(base->I2SR & (1<<1)));      //IIF是否为0?等待传输完成
        base->I2SR &= ~(1<<1);              //IIF=0,清除标志位
        if(size==0){                        //最后一个数据读取完毕
            i2c_master_stop(base);          //主机发送Stop标志
        }
        if(size==1){                        //倒数第二个数据
            base->I2CR |= (1<<3);           //NoACK 
        }

        *buf++ = base->I2DR;                //读取信息
    }
}



/*
 * @description    : I2C数据传输,包括读和写
 * @param - base: 要使用的IIC
 * @param - xfer: 传输结构体
 * @return         : 传输结果,0 成功,其他值 失败;
 */
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer)
{
    unsigned char ret = 0;
    enum i2c_direction direction = xfer->direction;    

    base->I2SR &= ~((1 << 1) | (1 << 4));            /* 清除标志位 */

    /* 等待传输完成 */
    while(!((base->I2SR >> 7) & 0X1)){}; 

    /* 如果是读的话,要先发送寄存器地址,所以要先将方向改为写 */
    if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read))
    {
        direction = kI2C_Write;
    }

    ret = i2c_master_start(base, xfer->slaveAddress, direction); /* 发送开始信号 */
    if(ret)
    {    
        return ret;
    }

    while(!(base->I2SR & (1 << 1))){};            /* 等待传输完成 */

    ret = i2c_check_and_clear_error(base, base->I2SR);    /* 检查是否出现传输错误 */
    if(ret)
    {
          i2c_master_stop(base);                         /* 发送出错,发送停止信号 */
        return ret;
    }
    
    /* 发送寄存器地址 */
    if(xfer->subaddressSize)
    {
        do
        {
            base->I2SR &= ~(1 << 1);            /* 清除标志位 */
            xfer->subaddressSize--;                /* 地址长度减一 */
            
            base->I2DR =  ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //向I2DR寄存器写入子地址
  
            while(!(base->I2SR & (1 << 1)));      /* 等待传输完成 */

            /* 检查是否有错误发生 */
            ret = i2c_check_and_clear_error(base, base->I2SR);
            if(ret)
            {
                 i2c_master_stop(base);                 /* 发送停止信号 */
                 return ret;
            }  
        } while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));

        if(xfer->direction == kI2C_Read)         /* 读取数据 */
        {
            base->I2SR &= ~(1 << 1);            /* 清除中断挂起位 */
            i2c_master_repeated_start(base, xfer->slaveAddress, kI2C_Read); /* 发送重复开始信号和从机地址 */
            while(!(base->I2SR & (1 << 1))){};/* 等待传输完成 */

            /* 检查是否有错误发生 */
            ret = i2c_check_and_clear_error(base, base->I2SR);
            if(ret)
            {
                 ret = I2C_STATUS_ADDRNAK;
                i2c_master_stop(base);         /* 发送停止信号 */
                return ret;  
            }                         
        }
    }    

    /* 发送数据 */
    if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0))
    {
        i2c_master_write(base, xfer->data, xfer->dataSize);
    }

    /* 读取数据 */
    if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0))
    {
           i2c_master_read(base, xfer->data, xfer->dataSize);
    }
    return 0;    
}
bsp_i2c.c

头文件里定义的结构体要注意一下

struct i2c_transfer
{
    unsigned char slaveAddress;          /* 7位从机地址*/
    enum i2c_direction direction;        /* 传输方向*/
    unsigned int subaddress;             /* 寄存器地址*/
    unsigned char subaddressSize;        /* 寄存器地址长度*/
    unsigned char *volatile data;        /* 数据缓冲区*/
    volatile unsigned int dataSize;      /* 数据缓冲区长度*/
};

这个结构体是在后面需要进行通讯的时候声明的,注意数据缓冲是一个指针,所以在读取数据的时候是没有返回值的。

上面的代码完成后,可以通过make检验一下。

 

I2C通讯

上面讲的是i2c的驱动,下面讲下该怎么用这个驱动进行数据交互。和前面一样,我们一个一个函数讲

读取一个字节数据

首先是读取一个Byte多数据

 * @brief ap3216c读取一个byte数据
 * 
 * @param addr 3216的地址
 * @param reg  读取寄存器地址
 * @return unsigned char 读取数据
 */
unsigned char ap3216c_readonebyte(unsigned char addr,unsigned char reg)
{   
    unsigned char val = 0;
    struct i2c_transfer masterXfer;
    masterXfer.slaveAddress = addr;
    masterXfer.direction = kI2C_Read;
    masterXfer.subaddress = reg;
    masterXfer.subaddressSize = 1;
    masterXfer.data = &val;
    masterXfer.dataSize = 1;
    i2c_master_transfer(I2C1, &masterXfer);
    return val;
}

这里偷了个懒,注意一下定义的变量val,原来是打算把最后的状态字作为函数的返回值得,但是函数定义的时候没有传入通讯的结构体,就借用val把I2C读取的数据给了val作为函数的返回值。函数体内部调用了I2C的接口,在初始化以后可以直接调用函数进行I2C数据交互

写入一个字节数据

写数据和读基本上一样,也是调用了I2C接口函数

/**
 * @brief ap3216写1个寄存器
 * 
 * @param addr 3216从机地址
 * @param reg  写寄存器地址
 * @param data 数据
 * @return unsigned char 状态字:1异常,0正常
 */
unsigned char ap3216c_writeonebyte(unsigned char addr,
                                    unsigned char reg,
                                    unsigned char data)
{   
    unsigned char writedata = data;
    unsigned char status = 0;
    struct i2c_transfer masterXfer;
    masterXfer.slaveAddress = addr;
    masterXfer.direction = kI2C_Write;
    masterXfer.subaddress = reg;
    masterXfer.subaddressSize = 1;
    masterXfer.data = &writedata;
    masterXfer.dataSize=1;

    if (i2c_master_transfer(I2C1, &masterXfer)){
        status = 1;  //如果错误,返回1
    }
    return status;
}

写寄存器返回值是状态,通讯正常返回0,异常返回1

初始化

在定义好寄存器的读写函数以后,要进行初始化

/**
 * @brief AP3216C初始化
 * 
 */
void ap3216c_init(void)
{
    /*IO初始化*/
    IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL,1);
    IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL,0x70B0);

    IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA,1);
    IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA,0x70B0);

    /*I2C初始化*/
    i2c_init(I2C1);

    /*芯片初始化*/
    int value=0;
    ap3216c_writeonebyte(AP3216C_ADDR,AP3216C_SYSTEMCONG,0x4);      //软复位
    delay_ms(50);
    ap3216c_writeonebyte(AP3216C_ADDR,AP3216C_SYSTEMCONG,0x3);      //使能ir、ps、als
    value = ap3216c_readonebyte(AP3216C_ADDR,AP3216C_SYSTEMCONG);
    printf("ap3216c systemconfig reg = %#x\r\n",value);
}

初始化过程是先对IO复用初始化、IO电气初始化、I2C初始化、芯片初始化。

电气初始化给的值是0x70B0,对应下面的性能

  • bit 16:0 HYS关闭
  • bit [15:14]: 1 默认47K上拉
  • bit [13]: 1 pull功能
  • bit [12]: 1 pull/keeper使能
  • bit [11]: 0 关闭开路输出
  • bit [7:6]: 10 速度100Mhz
  • bit [5:3]: 110 驱动能力为R0/6
  • bit [0]: 1 高转换率

芯片初始化是给0x00地址写个0x4进行复位,再写入0x3使能ir、ps和als功能

 芯片的I2C地址是厂商已经固定的0x1E,这个IC没有whoami功能,我们在初始化完成后通过读了一个写的寄存器0x00,打印出来的值就是我们写的值

 这样就能说明我们的读写函数(特别是I2C的驱动)功能都是没问题的。

数据读取

数据读取要根据芯片手册给定的寄存器结构

 每个传感器值对应2个寄存器,一共6个寄存器, 把这连续6个寄存器的值读出来再进行处理

 1 // AP3216C数据读取
 2 void ap3216c_readdata(unsigned short *ir,unsigned short*ps,unsigned short *als)
 3 {
 4     unsigned char buf[6];
 5 
 6     for(int i=0;i<6;i++){
 7         buf[i] = ap3216c_readonebyte(AP3216C_ADDR,AP3216C_IRDATALOW +i);
 8     }
 9     if(buf[0] &= 0x80){     //buf[0][bit7]==1时,IR、PS数据无效
10         *ir=0;
11         *ps=0;
12     }
13     else{
14         /*ir数据10bit buf[1]高8位,buf[0][bit1:0]为低2位*/
15         *ir = ((unsigned short)buf[1]<<2) | (buf[0] & 0x03);    //与0x03,只保留低bit[1:0]
16         /*ps数据10bit buf[5][bit5:0]高6位,buf[4][bit3:0]为低4位*/        
17         *ps = (((unsigned short)buf[5] & 0x3F) <<4) | (buf[4] & 0x0F); //与0x3F保留bit[5:0],与0x0F保留bit[4:0]
18     }
19     /*als数据12位,buf[3]高8位,buf[2]低8位*/
20     *als = (unsigned short)buf[3] << 8 |buf[2];
21 }

在上面的程序中国中,先声明了一个数组,数组元素为6。

第6行开始,用了1个for循环,循环体执行力6次,因为IR的低位数据0x0A是6个寄存器的起始寄存器,我们就从这个寄存器开始读取,每次循环地址+1,读取下一个寄存器。for循环结束后,6个寄存器的值就拿到了。

从第9行开始是对数据进行换算。

buf[0]是0x0A的值,和0x80进行与运算是判定其bit[7]是否为1,1时IR、ALS数据无效,直接赋值为0,bit[7]不为1时,通过14、15行计算ALS、IR的值

15行计算IR,IR数据一共10位,buf[0]的bit[1:0]为IR数据低2位,buf[1]的bit[7:0]对应IR高8位,所以我们把buf[1]的值左移2位构成高位,buf[0]的值取bit[1:0],即与上0x03。两个结果或一下就是IR的值

17行计算PS,PS数据值为10位(只有值,因为PS对应寄存器还有数据无效、物体接近/远离判定。我们只留数据位)。buf[4]的bit[3:0]为低4位,buf[5]的bit[5:0]为高6位,计算方法就不说了,和上面一样

20行计算ALS数据,ALS数据为16位,buf[2]为低8位,buf[3]为高8位,所以就是将buf[3]左移8位后和buf[2]进行或运算。

最终代码

先是目录结构

 头文件

/**
 * @file bsp_ap3216c.h
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2022-01-22
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#ifndef __BSP_AP3216C_H
#define __BSP_AP3216C_H

#include "imx6ul.h"
#include "bsp_gpio.h"
#include "bsp_i2c.h"
#include "bsp_delay.h"

#define AP3216C_ADDR            0x1E
//寄存器地址
#define AP3216C_SYSTEMCONG      0x00
#define AP3216C_INTSTATUS       0x01
#define AP3216C_INTCLEAR        0x02
#define AP3216C_IRDATALOW       0x0A
#define AP3216C_IRDATAHIGH      0x0B
#define AP3216C_ALSDATALOW      0x0C
#define AP3216C_ALSDATAHIGH     0x0D
#define AP3216C_PSDATALOW       0x0E
#define AP3216C_PSDATAHIGH      0x0F

void ap3216c_init(void);
unsigned char ap3216c_readonebyte(unsigned char addr,unsigned char reg);
unsigned char ap3216c_writeonebyte(unsigned char addr,unsigned char reg,unsigned char data);
void ap3216c_readdata(unsigned short *ir,unsigned short*ps,unsigned short *als);
#endif
bsp_ap3216c.h

c文件

/**
 * @file bsp_ap3216c.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2022-01-22
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include "bsp_ap3216c.h"
#include "stdio.h"

/**
 * @brief AP3216C初始化
 * 
 */
void ap3216c_init(void)
{
    /*IO初始化*/
    IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL,1);
    IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL,0x70B0);

    IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA,1);
    IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA,0x70B0);

    /*I2C初始化*/
    i2c_init(I2C1);

    /*芯片初始化*/
    int value=0;
    ap3216c_writeonebyte(AP3216C_ADDR,AP3216C_SYSTEMCONG,0x4);      //软复位
    delay_ms(50);
    ap3216c_writeonebyte(AP3216C_ADDR,AP3216C_SYSTEMCONG,0x3);      //使能ir、ps、als
    value = ap3216c_readonebyte(AP3216C_ADDR,AP3216C_SYSTEMCONG);
    printf("ap3216c systemconfig reg = %#x\r\n",value);
}

//AP3216C读1byte数据 这里偷了个懒,理论上master_transfer返回值是错误类型,但是通过指针指向的val是真实读取的值
/**
 * @brief ap3216c读取一个byte数据
 * 
 * @param addr 3216的地址
 * @param reg  读取寄存器地址
 * @return unsigned char 读取数据
 */
unsigned char ap3216c_readonebyte(unsigned char addr,unsigned char reg)
{   
    unsigned char val = 0;
    struct i2c_transfer masterXfer;
    masterXfer.slaveAddress = addr;
    masterXfer.direction = kI2C_Read;
    masterXfer.subaddress = reg;
    masterXfer.subaddressSize = 1;
    masterXfer.data = &val;
    masterXfer.dataSize = 1;
    i2c_master_transfer(I2C1, &masterXfer);
    return val;
}

/**
 * @brief ap3216写1个寄存器
 * 
 * @param addr 3216从机地址
 * @param reg  写寄存器地址
 * @param data 数据
 * @return unsigned char 状态字:1异常,0正常
 */
unsigned char ap3216c_writeonebyte(unsigned char addr,
                                    unsigned char reg,
                                    unsigned char data)
{   
    unsigned char writedata = data;
    unsigned char status = 0;
    struct i2c_transfer masterXfer;
    masterXfer.slaveAddress = addr;
    masterXfer.direction = kI2C_Write;
    masterXfer.subaddress = reg;
    masterXfer.subaddressSize = 1;
    masterXfer.data = &writedata;
    masterXfer.dataSize=1;

    if (i2c_master_transfer(I2C1, &masterXfer)){
        status = 1;  //如果错误,返回1
    }
    return status;
}


// AP3216C数据读取
void ap3216c_readdata(unsigned short *ir,unsigned short*ps,unsigned short *als)
{
    unsigned char buf[6];

    for(int i=0;i<6;i++){
        buf[i] = ap3216c_readonebyte(AP3216C_ADDR,AP3216C_IRDATALOW +i);
    }
    if(buf[0] &= 0x80){     //buf[0][bit7]==1时,IR、PS数据无效
        *ir=0;
        *ps=0;
    }
    else{
        /*ir数据10bit buf[1]高8位,buf[0][bit1:0]为低2位*/
        *ir = ((unsigned short)buf[1]<<2) | (buf[0] & 0x03);    //与0x03,只保留低bit[1:0]
        /*ps数据10bit buf[5][bit5:0]高6位,buf[4][bit3:0]为低4位*/        
        *ps = (((unsigned short)buf[5] & 0x3F) <<4) | (buf[4] & 0x0F); //与0x3F保留bit[5:0],与0x0F保留bit[4:0]
    }
    /*als数据12位,buf[3]高8位,buf[2]低8位*/
    *als = (unsigned short)buf[3] << 8 |buf[2];
}
bsp_ap3216c.c

这里有个疑问:观察一下可以发现其实我们写的读一个数据和写一个数据那两个函数是和AP3216C这个芯片没什么关系的,任何I2C设备的读写都可以通过这两个函数进行,我不明白为什么要把这两个函数放在这个路径下而不放在I2C的驱动里。不知道和后期驱动开发有没有关系。

驱动调用

上面两部分完成后就可以在main函数调用这个驱动了

int main(void)
{   
    static unsigned int led_state = ON;
    int_init();
    imx6u_clkinit();
    clk_enable();
    delay_init();
        
    uart_init();
    led_init();
    beep_init();
   
    ap3216c_init();
    unsigned short ir,ps,als;
    while(1){
    led_state = !led_state;
    led_switch(LED0,led_state);
    ap3216c_readdata(&ir,&ps,&als);
    printf("ir=%d,ps=%d,als=%d\r\n",ir,ps,als);
    delay_ms(500);
    }
    return 0;
}

注意在循环体内部我们延时了500ms,因为在前面一章讲到过,在IR、PS和ALS都使能的情况下,每次读数时间间隔是由要求的

 我们用的模式就是0x3,按要求应该不小于232ms。所以延时了500ms。

最终的效果就是上面的图,ir是红外,不知道怎么测试,ps为接近传感器,越靠近芯片值越大,到1023就对应了2^10,数据就溢出了。als为环境光传感器,我用手机闪关灯照了芯片,能看到最高值为30000多,数据位数为16位最高能到65535。

相关