μCOS-III内核实现与应用开发实战指南:基于STM32(txt+pdf+epub+mobi电子书下载)


发布时间:2021-03-09 12:07:34

点击下载

作者:刘火良,杨森

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

μCOS-III内核实现与应用开发实战指南:基于STM32

μCOS-III内核实现与应用开发实战指南:基于STM32试读:

前言

如何学习本书

本书从0开始教你如何把μC/OS-III写出来,既讲解源码实现,也讲解API如何使用。当你拿到本书开始学习时一定会惊讶,原来RTOS(Real Time Operation System,实时操作系统)的学习并没有那么复杂,原来自己也可以写操作系统,成就感立马爆棚。

全书内容循序渐进,不断迭代,前一章都是后一章的基础,因此最好从头开始阅读,不要跳跃。在学习时务必做到两点:一是不能一味地看书,要把代码和书本结合起来学习,一边看书,一边调试代码。如何调试代码呢?即单步执行每一条程序,看程序的执行流程和执行效果与自己所想的是否一致;二是在每学完一章之后,必须将配套的例程重写一遍(切记不要复制,哪怕是一个分号,但可以照书录入),以做到举一反三,确保真正理解。在自己写的时候肯定会错漏百出,这个时候要认真纠错,好好调试,这是你提高编程能力的最好机会。记住,编写程序不是一气呵成的,而是要一步一步地调试。

本书的编写风格

本书以μC/OS-III官方源码为蓝本,抽丝剥茧,不断迭代,教你逐步写出μC/OS-III。书中涉及的数据类型、变量名称、函数名称、文件名称、文件存放的位置都完全按照μC/OS-III官方的方式来实现。学完本书之后,可以无缝地切换到原版的μC/OS-III中使用。要注意的是,在实现的过程中某些函数中会去掉一些形参和冗余的代码,只保留核心的功能,但这并不会影响学习。注意,本书的目的并不是让你自己写一个操作系统,而是让你了解μC/OS-III是如何写出来的,着重讲解原理实现,当你学完这本书之后,再学习其他RTOS将会事半功倍。

本书的技术论坛

如果在学习过程中遇到问题,可以到野火电子论坛www.firebbs.cn发帖交流,开源共享,共同进步。

鉴于水平有限,本书难免有错漏之处,热心的读者也可把勘误发送到论坛上以便改进。祝你学习愉快,μC/OS-III的世界,野火与你同行。

引言

为什么学习RTOS

当我们进入嵌入式这个领域时,首先接触的往往是单片机编程,单片机编程又首选51单片机来入门。这里说的单片机编程通常都是指裸机编程,即不加入任何RTOS的编程。常用的RTOS有国外的FreeRTOS、μC/OS、RTX以及国内的Huawei LiteOS、RT-Thread等,其中开源且免费的FreeRTOS的市场占有率最高,历史悠久的μC/OS位居第二。

在裸机系统中,所有的程序基本都是用户自己写的,所有的操作都是在一个无限的大循环中实现。现实生活中的很多中小型电子产品中用的都是裸机系统,而且能够满足需求。那为什么还要学习RTOS编程,要涉及一个操作系统呢?一是基于项目需求,随着产品要实现的功能越来越多,单纯的裸机系统已经不能完美地解决问题,反而会使编程变得更加复杂,如果想降低编程的难度,可以考虑引入RTOS实现多任务管理,这是使用RTOS的最大优势。二是出于学习的需要,必须学习更高级的技术,实现更好的职业规划,为将来能有更好的职业发展做准备,而不是拘泥于裸机编程。作为一个合格的嵌入式软件工程师,学习是永远不能停止的,时刻都要为将来做准备。“书到用时方恨少”,希望当机会来临时,你不要有这种感觉。

为了帮大家厘清RTOS编程的思路,本书将简单分析这两种编程方式的区别,我们称之为“学习RTOS的命门”,只要掌握这一关键内容,以后的RTOS学习可以说是易如反掌。在讲解这两种编程方式的区别时,我们主要讲解方法论,不会涉及具体的代码,即主要还是通过伪代码来讲解。

如何学习RTOS

RTOS编程和裸机编程的风格不一样,而且很多人说学习RTOS很难,这就导致想要学习的人一听到RTOS编程就会忌惮三分,结果就是“出师未捷身先死”。

那么到底如何学习RTOS呢?最简单的方法就是在别人移植好的系统上先看一看RTOS中的API使用说明,然后调用这些API实现自己想要的功能,完全不用关心底层的移植,这是最简单、快速的入门方法。这种方法有利有弊:如果是做产品,好处是可以快速实现功能,将产品推向市场,赢得先机;弊端是当程序出现问题时,因对RTOS不够了解,会导致调试困难。如果想系统地学习RTOS,那么只会简单地调用API是不可取的,我们应该深入学习一款RTOS。

目前市场上的RTOS,内核实现方式差异不大,只需要深入学习其中一款即可。万变不离其宗,只要掌握了一款RTOS,以后换到其他型号的RTOS,使用起来自然也得心应手。那么如何深入地学习一款RTOS呢?这里有一个非常有效但也十分难的方法,就是阅读RTOS的源码,深入研究内核和每个组件的实现方式。这个过程枯燥且痛苦,但为了能够学到RTOS的精华,还是很值得一试的。

