各种RTOS比如zephyr、RT-Thread、Nuttx都带了自己shell,做demo或者各种功能测试的时候特别有用,那么能否给FreeRTOS也整一个shell呢?其实FreeRTOS官方有个仓库的目录中就有,所谓的FreeRTOS-Plus-CLI是也,但这玩意笔者玩过一段时间发现挺难用的,且依赖过多,所以最好是另外找一个移植简单、小巧玲珑的shell实现,笔者最后找到了nr_micro_shell和letter-shell,这两移植使用上基本差不多,选一个顺手的用用,笔者最后选择了nr_micro_shell。
注:nr_micro_shell也可以用于裸机环境,但结合RTOS会更加方便。
nr_micro_shell下载
git clone https://github.com/Nrusher/nr_micro_shell
然后把nr_micro_shell目录下的两子目录inc和src中的文件都copy到middlewares/nr_micro_shell/
nr_micro_shell在FreeRTOS中的使用
初始化的核心代码如下所示:
shell_init();
rx_queue = xQueueCreate(32, sizeof(uint8_t *));
xTaskCreate((TaskFunction_t )task_shell,
(const char* )"shell",
(uint16_t )1024,
(void* )NULL,
(UBaseType_t )2,
(TaskHandle_t* )NULL);
调用shell_init()初始化nr_micro_shell,然后创建一个queue用于串口中断中读出输入字符后传递给shell任务,然后就是创建名为shell的任务了,其中任务函数task_shell的代码如下:
static void task_shell(void const *argument)
{
unsigned char ch;
while (1) {
if (xQueueReceive(rx_queue, &ch, portMAX_DELAY))
shell(ch);
}
}
它的实现很简单,从队列中接收字符,然后作为参数传递给shell()函数,在队列为空即用户未输入时,shell任务会阻塞在队列接收上。接下来我们看看串口中断程序是怎么实现的,它是如何和shell任务交互的:
void USART1_IRQHandler(void)
{
uint8_t rxdata;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if(usart_interrupt_flag_get(USART1, USART_RDBF_FLAG) != RESET) {
/* read one byte from the receive data register */
rxdata = usart_data_receive(USART1);
xQueueSendFromISR(rx_queue, &rxdata, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
首先判断串口RDBF是否置位,如置位表明串口有数据可以读出,调用usart_data_receive()函数读出输入字符,用xQueueSendFromISR()函数发给队列,这时候阻塞在队列读取上的shell任务就会被唤醒,但能否拿到cpu执行需要等待任务切换机会,所以串口中断程序这里调用portYIELD_FROM_ISR()进行任务切换,方便已经就绪的shell任务及时运行。
至此,使用nr_micro_shell的框架已经搭好,接下来就可以实现各种各样的shell命令啦
实现几个nr_micro_shell命令
首先按nr_micro_shell的说明,向nr_micro_shell注册命令有两个方法:方法一,使用static_cmd[]全局大数组,把所有命令相关结构体都放在这个大数组里;方法二,定义NR_SHELL_USING_EXPORT_CMD宏>,然后NR_SHELL_CMD_EXPORT()导出每一个实现的命令。方法一每添加一个命令,都得改动static_cmd[]数组,从软件工程角度看不是太优美,所以方法一比较适合命令比较少或者很集中的场景;方法二比较优美,使用NR_SHELL_CMD_EXPORT()消除了全局数组的使用,特别适合各命令实现分布在很多文件中的场景,所以笔者采用了方法二。
然后利用FreeRTOS的支持,可以实现一些非常有意义的命令,比如uptime命令就可以用xTaskGetTickCount()来实现:
static void shell_uptime_cmd(char argc, char *argv)
{
printf("%lu\r\n", xTaskGetTickCount());
}
NR_SHELL_CMD_EXPORT(uptime, shell_uptime_cmd);
msleep命令可以用vTaskDelay()函数实现:
static void shell_msleep_cmd(char argc, char *argv)
{
unsigned int ms;
ms = strtoul(&(argv[(int)argv[1]]), NULL, 0);
vTaskDelay(pdMS_TO_TICKS(ms));
}
NR_SHELL_CMD_EXPORT(msleep, shell_msleep_cmd);
而利用vPortGetHeapStats()实现了free命令:
static void shell_free_cmd(char argc, char *argv)
{
HeapStats_t heap_stats;
vPortGetHeapStats(&heap_stats);
// format the heap stats
printf("Memory Statistics Bytes\r\n"
"------------------------------\r\n"
"Total heap:\t\t%u\r\n"
"Used heap:\t\t%u\r\n"
"Available heap:\t\t%u\r\n"
"Largest free block:\t%u\r\n"
"Smallest free block:\t%u\r\n"
"Num free blocks:\t%u\r\n"
"Min ever heap:\t\t%u\r\n"
"Num mallocs:\t\t%u\r\n"
"Num frees:\t\t%u\r\n",
configTOTAL_HEAP_SIZE,
(configTOTAL_HEAP_SIZE - heap_stats.xAvailableHeapSpaceInBytes),
heap_stats.xAvailableHeapSpaceInBytes,
heap_stats.xSizeOfLargestFreeBlockInBytes,
heap_stats.xSizeOfSmallestFreeBlockInBytes,
heap_stats.xNumberOfFreeBlocks,
heap_stats.xMinimumEverFreeBytesRemaining,
heap_stats.xNumberOfSuccessfulAllocations,
heap_stats.xNumberOfSuccessfulFrees
);
}
NR_SHELL_CMD_EXPORT(free, shell_free_cmd);
任务相关的一些命令ps和top分别基于vTaskList()和vTaskGetRunTimeStats()实现:
static void shell_ps_cmd(char argc, char *argv)
{
printf(" Min\r\n"
"Task State Pri Stack No\r\n"
"------------------------------------------\r\n");
int tasks_maxlen = 40 * uxTaskGetNumberOfTasks();
char *ps_msg = pvPortMalloc(tasks_maxlen);
vTaskList(ps_msg);
printf("%s", ps_msg);
vPortFree(ps_msg);
}
NR_SHELL_CMD_EXPORT(ps, shell_ps_cmd);
static void shell_top_cmd(char argc, char *argv)
{
printf("Task Runtime(us) Percentage\r\n"
"------------------------------------------\r\n");
int tasks_maxlen = 40 * uxTaskGetNumberOfTasks();
char *top_msg = pvPortMalloc(tasks_maxlen);
vTaskGetRunTimeStats(top_msg);
printf("%s", top_msg);
vPortFree(top_msg);
}
NR_SHELL_CMD_EXPORT(top, shell_top_cmd);
当然基于CMSIS也可以实现一些有趣的命令,比如类似重启/重置的reset命令就是基于__NVIC_SystemReset()实现的,它的效果相当于按reset按钮或者调试器里重置了。
static void shell_reset_cmd(char argc, char *argv)
{
__NVIC_SystemReset();
}
NR_SHELL_CMD_EXPORT(reset, shell_reset_cmd);
代码写完后修改CMakeLists.txt文件编译烧录,步骤不再赘述,请参考本测评系列前面文章。
美图欣赏
最后展示几个命令的运行截图供欣赏。
启动图:
uptime命令:
free命令:
ps命令:
top命令:
reset命令: