type
status
date
slug
summary
tags
category
icon
password

LIAO Yong (Alan)
UESTC Real-Time System Lab

Table of Contents
  • Introduciton
  • Kernel of ERTOS
  • Hardware System
  • Software System
  • 附录
    • BootLoader.s
    • 24年半期试题
    • 24年期末试题回忆版

本文是基于STM32嵌入式开发平台,对四轴飞行器控制系统底层运行平台搭建及设计的描述。主要意义是为了合理运用理论和专业知识解决工程设计中的问题,掌握工程设计的基本方法和步骤,为培养优秀的工程师奠定基础。本文主要介绍了嵌入式操作系统的移植,在上学期的基础上将μC/OS-Ⅱ操作系统移植到STM32开发板上,主要工作是需要针对该开发板,首先重构在uc/os II下的新工程,然后完成关于os事件驱动机制和时间驱动机制的设计和实现,包括任务堆栈初始化、上下文切换机制、Systick定时器的配置、时钟中断服务程序的编写、OSStartHighRdy函数的实现以及进出临界区的保护等等,成功实现uc/os II在STM32上的移植;最后,我们对上学期的实现的功能进行了划分和整合,抽离并构建了功能相对独立又互相联系的几个核心任务,并应用System view对移植好的系统进行任务级的调试,通过可视化界面,确认了系统运行的稳定性,进一步理解了os层面的任务并行。
关键词:四轴飞行器,μC/OS-Ⅱ,事件驱动机制,时间驱动机制,System view

第一章 需求建模与复杂工程问题归纳

软件工程实践是确保软件项目成功的关键,它涵盖了从项目启动到最终交付的整个生命周期。这些实践包括了原理、概念、方法和工具,它们共同构成了软件工程实施的基础。在本章中,我们将深入探讨我们团队在四轴项目立项之初所进行的一系列关键活动,包括开发规划、需求分析的深入探讨、复杂工程的问题归纳,以及复杂问题的归纳和解决策略。

1.1软件过程

在本挑战项目中,我们采用了一个详细和结构化的软件过程,以确保μC/OS-II操作系统在STM32开发板上的成功移植和四轴飞行器控制系统的稳定运行。软件过程的设计旨在确保每一个开发阶段都经过充分的规划、实施、测试和评估,以适应复杂工程问题的需求和动态变化。以下是过程的关键步骤:
  1. 需求收集与分析:与组员和老师交流探讨,收集具体的系统需求,包括功能性、性能和安全性需求。
  1. 系统设计:根据收集到的需求制定系统架构和模块设计,定义系统的高层结构和各个模块之间的交互,例如μC/OS Ⅱ重构、MCU启动流程、OS任务调度、事件驱动机制、时间驱动机制、移植操作系统设计等。
  1. 实现与集成:小组成员根据需求和设计编写代码实现具体的功能,每完成一个模块的开发,都会进行详尽的模块测试,确保模块按预期工作。我们的开发
notion image
图1-1 螺旋模型
按照如上图1-1所示的螺旋模型稳步推进,每当四轴的开发进入到一个完整的新阶段时,我们就会使用Git将Develop分支Push至main分支。
  1. 验证与测试:系统开发完成后,进行全面的测试,包括单元测试、集成测试和系统测试,以验证系统满足以上的需求。
  1. 部署与维护:在确保系统稳定运行后,进行部署。系统部署后,还需要进行持续的维护和升级,以应对新的需求和潜在的问题

1.2需求分析

  1. 实时性:控制系统必须能在毫秒级别响应飞行控制命令,确保飞行安全和稳定。
  1. 稳定性:系统需要在不同工作条件下保持稳定,避免异常情况的发生。
  1. 硬件冗余:关键组件如传感器和通信设备应考虑冗余配置,提高系统容错能力。
  1. 系统维护:系统设计应便于日常维护和故障排查,包括软件更新和硬件更换。
  1. 系统扩展性:设计应考虑未来功能扩展的可能性,如改进的数据处理算法。

1.3复杂工程问题归纳

经过了一个学期的努力和学习,我们对四轴飞行器的探索仅仅是开始的阶段。在这期间,我们通过实际的设计和实现,对飞行器的基础工作原理和操作系统有了初步的了解和掌握。然而,真正深入理解并完善一个高效、稳定的四轴飞行器控制系统,仍然需要我们在未来的学习和实践中不断探索和突破。在本学期的挑战中,我们遇到了以下的几个问题:
  • 用keil重构μC/OS Ⅱ新工程
  • ARM STM-32事件驱动机制
  • ARM STM-32时间驱动机制
  • 启动ARM STM-32
  • 任务上下文切换
  • μC/OS Ⅱ在STM-32上的移植
  • 应用子程序系统设计/System view与任务级调试
  • 系统工程重构与优化
  • 内核资源互斥访问机制
  • 用MakeFile构建自己的工程
μC/OS-II操作系统原本设计用于多种微处理器和微控制器,但每种硬件的特性和限制不同。在STM32开发板上移植时,需要确保操作系统能够充分利用STM32的硬件特性,如中断管理和时钟配置等,这需要深入了解STM32硬件的细节。STM32的硬件驱动程序,如定时器、中断控制器等,必须与μC/OS-II紧密集成且优化,以保证系统的响应速度和稳定性。这通常涉及底层编程和对硬件寄存器的精确操作,有很大的复杂性和出错可能。
对于os系统和硬件中各个参数的设置,比如tick的大小、任务和中断优先级的设置等,都需要我们从整个系统层面出发,去考虑整个架构运行所需要的时间、空间和效率,这需要我们有很强的系统性思维,能够全面的考虑各个影响因素,做出最综合的判断。 对于工程的重构,我们要学会MakeFile等相关知识,对在Linux下工程的搭建和升级要有很深刻的理解。同时,我们要掌握和理解uc/os II和STM32开发板的各项性能,以保证正确、稳定地实现操作系统在开发板上的移植操作,这对我们的工程能力是一个很大的考验。
针对系统级的调试,特别是os任务调度的调试,为了能更直观了解到os的运行状况,我们需要使用特殊的调试工具,如System view,来更好地辅助我们判断系统的运行情况,以便及时做出调整和改进。 串口驱动必须能够稳定地处理高速数据流,并且能在CPU资源有限的情况下高效运行,这要求我们要对串口通信协议和缓冲机制有深入理解。
在飞行控制系统中,实时接收和处理飞行控制指令至关重要。任何延迟或错误都可能导致飞行安全问题。因此,必须优化串口驱动以满足严格的实时性要求。 MPU6050传感器虽然功能强大,但任何环境干扰或硬件缺陷都可能导致数据读取不准确,需进行校准和滤波处理以确保数据质量。收集的数据需要通过复杂的算法进行处理,以计算飞行器的准确姿态。这些算法必须优化以在有限的计算资源中运行,同时保持高精度和低延迟。
第二章 针对复杂工程问题的方案设计与分析
通过对复杂工程问题进行研讨,我们在本章中提出针对性的设计方案,并进行详细的分析。这些设计方案将涵盖操作系统的选择、总体设计以及系统架构的具体实现。我们将详细阐述如何选择合适的操作系统和调试工具,μc/OS-II的任务调度,事件驱动,时间驱动模块,并通过μc/OS-II内核结构,架构图和流程图来展示系统的总体设计思路,最后通过os编写主函数的任务驱动逻辑。
2.1针对复杂工程问题的方案设计
2.1.1阶段性目标分析
(1)系统评估与规划:回顾上学期成果,并进行需求分析。明确实时操作系统带来的好处,例如任务优先级管理、实时调度等,以及新系统的具体需求。
(2)操作系统移植:准备必要的硬件和软件工具,配置开发环境,μC/OS-II具有轻量级、高可配置性及强大的任务管理能力,适合用于资源受限的嵌入式设备,故选择该操作系统。
(3)系统开发与集成:在STM32F401上实现μC/OS-II的基本功能,包括任务调度、时间管理和中断管理,并确保所有硬件模块在新系统架构下正常工作。
(4)系统优化与测试:使用任务级调试工具SystemView监控系统运行状态,进行性能分析和优化;测试四轴飞行器的基本功能和响应时间,确保满足实时性要求。
在上学期的进阶式挑战项目中,我们完成了四轴飞行器的物理结构搭建;设计并制作了PCB转接板,确保各模块之间的合理连接;之后成功读取并分析了GY-86传感器的数据;实现了对遥控器PWM波信号的捕获和解码;同时通过STM32输出PWM波信号调节舵机转速。
上学期我们通过循环操作来实现了类似于操作系统的轮询功能,但这种方法并不能满足我们本次挑战项目所需达到的实时性能,也不便于我们在下一学期中加入新的任务。为了确保飞行器系统能够按照任务的重要性和紧迫性,以有逻辑、有序的方式稳定运行,我们计划在本学期的进阶式挑战性综合项目II中,基于上一学期的裸机基础,进行实时操作系统μC/OS-II的移植。
2.1.2操作系统与调试工具的选择
(1)操作系统
操作系统负责管理计算机的硬件与软件资源,它具有调度资源的使用顺序、控制输入输出设备并管理文件系统等核心功能,其最关键的职责是分配CPU资源。在本次挑战项目中,我们需合理调配单片机资源——鉴于四轴飞行器的多个任务需依次执行,必须精确规划任务的执行顺序,确保每个任务都能获得必需的资源,从而维持飞行器的稳定操作。μC/OS-II的内存管理机制优化了资源的使用,减少了内存碎片,提高了系统的稳定性和响应速度。这对于资源受限的单片机系统来说尤其重要,因为它能够确保每个任务都能获得必要的内存资源,同时避免资源浪费。鉴于诸多限制,我们采用了μC/OS-II这一操作系统。
μC/OS-II支持多任务调度,还支持信号量、消息队列等任务间通信方式,这为四轴飞行器的各个任务模块之间的数据交换和同步提供了强大的支持。这种通信机制不仅提高了系统的可靠性,而且简化了任务间的交互,使得系统设计更加模块化和易于维护。另外,该操作系统开源且官方文档丰富,易于初学者们查找资料和学习。μC/OS-II的高可靠性,资源占用少,移植方便这三个优点恰好解决了我们这次挑战项目的需求痛点。
  1. 调试工具
keil的调试功能难以直观展示各个任务在操作系统上的运行情况,所以我们需要采取一个任务级的调试工具,来使得任务的运行情况图形化,方便我们尽快查找出问题,而SystemView能够很好地完成这一任务。
SystemView可以实时记录和显示系统行为,包括任务执行时间、系统调用和中断,便于性能优化;与uC/OS-II操作系统兼容良好,能够准确反映任务的执行情况;以图形化界面展示任务的运行时间和调度情况,便于快速定位和解决问题;同时SystemView的UI简洁清晰,易于使用和操作。与keil相比,SystemView可以显著提高开发效率和系统稳定性。
2.1.3 总体设计
notion image
图2-1 总体设计图
如图2-1所示,为了使各个硬件模块之间能够形成良好的逻辑顺序,我们将硬件模块分为初始化和运行任务两个不同的函数:初始化是在操作系统启动时配置好各个硬件模块,确保其处于正确的工作状态。运行任务是操作系统启动后创建并调度各个任务,实现四轴飞行器的功能。
本学期,我们小组成功设计并实现了一个基于μC/OS-II操作系统的四轴飞行器控制系统。项目伊始,我们首先完成了μC/OS-II操作系统到STM32开发板的移植工作,确保了操作系统能够在目标硬件上稳定运行。在硬件接口方面,我们选用了GY86传感器套件中的MPU6050模块,通过它来采集飞行器的运动状态和姿态数据。这些关键数据经过实时的计算和分析,为飞行器的稳定飞行和动态调整提供了坚实的数据支持。
为了提高系统的可靠性和调试效率,我们引入了先进的任务级调试工具,这使我们能够实时观察每个任务的运行情况,及时发现并解决潜在的问题。通过这种方式,我们对系统进行了细致的优化,确保了任务调度的高效性和飞行器控制的精确性。此外,我们还实现了一个用户友好的界面,使得操作者能够轻松监控飞行器的状态,并进行必要的操作。整个设计过程不仅考验了我们对嵌入式系统设计的理解和应用能力,还加深了对飞行控制系统理论与实践的融合知识。
2.2 针对复杂工程问题的推理分析
2.2.1 μC/OS-II操作系统架构设计
这一学期的目标是将μC/OS-II这一操作系统移植到我们的开发板上。在移植之前,我们需要先了解一下μC/OS-II这款系统的架构设计。
notion image
UCOSII 是一个可以基于 ROM 运行的、可裁减的、抢占式、实时多任务内核,具有高度可移植性,特别适合于微处理器和控制器,是和很多商业操作系统性能相当的实时操作系统(RTOS)。为了供最好的移植性能,μC/OS-II 最大程度上使用 ANSI C 语言进行开发,并且已经移植到近 40 多种处理器体系上,涵盖了从 8 位到 64 位各种CPU(包括 DSP)。为了兼容不同的运行平台,其构建了如下图2-?所示的架构:
图2-2 μC/OS-II 内核架构图
(1)Application Software
Application Software层是用户与μC/OS-II操作系统交互的主要接口。用户在这一层中编写具体的应用逻辑,并利用μC/OS-II提供的各种服务和功能来实现多任务处理、时间管理、任务间同步和通信等功能。
我们上学期的综合设计项目是基于前后台系统实现的。其中后台是一个轮询系统,程序依靠死循环实现的。前台系统引入了中断机制,能够处理外部请求。虽然前后台系统能对实时系统做出快速响应,但是也存在以下不足:
事务不可剥夺,例如某个事务正在执行的过程,就不可能执行其他的任务,这与实际情况有很大出入。
事务不可阻塞。事务没有暂停这一功能来阻塞自己,暂停当前的事务就意味着整个系统都停止了。同时事务必须返回,因为只有这样,其他的任务才有机会得到执行。
因此,我们引入μC/OS-II,并设计了将上学期书写的驱动程序移植到μC/OS-II的该层的计划。
(2)Application-Specific
Application-Specific部分是针对具体应用进行配置的关键部分。通过配置文件,用户可以定制μC/OS-II的行为和特性,以满足特定应用的需求。μC/OS-II Configuration部分主要包含配置文件,这些文件定义了操作系统的各种参数和选项,使得用户可以根据具体应用需求对操作系统进行定制和优化。主要的配置文件包括 OS_CFG.H 和INCLUDES.H。下面是对μC/OS-II Configuration (Application-Specific)部分设计的详细理解:
OS_CFG.H:μC/OS-II的主要配置文件,定义了操作系统的各种配置选项和宏。这些选项和宏控制着操作系统的核心功能和特性。以下是一些关键配置项的详细介绍:
任务优先级数量:定义系统中可用的任务优先级数量。例如:
代码2-1 任务优先级数量定义
#define OS_LOWEST_PRIO 63