市面上虽然有一些讲解RTOS源码的书,但如果基础知识掌握得不够,且先前没有使用过该款RTOS,那么只看源码不仅非常枯燥,而且难以从全局掌握整个RTOS的构成和实现。

现在,我们采用一种全新的方法来教大家学习一款RTOS,既不是单纯地介绍其中的API如何使用,也不是单纯地拿里面的源码一句句地讲解,而是从0开始,层层叠加,不断完善,教大家如何把一个RTOS从0到1写出来,让你在每一个阶段都能享受到成功的喜悦。在这个RTOS实现的过程中,只需要具备C语言基础即可,然后就是跟着本书笃定前行,最后定有所成。

如何选择RTOS

如何选择RTOS取决于是学习还是做产品,如果是学习,则可以选择一个历史较久、商业化成功、安全验证较多的来学习,而且应深入学习。符合前面这几个标准的只有μC/OS,所以学习RTOS,首选μC/OS,而且μC/OS的相关资料是很丰富的。当然,选择其他的RTOS来学习也是可以的。学完之后就要使用,如果是做产品,即在产品中使用μC/OS,则要面临授权问题,需要支付一定的费用,所以开源免费的FreeRTOS受到了各个半导体厂商和开发者的青睐。目前,FreeRTOS是市场占有率最高的RTOS,非常适合用来做产品。另外,国内的RT-Thread也在迅速崛起,它同样是开源免费的,也是不错的选择。第一部分 从0到1教你写μC/OS内核

本部分以μC/OS-III为蓝本,抽丝剥茧,不断迭代,教你如何从0开始把μC/OS-III写出来。这一部分着重讲解μC/OS-III实现的过程,当你学完这部分之后,再来重新使用μC/OS-III或者其他RTOS,将会得心应手,不仅知其然,而且知其所以然。在源码实现的过程中,涉及的数据类型、变量名称、函数名称、文件名称以及文件的存放目录都会完全按照μC/OS-III的来实现,一些不必要的代码将会被剔除,但这并不影响我们理解整个操作系统的功能。

本部分几乎每一章都以前一章为基础,环环相扣,逐渐揭开μC/OS-III的神秘面纱,让人读起来会有一种豁然开朗的感觉。如果把代码都敲一遍,仿真时得出的效果与书中给出的一样,那从心底油然而生的成就感简直就要爆棚,让人恨不得一下子把本书读完,真是看了还想看,读了还想读。第1章 新建工程——软件仿真

在开始写RTOS之前,先新建一个工程,Device选择Cortex-M3内核的处理器,调试方式选择软件仿真,到最后写完整个RTOS之后,再把RTOS移植到野火STM32开发板上。最后的移植其实已经非常简单,只需要换一下启动文件并添加bsp驱动即可。1.1 新建本地工程文件夹

在开始新建工程之前,我们先在本地计算机端新建一个文件夹用于存放工程。文件夹名设置为RTOS,然后在该文件夹下新建各个文件夹和文件,有关这些文件夹的包含关系和作用如表1-1所示。表1-1 工程文件夹根目录下的文件夹的作用1.2 使用KEIL新建工程

开发环境我们使用KEIL5,版本为5.15,高于版本5即可。1.2.1 New Project

首先打开KEIL5软件,新建一个工程,工程文件放在目录Project\RVMDK(uv5)下面,名称为YH-μC/OS-III,其中YH是野火拼音首字母的缩写,当然你也可以换成其他名称,但是必须是英文,不能是中文。1.2.2 Select Device For Target

当设置好工程名称之后会弹出Select Device for Target对话框,在该对话框中可以选择处理器,这里选择ARMCM3,具体如图1-1所示。图1-1 Select Device For Target对话框1.2.3 Manage Run-Time Environment

选择好处理器,单击OK按钮后会弹出Manage Run-Time Environment对话框。在CMSIS栏中选中CORE,在Device栏中选中Startup文件,如图1-2所示。图1-2 Manage Run-Time Environment对话框

单击OK按钮,关闭Manage Run-Time Environment对话框之后,刚刚选择的CORE(包含于CMSIS)和Startup(包含于Device)这两[1]个文件就会添加到工程组中,如图1-3所示。图1-3 CORE和Startup文件

其实这两个文件刚开始都是存放在KEIL的安装目录下,当配置Manage Run-Time Environment对话框之后,软件就会把选中的文件从KEIL的安装目录复制到工程目录Project\RTE\Device\ARMCM3下面。其中,startup_ARMCM3.s是用汇编语言编写的启动文件,system_ARMCM3.c是用C语言编写的与时钟相关的文件。更加具体的内容可参见这两个文件的源码。只要是Cortex-M3内核的单片机,这两个文件都适用。

[1] 部分图片所示的文件或文件夹名称中,μ或/无法显示,故将μ显示为u,省略/。——编辑注1.3 在KEIL工程中新建文件组

在工程中添加User、μC/OS-III Source、μC/OS-III Ports、C/CPU、C/LIB和Doc文件组,用于管理文件,如图1-4所示。

对于新手,这里有个问题就是如何添加文件组,具体的方法为右击Target1,在弹出的快捷菜单中选择Add Group...命令,如图1-5所示。需要多少个组,就按此方法操作多少次。图1-4 新添加的文件组图1-5 添加组1.4 在KEIL工程中添加文件

在工程中添加好组之后,需要把本地工程中新建的文件添加到工程,具体为把readme.txt文件添加到Doc组,将app.c文件添加到User组,与操作系统相关的文件我们还没有编写,那么操作系统相关的组就暂时为空,如图1-6所示。图1-6 往组里面添加好的文件

