type
slug
status
summary
icon
category
date
tags
password

μC/OS-II工程结构

为了移植μC/OS-II,首先了解的是一般嵌入式框架,才能对所移植的操作系统的工程结构进行详细设计。
notion image
可以看到嵌入式操作系统是介于用户程序和硬件间,操作系统对用户隐藏了硬件细节,用户只需要根据操作系统提供的接口便能对各种硬件资源进行访问。
notion image
上图是μC/OS—II内核架构,可以看见最上面的是”Application Software (Your Code!)”,这是我们自己写的程序代码,运行在μC/OS操作系统之上。下面分为几个部分:μC/OS-II(处理器无关代码)、uC/OS-II配置(应用特定)、uC/OS-II端口(处理器特定代码),最后是硬件部分,包括CPU和定时器。
首先,uC/OS-II(处理器无关代码)。这部分包含了操作系统的内核代码,比如任务管理、信号量、消息队列等功能。这些代码不依赖于具体处理器,可以在不同的硬件平台上运行。图中列出了很多文件,比如OS_CORE.C、OS_FLAG.C等,这些都是实现操作系统功能的模块。 接下来是uC/OS-II配置(应用特定)。这里是一些配置文件,比如OS_CFG.H和INCLUDES.H。这些文件允许我们根据具体的应用需求来配置操作系统,比如定义任务的数量、优先级等。这部分是针对具体应用的,会根据不同的项目需求进行调整。 然后是uC/OS-II端口(处理器特定代码)。这部分代码与具体处理器相关,比如OS_CPU.H、OS_CPU_A.ASM和OS_CPU_C.C。这些文件包含了与硬件相关的代码,比如中断处理、任务切换等。因为不同的处理器有不同的指令集和寄存器,所以这部分代码需要针对具体的硬件平台进行编写。
最后是硬件部分,包括CPU和定时器。操作系统运行在硬件之上,通过CPU执行指令,定时器则用于提供时钟中断,支持操作系统的多任务调度。
基于我们对μC/OS—II内核架构的理解,我们可以设计出移植μC/OS-II操作系统的工程结构:
notion image
  • 用户应用部分:包括我们自己实现的各种驱动程序和主函数。用户应用程序是最终运行在系统上的代码,比如main.c、gy86.c、usart.c等。应用程序更具操作系统提供的API请求操作系统的服务。
  • ucosii_source:包含操作系统的内核代码,与ARM处理器无关,包括任务、信号量、内存管理、队列等功能。一般不做修改。
  • ucosii_ports(移植层):ucosii_ports包含了与硬件相关的代码,比如os_cpu.c、os_cpu_a.s、os_cpu.h。这些代码负责将ucosii_core的通用代码适配到具体的硬件平台,比如STM32F401RE。是与ARM处理器直接相关的代码,也是本次在移植中需要重点修改的地方。这一部分主要是和一些处理器相关的函数/宏定义。
  • ucosii_cfg:操作系统参数配置文件,例如任务数量、堆栈大小、是否启用特定功能等。与ARM处理器无关。
  • BSP(Board Support Package版级支持包):启动文件stm32f401xx.s,这些代码负责初始化硬件,设置堆栈、中断向量表等。BSP和ucosii_ports之间应该有直接的联系,因为它们都需要访问硬件资源。
  • 硬件:最下层就是我们所使用的开发板STM32F401RE

μC/OS-II 移植

μC/OS-II操作系统的移植主要体现在os_cpu_a.s汇编文件的编写以及os_cpu.hos_cpu.c文件的修改上。

模块功能介绍

Source(核心,一般不作修改)
Source文件夹包含了 uC/OS II 内核的核心功能模块。这些模块提供了任务管理、时间管理、资源管理、任务通信和同步等核心功能,是 uC/OS II 内核 的基础。
  • os_core.c:负责操作系统内核的初始化和核心功能的实现,包括任务调度器的初始化和空闲任务的创建。
  • os_task.c:包含任务管理相关的函数,如任务的创建、删除和任务控制块(TCB)的操作。
  • os_flag.c:实现事件标志组功能,用于任务间的同步和通信。
  • os_mem.c:提供内存管理功能,包括内存块的分配和释放。
  • os_tmr.c:实现软件定时器功能,允许在指定时间后执行特定操作。
  • os_mutex.c:提供互斥信号量功能,用于保护共享资源,防止同时访问导致的数据不一致。
  • os_sem.c:uC/OS II中的信号量(Semaphore)功能在任务间的同步和互斥上起着重要作用。这个文件实现了信号量的接口函数
  • os_mbox.c: 邮箱(Mailbox)是uC/OS II用于任务间消息传递的机制之一。这个文件实现了邮箱功能的接口函数。
  • os_q.c:队列(Queue)也是任务间通信常用的方式之一,这个文件实现了队列的接口函数。
  • ucos_ii.c:这个源文件包含了uC/OS II内核的核心功能实现,如操作系统初始化、任务调度算法、时钟节拍处理、中断管理、任务创建与删除、事件标志组、信号量、互斥量、邮箱和队列等操作的代码。它是uC/OS II操作系统运行的基础,实现了多任务环境下的同步与通信机制。
  • ucos_ii.h:这个头文件包含了uC/OS II内核中使用的各种数据结构的定义,如任务、事件、链表、信号量等,以及函数声明。
PORT(移植层,与硬件平台相关,需要根据具体平台进行修改)
Ports 文件夹中的文件包含了针对不同处理器平台的移植代码。每个处理器 平台都有不同的硬件架构和操作系统接口,因此需要根据处理器的特点进行移 植,以确保 uC/OS II 内核可以在特定处理器上正确地运行。
  • os_cpu.h:定义与处理器架构相关的数据类型和宏,确保操作系统能够在特定的 CPU 上运行。
  • os_cpu_a.asm:包含与处理器架构相关的汇编代码,如上下文切换和中断处理等底层操作。
  • os_cpu_c.c:实现与处理器架构相关的 C 语言函数,如任务上下文初始化等。
CONFIG(配置,根据应用需求进行修改)
Cfg文件夹中的文件用于配置 uC/OS II 内核的各种功能和选项。
  • os_cfg.h:用于配置操作系统的功能和特性,如任务数量、堆栈大小、是否启用某些功能等。
  • app_cfg.h:包含应用程序所需的头文件,确保编译器能够找到所有必要的声明和定义。

重构μC/OS-II

新建工程

第一步:新建一个文件夹ucosii,并在文件夹下面再新增四个文件夹Source、Ports、Cfg,在Source文件夹中导入官网所给的文件。
第二步:打开keil 5,新建一个工程,选择芯片型号:STM32F401RETx。
第三步:右键单击 target 1-add group 新建文件夹,并重命名为 Source、Ports、 Cfg,右键单击文件夹Source,选择add existing files to group ‘Core’,添加文件。
第四步:添加头文件路径,点击魔术棒,在C/C++和Asm里的Include Path中写入这三个文件夹的路径。

开始debug

错误类型1:缺少文件或找不到文件

notion image
解决方法:手动添加相应的文件
这个报错信息提示我们缺少app_cfg.h文件,于是我们先建立一个空的app_cfg.h文件在Cfg文件夹中。
同理添加os_cfg.h、os_cpu.h

错误类型2:os_cpu.h文件中缺少相应的数据类型的定义和函数的声明

notion image
os_cpu_.h主要包括与编译器无关,但与移植平台有关的数据类型、堆栈的数据类型、进出临界区时开关中断的方式、堆栈增长的方向、任务切换时执行的代码。
这个报错提示我们INT8U这个变量没有被定义,于是我们在os_cpu.h文件中添加相应的定义。
由于signed int,unsigned int等原生类型时,其实际位数取决于处理器。比如在16位系统中int通常为16位,而在32/64位系统中,int通常为32位。这会导致移植时不同平台时产生数据溢出等情况。
μCOS/II的解决方案:自定义数据类型。通过typedef明确定义数据类型的位数,保证了跨平台的一致性。例如:INT32U:始终表示32位无符号整数(无论处理器是16位/32位/64位)
notion image
在 STM32F401(基于ARM Cortex-M4的32位微控制器)平台上,基于ARMv7-M架构,所以找到μC/OS官方开源代码下面的Ports/ARN-Cortex-M/ARMv7-M进行移植。为了方便使用堆栈,μC/OS定义了一个堆栈数据类型。在Cortex-M4中寄存器位32位,故定义堆栈的长度也为32位。(OS_STK)
Cortex-M4状态寄存器为32位,定义OS_CPU_SR为32为,在 μC/OS-II 的临界区保护(关中断/开中断)中,需通过此类变量保存中断状态。
 
