一、设计需求
设单片机的时钟12MHz,型号为AT89S52。单片机引脚连接有4个LED,其中:
- LED1以30Hz的固定频率闪烁;
- LED2在外部中断发生以后,亮0.2秒,然后熄灭;
- LED3在LED2熄灭以后以20Hz的频率闪烁10次然后熄灭;
- *将LED的亮灭情况实时通过串口输出;
二、题目分析
题目中4个LED需要“同时”执行不同的操作,如果使用常规的方式来构建,是有一些难度的;而如果用RTX51 tiny,实现起来将比较简单。我们只需要构建不同的task,每一个task都相当于在单独执行,但是宏观上看起来多个任务是在 “同时”执行的,根据题目要求,除了简单的创建task,我们还需要用到task之间的信号传递。对于rtos的理解有点类似于数码管,快速依次去点亮每一位数码管,这显然是串行的操作,但是看起来这几位数码管是同时亮起来的,又像是在并行工作。
三、模块分析
3.1 点亮LED1
题目要求LED1以30Hz的固定频率闪烁,经过计算可知LED1的电平状态需要33ms翻转一次。创建一个task,在这个task里每延时33ms让LED1电平翻转一次即可。
关于RTOS的延时,系统中给了os_wait2( )函数,这个函数有两个输入参数,详细可以看帮助文档。这里需要注意一个tick代表多长时间,这个可以在Conf_tny.51文件中查看,通过INT_CLOCK的值来计算,默认值为10000,如果使用12M的晶振,那么这里就是10ms,也就是说如果我们写了os_wait2( K_TMO,1),就表示延时10ms。我们发现这里最小的延时单位只能是10ms。可以更改INT_CLOCK的值来减小延时单位长度。这里将INT_CLOCK的值改为1000,一个延时单位就是1ms。需要注意,os_wait2( )中的参数类型是unsigned char,意味着我们最大只能写255,如果需要更长的延时,可以通过for循环来构建。
对LED1操作的代码如下:
void LED1_CTRL (void) _task_ 1
{
while (1)
{
os_wait2(K_TMO,33);
LED1 = !LED1;
}
}
将程序下载到开发板中,可以看到LED1在快速闪烁。
3.2 外部中断配置
LED2需要在外部中断发生以后,亮0.2秒,然后熄灭;这里需要用到按键来触发外部中断。在我们的开发板上有一个连接到P2^3引脚的按键,P2^3刚好是51单片机的外部中断引脚,我们可以对其进行配置来完成这个要求。
外部中断的配置比较简单,只需令EX0 = 1; IT0 = 1。EX0是外部中断0的使能控制位,IT0控制的是外部中断0的触发方式。IT0=0时,低电平触发;IT0=1时,后沿触发。
涉及到按键,就必须考虑到消抖的问题,在中断服务程序中进行消抖。当检测到中断发生之后,延时20ms再次检测按键的状态,如果这时按键依然处于按下的状态,就说明按键确实按下了。
void inter() interrupt 0
{
os_wait2(K_TMO,20);
if(key == 0)
{
key_flag = 1;
}
}
这里的key_flag是按键按下标志位。我们可以创建一个task,对key_flag的状态进行检测,如果检测到key_flag的电平为1,则点亮LED2,延时200ms后关闭LED2,并将key_flag的状态变为0。
将程序下载到开发板,可以看到当我们按下按键的时候,LED2会短暂的亮一下后熄灭,符合我们的要求。
3.3 任务间信号传递
LED3需要在LED2熄灭以后以20Hz的频率闪烁10次然后熄灭;LED2和LED3在两个不同的task中,但是却有所关联,这就需要我们在两个task之间传递信息。
在LED3的task中等待信号,当有信号时执行20Hz的频率闪烁10次然后熄灭的操作;在LED2的task中添加一句当LED2熄灭后发出信号到LED3所在任务的代码即可。详细代码参见附录。
将程序下载到开发板,可以看到当我们按下按键后,LED2短暂的亮一下后熄灭,之后LED3以20Hz的频率闪烁10次后熄灭,符合设计要求。
3.4 串口信息输出
所谓的串口其实就是一种通讯方式,通讯方式有很多种,像IIC、SPI这些都是常见的通讯方式。不同的通讯方式需要遵循不同的协议,就像是A给B发了一段密文,B必须知道相应的规则才能进行解析,不然就不能正常交流了。
数字逻辑只有0和1两种状态,当我们用特定的时序将0或1发送出去,就能传递一些信息,所以时序是我们需要特别注意的东西。串口最重要的是波特率的配置,单片机的串口需要用到定时器,主要就是用来产生特定的波特率。不同的波特率对应着不同的定时参数,定时器工作模式的不同也会影响参数的计算。由于RTOS已经占用了定时器0,所以这里用定时器1来对串口进行配置。
本实验中需要将LED的亮灭情况实时通过串口输出,需要注意LED状态是是在实时变化的,而我们知道串口发送一次数据是需要时间的,如果在发送数据的时候LED的状态发生了改变,当下一次发送的时候就会漏掉一些数据。这个问题就是两者的时间不同步造成的,在进行数据处理的时候我们经常会遇到这种情况。
为了解决这个问题,我们需要创建一个缓冲区(简单的做法是创建一个比较大的数组),LED的状态不停的写入到缓冲区中,由于对缓冲区的写入速度是很快的,所以我们不用担心数据会丢失。然后我们依次将缓冲区中的数据用串口发送出去,相当于一个栈,遵循先进先出的原则。虽然LED的变化速度和串口的发送速度存在差异,但是由于缓冲区的存在,我们不必担心数据会丢失。
由于缓冲区的代码实现上难度较大,所以没有深入去做,只实现了对串口的配置,然后在一个任务中持续打印P1端口的电平状态(LED连接在P1口),无疑这种方式数据丢失很严重。
代码下载到开发板后打开串口助手,可以看到单片机在持续发送P1的状态到PC。
四、总体方案设计
本实验借助了RTOS来实现,一定程度上减小了我们的设计难度,因为每一个task我们都可以单独去编写而几乎不用考虑别的模块。按照题目的要求依次去创建task,然后编写相应的操作逻辑即可。
五、总结
就题目而言,本次实验的难度是比较小的,但是通过简单的问题能更容易的去理解RTX51 tiny的运行方式。通过这次试验我们掌握了RTSO的基本操作,下一次遇到需要多任务并行的问题,我们便不会慌张了,不就是把对LED的操作换成了其它东西吗;同时对于task之间的信息传递也有了认识。
在解决问题的过程中,我们记住的肯定不是详细的代码,最终留下来的只有思考问题的能力。当我们学会去思考问题,不管是简单的还是复杂的任务,我们都能按部就班的去完成。
六、附完整代码
#include <rtx51tny.h>
#include <reg52.h>
#include <stdio.h>
volatile unsigned char sending;
sbit LED0 = P1^0;
sbit LED1 = P1^1;
sbit LED2 = P1^2;
sbit LED3 = P1^3;
sbit key = P3^2;
bit key_flag;
void uart_init()
{
TMOD&=0x0F; //定时器1模式控制在高4位
TMOD|=0x20; //定时器1工作在模式2,自动重装模式
SCON=0x50; //串口工作在模式1
TH1=0xFA; //计算定时器重装值
TL1=0xFA;
PCON|=0x80; //串口波特率加倍
ES=1; //串行中断允许
TR1=1; //启动定时器1
REN=1; //允许接收
}
void send(unsigned char d) //发送一个字节的数据,形参d即为待发送数据。
{
SBUF=d; //将数据写入到串口缓冲
sending=1; //设置发送标志
while(sending); //等待发送完毕
}
void job0 (void) _task_ 0
{
EA = 0;
EX0 = 1;
IT0 = 1;
uart_init();
EA = 1;
os_create_task (1);
os_create_task (2);
os_create_task (3);
while (1)
{
send(P1);
}
}
void LED1_CTRL (void) _task_ 1
{
while (1)
{
os_wait2(K_TMO,33);
LED1 = !LED1;
}
}
void LED2_CTRL (void) _task_ 2
{
while (1)
{
if(key_flag)
{
LED2 = 0;
os_wait2(K_TMO,200);
LED2 = 1;
key_flag = 0;
isr_send_signal (3);
}
}
}
void LED3_CTRL (void) _task_ 3
{
char i;
while (1)
{
os_wait(K_SIG,0,0);
for(i=0; i<20; i++)
{
os_wait2(K_TMO,50);
LED3 = !LED3;
}
}
}
void inter() interrupt 0
{
os_wait2(K_TMO,20);
if(key == 0)
{
key_flag = 1;
}
}
void uart(void) interrupt 4 //串口发送中断
{
if(RI) //收到数据
{
RI=0; //清中断请求
}
else //发送完一字节数据
{
TI=0;
sending=0; //清正在发送标志
}
}