当前位置:首页>排行榜>STM32C542开发板评测:按键与串口双控LED,实现两种闪烁模式

STM32C542开发板评测:按键与串口双控LED,实现两种闪烁模式

  • 更新时间 2026-09-21 18:09:53
STM32C542开发板评测:按键与串口双控LED,实现两种闪烁模式

摘要

本文基于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。
视频讲解:

二、学习目标

通过本Demo可以掌握如下嵌入式裸机核心知识点:
STM32从上电复位到执行main函数完整启动流程;
HAL自动生成代码与用户业务代码分工边界;
GPIO输入输出配置、USART串口基础收发;

HAL_GetTick()

实现无阻塞软件定时原理;
机械按键软件消抖、单次按下单次触发逻辑;
裸机超级循环架构,多短任务协作调度;
枚举、状态变量、模块化函数在嵌入式中的实践。

三、硬件外设配置

项目
配置参数
用途说明
开发板
NUCLEO‑C542RC
硬件载体
MCU
STM32C542RCT6 Cortex‑M33
主控芯片
系统时钟
HSE 24MHz,PSIS倍频至144MHz
CPU、AHB、APB总线全部144MHz
Flash
256KiB,起始地址0x08000000
存放程序代码、中断向量表
RAM
64KiB,起始地址0x20000000
全局变量、栈、堆空间
LED
PA5,推挽输出,无上下拉,高电平点亮
状态指示LED
用户按键
PC13,普通输入,无内部上下拉
模式切换按键,高电平判定按下
USART2_TX
PA2 复用AF7
串口发送
USART2_RX
PA3 复用AF7
串口接收
串口参数
115200‑8‑N‑1,无硬件流控
和PC串口终端通信
时间基准
SysTick 1kHz
1ms节拍,为HAL_GetTick提供计时

⚠️重要提示:PC13引脚没有开启芯片内部上下拉电阻,按键电平依靠开发板外部硬件电路。如果移植到自制PCB,一定要确认按键电平逻辑;如果按下输出低电平,需要修改代码里按键判断逻辑。

外设配置文件位置:
GPIO配置:generated/hal/mx_gpio_default.h/.c
USART2配置:generated/hal/mx_usart2.h/.c
时钟配置:generated/hal/mx_rcc.c

3.1STM32CubeMX配置

进入软件后,是一个引脚配置界面,可以进行引脚配置
咱们以PA5引脚为例,该引脚连接的是LED灯,高电平点亮,所以在右边将GPIO修改为Configured,如果点击齿轮图标进入设置。
在设置界面将其配置为推挽输出
然后咱们还使用了串口,需要将串口进行打开
完成之后,配置一下时钟,
然后再工程设置界面,生成工程即可。

四、工程文件分工说明

文件
主要作用
是否重点学习
main.c
用户业务逻辑:LED、按键、串口命令、主超级循环
✅核心重点
main.h
汇总生成外设驱动头文件
⭐简单了解
mx_system.c
系统总初始化:MPU、HAL、时钟、GPIO、串口初始化调用链
✅重点
mx_gpio_default.c
PA5输出、PC13输入引脚初始化
✅重点
mx_usart2.c
USART2外设、引脚复用功能配置
✅重点
mx_rcc.c
HSE、PSIS、总线分频、Flash等待周期配置
⭐了解
mx_icache.c
指令ICACHE缓存初始化
⭐了解
mx_cortex_mpu.c
MPU内存保护配置
📚进阶内容
startup_stm32c542xx.c
启动文件,复位中断向量、栈初始化
✅启动流程讲解使用
stm32c542xc_flash.ld
链接脚本,定义ROM、RAM、栈堆大小
📚进阶内容
CMakeLists.txt
CMake编译构建脚本
📚构建系统讲解

注意: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_SLOWled_mode_t;

6.2 时间宏定义

全部时间参数使用宏定义,避免代码魔法数字,方便实验修改参数。
数值
含义
LED_FAST_INTERVAL_MS
500
快速模式LED翻转间隔ms
LED_SLOW_INTERVAL_MS
2000
慢速模式LED翻转间隔ms
BUTTON_DEBOUNCE_MS
20
按键消抖稳定时间
UART_REPORT_INTERVAL_MS
500
串口自动上报状态间隔
UART_TX_TIMEOUT_MS
100
阻塞串口发送超时时间

6.3 关键状态变量

全部使用static修饰,作用域限定main.c,程序全程保存状态。
变量
功能
uart2_handle
USART2外设HAL句柄
current_mode
当前LED快慢模式
led_interval_ms
当前模式对应的翻转时间
led_last_toggle_tick
LED上一次翻转的时间戳
led_is_on
软件镜像记录LED亮灭状态,用于串口打印
uart_last_report_tick
串口上次自动上报时间戳
button_raw_state
按键原始读取电平
button_stable_state
消抖之后确认稳定电平
button_change_tick
按键电平最近跳变时刻
button_armed
按键松开之后才允许下次触发,防止长按重复触发

