本帖最后由 eltonchang2001 于 2022-12-14 13:42 编辑
一.序言 1.1.起步-安装工具
要运行本资料中的示例代码,必须安装MDK-ARM工具链。可在如下地址下载最新版本,并运行安装程序。 http://www.keil.com/mdk5/install 该安装程序是MDK-ARM的核心,包括IDE,编译/链接工具和基本调试模块。不包括对基于Cortex-M构架微控制器的具体型号支持,要支持具体的型号需要下载"Device Family Pack"(集合了启动文件,Flash编程算法和调试支持)。 本资料中的练习,均基于STM32F103RB微控制器,所以需要安装对应的"Device Family Pack"。 初次安装完毕,Pack Installer会自动启动。也可通过工具栏启动,如图所示: 在Pack Installer选择相应的模块安装,Pack Installer自动下载并安装。 1.2.安装例程
本资料中涉及到的例程也被做成CMSIS pack下载。在Pack Installer中双击Hitex.CMSIS_RTOS_Turorial.xxxx.pack file即可自动安装。 安装完成后界面如图: 1.3.硬件需求
不需要硬件支持! Keil工具链包含基于Cortex-M构架微控制器的模拟器,并可完全模拟其模型(包括CPU和外设)。这就意味着可以在debug模式下模拟运行。
二.综述
学习本资料,分三个步骤: - 首先建立一个基于Cortex-M构架微控制器的RTOS工程,并运行起来;
- 进一步深入RTOS每个细节,体验RTOS对应用程序的贡献和闪光点;
- 对RTOS有个整体认识后,深入探讨如何配置RTOS选项;
对于没有接触过RTOS的新手来说,以下两点需要克服:
- 进程(或者叫任务),重点在理解进程的运行原理;
- 进程见通讯,重点是理解进程间的同步通讯;
2.1.进入RTOS的第一步
2.2导入CMSIS-RTOS Api
2.3进程
标准C语言的最小程序块是函数(函数被其他代码并完成某些特定的运算)。在CMSIS-RTOS中,基本代码单元是进程。进程与函数类似,但也有一些不同。 函数示例: 进程示例: Unsigned int fun(void) void thread(void) { { .... ...... Return(ch); while(1){........} } } 最大的区别是,函数总归要返回到被调用处,而进程则是无限循环,不会主动结束。 RTOS程序有一定数量的进程组成,在调度器控制下运行。调度器使用Systick中断生成时间片,并以时间片为单位分配各个进程的运行时间。如,进程1运行5ms,而后调度器将CPU分配给进程2一个相似的时间段,然后将cpu分别给进程3,......。宏观来看,好像各个进程同步运行。 在概念层面,程序包含数个同步运行的进程,而每个进程则是完成特定功能的独立代码段。进程代码的独立性,使设计代码、测试可限制在进程内进行,进而组合各个进程完成程序设计,这就使进程利于面向对象程序设计。同样道理,进程可将调试局限在进程内,也更利于调试。后续可发现,进程对提高代码复用性同样有利。 进程在创建时,系统自动分配进程ID,用作进程的标识,如下: osThreadId id1,id2,id3 完成进程切换,需要制定硬件定时器作为RTOS时间片参考,并消耗一定的代码开销。当进程切换时,系统需要保存正在运行的进程信息,同时加载将要运行的进程的信息,统称为"任务切换时间",它是评估RTOS的重要指标。进程信息保存在进程控制块中。
2.4创建进程
一个创建进程的示例: Void thread1(void const *parm); osThreadDef(thread1,osPriorityNormal, 1, 0); 优先级 实例数 stacksize(0默认size) osThreadId thread1_id = osThreadCreate(osThread(thread1),NULL); Void thread1(void const *parm) { //init code While(1) //thread body { ...... } }
2.5进程优先级和进程管理
一经创建,系统分配进程ID,进程ID是管理进程的标识。 进程有优先级特性,如图: 管理进程包括设置优先级,读取优先级,进程消亡等,Api如下: osStatus osThreadSetPriority(threadID,priority); osPriority osThreadGetPriority(threadID); osStatus osThreadTerminate(threadID);
2.6多实例进程
进程支持多实例,如Ex4 Multiple Instance,代码摘录: void Led_Switch(void const *argument) { LED_ON((uint32_t)argument); Delay(500); LED_OFF((uint32_t)argument); Delay(500); } osThreadDef(osThread(Led_Swtich),osPriorityNormal,2,0); osThreadId id1 = osThreadCreate(osThread(Led_Swtich),(void *)1); osThreadId id2 = osThreadCreate(osThread(Led_Swtich),(void *)2);
2.7开始RTOS
默认,RTOS自main函数起接管系统调度,所以main函数是第一个运行中进程。一旦进入main函数,首先调用osKernelInitialize()停用调度,以争取时间完成硬件初始化(如GPIO配置,USART配置等)和创建需要的进程或其他RTOS组件,而后调用osKernelStart()将系统控制权交还给RTOS,如下代码示例: Void main(void) { osKernelInitialize(); //user code Init_thread(); osKernelStart(); } 上例中,main进程在完成进程创建后运行到"}"时消亡,这在RTOS中是不推荐的。Main函数也可作为一个进程,并通过ID管理,如下: osThreadId main_id; Void main(void) { osKernelInitialize(); //user code main_id = osThreadGetId(); //返回当前进程ID Init_thread(); osKernelStart(); While(1) //thread body { } } 三.时间管理RTOS提供基本的时间管理组件。 3.1延时函数
事件组件中最基本的服务就是延时函数,在应用程序中可直接调用延时函数,非常方便。 插曲:尽管RTOS内核约5K左右,相比于非RTOS系统中延时循环的无用代码消耗,RTOS还是优势明显,这就是RTOS出现的原因。 函数原型:void osDelay(uint32_t millisec) 调用延时函数的进程会进入WAIT_DELAY状态并持续延时函数制定的时间(millisec),调度器转向其他READY状态进程。延时结束,进程进入READY状态,等待调度器调度。 3.2等待事件
除了等待制定的时间,osWait也可中断进程并使进程进入等待状态直至被重新触发(触发时间可以是信号、信号量、消息等),并且osWait同样支持指定延时的周期。 函数原型:osStatus osWait(uint32_t millisec) 注意:keil RTX不支持此节内容。 3.3虚拟定时器
CMSIS-RTOS支持虚拟定时器,虚拟定时器向下计数,溢出时运行用户定义的call-back函数。虚拟定时器可以定位为单次和循环模式,步骤如下: - 定义回调函数(call-back);
- 定义定时器结构体;
- 创建定时器;
- 启动定时器;
如Ex 6 Virtual Timers代码摘录: void callback(void const *pram) { Switch((uint32_t) pram) { case 0: GPIOB->ODR ^= 0x8; Break; case 1: GPIOB->ODR ^= 0x4; Break; case 2: GPIOB->ODR ^= 0x2; Break; case 3: Break; } } osTimerDef(osTimer(Timer0_handle),callback); osTimerDef(osTimer(Timer1_handle),callback); osTimerDef(osTimer(Timer2_handle),callback); osTimerDef(osTimer(Timer3_handle),callback); osTimerId timer0 = osTimerCreate(osTimer(Timer0_hanlder),osTimerPeriodic,(void *)0); osTimerId timer1 = osTimerCreate(osTimer(Timer1_hanlder),osTimerPeriodic,(void *)1); osTimerId timer2 = osTimerCreate(osTimer(Timer2_hanlder),osTimerPeriodic,(void *)2); osTimerId timer3 = osTimerCreate(osTimer(Timer3_hanlder),osTimerPeriodic,(void *)3); osTimerStart(timer0_handle,0x100); osTimerStart(timer1_handle,0x100); osTimerStart(timer2_handle,0x100); osTimerStart(timer3_handle,0x100); 3.4微秒延时
借用系统中的Systick原始计数值可实现微妙延时。微妙延时不触发调度,它仅仅暂停执行指定时间段,遵循如下步骤: - 获取Systick原始计数值;
- 定义延时时间段;
- 等待延时结束;
示例代码如下: Uint32_t tick,delayPeriod; tick = osKernelSysTick(); //1 delayPeriod = osKernelTickMicroSec(100); //2 do{ .... }while((osKernelSysTick()-tick) < delayPeriod); //3 3.5空闲进程
空闲进程在系统没有可用进程时被调用,以防止系统没有进程可用。 空闲进程定义在RTX_Conf_CM.c中,并允许用户自定义代码。通常情况下,空闲进程中配置CPU进入低功耗状态。如此一来,当Systick中断或其他中断时唤醒调度,如有可运行进程则运行进程,否则继续进入低功耗状态。 示例代码:Ex 7 Idle 四.信号4.1功能描述
CMSIS-RTOS keil RTX的进程支持多达16个信号,信号保存在进程控制块中。当进程中存在等待信号时(不管是单个等待信号还是多个等待信号),进程暂停执行直至其他进程发出了被等待的信号。 调用信号等待函数,将触发当前进程中止运行并进入等待状态。处于等待状态的进程满足以下两个条件时退出等待,进入可被调度状态: 信号等待函数原型:osEvent osSignalWait(uint32_t signals,uint32_t millisec) 等待时间设置为0fffff,表示始终不溢出; 等待时间设置为0时,表示任一信号置位即可引起中止等待; 其他进程可置位或清除等待信号: Uint32_t osSignalSet(osThreadId thread_id,uint32_t signals) Uint32_t osSignalClear(osThreadId thread_id,uint32_t signals) 另,调用osEvent.value.signals,返回值指示当前被置位信号。 4.2例程
"Ex8 Signals" 4.3中断进程
CMSIS-RTOS使用Systick中断作为系统时钟。因Systick中断的服务等级设定为最低级,当中断服务程序(ISR)的运行时间超出一个Systick中断时,系统中断将受到影响。 于是,在CMSIS-RTOS中,正确的处理方式是将中断服务定义为一个进程,进程中等待信号。而在中断服务中,仅仅给中断进程发送信号。从而极大缩短中断服务程序的长度,转而在线程中执行。 示例代码: osThreadDef(osThread(isr_thread),osPriorityNormal,1,0); osThreadId Isr_thread_id = osThreadCreate(osThread(isr_thread),NULL); 中断线程:void isr_thread(void const *pram) { .... While(1) { osSignalWait(isrSignal,waitForever); ..... } } 中断服务程序:void IRQ_Handler(void) { osSignalSet(Isr_thread_id,isrSignal); } 4.4内核权限调用SVC
CMSIS-RTOS运行在unprivilege模式下,当需要在进程中访问privilege资源时,有两种方式: - 在配置文件中提升进程的权限至privilege状态(如下图所示),但会造成所有的进程运行在privilege模式下,影响系统安全。
参考例程"Ex9 interrupt signal" - 在需要privilege权限时运行"系统级"代码。
4.4.1SVC
遵循如下步骤: - 新建"系统级"代码列表(.s汇编文件),如下图:
SVC_Tables.s代码如下:
AREA SVC_TABLE, CODE, READONLY EXPORT SVC_Count SVC_Cnt EQU (SVC_End-SVC_Table)/4 SVC_Count DCD SVC_Cnt ; Import user SVC functions here. IMPORT __SVC_1 //第一个"系统级"代码 EXPORT SVC_Table SVC_Table ; Insert user SVC functions here. SVC 0 used by RTL Kernel. DCD __SVC_1 ; user SVC function SVC_End END 其中__SVC_1就是"系统级"用户代码函数。 - 建立"系统级"代码与"进程级"代码接口,示例:
void __svc(1) init_ADC (void); __svc(1)代表SVC_Tables.s中第一个"系统级"代码,同样如果有多个应用,依次递增即可。 - 编写"系统级"代码,如下图:
Void __SVC_1 (void) { ...... } 在完成定义后,进程中调用init_ADC()将自动执行__SVC_1()中的代码。
五.信号量5.1功能描述
与信号类似,信号量是两个或多个进程同步的方法。 信号量项是一个包含多个信号的容器。当进程执行到需要信号量的代码段(进程申请信号量),如果信号量中有信号可用(包含不少于一个的信号),则进程继续执行,并且信号量中的信号数自减一。相反,如信号量中无信号可用(包含0个信号),则进程中止执行并等待信号量中的信号可用。 同时,进程中可向信号量添加信号数目,从而引起信号量中的可用信号数增一。 如上图。假定信号量初始化为只有一个可用信号,当任务1提出申请时,信号量中含有1个可用信号,则任务1继续执行并引起信号量中的可用信号为0。此时任务2若提出申请因信号量中无可用信号,任务2进入信号量等待状态,直至任务1释放信号量。 可见,进程可以释放信号给信号量。 5.2创建信号量
示例代码: osSemaphoreId sem1; osSemaphoreDef(sem1); sem1 = osSemaphoreCreate(osSemaphore(sem1),SIX_TOKENS); 定义了一个含有6个可用信号的信号量sem1。 信号量初始化后,进程中即可申请信号量,使用函数: osSemaphoreWait(osSemaphoreId sem_id,uint_32 millisec) Millisec = 0xffff wait for ever 信号量使用结束后,释放信号量,使用函数: osSemaphoreRelease(osSemaphoreId sem_id); 5.3例程
Ex11 Interrupt Signals 5.4使用场景5.4.1信号
两个线程间同步执行是信号量最基本的应用。示例代码: osSemaphoreId sem_id; osSemaphoreDef(sem_id); void task1(void) { Sem_id = osSemaphoreCreate(osSemaphore(sem1),0); While(1) { Fun(A); osSemaphoreRelease(sem1); } } Void task2(void) { While(1) { osSemaphoreWait(sem1,osWaitForever); Fun(B); } } 在这个案例中,Fun(A)始终先于Fun(B)执行。 5.4.2限额
限额用于限制某些资源的配额。例如,某个指定的内存块只运行指定数目的应用访问。 如下例程,信号量初始化为5个信号,每个申请信号量的线程造成信号自减,当获取信号量的进程为5个时,后续申请信号量的进程进入等待状态,直至已获取配额的进程释放信号量,代码例程: osThreadId sem_id; osThreadDef(sem1); Void task1(void) { Sem_id = osThreadCreate(osThread(sem1),5); While(1) { osSemaphoreWait(sem1,osWaitForever); ProcessBuffer(); osSemaphoreRelease(sem1); } } Void task2(void) { While(1) { osSemaphoreWait(sem1,osWaitForever); ProcessBuffer(); osSemaphoreRelease(sem1); } } ...... 例程"Ex12 Multiplex"。 5.4.3互锁(2个线程同步)
互锁是两个线程同步的另一种通用模式。互锁确保两个线程得到同一互锁点。如下例程: osSemaphoreId arrival1,arrival2; osSemaphoreDef(sem1); osSemaphoreDef(sem2); Void task1(void) { arrival1 = osSemaphore(osSemephore(sem1),0); arrival2= osSemaphore(osSemephore(sem2),0); While(1) { FunA1(); osSemaphoreRelease(arrival2); osSemaphoreWait(arrival1); FunA2(); } } Void task2(void) { While(1) { FunB1(); osSemaphoreRelease(arrival1); osSemaphoreWait(arrival2); FunB2(); } } 此例程中,确保FunA2()、FunB2()同步执行。 5.4.4屏障(多个线程同步)
屏障是多个进程同步的有效模式,它的总体思路:设置一个初始化为0的信号量作为屏障,并在所有进程达到同步点时依次释放屏障中的信号量,达到同步执行的目的。 例程"Ex14 Barrier" 5.5注意事项
信号量是RTOS中极端有效的模式。然而,因信号量可在进程中增减甚至销毁,信号量中可用配额数比较难把控,使用时必须实时把控可用配额数。 六.互斥量6.1功能描述
单从功能来讲,互斥量可以看做只含有一个可用配额且不可被创建和销毁的特殊信号量。互斥量主要用于防止对硬件的访问冲突,比如同一时刻只能有一个应用访问串口,否则将造成数据混乱。 申请互斥量的进程,必须等待互斥量中存在有效配额,否则进入等待状态。 6.2创建互斥量
创建互斥量与创建信号量类似,示例代码如下: osMutexId uart_mutex; osMutexDef(Mutex1); 进程中创建互斥量:uart_mutex = osMutexCreate(osMutex(Mutex1)); 其他进程申请互斥量:osMutexWait(uart_mutex); 使用完毕释放互斥量:osMutexRelease(uart_mutex); 6.3例程
例程"Ex15 Mutex" 6.4注意事项
互斥量的使用限制多,也更安全,但扔要注意以下内容: - 使用完毕必须及时释放互斥量,否则将造成后续进程无法使用该资源;
- 调用ThreadTerminate()函数消亡进程时,必须确保该进程没有占用互斥量,否则将造成后续进程无法使用该资源;
七.数据交换信号、信号量、互斥量只用于进程之间的触发,但对进程间的数据交换无能为力。进程间数据交换最简单的方式是全局变量,但即使在简单的系统中,把握和灵活应用全局变量也是不小的挑战,因为全局变量会引起一系列不可预知错误。 在RTOS中,消息队列和邮箱队列是进程间数据交互最为有效、安全的方式。 消息队列和邮箱队列的工作方式基本一样,唯一的区别是消息队列中传输的是待交换数据,而邮箱队列中传输是指向待交换数据的指针,如下图所示: 使用消息队列和邮箱队列进行数据交换有如下好处: - 规范进程间数据交换的接口和缓存,为设计子系统提供可能;
- 规范进程的输入、输出,使进程独立测试、调试成为可能;
7.1消息队列7.1.1创建消息队列
创建消息队列,遵循如下步骤: - 声明消息队列ID,示例:osMessageQId Q_id;
- 定义消息队列结构体,示例:osMessageQDef(Q1,16_Message_Slots,unsigned int);其中16_Message_Slots指示空间大小为16,unsigned int指示空间类型;
- 在进程中创建消息队列,示例:Q_id = osMessageQCreate(osMessageQ(Q1),NULL);
- 声明解析消息队列数据的osEvent类型数据,示例:osEvent result;
- 在进程中发送数据到消息队列,例程:osMessagePut(Q_id,Data,osWaitForever);
- 在另一进程中获取消息队列数据,例程:result = osMessageGet(Q_id,osWaitForever);result.value.xxx;
其中,osEvent是个union结构体,如下所示: Union{ Uint32_t v; Void *p; Uint32_t signals; }value 7.1.2例程
"Ex16 Message queue" 7.2内存链7.2.1功能描述
消息队列中的数据类型可以是数据本身,也可以是指向数据的指针。 消息队列中存储的数据是指向特定内存区域的指针,这样的进程间交换数据的方式成为内存链。 结构体可以达到规范化特定内存区域的目的。 7.2.2创建内存链
创建内存链,遵循如下步骤: 1.定义结构体,用于规范内存块及初始化指针,示例:
Typedef struct { Uint8_t led1; Uint8_t led2; Uint8_t led3; Uint8_t led4; }memory_block_t; 2.初始化内存链,示例:
osPoolId pool_id; osPoolDef(pool_t,ten_blocks,memory_block_t); 在进程中创建pool_id = osPoolCreate(osPool(pool_t)); 3.初始化消息队列,示例:
osMessageQDef(q1,ten_blocks,memory_block_t); osMessageQid q_id; 在进程中创建:q_id = osMessageQCreate(osMessageQ(q1),NULL); 4.发送消息队列,示例:
memory_block_t *led = (memory_block_t *)osPoolAlloc(pool_id); led->led1 = xx;.... osMessagePut(q_id,led,osWaitForever); 5.读取消息队列,示例:
osEvent evt; evt = osMessageGet(q_id,osWaitForever); memory_block_t *temp = (memory_block_t *)evt.value.p; 6.使用完毕释放内存链:示例:
osPoolFree(pool_id,temp); 7.2.3例程
"Ex16 MemoryPool" 7.3邮箱队列7.3.1功能描述
邮箱队列是将内存链融合到消息队列中而成,邮箱队列中存储的同样是指向特定内存区域的指针。 7.3.2创建邮箱队列
同样采用7.2.2中的结构体作为数据基础,Typedef struct { Uint8_t led1; Uint8_t led2; Uint8_t led3; Uint8_t led4; }memory_block_t; 创建邮箱队列遵循如下步骤: 1.创建邮箱队列,例程:
osMailQDef(MQ_1,ten_blocks,memory_block_t); osMailQId mq_id; 进程中创建,mq_id = osMailQCreate(osMailQ(MQ_1),NULL); 2.发送数据,例程:
Memory_block_t *led = (memory_block_t *)osMailAlloc(mq_id); led->led1 = xx;.... osMailQPut(mq_id,led); 3.读取数据,例程:
osEvent evt; evt = osMailGet(mq_id,osWaitForever); Memory_block_t *temp = (memory_block_t *)evt.value.p; 4.使用完毕释放邮箱队列,示例:
osMailFree(mq_id,temp); 7.3.3例程
"Ex17 MailQueue" 八.系统配置
掌握前面的内容,对CMSIS-RTOS有了总体的认识。CMSIS-RTOS包括进程管理、时间管理、进程间通讯等。 本章着力于讨论如何配置系统。CMSIS-RTOS针对基于Cortex-M构架的处理器,提供一个统一的配置文件,RTX_Conf_CM.c,如下图: 8.1进程参数
在讨论进程的相关章节中一经接受了创建进程的基础知识。 每个进程,系统分配一块内存空间用作进程栈(默认200bytes),栈空间在进程创建时指定。 应用中最多允许运行的进程数可配置。 由于进程栈空间、进程数可配置,应用中的内存需求也可很容易的计算出来。 8.2内核调试支持
内核可配置项,包括: 8.2.1追踪溢出
选择该项"stack overflow checking",出现进程栈溢出RTOS内核调用os_error函数并进入死循环。该项主要用于调试阶段的问题追踪,当然也可自定义os_error函数用于最终的应用中打印错误信息,os_error代码在RTX_CONF_CM.C文件中,源码: /* OS Error Codes */ #define OS_ERROR_STACK_OVF 1 #define OS_ERROR_FIFO_OVF 2 #define OS_ERROR_MBX_OVF 3 #define OS_ERROR_TIMER_OVF 4 extern osThreadId svcThreadGetId (void); /// \brief Called when a runtime error is detected /// \param[in] error_code actual error code that has been detected void os_error (uint32_t error_code) { /* HERE: include optional code to be executed on runtime error. */ switch (error_code) { case OS_ERROR_STACK_OVF: /* Stack overflow detected for the currently running task. */ /* Thread can be identified by calling svcThreadGetId(). */ break; case OS_ERROR_FIFO_OVF: /* ISR FIFO Queue buffer overflow detected. */ break; case OS_ERROR_MBX_OVF: /* Mailbox overflow detected. */ break; case OS_ERROR_TIMER_OVF: /* User Timer Callback Queue overflow detected. */ break; default: break; } for (;;); } 8.2.2监控栈使用率
选择"stack usage watermark"项,oxcc样式自动写入进程栈。运行时,watermark用于计算最大栈内存使用率,并在"system and Event viewer"窗口报告,如下图所示: 8.2.3用户定时器数
如用户定时器数量与应用中使用的虚拟定时器不符,会造成os_timer()函数失效。 8.2.4进程运行权限选择
如"Ex9 interruter signal",进程运行权限可配置。 8.3系统时基
默认的系统时基是Cortex-M中的SysTick定时器。但,也支持自定义使用其他定时器作为系统时基。 8.4时间片
默认的时间片是5ms。 8.5调度选项
调度器支持如下三种调度模式: 1.抢占式
此模式下,系统中进程拥有不同的优先级,当拥有高优先级的进程进入"ready"状态,调度器转入高优先级进程运行。 2.轮询式
此模式下,系统根据时间片为每个进程分配运行时间,处于运行态的进程在时间片到来时触发调度(注意,即使高优先级的进程进入"ready"状态也要等时间片结束)。 3.轮询、抢占式(默认状态)
此模式下,系统根据时间片为每个进程分配运行时间,处于运行态的进程在时间片到来时或高优先级的进程进入"ready"态触发调度(注意,高优先级的进程进入"ready"状态将马上触发调度)。 4.协同式
此模式下,进程拥有相同的优先级,有且仅有运行态的进程主动申请系统调度才会引起调度。 8.6源码调试
如果用户需要源码级别的调试,遵循如下步骤: - 新建文本文件,命名为"xxx.ini";
- "xxx.ini"中添加,SET SRC = <PATH>,其中<PATH>是RTX源码的文件夹,默认是C:\Keil\ARM\pack\arm\cmsis\<version>\cmsis\rots\rtx。
- 在调试文件中导入"xxx.ini",如下图所示:
注:"xxx.ini"中xxx代表任意长度满足PC操作系统命名规格的字符串。
|