这表示系统中可以定义64个优先级(从0到63),优先级0为最高优先级。
堆栈大小:定义任务堆栈的大小。例如:
代码2-2 堆栈大小
#define OS_TASK_DEF_STK_SIZE 128

这表示每个任务的默认堆栈大小为128字。
信号量配置:控制信号量的启用和配置,例如:
代码2-3 信号量配置
#define OS_SEM_EN 1

这表示启用信号量功能。
事件标志配置:控制事件标志的启用和配置,例如:
代码2-4 事件标志的配置
#define OS_FLAG_EN 1

这表示启用事件标志功能。
邮箱和消息队列配置:控制邮箱和消息队列的启用和配置,例如:
代码2-5邮箱和消息队列配置
#define OS_MBOX_EN 1
#define OS_Q_EN 1

这表示启用邮箱和消息队列功能。
调度锁配置:控制调度锁功能,例如:
代码2-6 调度锁配置
#define OS_SCHED_LOCK_EN 1

这表示启用调度锁功能,用于临时禁止任务调度。
INCLUDES.H:包含应用程序所需的所有头文件。这是一个集中管理头文件的地方,方便在一个文件中包含所有必要的头文件。例如:
代码2-7 所有头文件
#include "os_cfg.h"
#include "ucos_ii.h"
#include "cpu.h"
#include "lib_def.h"
#include "app_cfg.h"

这个文件的作用是确保所有需要的头文件都能被正确包含,避免在各个源文件中重复包含头文件。
通过对μC/OS-II Configuration (Application-Specific)部分的配置,用户可以灵活定制操作系统的行为,使其更好地适应具体应用的需求。这一部分的配置文件是操作系统与应用程序之间的重要桥梁,确保了μC/OS-II的灵活性和可扩展性。
(3)Processor-Independent Code
μC/OS-II (Processor-Independent Code) 部分包含了与处理器无关的核心代码,这些代码适用于任何处理器架构。这部分代码为操作系统的核心功能提供支持,确保了μC/OS-II在不同硬件平台上的可移植性。下面是对每个文件的详细介绍:
OS_CORE.C:包含操作系统核心功能的实现,例如任务调度和系统初始化。这个文件是整个操作系统的核心,它管理任务的创建、就绪、运行和删除等操作。
OS_FLAG.C:实现事件标志组的功能,用于任务间的同步和通信。事件标志组允许任务通过设置和等待标志来进行同步操作,这在多任务环境中非常重要。
OS_MBOX.C:实现邮箱机制,用于任务间的消息传递。邮箱是一种用于在任务之间发送和接收消息的数据结构,确保消息能够有序、可靠地传递。
OS_MEM.C:提供内存管理功能,包括内存分配和释放。这个文件中的函数管理内存块的分配和释放,以确保系统中内存的有效使用。
OS_MUTEX.C:实现互斥锁,用于防止资源竞争。互斥锁是一种同步机制,确保同一时间只有一个任务可以访问共享资源,从而避免数据不一致的问题。
OS_Q.C:实现消息队列,用于任务间的消息传递。消息队列允许任务通过队列发送和接收消息,适用于需要有序处理消息的应用场景。
OS_SEM.C:实现信号量机制,用于任务间的同步。信号量是一种经典的同步工具,用于控制任务对资源的访问,避免资源冲突。
OS_TASK.C:管理任务的创建、删除和控制。这个文件包含了任务控制块(TCB)的管理函数,负责任务的初始化、状态切换和上下文切换等操作。
OS_TIME.C:提供时间管理功能,例如延时和周期性任务。这个文件中的函数用于管理系统时间,包括延时函数和定时器等。
uCOS_II.C:包含操作系统的主入口和其他核心功能。这个文件是操作系统的核心实现,包括系统初始化、任务调度和中断处理等功能。
uCOS_II.H:头文件,定义了操作系统的常量、类型和函数原型。这个文件包含了所有与μC/OS-II相关的宏定义、数据结构和函数声明,是其他源文件的基础。
这些文件共同构成了μC/OS-II操作系统的核心部分,提供了多任务处理、同步、消息传递和内存管理等关键功能。由于这些代码是与处理器无关的,因此可以移植到不同的硬件平台上,只需对处理器特定的代码进行适配即可。
(4)Processor-Specific Code
μC/OS-II Port (Processor-Specific Code) 部分是与处理器相关的代码,主要用于适配特定的处理器架构,因此也是本学期移植工作的核心。通过这些代码,μC/OS-II可以在不同的处理器上运行。Porting是将μC/OS-II适配到特定处理器架构的过程。虽然μC/OS-II的大部分代码是用C语言编写的,以保证其可移植性,但仍需要编写一些处理器特定的代码,这些代码包括C语言和汇编语言。主要原因是μC/OS-II需要操作处理器的寄存器,而这只能通过汇编语言来实现。下面是对μC/OS-II Port (Processor-Specific Code)部分的详细介绍:
OS_CPU.H:文件定义了与处理器相关的常量和类型。这些定义包括堆栈大小、任务控制块(TCB)中的处理器相关字段等。例如:
代码2-8 os_cpu.h文件示例
#define OS_CPU_EXCEPT_STK_SIZE 128
typedef struct os_tcb {
CPU_STK *StkPtr; // 处理器相关字段
} OS_TCB;

这个文件还定义了与处理器相关的函数原型,例如上下文切换函数。
OS_CPU_A.ASM:汇编代码,包含与处理器相关的低级操作,包括上下文切换(保存和恢复任务的CPU上下文)、中断处理(处理中断,并调用相应的中断服务例程)和启动任务(初始化任务堆栈,并启动第一个任务)。
OS_CPU_C.C:C代码,实现与处理器相关的功能,包括中断向量表(定义中断向量表,并将中断向量指向相应的处理函数)、中断服务例程(实现中断服务例程,处理具体的中断事件)和上下文切换(调用汇编代码,实现任务上下文的保存和恢复)
通过μC/OS-II Port (Processor-Specific Code)部分的移植,用户可以使μC/OS-II在不同的处理器架构上运行,充分利用其强大的实时多任务处理能力。
2.2.2 MCU启动流程设计
代码执行的本质就是 CPU 的取指、译码、执行与回写过程。在这一过程中,CPU 从存储器中取出指令,解码指令的含义,执行指令的操作,并将结果回写到寄存器或存储器中。而所谓 MCU(Microcontroller Unit,微控制器)的启动,也就是探讨在 MCU 启动时应该执行哪些代码。这个问题的研究不仅涉及启动代码的具体内容,还包括启动代码应该放置的位置、启动代码应该完成的任务、内存映射与寻址,以及启动代码编写过程中编译器和链接器应该负责的工作等一系列问题。以下内容将详细探讨这些方面,如图 2-3 所示。
notion image
图2-3 启动流程示意图
(1)内存映射——选择BootMode
由于硬件设计,通电后MCU从地址0x00000000处开始取指令执行。STM32BootModes就是用来选择将哪一个地址的代码映射到0x0处,从而实现从指定地址开始执行。映射其实就是对应的意思。事实上存储器本身并不具备地址,将芯片理论上的地址分配给存储器,这就是存储器映射。
在以前 ARM7/ARM9 内核的控制器在复位后,CPU 会从存储空间的绝对地址0x00000000 取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x00000000(PC = 0x00000000) ,同时中断向量表的位置也是固定的。而 Cortex-M4内核复位后的起始地址和中断向量表的位置可以被重映射。
(2)上电后发生的事
上电后,STM32将从哪里取指?显然,我们应该研究PC指针指向哪里。根据ProgramManual的指示,STM32上电后将进行以下两个由硬件实现的自动的操作:
读取0x00000000处的数值,并复制给SP指针
读取0x00000004处的数值,并复制给PC指针
可见,0x00000004处存储的数值所指向的代码即是上电后执行的第一行代码。
(3)启动代码需要完成的工作
一个板子想要正确的、高效的运行代码,我们需要对其进行一些必要的配置——时钟树配置、数据段&BSS段初始化、静态构造函数初始化,最后跳转进入main函数。我们需要对startup.s进行正确的设计。
我们参照主流的设计,将启动需要完成的工作封装在RESET中断服务程序中。MCU上电便直接执行Reset_Handler,包括:
  • 调用System_init()函数完成时钟树配置
时钟树配置是启动过程中至关重要的一步,因为它决定了系统的时钟源和各个外设的时钟频率。正确配置时钟树可以确保系统以预期的频率运行,从而实现高效和稳定的操作。
启动时钟源: 在启动时,系统通常使用默认的内部时钟源。我们需要在启动代码中切换到更稳定和精确的外部时钟源,例如晶振(HSE)。
设置时钟倍频器和分频器: 为了达到所需的系统频率,需要配置倍频器(如 PLL)和分频器。
启用外设时钟: 为各个外设模块(如 GPIO、UART、SPI 等)启用时钟,以确保它们能够正常工作。
  • 数据段和BSS段初始化
在启动过程中,还需要初始化数据段和 BSS 段。数据段包含全局和静态变量的初始值,而 BSS 段通常包含未初始化的全局和静态变量。
数据段初始化: 将数据段的初始值从只读存储区(如 Flash)复制到 SRAM。
BSS 段清零: 将 BSS 段的内存区域清零,以确保未初始化变量的值为零。
  • 静态构造函数初始化
在某些嵌入式系统中,可能会使用 C++ 编写代码,或者 C 代码中包含静态构造函数。启动代码需要调用这些静态构造函数,以确保全局对象在程序开始执行之前正确初始化。
  • 跳转进入main函数
完成上述所有初始化步骤后,启动代码需要跳转到用户定义的 main 函数,开始执行应用程序的主逻辑。
2.2.3 OS任务调度设计
(一)STM32中处理器工作模式
不同于2440A细分为了7大运行模式,STM32则只有简单的线程模式和处理器模式:
Tread Mode 线程模式
上电启动后的默认工作模式,用于执行应用程序代码
Handler Mode 处理器模式
当Cortex-M7处理器接收到异常时,就会从线程模式切换到处理器模式,这时候会执行异常处理程序或中断服务程序。在该模式中,处理器会以更高的特权级别状态执行,以确保对异常的正常处理;
Handler mode 提供了一组特殊的寄存器,用于保存和处理异常相关的上下文信息,例如异常返回地址、堆栈指针和异常状态寄存器。这些寄存器的使用和保存方式在 Cortex-M 处理器的架构中有明确定义。
由此,STM32的指令权限被分为了特权级别和非特权级别:
Unprivileged
对System Register的访问受到限制
不能使用CPS指令来屏蔽中断
不能访问System Timer、NVIC与System control block这三种片内外设
可以限制程序能够访问的内存与外设
Privileged
程序可以使用所有指令,对片内和片上的所有资源拥有访问权
在线程模式中,我们通过CONTROL Register来设定正在执行的程序的特权级别;在处理器模式下运行的所有程序总是为Privileged特权。只有Privileged特权级别的程序才能够读写CONTROL Register,修改线程模式下运行程序的特权级别。非特权程序可以使用 SVC 指令发起一个特权调用,以将控制权转移给特权程序。
(二)STM32中处理器的寄存器资源
STM32F401的内部CPU使用了一些关键寄存器来管理和控制其运行。以下是对这些寄存器的详细介绍,包括程序计数器(PC)和链接寄存器(LR)。片内寄存器的示意图如下:
notion image
图2-4 处理器核心寄存器
1. 程序计数器(PC)
程序计数器(Program Counter,PC)是一个关键的寄存器,用于存储当前正在执行的指令的地址。CPU通过PC寄存器来跟踪指令流的执行:
功能:PC寄存器指向下一条将要执行的指令地址。在执行指令时,PC会自动递增指向下一条指令。
初始化:在上电复位时,PC寄存器会从地址0x00000004处读取一个值并加载到PC中,这个值通常是复位向量地址,指向启动代码的第一条指令。
2. 链接寄存器(LR)
链接寄存器(Link Register,LR)用于存储子程序调用返回地址,是ARM Cortex-M4架构中的一个重要寄存器:
功能:当执行子程序调用指令(如BL或BLX)时,当前指令的下一条指令地址会被存储在LR寄存器中,以便在子程序完成后能够正确返回到调用点。
使用:在子程序结束时,通常会通过BX LR指令返回到调用点,即跳转到LR寄存器中存储的地址。
3. 堆栈指针(SP)
堆栈指针(Stack Pointer,SP)用于管理系统堆栈,分为主堆栈指针(MSP)和进程堆栈指针(PSP):
主堆栈指针(MSP):在复位后,MSP会从地址0x00000000处读取一个值并加载到MSP中,用于系统初始化和中断处理。
进程堆栈指针(PSP):用户可以选择在线程模式下使用PSP进行堆栈管理,以便实现更好的堆栈隔离。
4. 通用寄存器(R0-R12)
通用寄存器用于存储临时数据和中间计算结果,共有13个(R0-R12):
R0-R3:通常用于函数参数传递和返回值。
R4-R12:用于一般的临时数据存储。
5. 特殊功能寄存器
除了上述寄存器外,还有一些特殊功能寄存器:
程序状态寄存器(PSR):包含当前程序的状态信息,如条件码标志和中断状态。
控制寄存器(CONTROL):用于控制CPU的操作模式和堆栈指针选择。
异常返回寄存器(EXC_RETURN):用于异常处理返回。
STM32的特殊功能寄存器与2440有较大区别。不像2440A那样把所有控制集成在CPSR中,STM32单独引出了CONTROL寄存器(如下图2-5所示),这使得编程工作更加灵活。
notion image
图2-5模式总结
(三)uC/OS-II的任务调度
uC/OS-II 是一个基于任务优先级抢占式任务调度的实时操作系统。它采用了优先级抢占式调度策略,确保高优先级任务能够立即得到 CPU 执行,并通过任务控制块(Task Control Block,TCB)来管理和调度任务。在设计和实现实时操作系统时,理解这些基本概念和机制是至关重要的。它的本质即是不断地维护下图2-6所示的有限状态机。
notion image
图2-6 μC/OS-II 调度有限状态机
  1. 任务优先级抢占式调度
