catwill 发表于 2014-5-7 21:48:33

[分享][交流]发一个控制台模块

本帖最后由 Gorgon_Meducer 于 2014-5-8 23:12 编辑

傻孩子工作室出品
模块简介:
        这个控制台模块是一个使用全状态机编写的、移植性强的模块。
        模块内置help 与 clear两个命令。
        可以静态或者动态扩展需要的命令。
        支持上下方向键回溯历史命令功能,历史命令的保存数量可配置。
        支持F1 F3键回溯上一条历史命令功能,这个功能可以配置打开或者关闭。
效果图:
       
准备工作:
        你现在需要一个有串口、有两个独立按键、有独立LED指示灯的单片机系统。
        为了体现平台无关,请参考文档 《平台搭建》 搭建平台。
        该平台提供了串口收发函数、LED控制接口函数、独立按键读取函数。
        这样我们就拥有一个共同讨论的基础。
        我现在使用的是STM32F103C8T6的板子,提供了一个使用MDK编写的、基于STM32F103C8T6的范例。
        基于之前搭建的平台,代码移植起来是非常容易的。
        在平台的基础上,使用者需要提供两个函数来使用该模块:写数据函数、读取数据函数。
       
范例下载:
       
        使用MDK的软件仿真即刻体验 ~虽然不能实现F1 F3 / 方向键上下 / clear命令 等功能
       
模块下载:
       
        解压后可以看到:模块由console文件夹下的三个文件组成:console.c   console.h   app_cfg.h        
移植console模块:
        将模块文件夹复制到你工程的代码文件夹,然后包含console.c。
        提供写数据函数与读取数据函数:
        范例中:main.c提供了如下函数:
bool console_serial_out(uint8_t chByte)
{
    return serial_out(chByte);
}
bool console_serial_in(uint8_t *pchByte)
{
    return serial_in(pchByte);
}
        在顶层app_cfg.h中通过如下插入宏来注册这两个函数:
// console 读写数据插入宏
#define CONSOLE_WRITE_BYTE(__BYTE) console_serial_out(__BYTE)
extern bool console_serial_out(uint8_t chByte);

#define CONSOLE_READ_BYTE(__BYTE) console_serial_in(__BYTE)
extern bool console_serial_in(uint8_t *pchByte);
        该app_cfg.h被console模块中的app_cfg.h包含;进而被console.c包含。       
        经过以上的步骤,console模块的移植工作就完成了。使用时,需包含头文件console.h
使用console模块:
        接口函数:
// console_task 函数
// 返回值: 状态机运行状态
extern fsm_rt_t console_task(void);

// 动态扩展命令消息地图注册函数
// 参数:    ptMSG       -- 待注册的消息地图首指针
//          chMSGNum    -- 待注册消息地图中消息的数量
// 返回值:true      -- 注册成功
//          false       -- 参数错误
extern bool console_register_command(const msg_t *ptMSG, uint8_t chMSGNum);
        console_task 函数为console模块任务函数,该函数需要在主程序中被反复调用。
        console_register_command函数为动态扩展命令消息地图注册函数。
静态扩展命令:
        在顶层 app_cfg.h 中提供消息处理函数原型插入宏 CONSOLE_MSG_MAP_FUN_EXTERN
        提供方法举例:
#define CONSOLE_MSG_MAP_FUN_EXTERN\
extern fsm_rt_t test_handler(const msg_t *ptMSG, uint8_t *pchStr, uint8_t chNum);
        并且提供消息地图插入宏 CONSOLE_MSG_MAP_SET
        提供方法举例:
#define CONSOLE_MSG_MAP_SET{ "test", &test_handler, "test -- this is a test command" }
动态扩展命令:
        参见范例main.c中声明消息地图:
/* 动态扩展命令消息地图 ------------------------------------------------ */
const static msg_t s_tExpandMSGMap[] = {
    { "expand", &expand_handler, "expand -- this is a expand command" },
    { "abc", &abc_handler, "abc -- this is abc command" }
};
        编写处理函数:
// expand 命令的处理函数
// 参数:    ptMSG   -- 消息指针
//          pchStr-- 后续 token 指针
//          chNum   -- 后续 token 数量
// 返回值:状态机运行状态
static fsm_rt_t expand_handler(const msg_t *ptMSG, uint8_t *pchStr, uint8_t chNum)
{
    ……
}

