finsh是RT-Thread的命令行外壳(shell),提供一套供用户在命令行的操作接口,主要用于调试、查看系统信息。在大部分嵌入式系统中,一般开发调试都使用硬件调试器和printf日志打印,在有些情况下,这两种方式并不是那么好用。比如对于RT-Thread这个多线程系统,我们想知道某个时刻系统中的线程运行状态、手动控制系统状态。如果有一个shell,就可以输入命令,直接相应的函数执行获得需要的信息,或者控制程序的行为。这无疑会十分方便。
finsh支持两种模式:
1. C语言解释器模式, 为行文方便称之为c-style;
2. 传统命令行模式,此模式又称为msh(module shell)。C语言表达式解释模式下, finsh能够解析执行大部分C语言的表达式,并使用类似C语言的函数调用方式访问系统中的函数及全局变量,此外它也能够通过命令行方式创建变量。在msh模式下,finsh运行方式类似于dos/bash等传统shell。
大致工作流程
一、finsh组件初始化函数finsh_system_init(),并且添加了INIT_COMPONENT_EXPORT(finsh_system_init),支持组件初始化;
这个函数会初始化finsh组件,包括一些finsh变量以及相关数据结构。
然后它会创建一个线程,代码如下:
result = rt_thread_init(&finsh_thread, "tshell", finsh_thread_entry, RT_NULL, &finsh_thread_stack[0], sizeof(finsh_thread_stack), FINSH_THREAD_PRIORITY, 10); if (result == RT_EOK) rt_thread_startup(&finsh_thread);
可以看到,线程函数是finsh_thread_entry,在下一节中我们将分析它具体工作流程。
二、void finsh_set_device(const char* device_name)函数为finsh设置终端设备,在stm32中主要设置串口设备为终端。该函数一般放在组件初始化函数rt_component_init()后面,因为要先完成finsh组件初始化才能设置终端设备。
void finsh_set_device(const char* device_name) { rt_device_t dev = RT_NULL; RT_ASSERT(shell != RT_NULL); dev = rt_device_find(device_name); if (dev == RT_NULL) { rt_kprintf("finsh: can not find device: %s\n", device_name); return; } /* check whether it's a same device */ if (dev == shell->device) return; /* open this device and set the new device in finsh shell */ if (rt_device_open(dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX |\ RT_DEVICE_FLAG_STREAM) == RT_EOK) { if (shell->device != RT_NULL) { /* close old finsh device */ rt_device_close(shell->device); rt_device_set_rx_indicate(shell->device, RT_NULL); } shell->device = dev; rt_device_set_rx_indicate(dev, finsh_rx_ind); } }
这个函数为finsh组件设置使用的串口,从这个函数中我们可以总结出,如何使用串口设备。
-
调用rt_device_find使用设备的字符串名字查找设备,得到设备数据结构指针
-
调用rt_devcie_open打开设备,open_flag为RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX |RT_DEVICE_FLAG_STREAM
* 对finsh来说,还使用了rt_device_set_rx_indicate函数设置了一个回调函数finsh_rx_ind,它的作用我们后面会讨论
到这里设备就被打开了。
在serial.c中rt_hw_serial_isr()中有:
/* invoke callback */ if (serial->parent.rx_indicate != RT_NULL) { rt_size_t rx_length; /* get rx length */ level = rt_hw_interrupt_disable(); rx_length = (rx_fifo->put_index >= rx_fifo->get_index)? (rx_fifo->put_index - rx_fifo->get_index): (serial->config.bufsz - (rx_fifo->get_index - rx_fifo->put_index)); rt_hw_interrupt_enable(level); serial->parent.rx_indicate(&serial->parent, rx_length); }
上面计算得到rx_length,然后触发回调函数,也就是前面的finsh_rx_ind函数,即实际执行的是fins_rx_ind(device, rx_length)。
在shell.c中fins_rx_ind源码为:
static rt_err_t finsh_rx_ind(rt_device_t dev, rt_size_t size) { RT_ASSERT(shell != RT_NULL); /* release semaphore to let finsh thread rx data */ rt_sem_release(&shell->rx_sem); return RT_EOK; }
这个函数里只是简单的释放信号量。也就是说,当串口硬件上接收到一个字节,就会调用finsh_rx_ind函数来释放一个信号量。
三、finsh线程函数的工作流程概述
void finsh_thread_entry(void* parameter) { char ch; /* normal is echo mode */ shell->echo_mode = 1; #ifndef FINSH_USING_MSH_ONLY finsh_init(&shell->parser); #endif rt_kprintf(FINSH_PROMPT); /* set console device as shell device */ if (shell->device == RT_NULL) { #ifdef RT_USING_CONSOLE shell->device = rt_console_get_device(); RT_ASSERT(shell->device); rt_device_set_rx_indicate(shell->device, finsh_rx_ind); rt_device_open(shell->device, (RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM | RT_DEVICE_FLAG_INT_RX)); #else RT_ASSERT(shell->device); #endif } while (1) { /* wait receive */ if (rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER) != RT_EOK) continue; /* read one character from device */ while (rt_device_read(shell->device, 0, &ch, 1) == 1) { /* * handle control key * up key : 0x1b 0x5b 0x41 * down key: 0x1b 0x5b 0x42 * right key:0x1b 0x5b 0x43 * left key: 0x1b 0x5b 0x44 */ if (ch == 0x1b) { shell->stat = WAIT_SPEC_KEY; continue; } else if (shell->stat == WAIT_SPEC_KEY) { if (ch == 0x5b) { shell->stat = WAIT_FUNC_KEY; continue; } shell->stat = WAIT_NORMAL; } else if (shell->stat == WAIT_FUNC_KEY) { shell->stat = WAIT_NORMAL; if (ch == 0x41) /* up key */ { #ifdef FINSH_USING_HISTORY /* prev history */ if (shell->current_history > 0) shell->current_history --; else { shell->current_history = 0; continue; } /* copy the history command */ memcpy(shell->line, &shell->cmd_history[shell->current_history][0], FINSH_CMD_SIZE); shell->line_curpos = shell->line_position = strlen(shell->line); shell_handle_history(shell); #endif continue; } else if (ch == 0x42) /* down key */ { #ifdef FINSH_USING_HISTORY /* next history */ if (shell->current_history < shell->history_count - 1) shell->current_history ++; else { /* set to the end of history */ if (shell->history_count != 0) shell->current_history = shell->history_count - 1; else continue; } memcpy(shell->line, &shell->cmd_history[shell->current_history][0], FINSH_CMD_SIZE); shell->line_curpos = shell->line_position = strlen(shell->line); shell_handle_history(shell); #endif continue; } else if (ch == 0x44) /* left key */ { if (shell->line_curpos) { rt_kprintf("\b"); shell->line_curpos --; } continue; } else if (ch == 0x43) /* right key */ { if (shell->line_curpos < shell->line_position) { rt_kprintf("%c", shell->line[shell->line_curpos]); shell->line_curpos ++; } continue; } } /* handle CR key */ if (ch == '\r') { char next; if (rt_device_read(shell->device, 0, &next, 1) == 1) ch = next; else ch = '\r'; } /* handle tab key */ else if (ch == '\t') { int i; /* move the cursor to the beginning of line */ for (i = 0; i < shell->line_curpos; i++) rt_kprintf("\b"); /* auto complete */ shell_auto_complete(&shell->line[0]); /* re-calculate position */ shell->line_curpos = shell->line_position = strlen(shell->line); continue; } /* handle backspace key */ else if (ch == 0x7f || ch == 0x08) { /* note that shell->line_curpos >= 0 */ if (shell->line_curpos == 0) continue; shell->line_position--; shell->line_curpos--; if (shell->line_position > shell->line_curpos) { int i; rt_memmove(&shell->line[shell->line_curpos], &shell->line[shell->line_curpos + 1], shell->line_position - shell->line_curpos); shell->line[shell->line_position] = 0; rt_kprintf("\b%s \b", &shell->line[shell->line_curpos]); /* move the cursor to the origin position */ for (i = shell->line_curpos; i <= shell->line_position; i++) rt_kprintf("\b"); } else { rt_kprintf("\b \b"); shell->line[shell->line_position] = 0; } continue; } /* handle end of line, break */ if (ch == '\r' || ch == '\n') { #ifdef FINSH_USING_HISTORY shell_push_history(shell); #endif #ifdef FINSH_USING_MSH if (msh_is_used() == RT_TRUE) { rt_kprintf("\n"); msh_exec(shell->line, shell->line_position); } else #endif { #ifndef FINSH_USING_MSH_ONLY /* add ';' and run the command line */ shell->line[shell->line_position] = ';'; if (shell->line_position != 0) finsh_run_line(&shell->parser, shell->line); else rt_kprintf("\n"); #endif } rt_kprintf(FINSH_PROMPT); memset(shell->line, 0, sizeof(shell->line)); shell->line_curpos = shell->line_position = 0; break; } /* it's a large line, discard it */ if (shell->line_position >= FINSH_CMD_SIZE) shell->line_position = 0; /* normal character */ if (shell->line_curpos < shell->line_position) { int i; rt_memmove(&shell->line[shell->line_curpos + 1], &shell->line[shell->line_curpos], shell->line_position - shell->line_curpos); shell->line[shell->line_curpos] = ch; if (shell->echo_mode) rt_kprintf("%s", &shell->line[shell->line_curpos]); /* move the cursor to new position */ for (i = shell->line_curpos; i < shell->line_position; i++) rt_kprintf("\b"); } else { shell->line[shell->line_position] = ch; if (shell->echo_mode) rt_kprintf("%c", ch); } ch = 0; shell->line_position ++; shell->line_curpos++; if (shell->line_position >= 80) { /* clear command line */ shell->line_position = 0; shell->line_curpos = 0; } } /* end of device read */ } }
函数主体依然是一个while(1)循环,这是显然的,因为finsh要不停的监听终端上输入。
if (rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER) != RT_EOK) continue;
即,如果串口上没有收到任何数据,并且串口缓冲区中也无数据,即shell→rx_sem信号量的值为0,那么这个函数会使finsh线程休眠,RTT内核会执行其他线程。
当串口收到数据,串口终端调用回调函数finsh_rx_ind函数来释放信号量,这会唤醒finsh线程,rt_sem_take函数会执行完毕,继续执行接下来的代码。
接下来的代码调用rt_device_read函数从串口数据缓冲池中读取一个字节。
然后判断所读取的到这个字节(判断上下左右四个按键所代表的字节)。
(1) 如果是'\r',即表示用户按下了回车键,再调用rt_device_read函数来读取一个字节,如果读到,则这将更新读到的字节,一般情况下,这个函数会返回0,即没有读到新的字节。
(2) 如果是'\t',即表示用户按下了TAB键,则调用finsh_auto_complete函数,这个函数做自动补全操作,也就是根据当前已输入的字符串,从finsh内部已注册的函数/变量中查找匹配字符串,如果找到则会在终端上自动补全。
(3) 如果是0x7f或者0x08 说明:查ascii码表可知,0x08 表示按下了backspace键,【0x7f表示按下了DEL键,这个不对劲,如何知道当我们按下了键盘按键时,串口都收到了什么数据呢?】 这表示用户期望删除已经输入的字符串,根据测试结果,发送”\0x08 \0x08”,可以实现退格。
(4) 如果收到了'\r'或者'\n',则表示用户按下了回车,希望处理这个命令,那么finsh_run_line函数被执行,这个函数会从从finsh已注册的函数/变量中匹配当前从终端里获取的字符串,如果匹配到,则执行对应的函数(若字符串为函数名)或者打印变量的值(若字符串为已变量)。
(5) 回显字符,也就是将刚才从串口接收到终端发送的字符发送到终端软件上显示出来。这就是说,我们在终端软件上输入字符,并且可以看到我们输入的字符,实际上是板子上的串口重新发回来显示的。在上面finsh的线程代码中,rt_device_write函数是在rt_kprintf中调用的。
然后回到(1),重复这个过程。