type
slug
status
summary
icon
category
date
tags
password
0 对ucosii进入临界区的三种方法的研究
- 什么是临界区: 临界区指的是程序中对共享资源(如共享变量、数据结构、硬件资源等)的访问片段该区域内的代码需要以互斥(mutual exclusion)的方式执行,确保在任何时刻只有一个执行单元(如线程、进程或中断服务程序)在访问这些资源。
- 为什么要保护临界区:
- 防止竞态条件:当多个执行单元同时进入临界区而不加限制时,它们对共享资源的操作可能互相干扰,导致程序的行为依赖于执行顺序的不确定性,从而引发竞态条件,最终导致数据错误或程序不稳定。
- 保证数据一致性:通过保护临界区,可以确保共享数据在被修改过程中不会被其他执行单元同时访问,从而避免数据破坏或不一致性问题,保持数据状态的正确性。
方法一
进入临界区时直接调用一个指令来禁用所有中断,退出时则直接使能中断。
- cli: Clear Interrupt Flag
- sti: Set Interrupt Flag
- CPSID I[关中断,等效于x86 的 cli(Clear Interrupt Flag)](Change ProcessorState, Interrupt Disable)
- CPSIE I[开中断,等效于x86 的 sti(Set Interrupt Flag)](CPSIE: ChangeProcessor State, Interrupt Enable )

- 问题(1):无法满足嵌套的情况。如果有两层临界区保护,在退出内层临界区时就会开中断,使外层的临界区也失去保护。
- 举例说明
- 在function B中提前开始了中断,导致function A可能会受到外界影响。

- 问题(2):可能导致系统延迟变大。 cpsid 直接关闭所有中断,会导致高优先级中断(如时钟中断)无法执行,如果临界区代码执行时间过长,会影响系统调度,导致任务响应变慢

方法二
在关中断前保存之前的标志寄存器内容到栈中,退出临界区的时候从栈中恢复之前保存的状态。
- PRIMASK是一个1位宽度的寄存器,用于控制处理器是否响应外部中断。当PRIMASK的值为1时,处理器会屏蔽所有可屏蔽的异常中断,仅保留不可屏蔽中断(NMI)和硬件错误(HardFault)的响应能力。
- 问题:
- 若在临界区中调用了tick会产生什么影响?
- 此时由primask状态,tick被屏蔽,会时钟混乱,系统的实时性受到影响
- 与编译器有关:一些编译器不能很好地优化内联代码,无法知道堆栈指针已经被更改,此时再想去访问堆栈中的数据,极大可能出现错误的值,从而导致应用程序的崩溃。
- 手动压栈时会出现问题,导致栈没有办法还原
- 在函数调用函数,参数过多,使用栈传递参数时,便会出现问题。 现场演示压栈过程有:例如primask存到R0中,若传参过多,则R0也将被使用
- 代码演示:



- 出现问题! 编译器错误地假设某些局部变量或返回地址仍然位于预期的堆栈位置,但由于堆栈指针已被手动修改,实际位置已发生变化,导致访问错误的数据或返回地址。
- 而在参数不多,不需要通过栈来传递参数时,则没有这种问题.
方法三
使用变量保存CPSR寄存器(这样与第二种方式几乎没有区别,只不过是将CPSR寄存器的值存放在函数的局部变量区域)
在关中断前,使用局部变量保存中断状态:
cpu_sr是自己定义的变量,用来保存中断状态。
- OSCPUSaveSR():保存当前CPU的状态,返回值cpu_sr:用于存储当前CPU状态
- OSCPURestoreSR(cpu_sr):恢复之前CPU的状态
- 支持嵌套,也没有方法二中的编译器优化问题
ucos是一朵奇葩,为了兼容前两种方式,OSENTERCRITICALO/OS EXIT CRITICALO宏定义并没有提供传递状态参数的功能。所以它的临界去必须这么用:
知识点补充
- 任务切换
- 任务切换指的是操作系统从当前运行的任务切换到另一个任务,以实现多任务并发。在 uC/OS-Ⅱ中,任务切换一般通过中断驱动,由以下事件触发:
- 时钟中断(SysTickTimer)→触发时间片轮转和任务调度
- 外部中断(EXTI)→可能会引起高优先级任务抢占低优先级任务
- 任务主动让出CPU(如 osTimeDly()、osMutexPend())。
- 任务间通信事件(如ossemPend()等信号量操作)。
1 时钟配置
1.1 时钟树


