一、串口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是每个字节发送完置位一次)
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. 优化建议

  1. 非阻塞替代方案‌:若系统对实时性要求高,建议使用中断模式 HAL_UART_Transmit_IT() 或 DMA 模式 HAL_UART_Transmit_DMA(),它们会立即返回,后台完成发送。
  2. 缓冲区隔离‌:明确区分接收缓冲区和发送缓冲区,或在复制数据后再发送,避免竞态条件。
  3. 超时合理设置‌:根据波特率计算单字节时间,设置合理的 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. 常见陷阱与调试建议

  1. 硬故障 (HardFault) 风险‌:

    • 如果传入的 huart 指针为空或未初始化,或者 Flag 参数非法,访问寄存器可能导致 HardFault。
    • 如果 UART 时钟未使能,读取状态寄存器也可能导致总线错误。
  2. 死循环与看门狗‌:

    • 虽然该函数有超时保护,但如果 Timeout 设置为 HAL_MAX_DELAY(通常为 0xFFFFFFFF),函数将无限等待。在中断服务程序(ISR)中严禁使用无限超时,否则会导致系统死锁。
    • 若系统启用了看门狗(IWDG/WWDG),长时间的轮询等待可能触发看门狗复位。建议在长耗时发送前喂狗,或使用 DMA/中断模式。
  3. 硬件流控干扰‌:

    • 如果启用了硬件流控(CTS/RTS),但引脚未正确连接或配置,TXE 或 TC 标志位可能永远无法置位,导致函数等待直至超时。例如,若 CTS 引脚电平无效,UART 控制器会暂停发送,导致 TXE 保持为 0(满),从而触发超时。
  4. 中断冲突‌:

    • 在轮询发送过程中,如果同时开启了 UART 接收中断,且接收中断优先级高于当前上下文,可能会影响 HAL_GetTick() 的更新(如果 SysTick 优先级低于 UART 中断且被长时间占用),导致超时判断不准。需确保 SysTick 具有较高优先级。

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 中查看时产生了误解,原因可能有以下几点:

  1. 内联优化或简化显示‌:由于函数体只有一行 return,某些 IDE 在反汇编或单步调试时可能不会进入函数内部,而是直接获取值,让你感觉它“跳过了”函数体。
  2. 未找到源文件‌:如果你没有关联 HAL 库的源代码路径,IDE 可能无法显示具体的 .c 文件内容,只显示声明 uint32_t HAL_GetTick(void);,看起来就像没有实现。
  3. 误以为是宏‌:有些延时函数是宏定义,但 HAL_GetTick 是真正的函数。不过因为它太简单,有时会被编译器内联(Inline),导致在调用处直接展开为读取内存操作。

3. uwTick 是从哪里来的?

既然 HAL_GetTick 只是返回 uwTick,那么谁在更新 uwTick 呢?

  • 更新者‌:‌SysTick 中断服务函数‌。
  • 流程‌:
    1. Cortex-M 内核有一个 ‌SysTick 定时器‌,HAL 库初始化时(HAL_Init())将其配置为每 ‌1ms‌ 产生一次中断。
    2. 每次中断触发,CPU 跳转到 SysTick_Handler()
    3. 在 SysTick_Handler() 中,会调用 HAL_IncTick()
    4. HAL_IncTick() 的核心代码就是:uwTick++;

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_TransmitHAL_DelayHAL_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 库项目中,确保以下步骤已完成:

  1. 调用 HAL_Init()‌:
    在 main() 函数的最开始必须调用 HAL_Init();。这个函数内部会配置并启用 SysTick 中断(默认 1ms 间隔)。

    
      

    c

    int main(void) { HAL_Init(); // 关键:初始化 HAL 库并启动 SysTick SystemClock_Config(); // ... 其他初始化 }

  2. 检查 stm32xxxx_it.c 中的中断 handler‌:
    确保 SysTick_Handler() 存在且未被注释,并且其中调用了 HAL_IncTick()

    
      

    c

    void SysTick_Handler(void) { HAL_IncTick(); // 必须存在 }

  3. 检查 NVIC 优先级‌:
    虽然 SysTick 需要开启,但如果其优先级配置过低,且被高优先级中断长时间占用,可能导致 uwTick 更新不及时,造成时间误差(但不会完全停止)。确保 SysTick 优先级适中(通常建议为最低或较低优先级,以便让出 CPU 给实时任务,但不能被屏蔽)。

4. 特殊情况:如果不使用 SysTick 怎么办?

如果你因特殊原因不能使用 SysTick(例如需要将其用于其他高精度定时),你必须‌重写时间基准‌:

  1. 使用其他定时器‌:选择一个通用定时器(如 TIM2, TIM6 等),配置为每 1ms 产生一次更新中断。
  2. 重写弱符号函数‌:
    在你的代码中重新定义 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)

一、核心依据

  1. 标准初始化流程‌:原子例程的 main.c 开头必调用 HAL_Init()。该函数内部执行 HAL_SYSTICK_Config(),配置 SysTick 为 1ms 中断源,并使能 NVIC。
  2. 超时机制依赖‌:你提到的 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 中断 + 轮询查询‌ 的混合方式。

一、核心机制解析

  1. 时间基准更新(中断方式)(验证正确2026.6.16
    SysTick 定时器配置为每 1ms 产生一次中断。在中断服务函数 SysTick_Handler 中,调用 HAL_IncTick() 使全局变量 uwTick 自增。这是‌中断驱动‌的部分,确保时间计数在后台准确运行。

  2. 延时等待逻辑(查询方式)(源代码不是这样的,但是确实是没用到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

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