前两天写了一篇文章举了个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版本标准库)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。