// abc 命令的处理函数
// 参数:    ptMSG   -- 消息指针
//          pchStr-- 后续 token 指针
//          chNum   -- 后续 token 数量
// 返回值:状态机运行状态
static fsm_rt_t abc_handler(const msg_t *ptMSG, uint8_t *pchStr, uint8_t chNum)
{
    ……
}
        注册扩展命令:
console_register_command(s_tExpandMSGMap, UBOUND(s_tExpandMSGMap));
配置历史命令回溯数量:
        通过在顶层app_cfg.h中提供插入宏实现:
// 配置记录历史命令数量
#define CONSOLE_HIS_CMD_NUM   10
       
配置F1 F3回溯功能的开启与关闭:
        通过在顶层app_cfg.h中提供插入宏实现:
// 配置命令重复功能
#define CONSOLE_CMD_REPEAT_EN   ENABLED
       
注意事项:
        1console_task函数必须在主循环中被反复调用。
        2扩展命令的处理函数不能阻塞。
接口文件console.h:
/* console 模块的输出接口文件 */

#ifndef __CONSOLE_H__
#define __CONSOLE_H__

/* 文件包含 ----------------------------------------------------------------- */
#include".\app_cfg.h"

/* 模块说明 ----------------------------------------------------------------- */
// 使用本模块应在上层 app_cfg.h 中提供 写数据插入宏 CONSOLE_WRITE_BYTE(__BYTE)
// 与读取数据插入红 CONSOLE_READ_BYTE(__BYTE)
// 并且提供插入宏展开后的原型
//
// 应在上层 app_cfg.h 中提供用以配置最大命令字符数的 CONSOLE_CMD_CHAR_NUM 插入宏
// 如没有提供该插入宏,会产生一条警告信息;并且默认配置为最大64字符数
//
// 应在上层 app_cfg.h 中提供用以配置是否需要重复功能 ( F1 F3 等 )
// 的插入宏 CONSOLE_CMD_REPEAT_EN   ENABLED(1)--启用   DISABLED(0)--禁止
// 如没有提供该插入宏,会产生一条警告信息;并且默认启用该功能
//
// 应在上层 app_cfg.h 中提供用以配置历史命令记录数量的插入宏
// CONSOLE_HIS_CMD_NUM 至少为 1 每一条历史指令都将消耗 CONSOLE_CMD_CHAR_NUM + 2 字节RAM
// 如没有提供该插入宏,会产生一条警告信息;并且默认保存4条历史指令
//
// 应在上层 app_cfg.h 中提供用以配置 token 分隔符的插入宏 CONSOLE_TOKEN_CODE
// 提供方法举例:   #define CONSOLE_TOKEN_CODE" ,;- ."
// 如没有提供该插入宏,会产生一条警告系你系;并默认采用空格为 token 分隔符
//
// 静态扩展命令:
// 在上层 app_cfg.h 中提供消息处理函数原型插入宏 CONSOLE_MSG_MAP_FUN_EXTERN
// 提供方法举例:
// #define CONSOLE_MSG_MAP_FUN_EXTERN\
// extern fsm_rt_t test_handler(const msg_t *ptMSG, uint8_t *pchStr, uint8_t chNum);
// 并且提供消息地图插入宏 CONSOLE_MSG_MAP_SET
// 提供方法举例:
// #define CONSOLE_MSG_MAP_SET{ "test", &test_handler, "test -- this is a test command" }

/* 类型定义 ----------------------------------------------------------------- */
typedef struct _msg_t msg_t;
typedef fsm_rt_t MSG_HANDLER(const msg_t *ptMSG, uint8_t *pchStr, uint8_t chNum);   // 消息处理函数类型

struct _msg_t {
    uint8_t *pchMsgStr;   // 消息
    MSG_HANDLER *fnHandler; // 消息处理函数
    uint8_t *pchHelpStr;    // 帮助信息指针
};

/* 输出接口 ----------------------------------------------------------------- */
// console_task 函数
// 返回值: 状态机运行状态
extern fsm_rt_t console_task(void);

// 动态扩展命令消息地图注册函数
// 参数:    ptMSG       -- 待注册的消息地图首指针
//          chMSGNum    -- 待注册消息地图中消息的数量
// 返回值:true      -- 注册成功
//          false       -- 参数错误
extern bool console_register_command(const msg_t *ptMSG, uint8_t chMSGNum);

#endif

