打印

C语言跨平台驱动封装

[复制链接]
1266|3
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
suaig|  楼主 | 2018-6-12 19:11 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
大家以前是不是经常找驱动,屏幕显示驱动啊,摄像头驱动啊,ADC0832之类得AD芯片得驱动啊。找驱动麻烦,而且找得不一定能用。要调试,有时候驱动是51或者其他平台得,还要移植。在逛国外论坛时候,学到过一种封装方式,是C语言基于面向对象+接口得封装方式,这种封装方式可以无任何修改得在任何支持C编译器的单片机上运行,从此,驱动不再分STM32上的,51上的。所以想用这个封装方式建立一个库管理。大家按标准封装得库,可以上传到库管理器。
大概的过程是,用结构体,去实现面向对象的封装,用函数指针作为接口用来和底层连接。另函数指针可以作为回调函数,去执行用户代码。
下面用一个LED封装为例子,去封装一个跨平台的库。
首先,我们要定义一个结构体,或者称为对象
  • typedef struct LED_struct LED_t;  

接下来,我们给定义一个特别的函数指针,
  • typedef uint8_t (*LED_msg_cb)(LED_t *ser,uint8_t msg);  //主要传递了结构体指针和消息两个参数

这个相当于C++里面的虚函数。定义对象的时候是不存在的,需要用户去实现几个具体功能。这个函数指针的类型呗重新定义了,叫LED_msg_cb 是LED驱动消息回调函数的新类型。
这里插个东西,解释一下uint8_t是什么为什么这样定义。
很多新手,甚至是老师,都喜欢用define 去定义变量的别名,例如uchar。但是这样不好,在嵌入式平台,不同单片机相同数据类型名字,长度可能是不一样的。而且用define,仅仅是作为一个字符串的替换
极容易出错,所以定义别名,一定要用typedef这样编译器就知道,啊,这是一个变量。然后,我们要说明这个变量是有符号还是无符号,长度等。例如uint8_t,这里就说明了u是一个无符号的整形,长度为8,后面的_t表示这不是编译器原生的,而是用户定义的,可以追根溯源的。
  • typedef unsigned char uint8_t;  

所以就有了诸如此类的定义。这样提高了代码,在各平台的可移植性。另外变量的命名除了i,j,这类用于循环的变量,其他变量千万不要乱写一半用驼峰命名,例如TimCounter或者Tim_Counter,这样一方面可以增加可读性,一方面,可以节约代码的注释量。注释代码是很累的,而且我觉得好的代码应该不需要大篇幅的注释,基本的语法规则大家都懂。这个变量干什么用,变量名已经告诉你了。另外函数的命名上,一般是模块名加下划线,加功能。例如
LED_On  LED_off。或者  xx_read。当然命名有大家的喜好成分在里面,这里只是插一句,如果大家喜欢,以后可以开篇说一下。(好吧,我自认为我代码风格还可以,学弟们不需要我长篇注解也能理解我封装的函数的用途)

回到正题
我们要用结构体去定义一个完整的LED的对象,主要包括。LED的基本信息,例如,当前是开还是关(这部分可有可无,可以提供读函数去读取)。与底层接口的函数指针,
  • struct LED_struct  
  • {  
  •     uint8_t Mode;//是高电平点亮还是低电平  
  •     LED_msg_cb self_cb;//函数指针  
  • };  

LED灯对象的内容比较简单,因为封装比较简单,基本上都和这个函数指针有关。

那么我们接下来怎么做。前面提到了消息,这个回调函数主要得能实现和硬件相关的所有功能,在这里,和硬件相关的全部功能,无外乎,IO口的初始化,IO高低电平的输出,读状态。以及可能用到的延时函数。

下面我们就定义消息了。这里可以用宏,也可以用枚举去定义,我喜欢用宏。
  • #define LED_MSG_INIT  0x40  
  • #define LED_MSG_SET   0x41  
  • #define LED_MSG_RESET 0x42  
  • #define LED_MSG_READ  0x43  
  • #define LED_MSG_DELAY 0x44  

这里,的定义是完全按照上面说的定义的,现在我们去实现具体的内容(对,具体IO操作我们还不写。)
下面,我们这个驱动,需要完成几个功能,关闭LED,开启LED,读LED状态,翻转LED,最后为了突出函数指针是硬件的接口,我们再设计一个LED_Blink函数


等等我们还需要一个函数new_LED,帮助我们初始化这个对象。初始化一些参数,例如Mode啊。和函数接口啊,如果不new,我们就操作对象,那么就会软复位,为什么会软复位,待会讲。
new要做的操作很简单,无非就是给结构体中的Mode和函数指针赋值。我这个函数指针,

  • void new_LED(LED_t *self,uint8_t Mode,LED_msg_cb led_cb)  
  • {  
  •     self->Mode = Mode;  
  •     self->led_cb = led_cb;  
  • }  

