前面一篇文章简单写了一下怎么用上CMSIS-RTOS,很轻松,很随意。但是当我想系统的学学这个RTOS的时候,突然发现居然没有多少中文资料,连最基本的简易教程都木有,很失望,也许是因为CMSIS-RTOS是ARM刚推出不久的API吧,这个API是对一个老操作系统RTX的封装,所以网上寥寥几篇关于Keil使用RTOS的都是基于RTX来写的。所以我不得不自己研究ARM给的官方文档(安装新版keil 自动会有,路径...Keil_v5\ARM\Pack\ARM\CMSIS\4.5.0\CMSIS\Documentation\RTX 我装的是V5.21a评估版),单看英文文档时间短了行,长时间看就想睡觉,必须理论结合实践!于是我觉得是不是有必要翻译一下这篇文档,一来提高学习效率,二来帮助想学还苦于没有资料的同学。另外关于为啥想系统学习这个OS的原因以后会讲,现在只关心兴趣问题。
下面进入正题:
创建线程Creating Threads
一旦RTOS开始运行,就会有很多系统调用来管理和控制活跃的线程。默认情况下,main()函数自动被创建为第一个可运行的线程。在第一个例子里我们使用main()函数创建了其他线程,并且随后让main()结束运行。然而我们还可以让main当成一个真正的thread使用。首先,我们需要获取它的ID号。此时,我们第一个要调用的RTOS函数就是osThreadGetId(),这个函数返回当前运行thread的ID号,并把它存在ID句柄里。当我们未来某一时刻在OS调用中需要这个线程时,我们使用的就是这个线程的句柄,而非线程的函数名。
osThreadId main_id;//创建线程句柄
void main(void)
{
/*获取main线程的ID号*/
main_id = osThreadGetId();
while(1)
{}
}
现在我们有了main的ID句柄,就可以创建其他应用线程,并随后调用osTermainate(main_id)来结束main线程。相比让一个线程运行到最后一个花括号来结束,这种结束方法更好一些。当然,我们还可以添加while(1)这个循环来继续使用main函数。
在第一个例子中我们就看到main线程可以作为一个launcher 线程来创建其他应用线程。仅需要两步就行:第一步,定义线程结构体(同时还可以定义线程传递参数)。
osThreadId thread_id; //线程句柄
void thread1(void const *argument); //thread1的函数原型
osThreadDef(thread1, osPriorityNormal, 1, 0); //线程定义结构体
定义线程结构体需要先定义线程函数名、线程优先级、创建线程的实例化个数和它的栈大小。这些参数后续会详细说明。一旦定义了线程结构体,我们就可以用osThreadCreated() API来创建线程。线程经常在main线程里创建,当然也可以在其他地方。
thread1_id = osThreadCreated(osThread(thread1), NULL);
上面这条代码创建线程并启动它运行。另外,在启动线程时可以给它传递个参数。
uint32_t startupParameter = 0x23;
thread1_id = osThreadCreate(osThread(thread1), startupParameter);
当创建线程时,还将给它分配一个栈,用来存储上下文切换的数据。注意不要和Cortex处理器栈混淆,它只是分配给线程的一段存储空间。在RTOS配置文件里已经定义了一个默认的栈大小,当然我们也可以自定义栈的大小。osThreadDef()这个函数的最后一个参数设为0时表示使用默认栈大小。如果需要的话,也可以通过在线程结构体里定义一个更大的栈来增加额外的存储资源。
osThreadDef(thread1, osPriorityNomal, 1, 0); //分配默认栈大小
osThreadDef(thread2, osPriorityNomal, 1, 1024); //分配1kB的栈大小
当然,如果你分配了更大的栈空间,在RTOS配置文件里就需要增加额外的存储空间。
实战练习:创建并管理线程
我们将创建一个新的工程,在这个工程里创建并管理几个线程。每个线程里面执行GPIOB端口的某个管脚的反转动作,用来模拟LED闪烁。随后我们将在debug仿真里观察效果。
首先我们打开keil里面的pack installer,如下:
选择boards标签并选择CMSIS-RTOS_Tutorial。
现在在右边栏选择examples 标签,这时可以看到教程的所有实例工程。
选择“Ex 2 and 3 Threads" 并点击install按钮。
执行这一步后将会弹出对话框,选择合适的路径保存工程,新建的keil工程就会打开(如果你没安装这个pack,keil会自动从网上下载这个例程)。
打开RTE管理器
在Board Support选项里勾选MCBSTM32E:LED 。这一步会加载针对MCBSTM32E开发板的支持函数(无视这个板--译者注)。
双击打开main.c文件。
(以下不需要手动添加任何代码,全部自动搞好了--译者注)
当RTOS启动main线程时,我们会创建两个额外的线程。首先我们要给每个线程创建一个句柄,随后定义每个线程的参数,包括线程优先级,实例化个数和栈大小(使用默认值)。
osThreadId main_ID, led_ID1, led_ID2;
osThreadDef(led_thread2, osPriorityNormal, 1, 0);
osThreadDef(led_thread1, osPriorityNormal, 1, 0);
然后在main()函数里创建两个线程:
led_ID2 = osThreadCreate(osThread(led_thread2), NULL);
led_ID1 = osThreadCreate(osThread(led_thread1), NULL);
这里创建线程时传递的是NULL参数。
Build 工程并启动debugger仿真。
全速运行!
打开Debug->OS Support -> System and Thread Viewer
效果:
从上图可以看到四个活跃的线程,其中一个在running,另外的在ready。
打开Debug->OS Support -> Event Viewer:
效果:
在这个事件查看器里可以看到每个线程的执行时间。线程执行时间的可视化会给我们一个线程消耗多少CPU时间的直观感受(是不是很爽!)。
打开Peripherals->General Purpose IO->GPIOB 窗口(貌似会自动打开--译者注):
我们的两个LED线程都在执行GPIO 管脚的翻转,不要看代码,仔细观察两个翻转的管脚。如果你看不到这个现象,可以检查keil软件标题栏view->periodic window update这一项是否勾选。
void led_thread2(void const *argument)
{
for(;;){
LED_On(1);
delay(500);
LED_On(1);
delay(500);
}
}
每一个线程都会调用函数切换LED亮灭,并在亮灭中间插入延时函数。此处有几点需要注意:首先,延时函数可以被每个线程安全的调用。每个线程的本地变量都可以被保存在自有栈里面,而不用背担心被其他线程占用。其次,没有线程会进入调度取消状态,意思就是每个线程在切换到下一个线程之前都会在分配的整个时间片内运行。就像上面的例子中,线程大部分的执行时间都被浪费在延时循环中。最后,线程间没有同步。它们的运行被CPU隔离,观察GPIO仿真可以看到管脚随机的闪烁。