/* EOF */
        接口文件向外部提供了模块的使用说明以及需要用到的数据类型:消息类型的定义以及消息处理函数的原型。

湛泸骏驰 发表于 2014-5-7 21:59:53

mark ...........

rowa 发表于 2014-5-7 22:03:38

可以有。。。。。。。。

电子小生 发表于 2014-5-7 22:04:10

mark!顶一下

Shampoo 发表于 2014-5-7 22:06:41

{:handshake:}                  

lyg407 发表于 2014-5-7 22:11:51

谢谢楼主分享,有空试一试!

kinsno 发表于 2014-5-7 22:22:20

我试图下载了一下,结果打不开MDK,打开后IDE就崩溃,顿时兴趣全无;楼主有空了,看看咋回事?还有别人碰到这问题了吗?

catwill 发表于 2014-5-7 22:38:11

kinsno 发表于 2014-5-7 22:22
我试图下载了一下,结果打不开MDK,打开后IDE就崩溃,顿时兴趣全无;楼主有空了,看看咋回事?还有别人碰到 ...

我使用的是4.10版本的MDK;
刚刚我下载了试验一下,结果是可以打开的。

cslrd 发表于 2014-5-7 23:28:20

本帖最后由 cslrd 于 2014-5-7 23:29 编辑

同楼上一样,打不开
求楼主支招

catwill 发表于 2014-5-8 06:15:46

请问使用mdk的版本?

catwill 发表于 2014-5-8 06:43:22

刚刚我在另一台电脑上面下载附件,也是可以打开的。
这个MDK的版本是4.12

cslrd 发表于 2014-5-8 07:07:45

catwill 发表于 2014-5-8 06:15
请问使用mdk的版本?

MDK版本是4.23是否同C编译器版本有关?

DevLabs 发表于 2014-5-8 07:22:48

先顶,晚上看看。

霸气侧漏 发表于 2014-5-8 07:25:20

顶,很强悍的东西

catwill 发表于 2014-5-8 07:26:22

cslrd 发表于 2014-5-8 07:07
MDK版本是4.23是否同C编译器版本有关?

我也没有遇到过这样的问题……
我只能晚些时候再换台电脑试试看了

cslrd 发表于 2014-5-8 08:55:27

catwill 发表于 2014-5-8 07:26
我也没有遇到过这样的问题……
我只能晚些时候再换台电脑试试看了

刚才用uVision4.00a 可以打开,但是不带ARM编译器的。楼主可以用更高版本的MDK试一下是否有问题

y574924080 发表于 2014-5-8 13:00:39

我试试我的版本倒是可以打开



另外我把它转成uVision3 格式,大家可以试试能不能打开 train.Uv2

bondxie3 发表于 2014-5-8 13:06:06

标记,console控制台程序.

mummy108 发表于 2014-5-8 18:45:40

特地登录顶起~! 很强大的说 console 我本来打算自己弄一个的。。

catwill 发表于 2014-5-8 20:08:46

cslrd 发表于 2014-5-8 08:55
刚才用uVision4.00a 可以打开,但是不带ARM编译器的。楼主可以用更高版本的MDK试一下是否有问题 ...

如果可以的话,请把出错版本的MDK安装文件发到bbzsdts@126.com
晚些时候我会在我的电脑上试验一下

cslrd 发表于 2014-5-8 22:58:15

本帖最后由 cslrd 于 2014-5-8 23:03 编辑

catwill 发表于 2014-5-8 20:08
如果可以的话,请把出错版本的MDK安装文件发到
晚些时候我会在我的电脑上试验一下 ...

安装文件太大了,我就不发了。你如果有时间可以去下载一个4.23版本的测试一下。PS:我下载了18楼提供uv3格式与uv2格式,可以打开。可能是因为LZ提供的是uv4格式的原因

Jmhh247 发表于 2014-7-27 22:44:41

楼主你好,我是个菜鸟,我试了这个程序的仿真,运行的很好……我想请教下为什么要有静态和动态扩展两种命令,能举例说明什么情况用静态,什么情况用动态吗?

catwill 发表于 2014-7-28 08:42:01

静态是编译时决定的
动态是运行时决定的
大部分情况都可以用静态——除非你需要运行时改变的特性

Jmhh247 发表于 2014-7-28 14:08:23

catwill 发表于 2014-7-28 08:42
静态是编译时决定的
动态是运行时决定的
大部分情况都可以用静态——除非你需要运行时改变的特性 ...

