1

  前两天写了一篇文章举了个STM32开发里面使用宏提高可移植性的栗子(啊,有点饿),不过嘛,有时候光使用宏可能不太能满足需求。这次,我们举个结构体的例子。当然,还是基于STM32F1的标准库。

  (温馨提示:默认读者对IIC协议有一定了解)

  另外,链接在这里:https://segmentfault.com/a/11...

  ST提供的标准库里面大量使用了结构体,用于封装一些经常会被一同使用到的、而且往往是在描述同一个对象的属性的数据,比如,GPIO初始化结构体是这样的:

typedef struct
{ 
  uint16_t GPIO_Pin;             
  GPIOSpeed_TypeDef GPIO_Speed;  
  GPIOMode_TypeDef GPIO_Mode;   
  
}GPIO_InitTypeDef;

  这个结构体就封装了GPIO的基本设置信息——指定的引脚号、输出速率、模式。

  前面的文章里说到过,分层的设计可以提高程序的可移植性,而这种“分层”不仅可以通过宏来实现,也可以通过传参来实现。

  在最糟糕的情况下,我们驱动程序里用到的参数和驱动程序混杂在一起,可读性差、可移植性差;如果使用了一些宏来对参数进行封装,虽然我们都知道实际上编译器干的事情就是字符串替换。但是,其实也可以理解为我们专门建了个“仓库”来存放一些可能会变更的参数,这个“仓库”大概率位于你的驱动程序的“.h文件”的头部,你可以方便地找到它并更改其中的内容。

  举个例子,在用GPIO模拟IIC协议的时候你可以这么干:

#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN  GPIO_Pin_10

#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN  GPIO_Pin_11

  然后把IIC驱动程序里的GPIOB、GPIO_Pin_10之类的参数都换成自己定义的宏。

  但是,有个问题,如果你要模拟两条,或者更多的IIC总线呢?

  或许你觉得可以这样:

#define IIC1_SCL_PORT GPIOB
#define IIC1_SCL_PIN  GPIO_Pin_10
······

  但是,这样会很麻烦,因为你需要根据不同的情况去决定使用哪个宏,可能你会选择增加额外的参数,然后进行判断,决定调用某一套代码——是的,如果你坚持只是简单地使用宏来实现的话,你大概率得在函数里针对不同的情况分别进行实现,这太麻烦了。

  有没有啥更好的办法呢?

  我们来想一想,前面说利用宏的时候可以理解为建立了一个参数的“仓库”,那么,针对不同的情况,可以建立不同的仓库,同时,在调用时传入参数来决定调用某一个“仓库”里的数据,这样就会使你的实现变得更简洁。

  考虑到我们“仓库”里的数据所描述的对象——不同的模拟IIC总线,各个”仓库“里的数据应该拥有近似的类型,所以,可以采用结构体数组来存储。

  当然,首先我们得建个结构体。IIC总线有两根线,SCL和SDA,SCL和SDA又具有各自的模式、速率和引脚号,如果完全依照层次关系来建立结构体,我们的数据结构可能是描述引脚的结构体套着描述总线的结构体,又套了个描述设备的结构体······不不不!这么干伤头发,想办法简化一下吧,引脚的速率在IIC通信的时候一般直接设成最高一档就可以了;模式嘛,根据读取数据和发送数据的需要进行切换,不用从驱动程序以外进行控制(应用层程序无需负责);设备信息,一般会另外单开文件,而且有时候比较复杂,不太方便放进来。所以,这几个就不放到咱的结构体里了。

  那我们需要啥呢?SCL、SDA的端口号、引脚号,先放这几个,有需要再添加。

  写出来是这样:

typedef struct 
{
    GPIO_TypeDef *SCL_GPIOx;
    uint16_t SCL_Pin;
    
    GPIO_TypeDef *SDA_GPIOx;
    uint16_t SDA_Pin;
    
}birch_iic_bus_t;

  然后就可以建立一个数组来管理不同的模拟IIC总线:

birch_iic_bus_t birch_iic_bus[3] = {
    [0] =     {.SCL_GPIOx = GPIOB,.SCL_Pin = GPIO_Pin_5, 
             .SDA_GPIOx = GPIOB,.SDA_Pin = GPIO_Pin_6},
    
    [1] =     {.SCL_GPIOx = GPIOB,.SCL_Pin = GPIO_Pin_10, 
             .SDA_GPIOx = GPIOB,.SDA_Pin = GPIO_Pin_11},
    
    [2] =     {.SCL_GPIOx = GPIOA,.SCL_Pin = GPIO_Pin_5, 
             .SDA_GPIOx = GPIOA,.SDA_Pin = GPIO_Pin_6}
};

  然后,用个带参宏,把最基础的几个总线操作写出来:

#define BIRCH_IIC_SCL_H(bus) Birch_IIC_Set_Pin((bus)->SCL_GPIOx,(bus)->SCL_Pin)
#define BIRCH_IIC_SCL_L(bus) Birch_IIC_Reset_Pin((bus)->SCL_GPIOx,(bus)->SCL_Pin)

#define BIRCH_IIC_SDA_H(bus) Birch_IIC_Set_Pin((bus)->SDA_GPIOx,(bus)->SDA_Pin)
#define BIRCH_IIC_SDA_L(bus) Birch_IIC_Reset_Pin((bus)->SDA_GPIOx,(bus)->SDA_Pin)

#define BIRCH_IIC_READ_SDA(bus) GPIO_ReadInputDataBit((bus)->SDA_GPIOx,(bus)->SDA_Pin)

  能够对总线进行拉高拉低、读取电平状态这些操作之后,就可以开始实现通信协议了。

  哦,对了,操作引脚的函数是这样的:

void Birch_IIC_Set_Pin(GPIO_TypeDef *GPIOx,uint16_t Pin)
{
    GPIOx->BSRR = (uint32_t)Pin;
}
void Birch_IIC_Reset_Pin(GPIO_TypeDef *GPIOx,uint16_t Pin)
{
    GPIOx->BRR = (uint32_t)Pin;
}

  为了提高效率,直接操作寄存器了,调用库函数也可以实现功能(而且开O3优化以后速度差不了多少)。

  突出重点,引脚模式的切换放到一边,直接看时序的实现吧,如下:

void Birch_IIC_Start(birch_iic_bus_t *bus)
{
    Birch_IIC_SDA_OUT(bus);
    
    BIRCH_IIC_SDA_H(bus);
    BIRCH_IIC_SCL_H(bus);
    
    delay_us(2);
    
    BIRCH_IIC_SDA_L(bus);
        
    delay_us(2);
    
    BIRCH_IIC_SCL_L(bus);
}

  以起始条件为例,我们传入的参数应当是结构体数组中某个成员的首地址,然后,根据这一信息,设置相应IIC总线的SDA线为输出模式,拉高两根线保证总线处于没有进行通信时的常规状态,然后,延时一段时间,发送起始条件(拉低指定总线的SDA)。然后,再延时一段时间确保SDA线上的信号被接收到。之后再拉低SCL,准备开始发送数据。

  由于你已经了解了IIC通信的时序,相信知道了怎么传参以后,你能够很容易地实现终止条件、读取、发送等等操作了,这里不再啰嗦,需要参考的话请到文末下载代码。

  继续来发现问题和解决问题。

  以起始条件的发送函数为例,虽然能够根据传入的参数,选择某一指定的总线进行操作,但是,还有一个数据混杂在驱动程序里了——延时时间。如果延时时间是固定的,那么我们模拟的不同总线的速率就会是一样的,这可能没法满足实际使用的需要——有些传感器可能只支持100KHz速率的”标准模式“(虽然现在大部分都支持400K的模式),而有些IIC设备可能需要比较快的通信速率(比如屏幕)。所以,得想个法子为不同的总线指定不同的延时时间。

  怎么办呢?加个参数就好了:

typedef struct 
{
    GPIO_TypeDef *SCL_GPIOx;
    uint16_t SCL_Pin;
    
    GPIO_TypeDef *SDA_GPIOx;
    uint16_t SDA_Pin;
    
    uint8_t Speed_range;    //速率等级,值越小速率越高
    
}birch_iic_bus_t;

  定义一个无符号字符型数据,用于存储总线的”速率等级“,然后,写个根据不同情况进行延时的函数:

inline void Birch_IIC_Delay(birch_iic_bus_t *bus)
{
    if(0 == bus->Speed_range){
        __nop();__nop();__nop();
    }
    else if(1 == bus->Speed_range){
        delay_us(2);
    }
    else{
        delay_us(4);
    }
}

  当然,还得去改改结构体数组:

birch_iic_bus_t birch_iic_bus[3] = {
    [0] =     {.SCL_GPIOx = GPIOB,.SCL_Pin = GPIO_Pin_5, 
             .SDA_GPIOx = GPIOB,.SDA_Pin = GPIO_Pin_6,
             .Speed_range = 2},
    
    [1] =     {.SCL_GPIOx = GPIOB,.SCL_Pin = GPIO_Pin_10, 
             .SDA_GPIOx = GPIOB,.SDA_Pin = GPIO_Pin_11,
             .Speed_range = 0},
    
    [2] =     {.SCL_GPIOx = GPIOA,.SCL_Pin = GPIO_Pin_5, 
             .SDA_GPIOx = GPIOA,.SDA_Pin = GPIO_Pin_6,
             .Speed_range = 2}
};

(其实也可以设成值越大速率越高然后搞个”前进四!“啥的)

  还有一个需要稍微注意一下的地方是,读取函数应该设置一个参数用于决定是否发送响应,很多传感器一次会发送多个8位数据。

  为了提高程序的可读性,可以写成这样:

#define BIRCH_IIC_ACK    1        //用于决定是否发送响应,示意从机继续发送
#define BIRCH_IIC_NACK     0

  函数实现:

uint8_t Birch_IIC_Base_Read_Byte(birch_iic_bus_t *bus,uint8_t Ack)
{
    uint8_t receive = 0;
    Birch_IIC_SDA_IN(bus);
    
    for(uint8_t i=0;i<8;i++)
    {
        BIRCH_IIC_SCL_L(bus);
            Birch_IIC_Delay(bus);
        
        BIRCH_IIC_SCL_H(bus);
        
        receive = (receive << 1) | BIRCH_IIC_READ_SDA(bus);
            Birch_IIC_Delay(bus);
    }
    if(BIRCH_IIC_ACK == Ack)
        Birch_IIC_Send_Ack(bus);
    else
        Birch_IIC_Send_NAck(bus);
    
    return receive;
}

  调用的示例:Birch_IIC_Base_Read_Byte(&birch_iic_bus[0],BIRCH_IIC_ACK);

  还有,还有,IIC读取Ack可能会出现异常,为了不让程序卡死在这,得加个超时退出的机制,并注意返回错误提示,你可以这样实现:

#define BIRCH_IIC_STATUS_FAILED      1        //IIC通信状况
#define BIRCH_IIC_STATUS_OK            0
uint8_t Birch_IIC_WaitAck(birch_iic_bus_t *bus)
{
    uint8_t ucErrTime = 0;
    
    Birch_IIC_SDA_IN(bus);
    
    BIRCH_IIC_SCL_H(bus);
        Birch_IIC_Delay(bus);
    
    while(BIRCH_IIC_READ_SDA(bus))
    {
        ucErrTime++;
        if(ucErrTime > 250)
        {
            return BIRCH_IIC_STATUS_FAILED;
        }
    }
    
    BIRCH_IIC_SCL_L(bus);
        Birch_IIC_Delay(bus);
    
    return BIRCH_IIC_STATUS_OK;
}

  最后给大家看个实际使用的小例子,基于别人的程序移植过来的:

void single_write_Si7021(u8 REG_address)
{
    //IIC_Start();
    Birch_IIC_Start(&birch_iic_bus[0]);
    
    //IIC_Send_Byte((SLAVE_ADDR<<1)|0);
    Birch_IIC_Base_Send_Byte(&birch_iic_bus[0],SLAVE_ADDR<<1|0);
    //IIC_Wait_Ack();
    Birch_IIC_WaitAck(&birch_iic_bus[0]);
    
    //IIC_Send_Byte(REG_address);
    Birch_IIC_Base_Send_Byte(&birch_iic_bus[0],REG_address);
    //IIC_Wait_Ack();
    Birch_IIC_WaitAck(&birch_iic_bus[0]);
    
    //IIC_Stop();
    Birch_IIC_Stop(&birch_iic_bus[0]);
}

  注释掉的部分是原先的代码,可以看到,移植的时候程序结构基本不用变化,简单换换函数就行了。

  好了,要注意的地方都说完了,详细的,请看代码!

工程下载地址:https://www.lanzous.com/iajofpi

(还不是很完善所以暂时不扔到某hub上丢人)

(另:测试代码基于STM32F1 V3.5版本标准库)


ngHackerX86
22 声望24 粉丝

000000