type
slug
status
summary
icon
category
date
tags
password
在使用SystemView之后发现的一些问题和学习的地方
1 Keil的调试
1.1 知识点

从左往右边依次介绍
- Reset:复位
- Run:
- step in:进入函数里面,一行行走
- step over:一行行走,但是不进入函数内部(一口气运行完)
- step out:跳出这个函数
- 最后那个是执行到光标所在的位置
- 设置断点之后,run就会停止在断点处
鼠标放在C函数窗口,就是一行行C代码运行;鼠标放在汇编窗口,那么就是一行行汇编指令运行。
查看变量的值

在这里也可以看到外设

断点的高级调试

这个窗口的使用
- 可以看到当前已经设置的断点
- 在main.c的176行,0x08001F70是那条指令的地址,E代表执行断点
- \\表示工程根目录
- 也可以把想在某个函数停下来的函数名复制下来,放入Expression,然后Define

- 也可以使用汇编地址打断点,放入Expression,然后Define

- 在一个for循环里面如果想停止在中间
- 那么设置那个count的值
- 一个非常详细的博客


1.2 实例
进入main.c
然后进入OSInit()
在OSInitHookBegin里面我们进行了,异常栈的建立初始化,以及优先级屏蔽边界的设置
然后在OSInit()中进行了IDLE和Stat任务的初始化
然后进入创建任务,任务命名,传给systemview的函数内部

其中我们定义了
便进入这两个汇编函数

然后进行为任务分配栈空间
在这里我们可以知道
- 任务0
- 任务栈指针为 0x20001B34
- TCB指针为0x2000BCC
- 任务1
- 任务栈指针为 0x20002334 0x200022F8
- TCB指针为0x2000B70
进入OSStart

此时OSRunning = 0x00
然后到OS_SchedNew()寻找就绪队列中优先级最高的任务

因为我们创建了两个任务,一个任务的优先级是9,一个任务的优先级是10.所以最高优先级是9,第0行为0~7,第1行为8~15.(9/8=1),所以y=1,OSPrioHighRdy=9


这个任务的TCB指针


第一个问题出现了:OSTCBStkPtr不应该是和OSTCBHighRdy的值一样吗?
一个是指向TCB的指针,是TCB的首地址,一个是任务栈的栈指针SP,在最低。。。。这个后面再看一下是怎么个回事
然后进入OSStartHighRdy().是0x0800296

这里我们也可以看到流水线PC和LR

感觉很奇怪这里差了29个字节
进入这个函数后

PC指向这个函数的首地址,LR存放返回地址,

进入OSStartHighRdy
- PendSV优先级设置
- PSP清零
- 标记OS开始运行

- 同步指令流使用PSP


这里是因为最开始的时候TCBCur=TCBHighRdy,在OSStart中已经实现
R2存放的是OSTCBHighRdy的值,即当前TCB的地址
PSP指向当前任务栈stack

然后从任务栈恢复上下文R4~R11,LR

R1存放的是任务0的地址,执行任务0

执行OSTimeDly,将任务移出就绪队列,优先级位图各位清0
然后进入OS_Sched(),
- 执行OS_SchedNew()寻找当前任务优先级最高的任务。此时OSPrioHighRdy已经变成了10(0x0A)

- 修改最高优先级任务的指针

- 进入OSCtxSw

- 使能PendSV后,退出OS_Sched,硬件自动跳转PendSV_Handler,然后跳转到OS_CPU_PendSVHandler执行任务切换
设置中断屏蔽优先级为0x40
在这里我们可以知道
- 任务0
- 任务栈指针为 0x20001B34
- TCB指针为0x2000BCC
- 任务1
- 任务栈指针为 0x20002334
- TCB指针为0x2000B70
- Core
- PSP:0x200022F8
- MSP:0x20000470
- 当前使用的是MSP模式下的R4~R11,LR
旧任务的SP=0STCBCurStkPtr=0x00022D4

然后将LR存入R4,因为一会会被覆盖掉,这里的LR是旧任务的上下文
然后调用OSTaskSwHook函数
然后正常的切换优先级和指针

现在是在中断模式
R2存放的是0X20000B70,由上面可知,这个是任务1TCB的地址
则恢复了新任务的上下文

根据0xFFFFFFED返回线程栈,进入OS_TaskStat

然后又通过OSSched进入OS_TaskIdle
1.3 发现的问题
systick并没有执行,所以tick—也没有执行,所以这两个任务一旦被挂起就会一直在等待队列里面
pendsv究竟哪里出现了问题
这个出现的TASK 0XAB8究竟是什么
2 PendSV再探
我的PendSV
这个是官方的PendSV
编译通过不报错版本
找不到那个出错的版本了
大概就是
这两行的位置放错了,导致LR值错误最后恢复的时候并不能跳转到PSP栈中,因为这个LR是旧任务的EXC_RETURN,所以一定要在旧任务中存放。

3 Systick再探
目前遇到的问题就是Systick这个任务在systemview里面没有运行
下面是汇编实现
我们的依然是正确的
3.1 解决Systemview里面Systick的显示问题

- 解决上面那个问题:
- 在
SysTick_Handler()
中添加 SystemView 跟踪 - 额外建议:你也可以在
OSTimeTick()
开头加一条:SEGGER_SYSVIEW_Print("OSTimeTick Called");
能帮你在 SystemView 事件窗口中确认时钟节拍确实在运行。
如果我想在systemview中看到PendSv的执行过程
手动通知进入中断,如上会导致卡在pendsv无法实现上下文切换
原因如下:
SystemView 本质上是一个调试工具,用于记录中断、任务切换等事件,但它本身依赖堆栈、全局变量、临界区保护、甚至可能调用 C 函数,而这些行为在 PendSV 中断中是非常危险的


3.2 Systick和任务切换
再复习一下systick和任务切换的关系
SysTick 是 uC/OS-II 的节拍器,决定“时间片”的长短;而 PendSV 是切换任务时真正用来“搬运行李”(保存和恢复上下文)的工具。
3.2.1 Systick_Handler
的作用是什么?
- 是一个定时中断,用于定时产生“时钟节拍”
- uC/OS-II 是一个 时钟驱动的实时操作系统,所有的任务延时、任务调度、时间片轮转都需要靠“时钟滴答”推动。
- 这个“滴答”由 SysTick 定时器触发:每过
1ms
(或你配置的时间),就触发一次中断。
3.2.2 OSTimeTick()
做了什么?
详情见EOS Chapter2
- 所有调用了
OSTimeDly()
的任务,会有一个延时计数器;
OSTimeTick()
会每毫秒调用一次,检查哪个任务延时到期了;
- 如果有更高优先级任务到期恢复,就会标记
OSIntExit()
中需要任务切换。
这里贴一个总结版本
OSTimeTick
是在Systick_Handler/OSTickISR里面被调用,也就是每次系统时钟中断的时候会调用这个函数进行核心的中断服务处理,它会记录中断次数到一个全局变量OSTime,并且遍历OSTcbList
(也就是使用的TCB链表),对于每个TCB去判定是否OSTCBDly
非0,非0说明被阻塞延时了。OSTCBDly
表示仍然需要等待的tick数,这个时候需要更新它(-1操作),然后进一步判断此时是否OSTCBDly
为0,为0说明等待时间到达,如果达到并且任务没有被suspend
或者没有等待某个事件,那么就把该任务放回就绪队列。最后在整个中断服务程序(Systick_Handler/OSTickISR)返回之前,会调用UC的
OSIntExit()
函数更新OSIntNesting
以及进行可能的任务重调度(OSIntCtxSw
)。
3.2.3 OSIntExit()
是关键切换点