notion image
这个警告提示我们OS_ENTER_CRITICAL还能输在os_core.c文件中被隐式声明,即它在被调用前没有显示声明,于是我们需要在os_cpu.h文件中声明这个函数。在os_cpu_a.s中定义这个函数。以实现汇编中断函数为例,见错误类型六。
(在官方文件的函数原型中有更加详细的函数声明,定义以及参数声明,这是我们在后续挑战点完成过程中应该修改的地方。在这里只为了通过编译,骗过编译器,所以我们采用了空实现的方式。)
 
临界区代码
临界区(Critical Section)是指在多线程或多进程环境中,访问共享资源(如变量、文件、设备等)的那段代码。这段代码必须保证在任何时刻只有一个线程或进程可以执行,以避免数据竞争和不一致的问题。 例如,如果有多个线程同时对一个共享变量进行读写操作,而这些操作没有被正确地同步,就可能导致数据错乱。因此,访问共享资源的代码区域被称为“临界区”。
为什么需要关闭中断:
关闭中断的意思是:在执行临界区代码的时候,不允许任何其他中断来打扰,这样就能保证当前的操作(临界区代码)能够完整、安全地执行,不会被其他事情打断。
BASEPRI(Base Priority Mask Register)是 ARM Cortex-M 微控制器中的一个寄存器,用于控制中断的优先级屏蔽。它的主要作用是通过设置一个优先级阈值,来决定哪些中断可以被屏蔽,从而保护临界区代码的执行。以下是 BASEPRI 的工作机制:
💡
1. BASEPRI 的基本概念
  • 寄存器位置BASEPRI 是一个 CPU 寄存器,不同于通用寄存器(如 R0 ~ R15)。它用于控制中断的优先级屏蔽。
  • 优先级屏蔽BASEPRI 的值决定了哪些中断可以被屏蔽。具体来说,它屏蔽所有优先级数值 大于等于 BASEPRI 值的中断。优先级数值越大,优先级越低。
2. BASEPRI 的工作机制
  • 设置 BASEPRI 的值
    • BASEPRI 被设置为某个值 n 时,所有优先级数值 大于等于 n 的中断都会被屏蔽。
    • 例如,如果 BASEPRI 被设置为 0x0F,那么所有优先级数值大于等于 0x0F 的中断都会被屏蔽。
  • 屏蔽和恢复中断
    • 在进入临界区之前,可以通过设置 BASEPRI 来屏蔽中断,防止低优先级的中断打断临界区代码的执行。
    • 临界区代码执行完毕后,需要恢复 BASEPRI 的值,以重新启用被屏蔽的中断。
💡
以下是关于这些函数的介绍:
#define OS_CRITICAL_METHOD 3u
ucosii提供了三种可供选择的进出临界区的方法:第一种是进入关键区时关中断,退出关键区时开中断,而不管进入时的中断是否是关闭状态;第二种时进入关键区时保存当前中断再关闭,退出关键区时恢复进入关键区前的中断状态;第三种方法的思想与第二种一致,只是需要在使用进出关键区的函数中定义一个OS_CPU_SR类型的变量cpu_sr,用于保存进入临界区前的中断状态。我们这里选择第三种方法。【更详细版:第三种方法(即使用 BASEPRI 寄存器)BASEPRI 寄存器:ARM Cortex-M 系列处理器提供了一个名为 BASEPRI 的寄存器,用于设置中断优先级屏蔽。通过设置 BASEPRI 寄存器,可以屏蔽所有优先级低于某个值的中断。使用 BASEPRI 寄存器可以灵活地控制中断的屏蔽级别,而不是简单地关闭所有中断。这在多任务操作系统中尤为重要,因为不同的任务可能需要不同的中断优先级。】
#define OS_TASK_SW() OSCtxSw()
这一行代码定义了任务切换函数。使用宏定义函数的优点是,可以在不同的CPU架构之间共享代码,同时确保在必要时可以针对特定的架构进行优化。
#define OS_STK_GROWTH 1u
这一行代码定义了堆栈增长的方式,1为从高到底到低增长,0为从低到高增长。基于Cortex-M 内核的堆栈是 向下增长的满栈,即堆栈从高地址向低地址增长
void OS_CPU_IRQ_ISR(void);
这一行代码声明了IRQ中断的入口函数。
void OSCtxSw(void);
这一行代码声明了任务切换函数。
void OSIntCtxSw(void);
这一行代码声明了中断执行时的任务切换函数。
void OSStartHighRdy(void);
这一行代码声明启动当前最高优先级的任务,由OSStart()调用。
 
 
下面是os.cpu.h的代码:
 