将本地工程中的文件添加到工程组的具体的方法为双击相应的组,在弹出的对话框中找到要添加的文件,默认的文件类型是C文件,如果要添加的是文本或者汇编文件,那么此时将看不到,这时就需要把文件类型设置为All files,最后单击Add按钮即可,如图1-7所示。图1-7 向组中添加文件

下面编写main()函数。

一个工程如果没有main()函数是无法编译成功的,因为系统在开始执行时先执行启动文件中的复位程序,复位程序中会调用C库函数__main,__main的作用是初始化系统变量,如全局变量、只读变量可读可写变量等。__main最后会调用__rtentry,再由__rtentry调用main()函数,从而由汇编进入C的世界,这里面的main()函数就需要我们手动编写,如果没有编写main()函数,就会出现main()函数未定义的错误,如图1-8所示。图1-8 未定义main()函数的错误

我们将main()函数写在app.c文件中,因为是刚刚新建的工程,所以main()函数暂时为空,具体参见代码清单1-1。代码清单1-1 main()函数1 int main(void)2 {3 for (;;) {4 /* 无操作 */5 }6 }1.5 调试配置1.5.1 设置软件仿真

最后,我们再配置一下调试相关的配置参数。为了方便,全部代码都用软件仿真,既不需要开发板,也不需要仿真器,只需要一个KEIL软件即可,有关软件仿真的配置具体如图1-9所示。图1-9 软件仿真配置1.5.2 修改时钟大小

在时钟相关文件system_ARMCM3.c的开头,有一段代码定义了系统时钟的大小为25MHz,具体参见代码清单1-2。在软件仿真时,为确保准确性,代码中的系统时钟与软件仿真的时钟必须一致,所以Options for Target对话框中Target的时钟频率应该由默认的12MHz改成25MHz,如图1-10所示。图1-10 软件仿真时钟配置代码清单1-2 时钟相关宏定义1 #define __HSI ( 8000000UL)2 #define __XTAL ( 5000000UL)3 4 #define __SYSTEM_CLOCK (5*__XTAL)   /* 5×5000000 = 25M */1.5.3 添加头文件路径

在C/C++选项卡中指定工程头文件的路径,否则编译会出错,头文件路径的具体设置方法如图1-11所示。图1-11 指定头文件的路径

至此,一个完整的基于Cortex-M内核的软件仿真工程建立完毕。第2章 裸机系统与多任务系统

在真正开始动手写RTOS之前,先来讲解一下单片机编程中的裸机系统和多任务系统的区别。2.1 裸机系统

裸机系统通常分成轮询系统和前后台系统,有关这两者的具体实现方式参见下面的讲解。2.1.1 轮询系统

轮询系统即在裸机编程时,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地处理各种事件,大概的伪代码参见代码清单2-1。轮询系统是一种非常简单的软件结构,通常只适用于仅需要顺序执行代码且不需要外部事件来驱动就能完成的事情。在代码清单2-1中,如果只是实现LED翻转、串口输出、液晶显示等操作,那么使用轮询系统将会非常完美。但是,如果加入了按键操作等需要检测外部信号的事件,例如用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。假设DoSomething3是按键扫描,当外部按键被按下,相当于一个警报,这个时候,需要立刻响应并做紧急处理,而这时程序刚好执行到DoSomething1,如果DoSomething1需要执行的时间比较久,久到按键释放之后还没有执行完毕,那么当执行到DoSomething3时就会丢失一次事件。由此可见,轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性就会降低。代码清单2-1 轮询系统伪代码1 int main(void) 2 { 3 /* 硬件相关初始化 */ 4 HardWareInit(); 5 6 /* 无限循环 */ 7 for (;;) { 8 /* 处理事件1 */ 9 DoSomethin1();10 11 /* 处理事件2 */12 DoSomething2();13 14 /* 处理事件3 */15 DoSomething3();16 }17 }2.1.2 前后台系统

相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里称为前台,main()函数中的无限循环称为后台,大概的伪代码参见代码清单2-2。代码清单2-2 前后台系统伪代码1 int flag1 = 0; 2 int flag2 = 0; 3 int flag3 = 0; 4 5 int main(void) 6 { 7 /* 硬件相关初始化 */ 8 HardWareInit(); 9 10 /* 无限循环 */11 for (;;) {12 if (flag1) {13 /* 处理事件1 */14 DoSomething1();15 }16 17 if (flag2) {18 /* 处理事件2 */19 DoSomething2();20 }21 22 if (flag3) {23 /* 处理事件3 */24 DoSomething3();25 }26 }27 }28 29 void ISR1(void)30 {31 /* 置位标志位 */32 flag1 = 1;33 /* 如果事件处理时间很短,则在中断里面处理34 * 如果事件处理时间比较长,则回到后台处理 */35 DoSomething1();36 }37 38 void ISR2(void)39 {40 /* 置位标志位 */41 flag2 = 1;42 43 /* 如果事件处理时间很短,则在中断里面处理44 * 如果事件处理时间比较长,则回到后台处理 */45 DoSomething2();46 }47 48 void ISR3(void)49 {50 /* 置位标志位 */51 flag3 = 1;52 53 /* 如果事件处理时间很短,则在中断里面处理54 * 如果事件处理时间比较长,则回到后台处理 */55 DoSomething3();56 }