- 三种不同的时钟源可用于驱动系统时钟(SYSCLK)
- HSI 振荡器时钟
- HSE 振荡器时钟
- PLL(PLL)时钟
- 两个辅助时钟源
- 32 kHz 低速内部 RC(LSI RC),用于驱动独立看门狗,可选地,用于从停止/待机
- 32.768 kHz 低速外部晶振,可选择驱动 RTC 时钟(RTCCLK)
- 每个时钟源可以在不使用时独立打开或关闭,以优化耗电量。
- 一些讲解:



1.2 初始化时钟树的步骤总览



1.3 相关寄存器介绍
RCC_CR
- 作用:启动外部时钟源

RCC_PLLCFGR

- PLL(锁相环)可以将输入时钟进行倍频或分频等操作。通过配置PLL 参数,如倍频和分频因子等,可以得到满足系统要求的更高频率的时钟信号。
- 只能在锁相环这里倍频

- 配置0-5位 => M (PLL的分频系数,最高可为63)

- 配置6-14位 => N(PLL的倍频系数)

- 配置16-17位 => P (PLL的分频因子)

下面是F401的参考和参数配置:

RCC_CFGR
- 0-1位 => 选择源,此处为10

- 4-7 位 => AHB分频

- 10-12位 => APB1 分频

- 13-15位 => APB2 分频


1.4 SysTick
- 系统定时器
- SysTick定时器的关键作用之一是为实时操作系统(RTOS)提供时基。在RTOS里,任务调度通常要依据精确的时间间隔来进行,SysTick定时器能够定期产生中断,进而驱动任务调度器的运行。
- 工作机制: 当SysTick定时器被使能后,它会从 SysTick->LOAD 寄存器中加载初始计数值,然后开始递减计数。每经过一个时钟周期,计数值就减1。当计数值减到0时,会产生一个SysTick中断,并且自动重新从 SysTick->LOAD 寄存器中加载初始计数值,继续递减计数。
- 影响Systick计数速度的因素



1.4.1 LOAD
- SysTick->LOAD(232 页):重装载值寄存器,用于设置定时器的初始计数值。
- 通过目标中断频率来确定LOAD的值
- 计算公式
LOAD = (HCLK_Frequency / Desired_Interrupt_Frequency)
HCLK
是系统时钟
- 示例:若 HCLK = 84MHz,需1ms(1000Hz),中断一次,则 LOAD = 84000000 / 1000 - 1 = 83999(十六进制0x147AF)。


1.4.3 CTRL
- SysTick->CTRL:控制和状态寄存器,用于使能定时器、选择时钟源、使能中断等操作。

- ENABLE (bit0):启动计数器。
- TICKINT (bit1):允许 SysTick 中断触发。
- CLKSOURCE (bit2):选择时钟源(已设置为 HCLK)。

1.4.4 VAL
- SysTick->VAL(233 页):当前计数值寄存器,用于读取当前的计数值。


1.5 补充
FLASH_ACR
(手册60⻚,46⻚降低CPU频率)
- 物理限制:STM32F4的Flash存储器在3V供电时,最高支持30MHz访问速度
- 解决方案:通过插入等待周期降低"有效访问频率"
- 计算公式:实际访问周期数 = LATENCY + 1
例如:当LATENCY=2时,每个Flash访问需要3个HCLK周期,等效频率 = 84MHz / 3 = 28MHz < 30MHz
- 为何要在PLL之前配置?
若先提高时钟频率再设置等待周期,在两者之间的短暂时间内,CPU会以⾼频率访问未准备好的
Flash,必然导致总线错误。因此必须遵循以下顺序:
1. 启用HSE但保持低速时钟
2. 配置Flash参数
3. 提升时钟频率
4. 切换时钟源
- PRFTEN(Prefetch Enable)⸺预取使能
- 预取机制:Flash 控制器在 CPU 读取当前指令时,提前预取后续地址的指令(通常是 128-bit 或 256-bit 宽度),存⼊预取缓冲区。
- 连续代码加速:适⽤于顺序执⾏的代码(如循环、线性代码),减少CPU等待时间。
作用:
典型场景:
任何需要⾼效执⾏的应⽤(如实时控制、信号处理)。代码体积较⼤且主要顺序执⾏。
- ICEN(Instruction Cache Enable)⸺指令缓存使能
作⽤:
1.缓存机制:将最近执⾏的指令缓存到 64-bit 的缓存⾏中,避免重复从 Flash 读取。
2. 跳转代码优化:适⽤于分⽀密集的代码(如状态机、中断服务程序)。
启⽤条件:
1. 推荐启⽤:除⾮代码完全线性且⽆跳转。
2. 典型场景:频繁调⽤的函数或中断服务程序(ISR)。
使⽤ if/switch 或函数指针的代码。 如果 CPU 读取 Flash 地址 0x8000 0020 ,且 DCEN 已启⽤,则缓存该地址所在的⾏(如 64-bit)。如果 CPU 再次访问同⼀缓存⾏内的数据(如 A+4 ),则直接命中缓存(⽆需读 Flash)。如果访问其他地址,可能触发新缓存⾏的加载(旧数据可能被替换)。
下面是自己所学和补充的内容:
2 STM32时钟体系
2.1 晶振和时钟
- 什么是晶振
- 晶振的全称叫做晶体振荡器,是晶体(石英)和电子元件组成,晶振有一个非常重要的特性:机电效应(压电效应),一般晶振会提供高度稳定的频率(振荡频率是固定的),一般晶振的频率有 8MHZ、12MHZ、25MHZ、11.0592MHZ..晶振的单位是HZ,频率(单位时间震荡的次数)