错误类型3:os_cfg.h中缺少相应的定义。

os_cfg.h是uC/OS II的配置文件,通过配置该文件,可以表示μC/OS哪些功能可用,哪些功能不可用。
notion image
notion image
解决方法:在os_cfg.h中加上相关的定义。
tips:在后续的学习中,可以把相关的功能给禁用,注意部分参数的取值范围,若设定的参数不在范围内则会报错。
notion image
#define OS_TMR_EN 0u /* Enable (1) or Disable (0) code generation for TIMERS */
OS_TMR_EN 设置为 0u 会使 uC/OS-II 禁用定时器功能的代码生成。STM32F401具有多个定时器Timer,可以满足定时中断、PWM输出、输入捕获、输出比较等功能。也可以使用Systick(SysTick 是 Cortex-M 内核中的一个系统定时器,属于内核的一部分)
#define OS_DEBUG_EN 0u /* Enable(1) debug variables */

错误类型4:缺少启动文件导致链接失败

notion image
notion image
  • Reset_Handler 是程序的入口点,通常在启动文件(如 startup_stm32f4xx.sstartup_xxx.s)中定义。
  • __Vectors 是中断向量表的定义,通常也在启动文件中定义。
解决方法:新建一个Start文件夹,在Start文件夹中导入启动文件startup_stm32f40_41xxx.s

错误类型5:链接时重复定义

notion image
ucos_ii.o 和 os_core.o:这两个是编译生成的对象文件(.o 文件)。,那我们以这个报错为例: .\Objects\projects.axf: Error: L6200E: Symbol OSEventNameGet multiply defined (by ucos_ii.o and os_core.o).所以我们使用ctrl+f对这个变量进行寻找:
notion image
可以看见:OSEventNameGet在整个工程中出现了两次,分别在ucos_ii.h(797)和os_core.c(117)跳转,可见这两处的函数定义是一样的。
链接使得分离编译(separate compilation)成为可能,我们不用将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解成为更小、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单的重新编译它,并重新链接应用,而不必重新编译其他文件。
解决方案:
选择性编译,将其他.c文件不包含在最终的项目中,只将ucos_ii.c\ucos_iih,进行编译
新建文件夹:Core,将ucos_ii.c\ucos_iih,加入Core,并且删除Source里面的这两个文件,并且将Core路径加入c/c++ 以及Asm path里面。
 

错误类型6:os_cpu_.c中hook函数

找不到指定函数导致链接错误,因为函数的声明和定义的语法规则区别很大,编译器可以很轻松地区分什么是声明,什么是定义。如果分别编译单个文件时,只有声明没有定义,编译器暂时不会报错,会尝试在链接时去其他文件中找相应的函数定义,但是如果链接时还没有找到函数体,编译器就会报错。
所以我们需要新建一个os_cpu_c.c文件和一个os_cpu_a.s文件,在其中补充缺少的函数实现,目前我们不需要补全具体的函数实现细节,只需要写一个空定义来骗过编译器。
在os_cpu_c.c文件中,我们定义空的系统初始化函数、钩子函数、cpu异常栈基地址和这是内核可用的优先级边界。
钩子函数:钩子函数为用户提供了一个扩展点,使得用户无需修改操作系统的内核代码而实现具体应用的要求。
这几个钩子函数的具体作用:
堆栈初始化函数:
cpu异常栈:CPU异常栈 是用于处理 CPU 异常(如中断、异常等)时的栈空间。当 CPU 发生异常时,会自动将一些寄存器的值压入异常栈中,以便在异常处理完成后能够正确恢复现场。
在后续的操作中我们会定义异常栈的大小,异常栈的基地址。
内核优先级边界:
内核优先级边界 是指在多任务操作系统中,内核任务和用户任务之间的优先级边界。内核任务通常具有较高的优先级,以确保系统的稳定性和实时性。用户任务的优先级通常较低,以避免干扰内核任务的执行。
在 STM32F401 微控制器中,内核优先级边界可以通过配置中断优先级寄存器来实现。例如,可以设置内核任务的优先级为 0,用户任务的优先级为 1 或更高。这样可以确保内核任务在发生中断时能够优先执行,而用户任务则在内核任务完成后执行。
 

错误类型7:os_cpu_a.s汇编语言实现函数

os_cpu_a.s文件中,实现的是与处理器相关的函数,这一部分的函数涉及CPU 特权寄存器的访问、寄存器状态的保存与恢复,以及任务上下文切换。这些操作需要对CPU 体系结构和指令集进行精确控制,而高级语言(如 C)无法不提供直接读写特定硬件寄存器(如中断寄存器PRIMASK、堆栈PSP,任务控制块TCB)的机制,也难以确保任务切换的精确性和高效性。
因此,使用汇编语言,能够直接访问和精确管理寄存器避免编译器优化带来的不可预测行为,确保RTOS的任务调度和中断管理能够高效且正确地执行,且具有移植性强的特性。
💡
1.AREA |.text|, CODE, READONLY, ALIGN=2
  • AREA:这是一个伪指令,用于定义一个新的代码段或数据段。它告诉汇编器一个新的区域(AREA)开始。
  • |.text|:这是代码段的名称。| .text | 是一个通用的名称,用来表示存放程序可执行代码的部分。
  • CODE:这是一个属性,表示该区域将包含代码(指令),而不是数据。
  • READONLY:表示该代码段中的内容是只读的,在程序运行过程中不能被修改。
  • ALIGN=2:这是一个对齐要求,表示该代码段的起始地址必须 2 的幂次对齐(如 2 字节对齐,即地址是 2 的倍数)。
2. THUMB
  • THUMB 是一个指令集,通常用于 ARM 架构的处理器。它是一个压缩的指令集,使用更少的字节来表示指令,与标准的 ARM 指令集相比,可以节省代码空间,但可能会以性能为代价。
3. REQUIRE8 和 PRESERVE8
  • REQUIRE8:这可能是一个指令或编译器选项,表示程序需要 8 字节对齐的指针操作。它确保代码中的某些操作在 8 字节对齐的内存地址上执行,这在某些场景下可以提高性能或满足硬件要求。
  • PRESERVE8:这可能是一个指令或编译器选项,用于在代码优化过程中保持 8 字节对齐的规则。例如,当编译器进行代码优化时,它会确保指针仍然保持 8 字节对齐,以避免潜在的硬件或兼容性问题。
MRS 指令将系统寄存器 PRIMASK 的值读取到通用寄存器 R0
SR PRIMASK, R0
  • MSR 指令将通用寄存器 R0 的值写回系统寄存器 PRIMASK
💡
OS_CPU_SR_Save 和 OS_CPU_SR_Restore
作用
  • OS_CPU_SR_Save:保存当前的中断状态,并关闭中断。
  • OS_CPU_SR_Restore:恢复之前的中断状态。
OSCtxSw 和 OSIntCtxSw
作用
  • OSCtxSw:任务级上下文切换函数,用于在任务之间进行上下文切换。
  • OSIntCtxSw:中断级上下文切换函数,用于在中断服务例程(ISR)中进行上下文切换。
OSStartHighRdy
作用
  • OSStartHighRdy:启动最高优先级任务,将控制权交给就绪状态中优先级最高的任务。
PendSV_Handler
作用
  • PendSV_Handler:处理 PendSV 中断,用于任务切换和调度。
 

错误类型8:缺少main函数

.\Objects\test.axf: Error: L6218E: Undefined symbol main (referred from __rtentry2.o).
我们新建一个User文件夹,里面新建一个main.c文件来存放main函数入口。

创建任务的流程

