前言
在嵌入式的开发中,经常需要执行定时的操作。 聪明的同学肯定会想到, 我可以配置硬件定时器, 然后利用定时器中断来执行需要定时执行的代码。然而硬件定时器的数量总是有限,不一定可以满足我们定时的需求。因此我们常常需要用到软件定时的方法。
事实上,关于使用软件定时,如果有用到操作系统的内核(例如uCOS、FreeRTOS等)的话是非常爽的,因为内核已经帮你做了很多工作。你只需要调用定时器的API创建定时器结构体并且编写定时器回调函数,就可以完成定时工作的代码。
有时我们的项目很简单,不需要使用到操作系统。或者我的SOC资源极其有限,ROM和RAM小到连操作系统的代码都跑不起来,那么就可能需要我们自己来写实现定时的代码。
一些假设
本文假设读者:
- 掌握C语言
- 具有类似STM32等单片机/SoC的编程经验
一些说明
- 文中使用
...
来代替省略掉的代码,然后将注意力集中在我们需要讨论的内容。
工具/材料
- 一个stm32开发板(笔者使用的开发板是秉火指南者,SOC是STM32F103VET6)
- keil
一个不通用的定时实现
我们先来讨论一个不太通用的软件定时的实现。
- 假如我让一个LED灯定时闪烁,亮1s灭1s,要怎么实现呢?
当笔者还是菜鸡的时候(现在也是),就会想,这很简单,我直接
...
int main(void)
{
led_init();
while(1){
led_on();
delay_ms(1000);
led_off();
delay_ms(1000);
}
return 0;
}
...
不就可以了嘛。
好,可以,没问题。那么我要增加难度了。
我现在不仅要让led亮1秒暗一秒地闪,还要处理来自各个传感器(外设)的数据,然后实时显示到LCD屏幕上。
我们没有使用到类似uCOS这种抢占式可剥夺型的操作系统内核,没有任务切换,所有的代码都要放在一个死循环中,CPU会消耗大量的时间执行delay_ms,然后再执行其他代码。用上述延时的方式实现定时,必然会很不实时。
当然,如果有一款SOC,配置无限个中断,那么请忽略本文所讨论的内容。
我见过一种实现是这样的。利用了STM32中的嘀嗒定时器实现软件定时。
在led.c中声明2个变量,这两个变量必然是全局变量。
unsigned char led_state; // 用于记录led的状态,假设 0 表示led暗, 1表示led亮
unsigned int led_count; // 用于定时计数
在Main函数中,对led_state 和 led_count判断,执行不同的代码:
...
int main(void)
{
SystemInit(); // 系统初始化。如果你有留意到STM32的启动文件中的汇编代码的话,你会发现在进入main函数前,会先执行这个函数。
SysTick_Config(SystemCoreClock/1000); // 1ms 进入一次嘀嗒中断服务函数
led_init();
...
while(1){
...
if((led_count == 0) && (led_state == 0)){
led_on();
led_count = 1000;
}
if((led_count == 0) && (led_state == 1)){
led_off();
led_count = 1000;
}
...
}
}
然后在嘀嗒中断服务函SysTick_Handler
中,让 led_count > 0 时自减。这个函数通常放在 system_stm32f10x.c
中。
void SysTick_Handler(void)
{
if(led_count != 0x00)
{
led_count--;
}
}
这样的代码是可以满足我们的定时和实时性的要求的。但是,由于使用了2个全局变量led_state
和led_count
,并且这两个变量很可能还是跨文件的全局变量(通常而言, main.c led.c system_stm32f10x.c),就导致了这份代码的可读性和可维护性是非常糟糕的。在阅读这份代码的时候,不得不去找这两个变量首先在哪里声明,然后有那些地方修改了这两个变量。另外一方面,假如有关led的需求变了,我需要修改相关的代码,那么很可能我不得不修改main.c
、led.c
、system_stm32f10x.c
的代码。
一个基于stm32 Systick 的简单定时器(裸机)
抽象
我们前面讨论了一个不通用的代码,不管阅读还是维护都需要耗费极大的精力。
为了让代码变得更加通用,我们需要引入ADT(抽象数据类型,Abstract Data Type)的概念。即想办法将待求解的问题抽象成一个数据类型,然后思考并且实现这个数据类型支持的操作。比如说,int
类型的数据是C语言内建的数据类型,它可以进行+
、-
、×
、÷
的操作。我们可以尽情地使用(加减乘除)的操作处理整型数据而不用考虑加减乘除是怎么实现的。同样道理,对于新的数据类型,在我们实现它的操作集合之后,就可以使用它的操作集合处理这种新类型的数据而不需要考虑这个操作集合是怎么实现的。同时,当我们要修改操作集合的实现的时候,只要接口不变,那么应用的代码就不需要做任何更改
换成“面向对象编程”的说法就是,首先思考这个东西有什么属性,然后这个东西有什么行为。
大部分介绍数据结构的书籍,都会介绍ADT的概念。ADT及其操作集合