uC/OS-II 使用任务优先级来决定任务的执行顺序。每个任务都被赋予一个唯一的优先级,数值越小优先级越高。优先级的范围通常从 0 到某个最大值(例如 255),其中 0 是最高优先级。
抢占式调度意味着当一个高优先级任务变为就绪状态时,操作系统会立即中断当前正在运行的低优先级任务,将 CPU 分配给高优先级任务。这种机制确保了系统对高优先级任务的及时响应。
  1. 任务控制块(Task Control Block,TCB)
每个任务在 uC/OS-II 中都有一个对应的任务控制块(TCB),它包含了任务的所有关键信息,例如任务栈指针、任务优先级、任务状态等。TCB 是任务调度的核心数据结构。
TCB 通常包含以下字段:
任务栈指针:保存任务的栈顶指针,用于任务切换时保存和恢复任务的运行环境。
任务优先级:表示任务的优先级。
任务状态:表示任务当前的状态(就绪、运行、挂起等)。
任务延迟:用于任务延迟或定时。
  1. 任务管理
任务创建是通过调用操作系统提供的 API 完成的,例如 OSTaskCreate 函数。这个函数会初始化任务栈、设置 TCB 并将任务插入到就绪任务列表中。
任务切换是指从一个任务切换到另一个任务。uC/OS-II 通过保存当前任务的运行环境(如寄存器和栈指针),并恢复下一个任务的运行环境来实现任务切换。任务切换通常由系统时钟中断或任务调度请求触发。
  1. 中断和时钟管理
uC/OS-II 支持中断处理,当中断发生时,系统会保存当前任务的运行环境,执行中断服务程序(ISR),然后根据 ISR 的执行结果决定是否进行任务调度。
系统时钟是实时操作系统的重要组成部分。uC/OS-II 使用系统时钟来实现任务延迟、定时任务等功能。系统时钟通常由硬件定时器产生的周期性中断驱动。
  1. 同步与通信
信号量用于任务间的同步和互斥,uC/OS-II 提供了二进制信号量和计数信号量两种类型。任务可以通过获取和释放信号量来控制对共享资源的访问。
消息队列用于任务间的消息传递,任务可以通过发送和接收消息来进行通信。消息队列可以实现任务间的数据交换和事件通知。
  1. 资源管理