这里一个new就完成了。当然,为了可读性,我们要对Mode定义个枚举或者宏。增强可读性

  • #define LED_MODE_LOW_ON    0  
  • #define LED_MODE_HIGH _ON  1  


这里函数指针部分你可能还没看懂,待会,使用的时候就懂了,别急。

下面是初始化LED
LED_Init函数。
下面我们来看看怎么使用函数指针作为接口,

这里我们传入的是self的LED结构体指针,里面的接口是led_cb.那么我们调用他,self->led_cb(self,LED_MSG_INIT);这里就会调用函数指针,指向的函数,并传递参数。
这样如果直接写入代码,如果你代码要给别人看,那么不好意思,看起来很不友好。那么不妨用宏包一下,
  • #define LED_M_INIT()   (self->led_cb(self,LED_MSG_INIT))  
  • #define LED_M_SET()    (self->led_cb(self,LED_MSG_SET))  
  • #define LED_M_RESET()    (self->led_cb(self,LED_MSG_RESET))  
  • #define LED_M_READ()    (self->led_cb(self,LED_MSG_READ))  
  • #define LED_M_Delay()    (self->led_cb(self,LED_MSG_DELAY))  

这样看起来就好多了。这里宏用括号括起来,以免后面写连续语句的时候造成编译器的误判

后面是函数的实现
  • void new_LED(LED_t *self,uint8_t Mode,LED_msg_cb led_cb)  
  • {  
  •     self->Mode = Mode;  
  •     self->led_cb = led_cb;  
  • }  
  • void LED_Init(LED_t *self)  
  • {  
  •     LED_M_INIT();  
  • }  
  • void LED_On(LED_t *self)  
  • {  
  •     (self->Mode == LED_MODE_LOW_ON) ? LED_M_RESET():LED_M_SET();  
  •     //三目运算,如果模式是低电平打开灯,那么开灯就把IO配置成低电平,否则输出高  
  • }  
  • void LED_off(LED_t *self)  
  • {  
  •     (self->Mode == LED_MODE_LOW_ON) ? LED_M_SET():LED_M_RESET();  
  •         //三目运算,如果模式是低电平打开灯,那么关灯就把IO配置成高电平,否则输出低  
  • }  
  • uint8_t LED_read(LED_t *self)//读取灯的状态  
  • {  
  •     if(self->Mode == LED_MODE_LOW_ON)  
  •     {  
  •         return !LED_M_READ();  
  •     }  
  •     else  
  •     {  
  •         return LED_M_READ();  
  •     }  
  •     //这里我们定义返回1表示灯是开着的,读到的是0表示灯是关的,为了提高可读性,我们同样的,前面定义宏  
  •     //#define LED_IS_ON     1u  
  •     //#define LED_IS_OFF    0u  
  •     //  
  • }  
  • void LED_Toggle(LED_t *self)  
  • {  
  •     (LED_read(self) == LED_IS_ON) ? (LED_off(self)) : (LED_On(self));  
  • }  
  • void LED_Blink(LED_t *self)  
  • {  
  •     LED_Toggle(self);  
  •     LED_M_Delay();  
  • }  

关键的地方来了,这个方法目前看起来,并没有什么有点,而且封装似乎繁琐。

下面怎么使用这个库呢?

我们回到我们的Main,
第一步包含头文件。
定义一个新的LED灯对象。这里我定义2个,好演示面向对象和代码复用的特性。
LED_t LEDA,LEDB;
下面我们需要new这个对象,但是new之前,我们需要实现一个函数,结构和我们LED对象里的函数指针是一样的,传递和返回值,一样,
结构是这样的,
  • uint8_t AT8951_LED_CB(LED_t *ser,uint8_t msg)  
  • {  
  •     switch(msg)  
  •     {  
  •         case LED_MSG_INIT:  
  •             break;  
  •         case LED_MSG_SET:  
  •             break;  
  •         case LED_MSG_RESET:  
  •             break;  
  •         case  LED_MSG_READ:  
  •             break;  
  •         case LED_MSG_DELAY:  
  •             break;  
  •     }  
  • }  

