F4串口HAL_UART_Transmit(&UART6_Handler,(uint8_t*)USART_RX_BUF,len,1000)分析,SysTick 中断开始就开启,GetTick才可用
一、串口HAL_UART_Transmit(&UART6_Handler,(uint8_t*)USART_RX_BUF,len,1000)函数分析
1. 函数功能概述
该函数是 STM32 HAL 库中用于阻塞式(Blocking)串口数据发送的标准接口。它的作用是将指定长度的数据通过 UART6 外设发送出去,并在发送完成或超时前占用 CPU 资源,不会立即返回。
2. 参数详细解析
表格
| 参数 | 值/类型 | 含义与作用 |
|---|---|---|
| huart | &UART6_Handler |
UART 句柄指针。 指向已初始化的 UART6 外设结构体。它包含了 UART6 的寄存器基地址、波特率、字长等配置信息,相当于外设的“身份证”。 |
| pData | (uint8_t*)USART_RX_BUF |
数据缓冲区指针。 指向待发送数据的内存首地址。 ⚠️ 注意:这里使用的是 USART_RX_BUF(通常用于接收的缓冲区)。虽然语法上合法(强制转换为 uint8_t*),但在逻辑上需确保该缓冲区当前存放的是需要发送的数据,而非正在接收或未处理的数据,否则会导致发送错误内容。 |
| Size | len |
发送字节数。 指定要发送的数据长度(单位:字节)。必须确保 len 不超过缓冲区的实际有效数据长度,否则可能发送垃圾数据或导致内存越界。 |
| Timeout | 1000 |
超时时间(毫秒)。 表示每个字节发送等待的最大时间,而非整个数据包的总超时时间。 若设置为 1000ms,意味着如果硬件在 1 秒内无法发送完单个字节(例如 TX 引脚被拉低、硬件故障),函数将停止并返回 HAL_TIMEOUT。 |
3. 执行机制与潜在风险
A. 阻塞特性(CPU 占用)
- 机制:该函数采用轮询方式工作。CPU 会一直等待 UART 硬件的
TXE(发送寄存器空)标志位,每发送一个字节就检查一次,直到所有字节写入发送移位寄存器,并等待最后一个字节发送完毕(TC标志位置位)。 - 风险:在发送期间,CPU 无法执行其他任务。如果
len较大或波特率较低,程序会“卡住”较长时间。- 示例:若波特率为 9600,发送 1 字节约需 1ms。若
len=100,CPU 将被阻塞约 100ms+。在此期间,中断响应可能延迟,实时性任务受影响。 - 建议:避免在中断服务程序(ISR)或高优先级实时任务中调用此函数。(实际不是按总字节数计算阻塞时长,因为TC,TXE是每个字节发送完置位一次)
- 示例:若波特率为 9600,发送 1 字节约需 1ms。若
B. Timeout 参数的误区
- 常见误解:认为 1000ms 是发送完整个
len长度数据的总时间。 - 实际逻辑:HAL 库对每个字节的发送都单独计时。只要每个字节能在 1000ms 内发出,即使总耗时超过 1000ms,函数也会正常返回
HAL_OK。 - 设置建议:Timeout 值应大于单个字节的发送时间。对于常规波特率(如 115200),1000ms 是非常充裕的;但若设为 1ms 且在低波特率下,可能会频繁触发超时错误。
C. 缓冲区使用逻辑风险
- RX_BUF 用于发送?:变量名
USART_RX_BUF暗示这是接收缓冲区。- 如果这是为了回显(Echo)收到的数据,逻辑是成立的(收到什么发什么)。
- 如果这是为了发送独立数据,建议重新命名或使用独立的 TX 缓冲区,以避免读写冲突(特别是在中断接收模式下,若接收中断正在写入
RX_BUF,而主循环正在从中读取发送,可能导致数据不一致)。
4. 返回值处理
函数返回 HAL_StatusTypeDef 枚举类型,调用后务必检查返回值:
HAL_OK:发送成功。HAL_TIMEOUT:发送超时(硬件异常或波特率配置极低)。HAL_BUSY:UART 外设正忙(上一次发送尚未完成)。HAL_ERROR:参数错误或硬件错误。
5. 优化建议
- 非阻塞替代方案:若系统对实时性要求高,建议使用中断模式
HAL_UART_Transmit_IT()或 DMA 模式HAL_UART_Transmit_DMA(),它们会立即返回,后台完成发送。 - 缓冲区隔离:明确区分接收缓冲区和发送缓冲区,或在复制数据后再发送,避免竞态条件。
- 超时合理设置:根据波特率计算单字节时间,设置合理的 Timeout(通常 10-100ms 足够,1000ms 适用于极低速或调试场景)。
总结
该行代码实现了通过 UART6 同步发送 len 字节数据的功能,超时保护设为 1000ms/字节。虽然简单可靠,但需注意其阻塞 CPU 的特性以及复用接收缓冲区可能带来的逻辑混淆。
二、HAL_StatusTypeDef UART_WaitOnFlagUntilTimeout()实现分析
伪代码/简化实现如下:
c
static HAL_StatusTypeDef UART_WaitOnFlagUntilTimeout(UART_HandleTypeDef *huart, uint32_t Flag, FlagStatus Status, uint32_t Tickstart, uint32_t Timeout) { /* 循环等待,直到标志位达到期望状态 */ while ((__HAL_UART_GET_FLAG(huart, Flag) ? SET : RESET) != Status) { /* 检查是否设置了非无限超时限制 */ if (Timeout != HAL_MAX_DELAY) { /* 判断是否超时:当前时间 - 起始时间 > 设定超时值 */ /* 或者 Timeout 为 0 时直接视为超时(某些旧版本或特定配置下的保护逻辑) */ if (((HAL_GetTick() - Tickstart) > Timeout) || (Timeout == 0U)) { /* 超时处理:重置 UART 状态为就绪,返回超时错误 */ huart->gState = HAL_UART_STATE_READY; return HAL_TIMEOUT; } } } /* 标志位符合预期,返回成功 */ return HAL_OK; }
3. 关键机制深度解析
A. 超时计算机制 (HAL_GetTick)
- 时间基准:函数依赖
HAL_GetTick()获取当前的系统毫秒数。这要求 SysTick 中断必须正常运作且优先级配置合理。 - 溢出处理:由于
HAL_GetTick()返回的是uint32_t类型,当计数器溢出(约 49.7 天后回绕)时,无符号整数减法(Current - Start)依然能正确计算时间差,因此该实现天然支持长时间运行而不必担心溢出错误。 - 精度限制:超时精度取决于 SysTick 的配置(通常为 1ms)。如果
Timeout设置过小(如 < 1ms),可能因调度抖动导致误判超时。
B. 标志位轮询 (__HAL_UART_GET_FLAG)
- 宏定义展开:
__HAL_UART_GET_FLAG本质上是对 UART 状态寄存器(如SR或ISR)的特定位进行读取。 - 常见标志位:
UART_FLAG_TXE(Transmit Data Register Empty):发送数据寄存器空,表示可以写入下一个字节。UART_FLAG_TC(Transmission Complete):发送完成,表示移位寄存器中的数据也已发送完毕。UART_FLAG_RXNE(Read Data Register Not Empty):接收数据寄存器非空,表示有数据可读。
- 状态匹配:函数通过三元运算符
(__HAL_UART_GET_FLAG(...) ? SET : RESET)将硬件寄存器的位值转换为标准的SET(1) 或RESET(0),然后与传入的Status参数比较。
C. 特殊保护逻辑 (Timeout == 0)
- 在某些 HAL 库版本中,存在
|| (Timeout == 0U)的判断。这意味着如果用户传入超时时间为 0,函数会立即返回HAL_TIMEOUT。这是一种防御性编程,防止用户误传 0 导致潜在的逻辑歧义(尽管在标准逻辑中,0ms 超时通常意味着“不等待”,但在轮询函数中通常被解释为“立即检查,若未满足则报错”或“非法参数”)。
D. 状态复位 (huart->gState)
- 当发生超时时,函数会将
huart->gState重置为HAL_UART_STATE_READY。这是为了确保上层应用在捕获到超时错误后,可以立即尝试重新发起发送或接收操作,而不会因为状态机卡在BUSY状态而无法再次调用 API。
4. 常见陷阱与调试建议
-
硬故障 (HardFault) 风险:
- 如果传入的
huart指针为空或未初始化,或者Flag参数非法,访问寄存器可能导致 HardFault。 - 如果 UART 时钟未使能,读取状态寄存器也可能导致总线错误。
- 如果传入的
-
死循环与看门狗:
- 虽然该函数有超时保护,但如果
Timeout设置为HAL_MAX_DELAY(通常为 0xFFFFFFFF),函数将无限等待。在中断服务程序(ISR)中严禁使用无限超时,否则会导致系统死锁。 - 若系统启用了看门狗(IWDG/WWDG),长时间的轮询等待可能触发看门狗复位。建议在长耗时发送前喂狗,或使用 DMA/中断模式。
- 虽然该函数有超时保护,但如果
-
硬件流控干扰:
- 如果启用了硬件流控(CTS/RTS),但引脚未正确连接或配置,
TXE或TC标志位可能永远无法置位,导致函数等待直至超时。例如,若 CTS 引脚电平无效,UART 控制器会暂停发送,导致TXE保持为 0(满),从而触发超时。
- 如果启用了硬件流控(CTS/RTS),但引脚未正确连接或配置,
-
中断冲突:
- 在轮询发送过程中,如果同时开启了 UART 接收中断,且接收中断优先级高于当前上下文,可能会影响
HAL_GetTick()的更新(如果 SysTick 优先级低于 UART 中断且被长时间占用),导致超时判断不准。需确保 SysTick 具有较高优先级。
- 在轮询发送过程中,如果同时开启了 UART 接收中断,且接收中断优先级高于当前上下文,可能会影响
5. 总结
UART_WaitOnFlagUntilTimeout 是一个典型的带超时保护的轮询等待原语。其核心价值在于将“硬件状态等待”与“时间安全边界”结合,确保了嵌入式软件在面临硬件异常时的鲁棒性。理解其实现有助于开发者合理设置 Timeout 参数,避免不必要的 CPU 浪费或通信失败。
三、HAL_GetTick()原型为什么没东西呢,只有一个返回的全局变量值
你看到的 HAL_GetTick() 函数原型“没东西”(即函数体看起来是空的或者只有一个简单的返回语句),是因为它被声明为 弱符号(Weak Symbol),且其核心逻辑极其简单:直接返回一个全局变量。
以下是详细解析:
1. 函数原型真相
在 STM32 HAL 库源码(通常在 stm32xxxx_hal.c 或 stm32xxxx_hal_timebase_template.c)中,它的定义如下:
c
__weak uint32_t HAL_GetTick(void) { return uwTick; }
-
__weak关键字:这是关键。它告诉编译器:“这个函数有一个默认实现,但如果用户在别的文件中重新定义了一个同名函数(没有__weak),请优先使用用户定义的那个。” -
return uwTick;:函数体并非真的“没东西”,而是只有一行代码。它直接返回全局变量uwTick的值。
2. 为什么看起来像“没东西”?
你可能在调试器或 IDE 中查看时产生了误解,原因可能有以下几点:
- 内联优化或简化显示:由于函数体只有一行
return,某些 IDE 在反汇编或单步调试时可能不会进入函数内部,而是直接获取值,让你感觉它“跳过了”函数体。 - 未找到源文件:如果你没有关联 HAL 库的源代码路径,IDE 可能无法显示具体的
.c文件内容,只显示声明uint32_t HAL_GetTick(void);,看起来就像没有实现。 - 误以为是宏:有些延时函数是宏定义,但
HAL_GetTick是真正的函数。不过因为它太简单,有时会被编译器内联(Inline),导致在调用处直接展开为读取内存操作。
3. uwTick 是从哪里来的?
既然 HAL_GetTick 只是返回 uwTick,那么谁在更新 uwTick 呢?
- 更新者:SysTick 中断服务函数。
- 流程:
- Cortex-M 内核有一个 SysTick 定时器,HAL 库初始化时(
HAL_Init())将其配置为每 1ms 产生一次中断。 - 每次中断触发,CPU 跳转到
SysTick_Handler()。 - 在
SysTick_Handler()中,会调用HAL_IncTick()。 HAL_IncTick()的核心代码就是:uwTick++;
- Cortex-M 内核有一个 SysTick 定时器,HAL 库初始化时(
c
// 在 stm32xxxx_it.c 中 void SysTick_Handler(void) { HAL_IncTick(); // 这里让 uwTick 加 1 } // 在 HAL 库内部 __weak void HAL_IncTick(void) { uwTick++; }
4. 为什么要设计成“弱符号”?
这种设计是为了灵活性:
- 默认情况:使用 SysTick 作为时间基准,每 1ms 中断一次,
uwTick自增。 - 自定义情况:如果你的项目需要更高精度(如微秒级),或者你想用其他定时器(如 TIM2)作为系统心跳,你可以重写
HAL_GetTick()和HAL_IncTick()。- 例如,你可以在自己的文件中写:
c
uint32_t HAL_GetTick(void) { return __HAL_TIM_GET_COUNTER(&htim2); // 返回定时器2的计数值 } - 链接器会发现你定义的版本没有
__weak,就会忽略库里的默认版本,使用你的版本。
- 例如,你可以在自己的文件中写:
5. 总结
- HAL_GetTick() 不是空的,它返回全局变量
uwTick。 -
uwTick由 SysTick 中断每秒更新 1000 次(每次加 1)。 -
__weak允许你自定义时间基准,这是 HAL 库的高级特性。 - 如果程序卡在依赖
HAL_GetTick()的超时判断中(如UART_Receive超时),通常是因为 SysTick 中断没有正常工作(例如中断被禁用、优先级配置错误、或HAL_Init()未调用),导致uwTick永远不增加,从而造成“假死”。
检查建议:
确保你的 main() 函数开头调用了 HAL_Init();,并且确认 SysTick_Handler 在中断向量表中正确注册并能被执行。
四、SysTick 中断没开启,HAL_GetTick()还能使用吗
不能正常使用。
如果 SysTick 中断没有开启(或未正确配置、被禁用),HAL_GetTick() 将无法返回递增的时间值,导致所有依赖该函数的延时、超时判断逻辑失效。
以下是详细原因分析及后果:
1. 核心机制:HAL_GetTick() 依赖中断更新
HAL_GetTick() 函数本身非常简单,它只是返回一个全局变量 uwTick 的值:
c
__weak uint32_t HAL_GetTick(void) { return uwTick; }
这个全局变量 uwTick 不会自动增加。它的唯一更新来源是 SysTick 中断服务程序。
- 正常情况下,SysTick 定时器每 1ms 产生一次中断。
- 在中断服务函数
SysTick_Handler()中,会调用HAL_IncTick()。 HAL_IncTick()执行uwTick++。
结论:如果 SysTick 中断没开启,HAL_IncTick() 永远不会被执行,uwTick 将始终保持初始值(通常为 0)。因此,HAL_GetTick() 将永远返回 0(或某个固定不变的值)。
2. 直接后果:系统功能瘫痪
由于 HAL_GetTick() 是 HAL 库时间基准的核心,其失效会导致以下严重问题:
-
阻塞式 API 永久卡死:
所有带有Timeout参数的 HAL 函数(如HAL_UART_Transmit,HAL_Delay,HAL_I2C_Master_Transmit等)内部都使用HAL_GetTick()来计算超时。- 现象:程序进入这些函数后,因为起始时间戳和当前时间戳永远相等(或差值永远小于超时阈值),循环条件
while ((HAL_GetTick() - tickstart) < Timeout)永远为真,导致程序死循环卡死。
- 现象:程序进入这些函数后,因为起始时间戳和当前时间戳永远相等(或差值永远小于超时阈值),循环条件
-
HAL_Delay()失效:HAL_Delay(ms)内部也是通过轮询HAL_GetTick()实现的。如果 SysTick 不开启,HAL_Delay将无限等待,程序无法继续执行。 -
RTOS 调度失败:
如果使用 FreeRTOS 等实时操作系统,它们通常也依赖 SysTick 作为心跳时钟。SysTick 不开启,任务调度将停止,系统完全静止。
3. 如何确认 SysTick 是否开启?
在 STM32 HAL 库项目中,确保以下步骤已完成:
-
调用
HAL_Init():
在main()函数的最开始必须调用HAL_Init();。这个函数内部会配置并启用 SysTick 中断(默认 1ms 间隔)。c
int main(void) { HAL_Init(); // 关键:初始化 HAL 库并启动 SysTick SystemClock_Config(); // ... 其他初始化 } -
检查
stm32xxxx_it.c中的中断 handler:
确保SysTick_Handler()存在且未被注释,并且其中调用了HAL_IncTick()。c
void SysTick_Handler(void) { HAL_IncTick(); // 必须存在 } -
检查 NVIC 优先级:
虽然 SysTick 需要开启,但如果其优先级配置过低,且被高优先级中断长时间占用,可能导致uwTick更新不及时,造成时间误差(但不会完全停止)。确保 SysTick 优先级适中(通常建议为最低或较低优先级,以便让出 CPU 给实时任务,但不能被屏蔽)。
4. 特殊情况:如果不使用 SysTick 怎么办?
如果你因特殊原因不能使用 SysTick(例如需要将其用于其他高精度定时),你必须重写时间基准:
- 使用其他定时器:选择一个通用定时器(如 TIM2, TIM6 等),配置为每 1ms 产生一次更新中断。
- 重写弱符号函数:
在你的代码中重新定义HAL_GetTick()和HAL_IncTick()(去掉__weak属性),让它们读取该定时器的计数值或在中断中递增自定义变量。c
// 示例:使用 TIM2 作为时间基准 uint32_t HAL_GetTick(void) { return __HAL_TIM_GET_COUNTER(&htim2); // 返回定时器计数值 } // 并在 TIM2 的中断中处理溢出计数,以扩展量程
总结
SysTick 中断是 HAL_GetTick() 的“心跳”。没有它,HAL_GetTick() 就是一个返回常数的废函数,会导致所有基于时间的 HAL 库功能(延时、超时)彻底失效并卡死程序。 务必确保 HAL_Init() 被正确调用,且 SysTick 中断正常运行。
五、原子串口例程中HAL_UART_Transmit(&UART6_Handler,(uint8_t*)USART_RX_BUF,len,1000),开启了SysTick 中断吗
开启了。正点原子(Atomic)的 STM32 HAL 库例程均严格遵循 ST 官方标准,HAL_Init() 是 main 函数的第一步,它必然初始化并开启 SysTick 中断以维持系统时间基准(查例程代码确实开启了,这点和F1例程不同2026.6.16)。
一、核心依据
- 标准初始化流程:原子例程的
main.c开头必调用HAL_Init()。该函数内部执行HAL_SYSTICK_Config(),配置 SysTick 为 1ms 中断源,并使能 NVIC。 - 超时机制依赖:你提到的
HAL_UART_Transmit最后一个参数1000是超时时间。若 SysTick 未开启,HAL_GetTick()返回值不变,函数将因无法计算时间差而永久卡死。既然例程能正常运行,证明 SysTick 必定工作。
二、代码验证位置
在原子例程的 stm32f4xx_it.c(或其他对应系列 IT 文件)中,你可以找到以下标准中断服务函数,证明中断已挂载:
c
void SysTick_Handler(void) { HAL_IncTick(); // 每次中断 uwTick++,为 HAL_GetTick() 提供数据 }
三、注意事项
虽然 SysTick 已开启,但需注意中断优先级。原子例程通常将 SysTick 优先级设为较低(如 15),若你的 UART 接收使用了高优先级中断且处理耗时过长,可能会轻微影响 uwTick 的更新实时性,但不会导致其停止工作
六、原子stm32f4xx例程中延时函数没有用操作系统,延时函数是用SysTick定时器查询还是中断方式
在正点原子 STM32F4 HAL 库例程(无操作系统)中,延时函数 HAL_Delay() 采用的是 SysTick 中断 + 轮询查询 的混合方式。
一、核心机制解析
-
时间基准更新(中断方式)(验证正确2026.6.16)
SysTick 定时器配置为每 1ms 产生一次中断。在中断服务函数SysTick_Handler中,调用HAL_IncTick()使全局变量uwTick自增。这是中断驱动的部分,确保时间计数在后台准确运行。 -
延时等待逻辑(查询方式)(源代码不是这样的,但是确实是没用到while()等待查询标识位变量的这种类似方式,而是用摘取方法在while(1)中等待差值超过设定值,才break跳出,,和F1例程不同。2026.6.16)
HAL_Delay(ms)函数内部通过while循环不断查询HAL_GetTick()的返回值。它计算当前时间与起始时间的差值,直到差值大于等于设定的毫秒数才退出。这是前台轮询的部分,期间 CPU 处于忙等待状态,不执行其他任务。
二、代码逻辑示意
c
// 1. 中断部分:后台自动更新计数 void SysTick_Handler(void) { HAL_IncTick(); // uwTick++ } // 2. 查询部分:前台阻塞等待 __weak void HAL_Delay(uint32_t Delay) { uint32_t tickstart = HAL_GetTick(); // 获取起始时间 while ((HAL_GetTick() - tickstart) < Delay) // 轮询检查时间差 { // CPU 在此空转,直到时间到达 } }
三、结论与影响
- 本质:属于阻塞式延时。
- 优点:实现简单,无需操作系统支持,时间精度较高(取决于 SysTick 优先级)。
- 缺点:延时期间 CPU 无法处理其他业务逻辑(除非有更高优先级的中断打断),实时性较差。若需非阻塞延时,需自行使用定时器中断或状态机实现。
原子F4例程延时源代码
#else //不用ucos时
//延时nus
//nus为要延时的us数.
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
//延时nms
//nms:要延时的ms数
void delay_ms(u16 nms)
{
u32 i;
for(i=0;i<nms;i++) delay_us(1000);
}
#endif
更多推荐



所有评论(0)