uC/OS-II 提供了内存管理功能,包括固定大小内存块的分配和释放。内存管理的高效实现对于嵌入式系统的性能和可靠性至关重要。
除了内存,uC/OS-II 还可以管理其他硬件资源,如 I/O 设备、网络接口等,通过设备驱动程序和系统调用接口来实现资源管理。
2.2.4 事件驱动机制设计
所谓事件驱动机制,即是对中断事件的管理。尤其是在引入操作系统之后,软件的架构从前后台的裸板程序变化到了带有任务调度的系统架构,其中断处理是大大不同的。为此,STM32中特地设计了PendSV(可挂起的系统服务调用)中断服务程序
(一)引入操作系统前后中断处理的区别
注意一点,裸板下的IRQ中断服务程序,执行完后,回到某条指令执行,它整个代码可以看做是只有一个任务(线程),不管怎么返回也都是在这一个任务中执行。带有OS时,执行完IRQ后有可能就调度到其他任务去了,不会返回原中断点执行。但是OS就不一样了,它必须保存LR_IRQ到任务栈中,才能保证该任务能从被终端点处回复执行。
没有操作系统时,所有代码均为一个线程。不管怎么弄跳转都在一个线程里面跳
有操作系统时,跳转就牵扯到任务(线程)的切换了!被中断的线程不一定会在中断结束后立即恢复执行。这时候要保证被中断的线程能再度从被中断处继续执行,因此多了很多操作。
μC/OS II是一个非常精简的RTOS,其内核没有专门针对对中断的管理机制,但对于中断服务程序的编写有一定要求。
μC/OS II是一个可抢占的内核,中断处理完成之后,内核调度程序OS_Sched()会从就绪队列中找到优先级最高的任务运行(优先级最高的任务并不一定是被中断的那个任务,因此在发生中断、进入用户的中断服务程序前,首先需要保存被中断任务的上下文。
此外,μC/OS II允许中断嵌套,所以在中断服务程序中还需要增加中断嵌套计数器,然后根据嵌套数量来决定是否重新调度。
这些就是μC/OS II中的中断与裸板中断的区别。
(二)PendSV
在STM32中,PendSV(可挂起的系统服务调用)是一个非常重要的中断,用于实现任务的上下文切换。它通常由操作系统在需要进行任务切换时触发。在μC/OS-II操作系统中,PendSV中断的触发和处理是任务调度的核心部分。以下是关于PendSV的详细解释:
PendSV是一个可挂起的系统服务调用中断,专门用于处理任务切换。它的优先级通常被设置为最低,以确保它只在所有其他高优先级中断处理完成后才被执行。
当操作系统决定进行任务切换时,例如一个高优先级任务变为就绪状态,或当前任务需要挂起时,操作系统会设置PendSV中断挂起位,以触发PendSV中断。这个过程通常通过以下代码实现:
代码2-9
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; // 设置PendSV中断挂起位

这行代码将PendSV中断挂起位设置为1,触发PendSV中断。
当PendSV中断触发时,处理器会自动保存当前任务的上下文,然后跳转到PendSV中断处理程序。以下是PendSV中断处理的主要步骤:
  1. 保存当前任务的上下文
硬件会自动保存部分上下文,包括R0-R3、R12、LR、PC和xPSR寄存器。
PendSV中断处理程序会进一步保存其余的寄存器(如R4-R11)到当前任务的堆栈。
  1. 选择下一个任务
操作系统会选择下一个要运行的任务,并更新相关的任务控制块(TCB)指针。
  1. 恢复下一个任务的上下文
PendSV中断处理程序从下一个任务的堆栈中恢复其寄存器(如R4-R11)。
硬件会自动从堆栈中恢复其余的寄存器(如R0-R3、R12、LR、PC和xPSR)。
2.2.5 时间驱动机制设计
在STM32中,μC/OS-II的时间触发机制是通过时钟滴答(tick)来实现的。时钟滴答是由硬件定时器生成的周期性中断事件,用于驱动操作系统内核的时间管理功能。具体来说,μC/OS-II的时间触发机制包括以下几个关键部分:
1. 时钟滴答定时器的配置
首先,需要配置一个硬件定时器来产生固定频率的时钟滴答中断。在STM32中,通常使用SysTick定时器或普通的通用定时器(如TIMx)来实现这一功能。定时器的频率(即时钟滴答频率)可以根据应用需求进行设置,一般在10到1000 Hz之间。
2. 时钟滴答中断服务程序
当定时器产生时钟滴答中断时,处理器会进入时钟滴答中断服务程序(ISR)。在μC/OS-II中,这个中断服务程序需要调用操作系统内核提供的OSTimeTick()函数。这个函数负责更新操作系统的内部时间计数器,并处理基于时间的任务调度和延时操作。
示例代码如下:
代码2-10 时钟滴答中断服务程序示范
void SysTick_Handler(void) {
OSIntEnter(); // 进入中断
OSTimeTick(); // 调用操作系统的时钟滴答函数
OSIntExit(); // 退出中断
}

3. 时间管理服务
μC/OS-II提供了一系列时间管理服务,允许任务基于时间进行延时和同步。这些服务包括:
OSTimeDly(): 使调用任务延时指定的时钟滴答数。当任务调用这个函数时,它会进入延时状态,直到指定的时间过去或被其他任务唤醒。
OSTimeDlyHMSM(): 使任务延时指定的小时、分钟、秒和毫秒。这是OSTimeDly()的扩展版本,允许更精细的时间控制。
OSTimeDlyResume(): 立即恢复一个延时任务,使其进入就绪状态。
OSTimeGet(): 获取当前的系统时间计数器值。
OSTimeSet(): 设置系统时间计数器的值。
4. 定时器服务
除了基本的时间管理服务,μC/OS-II还提供定时器服务(Timer Service),允许应用程序创建和管理软件定时器。定时器是向下计数的计时器,当计数到零时,会调用用户定义的回调函数。这些定时器可以用于执行周期性任务或单次延时任务 。
配置定时器的步骤包括:
启用定时器服务:在os_cfg.h中设置OS_TMR_EN为1。
创建定时器:使用OSTmrCreate()函数创建一个定时器。
启动定时器:使用OSTmrStart()函数启动定时器。
5. 中断嵌套管理
在处理时钟滴答中断时,μC/OS-II允许中断嵌套,这意味着高优先级的中断可以打断低优先级的中断服务程序。这通过在进入和退出中断服务程序时调用OSIntEnter()和OSIntExit()函数来实现。这些函数负责管理中断嵌套计数器,确保系统在处理中断时的稳定性和正确性。
6. 影响系统性能的因素
时钟滴答频率的选择对系统性能有重要影响。较高的时钟滴答频率可以提供更高的时间分辨率,但会增加系统的开销,因为时钟滴答中断处理需要消耗处理器时间。相反,较低的时钟滴答频率会减少系统开销,但可能导致时间分辨率不足。因此,选择合适的时钟滴答频率需要根据具体应用的需求进行权衡、
通过以上机制,STM32中的μC/OS-II能够实现精确的时间管理和任务调度,确保实时操作系统的高效运行和响应性。
2.2.6 OS启动代码设计
启动的部分分为两大步骤:开发板的启动,也就是Rest_Handler;OS的启动,即OS_init().
(一)startup.s 的编写
startup.s文件是系统启动代码,负责初始化硬件和设置基本的运行环境。其主要内容和作用包括:
中断向量表:定义中断向量表,并将复位向量指向Reset_Handler。这确保了系统在上电或复位后能够正确找到并执行启动代码。
复位处理程序(Reset_Handler):在复位处理程序中,完成以下几个关键任务:
设置堆栈指针:初始化堆栈指针,以便后续代码可以正常使用堆栈。
系统初始化:调用SystemInit函数,配置系统时钟和基本硬件资源。
静态构造函数初始化:调用__libc_init_array函数,初始化全局和静态对象(特别是C++代码)。
跳转到主程序入口:最终跳转到main函数,开始执行用户代码
(二)linker.ld链接器脚本的编写
链接器脚本linker.ld定义了程序的内存布局和各段的地址范围。其主要内容和作用包括:
内存区域定义:指定程序的各个内存段(如闪存和SRAM)的起始地址和长度。这确保了编译器和链接器在生成二进制文件时能够正确地分配和定位代码和数据。
段定义:定义代码段、数据段、BSS段等在内存中的具体位置。这些段定义了程序的各个部分在内存中的布局,例如代码段通常放在闪存中,而数据段和BSS段则放在SRAM中。
堆栈和堆的配置:指定堆栈和堆的起始地址和大小,确保程序运行时有足够的栈空间和动态内存空间
(三)main.c中OS初始化部分的编写
在main.c中进行操作系统初始化是启动μC/OS-II的关键步骤。其主要内容和作用包括:
操作系统初始化:调用OSInit函数初始化μC/OS-II。这包括初始化系统数据结构、任务控制块(TCB)和其他内核资源 。
创建任务:使用OSTaskCreate或OSTaskCreateExt函数创建用户任务。每个任务都有一个任务函数,定义了任务的具体执行逻辑 。
启动任务调度:调用OSStart函数启动任务调度器,使μC/OS-II开始管理和调度任务。这一步标志着操作系统的正式运行,系统进入多任务调度状态
这些步骤的作用和意义在于确保STM32F401RE-Nucleo开发板能够正确启动并运行μC/OS-II操作系统,从而实现实时多任务处理:
startup.s文件确保系统在上电或复位后能够正确初始化硬件和基本运行环境,为操作系统的启动打下基础。
linker.ld链接器脚本定义了程序的内存布局,确保代码和数据在正确的内存位置,为程序的稳定运行提供保障。
main.c中OS初始化部分负责初始化操作系统和任务调度,使μC/OS-II能够管理和调度任务,实现复杂的实时多任务应用。
2.2.7 移植操作系统设计
移植μC/OS-II到不同的处理器上是一个相对简单的过程,但它需要一定的理解和细致的工作。μC/OS-II的大部分代码是用C语言编写的,以保证其可移植性,但仍需要编写一些处理器特定的代码,这些代码包括C语言和汇编语言。以下是移植操作系统到新的处理器架构的设计和步骤。
1. 确认处理器要求
在开始移植之前,需要确保目标处理器满足μC/OS-II的基本要求:
处理器具有能够生成重入代码的C编译器。
可以从C代码中禁用和启用中断。
处理器支持中断,并且能够提供定期发生的中断(通常为10到100Hz)。
2. 编写处理器特定的代码
移植μC/OS-II的核心工作在于编写处理器特定的代码,这些代码主要包括以下几个文件:
OS_CPU.H:定义与处理器相关的常量和类型。
OS_CPU_A.ASM:汇编代码,包含与处理器相关的低级操作,例如上下文切换。
OS_CPU_C.C:C代码,实现与处理器相关的功能,例如中断处理。
3. OS_CPU.H 文件
OS_CPU.H文件定义了与处理器相关的常量和类型。这些定义包括堆栈大小、任务控制块(TCB)中的处理器相关字段等。例如:
代码2-11 os_cpu.h文件示例
#define OS_CPU_EXCEPT_STK_SIZE 128
typedef struct os_tcb {
CPU_STK *StkPtr;
} OS_TCB;

这个文件还定义了与处理器相关的函数原型,例如上下文切换函数。
4. OS_CPU_A.ASM 文件
OS_CPU_A.ASM文件包含了与处理器相关的汇编代码。主要实现以下功能:
上下文切换:保存和恢复任务的CPU上下文(寄存器状态)。
中断处理:处理中断,并调用相应的中断服务例程。
启动任务:初始化任务堆栈,并启动第一个任务。
5. OS_CPU_C.C 文件
OS_CPU_C.C文件包含了与处理器相关的C代码。主要实现以下功能:
中断向量表:定义中断向量表,并将中断向量指向相应的处理函数。
中断服务例程:实现中断服务例程,处理具体的中断事件。
上下文切换:调用汇编代码,实现任务上下文的保存和恢复。
6. 测试和调试
在完成处理器特定代码的编写后,进行充分的测试和调试,确保移植后的μC/OS-II在目标处理器上稳定运行。这一步通常包括:
验证中断处理:确保中断服务例程能够正确处理中断事件。
检查上下文切换:确保任务能够正确切换,且上下文能够正确保存和恢复。
运行示例应用:通过运行一些简单的示例应用来验证操作系统的基本功能。
第三章 针对复杂工程问题的方案实现
在面对μC/OS-II的移植与实现这一复杂工程问题时,方案的实现往往需要深入理解系统的架构、各个软件设计模块以及硬件特性。本章将详细探讨如何将理论知识应用于实践,通过一系列具体的技术步骤,解决嵌入式系统中的复杂问题,我们将基于针对复杂工程问题的方案设计,详细分析和实现操作系统的关键组件,包括上下文切换、时钟配置和临界区保护等模块。
3.1 在 Keil 中重构μC/OS-II 新工程
在移植之前,我们需要先理清楚μC/OS-II的代码架构,才能在keil中重构μC/OS-II的新工程,把无需改动的代码复制粘贴到合理的位置,先通过各种方式逐步消除基本的报错,再根据需求重新编写涉及stm32底层的代码。
  1. 复制原有代码
我们简单地把uc/OS-II的代码分为三类——Config、Core与Port。
Core 文件夹包含了 uC/OS II 内核的核心功能模块。这些模块提供了任务管理、时间管理、资源管理、任务通信和同步等核心功能,是 uC/OS II 内核 的基础。Port文件夹中的文件包含了针对不同处理器平台的移植代码。
每个处理器平台都有不同的硬件架构和操作系统接口,因此需要根据处理器的特点进行移植,以确保 uC/OS II 内核可以在特定处理器上正确地运行。 Config文件夹中的文件用于配置 uC/OS II 内核的各种功能和选项。
Core、Port 和 Config 是 uC/OS II 内核的三大模块,通过这些模块的工作协同,我们可以根据目标系统的需求定制和优化 uC/OS II 内核,使其在 STM32F401 开发板上高效运行。
notion image
图3-1 uc/OS-II的文件夹
需要复制到我们原有工程的是:Cfg、Ports、Sources三个文件夹
我们可以根据自己的需求把它们放在自己的工程里,一种可行的方案如下:
1.新建Config文件夹,把\uC-OS2-2.93.01\Cfg\Template中的三个文件复制至其中。
2.新建Sources文件夹,把\uC-OS2-2.93.01\Source中除去ucos_ii.c之外的所有文件复制至其中。
3.新建Ports文件夹,把uC-OS2-2.93.01\Ports\ARM-Cortex-M\ARMv7-M\中的os_cpu_c.c复制至其中,并根据编译工具链,选择ARM文件夹或者GNU等文件夹里除去os_dbg.c之外的两个文件复制至其中。
(二)构建新工程
接着,我们需要打开 keil 5,把我们创建的这三个文件夹导入到项目中,如下图3-2所示。选择 project->new uversion project, 然后选好芯片型号:STM32F401RETx。再单击 target 1-add group 新建文件夹,并重命名为 config、core 和 ports,右键单击文件夹,add existing files to group ‘xxx’添加文件。
图3-2 uc/OS-II的文件夹
notion image
notion image
这之后,我们需要添加头文件路径,如下图3-3所示。打开魔术棒,在 C/C++和 Asm 里的 Include Path 中写入C 程序和汇编文件的路径。
图3-3 文件夹路径
(三)消除报错
在初步构建完成系统工程后,我们将所有文件编译,可以查看到出现的许多error和warning报错,并一一修改他们。常见的报错类型如下:
error1:缺少文件或找不到文件。
.\ucos\ucos_ii.h(45): error: #5: cannot open source input file "xxx": No such file or directory
解决方式:手动添加响应文件。
error2:由非 INTxxxU 型变量或宏定义中的变量未定义引发的错误。
.\ucos\ucos_ii.h(719): error: #20: identifier "xxx" is undefined
解决方式:在os_cfg.h中手动添加所需宏定义,#define如下。
代码3-1 添加宏定义
#define OS_MAX_TASKS 64
#define OS_LOWEST_PRIO 64
#define 0S_TASK_IDLE_STK_SIZE 64

error3:找不到指定函数的定义。
.\Objects\baogao.axf: Error: L6218E: Undefined symbol
解决方式:补充函数定义,举例如下。
代码3-2 补充函数
void SystemInit(){}
OS_STK *OSTaskStkInit(void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt){ return ptos; }
void OS_KA_BASEPRI_Boundary(){}
void OSTaskCreateHook(){}

warning:函数未直接声明。
.\ucos\os_core.c(722): warning: #223-D: function "OSIntCtxSw" declared implicitly
解决方式:在未直接声明的函数所对应的文件中,使用extern语句。
代码3-3 补充extern
extern void OSIntCtxSw(void);

在大致解决上述报错及警告后,即可成功编译链接,做到0 error 0 warning。
notion image
图3-4 编译结果图
在完成对μC/OS-II代码架构的分析和理解后,我们采取了一系列步骤来重构其在Keil环境下的新工程。首先,我们对原有的μC/OS-II代码进行了分类,将Config、Core和Port三个关键模块的文件复制到新工程中合适的位置。接着,我们利用Keil MDK-ARM IDE创建了新的工程,并导入了这三个文件夹,同时配置了必要的头文件路径。
在初步构建工程后,我们进行了编译,面对编译过程中出现的error和warning,我们逐一进行了排查和修正。例如,对于宏定义未定义的错误,我们在os_cfg.h文件中添加了相应的宏定义;对于未找到函数定义的错误,我们补充了相应的函数实现;对于函数未声明的警告,我们使用了extern关键字进行声明。
3.2 在Linux中重构μC/OS-II 新工程
3.2.1 编译工具链配置
notion image
一直以来IDE用的太多,代码编译等基本环节知识欠缺了许多,在这个学期的移植中学到了不少。对于其他类unix系统,下文步骤中的包管理器和环境变量配置可能有些需要更改的地方。
图3-5 工具链示意图
(一)安装交叉编译链 GNU Armmbedded Toolchain
打开terminal,输入
brew install --cask gcc-arm-embedded
这一行指令会安装arm-none-eabi-gcc、arm-none-eabi-gdb以及 arm-none-eabi-binutils的完整工具链。
安装完毕后,输入 arm-none-eabi-gcc -v 、 arm-none-eabi-gdb -v 指令,如果出现版本信息即可
notion image
3-6 版本信息
(二)安装openOCD
打开terminal,输入:brew install open-ocd 即可完成安装
(三)安装STLink
打开terminal,输入:brew install stlink
随后使用st-info --probe查看STlink连接情况,如下图3-8所示
notion image
图3-7 stlink连接情况
(四)终端命令行测试
在命令行里使用make与openocd指令对一demo进行测试,解决所有问题后编辑VScode里Workspcae的配置后开始编程。
使用Cube32MX快速新建一个点灯的demo;Generate Code时选择“Makefile”选项
想要编译项目,在终端里输入:
make
终端返回(在当前目录新建了build目录)
arm-none-eabi-size build/TESTFOROpenOCD.elf
text data bss dec hex filename
4264 20 1572 5856 16e0 build/TESTFOROpenOCD.elf
arm-none-eabi-objcopy -O ihex build/TESTFOROpenOCD.elf build/TESTFOROpenOCD.hex
arm-none-eabi-objcopy -O binary -S build/TESTFOROpenOCD.elf build/TESTFOROpenOCD.bin
(五)烧录
在终端中输入:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program build/TESTFOROpenOCD.elf verify reset exit"
终端返回以下信息说明烧录成功
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : clock speed 2000 kHz
Info : STLINK V2J37M26 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.254104
Info : [stm32f4x.cpu] Cortex-M4 r0p1 processor detected
Info : [stm32f4x.cpu] target has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000fcc msp: 0x20018000
Info : Unable to match requested speed 8000 kHz, using 4000 kHz
Info : Unable to match requested speed 8000 kHz, using 4000 kHz
** Programming Started **
Info : device id = 0x10016433
Info : flash size = 512 KiB
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
shutdown command invoked
板载小灯此时亮起,烧录成功。
3.2.2 VSCode 配置
(一)编译&下载:配置Task.json
配置三个task:编译、烧录以及清除已编译的文件。这样一来,我们仅仅需要通过command+shift+B即可执行上面在命令行中输入的一长串指令。下面以烧录为例,其余两条指令只需要调整command以及args部分。
代码3-4 配置Task.json
"tasks": [
{
"type": "shell",
"label": "烧录",
"command": "openocd",
"args": [
"-f",
"interface/stlink.cfg",
"-f",
"target/stm32f4x.cfg",
"-c",
"program build/TESTFOROpenOCD.elf verify reset exit"
],
"problemMatcher": [
"$gcc"
],
"group": "build",
"detail": "By arm-none-eabi-gcc 10.3.1 20210824 (release)"
},
]

(二)调试:配置Launch.json
创建Launch.json,添加如下:
代码3-5 Launch.json添加
"configurations": [
{
"name": "ST-Link Cortex-M4 Debug",
"type": "cortex-debug",
"request": "launch",
"servertype": "openocd",
"cwd": "${workspaceRoot}",
"executable": "${workspaceRoot}/build/TESTFOROpenOCD.elf",
"device": "STM32F401RE",
"configFiles": [
"interface/stlink.cfg",
"target/stm32f4x.cfg"
],
"svdFile": "${workspaceRoot}/Script/STM32F401.svd",
"runToEntryPoint": "main",
},
]

而这行代码则设置了一个特殊的返回值到新创建的任务堆栈中,值模拟了在发生异常时,处理器会加载到PC寄存器的值,确保了当任务第一次被调度器选中执行时,它能够从正确的状态开始执行。而值0xFFFFFFFD这个值可以解释为:
  1. 使用进程堆栈(PSP),
  1. 清除IT状态,
  1. 返回地址是异常处理程序的地址加上。
3.4.2任务上下文切换
notion image
在μC/OS-II实时操作系统中,任务上下文切换是确保多任务环境顺利运行的关键机制。它允许系统在多个任务之间高效、安全地切换执行权,无论是任务主动放弃CPU使用权,还是由于中断处理的需要而进行的被动切换。
图3-15 任务切换流程图
主动切换:当任务需要等待某些事件发生或者是等待某些资源变得可用时,任务会主动放弃CPU使用权,这时便会调用OSCtxSw()函数。当系统执行任务A选择主动放弃CPU时,系统将首先捕获并存储任务A的所有执行上下文,这包括其寄存器状态和堆栈信息。随后,系统将更新程序计数器PC,确保控制权顺利移交给任务B,并从保存的状态中恢复任务B的执行环境,允许任务B从上次挂起的点继续执行。
被动切换:当任务执行过程中触发了中断,则任务会被动放弃CPU使用权,由中断引发任务切换,μC/OS-II为此提供了OSIntCtxSw()函数。当中断服务程序运行完成并准备返回时,将调用该函数。保存中断退出时的现场,同时检查并准备恢复下一个任务的执行环境。这样,一旦中断处理完毕,系统能够确保恢复到正确的任务执行流程,无论是继续执行先前的任务还是切换到更高优先级的任务,从而保障系统的响应性和稳定性。
任务上下文切换的基本过程已由上图3-15指出:
(1)保存当前任务的环境
(2)保存当前任务的堆栈指针
(3)设置新任务的堆栈指针
(4)恢复新任务的环境
3.4.2.1 OSCtxSw()与 OSIntCtxSw()函数的编写
在ARM处理器中,OSCtxSw()和OSIntCtxSw()函数是μC/OS-II操作系统用来触发任务上下文切换的软件中断函数。这两个函数虽然在不同的场景下被调用,但其核心目的都是告诉处理器需要进行任务切换,两者大致上可以采用相同的实现方法。
代码3-26 OSCtxSw()和OSIntCtxSw()函数
OSCtxSw
OSIntCtxSw
LDR R0, =NVIC_ICSR
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR

这段代码中,NVIC_ICSR是嵌套向量中断控制器NVIC的中断控制寄存器ICSR的地址,用于控制中断的触发,其的值设为0xE000ED04,NVIC_PENDSVSET是一个宏定义,其值设为0x10000000,在该段程序里面,可以清晰的发现,我们将NVIC_PENDSVSET写入NVIC_INT_CTRL寄存器来触发PendSV中断,而之后再由PendSV_Handler函数来真正的任务切换。
图3-
notion image
16 ICSR寄存器定义图
如图3-16, 在ICSR寄存器中,将第28位设置为1,即打开了PENDSVSET相应位,将PendSV中断设置为待处理状态。这意味着,一旦处理器完成当前的中断服务例程ISR 并准备返回到线程模式,它将自动进入PendSV中断的服务例程,执行任务调度或其他挂起的操作。
3.4.2.2 PendSV_Handler函数的编写
PendSV(Pending Service Call)是一个特殊中断,它被设计用来作为软件触发的中断,用于实现任务切换。PendSV中断的优先级是最低的,有效地避免了优先级反转,减少上下文切换的频率。同时它可以被软件设置为可抢占其他中断,从而确保任务切换的及时性。一旦PendSV中断被触发,将执行中断处理程序PendSV_Handler,可以统一处理所有任务切换的上下文保存和恢复工作,使得任务切换逻辑集中且一致。
以下图是PendSV_Handler()函数的流程图,描述了实现代码的逻辑思路,我们的代码将基于该流程进行编写,后面我们将详细介绍整个过程。
notion image
图3-17 PendSV_Handler()函数流程图
代码3-26 PendSV_Handler
PendSV_Handler
;修改中断优先级边界:
CPSID I
MOV32 R2, OS_KA_BASEPRI_Boundary
LDR R1, [R2]
MSR BASEPRI, R1
CPSIE I
;保存当前任务的上下文:
MRS R0, PSP
STMFD R0!, {R4-R11, R14}
;保存当前任务的堆栈指针:
LDR R5, =OSTCBCur
LDR R1, [R5]
STR R0, [R1]
;调用钩子函数:
MOV R4, LR
BL OSTaskSwHook
;更新任务优先级:
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
;更新任务TCB指针:
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R5]
;恢复新任务的上下文:
ORR LR, R4, #0x04
LDR R0, [R2]
LDMFD R0!, {R4-R11, R14}
MSR PSP, R0
;取消中断优先级边界:
MOV32 R2, #0
CPSID I
MSR BASEPRI, R2
DSB
ISB
CPSIE I
BX LR
ALIGN
END

