stm32f1xx HAL库 freertos 移植letter-shell与log实战:从零到终端调试
1. 环境准备与工程创建
在开始移植之前,我们需要准备好开发环境。我使用的是STM32CubeMX和Keil MDK-ARM,这两个工具是STM32开发的黄金搭档。STM32CubeMX可以图形化配置芯片引脚和中间件,大大简化了初始化工作。
首先打开STM32CubeMX,选择STM32F103C8T6芯片(这是最常用的F1系列芯片)。在Pinout界面中,我们需要配置几个关键部分:
- RCC:选择外部高速晶振,这样系统时钟更稳定
- SYS:调试接口选择SW,因为我们要用ST-LINK下载调试
- USART1:选择异步模式,波特率保持115200,开启全局中断
配置FreeRTOS时有个重要细节:系统滴答时钟默认使用SysTick,但HAL库也需要用SysTick,这会产生冲突。我的做法是切换到TIM1作为时基,这样就不会有警告了。
时钟树配置也很关键。STM32F103的最高主频是72MHz,我们需要通过PLL倍频达到这个频率。具体设置是:外部8MHz晶振,经过PLL倍频9倍,得到72MHz系统时钟。
最后设置工程目录时,我习惯单独创建一个"Middlewares"文件夹存放第三方组件,这样项目结构更清晰。生成代码时选择MDK-ARM V5,虽然我用VSCode写代码,但编译还是用Keil更稳定。
提示:记得在工程设置中勾选"Use MicroLIB",这样串口打印才能正常工作。
2. letter-shell移植详解
letter-shell是一个轻量级嵌入式Shell工具,特别适合资源有限的STM32芯片。我选择3.12版本是因为它稳定且功能完善。
首先从GitHub下载源码,将src目录下的所有文件复制到工程中。重点要修改的是shell_port.c文件,这里需要实现Shell的读写接口。
// shell_port.c
short userShellWrite(char *data, unsigned short len) {
return HAL_UART_Transmit(&huart1, (uint8_t *)data, len, 100);
}
写函数比较简单,直接调用HAL库的串口发送函数。读函数就比较讲究了,我建议使用中断方式接收数据,而不是阻塞读取。阻塞读取在RTOS中容易导致任务卡死。
配置shell_cfg.h时需要根据实际需求调整参数。我通常启用这些功能:
#define SHELL_TASK_WHILE 1 // 使用任务循环
#define SHELL_USING_CMD_EXPORT 1 // 使用命令导出
#define SHELL_ENTER_CR 1 // 支持回车键
#define SHELL_ENTER_LF 1 // 支持换行键
#define SHELL_PRINT_BUFFER 128 // 输出缓冲区大小
在FreeRTOS中创建Shell任务时,我给了256字节的栈空间,优先级设为普通。关键是要在串口中断回调函数中处理数据接收:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
shellHandler(&shell, received_data);
HAL_UART_Receive_IT(&huart1, &received_data, 1);
}
}
这样每收到一个字节就会触发中断,交给letter-shell处理。实测下来很稳定,不会丢失数据。
3. FreeRTOS集成与任务调度
在FreeRTOS环境中集成letter-shell需要注意任务调度问题。我创建了两个任务:一个Shell任务,一个应用任务。
Shell任务负责处理用户输入,应用任务用来测试其他功能。两个任务之间通过消息队列通信,这样不会互相阻塞。
// FreeRTOS配置
osThreadDef(myshell, shellTask, osPriorityNormal, 0, 256);
myshellHandle = osThreadCreate(osThread(myshell), NULL);
这里有个坑要注意:CubeMX生成的代码中,任务函数默认有const修饰符,需要去掉才能正常编译。
Shell任务函数需要适当加入延时,否则会占用太多CPU资源:
void shellTask(void const * argument) {
for(;;) {
osDelay(10); // 每10ms检查一次输入
}
}
在实际项目中,我还会给Shell任务设置看门狗,防止卡死。如果Shell任务5秒内没有运行,就会触发复位,这个机制在生产环境中很实用。
4. 调试技巧与常见问题解决
移植过程中最容易遇到的问题是串口无法输入。我踩过这个坑,原因是HAL库的返回值判断错误。
letter-shell默认判断读取函数返回值是否为1,但HAL_UART_Receive返回的是HAL_StatusTypeDef枚举值。正确的判断应该是:
if (shell->read && shell->read(&data, 1) == HAL_OK) {
shellHandler(shell, data);
}
另一个常见问题是输出乱码。这通常是时钟配置错误导致的。确保系统时钟、串口时钟都配置正确,特别是APB2总线时钟要是72MHz。
我用逻辑分析仪抓过波形,发现当系统时钟偏差时,波特率也会不准。建议用示波器测量实际波特率,确保与终端软件设置一致。
内存不足也是常见问题。letter-shell需要512字节缓冲区,FreeRTOS任务也需要栈空间。如果出现HardFault,可以先检查栈空间是否足够。
注意:调试时先确保纯串口收发正常,再集成letter-shell,这样可以排除硬件问题。
5. log模块集成与使用
log模块是letter-shell的扩展功能,可以提供分级日志输出。我将log.c和log.h添加到工程中,然后在shell_port.c中初始化。
Log uartLog = {
.write = uartLogWrite,
.active = LOG_ENABLE,
.level = LOG_DEBUG
};
void userShellInit(void) {
shell.write = userShellWrite;
shellInit(&shell, shellBuffer, 512);
logRegister(&uartLog, &shell);
}
log支持多种级别:DEBUG、INFO、WARN、ERROR等。在实际项目中,我通常在生产环境设置LOG_LEVEL为INFO,开发环境设置为DEBUG。
使用log非常简单:
logDebug("传感器读数: %d", sensor_value);
logError("系统初始化失败");
log还支持十六进制数据导出:
uint8_t data[4] = {0x01, 0x02, 0x03, 0x04};
logHexDumpAll(data, 4);
这个功能在调试通信协议时特别有用,可以直观查看原始数据。
6. 实际应用与性能优化
在实际项目中,letter-shell帮我节省了大量调试时间。我可以用命令行实时查看系统状态、修改变量值、调用测试函数。
比如我经常用这些命令:
task status # 查看任务运行状态
mem info # 查看内存使用情况
log level 3 # 设置日志级别
性能方面,我做了这些优化:
- 减小打印缓冲区到128字节,足够一般使用
- 关闭历史记录功能节省内存
- 使用短命令名减少输入时间
- 将常用命令编译进固件,不常用命令通过文件系统加载
资源占用情况:整个letter-shell加上log模块,占用约3KB Flash和1KB RAM,在STM32F103上完全可接受。
稳定性方面,连续运行72小时压力测试没有出现丢数据或卡死现象。我做了这些测试:
- 长时间高速输入命令
- 同时进行文件传输和命令操作
- 在低电压环境下测试
- 高温环境下运行
7. 高级功能与扩展应用
letter-shell还有一些高级功能很实用。比如命令自动补全,按Tab键可以提示可用命令。还有参数解析功能,支持字符串、数字等多种参数类型。
我扩展了一个文件系统支持,可以通过命令行读写SD卡文件:
cat log.txt # 查看日志文件
rm old_file.bin # 删除文件
ls / # 列出根目录
另一个实用功能是执行脚本。我可以把常用命令序列保存成脚本,一次性执行:
# debug.txt
task status
mem info
log level 4
然后通过命令执行:exec debug.txt
对于远程调试,我增加了TCP支持。通过ESP8266模块,可以用Telnet连接设备,实现无线调试。这个功能在现场调试时特别方便,不用插串口线了。
最后,我建议增加权限管理功能。不同用户有不同的操作权限,生产环境只开放基本命令,开发环境开放全部命令。这样既方便又安全。
letter-shell的模块化设计让这些扩展都很容易实现。我在实际项目中根据需求灵活选择功能,既满足需求又不浪费资源。
更多推荐
所有评论(0)