在顺序执行后台程序时,如果出现中断,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序中标记事件。如果事件要处理的事情很简短,则可在中断服务程序中处理;如果事件要处理的事情比较多,则返回后台程序中处理。虽然事件的响应和处理分开了,但是事件的处理还是在后台顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大提高程序的实时响应能力。在大多数中小型项目中,前后台系统运用得好,堪称有操作系统的效果。2.2 多任务系统

相比前后台系统,多任务系统的事件响应也是在中断中完成的,但是事件的处理是在任务中完成的。在多任务系统中,任务与中断一样,也具有优先级,优先级高的任务会优先执行。当一个紧急事件在中断中被标记之后,如果事件对应的任务的优先级足够高,就会立刻得到响应。相比前后台系统,多任务系统的实时性又提高了。多任务系统的伪代码参见代码清单2-3。代码清单2-3 多任务系统伪代码1 int flag1 = 0; 2 int flag2 = 0; 3 int flag3 = 0; 4 5 int main(void) 6 { 7 /* 硬件相关初始化 */ 8 HardWareInit(); 9 10 /* 操作系统初始化 */11 RTOSInit();12 13 /* 操作系统启动,开始多任务调度,不再返回 */14 RTOSStart();15 }16 17 void ISR1(void)18 {19 /* 置位标志位 */20 flag1 = 1;21 }22 23 void ISR2(void)24 {25 /* 置位标志位 */26 flag2 = 2;27 }28 29 void ISR3(void)30 {31 /* 置位标志位 */32 flag3 = 1;33 }34 35 void DoSomething1(void)36 {37 /* 无限循环,不能返回 */38 for (;;) {39 /* 任务实体 */40 if (flag1) {41 42 }43 }44 }45 46 void DoSomething2(void)47 {48 /* 无限循环,不能返回 */49 for (;;) {50 /* 任务实体 */51 if (flag2) {52 53 }54 }55 }56 57 void DoSomething3(void)58 {59 /* 无限循环,不能返回 */60 for (;;) {61 /* 任务实体 */62 if (flag3) {63 64 }65 }66 }

相比前后台系统中后台顺序执行的程序主体,在多任务系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的、无限循环且不能返回的小程序,这个小程序称为任务。每个任务都是独立的、互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,我们在编程时不需要精心地设计程序的执行流,也不用担心每个功能模块之间是否存在干扰。加入了操作系统,我们的编程反而变得简单了。整个系统的额外开销仅为操作系统占据的少量FLASH和RAM。如今,单片机的FLASH和RAM越来越大,完全足以抵消RTOS的开销。

无论是裸机系统中的轮询系统、前后台系统还是多任务系统,我们不能简单地说孰优孰劣,它们是不同时代的产物,在各自的领域都有相当大的应用价值,只有合适的才是最好的。有关这三者的软件模型区别如表2-1所示。表2-1 轮询、前后台和多任务系统软件模型的区别第3章 任务的定义与任务切换

本章我们真正开始从0到1写RTOS。必须学会创建任务,并重点掌握任务是如何切换的。因为任务的切换是由汇编代码来完成的,所以代码看起来比较难懂,但是我们会尽力把代码讲得透彻。如果不能掌握本章内容,那么后面的内容根本无从下手。

在本章中,我们会创建两个任务,并让这两个任务不断地切换,任务的主体都是让一个变量按照一定的频率翻转,通过KEIL的软件仿真功能,在逻辑分析仪中观察变量的波形变化,最终的波形图如图3-1所示。图3-1 任务轮流切换波形图

其实,图3-1所示的波形图并不是真正的多任务系统中任务切换的效果图,这个效果其实可以完全由裸机代码实现,具体参见代码清单3-1。代码清单3-1 裸机系统中两个变量轮流翻转1 /* flag 必须定义成全局变量才能添加到逻辑分析仪中观察波形 2 ** 在逻辑分析仪中要设置为位(Bit)模式才能看到波形,不能使用默认的模拟量 3 */ 4 uint32_t flag1; 5 uint32_t flag2; 6 7 8 /* 软件延时,不必纠结具体的时间 */ 9 void delay( uint32_t count )10 {11 for (; count!=0; count--);12 }13 14 int main(void)15 {16 /* 无限循环,顺序执行 */17 for (;;) {18 flag1 = 1;19 delay( 100 );20 flag1 = 0;21 delay( 100 );22 23 flag2 = 1;24 delay( 100 );25 flag2 = 0;26 delay( 100 );27 }28 }

在多任务系统中,两个任务不断切换的效果图应该如图3-2所示,即两个变量的波形是完全一样的,就好像CPU在同时做两件事,这才是多任务的意义。虽然两者的波形图一样,但是代码的实现方式是完全不同的,由原来的顺序执行变成了任务的主动切换,这是根本区别。本章只是开始,我们先掌握好任务是如何切换的,在后面章节中,会陆续完善功能代码,加入系统调度,实现真正的多任务。图3-2 多任务系统中的任务切换波形图3.1 多任务系统中任务的概念

在裸机系统中,系统的主体就是main()函数中顺序执行的无限循环,在这个无限循环中,CPU按照顺序完成各种操作。在多任务系统中,根据功能不同,可以把整个系统分割成一个个独立的且无法返回的函数,这种函数称为任务,也有人称之为线程。任务的大概形式具体参见代码清单3-2。代码清单3-2 多任务系统中任务的形式1 void Task (void *parg) 2 { 3 /* 任务主体,无限循环且不能返回 */ 4 for (;;) { 5 /* 任务主体代码 */ 6 } 7 }3.2 创建任务3.2.1 定义任务栈