处理完中断嵌套之后,会进行任务切换(如果当前有更高优先级的任务就绪)
在OSIntCtxSw()中使能PendSV,即触发PendSV中断,进入PendSV切换上下文
3.2.4 SysTick_Init(uint32_t SYSCLK)
SysTick 每计数 84000 次就进入一次中断,刚好是 1 ms一次中断
最终形成的中断频率关系
项目 | 值 |
主频(HCLK) | 84,000,000 Hz |
OS 每秒节拍次数 | 1,000(即 1ms 一次tick) |
每次中断需多少个 tick | 84,000 个 SysTick tick |
最终中断频率 | 每 1ms 触发一次 |
SysTick_Handler 调用频率 | 每 1ms 一次 |
这些设置和OSTimeDly的关系,以我们的任务为例子
OSTimeDly
- OSTimeDly的作用是将当前任务移除就绪队列,同时赋值OSTCBDly=ticks,并进行任务切换。
整个的流程示意图
这里让我混乱的问题是:两个tick的意思和换算规律完全不一样啊
“tick”是一个抽象时间单位,不同地方的“tick”可以指不同的“时间粒度”,但它们最终都建立在SysTick硬件定时器的中断节拍之上。
这两个地方的tick
名称 | tick 的含义 | 换算单位 | 举例 |
SysTick 的 tick | CPU时钟周期的基本单位 | 1 tick = 1/SYSCLK 秒 | SYSCLK = 84MHz,则 tick = 11.9ns |
OS 的 tick(节拍中断) | 操作系统调度的时间粒度 | 1 tick = 1/OS_TICKS_PER_SEC 秒 | OS_TICKS_PER_SEC=1000,则 OS tick = 1ms |
层次 | 名称 | 单位 | tick的意义 |
硬件 | SysTick定时器 | 1 tick = 1/SYSCLK 秒 | CPU计数用,计到0发中断 |
软件/OS | OS tick | 1 tick = 1/OS_TICKS_PER_SEC 秒 | OS调度/延时用,一般设置成 1ms |
🔁 他们的关系:SysTick 的 tick 是硬件计数单位,OS 的 tick 是软件节拍单位
- 每数84000个硬件tick,触发一次Systick_Handler中断。
- OS_TICKS_PER_SEC=1000,1 tick = 1/OS_TICKS_PER_SEC=1ms,OSTimeDly(ticks)即延时ticks毫秒。
- 这里的设置是操作系统调度与底层硬件联动的关键
- 为什么要用
cnts = SYSCLK / OS_TICKS_PER_SEC
来统一硬件 SysTick 和软件 OS tick?这种“统一”到底是为了什么?
为了让操作系统能以“人类设定的时间节奏”(如 1ms)进行调度、延时和统计等功能,而不需要关心底层CPU跑多快。
层次 | 原始情况 | OS期望 | 解决方式 |
硬件(SysTick) | 每个 tick 是 CPU周期,非常快,比如 1/84,000,000 秒 | OS 不关心 CPU 频率,它只希望 “每 1ms 触发一次中断” | 所以我们要计算出:多少个硬件tick == 1ms |
软件(OS调度器) | 延时、任务调度要有统一节奏 | 希望统一成“tick单位”,比如 OSTimeDly(5) 就是延时5个tick | 每 tick == 1ms(由你代码设定) |
- 让 SysTick 定时器 精准地每隔 1ms 产生一次中断,每次中断调用一次OSTimeTick(),在OSTimeTick()中遍历
OSTcbList
(也就是使用的TCB链表),对于每个TCB去判定是否OSTCBDly
非0,非0说明被阻塞延时了。
OSTCBDly
表示仍然需要等待的tick数,这个时候需要更新它(-1操作),然后进一步判断此时是否OSTCBDly
为0,为0说明等待时间到达,如果达到并且任务没有被suspend
或者没有等待某个事件,那么就把该任务放回就绪队列。
- 最后在整个中断服务程序(Systick_Handler/OSTickISR)返回之前,会调用UC的
OSIntExit()
函数更新OSIntNesting
以及进行可能的任务重调度(OSIntCtxSw
)
3.2.5 两次任务切换
- 第一次任务切换——任务主动调用OSTimeDly
my_task_0_t_()
运行中调用OSTimeDly(1000)
- 在
OSTimeDly()
中会实现 - 设置
OSTCBCur->OSTCBDly = 1000
- 当前任务的从就绪队列中清除,设置为挂起态/延时态
- 调用
OS_Sched()
触发一次任务调度,因为OS_Sched里面有OS_TASK_SW()(即OSCtxSw()); OS_Sched()
找到就绪任务中优先级最高的(例如my_task_1_t_()
)去运行- 触发PendSV异常 → 进入
PendSV_Handler
→ 完成任务上下文切换
这是由任务主动让出CPU触发的一次切换。
- 第二次任务切换 —— 由SysTick中断触发的
OSTimeTick()
、OSIntExit()
调用 - 1ms 后,SysTick 定时器产生一次中断,进入
SysTick_Handler()
,调用OSTimeTick - 在
OSTimeTick()
中会实现 - 遍历所有处于挂起(延时)状态的任务 TCB
- 对
OSTCBDly
不为0的任务执行--
操作 - 如果
OSTCBDly == 0
了(比如我们的任务刚好延时完1000个tick) - 将其状态从挂起态 → 变为就绪态(优先级位图的对应位置1)
- 在
OSIntExit()
中会实现 - 并检查当前任务是否是优先级更高的任务(因为当前任务肯定是上个周期里面优先级最高的任务,所以实际上最本质是比较刚刚移入就绪队列里面的任务与当前任务的优先级)
- 如果当前任务并不是优先级最高的任务,选择优先级最高的任务,进入OSIntCtxSw,触发PendSV进行任务切换。
- OSIntExit源码中有个OSSchedNew()。这个并没有实现任务的切换,而是实现优先级位图等成员的一些列操作