七、核心业务逻辑讲解

7.1 串口发送封装 uart_send

HAL_UART_Transmit简单封装,统一处理判空、字符串长度、超时参数。教学Demo没有处理返回错误,正式产品需要增加错误判断。

7.2 set_mode 模式切换统一入口

不管是按键触发切换还是串口命令切换模式,全部调用同一个set_mode()函数:
保存新的闪烁模式;
更新LED翻转时间间隔;
立刻点亮LED,重置计时时间戳;
串口打印切换来源(上电/按键/串口)。
好处:一套逻辑多处复用,不会出现按键和串口切换行为不一致bug。

7.3 led_task无阻塞LED闪烁

核心逻辑:获取当前系统tick,和上一次翻转时间做无符号减法,判断间隔时间到达。不会原地等待延时,不阻塞主循环

重点:使用uint32无符号数做时间差值计算,可以天然兼容32位SysTick计数器溢出回绕问题。

7.4 button_task按键消抖状态机

机械按键按下松开存在电平抖动,如果直接读取引脚电平,一次物理按下会识别多次触发。 处理逻辑:
读取PC13原始引脚电平;
如果电平发生跳变,记录跳变时间,不立刻判定按下;
如果电平保持不变,判断电平稳定持续时间超过20ms;
确认稳定电平发生变化;
变为高电平(按下):button_armed允许触发则切换模式,关闭armed;
变为低电平(松开):重新打开armed,等待下一次按键。

button_armed变量用来解决长按重复触发:只有松开按键之后,才允许下一次按下动作生效。实现按一次切换一次。

7.5 uart_task单字符串口命令解析

串口接收超时设置为0,没有收到字符函数立刻返回,不阻塞超级循环。支持单字符命令:
命令
功能
1
切换快速闪烁模式
2
切换慢速闪烁模式
t/T
切换快慢模式
s/S
打印当前运行状态
h/H
打印帮助菜单
回车换行
忽略

局限:只支持单字符,没有接收缓冲区,不支持多字符串指令,适合入门演示。

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_SLOWled_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, 1U0U) != 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 硬件接线准备

硬件:NUCLEO‑C542RC开发板;
USART2:确认开发板跳线,把PA2(TX)、PA3(RX)连接板载ST‑Link虚拟串口;
PC端打开串口调试助手,串口参数:115200 8‑N‑1,无流控

9.2 演示操作步骤

CMake编译工程test_cmake,烧录固件到开发板;
按下开发板复位按键,串口终端打印启动欢迎信息与命令帮助;
观察PA5 LED上电默认快速闪烁;
物理按下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 串口扩展整行命令:增加字符接收缓冲区,实现fastslow字符串指令;学习缓冲区处理;

实验6 改成中断/DMA实现:按键改为EXTI外部中断,串口改成中断或者DMA收发,对比轮询、中断、DMA三者CPU开销、代码复杂度差异。

十一、常见问题排查

1. LED不亮

确认硬件LED对应引脚PA5;确认高电平点亮;
调试看mx_system_init()返回值,初始化失败会直接退出main;
使用调试器断点,观察led_is_on变量以及GPIO输出寄存器状态。

2. 按键无反应、频繁误触发

PC13没有内部上下拉电阻,自制板必须外部硬件偏置;
代码逻辑默认按下是高电平;如果硬件按下输出低电平,需要翻转判断条件;
调试监视button_raw_statebutton_stable_statebutton_armed变量。

3. 串口没有任何输出

TX/RX交叉接线,设备和PC必须共地;
确认串口工具波特率115200‑8‑N‑1,COM口选择正确;
确认板载ST‑Link跳线已经把PA2 PA3接到虚拟串口;
调试查看uart2_handle句柄是否有效,USART初始化是否成功。

4. 串口发送命令程序没有反应

发送ASCII字符'1',不要发送十六进制0x01;
T/S/H大小写都支持;换行回车会被程序忽略;
确认串口工具没有开启十六进制发送模式。

5. 串口打印信息刷屏

UART_REPORT_INTERVAL_MS设置500ms,上报频率高;调大这个时间宏,或者临时注释掉uart_report_task()调用。

十二、总结

这个Demo是从简单点灯过渡到复杂裸机工程非常经典示例:SysTick提供1ms全局时间基准,主循环轮询多个短任务;LED靠时间戳翻转引脚,按键依靠时间窗口做软件消抖,串口任务接收外部命令修改系统状态。
记住两句核心裸机思想:
任务不去等待时间,只判断时间是否已经到达
系统全部历史状态保存到状态变量,while循环每一轮只向前推进一小步。

随机文章