实验二:信号量与邮箱
1、实验目的
深入理解信号量与邮箱在多任务处理中的应用,掌握其基本原理与使用方法。
2、实验内容
通过阅读和编译运行信号量与邮箱的相关代码,观察实验运行结果。
3、实验原理和实验步骤
实验原理:
1.信号量原理:
信号量是一种用于多任务同步与互斥的机制,通常用于控制对共享资源的访问。信号量的值可以用于表示可用资源的数量。在嵌入式系统中,信号量常被用于解决竞争条件和确保资源的正确共享。当一个任务需要使用某资源时,它会尝试获取信号量;如果信号量的值大于零,任务就可以获得资源并将信号量值减一;如果信号量值为零,任务就会被阻塞,直到有其他任务释放资源。
2.邮箱原理:
邮箱是一种用于在不同任务之间传递消息的通信机制。每个任务可以通过邮箱发送消息给其他任务,并从邮箱接收消息。在嵌入式系统中,邮箱可以用于实现任务之间的信息传递,协调任务的执行顺序,以及处理异步事件。任务通过邮箱发送和接收消息,实现了解耦和通信的目的。
实验步骤:
1.信号量实验步骤:
(1)创建一个共享资源,如共享变量或共享设备。
(2)定义一个信号量,用于控制对共享资源的访问。
(3)编写两个或多个任务,分别尝试获取和释放信号量。
(4)观察任务之间如何协作,确保对共享资源的访问是安全和有序的。
2.邮箱实验步骤:
(1)创建两个或多个任务,它们将通过邮箱进行通信。
(2)定义一个邮箱,用于在任务之间传递消息。
(3)编写发送任务,将消息发送到邮箱。
(4)编写接收任务,从邮箱中接收消息。
(5)观察任务之间的消息传递,确保信息传递是可靠和同步的。
4、问题分析及算法描述
本实验主要是通过按键来控制信号量和邮箱的输送,以此来控制LED任务的闪烁,通过在触摸区画画,改变CPU的使用率,当我们执行清屏校准任务时,触摸屏任务则会被挂起,整体流程图如下:
流程图1 整体流程图
1)首先我们要对主函数进行设置,初始化系统硬件和UCOS-II操作系统,创建了一个启动任务,并启动了操作系统,使得多个任务可以并发运行。
主要代码如下:
int main(void)
{
delay_init(); //延时函数初始化
uart_init(72);
NVIC_Configuration();
LED_Init(); //初始化与LED连接的硬件接口
LCD_Init(); //初始化LCD
KEY_Init(); //按键初始化
tp_dev.init(); //触摸屏初始化
ucos_load_main_ui(); //加载主界面
OSInit(); //初始化UCOSII
OSTaskCreate(start_task,(void*)0(OS_STK*)&START_TASK_STK[START_STK_SIZE1],START_TASK_PRIO );//创建起始任务
OSStart();
}
以下是对这段代码的解释:这段代码是一个嵌入式系统中使用了μC/OS-II(Micro C Operating System-II)实时操作系统的 main 函数。下面是对每个函数调用的简要解释:delay_init():延时函数初始化,可能是为了提供在程序中进行一些延时的功能。uart_init(72):串口初始化函数,配置串口通信参数,其中 72 可能表示波特率或其他串口配置参数。NVIC_Configuration():配置中断向量表(Nested Vectored Interrupt Controller),可能用于初始化中断相关设置。LED_Init():LED初始化函数,用于配置与LED连接的硬件接口。
LCD_Init():LCD初始化函数,初始化液晶显示屏相关的硬件接口。KEY_Init():按键初始化函数,用于初始化与按键相关的硬件接口。tp_dev.init():触摸屏初始化函数,初始化触摸屏硬件接口。ucos_load_main_ui():加载主界面函数,可能用于加载嵌入式系统的主界面。OSInit():UCOS-II初始化函数,用于初始化UCOS-II实时操作系统。OSTaskCreate(start_task, (void *)0, (OS_STK *)&START_TASK_STK[START_STK_SIZE-1], START_TASK_PRIO):创建任务函数,其中:start_task 是一个任务函数的指针,表示系统启动时要运行的任务。
(void *)0 是任务的参数,这里设置为 0。(OS_STK *)&START_TASK_STK[START_STK_SIZE-1] 表示任务的栈指针,指向任务栈的顶部。START_TASK_PRIO 是任务的优先级。OSStart():启动UCOS-II实时操作系统,开始多任务调度。
2)开始任务。它创建了两个任务,为信号量与邮箱,同时开启一系列任务,并在最后挂起当前任务。这样,在系统启动后,各个任务会按照其优先级被调度执行。
主要代码如下:
//开始任务
void start_task(void *pdata)
{
OS_CPU_SR cpu_sr=0;
pdata = pdata;
msg_key=OSMboxCreate((void*)0); //创建消息邮箱
sem_led1=OSSemCreate(0); //创建信号量
OSStatInit(); //初始化统计任务.这里会延时1秒钟左右
OS_ENTER_CRITICAL(); //进入临界区(无法被中断打断)
OSTaskCreate(led0_task,(void *)0,(OS_STK*)&LED0_TASK_STK[LED0_STK_SIZE-1],LED0_TASK_PRIO);
OSTaskCreate(touch_task,(void *)0,(OS_STK*)&TOUCH_TASK_STK[TOUCH_STK_SIZE-1],TOUCH_TASK_PRIO);
OSTaskCreate(led1_task,(void *)0,(OS_STK*)&LED1_TASK_STK[LED1_STK_SIZE-1],LED1_TASK_PRIO);
OSTaskCreate(main_task,(void *)0,(OS_STK*)&MAIN_TASK_STK[MAIN_STK_SIZE-1],MAIN_TASK_PRIO);
OSTaskCreate(key_task,(void *)0,(OS_STK*)&KEY_TASK_STK[KEY_STK_SIZE-1],KEY_TASK_PRIO);
OSTaskSuspend(START_TASK_PRIO); //挂起起始任务.
OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断)
}
下面是对该算法的解释:
OS_CPU_SR cpu_sr=0;:定义一个变量 cpu_sr,用于存储CPU的状态寄存器值。
msg_key=OSMboxCreate((void*)0);:创建一个消息邮箱 msg_key,用于任务之间的消息传递。sem_led1=OSSemCreate(0);:创建一个信号量 sem_led1,初始化为0。OSStatInit();:初始化统计任务,该函数通常用于启动一个统计任务,用于收集有关任务和中断的运行时间等信息。这里可能还包含了一个延时,大约1秒钟左右。OS_ENTER_CRITICAL();:进入临界区,这是一个关中断的操作,确保在执行一些关键操作时不被中断打断。任务创建:OSTaskCreate(led0_task, (void *)0, (OS_STK*)&LED0_TASK_STK[LED0_STK_SIZE-1], LED0_TASK_PRIO);:创建LED0任务。
OSTaskCreate(touch_task, (void *)0, (OS_STK*)&TOUCH_TASK_STK[TOUCH_STK_SIZE-1], TOUCH_TASK_PRIO);:创建触摸屏任务。
OSTaskCreate(led1_task, (void *)0, (OS_STK*)&LED1_TASK_STK[LED1_STK_SIZE-1], LED1_TASK_PRIO);:创建LED1任务。
OSTaskCreate(main_task, (void *)0, (OS_STK*)&MAIN_TASK_STK[MAIN_STK_SIZE-1], MAIN_TASK_PRIO);:创建主任务。
OSTaskCreate(key_task, (void *)0, (OS_STK*)&KEY_TASK_STK[KEY_STK_SIZE-1], KEY_TASK_PRIO);:创建按键任务。
OSTaskSuspend(START_TASK_PRIO);:挂起当前任务(即 start_task 任务)。这是因为一旦所有任务都创建完毕,不再需要 start_task 任务运行。
OS_EXIT_CRITICAL();:退出临界区,允许中断。
流程图2开启任务
3)下面是LED0和LED1的任务。
主要代码如下:
//LED0任务
void led0_task(void *pdata)
{
u8 t;
while(1)
{
t++;
delay_ms(10);
if(t==8)LED0=1; //LED0灭
if(t==100) //LED0亮
{
t=0;
LED0=0;
}
}
}
//LED1任务
void led1_task(void *pdata)
{
u8 err;
while(1)
{
OSSemPend(sem_led1,0,&err);
LED1=0;
delay_ms(200);
LED1=1;
delay_ms(800);
}
}
下面是对这个算法的描述:LED0设置了无符号整数t,当t等于8时,LED0熄灭,等于100时候闪烁。延迟10毫秒,LED1:OSSemPend(sem_led1,0,&err);任务会在这里等待 sem_led1 变为非零。如果 sem_led1 的值为0,任务会一直等待,直到有其他任务或中断释放了 sem_led1,使其值变为非零。等待结束后,err 变量将存储函数执行的错误码。当成功获取信号量后,LED1闪烁,延迟200毫秒后,LED1熄灭,再次延迟800毫秒,以此循环反复。
流程图3 LED任务
4)主任务通过等待按键信息和定时器计数,执行相应的操作,包括发送信号量、清屏、触摸屏校准等,并在LCD屏幕上显示相关信息。
主要代码如下:
void main_task(void *pdata)
{
u32 key=0;
u8 err;
u8 semmask=0;
u8 tcnt=0;
while(1)
{
key=(u32)OSMboxPend(msg_key,10,&err);
switch(key)
{
case KEY0_PRES://发送信号量
semmask=1;
OSSemPost(sem_led1);
break;
case KEY1_PRES://清除
LCD_Fill(0,121,lcddev.width,lcddev.height,WHITE);
break;
case WKUP_PRES://校准
OSTaskSuspend(TOUCH_TASK_PRIO); //挂起触摸屏任务
if((tp_dev.touchtype&0X80)==0)TP_Adjust();
OSTaskResume(TOUCH_TASK_PRIO); //解挂
ucos_load_main_ui(); //重新加载主界面
break;
}
if(semmask||sem_led1->OSEventCnt)//需要显示sem
{
POINT_COLOR=BLUE;
LCD_ShowxNum(192,50,sem_led1->OSEventCnt,3,16,0X80);//显示信号量的值
if(sem_led1->OSEventCnt==0)semmask=0; //停止更新
}
if(tcnt==50)//0.5秒更新一次CPU使用率
{
tcnt=0;
POINT_COLOR=BLUE;
LCD_ShowxNum(192,30,OSCPUUsage,3,16,0); //显示CPU使用率
}
tcnt++;
delay_ms(10);
}
}
以下是对这段代码的解释:key=(u32)OSMboxPend(msg_key,10,&err);: 从消息邮箱 msg_key 中获取按键信息,等待时间为10个时钟节拍。获取的按键信息会被存储在 key 变量中。switch(key) { ... }: 根据获取的按键信息执行相应的操作,根据按键值进行分支判断。case KEY0_PRES:: 如果按下了KEY0按键,设置 semmask 为1,并通过 OSSemPost(sem_led1) 发送信号量 sem_led1。case KEY1_PRES:: 如果按下了KEY1按键,用白色填充LCD屏幕的一部分,相当于清屏操作。case WKUP_PRES:: 如果按下了WKUP按键,挂起触摸屏任务,进行触摸屏校准,然后解挂触摸屏任务,并重新加载主界面。if(semmask || sem_led1->OSEventCnt) { ... }: 如果 semmask 为真或者信号量 sem_led1 的计数不为零,表示需要显示信号量信息,然后进行显示操作。显示信号量的值在LCD屏幕上指定位置,蓝色字体。if(tcnt==50) { ... }: 如果计数器 tcnt 达到50,即0.5秒,显示操作系统的CPU使用率(OSCPUUsage)在LCD屏幕上指定位置,蓝色字体。tcnt++;: 计数器递增,用于计时。delay_ms(10);: 延时10毫秒,控制任务执行的速率。
5)对触摸屏任务进行设置,判断触摸屏被按下的时候,在规定的区域进行画图。
主要代码如下:
//触摸屏任务
void touch_task(void *pdata)
{
while(1)
{
tp_dev.scan(0);
if(tp_dev.sta&TP_PRES_DOWN) //触摸屏被按下
{
if(tp_dev.x[0]<lcddev.width&&tp_dev.y[0]<lcddev.height&&tp_dev.y[0]>120)
{
TP_Draw_Big_Point(tp_dev.x[0],tp_dev.y[0],RED); //画图
delay_ms(2);
}
}else delay_ms(10); //没有按键按下的时候
}
}
以下是对这段代码的解释:void touch_task(void *pdata): 定义了一个名为 touch_task 的函数,该函数接受一个 void 类型指针参数 pdata,通常在嵌入式系统中用于传递任务的数据。while(1) { ... }: 无限循环,表示触摸屏任务将一直运行tp_dev.scan(0);: 调用触摸屏设备的 scan 函数,可能是用于扫描触摸屏状态,更新触摸屏的内部数据结构。if(tp_dev.sta & TP_PRES_DOWN) { ... }: 检查触摸屏状态是否被按下。TP_PRES_DOWN 是一个宏定义,表示触摸屏被按下的状态。if(tp_dev.x[0] < lcddev.width && tp_dev.y[0] < lcddev.height && tp_dev.y[0] > 120) { ... }: 检查触摸点的坐标是否在合理范围内,例如在屏幕内且在垂直方向上在120以上。TP_Draw_Big_Point(tp_dev.x[0], tp_dev.y[0], RED);: 如果条件满足,调用 TP_Draw_Big_Point 函数,在触摸点的位置画一个大点,颜色为红色。delay_ms(2);: 引入一个短暂的延时,可能是为了稳定触摸屏的输入。else { delay_ms(10); }: 如果触摸屏没有被按下,引入一个相对较长的延时,减少任务执行的频率,节省系统资源。
5、实验结果
(1) LED任务
实验结果如图4所示:
图4 LED任务实验结果
实验结果描述:上面的运行结果如图所示:首先是初始界面,然后我们按下按键输送信号量后,LED1闪烁过了200毫秒熄灭,过800毫秒再次闪烁,以此反复,而到LED0计数循环到100后,LED0闪烁,延迟10毫秒后又重新开始计数,计数到8,熄灭。以此反复。上图中绿色代表LED0,红色代表LED1。
(2) 触摸屏任务
图5 触摸屏实验结果
实验结果描述:在触摸区进行画画,当我们画入后,我们CPU的使用量也会从0%,慢慢增加,每0.5秒更新一次
(3)清屏校准任务
图6 清屏校准实验结果
实验结果描述:按下KEY1进行到我们的清屏校准界面,挂起我们的触摸屏任务。
6、分析与心得
在进行嵌入式系统中信号量与邮箱的实验过程中,我深刻体会到了它们在多任务处理和通信方面的重要性。以下是我在实验中的一些心得体会:信号量的一大优势是能够方便地管理系统中的资源。通过对资源的分配和释放使用信号量来进行控制,可以有效地避免资源竞争和浪费。在实验中,我学会了如何使用信号量来管理共享资源,确保系统的高效运行。通过这次实验,我更深入地了解了嵌入式系统中信号量和邮箱的原理和应用。这些技术在实际项目中有着广泛的应用,对于构建高效、可靠的嵌入式系统非常关键。