摘要
本文基于NUCLEO‑C542RC开发板(STM32C542RCT6,Cortex‑M33内核),实现一套入门级裸机多任务Demo。不使用HAL_Delay()阻塞延时,依靠HAL_GetTick()系统节拍实现协作式超级循环调度;完成LED快慢闪烁、按键切换模式、USART2串口命令控制、周期状态上报。适合STM32裸机入门,理解GPIO、串口、按键消抖、裸机任务调度思想。工程:Project2/test_cmake 主控:STM32C542RCT6(Cortex‑M33)
一、程序实现功能
PA5引脚LED默认快速闪烁,每500ms电平翻转一次,完整亮灭周期1000ms;板载PC13按键按下,在快速闪烁、慢速闪烁两种模式之间切换;USART2支持接收单字符串口命令,可以切换闪烁模式、查询设备运行状态;USART2每500ms自动上报当前工作模式与LED状态;全部业务逻辑采用超级循环轮询,全程不使用HAL_Delay()阻塞等待,多个任务近似并行运行。概念区分:
- 翻转间隔:引脚电平翻转的时间;快速模式500ms翻转,慢速模式2000ms翻转。
- 完整闪烁周期:亮→灭→亮,快速模式1000ms,慢速模式4000ms。
二、学习目标
STM32从上电复位到执行main函数完整启动流程;HAL_GetTick()
三、硬件外设配置
⚠️重要提示:PC13引脚没有开启芯片内部上下拉电阻,按键电平依靠开发板外部硬件电路。如果移植到自制PCB,一定要确认按键电平逻辑;如果按下输出低电平,需要修改代码里按键判断逻辑。
GPIO配置:generated/hal/mx_gpio_default.h/.cUSART2配置:generated/hal/mx_usart2.h/.c时钟配置:generated/hal/mx_rcc.c3.1STM32CubeMX配置
咱们以PA5引脚为例,该引脚连接的是LED灯,高电平点亮,所以在右边将GPIO修改为Configured,如果点击齿轮图标进入设置。四、工程文件分工说明
| | |
|---|
| | |
| | |
| 系统总初始化:MPU、HAL、时钟、GPIO、串口初始化调用链 | |
| | |
| | |
| HSE、PSIS、总线分频、Flash等待周期配置 | |
| | |
| | |
| | |
| | |
| | |
注意:generated/hal目录全部属于工具自动生成代码,业务代码写在main.c。重新生成工程的时候,不要覆盖用户编写的业务逻辑。
五、程序上电运行流程
开发板上电/硬件复位,跳转到启动文件Reset_Handler复位处理函数;设置栈空间,执行SystemInit(),C运行库初始化,清零bss段、初始化data段,跳转main();main函数首先执行mx_system_init()系统总初始化,依次初始化MPU、HAL库、SysTick、NVIC中断分组、ICACHE指令缓存、144M系统时钟、GPIO引脚、USART2外设;执行app_init()应用初始化:获取串口句柄、初始化按键状态、串口打印欢迎提示,设置LED初始为快速闪烁模式;进入while(1)无限超级循环,循环依次执行:串口任务、按键扫描任务、LED闪烁任务、串口周期上报任务;每个任务执行完毕立刻返回,不会阻塞主循环,循环往复执行。SysTick说明:SysTick产生1ms中断,调用HAL_IncTick()递增节拍计数器;业务代码调用HAL_GetTick()读取时间戳做软件计时。本Demo只有SysTick使用中断,LED、按键、串口全部轮询处理,不使用额外外设中断。
六、核心数据与状态设计
6.1 模式枚举
使用枚举定义LED工作模式,代替裸数字,可读性更强。typedef enum{ LED_MODE_FAST = 0, LED_MODE_SLOW} led_mode_t;
6.2 时间宏定义
全部时间参数使用宏定义,避免代码魔法数字,方便实验修改参数。6.3 关键状态变量
全部使用static修饰,作用域限定main.c,程序全程保存状态。七、核心业务逻辑讲解
7.1 串口发送封装 uart_send
对HAL_UART_Transmit简单封装,统一处理判空、字符串长度、超时参数。教学Demo没有处理返回错误,正式产品需要增加错误判断。7.2 set_mode 模式切换统一入口
不管是按键触发切换还是串口命令切换模式,全部调用同一个set_mode()函数:好处:一套逻辑多处复用,不会出现按键和串口切换行为不一致bug。7.3 led_task无阻塞LED闪烁
核心逻辑:获取当前系统tick,和上一次翻转时间做无符号减法,判断间隔时间到达。不会原地等待延时,不阻塞主循环。重点:使用uint32无符号数做时间差值计算,可以天然兼容32位SysTick计数器溢出回绕问题。
7.4 button_task按键消抖状态机
机械按键按下松开存在电平抖动,如果直接读取引脚电平,一次物理按下会识别多次触发。 处理逻辑:如果电平保持不变,判断电平稳定持续时间超过20ms;变为高电平(按下):button_armed允许触发则切换模式,关闭armed;变为低电平(松开):重新打开armed,等待下一次按键。button_armed变量用来解决长按重复触发:只有松开按键之后,才允许下一次按下动作生效。实现按一次切换一次。
7.5 uart_task单字符串口命令解析
串口接收超时设置为0,没有收到字符函数立刻返回,不阻塞超级循环。支持单字符命令:局限:只支持单字符,没有接收缓冲区,不支持多字符串指令,适合入门演示。
7.6 uart_report_task周期上报任务
和LED任务完全相同的无阻塞tick判断逻辑,每500ms打印一次设备状态。知识点:裸机增加周期任务,不需要新增定时器,只需要给每个任务维护一个“上一次执行的时间戳”。
7.7 超级循环主while(1)
while (1){ uart_task(); button_task(HAL_GetTick()); led_task(HAL_GetTick()); uart_report_task(HAL_GetTick());}
每一个任务执行完马上返回,不会卡死循环。如果使用HAL_Delay(),CPU卡在延时函数里面,按键扫描和串口命令全部得不到响应,系统卡顿。注意:本Demo串口发送是阻塞式,短报文影响很小;大批量发送数据建议改成中断/DMA。
八、源代码
下面是完整main.c源码
/** ****************************************************************************** * file : main.c * brief : Main program body * Calls target system initialization then loop in main. ****************************************************************************** * * Copyright (c) 2025 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** *//* Includes ------------------------------------------------------------------*/#include"main.h"#include<stdbool.h>#include<string.h>/* Private typedef -----------------------------------------------------------*/typedef enum{ LED_MODE_FAST = 0, LED_MODE_SLOW} led_mode_t;/* Private define ------------------------------------------------------------*/#define LED_FAST_INTERVAL_MS 500U#define LED_SLOW_INTERVAL_MS 2000U#define BUTTON_DEBOUNCE_MS 20U#define UART_REPORT_INTERVAL_MS 500U#define UART_TX_TIMEOUT_MS 100U/* Private macro -------------------------------------------------------------*//* Private variables ---------------------------------------------------------*/static hal_uart_handle_t *uart2_handle;static led_mode_t current_mode;static uint32_t led_interval_ms;static uint32_t led_last_toggle_tick;static bool led_is_on;static uint32_t uart_last_report_tick;static bool button_raw_state;static bool button_stable_state;static bool button_armed;static uint32_t button_change_tick;/* Private functions prototype -----------------------------------------------*/staticvoidapp_init(void);staticvoidled_task(uint32_t now);staticvoidbutton_task(uint32_t now);staticvoiduart_task(void);staticvoiduart_report_task(uint32_t now);staticvoidset_mode(led_mode_t mode, constchar *source);staticvoidtoggle_mode(constchar *source);staticvoidprint_status(void);staticvoidprint_help(void);staticvoiduart_send(constchar *text);/** * @brief Send a null-terminated string through USART2. * @param text String to send. * @retval None */staticvoiduart_send(constchar *text){ if ((uart2_handle != NULL) && (text != NULL)) { (void)HAL_UART_Transmit(uart2_handle, text, (uint32_t)strlen(text), UART_TX_TIMEOUT_MS); }}/** * @brief Print the supported single-character commands. * @retval None */staticvoidprint_help(void){ uart_send("Commands:\r\n"); uart_send(" 1 - FAST mode (500ms ON / 500ms OFF)\r\n"); uart_send(" 2 - SLOW mode (2000ms ON / 2000ms OFF)\r\n"); uart_send(" T - Toggle FAST/SLOW mode\r\n"); uart_send(" S - Show current status\r\n"); uart_send(" H - Show this help\r\n");}/** * @brief Print the current mode and LED output state. * @retval None */staticvoidprint_status(void){ if (current_mode == LED_MODE_FAST) { uart_send("STATUS: MODE=FAST, INTERVAL=500ms, LED="); } else { uart_send("STATUS: MODE=SLOW, INTERVAL=2000ms, LED="); } uart_send(led_is_on ? "ON\r\n" : "OFF\r\n");}/** * @brief Select a blink mode and restart the blink cycle from LED ON. * @param mode Requested LED mode. * @param source Text describing the source of the mode change. * @retval None */staticvoidset_mode(led_mode_t mode, constchar *source){ current_mode = mode; led_interval_ms = (mode == LED_MODE_FAST) ? LED_FAST_INTERVAL_MS : LED_SLOW_INTERVAL_MS; HAL_GPIO_WritePin(PA5_PORT, PA5_PIN, PA5_ACTIVE_STATE); led_is_on = true; led_last_toggle_tick = HAL_GetTick(); if (mode == LED_MODE_FAST) { uart_send("MODE: FAST, 500ms ON / 500ms OFF, SOURCE: "); } else { uart_send("MODE: SLOW, 2000ms ON / 2000ms OFF, SOURCE: "); } uart_send(source); uart_send("\r\n");}/** * @brief Toggle between the fast and slow blink modes. * @param source Text describing the source of the mode change. * @retval None */staticvoidtoggle_mode(constchar *source){ set_mode((current_mode == LED_MODE_FAST) ? LED_MODE_SLOW : LED_MODE_FAST, source);}/** * @brief Initialize application state and print the startup banner. * @retval None */staticvoidapp_init(void){ hal_gpio_pin_state_t button_pin_state; uart2_handle = mx_usart2_uart_gethandle(); button_pin_state = HAL_GPIO_ReadPin(PC13_PORT, PC13_PIN); button_raw_state = (button_pin_state == HAL_GPIO_PIN_SET); button_stable_state = button_raw_state; button_armed = !button_stable_state; button_change_tick = HAL_GetTick(); uart_send("\r\nSTM32C542 LED Mode Demo\r\n"); uart_send("UART2: 115200 baud, 8 data bits, no parity, 1 stop bit\r\n"); print_help(); set_mode(LED_MODE_FAST, "POWER-ON"); uart_last_report_tick = HAL_GetTick();}/** * @brief Toggle the LED when the selected software interval expires. * @param now Current HAL tick in milliseconds. * @retval None */staticvoidled_task(uint32_t now){ if ((uint32_t)(now - led_last_toggle_tick) >= led_interval_ms) { HAL_GPIO_TogglePin(PA5_PORT, PA5_PIN); led_is_on = !led_is_on; led_last_toggle_tick = now; }}/** * @brief Debounce the active-high button and toggle once per press. * @param now Current HAL tick in milliseconds. * @retval None */staticvoidbutton_task(uint32_t now){ bool raw_state = (HAL_GPIO_ReadPin(PC13_PORT, PC13_PIN) == HAL_GPIO_PIN_SET); if (raw_state != button_raw_state) { button_raw_state = raw_state; button_change_tick = now; } if ((button_raw_state != button_stable_state) && ((uint32_t)(now - button_change_tick) >= BUTTON_DEBOUNCE_MS)) { button_stable_state = button_raw_state; if (button_stable_state) { if (button_armed) { button_armed = false; toggle_mode("KEY"); } } else { button_armed = true; } }}/** * @brief Poll and process one USART2 command character. * @retval None */staticvoiduart_task(void){ uint8_t command; if (HAL_UART_Receive(uart2_handle, &command, 1U, 0U) != HAL_OK) { return; } switch (command) { case '1': set_mode(LED_MODE_FAST, "UART"); break; case '2': set_mode(LED_MODE_SLOW, "UART"); break; case 'T': case 't': toggle_mode("UART"); break; case 'S': case 's': print_status(); break; case 'H': case 'h': print_help(); break; case '\r': case '\n': break; default: uart_send("ERROR: Unknown command. Send H for help.\r\n"); break; }}/** * @brief Periodically report the current mode and LED state through USART2. * @param now Current HAL tick in milliseconds. * @retval None */staticvoiduart_report_task(uint32_t now){ if ((uint32_t)(now - uart_last_report_tick) >= UART_REPORT_INTERVAL_MS) { uart_last_report_tick = now; print_status(); }}/** * brief: The application entry point. * retval: none but we specify int to comply with C99 standard */intmain(void){ /** System Init: this code placed in targets folder initializes your system. * It calls the initialization (and sets the initial configuration) of the peripherals. * You can use STM32CubeMX to generate and call this code or not in this project. * It also contains the HAL initialization and the initial clock configuration. */ if (mx_system_init() != SYSTEM_OK) { return (-1); } else { app_init(); while (1) { uart_task(); button_task(HAL_GetTick()); led_task(HAL_GetTick()); uart_report_task(HAL_GetTick()); } }} /* end main */
九、实物演示章节
9.1 硬件接线准备
USART2:确认开发板跳线,把PA2(TX)、PA3(RX)连接板载ST‑Link虚拟串口;PC端打开串口调试助手,串口参数:115200 8‑N‑1,无流控。9.2 演示操作步骤
CMake编译工程test_cmake,烧录固件到开发板;按下开发板复位按键,串口终端打印启动欢迎信息与命令帮助;物理按下PC13用户按键,观察LED切换快慢闪烁模式;长按按键,确认不会反复切换;松开按键才可以再次切换;观察串口每隔500ms自动输出STATUS状态信息。9.3 现象参考,串口上电打印示例
STM32C542 LED Mode DemoUART2: 115200 baud, 8 data bits, no parity, 1 stop bitCommands: 1 - FAST mode (500ms ON / 500ms OFF) 2 - SLOW mode (2000ms ON / 2000ms OFF) T - Toggle FAST/SLOW mode S - Show current status H - Show this helpMODE: FAST, 500ms ON / 500ms OFF, SOURCE: POWER-ONSTATUS: FAST ,LED ONSTATUS: FAST ,LED OFF……
提示:输入字符是否回显取决于串口工具的本地回显选项,不属于开发板程序问题。
十、可动手实验
实验1 修改闪烁速度:修改LED_FAST_INTERVAL_MS为100ms,预测并且观察LED实际周期;
实验2 去掉按键消抖:删除20ms消抖逻辑,直接读到高电平就切换模式,观察按键抖动带来多次误触发;
实验3 阻塞延时对比实验:while循环加入HAL_Delay(2000),体验阻塞延时下按键、串口完全失去响应;之后恢复原代码;
实验4 新增第三种闪烁模式:枚举增加MEDIUM中等模式,扩展set_mode函数、增加串口命令;练习状态管理;
实验5 串口扩展整行命令:增加字符接收缓冲区,实现fast、slow字符串指令;学习缓冲区处理;
实验6 改成中断/DMA实现:按键改为EXTI外部中断,串口改成中断或者DMA收发,对比轮询、中断、DMA三者CPU开销、代码复杂度差异。
十一、常见问题排查
1. LED不亮
调试看mx_system_init()返回值,初始化失败会直接退出main;使用调试器断点,观察led_is_on变量以及GPIO输出寄存器状态。2. 按键无反应、频繁误触发
PC13没有内部上下拉电阻,自制板必须外部硬件偏置;代码逻辑默认按下是高电平;如果硬件按下输出低电平,需要翻转判断条件;调试监视button_raw_state、button_stable_state、button_armed变量。3. 串口没有任何输出
确认串口工具波特率115200‑8‑N‑1,COM口选择正确;确认板载ST‑Link跳线已经把PA2 PA3接到虚拟串口;调试查看uart2_handle句柄是否有效,USART初始化是否成功。4. 串口发送命令程序没有反应
发送ASCII字符'1',不要发送十六进制0x01;5. 串口打印信息刷屏
UART_REPORT_INTERVAL_MS设置500ms,上报频率高;调大这个时间宏,或者临时注释掉uart_report_task()调用。十二、总结
这个Demo是从简单点灯过渡到复杂裸机工程非常经典示例:SysTick提供1ms全局时间基准,主循环轮询多个短任务;LED靠时间戳翻转引脚,按键依靠时间窗口做软件消抖,串口任务接收外部命令修改系统状态。系统全部历史状态保存到状态变量,while循环每一轮只向前推进一小步。