- 什么是时钟?
- 时钟相当于处理器的“心脏”,外部晶振经过振荡会产生高度稳定的信号,由芯片的引脚输入芯片内部,再经过芯片内部的频率放大器进行放大或者缩小,最终变成各种总线的时钟频率。
- 处理器的外设都是由时钟控制的,想要进行同步通信的话,也是需要时钟。举个例子:交响乐团指挥
- 思考:时钟信号是由什么产生的?
- 时钟信号是由时钟源产生的,比如外部晶振产生的时钟信号就是时钟源的一种,但是有的外设不需要太高的时钟频率,所以STM32提供了多种时钟源给用户使用。参考 STM32 中文参考手册第6章一共有5种时钟源
- 看门狗:一旦有一次没有喂狗,就代表主人死了,那么就会将程序复位。必须要用一个时钟源来驱动

- HSE
- HSE:HighSpeed External 高速外部时钟,特点是精度高,一般可以作为系统时钟
- 设计硬件电路时,需要将晶振电路和芯片对应引脚设计尽可能接近,以尽量减小输出失真和起振稳定时间。

- HSI
- HSl:High Speed Internal 高速内部时钟,频率为 16MHZ 的 RC振荡器,优点是成本低,缺点是精度低,容易受温度影响
- 不需要外部晶振,成本较低,对于功耗要求低,精度不要求那么高的设备可以使用

- LSE
- LSE : LowSpeed External 低速外部时钟,频率为 32.768KHZ 的晶振,可以驱动 RTC实时时钟,优点是功耗低、精度高。

- LSI
- LSI:LowSpeed Internal 低速内部时钟,频率为 32KHZ 的 RC振荡器,可以驱动独立看门狗和低功耗唤醒功能

- PLL:倍频率锁相环
- 8MHz外部晶振信号进入后,PLL进行倍频,可以最大增加到168MHz(即芯片主频这里用的芯片是STM32F407),也可以通过配置参数达到超频效果。
- 这个是F401的PLL设置


总结如下:

2.2 STM32时钟树


注意:这里是以25MHz举例,配置PLL_M = 25


8MHz / 8(M)* N(336) / P(2) = 168MHz

- 配置M,N,P
- P: /P
- N: *N
- 超频会导致芯片使用寿命降低和发烫
- M: /M



总结时钟树

- PRESC : Prescaler 分频


- 如果AHBx的分频系数为1,那么就作为定时器时钟;如果AHBx的分频系数不为1,那么就*2作为定时器时钟;
- 举例子:APB1的总线频率为42MHz,根据图可知外设时钟为42MHz。分频系数为4,那么APB1的定时器时钟就为84MHz。根APB2的总线频率为84MHz,根据图片外设时钟为84MHz,分频系数为2,那么APB2的定时器时钟就为168Hz。
- 上面的系统定时器,是精准延时需要用到的定时器

总结:

2.2 PLL参数的修改
大家可以看到标准库中使用的外部品振是 25MHZ,而开发平台实际使用是8MHZ,所以如果不去修改工程的 PLL 参数,会导致芯片的运行频率是错误的,所以需要修改 PLL参数,可以去 system stm32f4xx.c和 stm32f4xx.h 两个文件中修改。
这样以后在使用延时和定时器的时候才不会出现问题。