首先我们先修改中断优先级边界,以确保只有高于特定优先级的中断才能被CPU处理。这在多任务操作系统中非常重要,因为它允许系统设置中断的优先级,以防止低优先级的中断打断高优先级任务的执行。
禁用中断后,将OS_KA_BASEPRI_Boundary 存的中断优先级边界值加载到R1中,再R1寄存器的值移动到BASEPRI(Base Priority)寄存器,这样即定义了中断请求的最低优先级。最后再重新启用IRQ中断。
代码3-27 修改中断优先级边界
CPSID I
MOV32 R2, OS_KA_BASEPRI_Boundary
LDR R1, [R2]
MSR BASEPRI, R1
CPSIE I

notion image
BASEPRI寄存器各个位配置如下图所示,我们只需更改其[7:4]位所代表的Priority mask bits即可设置中断优先级边界值。
图3-18 BASEPRI寄存器定义
其次我们再保存当前任务的上下文,即R4-R11, R14这些寄存器中的内容,再把当前任务的堆栈指针PSP存入TCB中。
代码3-28 保存上下文及存PSP
MRS R0, PSP
STMFD R0!, {R4-R11, R14}
LDR R5, =OSTCBCur
LDR R1, [R5]
STR R0, [R1]

之后我们可以调用任务切换的钩子函数,可以自定义其中的功能,如处理浮点寄存器的保存和恢复。
代码3-29 钩子函数
MOV R4, LR
BL OSTaskSwHook

这段代码用于确保了操作系统能够跟踪当前运行任务的优先级,并在任务切换时更新为最高优先级就绪任务的TCB,从而允许该任务获得CPU的控制权并继续执行。读取当前优先级变量的地址及最高优先级就绪任务的优先级,再更新当前任务的优先级。读取最高优先级就绪任务的TCB指针后,再更新当前TCB指针。
代码3-30 更新当前TCB指针
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R5]

然后我们再恢复新任务的上下文,修改LR寄存器,确保使用进程栈进行异常返回,从新任务的TCB中获取新任务的堆栈指针,从新任务的堆栈中弹出R4-R11和R14寄存器的值,将新任务的堆栈指针设置为进程栈指针。
代码3-31 恢复新任务的上下文
ORR LR, R4, #0x04
LDR R0, [R2]
LDMFD R0!, {R4-R11, R14}
MSR PSP, R0

最后我们恢复中断优先级,取消中断优先级边界。禁用中断后再次更改BASEPRI寄存器的值,将其设置基优先级为0,即允许所有优先级的中断,再重新启用中断,最后使用LR中的值自动恢复上下文。
代码3-32 取消中断优先级边界
MOV32 R2, #0
CPSID I
MSR BASEPRI, R2
CPSIE I
BX LR
ALIGN
EN

3.5 实现uc/0S II在STM32上的移植
为了实现uc/os II在STM32上的正常运行,我们除了实现os事件驱动机制所需要的任务堆栈初始化、上下文切换和中断响应等功能,还需要保证STM32的滴答定时器正常工作,为os的时间驱动机制提供时钟基准,保障周期性任务的有序进行。此外,我们还需要编写一些os启动相关的代码,比如OSStartHighRdy,用于os启动后初始任务的运行;对于临界资源的保护,我们需要提供进出临界区的函数。这些都与硬件种类强相关,需要我们针对不同的硬件编写不同的函数。接下来,我们将针对STM32F401RET6进行以上相关uc/os II的移植操作。
3.5.1 设置时钟即配置Systick及时钟中断服务程序编写
Systick定时器就是系统滴答定时器,它是一个24位的倒计数定时器,它的初值从自动装载定时器(RELOAD)中获取,每个时钟周期后,其值会减1,当其计数到0后,会自动从自动重装载寄存器中装载定时初值,并继续减1,循环往复,只要我们不关闭其使能位,Systick定时器便会一直运行计时。此时,如果我们使能了其中断控制位,那么每次计数到0以后,Systick定时器便会发送一次中断请求,触发一次中断,执行相应的中断服务程序。因此,我们便可利用系统滴答定时器周期性重装载并触发中断的特性,在其中断服务程序中执行os运行所需要的周期性操作,如周期性任务的计时等等。
为了实现上述功能,我们需要编写两个主要函数,一个是OS_CPU_SysTickConfig()函数,用于初始化系统滴答定时器;一个是SysTick_Handler()函数,编写Systick定时器的中断服务程序。
3.5.1.1OS_CPU_SysTickConfig()函数的编写
以下图是OS_CPU_SysTickConfig()函数的流程图,是我们实现代码的逻辑思路,我们的代码将基于该流程进行编写,后面我们将详细介绍整个过程。
 
notion image
图3-19 OS_CPU_SysTickConfig()函数流程图
先是OS_CPU_SysTickConfig()函数的编写,下表是其代码实现:
代码3-33 OS_CPU_SysTickConfig()函数
void OS_CPU_SysTickConfig (INT32U cnts)
{
SysTick->LOAD |= cnts - 1u; //设置Reload寄存器
SysTick->VAL |= cnts - 1u; //设置value寄存器初始值
//设置Systick中断服务程序优先级
INT8U prio;
prio = SCB->SHP[11];
prio &= 0x00u;
prio |= 0x04<<4;
SCB->SHP[11] = prio;
SysTick->CTRL |= 0x01; //使能定时器中断
SysTick->CTRL |= 1<<1; //使能定时器
}

notion image
首先,我们对Systick定时器的重装载寄存器进行配置,下图3-18是内核编程手册中对其各位的介绍。
图3-20 重装载寄存器各位定义图
从该图中我们可以看到,这是一个24位有效的寄存器,当定时器的计数器计数到0时,便会自动从中加载值继续进行减1操作,为了实现cnt个处理器时钟周期后触发重装载,我们需要将其值配置为cnt-1.
代码3-34 配置cnt-1
SysTick->LOAD |= cnts - 1u; //设置Reload寄存器

然后,我们需要手动设置定时器中计数的初值为cnt-1,保证定时器启动后从预定值开始减1操作,其寄存器描述如下图3-19:
notion image
图3-21 计数器值寄存器各位定义图
从图中看出,该寄存器与重装载寄存器定义一样,所以在初始化操作中我们仅需将该寄存器的值也设为重装载寄存器中的值即可,确保其从预定值开始运行。
代码3-35 value寄存器初始值
SysTick->VAL |= cnts - 1u; //设置value寄存器初始值

接下来,我们要设置Systick时钟中断的优先级,确保其正常运行,不被其它中断或事件所打断。为此,我们需要配置SCB寄存器中的SHPR3寄存器。
SCB寄存器即系统控制块,是内核外设的主要模块之一,提供系统控制以及系统执行信息,包括配置、控制以及报告系统异常等。而SHPR3是其中的一个寄存器,主要用于系统中断优先级的配置,下图3-20是其寄存器各位的定义图。
notion image
notion image
图3-22 SHPR3寄存器各位定义图
通过阅读手册,我们可以知道PRI_15[31:24]位用于配置Systick的中断优先级,我们对它写入即可实现Systick中断优先级的配置。我们将它配置为不受临界区中断屏蔽所影响的中断优先级,保障实时系统任务的周期性运行,严格保证系统的实时性。
代码3-36 SysTick中断服务程序优先级
//设置SysTick中断服务程序优先级
INT8U prio;
prio = SCB->SHP[11];
prio &= 0x00u;
prio |= 0x04<<4;
SCB->SHP[11] = prio;

notion image
最后,我们需要使能系统滴答定时器的中断响应,并使能Systick定时器,配置CTRL寄存器即可实现,如下图3-21是该寄存器的位定义图。
图3-23 Systick控制寄存器位定义图
从手册中我们知道,该定时器默认的时钟频率是AHB/8,Bit0是定时器使能位,Bit1是Systick中断使能位,对它们进行设置,即可允许中断并启动定时器。
代码3-37 启用定时器中断
SysTick->CTRL |= 0x01; // 启用定时器中断
SysTick->CTRL |= 1<<1; // 启用定时器

3.5.1.2 SysTick_Handler()函数的编写
在初始化了Systick定时器之后,我们再编写相应的中断服务程序,实现对周期性任务的管理。
以下是SysTick_Handler()函数的代码:
代码3-38 SysTick_Handler()函数
void SysTick_Handler (void)
{
#if OS_CRITICAL_METHOD == 3u //分配存储CPU状态寄存器的变量
OS_CPU_SR cpu_sr;
#endif
OS_ENTER_CRITICAL(); // 进入临界区,禁止中断
OSIntEnter(); // 通知uC/OS-II我们正在开始一个中断服务程序
OS_EXIT_CRITICAL(); // 退出临界区,允许中断
OSTimeTick(); // 调用uC/OS-II的OSTimeTick()来实现时钟滴答功能
OSIntExit(); // 通知uC/OS-II我们在离开中断服务程序
}