先回想一下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生,那么系统在运行时,全局变量放在哪里?子函数调用时,局部变量放在哪里?中断发生时,函数返回地址放在哪里?如果只是单纯的裸机编程,可以不考虑上述问题,但是如果要写一个RTOS,就必须明确这些参数是如何存储的。在裸机系统中,它们统统放在栈中。栈是单片机RAM中一段连续的内存空间,其大小由启动文件中的代码配置,具体参见代码清单3-3,最后由C库函数__main进行初始化。代码清单3-3 裸机系统中的栈分配1 Stack_Size EQU 0x00000400 2 3 AREA STACK, NOINIT, READWRITE, ALIGN=3 4 Stack_Mem SPACE Stack_Size 5 __initial_sp

但是,在多任务系统中,每个任务都是独立的、互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组。这些任务栈也存在于RAM中,能够使用的最大的栈尺寸也是由代码清单3-3中的Stack_Size决定的。只是多任务系统中任务的栈就是在一个统一的栈空间里面分配好一个个独立的“房间”,每个任务只能使用各自的房间,而需要在裸机系统中使用栈时,则可以天马行空,在栈里寻找任意空闲空间加以使用。

本章我们要实现两个变量按照一定的频率轮流翻转,需要用两个任务来实现,那么就需要定义两个任务栈,具体参见代码清单3-4。在多任务系统中,有多少个任务就需要定义多少个任务栈。代码清单3-4 定义任务栈1 #define TASK1_STK_SIZE 128 (1) 2 #define TASK2_STK_SIZE 128 3 4 static CPU_STK Task1Stk[TASK1_STK_SIZE];(2) 5 static CPU_STK Task2Stk[TASK2_STK_SIZE];

代码清单3-4(1):任务栈的大小由宏定义控制,在μC/OS-III中,空闲任务的栈最小应该大于128,这里的任务栈也暂且配置为128。

代码清单3-4(2):任务栈其实就是一个预先定义好的全局数据,此处数据类型为CPU_STK。在μC/OS-III中,凡是涉及数据类型的地方,μC/OS-III都会将标准的C数据类型用typedef重新设置一个类型名,命名方式则采用见名知义的方式且使用大写字母。凡是与CPU类型相关的数据类型统一在cpu.h中定义,与操作系统相关的数据类型则在os_type.h中定义。CPU_STK就是与CPU相关的数据类型,具体参见代码清单3-5。首次使用cpu.h,需要自行在C-CPU文件夹中新建并添加到工程的C/CPU组中。代码清单3-5中除了CPU_STK外,其他数据类型重定义是本章后面内容中需要用到的,这里统一给出,后面将不再赘述。代码清单3-5 cpu.h中的数据类型1 #ifndef CPU_H 2 #define CPU_H 3 4 typedef unsigned short CPU_INT16U; 5 typedef unsigned int CPU_INT32U; 6 typedef unsigned char CPU_INT08U; 7 8 typedef CPU_INT32U CPU_ADDR; 9 10 /* 栈数据类型重定义 */11 typedef CPU_INT32U CPU_STK;12 typedef CPU_ADDR CPU_STK_SIZE;13 14 typedef volatile CPU_INT32U CPU_REG32;15 16 #endif/* CPU_H */3.2.2 定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。本章定义的两个任务具体参见代码清单3-6。代码清单3-6 任务函数1 /* flag 必须定义成全局变量,才能添加到逻辑分析仪中观察波形 2 ** 在逻辑分析仪中要设置为Bit模式才能看到波形,不能使用默认的模拟量 3 */ 4 uint32_t flag1;(1) 5 uint32_t flag2; 6 7 8 /* 任务1 */ 9 void Task1( void *p_arg )(2)10 {11 for ( ;; ) {12 flag1 = 1;13 delay( 100 );14 flag1 = 0;15 delay( 100 );16 }17 }18 19 /* 任务2 */20 void Task2( void *p_arg )(3)21 {22 for ( ;; ) {23 flag2 = 1;24 delay( 100 );25 flag2 = 0;26 delay( 100 );27 }28 }

代码清单3-6(1):如果要在KEIL逻辑分析仪中观察波形的变量,则需要将其定义成全局变量,且要以Bit模式观察,不能使用默认的模拟量。

代码清单3-6(2)(3):正如介绍的那样,任务是一个独立的、无限循环且不能返回的函数。3.2.3 定义任务控制块

在裸机系统中,程序的主体是CPU按照顺序执行的,而在多任务系统中,任务的执行是由系统调度的。系统为了顺利地调度任务,为每个任务都额外定义了一个任务控制块(Task Control Block,TCB),这个任务控制块相当于任务的身份证,里面存有任务的所有信息,比如任务栈、任务名称、任务形参等。有了TCB,以后系统对任务的全部操作都可以通过这个TCB来实现。TCB是一个新的数据类型,在os.h头文件中声明(第一次使用os.h时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),有关TCB具体的声明参见代码清单3-7,使用它可以为每个任务都定义一个TCB实体。代码清单3-7 任务控制块类型声明1 /* 任务控制块重定义 */ 2 typedef struct os_tcb OS_TCB;(1) 3 4 /* 任务控制块数据类型声明 */ 5 struct os_tcb {(2) 6 CPU_STK *StkPtr; 7 CPU_STK_SIZE StkSize; 8 };