这是消息机制的一种处理方式。
然后我们在对应的地方去完成其功能,我用51演示的,所以INIT功能直接留空就行,然后这里有一个对象的问题,这个LED_t是传过来的对象,我们既然是多对象,这里就要判断一下,对象是哪个对象。不同对象的行为是不一样的,这又是C++的思想。就比如我和你都是人类,但是我们打人方式不同。
我这里完整的实现。

  • uint8_t AT8951_LED_CB(LED_t *ser,uint8_t msg)  
  • {  
  •     switch(msg)  
  •     {  
  •         case LED_MSG_INIT:  
  •             break;  
  •         case LED_MSG_SET:  
  •             if(ser == &LEDA)//这里要取对象的地址去判断,C语言无法直接判断复合类型相等。  
  •             {  
  •                 P1 |= 0x01;  
  •             }  
  •             if(ser == &LEDB)  
  •             {  
  •                 P1 |= 0x02;  
  •             }  
  •             break;  
  •         case LED_MSG_RESET:  
  •             if(ser == &LEDA)//这里要取对象的地址去判断,C语言无法直接判断复合类型相等。  
  •             {  
  •                 P1 &= ~0x01;  
  •             }  
  •             if(ser == &LEDB)  
  •             {  
  •                 P1 &= ~0x02;  
  •             }  
  •             break;  
  •         case  LED_MSG_READ:  
  •             if(ser == &LEDA)//这里要取对象的地址去判断,C语言无法直接判断复合类型相等。  
  •             {  
  •                 return (P1&0x01);  
  •             }  
  •             if(ser == &LEDB)  
  •             {  
  •                 return (P1&0x01);  
  •             }  
  •             break;  
  •         case LED_MSG_DELAY:  
  •             if(ser == &LEDA)//这里要取对象的地址去判断,C语言无法直接判断复合类型相等。  
  •             {  
  •                 Delay100ms();  
  •             }  
  •             if(ser == &LEDB)  
  •             {  
  •                 Delay50ms();  
  •             }  
  •             break;  
  •     }  
  •     return 0;  
  • }  

现在我们来new,
        new_LED(&LEDA,LED_MODE_LOW_ON,AT8951_LED_CB);
        new_LED(&LEDB,LED_MODE_HIGH_ON,AT8951_LED_CB);

这里对LEDA,LEDB,是不一样,一个高电平点亮,一个低电平点亮。
然后我们new完就可以Init了
LED_Init(&LEDA);LED_Init(&LEDB);
这样就相当于调用了我们传递函数里面消息INIT部分代码,
现在先全部关闭LED。LED_off(&LEDA);LED_off(&LEDB);

我这里没加限流电阻,大家不要学。然后可以看到,LED灯都是关闭的,现在打开。LED_on(&LEDA);LED_on(&LEDB);


致此,我们以及完成了对象化,以及面向对象的多态。完成了面向对象的最大的特性,继承。

至此我们完成了,和硬件平台的对接,利用函数指针。我此前封装了一些库,比如跨平台的串口缓冲区封装,可以在各种单片机上运行,以及伪多线程封装。以后有时间再更。伪多线程其实有两个版本,从最初的我学习了状态机和时基之后写的初代版本,到后来发现外国人写的ProtoThreads线程库,然后我加入时基形成目前我所使用的库。这些库,都使得开发效率大幅度提升,现在单片机基本不缺资源,快速开发,占领市场是目前所需要的。
晚上我会从国外技术论坛移植一篇**,我觉得非常棒,是关于用串口UART去驱动18B20的。大家如果感兴趣,可以按此封装方式封装其他的库,目前我封装了,串口,PID,595等。现在最缺的还是需要懂网络服务器的朋友,去开发一个嵌入式库管理搜索软件。哈哈,封装例子在附件。需要的可以下载详细看一下,另外,设计库的时候注意函数调用深度,竟可能不要使用递归,这会影响代码在不同平台的移植,另外基本上在51上能正常的,其他平台大多没问题。51的传参是靠着几个寄存器,传参有限,写库的时候注意寄存器数量是否够,不够需要使用模拟堆栈,可重入关键字,使得函数使用模拟堆栈,实现可重入,但是这里也要注意模拟堆栈的大小问题。大概就这些了。非常非常期待,大家封装的库。个人能力实在有限,下面有我目前封装的库,有些库发给别人的,我写的注解就详细点,有些自用的,就。。嘿嘿,你懂的


库.zip (18.17 MB)


pro.zip

57.22 KB

封装例子

相关帖子

沙发
suaig|  楼主 | 2018-6-12 19:12 | 只看该作者
仿真是proteus 8.7 sp3

使用特权

评论回复
板凳
suaig|  楼主 | 2018-6-12 21:42 | 只看该作者
怎么目录找不到我帖子,奇怪。

使用特权

评论回复
地板
suaig|  楼主 | 2018-6-12 21:50 | 只看该作者
??奇了怪了

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

4

主题

19

帖子

2

粉丝