其代码处理步骤如下:
  1. 声明局部变量。我们采用第三种进入临界区的方式,因此需要定义一个局部变量,用于保存当前xPSR的值,前三行代码用于声明该变量。
  1. 进入临界区。我们需要显式告知os有一个中断服务程序正在运行,通过对OSIntNesting变量操作实现,该操作在OSIntEnter中,为了实现变量的互斥操作,我们需要关闭中断即进入临界区。
  1. 调用OSIntEnter函数,告诉os进入一个中断
  1. 退出临界区
  1. 调用OSTimeTick()函数实现对等待队列的周期性操作
  1. 调用OSIntExit()退出中断服务程序,并根据就绪队列以及中断嵌套情况决定是否执行任务重调度。
这样,我们便能正常响应中断请求并执行相应的中断服务程序,实现任务的周期性调度和执行。
3.5.2实现 OSStartHighRdy函数
首先,OSStartHighRdy()函数负责初始化任务调度系统,这是操作系统启动过程中的一个关键步骤。它确保了任务就绪队列能够正确地被构建和管理,使得系统能够根据任务的优先级进行调度。
其次,该函数还涉及到PendSV优先级的设置。通过设置合适的PendSV优先级,我们可以确保在任务调度过程中,中断处理不会影响系统的实时性能。
OSStartHighRdy()函数还负责初始化MSP(主堆栈指针)和PSP(进程堆栈指针)。这两个寄存器在任务切换时起着至关重要的作用,它们分别指向了当前运行任务的堆栈和任务控制块。OSStartHighRdy()函数的核心功能之一是实现任务选择机制。它通过检查就绪队列,选取其中优先级最高的任务,并开始执行。这一过程确保了高优先级任务能够获得及时的处理,从而满足实时系统的响应需求。
以下图是OSStartHighRdy()的处理流程图,参考该图的流程,我们可以更清晰地实现该函数的编写,关于流程的详细介绍,我们将在后面详细说明。
notion image
图3-24 OSStartHighRdy的流程图
其函数代码如下图所示:
代码3-39 OSStartHighRdy()函数
OSStartHighRdy
CPSID I ; 禁止全局中断
LDR R0, =NVIC_SYSPRI14 ; 设置用于上下文切换的中断PendSV的优先级
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; 初始化PSP指针为0
MSR PSP, R0
LDR R0, =OS_CPU_ExceptStkBase ; 初始化MSP指针指向OS_CPU_ExceptStkBase
LDR R1, [R0]
MSR MSP, R1
BL OSTaskSwHook ; 执行OSTaskSwHook()函数
LDR R0, =OSRunning ; 设置OSRunning值为TRUE,表示os开始运行
MOVS R1, #1
STRB R1, [R0]
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 存的是新任务的SP; SP = OSTCBHighRdy->OSTCBStkPtr
MSR PSP, R0 ; 将sp存到PSP中
MRS R0, CONTROL ; 确保当前使用的是PSP寄存器,而不是MSP
ORR R0, R0, #2
MSR CONTROL, R0
LDMFD SP!, {R4-R11, LR} ; 从PSP栈中恢复R4-R11,LR寄存器的值
LDMFD SP!, {R0-R3} ; 恢复R0-R3的值
LDMFD SP!, {R12, LR} ; 恢复R12,LR的值
LDMFD SP!, {R1, R2} ; 加载PC和xPSR的值到R1和R2
CPSIE I
BX R1

代码实现流程如下:
  1. 首先配置PendSV的中断优先级,需要使用上文配置Systick中断优先级所使用的寄存器SHPRI3,如下图3-22所示,查阅手册可以知道,PRI_14[23:16]用于配置PendSV中断的优先级,我们给PendSV赋最低优先级0xFF,以保证其它中断的正常运行,不被PendSV打断,确保os的有序运行。
    1. notion image
图3-25 SHPR3寄存器各位定义图
  1. 初始化堆栈寄存器MSP,PSP。MSP为系统堆栈区,处理中断或异常等时使用,系统初始化时事先已经开辟了这样一块空间,用OS_CPU_ExceptStkBase指向那里,因此,将其值赋给MSP以实现初始化操作;PSP先赋初值为0,后面会由第一个任务运行时加载它的堆栈指针。
  1. 如有特殊需求,可以在此调用OSTaskSwHook()函数,以实现任务监控和统计功能。
  1. OSRunning置为1,表示操作系统已经开始运行。
  1. 将OSPrioHighRdy给OSPrioCur,将OSTCBHighRdy给OSTCBCur,准备调度就绪队列中最高优先级的任务。
  1. 加载最高优先级的任务的堆栈指针到PSP,准备初始化任务现场。
  1. 我们还要确保是从PSP指向的栈中恢复任务现场,需要对CONTROL寄存器进行操作。下图3-23是CONTROL寄存器各位的定义图。从图中可以看出,Bit1位用于控制当前STM32使用的堆栈指针是MSP还是PSP,我们对该为写1,确保现在使用的是PSP。
notion image
图3-26 CONTROL寄存器位定义图
  1. 按照堆栈初始化规定的顺序恢复任务的初始化现场,并跳转到任务函数开始执行。
这样,我们就实现了初始化任务调度以及调度最高优先级任务开始运行的功能,当执行完该函数后,os将有序地调度各个任务执行,通过os的事件和时间驱动机制,推动os不停地向前运行。
3.5.3 实现进出临界区功能代码
对于OSIntNesting等临界资源的使用需要进行任务间的互斥访问,为了实现这一功能,uc/os II在进入临界区前需要禁止中断,以防止被其它中断或任务所抢占;在访问完临界资源后,开启中断,以及时相应紧急请求。
uc/os II定义了两个宏来实现进出临界区的操作,分别是OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()来关闭中断和打开中断。这两个宏一共有三种实现方式。第一种是直接开关中断,这种方式最简单,但是无法实现嵌套使用,因为它无法知道外层开关中断的情况,很容易导致临界资源不受保护,所以一般不被使用;第二种方式是使用任务堆栈,即进入临界区时,先使用任务堆栈保存状态寄存器的内容,再关闭中断;退出临界区时直接从栈中恢复状态寄存器,也就是外层嵌套的中断开关状态,这种方式能够实现嵌套,但是如果编译时采用栈顶指针相对寻址,并且我们自己程序中使用了栈顶指针话,很容易导致出入栈内容混乱,而丢失状态寄存器的内容;而第三种方式,采用提前声明局部变量的方法,使用局部变量来存储进入临界区前状态寄存器的内容,退出临界区时再利用局部变量恢复,这种方法不仅能实现嵌套,还能解决第二种方法栈指针使用的问题,更安全,更可靠。
STM32F401RET6中用来控制中断的状态寄存器并不是xPSR,而是BASEPRI寄存器,以禁止一定范围的中断,后续将会详细说明。
以下是宏定义的代码实现:
代码3-40 宏定义
#define OS_ENTER_CRITICAL() do { cpu_sr = OS_CPU_SR_Save(CPU_CFG_KA_IPL_BOUNDARY << (8u - CPU_CFG_NVIC_PRIO_BITS));} while (0)
#define OS_EXIT_CRITICAL() do { OS_CPU_SR_Restore(cpu_sr);} while (0)

为了实现不影响系统紧急情况的处理,我们在进入临界区时只禁止部分中断,开放部分异常处理的中断,以保证系统能够及时响应紧急情况,这是实时操作系统可靠性的重要保障。
notion image
因此,在设计OS_CPU_SR_Save()函数时,我们传入一个参数,用于表示要禁止中断的等级,优先级大于该数的中断都将被禁止。其中,CPU_CFG_KA_IPL_BOUNDARY表示所要禁止的中断的范围,禁止优先级大于该数的中断,而 8u - CPU_CFG_NVIC_PRIO_BITS用来针对不同硬件的中断屏蔽寄存器,对于STM32F401RET6来说,它的中断屏蔽寄存器是BASEPRI,其位定义图3-24如下所示:
图3-27 BSEPRI寄存器位定义图
从图中可以看出,Bits[7:4]用于控制需要禁止的中断优先级,当其值大于0时有效,处理器会禁止优先级大于等于它的所有中断,而优先级数小于该值的中断将不受影响,这将很大程度上保证高优先级中断的正常运行。8u - CPU_CFG_NVIC_PRIO_BITS用来将数值移位到BASEPRI寄存器的有效位。
通过查阅手册(如下图3-25),我们知道系统最重要的几个中断的优先级是持续到3,所以当进入临界区时,我们可以将优先级为4及其以上的中断禁止,并且保证重要的中断能及时响应。因此,CPU_CFG_KA_IPL_BOUNDARY 的值我们给到4u。
notion image
图3-28 系统中断优先级向量表
所以,我们在进出临界区时,其实需要保存和恢复的中断状态寄存器是BASEPRI寄存器,而非xPSR寄存器,因为BASEPRI寄存器能实现一定范围的中断禁止,而非关闭全部中断。
有了以上基础和理解,我们便能真正实现第三种进入临界区方式的代码,其代码如下:
代码3-41 进出临界区代码实现
OS_CPU_SR_Save
CPSID I ; 禁止中断
PUSH {R1} ; 保存R1,即将使用
MRS R1, BASEPRI ; 保存BASEPRI的值到R1
MSR BASEPRI, R0 ; 将相应优先级的中断禁止
DSB
ISB
MOV R0, R1 ; 将之前BASEPRI寄存器的值给R0,准备返回值
POP {R1} ; 恢复R1
CPSIE I ; 开启中断
BX LR ; 函数返回
OS_CPU_SR_Restore
CPSID I ; 禁止中断
MSR BASEPRI, R0 ; 从参数中取出局部变量的值给BASEPRI
DSB
ISB
CPSIE I ; 开启中断
BX LR ; 函数返回

OS_CPU_SR_Save()实现的功能是:先将BASEPRI值保存到R1,然后将要禁止的中断优先级给BASEPRI,将R1保存的BASEPRI的初值通过R0返回,从而实现禁止相应中断的功能。
OS_CPU_SR_Restore()实现的功能是:将局部变量的值,即BASEPRI寄存器的初值还原,实现退出临界区的功能。
这样,我们在临界区前后,分别使用上述宏定义,便能实现任务间临界资源的互斥访问,保障os和任务的有序进行,保证结果的可靠性和安全性。
第四章 对于软件子系统的功能性测试
完成方案实现后,测试是确保系统功能和性能的重要步骤。本章将介绍如何搭建SystemView测试环境,并通过SystemView对系统进行监控和分析,以验证系统的可靠性和稳定性。我们将详细讲述SystemView的功能和配置方法,展示如何通过SystemView进行系统监控,分析各个任务的运行状态和性能瓶颈,同时测试MPU6050驱动程序的正确性和稳定性。
4.1 测试环境SystemView搭建
4.1.1 SystemView简介
在RTOS应用的设计过程中,由于任务调度切换是由RTOS任务调度器来管理的,RTOS应用的源代码并不能完全反映多任务系统运行时的实时行为,多任务系统的实时行为还取决于任务、中断、输入和他们的相互作用。因此RTOS应用的实时行为对于开发者而言并不是非常直观的,此时就可以用到SystemView这样的RTOS可视化分析工具来帮助分析应用的实际执行过程。
SystemView是SEGGER公司开发的嵌入式系统可视化分析工具,提供了对应用程序的完整洞察,包括时间轴、CPU负载、运行时间信息、上下文运行时信息等可视化窗口,能够帮助开发者获得对应用运行时行为的深入理解。SystemView支持μC/OS-II、μC/OS-III、FreeRTOS、embOS和无OS的裸机系统。
基于对systemview的基本了解我们项目组将在STM32F401RE开发板的μC/OS-II移植中来应用Segger SystemView,包含设备端SystemView相关应用代码的添加和PC端SystemView软件的设置。
STM32F401RE开发板板载的ST-Link可以使用Seggger提供的STLinkReflash软件将固件更新为J-Link,故我们可以直接使用f401自带的stlink烧录线,而无需单独使用J-linkSystemView支持3种工作模式,持续记录模式下SystemView可以在目标程序运行时实时地记录目标执行情况,我们使用板载的J-Link调试器和SEGGER实时传输技术(RTT)来实现使用SystemView跟踪μC/OS-II。
4.1.2 根据操作系统ucosii配置SystemView文件参数
在配置 uC/OS-II 以适配 SystemView 的过程中,除了需要包含 SYSTEMVIEW 和 RTT 核心模块外,还需要将以下文件包含到应用项目中:
1.SEGGER_SYSVIEW_Config_uCOSII.c:
提供了 SystemView 所需的额外函数,并允许配置以适应目标系统。可以定义应用程序名称、目标设备和目标核心频率等。示例配置文件通常适用于大多数 Cortex-M3、Cortex-M4 和 Cortex-M7 目标。详细的配置示例可参考支持的 CPU 列表(页码 83)。
2.SEGGER_SYSVIEW_uCOSII.c 和 os_trace_events.h:
提供了 uC/OS-II 与 SystemView 之间的接口。通常情况下,这些文件不需要修改。
3.os_cfg_trace.h:
这是必需的最小化配置文件,用于 uC/OS-II 的 SystemView 跟踪。如果项目已经包含此文件,请确保其内容适合应用程序。该文件包括两个宏定义,用于配置在 SystemView 记录中管理和命名的最大任务数和最大资源数。
具体的配置如代码4-1:
代码4-1 最大任务数和最大资源数宏定义
#define TRACE_CFG_MAX_TASK 16u
#define TRACE_CFG_MAX_RESOURCES 16u

