使用RT-Thread Finsh

关于Finsh

了解一个东西,首先要知道组件的历史渊源,才能更好的理解它要解决的问题,这样就能更容易的理解它的架构和设计基础

官网对Finsh的介绍

finsh是RT-Thread的命令行外壳(shell),提供一套供用户在命令行的操作接口,主要用于调试、查看系统信息。 finsh支持两种模式:1. C语言解释器模式, 为行文方便称之为c-style;2. 传统命令行模式,此模式又称为msh(module shell)。

C语言表达式解释模式下, finsh能够解析执行大部分C语言的表达式,并使用类似C语言的函数调用方式访问系统中的函数及全局变量,此外它也能够通过命令行方式创建变量。

在msh模式下,finsh运行方式类似于dos/bash等传统shell。

在大部分嵌入式系统中,一般开发调试都使用硬件调试器和printf日志打印,在有些情况下,这两种方式并不是那么好用。比如对于RT-Thread这个多线程系统,我们想知道某个时刻系统中的线程运行状态、手动控制系统状态。如果有一个shell,就可以输入命令,直接相应的函数执行获得需要的信息,或者控制程序的行为。这无疑会十分方便。

在嵌入式领域,C语言是最常用的开发语言,如果shell程序的命令是C语言的风格,那无疑是非常易用且有趣的。嵌入式设备通常采用交叉编译,一般需要将开发板与PC机连接起来通讯,常见连接方式包括,串口、USB、以太网、wifi等。一个灵活的shell应该可以在多种连接方式上工作。

finsh正是基于这些考虑而诞生的,finsh运行于开发板,它可以使用串口/以太网/USB等与PC机进行通信。下图是finsh的实际运行运行效果图。开发板运行RT-Thread,并使能了finsh组件,通过串口与PC机连接,PC上运行Secure CRT。按下回车,然后输入list_thread()将会打印系统当前所有线程,以及状态。

传统的打印式手段,属于单向的;通过调试器来调试,不仅可以输出,而且可以输入,就属于双向的,但IDE的调试擅长于静态调试,比如设置断点,检查变量什么的;Finsh在一定程度上增强了动态调试功能,也使远程调试变得可能;RT-Thread为我们搭建了调试框架,用户不在需要单独编写解析命令机制,只需要关注需要导出的函数和变量;

Finsh里面动态线程的栈使用量,是一个很实用的功能
另,参考资料里面有个RT-Thread测试框架的介绍文章,可以构建自动化测试框架

初步体验

这部分,RT-Thread官方的手册和跟我学RT-Thread写的很好,我就不自己敲字了,避免重复造轮子
请参考 http://www.rt-thread.org/download/manual/study_rtt.pdf 第8篇和http://www.rt-thread.org/download/manual/rtthread_manual.zh.pdf 第11章

移植

首先参考http://www.rt-thread.org/download/manual/study_rtt.pdf 第8篇和http://www.rt-thread.org/download/manual/rtthread_manual.zh.pdf 第11章,在MDK环境下完成移植

如果您需要使用IAR工程,务必请阅读下一章的IAR工程问题

常见问题

1. IAR工程问题

RT-Thread scons脚本有一些bug,导致通过scons --target=iar -s生成的脚本,没有包含启动代码和链接脚本,从而致使IAR工程工作不正常,具体表现为1)FINSH无法获取函数名,变量名
2)不能使用中断 3)系统初始化不正常

修复手段:

1.    参考下面两个bug fix文件,修改BSP目录下对应的链接脚本和scons脚本
    1.    https://github.com/RT-Thread/rt-thread/pull/566/files
    2.    https://github.com/RT-Thread/rt-thread/pull/565/files
2.    删掉旧工程,重新使用`scons --target=iar -s`生成IAR工程
3.    手工在IAR工程中,添加link脚本(icf文件)

2. 如何禁用HISTORY

默认情况下,rtconfig.h没有出现FINSH_USING_HISTORY宏,RT-Thread启用HISTORY功能,并保持默认5条历史记录
可以通过在rtconfig.h中,显示定义#define FINSH_USING_HISTORY 0的方式来禁用HISTORY功能
通过定义#define FINSH_HISTORY_LINES 5来调整历史记录的长度

原理分析

0. 整体流程

[占位符]以后添加

1. 导出函数/变量

Finsh将需要导出的函数,添加信息后,封装到一个新的finsh_sysvarfinsh_syscall 结构体中,并将这个结构体放置在固定的段中FSymTab, VSymTab,最后通过__section_begin__section_end两个编译器函数来获取段的开始位置和结束位置
这些步骤等效于手工将导出结构体变量添加到一个预设的结构体数组中,如下所示

1
2
3
4
5
6
7
8
struct finsh_syscall _syscall_table[] =
{
{"hello", hello},
{"version", version},
{"list", list},
{"list_thread", list_thread},
...
}

但这么容易导致bug,因为注册表和函数实现的地方,往往不在同一个文件夹,过于分散,有时候,删除某个函数后,会忘记手工删除注册表的信息

RTT通过提供统一导出接口,将这个工作交给了编译器,最大程度的减少了bug的发生

岔开一个话题:对于某些静态驱动注册表来说,也可以利用这种方式实现。注意,仅仅是静态的,也就是说,段size在编译时就已经确定
RT-Thread的驱动架构没有这么做,而是提供驱动注册API,就是为了给静态驱动和动态驱动,提供统一的入口,更加方便灵活

3. 编译和虚拟机执行

这部分请参考阿莫论坛文章http://www.amobbs.com/thread-5539405-1-1.html

参考资料

  1. RT-Thread参考手册
  2. 跟我一起学习RT-Thread
  3. 体验Finsh组件
  4. Amobbs:Finsh原理分析
  5. RT-Thread测试框架的使用