N固定,那么就修改M和P就可以了
- 修改 stm32f4xx.h 的 123 行,把25000000改为8000000修改外部晶振的频率(根据开发板)

- 修改system_stm32f4xx.c的316行 PLL参数 修改 PLL_M 的值 把 25 改为 8

2.3 STM32和51单片机时钟的对比(了解)
- 11.0592MHz:精准波特率
- 12MHz:精准延时时间
- 用一个晶振来同步所有外设资源,性能低功耗低

- STM32为什么8MHz性能就能很高:因为可以PLL倍频

- STM32挂载的外设又通过存储器映射分出去了,即不同外设可以根据不同实际情况使用适合的频率,节约资源也可以提供更多选择。


- 但是需要注意的是,不是所有的产品都需要PLL,因为使用 PLL会增大功耗,减少产品的续航能力(智能手表、手机锁屏)
- 项目经验:根据实际的情况来修改PLL的参数,这样就可以降低功耗或者提供性能,一般修改 PLL_N 的值(192~432),针对高性能模式、平衡模式、低功耗模式(以手机为例)
- 高性能模式 :如果修改 PLL N的值为 432,则主频会达到 216MHZ最高为多少 MHZ?
- 平衡模式:如果修改 PLL N的值为 336,则主频会达到 168MHZ
- 低功耗模式:如果修改 PLL_N的值为192,则还得修改M和P…主频会降到 84MHZ 最低能降到8MHZ!
- 注意:如果芯片主频超过 168MHZ,就是超频工作,增加芯片功耗以及会导致芯片发烫,谨 慎使用。
3 STM32系统定时器
3.1 Systick概述
- 定时器是 STM32 中非常常用的一个外设,对于 STM32 来说,提供多种定时器供用户使用,比如高级定时器、通用定时器、基本定时器在使用时比较繁琐,所以内核就提供一款定时器叫做系统定时器,也被称为 Systick 定时器(嘀嗒定时器)。
- Sxstick,定时器是属于内核中的一个外设,内嵌在 NVIC中,在 CortexM3/M4 内核中都存在,方便用户在使用不同类型的芯片的时候进行移植。
- 注意:因为 Systick,定时器是内核外设,所以在 STM32F4 中文参考手册是找不到的,可以参考 cortexM3 权威指南 +M3/M4 内核权威指南。

- Systick 定时器是一个 24bit 的倒计时(向下计数)定时器,功能就是实现简单的延时。
- 裸机模式:提供简单的延时 实现微秒、亳秒级别的延时闹钟、秒表、洗衣机、微波炉
- 操作系统:提供给操作系统一个稳定的时基,因为操作系统的架构是并行的。
3.2 系统定时器的时钟源
Systick,定时器有两个时钟源可以选择,这里参考 STM32F4 中文参考手册的时钟树,如下图:

可以看到Systick,定时器的时钟源可以选择内部时钟(168MHZ),或者外部时钟(8 分频之后的系统时钟 168MHZ/8=21MHZ)
- 思考:时钟源的选择有什么区别?
- 选择不同的时钟源会影响延时时间的长短
- 如果选择 168 MHZ 作为 Systick定时器的时钟源:
- 1s(1000000us)振荡 168000000 次,振荡1次的周期是 1/168 us, 也就是振荡 168 次花费 1us
- Systick 定时器是一个 24bit 的倒计时(向下计数)定时器
- 2^24-1(计数从0~2^24-1,一共倒计时2^24次) 最大延时时间为99.86ms(2^24/168000000)
- 如果选择 21 MHZ 作为 Systick定时器的时钟源:
- 1s(1000000us)振荡 21000000 次,振荡1次的周期是 1/21 us, 也就是振荡 21 次花费 1us
- Systick 定时器是一个 24bit 的倒计时(向下计数)定时器
- 2^24-1(计数从0~2^24-1,一共倒计时2^24次) 最大延时时间为798.9ms(2^24/21000000)
- 一般选择8分频为时钟源,延时时间更长,延时区间更加灵活。如果我需要延时1s,两个都实现不了,但我们还是选择21MHz,因为只需要2个500ms,即可完成延时。CPU执行两次延时就可以完成操作,前者需要执行20个50ms级别的延时才可完成延时。