这是系统时钟自动唤醒任务并调度触发的切换。
调用
OSTimeDly(1000)
后,当前任务立即挂起,调度其他任务运行。而 1ms 的 SysTick 中断会驱动 OSTimeTick()
和OSIntExit()
每次检查和唤醒延时结束的任务和再次触发调度。

4 Systemview再探

0xAB4是Idle任务的起始,0xAB8就是Idle任务
5 任务设计
5.0 一些需要解决的问题
信号量究竟是什么?
串口为什么需要重构?
轮询or中断使用串口?
5.1 信号量机制
在uCOSII中,信号量属于事件,信号量机制主要有三个部分
- 创建信号量
- 在uC/OSⅡ中,信号量算是一个事件,所以创建信号量时需要从空件队列中取出一个ECB(EventControlBlock)。一个ECB 包含的信息有:
- OSEventType:表示当前时间的类型,可以是信号量等
- OSEventPtr:用于管理事件队列的变量,ECB队列与TCB队列相似,也是一个链表
- OSEventCnt:在使用信号量时,这个成员表示信号量的值,也就资源的数量。
- OSEventGrp:在优先级位图法中使用,用于表示在当前事件上的阻塞队列。
- OSEventTbl:在优先级位图法中使用,用于表示在当前事件上的阻塞队列。
- 申请信号量
- 在uCOSⅡ中,申请信号量的主要流程为:先判断当前有无资源可以用,如有资源可以使用,则执行OSEventCnt--;若没有资源可以使用,则需更新当前尝试获取信号量的任务的状态,即将任务状态从就绪状态转换为阻塞状态,如设置任务的状态,任务最大延时时间等。
- 任务最大延时时间可以在申请信号量时进行设置。通过设置任务最大延时间,可以有效避免任务无休止地等待下去。如果一个任务的最大延时时间的值大于0,那么该任务就将一直等待直到可以获取对应信号量或者等待超时:如果一个任务的最大延时时间的值为0,该任务将一直等待下去直至可以获取对应的信号量。
- 而关于任务状态的变换,则涉及到优先级位图相关的操作。也就是将当前申请信号量的任务从就绪优先级位图中移除,并放入当前信号量的阻塞优先级位图中。
- 释放信号量
- 在释放信号量时,释放信号量的任务有义务去唤醒一个阻塞在当前信号量上的任务。因此在释放信号量时,只需要从当前事件的阻塞队列中选取出优先级最高的任务,然后将其重新放入就绪队列,并将其从阻塞队列中移除。在更新就绪优先级位图以及当前事件的阻塞优先级位图后,需要执行重调度。
5.2 中断机制
实际上单单依靠中断是无法驱动操作系统的任务调度的,因为任务本身是无法感知到中断是否发生的,所以如果需要使用中断驱动操作系统的任务调度,一方面需要在中断退出中进行重调度,另一方面必须在中断处理程序中进行相关的操作实现相关任务状态的转换,使相关任务进入就绪队列。而要在中断中实现任务状态的变化,就需要在中断处理程序中使用信号量等机制。
在进阶式挑战性综合项目II中,我们使用的中断主要有两个(不考虑SysTick 中断,因为其属于时间驱动机制):
- 串口接收中断
- 在串口接收中断中,当接收到一条完整的数据时,需要释放相关的资源信号量以实现串口接收任务从阻塞状态转换为就绪状态。进而实现中断形式的事件驱动机制。
- PPM 输入捕获中断
- 在PPM的输入捕获中断中,同样需要在捕获到一个完整的PPM波时释放相关的信号量,以唤醒电机占空比调节任务进行电机转速的调节。进而实现中断形式的事件驱动机制。
5.3 姿态传感器GY-86任务设计
5.4 蓝牙(Bluetooth)驱动任务设计
使用USART2实现蓝牙串口的发生和接受功能。
5.4.1 串口发送设计
由于在本阶段进阶式挑战性综合项目中拥有操作系统的支持,所以将出现多个任务需要使用串口进行数据发送的情况。故之前的直接重写系统调用write,并直接使用print进行串口发送的形式在当前并发的运行环境下就不可用了。这是因为printf函数本身并不是线程安全的,可能当一个任务输出数据输出一半被打断,然后另一个任务恰好要输出数据,这个时候就会出现打印信息交错的情况;更微观一点,可能在串口数据寄存器中的数据还没有转移到移位寄存器中时就被打断,这个时候甚至可能出现数据的丢失。因此需要对串口相关代码进行重构,以保证串口发送的线程安全。
分析出现上述问题的原因,不难发现应该是由于多个任务都想要对临界资源进行访问。
因此在重构串口发送时,需要保证每一个任务都能够使得当前需要传送的所有字符都成功通过串口传送或者全部放到共享内存中后才能允许下一个任务进入。即需要实现任务对串口外设临界资源的互斥访问。
实现串口发送的互斥有两种方式:
- 任务形式
- 在任务形式下,所有需要通过串口发送数据的任务都互斥地将数据存放在一个共享数组中,并使用一个周期性任务对共享内存中读取数据并通过串口进行发送。因此如果使用任务形式必然需要使用共享内存。
- 关于任务形式的开销,主要有以下几点
- 空间消耗较大。使用任务形式实现串口的发送需要创建一个任务,还需要创建一个共享内存,对空间的消耗较大。
- 任务切换较多。由于新建了一个周期性任务,任务切换的频率应该会变高,进而将使得指令执行的流水线将在一定程度上被打乱。
- 实时性较差。由于使用的是共享内存机制,当前的任务可能已经将数据放到共享内存处了,但是串口发送任务如果周期性执行的频率较低,就不会立即传送出该数据。这对监视姿态数据等对实时性要求较高的场景是不能忍受的。
- 复杂度较高。使用任务形式是一个多生产者单消费者的问题,相关信号量的使用比较复杂。
- 纯信号量形式
- 纯信号量形式单纯使用信号量来实现互斥。主要的逻辑就是,重新封装printf函数。当有任务需要使用pintf函数的时候就申请信号量,printf函数结束之后再释放互斥信号量。这种情况下没有使用共享内存,而是直接使用出将数据传送出去。当然也可以理解为将串目的数据器存器当成了一个共享内存。
- 关于纯信号量形式的开销,有如下几点:
- 空间开销低。纯信号量形式不需要使用额外的共享内存空间,因此空间的开销较低。
- 切换成本降低。使用纯信号量机制在能获得信号量时只是单纯的函数切换,而不是任务切换,切换成本降低数据实时性较高。
- 数据实时性较高。使用纯信号量机制进行串口发送时,只要串口空闲,就可以进行发送,故使用纯信号量机制时的串口发送数据的实时性更高。
- 实现较为简单。使用纯信号量机制进行串口发送时,只需要对每一个Printf都加上信号量相关的操作即可,其实现相较于任务形式的多生产者单消费者更加简单。
综合以上的考虑,发送数据选择使用纯信号量的形式,以实现printf的互斥关系。
下面是串口发送数据任务的流程:
另外,由于考虑到user fiendly等因素,我们希望有关信号量的操作不需要由串口发送函数的使用者实现,故需要对printf函数以及信号量的相关操作进行封装,以便串口发送函数的使用者可以像使用printf一样使用串口发送函数。
5.4.2 串口接收任务设计
串口接收数据主要是接收从上位机传来的信息,并且接收数据是使用中断实现的,即通过中断形式的事件驱动机制。
在uCOSII中,虽然不能在中断中获取一个信号量,但uCOS却允许在中断中去释放一个信号量。在中断中直接释放一个信号量跟一般情况下释放一个信号量唯一的区别就是在信号量释放之后有没有执行任务的调度,因为有中断嵌套的情况下uC/OSⅡ不允许调度,但是在中断退出的时候会执行中断退出函数,在中断退出函数中就会实现任务调度。
此外,由于接收数据一定需要使用中断,因此无法采用纯信号量限制ISR的执行,故必须采用共享内存的方式实现ISR与普通任务之间的通信。即中服务程序将接收到的一条完整数据存放在共享内存处,并释放一个信号量告知任务已接收到数据以实现同步。
下面的图是:
图2-21串口接收任务流程图
另外由于uC/OSII是允许中断嵌套的,所以在中断服务程序中需要设置临界区,防止中断在访问共享资源的时候被其他中断打断造成共享区域不互斥的情况。
6 任务实现
6.1 uCOSII的移植
6.2 姿态传感器GY86任务实现
6.3 蓝牙(Bluetooth)串口任务实现
6.3.2 串口发送实现
根据对串口发送模块的设计,串口发送将使用纯信号量机制实现。并且需要对信号量相关操作进行封装,并暴露给调用者如printf的函数接口。
6.3.3 串口接收实现
- 作者:🐟🐟
- 链接:https://www.imyuyu.top//article/Quadcopter2-6-1
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。