大家以前是不是经常找驱动,屏幕显示驱动啊,摄像头驱动啊,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)
|