1.调用OSInit() :初始化μC/OS-II的内核,设置系统就绪状态,为任务调度做准备,无返回值。
2.配置任务栈:为每个任务分配一个栈空间,用于存储任务运行时的上下文信息。
OS_STK my_task_0[MY_TASK_SIZE_0];
其中MY_TASK_SIZE_0是一个宏定义,表示任务栈的大小
3.OSTaskCreateExt():任务创建的核心函数,用于创建一个任务并设置其属性。
函数原型
notion image
以任务0为例说明各参数含义
(void)OSTaskCreateExt(my_task_0_t_, (void *)0, &my_task_0[MY_TASK_SIZE_0 - 1u], 12,1,my_task_0,sizeof(my_task_0),NULL,0,"LED_OFF");
  • task任务函数:表示指向任务函数my_task_0_t_的指针。
  • p_arg任务参数:传递给任务函数的参数。任务开始执行时,这个参数将被传递给任务函数。设置为NULL,表示任务不需要额外参数。
  • ptos栈顶地址&my_task_0[MY_TASK_SIZE_0 - 1u],栈顶指针,指向任务栈的顶部。在 μC/OS-II 中,栈是向下增长的,所以这个指针实际上是栈的最高地址。
  • prio优先级12,表示任务优先级较低(数字越小,优先级越高)。
  • id选项1,任务的唯一标识符,通常用于调试和跟踪。
  • pbos栈底地址my_task_0,栈数组的起始地址,栈底指针,指向任务栈的底部,存放栈数组的起始地址。
  • stk_size栈大小sizeof(my_task_0),栈数组的大小。
  • pext扩展数据地址NULL,无扩展数据。该参数表示指向任务扩展数据的指针。任务扩展数据用于存储任务特定的信息,如事件标志组、互斥信号量等。
  • opt扩展数据大小0,表示无任务特定选项。这个参数是任务栈初始化选项。这个参数通常用于指定栈的对齐方式或其他与栈初始化相关的选项。在大多数情况下,这个值设置为 0。
  • name任务名称"LED_OFF",表示任务的功能是关闭 LED。
4.OSStart():启动操作系统,开始调度任务。
以后可能需要用到的其他函数
在μC/OS-II实时操作系统中,信号量(Semaphore)和内存管理是多任务同步和数据交换的重要机制。
OSSemCreate():此函数用于创建一个信号量。
notion image
参数
信号量是一个非负整数计数器,用于表示可用资源的数量。它通常用于以下两种场景:
  1. 互斥访问(Mutex):用于确保同一时间只有一个线程或进程可以访问某个共享资源。
  1. 同步访问(Counting Semaphore):用于控制多个线程或进程对多个相同资源的访问。
cnt:信号量的初始计数。这个值决定了信号量的初始状态。
1 表示信号量一开始是“可用”的,任何任务都可以立即获得这个信号量。
0 表示信号量一开始是“不可用”的,需要其他任务释放信号量后才能被获取。

验证工程搭建

Idea1:调用函数

eg:OSTimeDly(1000 * 2);
预期结果:编译通过且不报错,说明工程搭建成功。

Idea2:点亮小灯

由于任务创建点亮小灯需要例如创建tcb,堆栈大小优先级,队列,延时等知识,在后续的学习中我们才会学到。但我们仍想通过点亮小灯来验证工程搭建成功。
因此我们的思路是:
写一个led.c,通过上面我们提到的钩子函数调用led.c,再在main.c里面调用钩子函数,从而达到通过点亮小灯验证工程搭建是否成功。由于时间关系,我们直接来看我们写好的点灯程序。
这里我们需要注意的是我们需要注释掉我们自己在cpu_czh中写的空的SystemInt()函数,因为在f401已经含有初始化系统的函数。
预期结果:编译通过且不报错,板载小灯L2闪烁,说明工程搭建成功。
 
1-4-1 USART串口协议2-4 启动ARM STM32
Loading...
🐟🐟
🐟🐟
在坚冰还盖着北海的时候,我看到了怒放的梅花
最新发布
2-1 用Keil实现μC/OS-II工程搭建
2025-5-10
Chapter1:Introduction
2025-5-7
2-4 启动ARM STM32
2025-5-6
1-4-1 USART串口协议
2025-5-6
1-1-2 GPIO小记
2025-5-6
1-1-1 用Keil点亮一盏小灯
2025-5-6
公告
🎉NotionNext 3.15已上线🎉
-- 感谢您的支持 ---
👏欢迎更新体验👏