NUCLEO-WBA65RI开发板是STMicroelectronics推出的一款基于STM32WBA52系列微控制器的评估板。作为一款面向无线应用的开发平台,它集成了蓝牙5.3低功耗功能,同时保留了传统STM32系列丰富的外设资源。本次测评将重点展示如何通过串口Shell实现LED流水灯控制,这种交互方式在实际嵌入式开发中具有典型意义。
在嵌入式系统开发中,Shell交互是一种高效的人机交互方式。通过串口终端输入命令控制硬件,可以快速验证功能、调试程序,而无需反复烧录固件。对于初学者而言,掌握这种开发模式能显著提升开发效率;对于有经验的工程师,完善的Shell框架更是项目后期维护和功能扩展的利器。
NUCLEO-WBA65RI开发板自带ST-LINK调试器,极大简化了开发环境搭建。我们需要准备的硬件包括:
开发板上的串口接口通过ST-LINK虚拟COM端口实现,连接电脑后会自动识别为串口设备。在Windows设备管理器中可以查看分配的COM端口号,这是后续终端连接的关键参数。
推荐使用以下工具组合:
特别提醒:安装STM32CubeWBA固件包时,建议通过STM32CubeIDE内置的包管理器下载,确保版本匹配。我在实际使用中发现,手动下载的固件包有时会出现头文件路径问题。
在STM32CubeMX中配置USART1为异步模式,参数设置为:
生成代码后,需要手动添加重定向代码实现printf输出到串口:
c复制#include <stdio.h>
int __io_putchar(int ch) {
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
采用简单高效的分段式命令解析方案:
c复制typedef struct {
const char *cmd; // 命令字符串
void (*func)(int argc, char *argv[]); // 处理函数
const char *help; // 帮助信息
} shell_cmd_t;
// 命令表
static const shell_cmd_t cmd_table[] = {
{"led", cmd_led_handler, "LED控制: led [on|off|blink] [pin]"},
{"flow", cmd_flow_handler, "流水灯控制: flow [start|stop] [interval_ms]"},
{"help", cmd_help_handler, "显示帮助信息"},
{NULL, NULL, NULL}
};
// 命令解析主函数
void shell_exec(char *cmdline) {
char *argv[SHELL_MAX_ARGS];
int argc = 0;
// 分割命令行参数
char *token = strtok(cmdline, " ");
while (token != NULL && argc < SHELL_MAX_ARGS) {
argv[argc++] = token;
token = strtok(NULL, " ");
}
// 查找并执行命令
for (int i = 0; cmd_table[i].cmd != NULL; i++) {
if (strcmp(argv[0], cmd_table[i].cmd) == 0) {
cmd_table[i].func(argc, argv);
return;
}
}
printf("未知命令,输入'help'查看支持命令\r\n");
}
注意:在实际项目中,建议添加命令历史记录和Tab补全功能。我在商业项目中曾使用FreeRTOS+CLI库,其成熟度更高但占用资源较多,对于WBA52这类资源有限的芯片,轻量级实现更为合适。
在CubeMX中配置用户LED引脚(PC13)为GPIO输出模式。生成代码后,建议封装LED操作接口:
c复制#define LED_PIN GPIO_PIN_13
#define LED_PORT GPIOC
void led_on(void) {
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);
}
void led_off(void) {
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
}
void led_toggle(void) {
HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
}
实现可调速的流水灯效果需要用到定时器中断。我们使用TIM2作为基础定时器:
c复制// 定时器初始化
void tim2_init(uint32_t interval_ms) {
__HAL_RCC_TIM2_CLK_ENABLE();
TIM_HandleTypeDef htim2 = {
.Instance = TIM2,
.Init = {
.Prescaler = 64000 - 1, // 64MHz/64000 = 1kHz
.CounterMode = TIM_COUNTERMODE_UP,
.Period = interval_ms - 1, // 中断周期
.ClockDivision = TIM_CLOCKDIVISION_DIV1,
.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE
}
};
HAL_TIM_Base_Init(&htim2);
HAL_TIM_RegisterCallback(&htim2, HAL_TIM_PERIOD_ELAPSED_CB_ID, tim2_callback);
HAL_TIM_Base_Start_IT(&htim2);
}
// 中断回调函数
void tim2_callback(TIM_HandleTypeDef *htim) {
static uint8_t state = 0;
switch(state++ % 4) {
case 0: led_on(); break;
case 1: led_off(); break;
case 2: led_on(); HAL_Delay(50); led_off(); break;
case 3: for(int i=0; i<3; i++) { led_toggle(); HAL_Delay(100); } break;
}
}
c复制void cmd_led_handler(int argc, char *argv[]) {
if (argc < 2) {
printf("用法: led [on|off|blink] [pin]\r\n");
return;
}
if (strcmp(argv[1], "on") == 0) {
led_on();
printf("LED已开启\r\n");
}
else if (strcmp(argv[1], "off") == 0) {
led_off();
printf("LED已关闭\r\n");
}
else if (strcmp(argv[1], "blink") == 0) {
uint32_t times = (argc > 2) ? atoi(argv[2]) : 5;
uint32_t delay = (argc > 3) ? atoi(argv[3]) : 200;
for (int i = 0; i < times; i++) {
led_toggle();
HAL_Delay(delay);
}
printf("已完成 %d 次闪烁\r\n", times);
}
}
c复制static uint8_t flow_running = 0;
void cmd_flow_handler(int argc, char *argv[]) {
if (argc < 2) {
printf("用法: flow [start|stop] [interval_ms]\r\n");
return;
}
if (strcmp(argv[1], "start") == 0) {
uint32_t interval = (argc > 2) ? atoi(argv[2]) : 500;
tim2_init(interval);
flow_running = 1;
printf("流水灯已启动,间隔 %dms\r\n", interval);
}
else if (strcmp(argv[1], "stop") == 0) {
HAL_TIM_Base_Stop_IT(&htim2);
flow_running = 0;
led_off();
printf("流水灯已停止\r\n");
}
}
在主循环中添加串口接收处理:
c复制char rx_buf[256];
uint8_t rx_pos = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (rx_buf[rx_pos-1] == '\r' || rx_buf[rx_pos-1] == '\n') {
rx_buf[rx_pos-1] = '\0';
shell_exec(rx_buf);
rx_pos = 0;
printf("\r\n> ");
}
HAL_UART_Receive_IT(huart, (uint8_t*)&rx_buf[rx_pos++], 1);
}
// 在main()初始化中启动接收
HAL_UART_Receive_IT(&huart1, (uint8_t*)&rx_buf[rx_pos++], 1);
printf("Shell已就绪,输入'help'查看命令\r\n> ");
考虑到WBA系列的蓝牙特性,可以添加低功耗模式支持:
c复制void enter_low_power(void) {
HAL_TIM_Base_Stop_IT(&htim2); // 关闭定时器
HAL_UART_DeInit(&huart1); // 关闭串口
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
// 配置唤醒源
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1_LOW);
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
// 唤醒后重新初始化
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
HAL_UART_Receive_IT(&huart1, (uint8_t*)&rx_buf[rx_pos++], 1);
printf("系统已唤醒\r\n> ");
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 串口无输出 | 波特率不匹配 | 检查终端和代码中的波特率设置是否一致 |
| 命令无响应 | 换行符设置错误 | 终端应设置为发送CR(\r)或CRLF(\r\n) |
| LED不亮 | 引脚配置错误 | 使用STM32CubeMX重新检查GPIO配置 |
| 定时器不准 | 时钟源配置错误 | 检查SystemClock_Config()中的时钟树配置 |
| 系统卡死 | 堆栈溢出 | 在启动文件中增加堆栈大小 |
__attribute__((section(".ramfunc")))将其放到RAM中执行__WFI()指令降低功耗const修饰并将其放到FLASH中节省RAM我在实际测试中发现,当流水灯间隔设置小于100ms时,频繁的中断会影响串口响应速度。解决方案是使用硬件PWM直接驱动LED,或者将定时器中断优先级设置为低于串口中断。
led on 2控制第二个LEDsave命令将当前配置保存到Flash,上电自动恢复run命令执行预存的命令序列info命令显示CPU利用率、内存状态等信息一个实用的技巧是为每个命令添加简写形式,例如led on可以简写为lo。这可以通过在命令表中添加别名实现:
c复制{"lo", cmd_led_handler, "led on的简写"},
{"lf", cmd_led_handler, "led off的简写"}
通过串口Shell控制LED虽然是个基础功能,但其中蕴含的架构思想可以扩展到更复杂的嵌入式系统。我在工业控制器项目中就采用类似的框架,通过Modbus协议替代串口,实现了产线设备的远程调试和维护。