单片机工程经验 - 分层思想
单片机工程经验是本人对单片机编程从业以来的一点经验总结,希望能为大家的职业生涯提高一点点效率,不要陷入整日重复造轮子的境地。人生不只有工作,还有生活。
分层思想
分层思想其实一点都不神秘,它就是简单的把一个项目分成多个层级,仅可上层调用下层接口,且不能跨层级调用。这里我简单的把单片机程序分为3层。
矩阵键盘为例子
这里我们以矩阵键盘为例子。
- 将ROW1 ~ ROW4设置为输出模式、将COL1 ~ COL4设置为输入。
- 将ROW1输出低电平,ROW2、ROW3、ROW4输出高电平。
- 读取COL1 ~ COL4电平,如果有一列电平为低,则相应的按键按下,比如K2按下时COL2就会收到低电平,而此时K5~K16无论是否按下,对这个电平都没有影响,因为即使按下,也只是增加一个3.3V的上拉。
- 将不同的ROW输出低电平,其他输出高重复读取COL上的电平。这样每次哪一行上的ROW置低就能读取到这一ROW上4个按键的状态。
分层
硬件层
ROW1 ~ ROW4,COL1 ~ COL4需要连接到单片机的8个普通IO口上,而不同单片机的IO口操作寄存器或者库函数都不同,如果这里不做分层,那么换一个单片机就得重写矩阵键盘的驱动,这显然是我们不想要的。
首先我们对IO口操作进行抽象化处理,我们发现IO口操作无非就是设置IO输入输出模式,输出高低电平,检测输入电平的高低。 然后我们就可以抽象出这么三个函数
drv_pin_mode(int id,int mode);
drv_pin_write(int id,int level);
drv_pin_read(int id);
因为不同的IO口都可以单独配置,所以我们要将IO传入函数,这里不同的id就能代表不同的IO口。 不同单片机这三个函数的实现都不同,但这没有关系,只要我们把这三个函数的实现单独写成一个源文件,那么换单片机是不是只要换这个源文件就可以了?这样我就实现了硬件层的分层。
驱动层
驱动层就需要考虑我们需要为上层应用层提供什么东西,比如矩阵键盘需要提供哪个按键被按下、led需要提供亮灯灭灯闪烁、温度传感器需要提供温度值。而对上层应用来说,我不需要知道你是怎么实现的,驱动只要提供应用层想要的东西就行了。 对于矩阵键盘来说,应用层只需要一个函数
char get_key_date();
应用层实时调用这个函数就可以知道哪个按键被按下了。 或者我们可以使用回调函数的方式。
void set_key_callback(void (*key_date)(char key));
上层应用先写好函数,这个函数可以处理不同的键值按下,然后调用上方函数设置好回调,当矩阵键盘驱动检测到按键被按下时就可以自动调用这个回调函数。
采用这两种方法就可以实现矩阵键盘驱动写好后,下次需要使用可以直接将驱动复制而无需更改任何代码。
矩阵键盘驱动实现
首先我们思考矩阵键盘和硬件有哪些关联。
- ROW和COL的数量是不确定的
- 每个ROW和COL对应的IO口是不确定的。
因为这两个不确定,所以我们需要把这两个不确定的条件传入驱动
void set_key_row_col(int *rowMap,int rowNum,int *colMap,int colNum);
这里rowMap是ROW列的引脚id数组,rowNum是数组长度,col也同理。 比如row的id是1357,col的id是2468,那么可以这样调用
int row[] = {1,3,5,7};
int col[] = {2,4,6,8};
set_key_row_col(row,4,col,4);
在驱动层,我们就可以将这几个参数保存下来,以便后续使用。
接着我们要思考矩阵键盘的具体实现了,我们知道一开始需要将row设置成输出,col设置成输入,所以可以定义一个初始化函数
void keyi_init()
{
int i = 0;
for(i = 0; i < rowNum;i++)
{
drv_pin_mode(rowMap[i],PIN_MODE_OUT);
drv_pin_write(rowMap[i],1);
}
for(i = 0; i < colNum;i++)
{
drv_pin_mode(colMap[i],PIN_MODE_IN);
}
}
然后我们就要考虑轮流去读取col的值。
void key_thread()
{
static int rowIndex = 0;
int i = 0;
drv_pin_write(rowMap[rowIndex],0);
for(i = 0; i < colNum;i++)
{
if(drv_pin_read(colMap[i]) == 0)
{
if(key_date)
{
key_date(i*rowNum + rowIndex);
}
}
}
drv_pin_write(rowMap[rowIndex],1);
rowIndex++;
if(rowIndex >= rowNum)
{
rowIndex = 0;
}
}
只要应用层不断的去调用key_thread() 函数,那么就可以十分简单的获取到键值。当然这个驱动没有考虑按键消抖,辨别长按双击等,这里只介绍思想,不作深入研究,你可以根据这些思想去思考怎么完善这个程序。
应用层
经过上述介绍,应用层需要完成的事情非常简单
- 定义好row和col的id,然后调用
set_key_row_col 函数 - 编写好回调函数然后调用
set_key_callback 告知驱动 - 调用
keyi_init 函数对驱动进行初始化 - 不断调用
key_thread 函数完成驱动的操作
至此,我们就可以发现,无论是什么单片机,无论几个口的矩阵键盘,只要将硬件层和驱动层复制到项目中,都只需要上述4部即可完成对矩阵键盘的使用。
总结
分层思想的好处是,硬件层和驱动层都只需要编写一次,极大的减少工作量,工程师可以专心思考应用层的实现。而由于这两层完全不涉及逻辑,可以轻松编写单元测试代码,只要一次测试通过,以后这两层代码将安心使用而不用担心bug的出现,减少测试压力。 当然分层思想也不是没有缺点,首先就是资源占用的增加,分层势必增加许多冗余代码,而且每一层都有自己的资源分配,尽管不同层级的资源代表的东西是相同的,但是为了减少耦合不得不另外分配资源,同时部分编译器可能对调用堆栈有限制,分层的实现会加重函数调用的深度。而且部分的代码可能不支持,比如51单片机对于函数指针的使用可能会出现问题。 总结下就是,对于资源丰富的现代单片机建议使用分层思想来构筑程序,这样的程序不仅写的快而且质量高。如果单片机是落后的八九十年代的产品,那还是不要用了。
|