代码清单3-7(1):在μC/OS-III中,所有的数据类型都会被重新设置一个名称且用大写字母表示。

代码清单3-7(2):目前TCB里面的成员还比较少,只有栈指针和栈大小。为了以后操作方便,我们把栈指针作为TCB的第一个成员。

此处,在app.c文件中为两个任务定义的TCB具体参见代码清单3-8。代码清单3-8 定义TCB1 static OS_TCB Task1TCB;2 static OS_TCB Task2TCB;3.2.4 实现任务创建函数

任务栈、任务的函数实体、任务的TCB最终需要联系起来才能由系统进行统一调度,这个联系的工作由任务创建函数OSTaskCreate()实现,该函数在os_task.c中定义(第一次使用os_task.c时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),所有与任务相关的函数都在这个文件中定义。OSTaskCreate()函数的实现具体参见代码清单3-9。代码清单3-9 OSTaskCreate()函数1 void OSTaskCreate (OS_TCB *p_tcb,(1) 2 OS_TASK_PTR p_task,(2) 3 void *p_arg,(3) 4 CPU_STK *p_stk_base, (4) 5 CPU_STK_SIZE stk_size, (5) 6 OS_ERR *p_err) (6) 7 { 8 CPU_STK *p_sp; 9 10 p_sp = OSTaskStkInit (p_task,(7)11 p_arg,12 p_stk_base,13 stk_size);14 p_tcb->StkPtr = p_sp;(8)15 p_tcb->StkSize = stk_size;(9)16 17 *p_err = OS_ERR_NONE;(10)18 }

代码清单3-9:OSTaskCreate()函数遵循μC/OS-III中的函数命名规则,以OS开头,表示这是一个外部函数,可以由用户调用;以OS_开头的函数则表示内部函数,只能在μC/OS-III内部使用。紧接着是文件名,表示该函数放在哪个文件中,最后是函数功能名称。

代码清单3-9(1):p_tcb是任务控制块指针。

代码清单3-9(2):p_task是任务名,类型为OS_TASK_PTR,原型声明在os.h文件中,具体参见代码清单3-10。代码清单3-10 OS_TASK_PTR原型声明1 typedef void (*OS_TASK_PTR)(void *p_arg);

代码清单3-9(3):p_arg是任务形参,用于传递任务参数。

代码清单3-9(4):p_stk_base用于指向任务栈的起始地址。

代码清单3-9(5):stk_size表示任务栈的大小。

代码清单3-9(6):p_err用于存储错误码。μC/OS-III中为函数的返回值预先定义了很多错误码,通过这些错误码可以知道函数出现错误的原因。为了方便,我们现在把μC/OS-III中所有的错误码都给出来。错误码是枚举类型的数据,在os.h中定义,具体参见代码清单3-11。代码清单3-11 错误码枚举定义1 typedefenum os_err { 2 OS_ERR_NONE = 0u, 3 4 OS_ERR_A = 10000u, 5 OS_ERR_ACCEPT_ISR = 10001u, 6 7 OS_ERR_B = 11000u, 8 9 OS_ERR_C = 12000u,10 OS_ERR_CREATE_ISR = 12001u,11 12 /* 限于篇幅,此处将中间部分删除,具体内容可查看本章配套的例程 */199 200 OS_ERR_X = 33000u,201 202 OS_ERR_Y = 34000u,203 OS_ERR_YIELD_ISR = 34001u,204 205 OS_ERR_Z = 35000u206 } OS_ERR;

代码清单3-9(7):OSTaskStkInit()是任务栈初始化函数。当任务第一次运行时,加载到CPU寄存器的参数就放在任务栈中,在任务创建时,预先初始化好栈。OSTaskStkInit()函数在os_cpu_c.c中定义(第一次使用os_cpu_c.c时需要自行在文件夹C-CPU中新建并添加到工程的C/CPU组),具体参见代码清单3-12。代码清单3-12 OSTaskStkInit()函数1 CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,(1) 2 void *p_arg,(2) 3 CPU_STK *p_stk_base,(3) 4 CPU_STK_SIZE stk_size)(4) 5 { 6 CPU_STK *p_stk; 7 8 p_stk = &p_stk_base[stk_size];(5) 9 /* 异常发生时自动保存的寄存器 */(6)10 *--p_stk = (CPU_STK)0x01000000u; /* xPSR的位24必须置1 */11 *--p_stk = (CPU_STK)p_task; /* R15(PC)任务的入口地址*/12 *--p_stk = (CPU_STK)0x14141414u; /* R14 (LR) */13 *--p_stk = (CPU_STK)0x12121212u; /* R12 */14 *--p_stk = (CPU_STK)0x03030303u; /* R3 */15 *--p_stk = (CPU_STK)0x02020202u; /* R2 */16 *--p_stk = (CPU_STK)0x01010101u; /* R1 */17 *--p_stk = (CPU_STK)p_arg; /* R0 : 任务形参 */18 /* 异常发生时需要手动保存的寄存器 */(7)19 *--p_stk = (CPU_STK)0x11111111u; /* R11 */20 *--p_stk = (CPU_STK)0x10101010u; /* R10 */21 *--p_stk = (CPU_STK)0x09090909u; /* R9 */22 *--p_stk = (CPU_STK)0x08080808u; /* R8 */23 *--p_stk = (CPU_STK)0x07070707u; /* R7 */24 *--p_stk = (CPU_STK)0x06060606u; /* R6 */25 *--p_stk = (CPU_STK)0x05050505u; /* R5 */26 *--p_stk = (CPU_STK)0x04040404u; /* R4 */27 28 return (p_stk);(8)29 }