这些宏定义允许配置 SystemView 记录中管理的最大任务数和最大资源数。
  1. 启用记录
可以在 os_cfg.h 中配置 uC/OS-II 事件的记录:
将 OS_CFG_TRACE_EN 定义为 1u 以启用基本记录。
如果定义 OS_CFG_TRACE_API_ENTER_EN 为 1u,还将记录 API 函数调用。
若要在 API 函数退出时也进行记录,还需将 OS_CFG_TRACE_API_EXIT_EN 定义为 1u。
在应用程序初始化后,应在系统初始化之后调用 TRACE_INIT(),例如代码4-2:
代码4-2 TRACE_INIT()调用
BSP_Init(); /* 初始化 BSP 函数 */
CPU_Init(); /* 初始化 uC/CPU 服务 */
#if (defined(OS_CFG_TRACE_EN) && (OS_CFG_TRACE_EN > 0u))
TRACE_INIT(); /* 初始化 uC/OS-II Trace,应在系统初始化后调用 */
#endif

4.1.3 操作系统中接入SystemView接口
在main函数中添加OS_TRACE_INIT(),其定义见代码4-2。
代码4-3 OS_TRACE_INIT()函数
#define OS_TRACE_INIT() SEGGER_SYSVIEW_Conf()
void SEGGER_SYSVIEW_Conf(void) {
SEGGER_SYSVIEW_Init(SYSVIEW_TIMESTAMP_FREQ, SYSVIEW_CPU_FREQ,
&SYSVIEW_X_OS_TraceAPI, _cbSendSystemDesc);
SEGGER_SYSVIEW_SetRAMBase(SYSVIEW_RAM_BASE);
}
//#define SYSVIEW_TIMESTAMP_FREQ (CPU_TS_TmrFreqGet(&local_err))
#define SYSVIEW_TIMESTAMP_FREQ (SystemCoreClock)
// System Frequency. SystemcoreClock is used in most CMSIS compatible projects.
//#define SYSVIEW_CPU_FREQ (CPU_TS_TmrFreqGet(&local_err))
#define SYSVIEW_CPU_FREQ (SystemCoreClock)

这里将原代码中定义时使用的CPU_TS_TmrFreqGet函数替换成SystemCoreClock。
4.2 根据SystemView进行系统监控
4.2.1 系统配置和准备
1.STM32F401RE开发板准备:确保开发板上的ST-Link可以通过Segger提供的STLinkReflash工具固件更新为J-Link。确保可以使用Segger的实时传输技术(RTT)与SystemView进行通信。在pc端中下载StlinkReflash和J-link,使用reflash将ST-Link固件更新为Jlink
2.SEGGER SystemView准备:内核文件整合,配置Jlink,连接STM32
4.2.2 集成SystemView
3.添加SystemView相关代码:根据SEGGER提供的文档,将SystemView的相关代码集成到μC/OS-II项目中。这些代码的作用是在任务切换时记录任务状态和事件。下图4-1是移植完成后的文件结构:
图4-1 文件结构
各文件具体作用如下表4-1:
表4-1 各文件作用
File
Description
/Config/Global.h
SEGGER 代码的全局类型定义
/Config/SEGGER_RTT_Conf.h
SEGGER 实时传输(RTT)配置文件
/Config/SEGGER_SYSVIEW_Conf.h
SEGGER SYSTEMVIEW 配置文件
/Config/SEGGER_SYSVIEW_Config_[SYSTEM].c
[SYSTEM] 的 SystemView 初始化文件
/Sample/OS/SEGGER_SYSVIEW_[OS].c
SYSTEMVIEW 与 [OS] 之间的接口实现文件
/Sample/OS/SEGGER_SYSVIEW_[OS].h
接口的头文件
/SEGGER/SEGGER.h
SEGGER 全局类型和通用工具函数的全局头文件
/SEGGER/SEGGER_RTT.c
SEGGER RTT 模块源代码
/SEGGER/SEGGER_RTT.h
SEGGER RTT 模块头文件
/SEGGER/SEGGER_SYSVIEW.c
SEGGER SYSTEMVIEW 模块源代码
/SEGGER/SEGGER_SYSVIEW.h
SEGGER SYSTEMVIEW 模块头文件
/SEGGER/SEGGER_SYSVIEW_ConfDefaults.h
SEGGER SYSTEMVIEW 配置的默认设置
/SEGGER/SEGGER_SYSVIEW_Int.h
SEGGER SYSTEMVIEW 内部头文件
4. 配置SystemView软件
(一)PC端设置SystemView
在PC端安装和配置Segger SystemView软件。通过USB连接STM32F401RE的J-Link(通过ST-Link接口),启动SystemView并设置好与目标设备的连接,见图4-2。
图4-2 PC端设置SystemView
在主程序中调用OS_TRACE_INIT()函数进行初始化。
注意,为了在SystemView的电脑软件中观察到具体的任务,使用OSTaskNameSet为一个任务设定名称,否则在软件中只能看得到问号。
(二)连接硬件配置
我们所使用的STM32F401RE-NUCLEO开发版有板载的ST-Link,SEGGER官方提供了一个ST-LINK Reflash 工具,可以用来把ST-Link变成J-Link来用。跟着其中的教程一步步走,先安装两种Link的驱动,再使用软件将ST-Link变成J-Link。这时候发现J-Link不能直接给板子供电,必须要外接一个供电设备比如另外一个ST-Link。
(三)软件配置
打开SystemView软件,在Target中可以看到Record Configuration,在这里选择J-Link并且自己选择好设备类型,之后就可以开始记录了。
5.SystemView工作模式选择
SystemView工作模式介绍:
持续记录模式:基于J-Link调试器和SEGGER实时传输技术(RTT),SystemView可以在目标程序运行时实时地记录目标执行情况,不使用J-Link RTT技术,也可以通过串口或者TCP/IP实现。
Single-Shot模式:当目标设备不支持RTT或没有使用J-Link时,SEGGER SystemView可以用于记录数据,直到其目标缓冲区被填充满时停止记录。
Post-Mortem模式:类似Single-Shot模式,但会在缓冲区填满时覆盖旧的事件,所以,记录到的是最新的系统活动事件。这个模式可用于分析某些应用突然崩溃的问题,SystemView可以显示系统崩溃前发生的情况。
选择持续记录模式:在SystemView中选择持续记录模式,这样SystemView可以在目标程序运行时实时记录任务的执行情况和事件。
6.使用J-Link和RTT实现实时传输
使用J-Link和RTT技术:通过STM32F401RE板载的J-Link和Segger的RTT技术,SystemView能够实时地从目标设备获取数据。RTT是一种轻量级的调试和跟踪技术,通过该技术,SystemView可以高效地获取任务切换和其他系统事件的信息。下图4-3是使用Jlink与Systemview连接的系统架构设计图:
notion image
图4-3 Jlink与Systemview连接的系统架构设计图
7.观察任务切换过程
notion image
任务切换的观察:启动的μC/OS-II应用程序后,通过SystemView的时间轴和任务状态视图观察任务的切换过程。SystemView会显示任务的活动时间、延迟、优先级和切换时间等关键信息,帮助分析系统的实时行为。下图4-4是我们测试小灯交替开关的任务状态视图。
图4-4 任务状态视图
8.分析和优化
实时行为分析:利用SystemView提供的数据,分析任务调度的效率和系统响应时间。通过识别和优化任务切换延迟或不必要的中断处理,改进系统的性能和稳定性。
9. 系统整体测试
我们重构了上学期的任务,并基于此使用System view可视化工具,同时观察整个飞行器,来测试操作系统是否正常工作,验证整个系统功能的正确性和设计的合理性。
我们一共设计了五个任务和一个中断,通过它们的协同工作来实现驱动电机和传输数据,通过观察Systemview和飞行器的运行状态,来测试系统是否正常运行。
五个任务的说明如下:
1.Initialize_task:用来完成系统的初始化工作,如GPIO、TIM定时器、Systick定时器、电机等等的初始化,在完成后自动退出,因为该任务必须首先执行,以保证整个系统运行的稳定性,所以我们给该任务赋予最高的优先级prio为3;
2.GY86GET_DATA_task:用来读取GY86的数据,并将其存放到相应的内存空间中,等待其电机驱动的任务读取并使用。该任务必须优于电机驱动任务执行,以保证电机始终使用的是最新的数据,所以,我们赋予该任务较高优先级prio为4;
3.Drive_moto_task:根据GY86和接收机收到的占空比来驱动电机,由于还未涉及姿态解算的内容,暂时先由占空比的大小来驱动电机运转,该任务优先级次于第二个,prio为5;
4.Send_data_task:用于方便用户监控飞行器的运行状态,以便及时做出决策。由于发送数据没有飞行控制紧急,所以优先级我们给它较低prio为6;
5.Idle:空闲任务,当没有任务处于就绪状态时,该任务便可运行,以防止系统进入不可靠状态,优先级赋最低值prio为63.
中断TIM2_IRQHandler用于接收接收机传来的数据,通过解析ppm波得到各个通道的占空比,将其存放在内存区域中,供驱动电机任务使用。由于不知道ppm波什么时候到来,为了提高cpu的使用效率,同时保障系统响应的及时性,我们使用中断来处理接收机的数据。当检测到高电平时,立即触发中断,收集占空比数据并进行处理,供驱动电机任务使用。
五个任务和一个中断之间的协同工作情况和联系如下图所示,与上文描述相同。
notion image
图4-5 任务和中断协同工作描述图
我们将程序烧录到开发板后,使用J-Link将飞行器与PC端System View测试工具相连,同时飞行器通过电池供电,保证电机的正常运转,开始测试系统是否正常工作。
首先,观察System View软件界面,如下图所示:
notion image
图4-6 系统测试System View界面图
我们截取了系统测试时System View软件界面的一张典型图片来说明系统是正常运行的。从图中可以看出来,在SysTick时间驱动机制的推动下,通过调度程序的事件触发机制,我们创建的几个核心任务按照我们的预期正被合理的调度,系统先通过GY86GET_DATA_task获取到GY86的数据,然后结合接收机的数据,通过Drive_moto_task来驱动电机工作,最后在系统较为空闲时,通过Send_Data_task来发送数据到用户。
同时,我们从硬件上观察飞行器是否正常工作。
notion image
图4-7 GY86GET_DATA_task的测试情况图
上图是GY86的数据被正常读取后,通过OLED屏显示出来,从图中可以看出,该任务正常工作,飞行器的姿态数据正确获取,测试正常。
notion image
图4-8 接收机测试情况图
上图是接收机接收数据情况的测试图。从图中可以看出,接收机的数据被正常解析,并将部分通道高电平所持续的时间正常显示在了第二个OLED屏上,而这也证明了系统的中断功能正常运行,数据正常接收并被解析,以供驱动电机任务使用。
notion image
图4-9 系统整体运行测试图
上图是系统整体的运行测试图,通过观察和遥控器操作,飞行器电机能根据相应油门大小正常运转,说明Drive_moto_task也正常运行,同时也验证了该任务正常运行所依赖的其它核心任务和中断的正常、有序工作,整个系统运行均稳定。
综上说明,整个系统测试正常,操作系统正常稳定运行,操作系统移植成功。而经过这一系列的系统测试,我们不仅验证了各个任务和中断的协同工作能力,证明了我们在设计和实现阶段所采用的方法和策略是有效的,还确保了操作系统的稳定性和可靠性。
第五章 知识技能学习情况
在项目实践及报告撰写的过程中,我们深入探讨在嵌入式系统开发过程中所学习到的知识和技能。通过对μC/OS-II的移植,我们对内核资源互斥访问、中断设置和操作系统移植等关键技术有了更深刻的认识和理解。以下是我们在这些领域的学习和实践情况。
5.1 内核资源互斥访问
在嵌入式系统设计中,确保任务在访问共享资源时的互斥性是至关重要的。在项目实践及知识学习中,我们对临界区和信号量机制进行了深入的分析。
5.1.1 互斥机制的实现
临界区:临界区是程序中访问共享资源的代码段。由于多个任务可能同时访问这些资源,因此必须确保在任何时刻只有一个任务可以执行临界区代码。如果任务在访问共享资源时发生切换,可能会导致数据不一致或系统崩溃,具体实现可见代码5-1。
代码5-1 临界区实现--关中断方式
#if OS_CRITICAL_METHOD==1
#define OS_ENTER_CRITICAL() (Cli())
#define OS_EXIT_CRITICAL() (Sti())
#elseif OS_CRITICAL_METHOD == 2
#define OS_ENTER_CRITICAL() (PushAndCli())
#define OS_EXIT_CRITICAL() (Pop())
#elseif OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR())
#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr))
#endif
//关中断方式一
Cli
MRS R0,CPSR //将 ARM 处理器当前状态寄存器 CPSR 的值保存在 R0 中
ORR R1,R0,#0X80 //将程序当前状态寄存器 CPSR 的控制域(CPSR_c)的第7位置1
MRS CPSR_c,R1 //关中断
MOV PC,LR //返回调用该函数处
Sti MRS R0,CPSR
AND R1,R0,#0xffffffbf //将程序当前状态寄存器 CPSR 的控制域(CPSR_c) 的第 7 位 置 0
MSR CPSR_c,R1 //开中断
MOV PC,LR
//关中断方式二
PushAndCli
MSR R0,CPSR_c
STMFD sp!,R0 //把 CPSR_c 的值压入堆栈
ORR R1,R0,#0x80
MSR CPSR_c,R1 //关中断
MOV PC,LR
Pop
LDMFD sp!,R0
MSR CPSR_c,R0 //将堆栈中 CPSR_c 的值恢复到 CPSR_c 中,达到恢复调 用关中断前中断状态的目的
//开中断方式三
OSCPUSaveSR
MRS R0, CPSR //保存寄存器 CPSR 的值到寄存器 R0
ORR R1, R0, #0x80 //将寄存器 R0 与 0x80 进行或运算,并将结果保存到 寄存器 R1
MSR CPSR_c, R1 //将寄存器 R1 的值写回到 CPSR 寄存器中
MOV PC, LR //将程序计数器的值保存到链接寄存器 LR,即保存返回地址
OSCPURestoreSR
MSR CPSR_c, R0 //将寄存器 R0 的值写回到 CPSR 寄存器中
MOV PC, LR //将链接寄存器 LR 的值保存到程序计数器 PC,即恢复返回地址

