嵌入式的编程思想:应用层与硬件层的“藕断丝连”
要做到嵌入式应用的代码逻辑清晰,且避免重复的造轮子,没有好的应用架构怎么行:
1. 如果没有好的架构,移植将会是一件很痛苦的事情;
2. 如果没有好的架构,复用是最大的难题,没法更大限度的复用原有的代码;
3. 如果没有好的架构,一旦驱动改了,所有的地方都要改,费时费力且很容易出错;
4. 如果没有好的架构,应用层中穿插着硬件驱动层的代码,看着会是一片混乱,逻辑不清,代码维护起来会很困难。
我们这里将代码分为两部分:应用层和硬件层,两者的关系如下:
假设一个场景:
老师给学生布置了作业,学生收到作业后开始写作业,写完之后通知老师查看,老师查看之后就可以回家。老师给学生发出“写作业”的指令(应用层向硬件层发送“指令”),学生完成后向老师报告“我写完作业了,等待您的批阅”的信息(硬件层向应用层发送信息)。如果还不清楚,我们使用STM32触摸/显示实验来说明一下硬件层面和应用层面的关系:
这里的硬件是TFTLCD,既可以被STM32驱动从而显示指定图形,也向STM32发送由于屏幕被触摸所产生的信号。应用层在这里作为一个长传下达的中间者,在编程时,我们最为困惑的是:那么不知道你们有没有碰到另外一种情况,就是应用程序需要采集硬件层的数据,比如串口接收数据,按键采集、ADC值采集。这种硬件层的数据怎么通知应用层来拿,或者怎么主动给它?
我们以往最简单粗暴的方式是不是就是用一个全局变量,比方说硬件层串口接收到数据来了,那么我们把数据丢到数组里,然后把接收完成全局变量标志位置1。
这样做当然可以实现功能,但是会存在移植性很差的问题。比如说你们老板让你把这个串口的硬件层封装起来给客户用,但不能让客户看到你实现的源代码,只提供接口(函数名)给对方用。那么这时候难道你要告诉客户先判断哪个变量为1,然后再取哪个数组的数据这么LOW的做法吗?
此时,我们要用到“回调函数“的概念了,在嵌入式中,回调函数说白了就是”硬件层面“向”应用层面“传输信息的一种比较牛逼的说法而已。回调函数在应用层和硬件层之间充当如下角色:
我们可以这样理解:当我们按下按键KEY1时,LED1状态反转。但是我们前面说过一个好的程序,各个硬件代码之间无相互联系。但是我们这里有要求KEY1动作引起LED1动作,这不是自相矛盾吗?其实,各个硬件之间的关联已不应属于硬件.c文件的内容了,各个硬件之间的关系应该交给应用层.c文件来完成。但是又有一个问题:获取KEY1被按下的信号的函数是在KEY1.c中的呀?那你咋传递出来”KEY1被按下”这个信号呢?这就要求我们熟悉C语言面向对象的编程思路了:
1. 当KEY1动作时,调用KEY1_Action函数,该函数在key.c文件中仅仅是声明,其参数是KEY_ID(哪个按键)和KEY_STATUS(该按键的状态);
2. 在应用层的.c文件中定义KEY1_Action函数的具体实现过程。
这样,当我们想要修改KEY1的响应动作时,我们只需修改KEY1_Action函数的实现过程即可。这样大大提高了代码复用性。这里的KEY1_Action函数就是我们所说的“硬件响应函数”。
在上述老师和学生的距离中,回调的概念,在这里面就体现的淋漓尽致,在这里面有两个角色,一个是老师,一个是学生。老师有两个动作,第一个是布置作业,第二个是查看作业。而学生有一个动作是做作业, 那么问题来了,老师并不知道学生何时才能做完作业,所以比较优雅的解决办法是等学生的通知,也就是学生做完之后告诉老师就可以。这就是典型的回调理念。
那么在编程中,该如何体现? 从上面的分析中,可以得出来回调模式是双方互通的,老师给学生布置作业,学生做完通知老师查看作业。
关于回调,这里面还分同步回调和异步回调两种模式:
1. 同步模式:
如果老师在放学后,给学生布置作业,然后一直等待学生完成后,才能回家,那么这种方法就是同步模式。就是硬件层面只要不回传给应用层面信息,应用层面就不会执行任何动作,就相当于“卡机了”,一动不动直至硬件层面回传信号。这种在嵌入式中也有用到,在轮询从USART接收数据时,我们经常会写“while(USART_GetFlagStatus(USART_FLAG_RXNE) != RESET)”,这句代码的含义就是“当我们没有从USART接收数据时,我们不做其他任何事情仅仅关注USART是否回传信息”,除非从USART接收到数据,否则就陷入死循环中无法前进。
2. 异步模式:
如果老师在放学后,给学生布置作业,这个时候老师并不想等待学生完成,而是直接就回家了,但告诉学生,如果完成之后发短信通知自己查看。这种方式就是异步的回调模式。我们在嵌入式中用的最多的就是“异步模式的回调函数”,例如:if(KEY1被按下) {…}else{…},每当一次轮询到来时,只要满足条件就会执行相应的程序代码,这种if循环不会阻止程序进一步执行。
这里还有一点要注意:(硬件的.c文件)key.c文件中除初始化/对外接口函数外,其他函数均被声明为static类型,这样可以保证这些函数只对于本key.c文件的成员可见,对于其他.c文件来说即使调用了key.h文件也无法使用这些函数。
下面我们用实例来说明“what is一个完美的(只是我个人认为)的嵌入式代码”:
1. key.h
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
#define KEY0_STATUS GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)
typedef enum
{
KEY_ID0,
}KeyID_TypeDef;
typedef enum
{
KEY_RELEASE,
KEY_PRESS,
}KeyStatus_TypeDef;
typedef void(*pKeyActionCallBack_TypeDef)(KeyID_TypeDef KeyID, KeyStatus_TypeDef KeyStatus);
void KEY_Init(void); //IO初始化
void KeyActionCallBackRegister(pKeyActionCallBack_TypeDef pKeyActionCB); // KEY动作响应函数
static KeyStatus_TypeDef KeyScan(void); //按键扫描函数
void KeyAction(void); //按键动作函数
#endif
上述代码中声明了“响应函数需要实现形式”,并且定义了一个 “用于将应用层实现的响应函数句柄赋值给key.c中静态全局变量”的函数——响应函数注册函数,注意:这个静态全局变量拥有static的属性,不会被外界看见,只对本文件内部的函数成员可见。其中,key.c对外的接口函数只有三个:初始化函数KEY_Init,按键动作函数KeyAction,响应函数注册函数KeyActionCallBackRegister。
2. key.c
#include "stm32f10x.h"
#include "key.h"
#include "sys.h"
#include "delay.h"
//按键初始化函数
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOE, &GPIO_InitStructure);
}
static pKeyActionCallBack_TypeDef pKeyActionCallBack;
void KeyActionCallBackRegister(pKeyActionCallBack_TypeDef pKeyActionCB)
{
if(pKeyActionCB != 0)
{
pKeyActionCallBack = pKeyActionCB;
}
}
static KeyStatus_TypeDef KeyScan(void)
{
if(KEY0_STATUS == 1)
{
return KEY_PRESS;
}
else
{
return KEY_RELEASE;
}
}
void KeyAction(void)
{
KeyStatus_TypeDef KeyStatus = KeyScan();
if(KeyStatus == KEY_PRESS)
{
(*pKeyActionCallBack)(KEY_ID0,KEY_PRESS);
}
else
{
(*pKeyActionCallBack)(KEY_ID0,KEY_RELEASE);
}
}
在这段代码中,静态全局变量pKeyActionCallBack(函数句柄指针)被声明,但具体实现形式在应用层.c文件(main.c)中被定义。指针有这样一条属性:声明和定义可分离。如果我们声明一条函数句柄,就不具备这样的属性了。
3. main.c
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
void KeyActionHandler(KeyID_TypeDef KEYID,KeyStatus_TypeDef KEYSTATUS)
{
if(KEYID == KEY_ID0)
{
if(KEYSTATUS == KEY_PRESS)
{
LED0 = !LED0;
}
else
{
return;
}
}
}
int main()
{
KEY_Init();
delay_init();
LED_init();
while(1)
{
KeyActionCallBackRegister(&KeyActionHandler);
KeyAction();
delay_ms(100);
}
}
这段代码中,我们尤其要注意应用层实现的响应函数的具体形式:
void KeyActionHandler(KeyID_TypeDef KEYID,KeyStatus_TypeDef KEYSTATUS)
{
if(KEYID == KEY_ID0)
{
if(KEYSTATUS == KEY_PRESS)
{
LED0 = !LED0;
}
else
{
return;
}
}
}
当我们使用我们的key.c中的函数之前,务必先注册一下我们在key.c中定义的静态全局变量pKeyActionCallBack(函数句柄指针),因为我们的KeyAction函数是基于pKeyActionCallBack(函数句柄指针)实现的:
void KeyAction(void)
{
KeyStatus_TypeDef KeyStatus = KeyScan();
if(KeyStatus == KEY_PRESS)
{
(*pKeyActionCallBack)(KEY_ID0,KEY_PRESS);
}
else
{
(*pKeyActionCallBack)(KEY_ID0,KEY_RELEASE);
}
}
我们如果采用外部中断服务函数来实现这个功能,我们该怎么做呢?很简单,我们只需将“注册key.c中定义的静态全局变量pKeyActionCallBack(函数句柄指针)”这一步放在中断服务函数中进行即可。采用外部中断的代码如下所示:
1. key.h
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
#define KEY0_STATUS GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)
typedef enum
{
KEY_ID0,
}KeyID_TypeDef;
typedef enum
{
KEY_RELEASE,
KEY_PRESS,
}KeyStatus_TypeDef;
typedef void(*pKeyActionCallBack_TypeDef)(KeyID_TypeDef KeyID, KeyStatus_TypeDef KeyStatus);
void KEY_Init(void); //IO初始化
void KeyActionCallBackRegister(pKeyActionCallBack_TypeDef pKeyActionCB); // KEY动作响应函数
static KeyStatus_TypeDef KeyScan(void); //按键扫描函数
void KeyAction(void); //按键动作函数
#endif
上述代码中声明了“响应函数需要实现形式”,并且定义了一个 “用于将应用层实现的响应函数句柄赋值给key.c中静态全局变量”的函数——响应函数注册函数,注意:这个静态全局变量拥有static的属性,不会被外界看见,只对本文件内部的函数成员可见。其中,key.c对外的接口函数只有三个:初始化函数KEY_Init,按键动作函数KeyAction,响应函数注册函数KeyActionCallBackRegister。
2. key.c
#include "stm32f10x.h"
#include "key.h"
#include "sys.h"
#include "delay.h"
//按键初始化函数
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0-KEY2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4
//初始化 WK_UP-->GPIOA.0 下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}
3. exit.c
#include "exti.h"
#include "led.h"
#include "key.h"
#include "delay.h"
#include "usart.h"
void KeyActionHandler(KeyID_TypeDef KEYID,KeyStatus_TypeDef KEYSTATUS)
{
if(KEYID == KEY_ID0)
{
if(KEYSTATUS == KEY_PRESS)
{
LED0 = !LED0;
}
else
{
return;
}
}
}
//外部中断0服务程序
void EXTIX_Init(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //使能复用功能时钟
//GPIOA.0 中断线以及中断初始化配置 上升沿触发 PA0 WK_UP
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Line=EXTI_Line0;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键WK_UP所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
}
//外部中断0服务程序
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)==SET) //WK_UP按键
{
KeyActionCallBackRegister(&KeyActionHandler);
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位
}
注意:我们这里在中断服务函数中只进行key.c中定义的静态全局变量pKeyActionCallBack(函数句柄指针)的注册,不进行KeyAction函数的调用,因为中断函数如果执行过多的程序长时间不退出运行,会导致通信的时序混乱(例如:当我们的中断服务函数内容很多且MCU与外部设备进行IIC通信时,中断服务函数执行期间除非有更高级别的中断打断其运行否则是不会理会其他程序运行的请求的,这就会导致外部设备传回的信息MCU无法及时接收,导致双方通信混乱,因此在中断服务函数中只建议做一些简单的操作(例如:赋值,简单的技计数操作…等))。
4. main.c
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "exti.h"
int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
LED_Init(); //初始化与LED连接的硬件接口
KEY_Init(); //初始化与按键连接的硬件接口
EXTIX_Init(); //外部中断初始化
while(1)
{
KeyAction();
delay_ms(100);
}
}
我们对比“轮询判断KEY0状态”的程序,我们发现:进行key.c中定义的静态全局变量pKeyActionCallBack(函数句柄指针)的注册不在main.c中而在外部中断服务函数中进行。
我们要注意:硬件.c文件中可以定义全局变量但是必须是static类型的,即只能在该硬件.c文件中使用不能跨文件使用,这就保证了硬件文件的独立性和可移植性。