代码清单3-12(1):p_task是任务名,表示任务的入口地址,在任务切换时,需要加载到R15,即PC寄存器,这样CPU就可以找到要运行的任务。

代码清单3-12(2):p_arg是任务的形参,用于传递参数,在任务切换时,需要加载到寄存器R0。R0寄存器通常用来传递参数。

代码清单3-12(3):p_stk_base表示任务栈的起始地址。

代码清单3-12(4):stk_size表示任务栈的大小,数据类型为CPU_STK_SIZE,在Cortex-M3内核的处理器中等于4字节,即一个字。

代码清单3-12(5):获取任务栈的栈顶地址,ARMCM3处理器的栈是由高地址向低地址生长的,所以在初始化栈之前,要获取栈顶地址,然后将栈地址逐一递减即可。

代码清单3-12(6):任务第一次运行时,加载到CPU寄存器的环境参数要预先初始化好。初始化的顺序固定,首先是异常发生时自动保存的8个寄存器,即xPSR、R15、R14、R12、R3、R2、R1和R0。其中xPSR寄存器的位24必须是1,R15 PC指针必须存储任务的入口地址,R0必须是任务形参。对于R14、R12、R3、R2和R1,为了调试方便,应填入与寄存器号相对应的十六进制数。

代码清单3-12(7):剩下的是8个需要手动加载到CPU寄存器的参数,为了调试方便,应填入与寄存器号相对应的十六进制数。

代码清单3-12(8):返回栈指针p_stk,这时p_stk指向剩余栈的栈顶。

代码清单3-9(8):将剩余栈的栈顶指针p_sp保存到TCB的第一个成员StkPtr中。

代码清单3-9(9):将任务栈的大小保存到TCB的成员StkSize中。

代码清单3-9(10):函数执行到这里表示没有错误,即OS_ERR_NONE。

任务创建好之后,需要把任务添加到就绪列表,表示任务已经就绪,系统随时可以调度。将任务添加到就绪列表的代码具体参见代码清单3-13。代码清单3-13 将任务添加到就绪列表1 /* 将任务添加到就绪列表 */2 OSRdyList[0].HeadPtr = &Task1TCB(1)3 OSRdyList[1].HeadPtr = &Task2TCB(2)

代码清单3-13(1)(2):把TCB指针放到OSRdyList数组中。OSRdyList是一个类型为OS_RDY_LIST的全局变量,在os.h中定义,具体参见代码清单3-14。代码清单3-14 全局变量OSRdyList定义(3) (2) (1)1 OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];

代码清单3-14(1):OS_CFG_PRIO_MAX是一个定义,表示这个系统支持多少个优先级(刚开始暂时不支持多个优先级,后面的章节中会支持),目前仅用来表示这个就绪列表可以存储多少个TCB指针。具体的宏在os_cfg.h中定义(第一次使用os_cfg.h时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),具体参见代码清单3-15。代码清单3-15 OS_CFG_PRIO_MAX宏定义1 #ifndef OS_CFG_H 2 #define OS_CFG_H 3 4 /* 支持最大的优先级 */ 5 #define OS_CFG_PRIO_MAX 32u 6 7 8 #endif/* OS_CFG_H */

代码清单3-14(2):OS_RDY_LIST是就绪列表的数据类型,在os.h中声明,具体参见代码清单3-16。代码清单3-16 OS_RDY_LIST数据类型声明1 typedefstruct os_rdy_list OS_RDY_LIST;(1) 2 3 struct os_rdy_list {(2) 4 OS_TCB *HeadPtr; 5 OS_TCB *TailPtr; 6 };

代码清单3-16(1):μC/OS-III中会为每个数据类型重新设置一个字母大写的名称。

代码清单3-16(2):OS_RDY_LIST中目前只有两个TCB类型的指针,一个是头指针,一个是尾指针。本章实验只用到头指针,用来指向任务的TCB。只有当后面讲到同一个优先级支持多个任务时才需要使用头尾指针来将TCB串成一个双向链表。

代码清单3-14(3):OS_EXT是一个在os.h中定义的宏,具体参见代码清单3-17。代码清单3-17 OS_EXT宏定义1 #ifdef OS_GLOBALS 2 #define OS_EXT 3 #else 4 #define OS_EXT extern 5 #endif

这段代码的意思是,如果没有定义OS_GLOBALS这个宏,那么OS_EXT就为空,否则为extern。

在μC/OS-III中,需要使用很多全局变量,这些全局变量都在os.h头文件中定义,但是os.h会被包含进很多文件中,那么编译时os.h中定义的全局变量就会出现重复定义的情况,而我们只想将os.h中的全局变量只定义一次,涉及包含os.h头文件时只是声明。有人提出可以加extern,那么该如何加?