谢谢楼主回复!
我明白了静态部分,还想请教下关于动态的问题:
所谓动态是不是说,可以在程序运行的任意时刻进行改变?
如果要进行改变的话,该怎么操作,是通过这个注册函数重新注册改变后的消息吗?

    // 注册扩展命令
    console_register_command(s_tExpandMSGMap, UBOUND(s_tExpandMSGMap));

我这些问题问的很乱,楼主不要介意哈,因为俺水平太菜啦{:sweat:}
楼主,希望你如果有闲暇时间的话,能再出个关于“需要运行时改变的特性”的范例吧,感觉这个功能很高级啊!(好像现有的范例没体现出这点,也可能是我没看出来{:sweat:} )
相信这样的话,会让更多的人,更容易的去理解你的作品,谢谢!{:lol:}

机器人天空 发表于 2014-7-28 14:14:00

mark。。。。。。

catwill 发表于 2014-7-29 14:14:52

Jmhh247 发表于 2014-7-28 14:08
谢谢楼主回复!
我明白了静态部分,还想请教下关于动态的问题:
所谓动态是不是说,可以在程序运行的任意 ...

所以说大部分的情况使用静态即可。
这个例子确实没有体现出动态的优势;确实没有用到运行时改变的特性。

通过这个
// 注册扩展命令
console_register_command(s_tExpandMSGMap, UBOUND(s_tExpandMSGMap));
即可在运行时改变——写两个消息地图即可

catwill 发表于 2014-7-29 14:16:05

说实话……
我目前也不知道什么时候需要使用动态扩展

Jmhh247 发表于 2014-7-29 17:29:43

catwill 发表于 2014-7-29 14:16
说实话……
我目前也不知道什么时候需要使用动态扩展

哈哈,楼主真坦诚,现在缺这种品质的人太多了{:handshake:} {:lol:}
这样的话,就等楼主知道了再分享一下吧……
另,虽然现在还搞不懂,我还是想请教下楼主,如果是动态的话,需不需要一个删除消息的函数呢?(现在已经有了注册函数){:smile:}

Jmhh247 发表于 2014-7-29 17:38:20

catwill 发表于 2014-7-29 14:14
所以说大部分的情况使用静态即可。
这个例子确实没有体现出动态的优势;确实没有用到运行时改变的特性。
...

我刚又看了源码,动态扩展是通过下面这个指针实现的,这样的话应该是不需要删除消息的函数,
就像你说那样,写几个消息,通过注册函数去切换就好……没看仔细啊{:sweat:}

const static msg_t *s_ptExpandMsgMap = NULL;    // 动态扩展消息地图指针
static uint8_t s_chExpandMSGNum = 0;            // 动态扩展消息数量

cn_x 发表于 2014-7-30 16:22:38

这个要支持一下,慢慢研究·······

tanek 发表于 2014-12-21 21:28:06

小宝加油,四哥看好你

slzm40 发表于 2014-12-24 09:41:22

名师出高徒,有空再分析下源程序。 好学习下。

slzm40 发表于 2014-12-24 21:43:32

分析了一下,不得不说,楼主状态机用的真牛,算是学习了。

slzm40 发表于 2014-12-24 23:29:39

想问下楼主,这个消息地图中,// parse 消息地图
typedef struct _msg_t msg_t;
typedef fsm_rt_t MSG_HANDLER(const msg_t *ptMsg, uint8_t *pchStr, uint8_t chNum);   // 消息处理函数类型

struct _msg_t {
    uint8_t *pchMsgStr;   // 消息
    MSG_HANDLER *fnHandler; // 消息处理函数
    uint8_t *pchHelpStr;    // 帮助信息指针
};

const static msg_t c_tMsgMap[] = {      // 消息地图
    { "help",&help_handler, "help -- print all command" },
    { "clear", &clear_handler, "clear -- clear the screen" },
    CONSOLE_MSG_MAP_SET               // 静态扩展命令插入宏
};

那个&help_handler 和这个 &clear_handler   取函数地址为什么要用&,不是取函数名就可以取地址了么?

y574924080 发表于 2014-12-25 01:12:01

slzm40 发表于 2014-12-24 23:29
想问下楼主,这个消息地图中,

我觉得c语法支持加上&和省略&,

但是加上&,让人第一眼就知道这个一个指针,