Systick定时器是属于内核中的一个外设,内嵌在 NVIC中,关于Systick,定时器的寄存器说明以及函数接口都存储在内核文件 misc.c以及 misc.h,core_cm4.h中。
寄存器地址的定义都在stm32f4xx.h里面
- 内核中提供了一个函数接口可以去修改 systick的时钟源函数为
SysTick_CLkSourceConfig()
- 函数参数:
SysTick_CLKsource
选择时钟源 - 一般选
SysTick_CLKSource_HCLK_Div8
21MHZ
- 这个宏指向了指向了了一个结构体地址,这个结构体地址又指向的是systick寄存器的地址

- 特点:精准延时,因为晶振的频率等等固定。之前写的delay一个变量—到0,这样的延时是模拟延时,提供的时间并不精准。
3.3 Systick的应用
3.3.1 Systick中断的方式来实现延时


- 一个补充的C语言小知识点:
- 内联函数
- 内联函数是一种在编译时将函数调用替换为函数体本身的函数。它的语法形式与普通函数类似,但在定义时需要使用
inline
关键字修饰。 - 减少函数调用开销:普通函数调用时,程序需要保存当前的上下文(如寄存器状态、返回地址等),跳转到函数地址执行,执行完后再返回。这个过程会消耗时间和栈空间。而内联函数在编译时直接将函数体插入到调用点,避免了函数调用的开销。
- 提高程序运行效率:对于一些简单的、频繁调用的函数(如简单的计算或赋值操作),使用内联函数可以显著提高程序的运行速度。
- 第一步:设置Systick定时器中断周期
- 函数分析 Systick_Config(ticks)

第二句:NVIC自动设置优先级,但这里设置的是最低优先级
第三句:设置当前数值寄存器VAL为0,这里的Wc是一旦写,就会清除COUNTFLAG


- 如果想要产生周期性中断,那么就得给出周期间隔等于多少
- 如果是时钟为30MHz,想要产生1KHz的时钟频率
- 那么就需要传入参数ticks = 30M / 1K = 30000
- 那么如果是168MHz,想产生1us的中断
- f = 1/1us = 10^6
- 传入参数ticks = 168M / (10^6=M) = 168
- 这个函数自动会帮你-1,判断reload value possibble?
- IO:volatile
volatile
是一种类型修饰符,用于告诉编译器该变量的值可能会在程序的运行过程中被外部因素(如硬件中断、DMA操作等)改变。因此,编译器在生成代码时不会对访问该变量的代码进行优化,以确保每次访问该变量时都能读取到最新的值。_IO
是一个宏定义,通常用于特定的开发环境(如 STM32 等 ARM Cortex-M 系列微控制器的开发)。它是一个平台相关的宏,用于修饰变量或寄存器,表示该变量或寄存器是可读写的。- 该变量的值可能会被硬件操作或中断服务例程(ISR)修改。例如,
TimingDelay
可能用于延时计数器,硬件中断可能会修改它的值,因此需要使用volatile
修饰。
- 第二步:编写延时函数



三者结合生成的代码如下:
注意注意:必须要修改的两个地方


3.3.2 Systick非中断的方式来实现延时
如果想要改时钟源
但SystemCoreClock这个宏是168MHz
所以如果想要使用21MHz的时钟源
完整代码如下:
下面是非中断方式来实现延时

- 重载值-1:是因为需要数到0
- 初始化定时器:1.关闭系统定时器,写入重装载值,写入VAL清空VAL并且清除COUNTFLAG

- 启动定时器:往CTRL第0位写1
- 举例:如果需要间隔1000,那么需要往LOAD里面写入999
- 查看 如果为1,就说明计时已到设定时间间隔(数完了);否则就没有数完


- 实现延时微妙,最多不能超过798900us
- 核心代码分析:下面是我们关于使用中断进行延时的操作,可以看到多位同时写入的写法
如果我们是想只写一次
当CTRL的第16位为1,即倒计时结束,与运算为1,跳出这个循环。如果为0,就呆在这个循环让CPU空转。等着延时结束。
- 实现延时毫秒,最多不能超过799.9ms
- 完整代码如下:


初版main.c的内容

4 MY SYSTICK
补充



8 MHz / 8 /4 *336 =84MHz












可以用你写的代码,但是必须要加上操作系统相关的内容
关于全局中断

关于volatile


代码
Systick.c
Systick.h
5 Led验证
其他组方案:


- 作者:🐟🐟
- 链接:https://www.imyuyu.top//article/Quadcopter2-3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。