通常采取的做法是在C文件中定义全局变量,然后在头文件中需要使用全局变量的位置添加extern声明,但是μC/OS-III中文件非常多,这种方法可行,但不现实,所以就有了在os.h头文件中定义全局变量,然后在os.h文件的开头加上代码清单3-17中宏定义的方法。但是这样还没有成功,μC/OS-III另外新建了一个os_var.c文件(第一次使用os_var.c时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),其中包含了os.h,且只在这个文件中定义OS_GLOBALS这个宏,具体参见代码清单3-18。代码清单3-18 os_var.c文件内容1 #define OS_GLOBALS2 3 #include "os.h"

经过这样的处理之后,在编译整个工程时,只有var.c中os.h的OS_EXT才会被替换为空,即变量的定义,其他包含os.h的文件因为没有定义OS_GLOBALS这个宏,所以OS_EXT会被替换成extern,即变成了变量的声明。这样就实现了在头文件中定义变量。

在μC/OS-III中,将任务添加到就绪列表其实是在OSTaskCreate()函数中完成的。每当任务创建好就把任务添加到就绪列表,表示任务已经就绪,只是目前这里的就绪列表的实现还比较简单,不支持优先级,也不支持双向链表,只是简单地将TCB放到就绪列表的数组中。第8章将专门讲解就绪列表,等完善就绪列表之后,再把这部分的操作放回OSTaskCreate()函数中。3.3 操作系统初始化

操作系统初始化一般是在硬件初始化完成之后进行的,主要是初始化μC/OS-III中定义的全局变量。操作系统初始化用OSInit()函数实现。OSInit()函数在文件os_core.c中定义(第一次使用os_core.c时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),具体实现参见代码清单3-19。代码清单3-19 OSInit()函数1 void OSInit (OS_ERR *p_err) 2 { 3 OSRunning = OS_STATE_OS_STOPPED;(1) 4 5 OSTCBCurPtr = (OS_TCB *)0;(2) 6 OSTCBHighRdyPtr = (OS_TCB *)0;(3) 7 8 OS_RdyListInit();(4) 9 10 *p_err = OS_ERR_NONE;(5)11 }

代码清单3-19(1):系统用一个全局变量OSRunning指示其运行状态,刚开始初始化系统时,默认为停止状态,即OS_STATE_OS_STOPPED。

代码清单3-19(2):全局变量OSTCBCurPtr是系统用于指向当前正在运行的任务的TCB指针,在任务切换时用得到。

代码清单3-19(3):全局变量OSTCBHighRdyPtr用于指向就绪列表中优先级最高的任务的TCB,在任务切换时用得到。本章暂时不支持优先级,则用于指向第一个运行的任务的TCB。

代码清单3-19(4):OS_RdyListInit()用于初始化全局变量OSRdyList[],即初始化就绪列表。OS_RdyListInit()在os_core.c文件中定义,具体实现参见代码清单3-20。代码清单3-20 OS_RdyListInit()函数1 void OS_RdyListInit(void) 2 { 3 OS_PRIO i; 4 OS_RDY_LIST *p_rdy_list; 5 6 for ( i=0u; iHeadPtr = (OS_TCB *)0; 9 p_rdy_list->TailPtr = (OS_TCB *)0;10 }11 }

代码清单3-19(5):代码运行到这里表示没有错误,即OS_ERR_NONE。

代码清单3-19中的全局变量OSTCBCurPtr和OSTCBHighRdyPtr均在os.h中定义,具体参见代码清单3-21。OS_STATE_OS_STOPPED这个表示系统运行状态的宏也在os.h中定义,具体参见代码清单3-22。代码清单3-21 OSInit()函数中出现的全局变量的定义1 OS_EXT OS_TCB *OSTCBCurPtr; 2 OS_EXT OS_TCB *OSTCBHighRdyPtr; 3 OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX]; 4 OS_EXT OS_STATE OSRunning;代码清单3-22 系统状态的宏定义1 #define OS_STATE_OS_STOPPED (OS_STATE)(0u) 2 #define OS_STATE_OS_RUNNING (OS_STATE)(1u)3.4 启动系统

任务创建好且系统初始化完毕之后,就可以启动系统了。系统启动函数OSStart()在os_core.c中定义,具体实现参见代码清单3-23。代码清单3-23 OSStart()函数1 void OSStart (OS_ERR *p_err) 2 { 3 if ( OSRunning == OS_STATE_OS_STOPPED ) {(1) 4 /* 手动配置任务1先运行 */ 5 OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;(2) 6 7 /* 启动任务切换,不会返回 */ 8 OSStartHighRdy();(3) 9 10 /* 不会运行到这里,如果运行到这里,则表示发生了错误 */11 *p_err = OS_ERR_FATAL_RETURN;12 } else {13 *p_err = OS_STATE_OS_RUNNING;14 }15 }

代码清单3-23(1):如果系统是第一次启动,则if为真,继续往下运行。

代码清单3-23(2):OSTCBHighRdyPtr指向第一个要运行的任务的TCB。因为暂时不支持优先级,所以系统启动时先手动指定第一个要运行的任务。

代码清单3-23(3):OSStartHighRdy()用于启动任务切换,即配置PendSV的优先级为最低,然后触发PendSV异常,在PendSV异常服务函数中进行任务切换。该函数不再返回,在文件os_cpu_a.s中定义(第一次使用os_cpu_a.s时需要自行在文件夹μC/OS-III\Ports中新建并添加到工程的μC/OS-III Ports组),用汇编语言编写,具体实现参见代码清单3-24。os_cpu_a.s文件中涉及的ARM汇编指令的用法如表3-1所示。表3-1 常用的ARM汇编指令

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载