STM32CubeMX Nucleo F767ZI 教程(1)
STM32CubeMX Nucleo F767ZI 教程(2)
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell
一般使用串口来打印调试信息之类的,正点原子的USMART也不错,这边引入了一个类似于Linux 命令行的调试功能,Letter Shell,功能很强大,github的链接在下面。
https://github.com/mcujackson/letter-shell
我们要移植一下这个功能,方便后面的调试。这个功能使用到了串口,接收到串口数据后回传到上位机,直到接收到命令行回车触发,也就是0x0A(LF 换行)以及0x0D(CR 回车),不同的软件,结束符可能也有点区别。确认收到完整的命令,就可以解析命令,根据不同函数的地址就可以进行调用了,这样子调试程序就很方便了。
效果图如下:
Letter Shell 仅需要用到串口,然后使用shell 的回调命令,就可以使用了,但是后续我们需要使用到FreeRTOS,这就涉及到中断优先级的问题了,STM32CubeMX有16个优先级,0为最高优先级,15为最低优先级,FreeRTOS可以管理管理包括优先级5-16的中断,我们这里先配置一下FreeRTOS,暂时不需要其他功能,默认配置有一个defaultTask的线程,我们用这个来控制LED以指示系统的正常运行,所以这个按照默认配置就可以了。
FreeRTOS需要使用时钟基,默认是SysTick,滴答定时器,默认配置是1ms中断一次,然后计时器+1,但是SysTick并不是很准确,我们可以在SYS 选项卡中改一下Timebase Source,这里我们改为定时器3当时钟基。
然后配置一下串口,因为Nucleo板载了ST-LINK V2.1,有一个虚拟串口连接到USART3,所以使能USART3,然后使能中断就可以了。点击GENERATE CODE生成代码。
先打开 “freertos.c”,里面有一个默认的线程,StartDefaultTask,我们在这里加一个LED闪烁的功能,先测试一下生成的工程没问题。
/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(LD3_GPIO_Port,LD3_Pin);
osDelay(100);
}
/* USER CODE END StartDefaultTask */
}
注意,配置的Keil工程,默认是没有设置下载完自动复位,我们需要按一下Nucleo 板子上面黑色的复位按钮,另外默认的配置下,有一个以太网的初始化 “MX_ETH_Init()”,需要接入网线到路由器,不然初始化不通过,会跳转到Error_Handler();
if (HAL_ETH_Init(&heth) != HAL_OK)
{
Error_Handler();
}
demo 有一些平台上面的例程
doc 是功能演示
extensions 有一些扩展功能
src 是源代码
tools 是一个用于遍历工程中命令导出的工具,位于tools/shellTools.py,需要python3环境运行,可以列出工程中,所有使用SHELL_EXPORT_XXX导出的命令名,以及位置,结合VS Code可以直接进行跳转
我们在STM32Cube 的工程项目下新建一个文件夹 Shell,然后把 src文件夹下的源代码都放进去,遵守开源公约,LICENSE也要记得放进去。放进去之后,demo\stm32-freertos 目录下还有三个文件,我们也拷贝过去覆盖即可。
然后在keil工程下添加这些文件。
编译一下,此时会报错
..\Shell\shell_cfg.h(15): error: #5: cannot open source input file "stm32f4xx_hal.h": No such file or directory
原因是letter shell的demo使用的是F4的板子, “shell_cfg.h”,里面包含了f4的头文件,#include “stm32f4xx_hal.h” 。我们这是 F7的板子,并且是STM32CubeMX生成的工程文件,所以我们只要替换成 #include “main.h”,这样改了一次以后,以后换芯片系列也能很方便的移植了。再次编译,仍然会报错。
..\Shell\shell_port.c(13): error: #5: cannot open source input file "serial.h": No such file or directory
这是因为 “shell_port.c” 中有 serial.h,看名字,这是与串口相关的文件,我们把这种不需要的头文件删掉,只留下两个头文件就可以了。
#include "shell.h"
#include "usart.h"
下面两个函数是shell读写的实现,里面使用的是固件库的函数。
/**
* @brief 用户shell写
*
* @param data 数据
*/
void userShellWrite(char data)
{
serialTransmit(&debugSerial, (uint8_t *)&data, 1, 0xFF);
}
/**
* @brief 用户shell读
*
* @param data 数据
* @return char 状态
*/
signed char userShellRead(char *data)
{
if (serialReceive(&debugSerial, (uint8_t *)data, 1, 0) == 1)
{
return 0;
}
else
{
return -1;
}
}
既然使用了STM32CubeMX来生成工程,这里我们改成HAL库的形式。
/**
* @brief 用户shell写
*
* @param data 数据
*/
void userShellWrite(char data)
{
HAL_UART_Transmit(&huart3,(uint8_t *)&data, 1,1000);
}
/**
* @brief 用户shell读
*
* @param data 数据
* @return char 状态
*/
signed char userShellRead(char *data)
{
if(HAL_UART_Receive(&huart3,(uint8_t *)data, 1, 0) == HAL_OK)
{
return 0;
}
else
{
return -1;
}
}
再次编译就不会出错了。
github 上有具体的使用说明。我这里是下载的最新版本,3.0.6,这里也是根据这个版本进行一些解释性的说明。
在 “shell_port.c” 中定义了 shell 的对象。这个Shell结构体定义了历史的输入,读写对象的函数指针等等。
Shell shell;
定义shell读,写函数,函数原型如下:
/**
* @brief shell读取数据函数原型
*
* @param char shell读取的字符
*
* @return char 0 读取数据成功
* @return char -1 读取数据失败
*/
typedef signed char (*shellRead)(char *);
/**
* @brief shell写数据函数原型
*
* @param const char 需写的字符
*/
typedef void (*shellWrite)(const char);
我们在上面已经进行了修改。
申请一片缓冲区
char shellBuffer[512];
调用shellInit进行初始化,函数实现如下:
/**
* @brief 用户shell初始化
*
*/
void userShellInit(void)
{
shell.write = userShellWrite;
shell.read = userShellRead;
shellInit(&shell, shellBuffer, 512);
}
以上步骤,除了要修改shell的读写函数,其他的在shell_port.c中都已经实现了,所以我们在main.c 添加头文件
/* USER CODE BEGIN Includes */
#include "shell_port.h"
/* USER CODE END Includes */
然后在 main 函数中初始化shell,基本上就移植完毕了。
/* USER CODE BEGIN 2 */
userShellInit(); //letter shell 的初始化
/* USER CODE END 2 */
此时编译一下,还是报错,老样子,是因为没有 “serial.h” 文件,我们双击报错点,跳到异常点,把这个注释掉即可。然后再次编译,就没有报错了。
..\Shell\shell_port.h(15): error: #5: cannot open source input file "serial.h": No such file or directory
我们把程序下载到Nucleo。
然后打开SecureCRT 或者 MobaXterm 或者 “putty”,建议是使用MobaXterm,因为这个功能强大,且有免费试用版。
我们需要建立一个串口连接, SecureCRT 点击快速连接, MobaXterm 点击 Session ,然后找到串口的选项,串口的端口就是 STLINK-V2.1的虚拟串口, 波特率为115200,其他的默认不变,点击 OK
由于我们还没编写串口接收方面的函数,按下字符是不会进行回传的,这里我们先按一下复位按钮,接上网线等到初始化完成后,就可以看到 letter shell 初始化完成后的串口信息。
第三节已经完成了Letter Shell的初始化,在这一步,已经能够shell初始化已经打印的功能,但是要想使用好letter shell,我们还需要对letter shell有进一步的了解。
在文件 “shell_cfg.h” 中定义了各种各样的宏,用户可以根据不同的需求来配置宏。
下面的表格列出了各个宏的意义。
宏 | 意义 |
---|---|
SHELL_TASK_WHILE | 是否使用默认shell任务while循环 |
SHELL_USING_CMD_EXPORT | 是否使用命令导出方式 |
SHELL_HELP_LIST_USER | 是否在输入命令列表中列出用户 |
SHELL_HELP_LIST_VAR | 是否在输入命令列表中列出变量 |
SHELL_HELP_LIST_KEY | 是否在输入命令列表中列出按键 |
SHELL_ENTER_LF | 使用LF作为命令行回车触发 |
SHELL_ENTER_CR | 使用CR作为命令行回车触发 |
SHELL_ENTER_CRLF | 使用CRLF作为命令行回车触发 |
SHELL_COMMAND_MAX_LENGTH | shell命令最大长度 |
SHELL_PARAMETER_MAX_NUMBER | shell命令参数最大数量 |
SHELL_HISTORY_MAX_NUMBER | 历史命令记录数量 |
SHELL_DOUBLE_CLICK_TIME | 双击间隔(ms) |
SHELL_MAX_NUMBER | 管理的最大shell数量 |
SHELL_GET_TICK() | 获取系统时间(ms) |
SHELL_SHOW_INFO | 是否显示shell信息 |
SHELL_CLS_WHEN_LOGIN | 是否在登录后清除命令行 |
SHELL_DEFAULT_USER | shell默认用户 |
SHELL_DEFAULT_USER_PASSWORD | 默认用户密码 |
SHELL_LOCK_TIMEOUT | shell自动锁定超时 |
在文件 “shell_cfg.h” 也有具体的说明。
/**
* @brief 是否使用默认shell任务while循环,使能宏`SHELL_USING_TASK`后此宏有意义
* 使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
* 任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`
*/
#define SHELL_TASK_WHILE 1
/**
* @brief 是否使用命令导出方式
* 使能此宏后,可以使用`SHELL_EXPORT_CMD()`等导出命令
* 定义shell命令,关闭此宏的情况下,需要使用命令表的方式
*/
#define SHELL_USING_CMD_EXPORT 1
/**
* @brief 是否使用shell伴生对象
* 一些扩展的组件(文件系统支持,日志工具等)需要使用伴生对象
*/
#define SHELL_USING_COMPANION 0
/**
* @brief 支持shell尾行模式
*/
#define SHELL_SUPPORT_END_LINE 0
/**
* @brief 是否在输出命令列表中列出用户
*/
#define SHELL_HELP_LIST_USER 0
/**
* @brief 是否在输出命令列表中列出变量
*/
#define SHELL_HELP_LIST_VAR 0
/**
* @brief 是否在输出命令列表中列出按键
*/
#define SHELL_HELP_LIST_KEY 0
/**
* @brief 是否在输出命令列表中展示命令权限
*/
#define SHELL_HELP_SHOW_PERMISSION 1
/**
* @brief 使用LF作为命令行回车触发
* 可以和SHELL_ENTER_CR同时开启
*/
#define SHELL_ENTER_LF 0
/**
* @brief 使用CR作为命令行回车触发
* 可以和SHELL_ENTER_LF同时开启
*/
#define SHELL_ENTER_CR 0
/**
* @brief 使用CRLF作为命令行回车触发
* 不可以和SHELL_ENTER_LF或SHELL_ENTER_CR同时开启
*/
#define SHELL_ENTER_CRLF 1
/**
* @brief 使用执行未导出函数的功能
* 启用后,可以通过`exec [addr] [args]`直接执行对应地址的函数
* @attention 如果地址错误,可能会直接引起程序崩溃
*/
#define SHELL_EXEC_UNDEF_FUNC 0
/**
* @brief shell命令参数最大数量
* 包含命令名在内,超过8个参数并且使用了参数自动转换的情况下,需要修改源码
*/
#define SHELL_PARAMETER_MAX_NUMBER 8
/**
* @brief 历史命令记录数量
*/
#define SHELL_HISTORY_MAX_NUMBER 5
/**
* @brief 双击间隔(ms)
* 使能宏`SHELL_LONG_HELP`后此宏生效,定义双击tab补全help的时间间隔
*/
#define SHELL_DOUBLE_CLICK_TIME 200
/**
* @brief 管理的最大shell数量
*/
#define SHELL_MAX_NUMBER 5
/**
* @brief shell格式化输出的缓冲大小
* 为0时不使用shell格式化输出
*/
#define SHELL_PRINT_BUFFER 128
/**
* @brief 获取系统时间(ms)
* 定义此宏为获取系统Tick,如`HAL_GetTick()`
* @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
*/
#define SHELL_GET_TICK() HAL_GetTick()
/**
* @brief shell内存分配
* shell本身不需要此接口,若使用shell伴生对象,需要进行定义
*/
#define SHELL_MALLOC(size) 0
/**
* @brief shell内存释放
* shell本身不需要此接口,若使用shell伴生对象,需要进行定义
*/
#define SHELL_FREE(obj) 0
/**
* @brief 是否显示shell信息
*/
#define SHELL_SHOW_INFO 1
/**
* @brief 是否在登录后清除命令行
*/
#define SHELL_CLS_WHEN_LOGIN 1
/**
* @brief shell默认用户
*/
#define SHELL_DEFAULT_USER "letter"
/**
* @brief shell默认用户密码
* 若默认用户不需要密码,设为""
*/
#define SHELL_DEFAULT_USER_PASSWORD ""
/**
* @brief shell自动锁定超时
* shell当前用户密码有效的时候生效,超时后会自动重新锁定shell
* 设置为0时关闭自动锁定功能,时间单位为`SHELL_GET_TICK()`单位
* @note 使用超时锁定必须保证`SHELL_GET_TICK()`有效
*/
#define SHELL_LOCK_TIMEOUT 0 * 60 * 1000
下面介绍一下比较重要的几个宏:
这个宏仅在这里面生效,这是在使用操作系统的时候使用到的,它会一直轮询地读取串口数据,然后调用 shellHandler 解析串口数据。这仅仅是个范例,因为这里没有进行任务切换,所以会一直堵塞在这里,而我们如果要使用FreeRTOS创建一个线程来处理这个任务,需要使用osDelay来进行切换。因为这里一次只读取一个字节,所以如果任务切换间隔过大,可能会丢失某些数据,另外,因为接收串口的时候要进行回显,所以任务间隔过大,也会影响操作体验,所以我们在串口接收中断调用 shellHandler 进行处理。我们不需要在线程中一直读取,所以这个宏 SHELL_TASK_WHILE 设为0,但是设为1也不影响就是了。
/**
* @brief shell 任务
*
* @param param 参数(shell对象)
*
*/
void shellTask(void *param)
{
Shell *shell = (Shell *)param;
char data;
#if SHELL_TASK_WHILE == 1
while(1)
{
#endif
if (shell->read && shell->read(&data) == 0)
{
shellHandler(shell, data);
}
#if SHELL_TASK_WHILE == 1
}
#endif
}
命令导出功能,通过这个宏,我们就可以在命令行中输入指定函数的名称以及传递函数,就可以直接调用该函数,用于调试,十分方便,所以这个宏不变,默认为1.
作者定义的伴生对象,用于一些扩展的组件(文件系统支持,日志工具等),但是目前这个功能还不是很完善,所以这个宏默认设为0,不启用。
这个与 SHELL_ENTER_LF 以及 SHELL_ENTER_CR 互斥,作为命令行回车触发,使用的SecureCRT 以及 MobaXterm 是使用 CR 作为命令行回车触犯,所以这三个的配置为
#define SHELL_ENTER_LF 0
#define SHELL_ENTER_CR 1
#define SHELL_ENTER_CRLF 0
这个宏定义了格式化输出的大小,在 shellPrint 中使用到,使用方法和 printf 类似,只是注意不要超过缓存大小。
这个宏也是按照默认值就可以了,也可以根据自己的需求进行修改。
其他的宏理解起来很容易,这里也就不进行细说了。
最终宏的定义如下:
#define SHELL_TASK_WHILE 0
#define SHELL_USING_CMD_EXPORT 1
#define SHELL_USING_COMPANION 0
#define SHELL_USING_COMPANION 0
#define SHELL_SUPPORT_END_LINE 0
#define SHELL_HELP_LIST_USER 0
#define SHELL_HELP_LIST_VAR 0
#define SHELL_HELP_LIST_KEY 0
#define SHELL_HELP_SHOW_PERMISSION 1
#define SHELL_ENTER_LF 0
#define SHELL_ENTER_CR 1
#define SHELL_ENTER_CRLF 0
#define SHELL_EXEC_UNDEF_FUNC 0
#define SHELL_PARAMETER_MAX_NUMBER 8
#define SHELL_HISTORY_MAX_NUMBER 5
#define SHELL_DOUBLE_CLICK_TIME 200
#define SHELL_MAX_NUMBER 5
#define SHELL_PRINT_BUFFER 128
#define SHELL_GET_TICK() HAL_GetTick()
#define SHELL_MALLOC(size) 0
#define SHELL_FREE(obj) 0
#define SHELL_SHOW_INFO 1
#define SHELL_CLS_WHEN_LOGIN 1
#define SHELL_DEFAULT_USER "letter"
#define SHELL_DEFAULT_USER_PASSWORD ""
#define SHELL_LOCK_TIMEOUT 0 * 60 * 1000
要正确的将串口接收到的数据当参数传递给 shellHandler,先要确保打开串口的中断,HAL_UART_Receive_IT ,使用这个串口中断,需要定义接收到的buffer数组,以及接收buffer的大小。
#define HAL_USART3_RXBUFFERSIZE 1
uint8_t HAL_USART3_RxBuffer[HAL_USART3_RXBUFFERSIZE]; //HAL库使用的串口接收缓冲
在main函数中打开串口中断
HAL_UART_Receive_IT(&huart3, (uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE);//使能串口中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
打开串口中断后,在串口中断中就能获取到串口数据了,在HAL库中,串口中断最终会回调 HAL_UART_RxCpltCallback,所以我们就在这个接收串口数据,然后调用 shellHandler 进行解析。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
uint8_t udata;
if(huart->Instance == USART3) //串口3的接收部分
{
HAL_UART_Receive_IT(&huart3,(uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE); //接收串口的缓存
udata = HAL_USART3_RxBuffer[0];
shellHandler(&shell, udata);
}
}
编译后下载程序,复位,这时候输入cmd 命令,按下回车,就会列出可以支持的命令了。后续如果自定义了导出函数,也会在这里显示。到这里,就说明了串口的收发是正常的,shell 也移植成功了。
如同上面的 cmds 命令,我们用户也可以自定义命令,在编写完一个函数之后,我们可以通过SHELL_EXPORT_CMD 导出该函数,这样我们就能通过命令行来调用这个命令执行,以达到调试的目的。
letter shell 3.0同时支持两种形式的函数定义方式,形如main函数定义的func(int argc, char *agrv[])以及形如普通C函数的定义func(int i, char *str, …),两种函数定义方式适用于不同的场景。
下面使用LED来举例。
void LD1_ON(void)
{
HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on, LD1_ON, LD1 ON);
void LD1_OFF(void)
{
HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_off, LD1_OFF, LD1 OFF);
上面写了两个函数,顾名思义,LD1_ON是点亮LD1,LD1_OFF是熄灭LD1,SHELL_EXPORT_CMD 就是导出命令,各个参数分别是 命令属性,命令名,命令函数,命令描述。
/**
* @brief shell 命令定义
*
* @param _attr 命令属性
* @param _name 命令名
* @param _func 命令函数
* @param _desc 命令描述
*/
#define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \
const char shellCmd##_name[] = #_name; \
const char shellDesc##_name[] = #_desc; \
const ShellCommand \
shellCommand##_name SECTION("shellCommand") = \
{ \
.attr.value = _attr, \
.data.cmd.name = shellCmd##_name, \
.data.cmd.function = (int (*)())_func, \
.data.cmd.desc = shellDesc##_name \
}
命令属性 这部分在 shell.h 中进行定义,几个属性可以通过 " | " 来进行连接。属性有以下几种:
/**
* @brief shell 命令权限
*
* @param permission 权限级别
*/
#define SHELL_CMD_PERMISSION(permission) \
(permission & 0x000000FF)
/**
* @brief shell 命令类型
*
* @param type 类型
*/
#define SHELL_CMD_TYPE(type) \
((type & 0x0000000F) << 8)
/**
* @brief 使能命令在未校验密码的情况下使用
*/
#define SHELL_CMD_ENABLE_UNCHECKED \
(1 << 12)
/**
* @brief 禁用返回值打印
*/
#define SHELL_CMD_DISABLE_RETURN \
(1 << 13)
/**
* @brief 只读属性(仅对变量生效)
*/
#define SHELL_CMD_READ_ONLY \
(1 << 14)
在上面的例子中,使用到的命令属性是
SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN
这个意思是这个命令任何用户均可使用、命令的类型是普通的C函数、执行完函数后,不在命令行上显示返回的结果
命令名 在命令行中,输入命令名就能执行对应的C函数,可以通过命令行的 cmds 命令进行查看
在上面的例子中,实现了两个命令名,分别是
led1_on
led1_off
命令函数 也就是我们在keil工程中,所编写的函数的函数名。
比如在命令行中,我们输入 led1_on ,那么实际调用的函数是 LD1_ON,输入 led1_off,那么实际调用的函数是 LD1_OFF
命令描述 命令描述出现在 cmds 命令中,它主要是简明扼要的说明这个函数的功能。
如图所示,在输入cmds命令之后,会打印出我们可执行的命令,这里是多出了两个命令,led1_on 以及 led1_off,我们输入对应的命令,就可以控制LED1的亮灭了。
一般我们写的程序,用普通的c函数的命令导出即可,main函数也可以,使用上也比较类似。使用此方式,一个函数定义的例子如下:
int func(int argc, char *agrv[])
{
printf("%dparameter(s)\r\n", argc);
for (char i = 1; i < argc; i++)
{
printf("%s\r\n", argv[i]);
}
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN), func, func, test);
终端调用
letter:/$ func "hello world"
2 parameter(s)
hello world
我们编写一个带参的函数,命令行是以空格划分传递的参数。
void LD1_ON_OFF_ms(uint16_t on_ms,uint16_t off_ms)
{
HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET); //点亮LD1
HAL_Delay(on_ms); //延时
HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET); //熄灭LD1
HAL_Delay(off_ms); //延时
HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET); //点亮LD1
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on_off_ms, LD1_ON_OFF_ms, LD1 on ms);
这个例子是控制LD1亮灭的时间,LD1首先点亮 on_ms 个ms,再熄灭 "off_ms"个ms,再重新点亮。
输入以下命令,就能看到 LD1 先亮 1000ms,然后熄灭 100ms,最后又点亮的状态。
letter:/$ led1_on_off_ms 1000 100
这是输入一个数字的例子,也可以传递字符串,比如说我们要使用文件管理系统 FatFs 来管理文件,很多地方就要使用到字符串了,letter shell 支持伴生对象,里面可以扩展文件管理系统的功能。
fs_support作为letter shell的插件,用于实现letter shell对常见文件系统操作的支持,比如说cd,ls等命令,fs_support依赖于letter shell的伴生对象功能,并且使用到了内存分配和内存释放,所以请确认已经配置好了letter shell
fs_support并非一个完全实现的letter shell插件,由于文件系统的接口和操作系统以及具体使用的文件系统相关,所以fs_support仅仅通过接入几个基本的接口以实现
cd
,ls
命令,具体使用时,可能需要根据使用的文件系统接口修改fs_support,letter shell的demo/x86-gcc下有针对linux平台的移植,可以及进行参考
因为这个功能作者还未完善,所以这部分的命令需要我们自己来实现,下面是我在 FatFs中对 mv
命令的实现,功能是在将 old_path 的文件移动到 new_path,或者是将 old_path的文件移动到 new_path,并且重命名该文件。
/**
* @brief 重命名/移动文件或子目录
*/
void shell_mv(char *old_path,char *new_path)
{
FRESULT res;
shell_active = shellGetCurrent(); //Get the currently active shelll
res = f_rename(old_path, new_path);
if(res != FR_OK)
{
shellWriteString(shell_active,"Operation failed! Error Code:");
shellPrint(shell_active,"%d\r\n",res);
}
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
输入以下命令,就能将 dir1目录下的 file1.txt 移动到 dir2 目录下,并且这个文件名将修改为 file2.txt。
mv dir1/file1.txt dir2/file2.txt
不过要注意一点,FatFs 的磁盘是以数字打头的,不像window,各个硬盘是以字符打头的,比如 "C:\file.txt"表示 C盘目录下有个 file.txt,在FatFs中,对应的目录是 “0:\file.txt” ,假如我们以 “0:\file.txt” 当成参数传递进去,那么接收是会存在问题的,这是因为第一个输入的是 0,在解析参数的时候,它会把这一整串字符串当成数字来解析。
我们先找到解析参数的函数,可以看到 if (*string == ‘-’ || (*string >= ‘0’ && *string <= ‘9’)) 那么就是要解析数字,我们在这个判断中,再次判断,从字符开始到后面,如果出现了任何一个不属于数字的字符,我们就判定这个是字符串。而如果在空格或者回车命令前,全数字的字符,那么我们就判断这个是数字,通过这个方式,就能规避 FatFs 的硬盘号会导致参数解析异常的问题了。
/**
* @brief 解析参数
*
* @param shell shell对象
* @param string 参数
* @return unsigned int 解析结果
*/
unsigned int shellExtParsePara(Shell *shell, char *string)
{
uint16_t i = 1;
if (*string == '\'' && *(string + 1))
{
return (unsigned int)shellExtParseChar(string);
}
else if (*string == '-' || (*string >= '0' && *string <= '9'))
{
while(1)
{
//寻找下一个表示该参数已经结束的字符
if((*(string + i) == ' ') //下一个为空格表示该参数已经结束
#if SHELL_ENTER_LF == 1 // LF 表示命令结束
|| (*(string + i) == 0x0A))
#endif
#if SHELL_ENTER_CR == 1 // CR 表示命令结束
|| (*(string + i) == 0x0D))
#endif
#if SHELL_ENTER_CRLF == 1 // CR LF 表示命令结束
|| ((*(string + i) == 0x0A) && (*(string + i + 1) == 0x0D))
#endif
break;
//出现不为小数点或者非数字的字符,表明该参数为字符串
if((*(string + i) != '.') || (*(string + i) < '0') || (*(string + i) > '9'))
return (unsigned int)shellExtParseString(string);
i++;
}
return (unsigned int)shellExtParseNumber(string);
}
else if (*string == '$' && *(string + 1))
{
return shellExtParseVar(shell, string);
}
else if (*string)
{
return (unsigned int)shellExtParseString(string);
}
return 0;
}
函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。
/**
* @brief shell 写字符串
*
* @param shell shell对象
* @param string 字符串数据
*
* @return unsigned short 写入字符的数量
*/
unsigned short shellWriteString(Shell *shell, const char *string)
letter shell采取一个静态数组对定义的多个shell进行管理,shell数量可以修改宏SHELL_MAX_NUMBER
定义(为了不使用动态内存分配,此处通过数据进行管理),从而,在shell执行的函数中,可以调用shellGetCurrent()
获得当前活动的shell对象,从而可以实现某一个函数在不同的shell对象中发生不同的行为,也可以通过这种方式获得shell对象后,调用shellWriteString(shell, string)
进行shell的输出。
/**
* @brief 获取当前活动shell
*
* @return Shell* 当前活动shell对象
*/
Shell* shellGetCurrent(void)
可以定义一个全局变量
Shell *shell_active; //当前活动的shell
如果我们使用多个串口创建shell 对象,如果通过不同的串口访问文件,那么就不容易知道应该在哪个界面进行输出,正如这个 mv
的命令,首先获取当前活动的shell,再通过 shellWriteString(shell_active,“Operation failed! Error Code:”); 进行输出,这样就可以避免冲突了。
/**
* @brief 重命名/移动文件或子目录
*/
void shell_mv(char *old_path,char *new_path)
{
FRESULT res;
shell_active = shellGetCurrent(); //Get the currently active shelll
res = f_rename(old_path, new_path);
if(res != FR_OK)
{
shellWriteString(shell_active,"Operation failed! Error Code:");
shellPrint(shell_active,"%d\r\n",res);
}
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
这个功能就是 vsnprintf 的简单封装了,用于向字符串中打印数据、数据格式用户自定义,但是要注意,打印的大小不要超过 SHELL_PRINT_BUFFER。
#if SHELL_PRINT_BUFFER > 0
/**
* @brief shell格式化输出
*
* @param shell shell对象
* @param fmt 格式化字符串
* @param ... 参数
*/
void shellPrint(Shell *shell, char *fmt, ...)
{
char buffer[SHELL_PRINT_BUFFER];
va_list vargs;
SHELL_ASSERT(shell, return);
va_start(vargs, fmt);
vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);
va_end(vargs);
shellWriteString(shell, buffer);
}
#endif
到这里,介绍就完了,而这也足够应付绝大部分的调试内容了。
源代码