时钟修改时间片原理
262.2.13时钟修改时间片原理
#include <rtthread.h> // 引入 RT-Thread 的系统节拍获取、内核打印和线程延时接口
#define DBG_TAG "main" // 设置 rtdbg 日志标签为 main
#define DBG_LVL DBG_LOG // 设置 rtdbg 日志级别
#include <rtdbg.h> // 引入 RT-Thread 调试日志宏
int main(void) // RT-Thread 用户主线程入口函数
{ // main 函数体开始
int count = 1; // 初始化课程用于维持主线程循环的计数变量
while (count++) // 课程用自增变量维持主线程循环,通常不会主动退出
{ // 主线程循环体开始
rt_kprintf("rt_tick_ is %u \r\n",rt_tick_get()); // 读取当前 OS tick 累计值,并按无符号十进制打印
//LOG_D("Hello RT-Thread!"); // 注释掉原有周期日志,避免干扰本节 tick 观察结果
rt_thread_mdelay(1000); // 挂起当前线程约 1000 ms,到期后重新进入就绪态
} // 主线程循环体结束
return RT_EOK; // 理论上的正常返回值;本例通常不会执行到这里
} // main 函数体结束
源码说明: 上方只收录视频在本节实际完成的
applications/main.c业务代码。配套压缩包中的12TIMER是后续课程继续修改后的最终工程,已包含定时器句柄、回调函数和定时器控制代码;这些内容在本节视频中尚未编写,因此没有提前抄入。drv_clk.c、drv_common.c、CubeMX 生成文件和 RT-Thread 内核源码属于 BSP、生成代码或库实现,本笔记只分析本节查看到的关键语句,不把它们混入业务源码区。
一、时钟源修改
本节新建 12TIMER 工程,并把 STM32F103 的系统时钟由工程默认使用的内部高速 RC 时钟(HSI)改为开发板上的外部 8 MHz 晶振(HSE),再经 PLL 倍频到 72 MHz。
先区分三个很容易混淆的概念:
| 概念 | 本工程中的含义 | 数值或单位 |
|---|---|---|
| 系统时钟 | CPU 和各总线运行所依据的硬件时钟 | HSE 8 MHz 经 PLL ×9 得到 SYSCLK 72 MHz |
| OS Tick(系统节拍) | 内核周期性获得的时间基准 | 每秒 1000 次,即每 tick 约 1 ms |
| 线程时间片 | 同优先级就绪线程轮转时,一次最多连续运行的 tick 数 | 创建线程时给出的 tick 数 |
修改 HSE/PLL 会改变硬件时钟基础;SysTick 再根据 HCLK 和 RT_TICK_PER_SECOND 计算重装值,以维持规定的 OS Tick 频率;线程时间片则以 OS Tick 为计量单位。不能把“72 MHz”“1 ms tick”和“某线程有 5 个时间片”当成同一个参数。
课程说外部晶振“更准”,方向正确,但更严谨的表述是:STM32F103 的 HSI 是片内 8 MHz RC 振荡器,频率会随温度、电压和器件差异漂移;符合规格并正确设计的外部晶体通常具有更小的频率误差。外部晶振并非天然绝对准确,晶体规格、负载电容、PCB 布局和温度同样会影响实际频率。
1.1 新建工程
在 RT-Thread Studio 中新建工程并命名为 12TIMER。创建界面的提示说明该 BSP 默认采用内部时钟;若要使用板载外部晶振,需要通过 drv_clk.c 的时钟初始化入口接入新的配置。
课程随后在工程菜单中打开 CubeMX Settings。这个入口会启动已安装的 STM32CubeMX,并把生成结果放回当前 RT-Thread Studio 工程。若弹出的器件选择窗口显示不全,可拉大窗口后确认,不要仅凭课程画面盲选封装。
本课程归档的 .ioc 最终记录为:
- 器件:
STM32F103ZEH6; - 封装:
LFBGA144; - 工程工具链:
EWARM V8.32。
这里的 H6 与常见开发板上采用的 ZET6 不是同一种封装后缀:课程所选 H6 是 LFBGA144,而 T6 通常对应 LQFP144。实际操作必须以开发板芯片表面的完整型号和板卡原理图为准。 封装选错不仅影响外形显示,也可能使引脚映射与实物不一致;本节的重点是学习配置流程,不代表所有 STM32F103ZE 开发板都应选择 H6。
1.2 CUBE MX配置修改
课程中的 CubeMX 配置步骤可整理为:
- 在 System Core → RCC 中,把 HSE 选择为 Crystal/Ceramic Resonator。
- 在 Clock Configuration 中输入板载 HSE 为 8 MHz,并把 SYSCLK 配到 72 MHz。
- 在 NVIC 的 Code Generation 页面,取消由 CubeMX 生成
SysTick、PendSV和SVC相关处理函数的选项,让 RT-Thread 提供这些异常处理入口。 - 在 Project Manager → Code Generator 中选择为每个外设生成独立的
.c/.h文件;课程选择 EWARM 工具链,以配合 RT-Thread Studio 的导入流程。 - 点击 Generate Code,生成完成后关闭 CubeMX,让 RT-Thread Studio 刷新工程文件。
- 课程首次生成后出现 USART 相关编译错误,又在 CubeMX 中把
USART1的模式设为 Asynchronous,重新生成后编译通过。
取消三个异常处理函数的生成不是“关闭这些中断”。它只是不让 CubeMX 再定义同名 ISR。SysTick 仍会工作,PendSV 仍负责 Cortex-M 上的线程上下文切换,SVC 也仍可由 RT-Thread 端提供;这样可避免 CubeMX 与 RT-Thread 各自生成同名函数而发生链接冲突或错误地进入 HAL 默认处理流程。
本工程的最终时钟链如下:
外部晶振 HSE 8 MHz
│ HSE 预分频 ÷1
▼
PLL ×9
│
▼
SYSCLK = 72 MHz
├─ AHB ÷1 → HCLK = 72 MHz(CPU、SysTick 使用)
├─ APB1 ÷2 → PCLK1 = 36 MHz
└─ APB2 ÷1 → PCLK2 = 72 MHz
CubeMX 生成的 SystemClock_Config() 还把 Flash 等待周期设为 FLASH_LATENCY_2。这是 72 MHz 运行时必须配合处理的访问时序,不能只改 PLL 倍频而忽略 Flash 和总线分频。STM32F1 中,当 APB 预分频不为 1 时,对应 APB 定时器时钟通常再乘 2,因此本配置下 APB1 总线是 36 MHz,但 APB1 定时器内核时钟可为 72 MHz;后续计算定时器参数时不能直接把 PCLK1 当作定时器时钟。
外部晶振电路怎样工作
外部 8 MHz 晶体通常接在 MCU 的 OSC_IN 与 OSC_OUT 两个引脚之间,晶体两端各有一个负载电容接地。可把它理解为:
- MCU 内部提供振荡放大器;
- 晶体是频率选择元件,使振荡回路主要稳定在其标称频率附近;
- 两个负载电容和引脚、走线的寄生电容共同形成晶体要求的负载;
- PLL 再把 8 MHz 倍频为 72 MHz,晶体本身并不直接输出 72 MHz。
负载电容不能只因“别人的板子用了某个值”就照抄。对两只负载电容,可用近似关系理解晶体看到的负载:
$ C_L \approx \frac{C_1C_2}{C_1+C_2}+C_{stray} $
其中 $C_{stray}$ 包括 MCU 引脚和 PCB 走线的寄生电容。实际取值应依据晶体数据手册的 $C_L$、ST 的振荡器设计指南和开发板原理图,并检查起振裕量;晶体应靠近 MCU,走线短、对称且远离高速干扰源。课程没有展示具体板卡原理图和晶体型号,因此不能凭空给出固定电容值。
另外,若板上使用的不是无源晶体,而是有源时钟模块从外部直接送入时钟,CubeMX 应选择相应的 Bypass Clock Source,不能仍选 Crystal/Ceramic Resonator。
1.3 编译问题及百度搜索相关错误处理
课程在第一次生成后遇到成批的串口相关编译错误,通过搜索错误信息发现 USART1 未在 CubeMX 中启用,配置 USART1 → Asynchronous 并重新生成后解决。
这种排查思路应保留,但“把报错复制到搜索引擎,照着答案修改”不够严谨。更可靠的顺序是:
- 从构建日志的第一条错误开始看,后续几十条错误可能只是连锁反应。
- 确认报错符号或文件属于 CubeMX 生成层、RT-Thread BSP,还是用户应用层。
- 对照
.ioc、board.h和实际硬件,确认外设实例及引脚一致。 - 修正 CubeMX 配置后重新生成,关闭 CubeMX 并刷新工程,再执行一次完整构建。
- 搜索网络资料时优先查 ST、RT-Thread 的官方文档和与当前版本一致的资料,不要直接套用芯片、BSP 或版本不同的答案。
还要注意本归档工程存在两套 USART1 引脚信息:CubeMX 的 .ioc 记录为 PB6/PB7,而 RT-Thread 的 drivers/board.h 将控制台 UART1 配为 PA9/PA10。CubeMX 生成的弱 main() 不会执行其中的 MX_USART1_UART_Init(),实际控制台由 RT-Thread 串口驱动和 board.h 控制。因此,本节“在 CubeMX 中打开 USART1”解决了生成代码的构建问题,却不能据此认定实物控制台一定接在 PB6/PB7;接线必须以 BSP 配置和板卡原理图为准。
1.3.1 例题: RT-Threadstudio错误处理
若再次遇到类似 USART 错误,可按下面的检查表处理:
| 检查对象 | 本工程应看到的内容 | 错误时可能出现的现象 |
|---|---|---|
CubeMX .ioc | USART1 已启用为异步模式 | 生成的 usart.c/.h 缺失或引用不完整 |
cubemx/Inc、cubemx/Src | 生成文件已刷新 | 仍在编译旧文件或头文件不匹配 |
drivers/board.h | BSP_USING_UART1 且控制台引脚明确 | RT-Thread 控制台无输出或引脚用错 |
rtconfig.h | 控制台设备名为 uart1 | shell/日志无法绑定预期设备 |
| 构建日志 | 从首条 error 定位根因 | 只追逐最后一条连锁错误,越改越乱 |
“编译通过”只能证明语法、符号和链接关系成立,不等于外部晶振已经可靠起振,也不等于串口引脚和实物一致。烧录后还应查看时钟打印、串口输出,必要时用 MCO 输出或示波器等手段验证时钟;测量晶体引脚本身会引入探头电容,初学者不要随意用普通探头直接夹在晶体两端并据此判断频率。
1.4 时钟修改文件
本工程真正生效的调用关系是:
RT-Thread 板级初始化
└─ hw_board_init(...)
├─ HAL_Init()
├─ clk_init(...)
│ └─ SystemClock_Config() ← CubeMX 生成的 HSE + PLL 配置
└─ rt_hw_systick_init() ← 在 72 MHz HCLK 基础上配置 OS Tick
先配置系统时钟,再配置 SysTick,这个顺序很重要。否则若先按旧 HCLK 装载 SysTick,之后再改变 HCLK,实际节拍周期就会偏离配置值。
1.4.1 clock information
原稿把 clock_information() 当成时钟配置函数,这是不准确的。drv_clk.c 中的该函数只调用 HAL 查询接口,并用 LOG_D 准备输出当前寄存器反映出的 SYSCLK、HCLK、PCLK1 和 PCLK2 频率;INIT_BOARD_EXPORT(clock_information) 把它注册到 RT-Thread 的板级自动初始化阶段。
还要结合日志级别看实际效果:该文件设置的是 DBG_LVL DBG_INFO,而 LOG_D 只有在级别达到 DBG_LOG 时才生成输出,因此配套工程当前构建中这些调试打印会被宏裁掉。函数仍会进入自动初始化表,但不能仅凭“代码里写了 LOG_D”就断定串口一定能看到频率信息。下面的数值是根据实际 RCC 配置应得到的查询结果;若要观察它们,需要另行调整日志级别或输出接口,这不是本节业务代码的改动。
按本工程配置,正常应得到约:
| 项目 | 频率 |
|---|---|
| SYSCLK | 72,000,000 Hz |
| HCLK | 72,000,000 Hz |
| PCLK1 | 36,000,000 Hz |
| PCLK2 | 72,000,000 Hz |
它适合验证配置结果,但不是高精度频率测量仪器:这些 HAL 查询函数根据 RCC 配置推算频率,并不会测出晶体自身的 ppm 偏差。
1.4.2 drv_clk.c
drv_clk.c 中仍保留了一个使用 HSI 的 system_clock_config(),所以只搜索到 RCC_HSI_ON 不能证明工程仍以 HSI 为系统时钟。关键应看哪一个函数真正被调用。
CubeMX 生成后,clk_init() 不再调用旧的 system_clock_config(target_freq),而是外部声明并调用 SystemClock_Config()。后者位于 cubemx/Src/main.c,其中的关键配置为:
- 振荡器类型选择 HSE,且
HSEState = ON; - PLL 输入选择 HSE,HSE 预分频为 1,PLL 倍频为 9;
- SYSCLK 来源选择 PLLCLK;
- AHB、APB1、APB2 分频分别为 1、2、1。
生成函数里同时出现 HSIState = ON,也不等于 CPU 正在使用 HSI。判断系统时钟来源应看 PLLSource 和 SYSCLKSource:本工程是 HSE → PLL → SYSCLK。HSI 只是仍保持开启状态。
1.4.3 k5 MS文件夹
原稿中的“k5 MS”是语音识别错误,实际是 cubemx 文件夹。它含有:
cubemx/Inc:GPIO、USART 和main.h等生成头文件;cubemx/Src:GPIO、USART、生成的main.c等源文件;- 工程根部或对应目录的
.ioc:保存 CubeMX 图形配置。
生成目录中确实也有一个 main.c,但“单片机上电后不知道选哪个 main”这一说法不正确。选择发生在链接阶段,不是 MCU 上电后临时判断。RT-Thread Studio 把 CubeMX 生成的入口改成 __WEAK int main(void);应用目录中的普通 int main(void) 是强符号,同名强符号存在时,链接器采用强定义,弱定义不成为最终入口。
1.4.4 main.c
两个 main.c 的作用应这样理解:
| 文件 | main() 属性 | 在本工程中的作用 |
|---|---|---|
applications/main.c | 强定义 | 作为 RT-Thread 用户主线程入口,执行本节打印和延时循环 |
cubemx/Src/main.c | __WEAK 弱定义 | 保留 CubeMX 生成结构,但有强 main() 时不被采用 |
弱 main() 不被采用,意味着它函数体里的 HAL_Init()、MX_GPIO_Init() 和 MX_USART1_UART_Init() 不会因为文件参与编译就自动运行。RT-Thread BSP 有自己的板级初始化流程。
但是,cubemx/Src/main.c 中的 SystemClock_Config() 是另一个普通的全局函数,drv_clk.c 的 clk_init() 会明确调用它。因此可以出现这种结果:CubeMX 的弱 main() 不执行,但同一文件里的时钟配置函数仍被链接并执行。课程利用的正是这一点。
1.5 时钟节拍原理
OS Tick 是 RTOS 的周期性时间基准。对本工程而言,Cortex-M3 内核自带的 24 位 SysTick 递减计数器每约 1 ms 产生一次异常,RT-Thread 在异常处理函数中通知内核“又过去了一个 tick”。它支撑线程延时、软件定时器、时间片轮转等功能。
OS Tick 是离散时间基准,不是高精度连续计时器。1 kHz tick 只能提供约 1 ms 的量化分辨率;中断屏蔽、调度、其他高优先级工作和输出阻塞还会给线程实际恢复运行的时刻带来延迟。换用更准的 HSE 能改善基础时钟误差,但不能消除调度抖动。
1.5.1 rtconfig.h文件
本工程 rtconfig.h 中定义:
RT_TICK_PER_SECOND = 1000
它的含义是每秒产生 1000 个 OS Tick,因此:
$ T_{tick}=\frac{1}{1000}\text{ s}=1\text{ ms} $
这个宏定义的是系统节拍频率,不是“每个线程的时间片”。线程时间片仍是线程控制块中的 init_tick/remaining_tick,只是它们的单位为 OS Tick。例如线程时间片配置为 5,在当前系统中对应约 5 ms 的连续运行额度;若把系统节拍频率改为 500 Hz,同样的 5 tick 就约为 10 ms。
提高 tick 频率可提高时间量化分辨率,但会增加 SysTick 中断和内核处理开销;降低 tick 频率可减少开销,但延时和定时粒度变粗。不能为了追求“更精确”无条件把该值调得很高。
1.5.2 sysTick配置
drivers/drv_common.c 中的板级配置以 HCLK 和 RT_TICK_PER_SECOND 计算 SysTick 周期:
$ N=\frac{f_{HCLK}}{f_{tick}}=\frac{72,000,000}{1000}=72,000 $
SysTick 从 HCLK 取时钟时,每 72,000 个内核时钟周期产生一次异常。CMSIS 的配置接口把重装寄存器写为 $N-1$,所以 LOAD = 71,999;从 71,999 递减到 0 共经历 72,000 个时钟周期,时间正好约为 1 ms。
该 BSP 随后选择 SYSTICK_CLKSOURCE_HCLK,并把 SysTick_IRQn 的 NVIC 优先级设为 (0, 0)。这里的数值是本课程工程的实际实现,不应脱离具体 RTOS 移植和中断优先级规则,随意照搬到其他工程。
SysTick 是 24 位计数器。72,000 小于 $2^{24}$,所以本配置可以直接实现;若 HCLK 很高而 tick 频率太低,计算出的重装值超过 24 位,配置接口就会失败。
1.5.3 中断管理
本工程的 SysTick_Handler() 每次进入后依次完成:
rt_interrupt_enter():通知内核已经进入中断上下文,并维护中断嵌套计数。HAL_IncTick():维护 STM32 HAL 自己的 tick 状态,用于 HAL 兼容。rt_tick_increase():通知 RT-Thread 一个 OS Tick 已过去。rt_interrupt_leave():退出 RT-Thread 的中断管理;若中断期间产生了调度需求,会在安全的时机进行线程切换。
在 Cortex-M 上,真正保存和恢复线程上下文通常由 PendSV 完成。即使 SysTick 中的内核逻辑请求调度,也不是在 ISR 中间随意跳走,而是在完成中断处理后进入相应的上下文切换流程。
1.5.3.1 例题: SysTick_Handler函数
裸机 CubeMX 工程的 SysTick 处理流程通常只需维护 HAL tick;RT-Thread 工程则必须额外把中断纳入内核管理并调用 rt_tick_increase()。因此两个工程虽然使用同一个硬件 SysTick,软件职责不同:
| 处理内容 | 裸机 HAL 工程 | 本 RT-Thread 工程 |
|---|---|---|
| HAL 时间基准 | HAL_IncTick() | 保留 HAL_IncTick() |
| RTOS 中断嵌套 | 无 | rt_interrupt_enter/leave() |
| 内核 OS Tick | 无 | rt_tick_increase() |
| 线程调度影响 | 无 RTOS 线程 | 可处理延时、定时器和时间片调度 |
这也解释了为何 CubeMX 中不能再生成一个独立的 SysTick ISR:最终工程必须只保留一个入口,并在这个入口中执行 RT-Thread 所需的完整处理链。
1.5.4 HAL_IncTick函数
在标准 STM32 HAL 中,HAL_IncTick() 增加 HAL 的全局 tick 变量,常规裸机 HAL_Delay() 会反复检查 HAL tick,等待时间到达。这种等待不会把当前工作变成 RTOS 的挂起线程,通常会持续占用 CPU 执行等待循环。
但必须以本 BSP 的实际实现为准:drivers/drv_common.c 重新实现了 HAL 的 tick 相关接口,其中:
HAL_GetTick()由rt_tick_get()换算得到毫秒值;HAL_InitTick()直接返回成功,不让 HAL 另建一套 tick 中断;HAL_SuspendTick()、HAL_ResumeTick()为空;HAL_Delay()也是空函数。
因此,本工程中不能依赖 HAL_Delay() 实现延时,它会直接返回。线程延时应使用 rt_thread_mdelay()、rt_thread_delay() 等 RT-Thread 接口,使当前线程进入挂起态,让 CPU 可以执行其他就绪线程。原稿中“一个 rt_tick_delay 阻塞占用 CPU,另一个毫秒版会挂起”的表述来自 OCR 混淆,应删除;RT-Thread 的线程延时接口本质上都会让线程按条件挂起,而不是忙等占用 CPU。
1.5.5 rt_tick_increase函数
本工程使用的 RT-Thread 5.0.2 中,rt_tick_increase() 的核心工作可概括为:
- 全局
rt_tick加 1,记录系统启动后累计经过的 OS Tick。 - 取得当前运行线程,将其
remaining_tick减 1。 - 若剩余时间片减到 0,就恢复为初始时间片
init_tick,给线程设置 yield 标志并调用调度器。 - 检查内核定时器,处理已经到期的定时事件。
“时间片归零就把线程切走”仍不够严谨。RT-Thread 是基于优先级的抢占式调度器:
- 更高优先级线程一旦就绪,可以不等当前线程时间片耗尽就抢占。
- 时间片轮转只对同一优先级且处于就绪态的线程有意义。
- 若当前优先级没有另一个就绪线程,即使时间片用完并触发调度,当前线程也可能继续运行,只是时间片被重新装载。
rt_thread_mdelay()会主动挂起线程,它不是靠把运行时间片“耗尽”来完成延时。
因此,OS Tick 同时参与“系统时间累计”和“线程运行额度递减”,但全局 rt_tick 与某个线程的 remaining_tick 是不同变量,不能都简单称为“时间片”。
1.6 时钟节拍获取方法
RT-Thread 接口 rt_tick_get() 返回系统从启动以来累计的当前 tick 值。该工程中 rt_tick_t 是 32 位无符号整数,因此课程用 %u 打印与当前 ARM 编译环境相符。
返回值是系统节拍计数,不是当前线程剩余时间片,也不是直接以“毫秒”命名的时间戳。只有在本工程 RT_TICK_PER_SECOND = 1000 时,数值增量才恰好与毫秒近似一一对应;换成其他 tick 频率后必须按比例换算。
32 位 tick 在 1 kHz 下会发生自然回绕:
$ \frac{2^{32}}{1000\times60\times60\times24}\approx49.71\text{ 天} $
所以程序不能假定 tick 永远递增且永不溢出。测量不超过一个回绕周期的短时间间隔时,通常利用无符号减法计算“当前 tick − 起始 tick”,使一次自然回绕仍能得到正确差值;直接使用“大于某个绝对 tick 值”的判断容易在回绕处出错。
1.6.1 时钟节拍获取示例
本节把下面这条实际业务语句放到主循环中、1000 ms 延时之前:
rt_kprintf("rt_tick_ is %u \r\n",rt_tick_get()); // 读取调用瞬间的累计 OS tick,并通过控制台输出
典型输出近似为:
rt_tick_ is 1
rt_tick_ is 1003
rt_tick_ is 2004
rt_tick_ is 3005
这里显示的是累计 tick,而不是“当前线程拥有 1、1003、2004 个时间片”。相邻值不严格相差 1000,主要由语句顺序和调度时序共同造成:
- 程序先读取 tick 并执行
rt_kprintf()。 - 串口格式化和输出可能消耗若干 tick,某些控制台发送路径还会等待硬件或缓冲区。
- 输出完成后才调用
rt_thread_mdelay(1000),线程按 1000 个 tick 的名义时长挂起;受调用时刻相对 tick 边界的影响,墙上时间存在一个 tick 以内的量化误差。 - 延时到期只表示线程重新就绪;它实际再次获得 CPU 还可能存在一个 tick 的量化边界和调度延迟。
- 下一轮又在打印调用处采样,因此两次采样间隔包含“上一次打印耗时 + 约 1000 ms 延时 + 唤醒/调度延迟”。
rt_tick_get() 本身只是读取内核 tick,不能笼统说它必然固定消耗 1~2 ms。课程现象中的额外 1~2 tick 更可能来自打印、tick 边界和调度时机。输出增量偶尔不同属于正常现象,也再次说明线程延时不是高精度周期定时器;若要得到稳定周期,应使用合适的软/硬定时器,并根据实时性要求分析回调上下文和抖动来源。
二、结束
本节应掌握以下主线:
- CubeMX 生成
SystemClock_Config(),RT-Thread 的clk_init()调用它,使系统采用 HSE 8 MHz 经 PLL ×9 得到 72 MHz。 - CubeMX 的弱
main()不会覆盖应用层强main(),但其同文件中的普通时钟配置函数仍可被 BSP 调用。 - SysTick 按
HCLK / RT_TICK_PER_SECOND配置;本工程每 72,000 个 HCLK 周期产生一次约 1 ms 的 OS Tick。 rt_tick_increase()同时推进全局系统节拍、检查线程剩余时间片和内核定时器;时间片轮转只适用于同优先级就绪线程。rt_tick_get()取得的是累计 OS tick,不是线程时间片。打印周期多出的 tick 来自打印和调度等实际开销,而不是接口承诺的固定偏差。- 本 BSP 的
HAL_Delay()是空实现,线程延时应使用 RT-Thread 接口。
核对依据: 本节步骤、业务代码和实验输出取自课程视频、转录文本及配套
12TIMER工程;时钟树、生成配置、弱符号、SysTick 初始化、HAL 接口重实现和rt_tick_t类型均与工程源码逐项核对。硬件时钟范围与晶振设计参考 ST 的 STM32F103xC/D/E 数据手册和 AN2867 振荡器设计指南;OS Tick 与时间片规则参考 RT-Thread 官方内核移植文档、线程管理文档及本工程所用 RT-Thread 5.0.2 的clock.c。