阅读源码的时候第一时间能得到更多信息,即便于阅读

slzm40 发表于 2014-12-25 11:20:29

y574924080 发表于 2014-12-25 01:12
我觉得c语法支持加上&和省略&,

但是加上&,让人第一眼就知道这个一个指针,


这个想法好,便于阅读,指明取址。

yutianyiren 发表于 2014-12-25 12:35:38

这个非常好,有时间试一下。

SealedGhost 发表于 2014-12-25 12:43:04

太好了,可以把我们用的换成这个了.真心感谢

90soso 发表于 2014-12-25 12:49:16

这个相当给力~~~~

swortering 发表于 2015-3-12 15:17:47

这个东东,非常好.准备下载下来好好改造.

随风允诺2015 发表于 2015-8-1 16:11:06

牛逼,有空研究一下源码。消化消化!

随风允诺2015 发表于 2015-8-7 15:21:16

想请教一下楼主
#define TOKEN_START         0
#define TOKEN_CHECK_BREAK   1
#define TOKEN_MOVE          2
#define TOKEN_RESET()       do { s_chState = 0; } while(0)
static fsm_rt_t token(uint8_t *pchStr, uint8_t *pchNum)
这个函数有什么作用?

cslrd 发表于 2015-8-9 07:54:16

随风允诺2015 发表于 2015-8-7 15:21
想请教一下楼主
#define TOKEN_START         0
#define TOKEN_CHECK_BREAK   1


将CONSOLE_TOKEN_CODE定义的标记转换成'\0'

love_zjb 发表于 2015-8-9 08:07:06

嗯 不错

Vampireyifeng 发表于 2015-8-9 23:07:09

控制台模块,mark 一下

guanglv2008 发表于 2015-8-9 23:14:25

这个是好东西   谢谢分享。。有空要试试

angg 发表于 2015-8-10 09:57:51

呵呵 好东西 是要收藏

淋湿的鸡毛 发表于 2015-8-27 10:36:00

楼主,这个控制台占大概用多少ram,我用stm8总是报溢出

Gorgon_Meducer 发表于 2015-8-27 14:52:58

淋湿的鸡毛 发表于 2015-8-27 10:36
楼主,这个控制台占大概用多少ram,我用stm8总是报溢出

你可以从以下几个地方来查找原因:
1、存储历史按键的部分是很消耗内存的,比如,如果你要保存过去5条历史输入,就需要64*5的存储器,你可以直接通过宏把这个功能关掉节省内存
2、命令行的最大长度应该也是可以用宏控制的,默认应该是64个字节,你可以改小,如果没有宏可以控制这个,请跟我一起鄙视作者
3、有很多const的东西应该是不占用SRAM的,如果占用了,你可以查查你用的编译器,然后找到对应的关键字确保他们没有占用不必要的SRAM
4、系统应该使用了FIFO来保存输入输出数据,可以缩小缓冲。

经过以上检查,我觉得对内存的占用不应该超过256个字节。

slzm40 发表于 2015-8-27 17:14:19

这份源码有做过详细分析。感觉在回车之后,获取到命令,后面就不需要过度的使用状态机。可以做详细的精简, 最后提供的handle接口,形如main那样,
handle(int argc,char *argv[]),在模块内部开一个数组指针,这样在handle里面获参数会更加方便。

Gorgon_Meducer 发表于 2015-8-27 17:24:00

slzm40 发表于 2015-8-27 17:14
这份源码有做过详细分析。感觉在回车之后,获取到命令,后面就不需要过度的使用状态机。可以做详细的精 ...

可能是有点过度,但也可以降低对处理器资源的占用,毕竟控制台是一个实时性要求非常低的任务,应该出让更多的
处理器时间给其他的状态机任务。

slzm40 发表于 2015-8-27 17:32:53

Gorgon_Meducer 发表于 2015-8-27 17:24
可能是有点过度,但也可以降低对处理器资源的占用,毕竟控制台是一个实时性要求非常低的任务,应该出让更 ...

可能的确是这方面的考虑,有时考虑到,一旦获取命令,就可以对命令进行整体解析,做尽快的回复。的确会占用一定的时间。 我作了比较多的改动,用于自己的工程。类似main参数,意思差不多,稍有点不同。


问下书月底放出来么?

