剖析RT-Thread中console与finsh组件实现(2)
最后,我们看一下一个cmd命令是怎么加入到命令空间里的:
关键在于宏定义 MSH_CMD_EXPORT ,内容如下:
这里在cmd名称前加了 __cmd_ 刚好可以与之前匹配代码相印证。
tips
关于预编译中#和##的两个用法
首先说#,它可以为传入参数加上引号,如下面的代码
///
#include <stdio.h>
#define STR(a) #a
int main()
{
char str[] = STR(ABCD);//等效于 char str[] = "ABCD";
printf("%s\n",str);
}
///
很容易看出来,其实就是给ABCD加了一个引号变成字符串了。
而##相当于分隔符的作用,当你想给变量加前缀你可能会这么做
///
#include <stdio.h>
#define AddPrefix(a) __a
int main()
{
int tmp = 1;
int __tmp = 2;
tmp = AddPrefix(tmp);//本意是tmp = __tmp;
printf("%d\n",tmp);
}
///
运行上面的代码会报错,因为__a是一个整体,编译器不会把__a中的a单独去替换前面的参数,所以这时候就需要##去把它们先分隔开
///
#include <stdio.h>
#define AddPrefix(a) __##a
int main()
{
int tmp = 1;
int __tmp = 2;
tmp = AddPrefix(tmp);
printf("%d\n",tmp);
}
///
这样替换的时候编译器会认为__a是__+a,会替换a再拼接。
宏定义 FINSH_FUNCTION_EXPORT_CMD 内容如下:
和之前自动初始化那里很相似,这里是把cmd的名字和注释变量的数组存于段.rodata.name内。而把cmd结构体填充之后存于段名FSymTab内。
为了更加直观,我把参数都替换一下,实际内容如下:
#define FINSH_FUNCTION_EXPORT_CMD(led, __cmd_led, RT-Thread first led sample) \
const char __fsym___cmd_led_name[] SECTION(".rodata.name") = "__cmd_led"; \
const char __fsym___cmd_led_desc[] SECTION(".rodata.name") = "RT-Thread first led sample"; \
const struct finsh_syscall __fsym___cmd_led SECTION("FSymTab")= \
{ \
__fsym___cmd_led_name, \
__fsym___cmd_led_desc, \
(syscall_func)&led \
};
对照之前的匹配cmd代码,可以看出当名字匹配成功时调用led函数指针执行实际的函数。
tips
最后补充一个知识点,关于什么是终端?什么是控制台?什么是tty?什么是shell?
终端(terminal)=tty=文本的输入输出环境
控制台(console)=物理终端
shell=命令行解释器
总结
整个finsh实现有几个很有意思的点:
1.自动初始化
2.设备–总线–驱动设计思路
抛去这些,其实就是finsh线程接收来自串口接收回调的信号量,然后调用相应函数。
最近在调试nrf52系列的SDK,发现nrf不开源的蓝牙协议栈写法有异曲同工的地方,但是不得不说,这种方法只适用于很成熟的代码,比如官方SDK,如果自己在项目中也这样写,调试的时候太费劲。