序言
状态机在单片机编程中是一个新兴名词,很多老鸟都号称自己编的程序是状态机。那么,状态机到底是什么?网上搜索一下,对它的定义一大把,有我看得懂的,也有我看不懂的,
我们这里不深究它的定义,知道怎么用它就行了。简单地说,状态机又称之为有限状态机,状态机系统有 N 个(有限个)状态,任一时刻,系统都工作于其中的一个状态,当有输入
(激励)时,系统执行某些动作,并切换到下一状态。
下面我以某个项目为例来谈一下基于状态机控制的面向对象的前后台协从多任务系统设计
一、任务分析
根据题目要求,划分任务如下:
- 键盘扫描线程
- 灯显示线程
- LED1-LED4四个独立线程
- 后台监视线程
- 串口收发中断
共计7个线程1个中断。
二、软件整体结构设计
后台 前台 串口中断
———| ————— ————–
| V | int10ms中断 | | serial中断 |
| ————- ————— ————–
| |监视monitor| | |
| ————- —————– ————–
| | |键盘扫描keyscan| | 收 RI 检查 |
———| —————– ————–
| |
—————– ————–
| 灯显示displed | | 发 TI 检查 |
—————– ————–
| |
————————– ————–
| LED1-LED4四个线程lednp | | RETI |
————————– ————–
|
—————–
| END |
—————–
图1 软件整体结构设计图
由图1可知,本软件是基于状态机控制的前后台协从多任务系统,其基本原理是通过均衡地分割CPU时间片达到并发多任务的目的。前台任务要求极精确定时,包括键盘扫描、灯显示、LED1-LED4控制,使用10ms定时中断作为步调,可保证时间误差不超过11ms。后台运行人机监控界面,实时性要求不高,只要满足人的生理要求(响应延迟不超过0.5秒)即可,因此将其放在后台,使用剩余时间片。串口收发属于随机事件,发生频率不高,但实时性要求严格,所以用中断实现最为妥帖。
三、功能描述
人机监控界面monitor
(1)help显示在线帮助,说明各种命令的使用方法;
(2)mb、mw、md以字节、字、长字格式向内存数据区写入数据/读出数据;
如:mX 地址 个数 数据
mb 10 2 —– 从地址10开始读出2个字节数据并显示
mb 20 3 0xaa —– 从地址20开始连续写入3个字节,值均为0xaa
(3)ls显示各线程状态号;
如:
ls monitor —– 显示监控状态机的状态变迁
ls led1p —– 显示LED1控制状态机的状态变迁
注:此命令只显示变化的状态号,不变化不显示,由此可以动态观察运行情况。例如:显示(0)->(1)->(4)…按CTRL+B键退出监视并显示提示符“%”;或者按CTRL+G切换到另一控制台继续输入命令/显示。
(4)bye挂起后台监控,以便节省能源,同时按下key1+key4+key8再次激活(只能在某些支持电源管理的单片机上实现);
(5)其他命令:退格、CTRL+C(重启)、CTRL+G(切换控制台,支持2个显存);键盘扫描keyscan
因为不需要软件去抖动,所以很简单。令key=P1即可。假定按下键盘为0。变量key保存采样的键值。
加入软件去抖动也很方便,只要把去抖动状态机层串接在keyscan前即可。这种模块化设计可以平滑增加新功能,不影响其他部分,而且可以实现相当复杂的处理算法。
keyscan每隔10ms采样一次键盘,相对于人手几百毫秒的机械运动就是实时的。灯显示displed
因为是直接驱动,所以直接令 P3[7..4]=led 即可。假定0为亮. 1为灭。变量led保存处理后的灯显示值。串口收发中断
关于串口收发中断和显示API函数及缓冲区队列处理不再叙述,详见www.hjhj.com下载中心ucos51v2.02相关源代码。四个LED控制
详见LED控制状态机一节的说明。
四、具体设计思路和采用技术
分析题目的功能需求,核心对象是LED,动作有四种:快闪灭、慢闪灭、快短亮长灭、慢长亮短灭,这样我们似乎要设计4种状态机进行控制,再进一步分析,其实本质只有闪灭和亮灭的区别,其他只不过是时间参数不同,这样我们只要设计两种状态机对应就可以了。再深入想一下,如果以后增加新的动作怎么办?或者这几个灯的动作需要互换位置如何才能不改动程序而灵活实现呢?用户的需求千奇百怪,怎么才能在不改程序的前提下满足未来用户不断增长和变化的需求呢?“数据驱动”技术可以解决这个问题。
============
* 数据驱动 *
============
数据是灵活的,程序是僵死的,用数据驱动程序流向,既灵活又稳定。(前提是严密的数据合法性检查)程序和数据在计算机里是分家的,程序位于ROM或只读RAM里,不可写,数据位于RAM里,可读写。如果把程序的流程用数据控制,那么不同的数据组合将产生千变万化的程序行为。我们可以把程序的所有功能写在同一个程序里,然后用数据配置定义个性化的程序行为。单一版本的软件为维护和管理带来了方便。
针对本题目,我们可以给每个灯设置一个配置项smsel,如下:
if(smsel==1) 闪灭状态机处理;
else if(smsel==2) 亮灭状态机处理;
else //可以新增其他动作的状态机处理;
由上可知,所有灯的处理程序都是由以上同一个程序段处理,差别只在于各个灯的配置数据不同。LED1和LED2的私有smsel均为1,LED3和LED4的私有smsel均为0,如果有一天客户提出改变LED1为亮灭动作,只要改它的配置数据即可,根本不用动程序。如果要增加LED5、LED6等新的处理,仍然只需调用这段程序控制动作,不必增加代码,加几个smsel私有配置数据就可以了。总之,增加新灯和改变动作,只动数据,不改程序。
-------------------- -------------------
| 状态号state | ^ | LED1数据结构 | ^
----------------- | ---------------- |
|状态机选择smsel| | | LED2数据结构 | |
----------------- | ---------------- |
| 亮时间on | | | LED3数据结构 | 数据区(属性)
----------------- | ---------------- |
| 灭时间off | 私有数据 | LED4数据结构 | |
----------------- | ---------------- |
| 闪亮时间fon | | | 。。。LEDn | V
----------------- | -------------------
| 闪灭时间foff | | | 闪灭状态机 | |
----------------- | | 亮闪状态机 | 行为动作(方法)
|当前时间计数cnt| V | 其他状态机 | |
-------------------- -------------------
| 键值keyval | 公有数据 图3 LED对象在内存里的映像
--------------------
图2 LED数据结构
如图2. 3所示,LED可以用面向对象(OOP)的方法分析。每个灯有自己的属性和方法,映射到内存中就是类的实例化。比如:每个灯有自己的私有数据,当前状态号、状态机选择、亮时间、灭时间、闪亮灭时间,计数,这些数据唯一确定了此灯与众不同的个性,是每个灯特有的。一个灯受A、B两个键的控制,这个键值对外可见,是公有数据。
如图3,所有灯对象使用同一个类方法,每个对象有自己独立的数据区,即:对象只有属性不同,调用的方法程序是同一个,这也可以说是一种数据驱动吧。其实,C++程序在内存里的映像就是图3。
class LED{
private:
unsigned char state;
unsigned char smsel;
unsigned int on,off,fon,foff,cnt;
public:
unsigned char keyval;
void smstate();//闪灭状态机
void lmstate();//亮灭状态机
}
LED led1,led2,led3,led4;
以上是对应图3的C++伪代码。
五、LED控制状态机
11
--->---
| |
---------
----------------->| 0空闲 | 10
| <--------------------->
| 00/01| |
| ----------- 00/01 ----------------
| | 2闭态灭 |<----------| 1延时fon秒亮 |<----
| -----------<------- ---------------- |
| | | | |10/11
| ---------------- | ----------------- |
| | 4延时off秒灭 | ----| 3延时foff秒灭 |----
| ---------------- 00/01-----------------
| |
---------------
无条件
图4 闪灭状态机变迁图
11
--->---
| |
无条件 --------- 无条件
--------------->| 0空闲 |<--------------
| <---------------> |
| 00/01 | |10 |
| ---------------- --------------- |
---| 2延时off秒灭 | | 1延时on秒亮 |----
---------------- ---------------
图5 亮灭状态机变迁图
图4. 5是LED控制状态机的变迁图,第一个数字是状态号,紧接着是状态描述,条件是AB键状态。如下:
B键 A键 描述
0 0 A、B键同时按下,B键优先级高,忽略A键
0 1 B键按下,关闭灯
1 0 A键按下,闪/亮灯,优先级低于关闭,随时可被B键中断(10ms采样)
1 1 没有键按下,维持原状态
图4的状态2和4不判断按键情况,其他状态每一步都先检测按键输入。
图5的状态1和2不判断按键情况,0状态每一步都先检测按键输入。
现在根据题目要求配置数据,就是初始化每个灯的属性。
LED1-----fon=0.5;foff=0.5;off=5;smsel=1
LED2-----fon=0.3;foff=0.3;off=1;smsel=1
LED3-----on=3;off=4;smsel=2
LED4-----on=10;off=6;smsel=2
-----------------------------------
| state=0 smsel=1 on=0 off=5 |<-------LED1数据区
| fon=0.5 foff=0.5 cnt=? |
-----------------------------------
| state=0 smsel=1 on=0 off=1 |<-------LED2数据区
| fon=0.3 foff=0.3 cnt=? |
-----------------------------------
| state=0 smsel=2 on=3 off=4 |<-------LED3数据区
| fon=0 foff=0 cnt=? |
-----------------------------------
| state=0 smsel=2 on=10 off=6 |<-------LED4数据区
| fon=0.5 foff=0.5 cnt=? |
-----------------------------------
| 闪灭状态机(参见图4状态变迁图) |<-------公用的行为方法
| 亮闪状态机(参见图5状态变迁图) |
-----------------------------------
图6 LED对象实例化内存映像
所有未用成员变量均缺省初始化为0,状态号初始一律为0,cnt是临时变量,保存当前时间,不必初始化。
由图6可知,所有灯的控制都是同一段程序,内存中只有一个副本,每个灯的特质体现在其独立拥有的数据区(属性)。尽管每个灯的行为各异,时间参数不同,但它们都属于灯类。
如果想改变LED1的行为,非常简单,把smsel改成2即可。想改变闪烁频率和亮灭比,只要改fon和foff。软件上如果需要引入更多的灯控,只要改一下数据区结构体数组的下标,根本不用动程序,面向对象和数据驱动的设计方法保证了后期维护升级的便利和可靠。
下面结合实例说明一下工作过程:
======================
======================
* 状态实例分析 *重点 *
======================
======================
先看看LED1,其状态变迁如图4所示。
初始状态号state=0,位于空闲状态,此时若无按键(11),则继续检查按键事件(10ms采样),下一状态仍为自己0。
若按下A键10,状态变迁到S1(亮灯并延时fon秒),LED1的fon=0.5,此态点亮灯LED1并延时0.5秒,同时随时检查B键是否按下,如果B键按下01或者A、B键同时按下00,则立即转到2状态关灯。否则,延时0.5秒后转3态。在3态关闭灯并延时foff秒(foff=0.5秒),同时随时检查B键是否按下,如果00/01则立即转2态。否则延时满0.5秒后回到1态,周而复始灯就按指定亮灭参数闪烁起来。
一旦进入2状态,立即关闭灯,下一步进入4态延时off秒(off=5秒),因为题目要求“off 状态>=5秒”,所以此状态内不进行按键检测,以保证至少关灯5秒。延时满5秒后回到0空闲态检测按键情况,如无按键或又按下B键则继续关灯(0态缺省保持关灯状态,所以此时保证>5秒闭灯)。如果按下A键则进入闪烁状态(此时保证了=5秒闭灯)。再看看LED3,其状态变迁如图5所示。
初始状态号state=0,位于空闲状态,此时若无按键(11),则继续检查按键事件(10ms采样),下一状态仍为自己0。
在0态,若按下A键10,状态变迁到S1,亮灯并延时on秒(on=3秒),因为题目要求“on 状态>=3秒”,所以此状态内不进行按键检测,以保证至少亮灯3秒。延时满3秒后回到0空闲态检测按键情况,如无按键或又按下A键则继续亮灯(0态缺省保持亮灯状态,所以此时保证>3秒亮灯)。如果按下B键则进入闭灯状态(此时保证了=3秒亮灯)。
在0态,若按下B键01或同时按下A、B键00,状态变迁到S2,亮灯并延时off秒(off=4秒),因为题目要求“off 状态>=4秒”,所以此状态内不进行按键检测,以保证至少灭灯4秒。延时满4秒后回到0空闲态检测按键情况,如无按键或又按下B键/AB键则继续灭灯(0态缺省保持灭灯状态,所以此时保证>4秒灭灯)。如果按下A键则进入亮灯状态(此时保证了=4秒灭灯)。可以保存每次采样到的键值,新值覆盖旧值,这样程序就能记住最后一次的按键值,以便状态变迁后处理。
如何调试
连接PC机和监控串口,输入以下命令:
%ls led1p<回车>
(0)->
此时monitor后台交互界面显示LED1在0状态。按下A键,显示变成
(0)->(1)->
表明LED1进入1状态,此时灯亮,过了0.5秒后显示
(0)->(1)->(3)->
表明LED1进入3状态,此时灯灭,再过0.5秒后显示
(0)->(1)->(3)->(1)->
灯又亮了,就这样每隔0.5秒在控制台上显示一个状态并改变一次灯的状态。过段时间,控制台可能显示
(0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->
此时按下B键/同时按下AB键,控制台立即显示
(0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->(2)->(4)->
表明LED1顺序进入2状态和4状态,灯马上灭了,延时5秒后,控制台显示
(0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->(2)->(4)->(0)->
此时进入0状态,灯依然是灭的,继续等待按键。
在PC键盘按CTRL+B退出状态显示,出现提示符
%
或者按CTRL+G切换到控制台2继续输入命令。
在提示符处输入
%mb 0 16<回车>
00000000 00 01 00 00 01 F4 00 32 — 00 32 00 10 00 00 01 00 …………….
%
其中显示的内容为LED数据区,00状态号、01状态机选择、00 00亮时间、01 F4灭时间(5秒=500个10ms=0x1F4)、00 32闪亮时间(0.5秒=50个10ms=0x32)、00 32闪灭时间(0.5秒)、00 10当前时间计数(此时为16个10ms,即0.16秒)、00键值(没有键按下)。
后面的数据是LED2的,00状态号、01状态机选择、00亮时间高8位。
可见运行情况可以通过查看内存数据区或者打印状态号获得。
六、总结
基于状态机的协从多任务就是把一个大任务分成若干小片,每一步(此处为10ms)顺序执行所有任务的一个状态(节约时间,增加实时性),这样CPU资源被各个任务瓜分,从微观上看是顺序执行,从宏观上看每个任务都好象独占一个CPU,任务是并发的。其实CPU本身就是数字系统,不连续而是离散运行的,完全可以认为分配了时间片的任务单独拥有一个慢些的CPU,用此观点看这个程序更容易理解其工作原理。
这中结构的程序可以看作多任务,虽然没有OS,没有任务调度,但状态机把任务调度过程固化在结构里了。此时没有切换消耗,所以调度过程极为迅速,只是设计者比较累。有网友说,“在程序员心中,每个程序员都是一个OS”,大概就是这个意思。
状态机在汇编和C中均可实现。在汇编里用散转方法,注意参数合法性检查;在C里用switch-case方法。
由于中断比较关键,可以单独设计一个软定时进程,在中断里仅处理时间,其他任务挪到主循环里完成,以避免中断响应延迟。
本结构的程序还可以灵活增加新功能,比如键盘去抖动模块、同时按下双键的特殊处理模块等。本结构的延时delay程序不会浪费CPU资源。
七、源程序(略)
这些细节在STM32F4的原厂参考手册中没有说明,只是很简短的列了一下。
我是查看ST前几天刚发布的接口库才最后弄明白的,不敢独享!
1、STM32F4有3个独立的ADC单元,性能强劲,可以独立使用,也可以联合使用它们。
联合使用在参考手册中叫Interleave模式,最大的目的是加倍提升采样速度。
2、采样速度大幅提高以后,就需要使用DMA来配合提取采样结果,从而发挥STM32F4
ADC模块的最大效能。
3、ADC模块使用DMA有4种模式可选,默认模式和模式1没有什么特别之处。
最有意思的是模式2和模式3:
模式2可以选择多达3个ADC模块工作于Interleave模式,ADC速度从单一模块的
2.4Msps暴涨为7.2Msps,而且还是12-bit的分辨率!唯一的要求是每完成2次转换,
允许DMA一次性取走2个采样值。
模式3跟模式2类同,但要求ADC模块的采样率为8-bit或6-bit,由于转换时间要比
12-bit时短,所以速度更快,适用于速度要求更快,但精度要求较低的场合。
比如用2个ADC模块很容易就可以做到6Msps的速率,而且2次的结果可以存为halfword,
经由DMA取走,耗用内存也比模式2来的少。
剩下的那一个ADC模块也不用闲着,可以工作于其他设定(比如:高精度)的模式。