学完uCOS-II和FreeRTOS后,想从裸机编程转到RTOS编程,但是想从裸机编程转到RTOS编程确实有点困难呀! 以下是向ba:向kinsno: 讨教如何设计某个仪表上的4个数码管在上电自检时显示、参数设置时的显示以及正常模式下显示测量数据的编程思路。
以下是向ba:向kinsno: 讨教如何设计某个仪表上的4个数码管在上电自检时显示、参数设置时的显示以及正常模式下显示测量数据的编程思路。 ba: 我要设计的显示任务需要完成的功能如下: (1)、仪表上电自检后,“四个”数码管每隔500ms依次显示“1111”,“2222”,“3333”,“版本号”。 (2)、自检完成(2秒钟)后,四个数码管开始显示测量数据 (3)、用户按面板上的《编程》键后,四个数码管显示“待设置的参数值”,同时当前正编程的数码管位置开始闪烁 (4)、当编程状态时,用户按下面板上的《确认》键 (a).如果参数在合法的范围内,则保存当前参数值,然后自动将下一个待设置的参数值显示在数码管上; (b).否则,当设置的参数值超出范围,则数码管显示“Err”,同时数码管持续1秒钟内都显示“Err”,1秒钟后,四个数码管重新将初始的参 数值显示在数码管上。 我设计的显示任务显然是一个周期性任务。其一般格式如下 task () { 初始化; while (1) { 显示任务代码; 延时节拍; // 系统提供的API函数 } } 问题的关键是其它任务和显示任务之间通过全局变量进行通信,同时显示任务自身又要修改通信的全局变量(例如:参数设置错误时,如果1秒钟到时,会自动修改状态机),不知道该如何办? ba: 操作说明: 1、单片机上电后,状态机 status = "仪表上电自检" 2、2秒钟后,数码管显示完“版本号”后,自检完成,状态机 status = "测量" 3、用户按下面板上的《编程》键后,状态机 status = "编程" 按键任务切换状态机从 “测量”---> “编程” 4、状态机 = “编程”时,用户按下面板上的《确认》键 如果参数设置正确,则保存当前参数到EEPROM,同时数码管自动显示下一个待编程的参数。 状态机仍然等于“编程”状态。 如果参数设置错误(超限),则状态机=“编程错误” 5、状态机=“编程错误” 数码管显示“err”,同时开始计时,当计时1秒钟到时后,自动切换状态机,从“编程错误”--->“编程” 以下是我的OS代码构思,请指正: 设计思路,利用状态机,保存当前显示任务正在处理哪个分支,问题的焦点是: 其它任务和显示任务之间通过全局变量进行通信,同时显示任务自身又要修改通信的全局变量(例如:参数设置错误时,如果1秒钟到时,会自动修改状态机),不知道该如何办? INT8U status = "仪表上电自检"; // 状态机,确定显示任务当前正在哪个分支 void TASK_DISPLAY() { INT8U st1; static INT8U self_n = 0; // 用于记录“自检”已经进行了几秒钟 static INT8U err_time = 0; // 用于记录编程错误时,显示"Err"时间已经进行了多久 while (1) { OS_ENTER_CRITICAL(); st1 = status; // 使用全局变量进行数据通信 OS_EXIT_CRITICAL(); switch (st1) { case 仪表上电自检: self_n++; // switch (self_n) { case 0: 数码管显示 1111; break; case 1: 数码管显示 2222; break; case 2: 数码管显示 3333; break; case 3: 数码管显示 8888; break; case 4: 数码管显示 “仪表版本号”; break; default: status = “测量”; // 自检完毕,切换状态机="测量" break; } break; case 编程模式: 四个数码管显示待修改参数的参数值 ; 还要处理在编程时,数码管闪烁(用于指示当前正在编辑哪一个数码管对应的数值) break; case 编程错误: 数码管显示 "Err"; err_time ++; if (err_time >= 2) { err_time = 0 ; status = “编程”; // 显示"Err"1秒钟后,切换状态机="编程" } break; case 测量模式: 四个数码管显示测量数据 break; } OSTimeDelay(50); // 延时节拍(延时500ms) } } kinsno:我明白了,你纠结的地方是哪?你无非就是纠结在“没有消息的时候,任务是挂起的”。 整个系统中,肯定是有挂起的时候,但是不要紧啊,挂起了又怎么样?它还不是在显示你上一次的数据,要么是按键数,要么是AD采样数啊。 在SWITCH外面的时候,我们接收ADI消队队列的时候,如果没有消息队列过来,它就停在你开机初始态啊,因为即便是你的AD也不是一直在显示的,你习惯了用前后台方式来操作, 事实上,AD采样显示的时候,AD采样的这段时间,你的系统不就相当于挂起了吗? /* 这里的核心是状态机或者是分支,不管是按键发送消息队列,还是AD发送消息队列,都是用这同一个消息队列。利用一个消息队列或短消息组成的数组,比如NUM[10],好,我们规定 NUM[0]=1 表示是采样任务,那么后面紧跟着的数据就是AD数据,我们解析出来显示。 NUM[0]=2 表示是按键动作了,那么后面紧跟着的是按键数据,然后我们解析按键代码 */ kinsno:不知道你看明白了没有? 你在没有按键,和AD采样的时候,你的CPU还不是在空运转,这段时间不就是所谓的任务挂起了嘛。难道你的任务一直是被显示霸占着的,显示只不过是我们眼睛看着,感觉得它一直在显示,事实中间还有大量的时间是空运转。 关键有两点: 1、理解前后台中系统空跑的时候,比如无聊的延时,AD采样,按键中间的消抖,这不都是空跑,这不就是任务挂起吗? 2、接任消息队列的时候,关键要设计好这个消息队列,要把所有对显示任务发送的各种情况,规划在这个里面,在你这里貌似只有按键和AD采样值。 ba: 你说的上述两点,只适合AD采集和串口等事件(事件,是瞬间发生的动作) 例如:采样任务采集完成后,给处理任务发送一个消息。 处理任务在没有接收到消息时,被挂起。 当处理任务接收到采样任务发送的消息时,处理任务开始计算。 串口中断接收到字符后,发送一个消息给“串口分析”任务。 “串口分析”任务没有接收到消息时,被挂起。 当 “串口分析”任务接收到消息后,开始运行,进行判断分析,是否已经接收到完整的一个数据包。 但是我上面提到的“显示任务”不能按照“采样任务”和“串口分析”任务那样设计,必须设计成周期性任务,原因如下: (1)、在测量状态时, “显示任务”必须实时显示测量数据 如果“显示任务”被挂起,如何“实时”呢? (2)、在编程状态 “显示任务”必须动态刷新数码管,例如当前正在编程第1个数码管上的数值,则该数码管必须一闪一闪(每隔500ms)的,用于指示当前正在编程第1个数码管的数值 (3)、在编程状态,用户按下《确认》键时,如果参数设置错误 则数码管必须持续1秒钟内显示“Err”,当1秒钟到时后,又必须将原始参数值显示到数码管上。同时当前正编程 的数码管上开始一闪一闪的。 kinsno: (1)、在测量状态时, “显示任务”必须实时显示测量数据 如果“显示任务”被挂起,如何“实时”呢? --------------------------------------------------------------- 答:显示任务挂起来,是因为没人给它发消息了;如何实时?当然是采样任务给显示任务发消息,发一次,显示任务就接收一次,接收了就显示,这不就实现实时了。 ………………………………………………………………………………………………………………………………………… (2)、在编程状态 “显示任务”必须动态刷新数码管,例如当前正在编程第1个数码管上的数值,则该数码管必须一闪一闪(每隔500ms)的,用于指示当前正在编程第1个数码管的数值 ------------------------------------------------------------------------ 答:在编程状态,它是一个死循环啊,还是可以继续接收按键消息;500ms是很快的,你可以弄成无等待接收,或者有定时接收,接不着就过去延时;接着了就显示啊。 假如你的按键发送消息之后,显示任务正在500ms之内,没关系,等500ms完了,回到接收消息处,它还是能正确收着按键消息,然后解析。事实上在这部分按键里,你没有必要弄的这么紧迫,因为它不是中断类任务,不是十万火急的任务。完全可以等500ms完了以后再来接收。 --------------------------------------------------------- 大概框架如下: Tsak_Disp() { while(1) //第一个死循环,接收按键或者AD采样 { 收着AD就显示。 收着按键,进入编程状态,就进入下面这个死循环 while(1) { 限时接收 500ms 刷新形成闪烁 if (确认键) { 判断是否有错,出错err 否则,退出。 } } } } ba: 答:显示任务挂起来,是因为没人给它发消息了;如何实时?当然是采样任务给显示任务发消息,发一次,显示任务就接收一次,接收了就显示,这不就实现实时了。 //------------------------------------------------------------------------------------------------------------ 你这样的做法是: 键盘任务给显示任务发送“状态机status 消息”,显示接收到消息后就开始显示,否则挂起。 这样做法有缺陷,疑问如下: (1)、进入“编程”状态后,就一直在“编程状态”下(除非按某一个退出键,退出编程状态,返回到测量状态)。 难道按键任务也要定时发送“状态机status”消息吗? 通常按键任务都设计成如下格式: 有按键按下,则发送按键消息。 (2)、仪表上电时,status = “上电自检”,仪表自检结束后,状态机status=“测量” 显然它不受“按键任务”管辖,又如何给“显示任务”发送消息呢? ba: 我这样设计显示任务,有什么地方不对呢?请指正。 我也对自己设计的显示任务不满意,原因如下: (1)、键盘任务,当检测到《编程》键按下后,会修改全局变量“status”。 使得 status 从 “测量模式”---> “编程模式” (2)、显示任务通过全局变量中获取“status ” (3)、显示任务发现 status = “仪表上电自检”,同时3秒到时后,会主动修改全局变量“status”。 使得 status 从 “仪表上电自检模式”---> “测量模式” (4)、键盘任务发现 status = “编程模式”,当用户按下《确认》键,保存刚刚修改的内部参数时, 如果发现所设置的参数值超出范围,也会主动修改全局变量“status”。 使得 status 从 “编程模式”---> “编程错误模式” (5)、显示任务发现 status = “编程错误模式”,当1秒到时后,也会主动修改全局变量“status”。 使得 status 从 “编程错误模式”---> “编程模式” 框架示意程序如下,请指正。 INT8U status = "仪表上电自检"; // 全局变量:状态机,确定显示任务当前正在哪个分支 void TASK_DISPLAY() { INT8U st1; static INT8U self_n = 0; // 用于记录“自检”已经进行了几秒钟 static INT8U err_time = 0; // 用于记录编程错误时,显示"Err"时间已经进行了多久 while (1) { OS_ENTER_CRITICAL(); st1 = status; // 使用全局变量进行数据通信 OS_EXIT_CRITICAL(); switch (st1) { case 仪表上电自检: self_n++; // switch (self_n) { case 0: 数码管显示 1111; break; case 1: 数码管显示 2222; break; case 2: 数码管显示 3333; break; case 3: 数码管显示 8888; break; case 4: 数码管显示 “仪表版本号”; break; default: status = “测量”; // 自检完毕,切换状态机="测量" break; } break; case 编程模式: 四个数码管显示待修改参数的参数值 ; 还要处理在编程时,数码管闪烁(用于指示当前正在编辑哪一个数码管对应的数值) break; case 编程错误: 数码管显示 "Err"; err_time ++; if (err_time >= 2) { err_time = 0 ; status = “编程”; // 显示"Err"1秒钟后,切换状态机="编程" } break; case 测量模式: 四个数码管显示测量数据 break; } OSTimeDelay(50); // 延时节拍(延时500ms) } } kinsno: 晕倒。先解答你上面的问题,再给你捋一遍。 ………………………………………………………………………………………… 这样做法有缺陷,疑问如下: (1)、进入“编程”状态后,就一直在“编程状态”下(除非按某一个退出键,退出编程状态,返回到测量状态)。 难道按键任务也要定时发送“状态机status”消息吗? 通常按键任务都设计成如下格式: 有按键按下,则发送按键消息。 (2)、仪表上电时,status = “上电自检”,仪表自检结束后,状态机status=“测量” 显然它不受“按键任务”管辖,又如何给“显示任务”发送消息呢? 答: (1)、难道你退出编程状态,是自然退出?不需要按任何键吗?从你上文描述中,明显是需要按“某一个键”退出的嘛。所以,你按下这个按键,理所当然要给显示任务发送消息,由这个消息去通知显示任务退出编程状态了。 (2)、你这个上电的四个状态“1111、2222、3333、4444”等,这些东西是在显示任务一开始的时候,就显示的嘛,显示完了,再进入死循环while(1),他们是开机状态下被执行一次就终结的一段代码。 …………………………………………………………………………………………………………………………………………………… 你还停留在前后台的那种模式,从你用UCOS开始,你就得彻底抛弃全局变量这种东东,完全利用通信机制。 我来设计一下代码思路,仅代表我个人想法和思路。 ------------------------------------ 首先,定义一个消息队列MSQ[10](10只是我打个比方的,实际上是怎么样的,要根据实际状态去设计),这个MSQ[10]既要传递AD采样值,也要传递按键值,如果还有别的值,同样也要传递。 那么我们规定: MSQ[0]=1 ----表示有按键按下 MSQ[0]=2 ----表示有AD采样过来 MSQ[0]=3 ----表示其它的什么功能,反正由我们自己设计。 在MSQ[0]=1时,MSQ[1]=1:表示1键,MSQ[1]=2:表示2键,MSQ[1]=3:表示3键,MSQ[1]=4:表示4键,MSQ[1]=5:表示5键,其它类推 第一个任务---扫键任务 TASKKEY { while(1) if(有按键按下) { //要把按键值打包到MSQ里面去,如 MSQ[0]=1 //因为我们规定MSQ[0]=1 表示有按键按下 MSQ[1]=? //这个值具体参照你自己设定的协议来 MSQPOST……//发送消息队列 } } 第二个任务---AD采样任务 TASKKEY { while(1) if(有新采样值形成) { //要把按键值打包到MSQ里面去,如 MSQ[0]=2 //因为我们规定MSQ[0]=2 表示有新AD采样 MSQ[1]=? //这个值具体参照你自己设定的协议来 MSQ[2]=? //把具体的采样值放到MSQ[1]-MSQ[10]中来 MSQPOST……//发送消息队列 } } 第三个任务----显示任务 TASKDISP { 显示1111,延时500 显示2222,延时500 显示3333,延时500 显示4444,延时500 while(1) { 限时等待接收消息(具体的API我忘了) //如果接收到消息了,那么就开始解析,根据上面我们设定好的MSQ if(MSQ[0]==1) //有按键按下 { switch(MSQ[1])//解析各个按键,然后根据各个按键进行操作 { case 1:编程模式 { 显示编程模式…… while(1) { 等待按键消息来; 同样,解析按键值 if(确认) { 退出编程模式 } } } case 2:什么模式 } } else if(MSQ[0]==2)//表示有AD采样 { 显示AD值 } 延时500 } } ba: 就是,我目前的难点还是停留在前后台编程思路中。 我看到一般的教材上都说: (1)、为了减少任务个数,使得操作系统调度尽量简单些,要把差不多功能的任务合并到一个任务中。 (2)、显示任务一般设计成“周期性”任务。 (3)、如果AD采样在“AD中断服务程序中采集”,可以设计成如下形式 (3.1)一个AD中断程序 (3.2)一个AD处理任务 AD处理任务设计成事件型任务,当接收到"AD中断程序"发送的消息,就立即开始得到CPU控制权, 因此为了快速得到CPU控制权,应该把“AD处理任务”的优先级设置的比较高。 当“AD中断服务程序中采集”完成,发送消息给AD处理任务,能够确保“AD处理任务”立即运行。 普通的任务设计我能够明白(一般教材上有叙述),比较复杂的任务设计一般教材上没有讲解,因此消化过程比较缓慢。 最主要是把前后台中比较混乱的结构如何编排成一个一个通信过程比较清晰的任务,是我目前的难点。 kinsno: 如果你要用RTOS,你一定要抛充掉你的全局变量的想法,所有任务之间,仅仅靠邮箱或消息队列。这个UCOS毒害了多少人了。 呵呵。 |
回想当年学习8086/8088汇编语言的时候可以用千辛万苦来形容,当掌握了086/8088汇编语言后,学习80C51和80C52汇编语言那是轻车熟路呀!
唉,从前后台转换到RTOS编程,最大的不同就是:
(1)、前后台面编程,变量随便用,你想在哪个地方用,就使用。
只有一种情况:当变量在中断中写,在程序中读时,只需要关中断即可。
(2)、RTOS编程,变量不能你想在哪个地方用,就使用,一个任务使用其它任务的就是,只能使用以下三种方法。
方法一、和前后台一样,用全局变量,使用前关中断,使用后开中断,但是这种方式严重影响系统的实时性,不建议。
方式二、消息邮箱,这会造成当前任务阻塞。
方法三、消息队列,这会造成当前任务阻塞。
以下是最终程序框架:
一、规划消息队列传输协议
定义一个消息队列MSQ[10](10只是我打个比方的,要根据实际状态去设计),这个MSQ[10]既要传递AD采样值,也要传递按键值。那么我们规定:
MSQ[0]=1 ----表示有按键按下
MSQ[0]=2 ----表示有AD采样过来
--------------------------------------------------------------
在MSQ[0]=1时,
MSQ[1]=1:表示【编程键】,
MSQ[1]=2:表示【确认键】,
MSQ[1]=3:表示【上键】,
MSQ[1]=4:表示【下键】,
MSQ[1]=5:表示【移位键】
在MSQ[0] = 2时,
MSQ[1] = 测量数据高字节
MSQ[2] = 测量数据低字节
--------------------------------------------------------------
[b]二、规划任务
规划3个任务:
1、AD采集任务 ---> 周期性任务,每隔200毫秒执行一次,采样完成,给数码管显示任务发送消息队列
2、按键任务 ---> 周期性任务,每隔50毫秒执行一次,如果采集到按键按下,给数码管显示任务发送消息队列
3、数码管显示任务 ---->等待消息队列任务
三、具体任务代码
1、第一个任务---AD采样任务
TASKAD
{
while(1)
{
采集AD值;
if (采集到采样值)
{
MSQ[0]=2 //因为我们规定MSQ[0]=2 表示有新AD采样
MSQ[1]=AD采样值的高字节 //这个值具体参照你自己设定的协议来
MSQ[2]=AD采样值的低字节
MSQPOST……//发送消息队列
}
延时节拍(200毫秒)
}
}
第二个任务---扫键任务
TASKKEY
{
while(1)
{
扫描按键;
if(有按键按下)
{
//要把按键值打包到MSQ里面去,如
MSQ[0]=1 //因为我们规定MSQ[0]=1 表示有按键按下
MSQ[1]=按键代码 //这个值具体参照你自己设定的协议来
MSQPOST……//发送消息队列
}
延时节拍50毫秒;
}
}
第三个任务----数码管显示任务
TASKDISP
{
uint8_t status = 0; //0=上电默认为测量状态 1= 编程模式
uint8_t no;//参数号
uint8_t bit;//数码管位号
显示1111,延时节拍500毫秒
显示2222,延时节拍500毫秒
显示3333,延时节拍500毫秒
显示4444,延时节拍500毫秒
显示版本号,延时节拍2秒
while(1)
{
限时等待接收消息队列消息
if (收到消息队列消息数据)
{
if(MSQ[0]==1) //按键按下消息
{
switch(MSQ[1])//解析各个按键,然后根据各个按键进行操作
{
case 1:编程模式
if (status ==0)
{
status = 1;//进入编程模式
no = 1;//参数号=1
bit=4;//最右边1个数码管编程
发出最右边1个数码管开始闪烁的命令
取出第1个参数号对应的数值,将该数值显示到数码管
}
else
status=0;//退出编程模式,返回测量状态
break;
case 2:【确认键】
if (status ==1)
{
检查参数值是否合法;
if (合法)
{
存参数
数码管显示OK;
延时节拍1秒
no++;//参数号+1
bit=4;//最右边1个数码管编程
取出no参数号对应的数值,将该数值显示到数码管
}
else
{
数码管显示ERROR;
延时节拍1秒
取出第no个参数号对应的数值,将该数值显示到数码管
}
break;
case 3: 【上键】
if (status ==1)
{
bit位对应的数码管上的数字+1
if (bit位对应的数码管上的数字 >9)
bit位对应的数码管上的数字=0
将该数值显示到数码管
}
break;
case 4: 【下键】
if (status ==1)
{
bit位对应的数码管上的数字-1
if (bit位对应的数码管上的数字 < 0)
bit位对应的数码管上的数字=9
将该数值显示到数码管
}
break;
case 5:【移位键】
if (status ==1)
{
bit-1; //左移一位数码管
if (bit <= 0)
bit=4
发出前一个数码管开始闪烁命令
}
break;
default:
break;
}
}
else if(MSQ[0]==2) //ad采样消息
{
读取AD采样数据
if (status ==0)//在测量状态
显示AD采样数据到数码管
}
}//if (收到消息队列消息数据)
确实RTOS编程模式和前后台编程模式差别太大啦!要想顺利的从前后台编程模式转到RTOS模式编程需要大量的实践呀!