为了实现互斥,可以采用以下几种方法:
1.关中断:通过禁用中断来阻止任务切换,确保临界区代码的执行不会被其他任务中断。这种方法简单但可能导致系统响应变慢,因为中断被禁用的时间过长,如代码5-2。
代码5-2 关中断
#define OS_ENTER_CRITICAL() do { Cli(); } while (0)
#define OS_EXIT_CRITICAL() do { Sti(); } while (0)

2.堆栈保存当前状态:在进入临界区之前,将当前的中断状态保存到堆栈,然后在退出临界区时恢复。这种方法可以减少中断被禁用的时间,如代码5-3。
代码5-3 堆栈保存
#define PushAndCli() do { STMFD sp!, {lr}; MSR CPSR_c, #0x80; } while (0)
#define Pop() do { LDMFD sp!, {lr}; } while (0)

3.软件中断服务例程(ISR)上下文保存:使用特定的函数来保存和恢复中断状态,这种方法适用于复杂的系统,可以更精细地控制中断状态,如代码5-4。
代码5-4 ISR
#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR())
#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr))

5.1.2 信号量机制
信号量(Semaphore)是uC/OS II中用于任务同步和互斥的一种机制。信号量由一个计数值和一个等待队列组成,可以用来控制对共享资源的访问。
二进制信号量:用于互斥,计数值只有0和1两个状态,确保只有一个任务可以访问共享资源。
计数信号量:用于同步,计数值可以大于1,允许多个任务访问共享资源。
信号量的使用:在uC/OS II中,信号量的使用通常包括创建、获取(P操作)和释放(V操作),如代码5-5:
代码5-5 PV操作
OS_SEM sem;
OS_SEM_Create(&sem, 1); // 创建一个初始值为1的信号量
void task1(void) {
while (1) {
OS_SEM_Pend(&sem, 0, &err); // 等待信号量,阻塞时间0
// 临界区:访问共享资源
OS_SEM_Post(&sem); // 释放信号量
}
}

5.2 中断设置
通过深入理解中断向量表的重定位技术和ARM架构的异常处理特性,我们能更有效地编写和优化嵌入式系统的中断和异常处理代码,从而提高系统的稳定性。
5.2.1 中断向量表的重定位技术
中断向量表是嵌入式系统中用于响应中断请求的跳转地址集合。在STM32F4这类基于ARM Cortex-M4的处理器中,中断向量表通常位于flash的起始位置。然而,出于性能或组织结构的考虑,开发者可能需要将中断向量表重定位到其他内存区域。
1.重定位原因:重定位中断向量表可以减少中断响应时间,因为它允许中断服务例程(ISR)更接近于缓存,从而加快访问速度。此外,重定位还可以帮助隔离中断服务代码,避免flash磨损。
2.重定位步骤
使用STM32F4的系统控制块(SCB)中的向量表偏移寄存器(VTOR)来指定新的中断向量表位置。
确保新的向量表地址符合STM32F4的对齐要求。由于使用了Thumb指令集,向量表的地址必须是4字节对齐的。
  1. 代码示例
代码5-6 中断向量表重定位
// 假设新的中断向量表位于RAM中的某个位置
#define NEW_INTERRUPT_VECTOR_TABLE_ADDRESS 0x20000000
// 重定位中断向量表
void RelocateInterruptVectorTable() {
SCB->VTOR = NEW_INTERRUPT_VECTOR_TABLE_ADDRESS & SCB_VTOR_TBLOFF_Msk;
}

4.注意事项:在重定位过程中,需要确保所有中断服务例程的入口地址都是正确的,并且新的向量表能够正确地被处理器识别和使用。
5.2.2 异常处理的ARM架构特性
ARM架构的异常处理机制是其核心特性之一,它定义了处理器如何响应各种异常情况,如中断、复位、未定义指令等。
1.异常类型:ARM定义了多种异常类型,包括IRQ(中断请求)、FIQ(快速中断请求)、Abort(数据/指令访问中止)等。
2.异常处理流程
保存现场:当异常发生时,处理器会自动保存当前的寄存器状态到相应的堆栈中。
调用异常处理函数:处理器会跳转到对应的异常处理函数,如中断服务例程。
恢复现场:异常处理完成后,处理器会从堆栈中恢复寄存器状态,继续执行被中断的代码。
3.异常处理函数编写
使用ARM的AAPCS(应用程序二进制接口)标准编写异常处理函数,确保寄存器的使用和保存符合规定。
利用__asm__块来编写汇编代码,直接操作CPSR等特殊寄存器。
4.代码示例
代码5-7 异常处理
attribute((naked, noreturn, used))
void HardFault_Handler(void) {
asm volatile (
"mrs r0, msp\n\t" // 将主堆栈指针保存到r0
"b HardFault_Handler_C" // 跳转到C语言编写的处理函数
);
}
// C语言编写的HardFault处理函数
void HardFault_Handler_C(unsigned int *hardfault_args) {
// 处理硬故障异常
// hardfault_args[0]是r0的值,即主堆栈指针
}

5.注意事项:在编写异常处理代码时,需要特别注意寄存器的保存和恢复,以及异常处理函数的入口属性(如naked和noreturn),以避免破坏现场或产生不可预测的行为。
5.3 其他
1.加深对裸板驱动开发与STM32结构理解
在编写裸板驱动之前,我们首先更加熟悉了STM32微控制器的架构,包括其内核、存储器映射、总线接口以及外设。在裸板启动过程中,我们也编写了初始化代码,包括堆栈的设置、中断向量表的配置以及必要的外设初始化。
2.深刻理解μC/OS-II操作系统移植与任务管理
在移植μC/OS-II操作系统的过程中,我们深入理解了操作系统的内核机制,包括任务调度、上下文切换和中断处理。例如,实现了OSCtxSw()函数,该函数负责保存当前任务的上下文,并恢复下一个任务的执行状态,通过该函数的实现,我们更清晰地理解了操作系统任务调度的基本原理。
通过编写任务创建和调度的代码,学习了如何管理多任务环境。例如,我们创建了多个具有不同优先级的任务,并观察了它们在μC/OS-II中的调度顺序。
我们还学习了如何响应外部中断和系统异常。例如,配置一个定时器中断,并在中断服务例程中更新了系统时间,这让我们更明确地理解了中断响应对实时系统的重要性。
3.巩固ARM编程基础与C语言高级应用
在操作系统的编写过程中,我们更加熟悉地运用了ARM处理器的体系结构知识,包括其寄存器、指令集和异常处理机制。如使用ARM汇编语言编写了中断服务例程,直接操作CPSR寄存器来管理中断使能和优先级。
在C语言编程模块中,我们深入应用了指针和链表等高级特性,我们可以实现一个动态内存管理模块,使用链表来跟踪内存分配情况,这不仅增强了内存管理能力,也加深了对C语言指针操作的理解。
第六章 分工协作与交流情况
6.1 任务分工
表6-1 任务分工
序号
姓名
学号
分工
完成情况
1
李晨熙
2022090909003
代码编写、报告撰写
完成
2
蔡良伟
2022090916012
测试、报告撰写
完成
3
程尧
2022090916010
代码编写、报告撰写
完成
4
丁嘉茵
2022090901023
代码编写、报告撰写
完成
5
张方宇
2022090914021
测试、报告撰写
完成
6.2 交流情况
本学期,我们小组坚持每周至少三到四次的线下见面交流。这种面对面的沟通方式可以即时解决疑惑,增进团队成员间的理解和信任。为了确保代码的一致性和追踪历史更改,我们使用Git作为我们的版本控制系统。这不仅帮助我们管理代码的迭代,也方便了团队成员之间的协作。
在遇到问题时,我们鼓励团队成员相互交流和讨论,共同寻找解决方案。这种开放的讨论氛围促进了知识的共享和团队成员之间的相互学习。当遇到难以攻克的技术难题时,我们会向工作室的学长求助,他们的经验和知识为我们提供了宝贵的指导和支持。为了确保信息的透明和可追溯性,我们详细记录了每次会议的讨论内容和决策结果,这些文档成为了我们项目的重要资产。
第七章 项目总结
7.1 总结与感悟
本学期专业课程密集,专修了嵌入式操作系统课程和ARM处理器及其应用课程,也是硬核程度大大提高了。而综合设计课程项目也是在众多专业性课程中更加强调实操及实践,本学期虽是艰难险阻、道阻且长,却也是按时按需地达到了预期及目标的规划,可谓是紧张刺激之余而皆大欢喜。
相较于上学期的综合设计项目I而言,本次综设II也有了许多的改进之处及更深刻的领悟:
  • “To learn by doing."
实践导向的学习模式成为了我们的主要学习策略。我们根据实际项目需求去驱动理论知识的探索和学习,再将这些理论应用于实践之中,用理论来推动实践,而实践的经验由反哺理论的研究,形成了一个良性的互动循环。这种由实际需求驱动的学习方式,极大地提高了我们的学习效率和质量。
  • 计算机系统结构思维
我们学会了从宏观的角度审视计算机系统结构,理解各个组件之间的相互联系和影响。“牵一发而动全身”,从整体的视角来设计、实现、修改系统中的各个模块才能提升系统总体上的性能与效能。这种全局视角使我们在设计和实现系统时能够考虑到整体的协调性和一致性,从而提高系统的性能和稳定性。
  • 时间管理
在项目进行过程中,我们意识到了抓住项目“黄金期”的重要性。我们学会了如何合理分配时间,既要保证项目的顺利进行,又要确保课内课程学习的质量。我们相信这种平衡艺术对于我们未来的职业生涯,同样也是至关重要的。
  • 慢就是快
我们认识到,追求高效率和高质量的工作成果,往往需要我们沉下心来,细致入微地研究问题。这种“慢工出细活”的态度及深思熟虑的工作节奏,使我们在面对复杂问题时能够更加从容不迫,从而找到更为精准和有效的解决方案。
7.2 未来展望
μC/OS-II的学习让我们不禁对市场上常见的嵌入式操作系统产生了好奇心。经过仔细调研,我们发现NuttX这款嵌入式操作系统十分引入瞩目。它是一个能烧录在STM32上的嵌入式操作系统,而其操作逻辑与Linux几乎一致——兼容POSIX-II标准、具备标准的Shell,具备极高的裁剪自由度。我们可以自由的在该操作系统上实现TCP/IP网络协议栈、USB驱动、文件读写等等功能,其架构图如下图7-1所示。我们规划在暑假进行μC/OS-II,实现类似Nuttx的功能。
notion image
图7-1 NuttX操作系统
参考文献
[1]HMC5883L中文规格书[J].Honeywell
[2]廖勇等.嵌入式操作系统[M].北京:高等教育出版社.2017.
[3]STM32F4xx中文参考手册[J].意法半导体
[4]STM32 F407最小系统板开发指南-寄存器版本[J].意法半导体
[5]STM32F4开发指南-寄存器版本[J].正点原子
[6]User manual f401re [J].意法半导体
[7]Datasheet f401re [J].意法半导体
[8]Reference manual f401re [J].意法半导体
[9]Roger S.Pressman等.SoftwareEngineering[M].北京:机械工业出版社.2021.
[10]SystemView Recording and analyzing runtime behavior of embedded systems User Guide [J].SEGGER
致谢
在廖勇导师的精心辅导和殷切关怀下,我们的项目得以圆满完成。廖老师的教学风和蔼可亲而富有洞见,他的课堂充满了探究性,让我们深刻领悟到了“知行合一”、“学以致用”的教学理念。在廖老师的引领下,我们不仅掌握了嵌入式操作系统的基础知识,更学会了如何将所学知识融会贯通,并通过实践来加深理解。我们学会了如何清晰地表达自己的思想,如何通过理论学习进而通过实践来深化理解,以及如何撰写详尽的项目报告。这些技能的提升,让我们对计算机系统架构有了更加深入的认识和理解。
学长们在系统构建、方案推敲、代码实现等关键环节中,也慷慨分享了他们的真知灼见,并对我们的工作提出了诸多富有启发性的建议,让我们在项目实施过程中受益匪浅。此外,其他小组也在课堂知识分享中也为我们点拨迷津,提供了慷慨的帮助和支持。在这个报告圆满完成的时刻,我们向每一位提供帮助的人表达最深切的感激之情和崇高的敬意!
四轴 I——硬件子系统及PCB设计四轴III——飞控算法
Loading...