bool LedControl(const void *ptMSG, uint8_t argc, uint8_t *argv[])
{
        if(argc != 2 ){
            console_print_str(console_led_warning);
                return true;
        }

        if(console_cmp_str(argv, "on")){
                console_print_str("led on is done!");
                HalLedSet(HAL_LED_ALL,HAL_LED_MODE_ON);
    }else if(console_cmp_str(argv, "off")){
                console_print_str("led off is done!");
                HalLedSet(HAL_LED_ALL,HAL_LED_MODE_OFF);
        }else if(console_cmp_str(argv, "flash")){
          console_print_str("led off is flash!");
                HalLedSet(HAL_LED_ALL,HAL_LED_MODE_FLASH);
        }else{
          console_print_str(console_led_warning);
        }
       
    return true;
}

淋湿的鸡毛 发表于 2015-8-28 09:02:00

Gorgon_Meducer 发表于 2015-8-27 14:52
你可以从以下几个地方来查找原因:
1、存储历史按键的部分是很消耗内存的,比如,如果你要保存过去5条 ...

我把历史消息改到一个,最大长度改为16,运行ok。谢谢傻孩子

Gorgon_Meducer 发表于 2015-8-28 13:32:32

本帖最后由 Gorgon_Meducer 于 2015-8-28 13:34 编辑

slzm40 发表于 2015-8-27 17:32
可能的确是这方面的考虑,有时考虑到,一旦获取命令,就可以对命令进行整体解析,做尽快的回复。的确会占 ...

Demo月底放出来。关于如何设计多任务的。另外,你把最后一个参数去掉我觉得没必要,毕竟确切地告诉你有多少个参数还是很有帮助的,也避免你重复计算。

slzm40 发表于 2015-8-28 16:06:14

Gorgon_Meducer 发表于 2015-8-28 13:32
Demo月底放出来。关于如何设计多任务的。另外,你把最后一个参数去掉我觉得没必要,毕竟确切地告诉你有多 ...

已经月底啦。 最后一个参数没有去掉。。我把它改成argc的名字了,以便和main 相似。只不过是改成总的参数数量,而非后续参数数量。

Gorgon_Meducer 发表于 2015-8-30 23:39:20

slzm40 发表于 2015-8-28 16:06
已经月底啦。 最后一个参数没有去掉。。我把它改成argc的名字了,以便和main 相似。只不过是改成总的参 ...

周一见……

FireHe 发表于 2015-8-31 01:16:32

{:lol:}
哦哦哦,大事件哦

新书终于来了

Chris_gong 发表于 2016-3-4 09:26:37


mark!顶一下

tanglj868 发表于 2016-3-18 12:55:15

标记标记。。。

qming51 发表于 2016-3-29 21:49:20

不错,准备试用

efree 发表于 2016-9-22 11:12:11

支持一下。牛。

didadida 发表于 2016-9-22 11:35:43

顶一下贴~~~

hotsauce1861 发表于 2016-10-13 10:16:30

真是牛人,顶顶顶

int 发表于 2016-10-22 23:36:54

支持一下!功能很强大,不过编译了一下,ROM大概占10k左右,RAM大概3K左右。鱼与熊掌不可兼得阿。

guo407214944 发表于 2016-12-17 09:15:31

你好,你能不能单独把这三个文件私发给我下,公司,家里都不能下载,应该不是网络原因。企鹅407214944,邮件也行。感激不尽!

guo407214944 发表于 2016-12-17 09:30:57

int 发表于 2016-10-22 23:36
支持一下!功能很强大,不过编译了一下,ROM大概占10k左右,RAM大概3K左右。鱼与熊掌不可兼得阿。 ...

你好,能不能把你在这个帖子中下载的三个文件发给我下,我直接在这个帖子上下载,没法下载。非常感谢/407214944企鹅

int 发表于 2016-12-19 10:45:52

guo407214944 发表于 2016-12-17 09:30
你好,能不能把你在这个帖子中下载的三个文件发给我下,我直接在这个帖子上下载,没法下载。非常感谢/407 ...

已发到邮箱,估计还是你的网络问题

会会 发表于 2016-12-28 09:25:10

多谢,学习了

bigwei 发表于 2018-7-8 10:03:15

多谢分享~~~~~~{:victory:}

guanglv2008 发表于 2018-12-19 15:07:48

好东西啊   看一次顶一次   感谢分享。。

jjj 发表于 2020-8-10 08:54:11

mark 串口控制台模块
页: [1]
查看完整版本: [分享][交流]发一个控制台模块