在这个专栏中,本喵要实现一个智能家居的小项目,先基于HAL库实现裸机版本,之后再实现一个RTOS版本,为了无缝实现从裸机到RTOS的移植以及维护,本喵会使用面向对象的思想,将整个项目分层来实现,构建一种编程架构。
本项目重点:
- 设计出优秀的程序框架:容易扩展、容易维护。
- 具体:
- 把项目拆分为各个子系统。
- 使用面向对象的思想,把子系统抽象为结构体。
- 编写函数时,有一定的封装细节,看函数名就知道怎么用,不需要深入函数内部看它的实现。
如上图,使用百问网的STM32F103ZET6
开发板,实现:
如上图所示,在程序设计过程中,分为几个层次:
如上图所示,在本项目中,可以分为6个子系统:
其中业务子系统包含其余5个子系统,可以看作是上层,并且同样也可以看作一个子系统。
首先来实现输入子系统,它可以接收来自按键,网络,标准输入等设备的数据,然后供上层业务子系统去使用。整个输入子系统划分为五个层次实现,这里本喵仅实现按键一个输入设备。
- 对于传递的"数据数据",我们把它称为"输入事件"。
如上图,在input_system.h
输入子系统头文件中定义输入事件结构体,用来描述发生的输入事件,无论是按键输入还是网络以及标准输入,都会创建一个这样的结构体对象,但是INPUT_EVENT_TYPE
不同,只有根据该成员变量的值才可以确定发生了哪种输入,通过其他成员变量可以获取到需要的事件属性,比如发生事件,按键编号,以及字符串数据等等。
输入事件类型有多种,在这个项目中并不会用到触摸屏输入,本喵这样写是为了表明拓展维护的方便性,在输入子系统层面,需要增加输入事件类型,以及描述输入事件的结构体InputEvent
中增加触摸屏触摸的位置。
接下来就是输入事件的来源了,从框图中看到有按键输入,网络输入,标准输入,以后甚至可以扩展更多的输入来源,这些输入来源产生输入事件。
- "输入事件"由"输入设备"产生。
如上图,在input_system.h
输入子系统头文件中定义输入设备结构体,用来描述输入设备,每一个设备都会创建一个这样的结构体对象,其中包含设备的名称,获取输入事件,初始化设备,去初始化设备等方法,以及下一个设备节点的指针。
每一个设备都自带获取输入事件的方法,也就是获取InputEvent
对象的函数,站在输入子系统的层面,它并不关心该方法是如何实现的,只在需要获取输入数据的时候直接调取该方法即可。
包括初始化和去初始化也是设备自带的方法,上层只需要直接调用即可,至于去初始化是在不需要某个设备的时候,将其配置恢复到初始化状态,从系统中抹除该设备。
为了管理多个设备,本喵将其放在一个链表中,所以还有一个pNext
指针指向下一个设备节点。
- 在输入子系统层面,并不关心获取输入事件函数是如何实现的,而且该函数的实现涉及到了硬件底层,所以并不在子系统层面实现。
如上图,在input_system.c
源文件中,创建一个全局链表,用来让输入子系统管理输入设备,并且实现注册输入设备,增加输入设备,初始化所有输入设备等函数。
注册输入设备的本质就是将新增加的输入设备节点插入到链表中,让输入子系统能够通过操作链表来维护使用输入设备。
增加输入设备也是输入子系统要处理的事情,在增加输入设备函数中再调用增加具体输入设备的函数,需要增加多少输入设备,就将对应设备的增加函数放进去。
初始化所有输入设备的时候,只需要变量链表中的设备节点,调用每个设备节点自带的初始化化函数即可。
- 这三个函数要在
input_system.h
中声明。
无论是一个输入设备还是多个输入设备,所产生的数据并不只一个,但是使用者只有输入子系统一个,为了防止数据丢失,所以这些数据也需要维护起来,这里使用环缓冲区列来维护,主要有输入事件产生,就将相应的InputEvent
对象放入环形缓冲区中,子系统只需要从环形缓冲区读取数据就可以,不用关心数据是怎么来的。
如上图所示,环形缓冲区本质上也是一个数组,就拿写来说,当这个数组被写满后ring_buffer[7] = data
,就通过取模运算pW = (7 + 1) % 8 = 0
重新从数组的起始位置开始写数据,读也是类似的道理。
- pR是向环形缓冲区读数据时的下标。
- pW是向环形缓冲区写数据时的下标。
通过pR
是否等于pW
来判断环形缓冲区中是否有数据,没有数据就相等,有数据就不相等,同样通过pW
是否等于pR
来判断环形缓冲区是否写满数据,相等就写满了,不相等就没写满。
如上图所示,定义环形缓冲区结构体,通过维护pW
和pR
来维护环状,以及从存放输入事件的buffer
中读写事件。
输入子系统还需要提供读写数据的方法:
如上图所示,创建一个全局的环形缓冲区对象,由于是静态全局变量,且没有初始化,所以编译器会用0去初始化,并放在未初始化数据段,读写事件都是在操作这个全局的环形缓冲区。
此时,输入子系统已经具有了上图所示结构以及对应的操作方法,输入子系统的层就完成了,到目前位置丝毫没有提及到和STM32F103ZE
开发板有关的内容,连一句相关的代码也没有,实现了应用层和硬件的解耦。
此时输入子系统中的上层部分已经完成了,还需要处理输入子系统设备层,这里本喵仅实现按键输入设备:
如上图所示,在gpio_key.h
中定义了两个按键的编号,之后直接使用即可。
如上图所示,在gpio_key.c
中实例化出一个按键对象,并进行初始化,赋值设备名,初始化函数等,还要提供一个增加按键设备的函数AddInputDeviceGPIOKey
供应用层在初始化所有设备时候调用。
- 对于裸机程序,事件获取方法不用注册到设备队列中,而是在后面中断函数中调用。
此时,已经实现了按键的设备层,包括按键设备的实例化,按键设备的初始化方法,以及增加按键设备的方法。
本喵想让这个系统支持多个系统,包括裸机,FreeRTOS,RT-Thread,甚至是Linux,这里将裸机也看作是一种内核。
不同内核下的数据来源:
内核抽象层中,根据不同的内核对按键进行初始化,本喵这里仅实现裸机的按键初始化:
如上图,初始化按键的时候,调用KAL_GPIOKeyInit
,在函数内部再调用不同内核对按键的初始化函数,对于裸机则调用芯片层的CAL_GPIOKinit
函数进行初始化,如果是RTOS,则仅需要将该函数改成对应的初始化函数即可。
- 设备抽象层调用的是该层的
KAL_GPIOKeyInit
,根本不关心具体的实现逻辑。
在描述输入事件的结构体InputEvent
中有一个time
成员变量用来记录事件发生的事件,而这个时间在不同的内核中表现方式不同,所以在内核抽象层需要实现获取时间的函数。
如上图,在使用的时候,直接调用内核抽象层的KAL_GetTime
获取时间即可,在该函数内部,根据具体的获取方式调用对应的函数。
如本喵使用的STM32F103ZET6
是通过滴答定时器来获取时间的,需要获取芯片中寄存器的值,所以要调用CAL_GetTime
从芯片获取时间。
对于Linux,它在系统内部会记录着时间,此时就可以直接返回时间,不用再向下调用。
此时,内核抽象层也实现了,设备层会调用内核抽象层的初始化函数。
项目的最终实现需要依托具体的芯片,本喵用的STM32F103ZET6
是支持HAL
库的,但是也有一些芯片并没有HAL
库,需要用它自己的库来操作,所以在这一层要实现对不同类型芯片的支持。
如上图,在芯片抽象层会调用CAL_GPIOKeyInit
来初始化按键,在函数内部根据不同的芯片再调用它对应的初始化函数,如ST芯片就调用KEY_GPIO_ReInit
。
同样,不同芯片获取时间的方式也不同,这里也要实现针对不同芯片获取时间的方式:
如上图所示,从芯片寄存器中获取时间的时候,对于ST芯片,调用HAL_GetTick
获取即可,对于其他芯片,放入对应的获取方式即可。
此时芯片抽象层也实现了,内核抽象层会调用该层的CAL_GPIOKeyInit
初始化按键。
本喵使用的是STM32F103ZET6
芯片,使用CubeMX
和HAL
库进行按键初始化,在初始化的时候,要在中断函数中进行输入数据的读取,并放入环形缓冲区中。
如上图,在driver_key.h
中进行一些芯片的资源定义,方便后面使用。
如上图,使用HAL
库对按键进行初始化,在按键中断函数中处理输入事件InputEvent
并且放入到环形队列中
此时,具体芯片的硬件配置也设置好了,输入子系统中按键设备就完全写好了。
如上图,现在整个代码结构是这样,其中智能家居项目部分全部放在了smartdevice
文件夹中,包含输入子系统的应用层,设备抽象层,内核抽象层,芯片抽象层。
其余部分是通过CubeMX
进行的基本外设配置,整个输入子系统中,只有在硬件操作的时候会用到这里的配置,其余四层都是独立的,不存在耦合。
为了观察按键按下后的现象,使用串口将发生的输入事件InputEvent
打印出来,此时串口配置并不属于我们实现的输入子系统,只是一个调试工具,直接使用HAL
库配置就可以。
如上图所示是串口的头文件,只包含串口的使能和失能函数声明。
如上图所示是串口的具体配置函数,这里同样需要一个环形缓冲区,这里本喵就不展示它的实现了,后面本喵会放源码。
在调用EnableDebugIRQ
打开串口后,在向串口发送数据的时候,直接调用printf
即可,因为printf
底层会调用fputc
函数,所以需要在这里将fput
重定向,使得printf
符合我们的要求。
在fputc
中,先将发送完成标志清0,然后调用HAL
库的中断发送函数发送一个字节,当发送完成标志位为0时就一直等待,说明没有发送完成。这个字节发送完成以后,会进入串口的发送中断回调函数,在中断函数中将发送标志位置1,让fputc
退出循环等待。printf
发送多个字节就调用多次fputc
。
在获取串口发送来的数据时,直接调用scanf
即可,因为scanf
底层会调用fgetc
函数,所以也需要重定向fgetc
函数,使得scanf
符合我们的要求。
当串口上有数据到来时,会发生串口中断,通过判断SR
寄存器的第五位确定是接收到了数据,并且将接收到的数据放入到环形缓冲区中。fgetc
直接从环形缓冲区中读取数据。
- 为了像在PC端一样使用标准库中的
printf
和scanf
,必须重新实现fputc
和fgetc
函数,让终端变成串口,符合我们的要求。
为了看我们设计的输入子系统是否正确,需要专门写一个单元测试函数来测试一下:
如上图所示,将按键设备添加到输入子系统中,然后进行初始化,在while(1)
循环中读取输入事件,并通过串口打印输入事件的信息。
在main
函数中调用该测试函数,通过串口调试助手查看打印信息:
如上图,将板子的串口和电脑连在一起后,通过串口调试助手可以看到,当按键1或者按键2按下后,会打印出发生的事件信息,包括事件类型,发生事件,按键编号,以及按键值,说明设计的输入子系统是成功的。
这部分代码是在OLED代码的基础上写的,包含源码以及串口调试工具,需要的小伙伴自取传送门。
这篇文章实现了智能家居项目中输入子系统中的按键设备,最重要的是介绍的代码框架和编程思想,之后的项目部分都会按照这个思路来扩展维护。