请大家先想一下,为什么想学uc/OS-II?
在通过例程学习单片机的时候有没有这样的疑问,为什么例程是一个个孤立的呢,能否整合在一起?RTOS就是这样一个多线程运行的好工具,我选取UCOS来实现,这篇文章主要是以uc/OS-II来讲,想更深入可以继续学uc/OS-III,有时间片轮转的新功能。
这篇先讲述代码移植和例程,后面在补充UCOS的工作原理。
一.首先是将ucosII移植到STM32C8T6上来,原来黄老师用的编译器是IAR,这里也选用IAR的ucos移植包,具体可以参考这位兄弟的移植教程
二.移植好以后可以我们以显示屏为主界面做个多任务并发运行,在用按键做多任务切换
1.建立任务前先定义优先级、堆栈大小、任务堆栈、检测实际使用堆栈大小的结构体、任务函数
//任务优先级
#define TASKKey_PRIO 10
//任务堆栈大小
#define TASKKey_STK_SIZE 256
//任务控制块 UCOSIII用
//OS_TCB TaskKeyTaskTCB;
//任务堆栈
OS_STK taskKey_stk[TASK1_STK_SIZE];
//实际使用堆栈大小
OS_STK_DATA TaskKeyStackBytes;
//任务函数
static void taskKey(void *p_arg);
2.再写开始任务,驱动的初始化都放在BSP_Init里,还给每个按键都定义了一个信号量,供其他函数使用,最后在开始函数里打印系统状态信息:OS版本、1秒几个节拍、CPU使用率、已运行的时间节拍、任务切换次数、任务x空闲字节、系统错误代码(这些打印在调试的时候很好使)
static void startup(void *p_arg)
{
OS_CPU_SR cpu_sr=0;
delay_init();
BSP_Init(); //BSP初始化
OSStatInit(); //开启统计任务
EventSem_Key0=OSSemCreate(0); //按键0信号量
EventSem_Key1=OSSemCreate(0); //按键1信号量
EventSem_Key2=OSSemCreate(0);
EventSem_Key3=OSSemCreate(0);
OS_ENTER_CRITICAL(); //进入临界区(关闭中断)
os_err = OSTaskCreateExt(taskKey,
(void *)0,
(OS_STK *)&task1_stk[TASK1_STK_SIZE - 1],
(INT8U)TASK1_PRIO,
(INT16U)TASK1_PRIO,
(OS_STK*)&task1_stk[0],
(INT32U) TASK1_STK_SIZE,
(void *)0,
(INT16U)(OS_TASK_OPT_STK_CHK|OS_TASK_OPT_STK_CLR));
......
OS_EXIT_CRITICAL(); //退出临界区(开中断)
while(1)
{
//OSTaskStkChk(OS_PRIO_SELF, &STARTUPStackBytes);
OSTaskStkChk(TASK1_PRIO, &Task1StackBytes);
......
printf("uC/OS-II:V%ld.%ld%ld\r\n",OSVersion()/100,(OSVersion()%100)/10,(OSVersion()%10)); //输出版本
printf("TickRate: %ld \r\n",OS_TICKS_PER_SEC); //输出时钟节拍
printf("CPU Usage: %ld% \r\n",OSCPUUsage);
printf("已运行的时间节拍#Ticks: %ld \r\n",OSTime);
printf("任务切换次数#CtxSw: %ld \r\n",OSCtxSwCtr);
printf("任务1空闲字节=%d\r\n",Task1StackBytes.OSFree);
......
delay_ms(5000);
if(os_err!=0u)
{
printf("Error Code=%d\r\n",os_err); //系统错误代码
}
}
}
3.给按键单独建个任务,用于输出信号量,为了更有感觉我们让每按一次叫一下
static void task1(void *p_arg)
{
while(1)
{
delay_ms(10);
if(KEY_Scan(1)==KEY0_PRES) //KEY0按下
{
LED0=ENABLE;
BEEP1=!BEEP1; //蜂鸣器响
delay_ms(300);
BEEP_OFF();
OSSemPost(EventSem_Key0); //有按键按下,发送信号量
while(KEY_Scan(1)==KEY0_PRES)
{
delay_ms(5); //等待按键释放
}
LED0=DISABLE;
}
else if(KEY_Scan(1)==KEY1_PRES)
{ .......
4.我在写例程的时候也介绍下UCOS里常用的功能,如果后期想用按键做任务切换,可以使用任务悬挂和恢复函数,用于切换显示屏。
OSTaskSuspend(12); //关闭状态显示
OSTaskSuspend(13); //关闭DMA传输
OSTaskSuspend(15); //关闭日历
OSTaskResume(14); //打开ADC
5.接下来介绍软件定时器,和硬件定时器的区别是这不用通过定时器中断来实现,节省资源
OS_TMR * tmr1; //软件定时器1
tmr1=OSTmrCreate(0,10,OS_TMR_OPT_PERIODIC(OS_TMR_CALLBACK)tmr1_callback,0,"tmr1",&err);//100ms执行一次tmr1里的任务
OSTmrStart(tmr1,&err);//启动软件定时器1
void tmr1_callback(OS_TMR *ptmr,void *p_arg) //定时器处理函数
{
LED3=!LED3;
}
6.接下来介绍信号量使用,前面按键提供了一个EventSem_Keyx的信号量,用于启动DMA传输
#define SEND_BUF_SIZE 4200 //发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
const u8 TEXT_TO_SEND[]={"xxxxxxxx"}; //为了时间足够长,把字符串循环写入SendBuff中
static void task4(void *p_arg)
{
u8 t;
u8 event_err;
float pro=0;//进度
OS_CPU_SR cpu_sr=0;
MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR,(u32)SendBuff,SEND_BUF_SIZE);//DMA1通道4,外设为串口1,存储器为SendBuff,长度SEND_BUF_SIZE.
while(1)
{
OSSemPend(EventSem_Key3, 0, &event_err);
display_clean();
display_string(0, 0, 1, "Start_Transimit");
printf("\r\nDMA DATA:\r\n");
delay_ms(700);
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
MYDMA_Enable(DMA1_Channel4);//开始一次DMA传输
while(1)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET) //判断通道4传输完成
{
DMA_ClearFlag(DMA1_FLAG_TC4); //清除通道4传输完成标志
break;
}
pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到当前还剩余多少个数据
pro=1-pro/SEND_BUF_SIZE;//得到百分比
pro*=100;
display_num(1, 0, 1, pro);
}
display_num(1, 0, 1, 100);
display_string(2, 0, 1, "Transimit_Finished");
}
}
7.接下来介绍UCOS任务调度功能,只要函数中有OS延时,等待OS信号量挂起等操作才会引起任务切换。如果想锁定任务不让切换,可以使用OSSchedLock()和OSSchedUnlock(),display里面有写延时函数,这里不像让他打断adc转换。
while(1)
{
OSSemPend(EventSem_Key0, 0, &event_err);
display_clean();
display_string(2, 0, 1, "ADC_Value");
display_string(5, 0, 1, "ADC_Voltage");
OSSchedLock();
adcx=Get_Adc_Average(ADC_Channel_1, 10);
display_num(2, 60, 1, adcx);
temp=(float)adcx*(3.3/4096);
adcx=temp;
display_num(5, 70, 1, adcx);
temp-=adcx;
temp*=1000;
display_num(5, 80, 1, adcx);
OSSchedUnlock();
delay_ms(100);
}
8.最后是RTC,时间作为主界面使用,因为时间一直需要刷新,不过优先级放最低,其他任务可以在延时的时候抢占。
while(1)
{
display_string(2, 0, 1, "Year");
display_string(3, 0, 1, "Mouth");
display_string(4, 0, 1, "Date");
display_string(5, 0, 1, "Hour");
display_string(6, 0, 1, "Minute");
display_string(7, 0, 1, "Second");
if(RTC_Alarm_Flag==DISABLE)
{
if(t!=calendar.sec)
{
t=calendar.sec;
display_num(2, 30, 1, calendar.w_year);
display_num(3, 36, 1, calendar.w_month);
display_num(4, 30, 1, calendar.w_date);
display_num(5, 30, 1, calendar.hour);
display_num(6, 42, 1, calendar.min);
display_num(7, 42, 1, calendar.sec);
}
}
else
{
LED2=!LED2;
printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间
delay_ms(500);
}
如下串口输出打印:
程序例程会在稍后提供,另外编写程序的时候会遇上堆栈溢出等问题,IAR编译器可以在flash.icf文件里修改堆栈大小
/*-Specials-*/
define symbol __ICFEDIT_intvec_start__ = 0x08000000; //向量表的起始地址
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; //ROM起始地址
define symbol __ICFEDIT_region_ROM_end__ = 0x080FFFFF; //ROM结束地址
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; //RAM起始地址
define symbol __ICFEDIT_region_RAM_end__ = 0x20017FFF; //RAM结束地址
/*-Sizes-*/
define symbol __ICFEDIT_size_cstack__ = 0x400; //栈大小
define symbol __ICFEDIT_size_heap__ = 0x200; //堆大小
/**** End of ICF editor section. ###ICF###*/
define memory mem with size = 4G; //定义芯片的存储空间4G 32位寻址空间最大4G
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__]; //ROM大小
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__]; //RAM大小
define block CSTACK with alignment = 8, size = __ICFEDIT_size_cstack__ { }; //堆与栈大小,8字节对齐
define block HEAP with alignment = 8, size = __ICFEDIT_size_heap__ { };
initialize by copy { readwrite }; //启动时将RW数据搬移到RAM中,进行RW数据初始化
do not initialize { section .noinit }; //不初始化有.noinit性质的块
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec }; //在0x08000000处放置.intvec,即向量表
place in ROM_region { readonly }; //在ROM中放置1.只读数据
place in RAM_region { readwrite,
block CSTACK, block HEAP }; //在RAM中放置1.可读写数据2.栈区3.堆区