OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 10773|回复: 27

关于状态机编程,以ALIENTEK V2.0为平台

[复制链接]

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
发表于 2014-1-9 17:44:34 | 显示全部楼层 |阅读模式

 

最近看到几个关于状态机编程的帖子,发现大家更多的是在辩论状态机编程好不好。这里,我以原子提供的《实验按键输入》为基础,着重以key.c中的按键扫描函数KEY_Scan()为例,写一个的基于状态机形式的KEY_Scan(),来扫描KEY_UPKEY0KEY1KEY2,希望能起到抛砖引玉的作用。工程项目文件我打包挂在下面,大家可以下载到自己的板子上运行,效果很好。

一般情况下,在按键扫描函数中最让人蛋疼的就是按键消抖延时。这是可靠程序所必须的。否则,按键一次,MCU读到的却是N多次。但是用延时来给按键消抖,代价是很大的。对于我们用户来说,KEY_Scan()中的“delay_ms(10); //抖动延时”并无大碍,但是对于STM32这样的72MHzMCU来说,delay_ms(10)是很漫长的。所以,如果你的系统里需要数码管显示数据,同时还要实时扫描按键,而你的KEY_Scan()中含有类似delay_ms(10)这样的语句的话,那么数码管的数据显示肯定会有问题,除非你搭载了uC/OS-II。除此之外,“delay_ms(10)”至少还有以下两点不足:

第一,如果需要实时扫描按键,那么扫描时间间隔一定要小于按键消抖延时,否则,会丢键;

第二,由于按键本身的机械性能不同,对某些按键而言,delay_ms(10)可能因延时过长而丢键,也可能因延时不足而一键多发。

归根结底,delay_ms(10)对要求不太严格的系统是可以用的,而且效果也不错。但是还是治标不治本。而状态机的优势就是不用delay_ms(10)这种方法。

下面,以状态机的方法,来写一个KEY_Scan()函数(其实状态机的方法真的用的很广,尤其是在多任务运行的情况下)

用状态机思想去进行单片机编程,比较通用的方法就是用switch分支语句来进行状态的跳转,不仅编程模式相对固定,而且程序可读性也很好。就像下面这种框架:

  

    switch(KeyStatus)

    {

        case KEY_SEARCH_STATUS:   //按键查询状态

        {

……            

KeyStatus=KEY_ACK_STATUS;  //按键下一个状态为确认状态

……

        }

 

        case KEY_ACK_STATUS:  //按键确认状态

        {

             ……

             KeyStatus=KEY_REALEASE_STATUS;  //按键下一个状态为释放状态

             ……

        }

 

        case KEY_REALEASE_STATUS:    //按键释放状态

        {

            ……

            KeyStatus=KEY_SEARCH_STATUS;  //按键下一个状态为释放状态

            ……

        }

        default: break;

    }

 

key.c中,状态机形式的KEY_Scan()如下所示,注意,整个函数是没有delay的。另外,原子提供的KEY_Scan()通过入口参数是可以选择是否支持长按的,这里我只把注意力集中于状态机上,你也可以实现类似选择是否支持长按,加一个静态变量来标识按键的状态即可,并不难。工程文件在下面。

 

u8 KEY_Scan(void)

{         

    static u8 KeyStatus=KEY_SEARCH_STATUS; //静态变量,保存按键状态

    static u8 KeyCurPress=0; //静态变量,保存当前按键的键值

           u8 KeyValue=0;

 

    if(KEY0==0||KEY1==0||KEY2==0||KEY3==1)   //如果有按键按下

    {
        if(KEY0==0) KeyValue=KEY_RIGHT;

        else if(KEY1==0) KeyValue=KEY_DOWN;

        else if(KEY2==0) KeyValue=KEY_LEFT;

        else if(KEY3==1) KeyValue=KEY_UP;

    }

  

    switch(KeyStatus)

    {

        case KEY_SEARCH_STATUS:   //按键查询状态

        {
             if(KeyValue)   //如果有按键按下(暂时不管到底是哪个Key按下)

                  KeyStatus=KEY_ACK_STATUS;  //则按键下一个状态为确认状态

             return 0;

        }

 

        case KEY_ACK_STATUS:    //按键确认状态

        {

             if(!KeyValue)      //如果没有按键按下

                  KeyStatus=KEY_SEARCH_STATUS;   //按键状态返回为查询状态

             else //否则有按键按下

             {

                  if(KEY0==0) KeyCurPress=KEY_RIGHT;

                  else if(KEY1==0) KeyCurPress=KEY_DOWN;

                  else if(KEY2==0) KeyCurPress=KEY_LEFT;

                  else if(KEY3==1) KeyCurPress=KEY_UP;

                  KeyStatus=KEY_REALEASE_STATUS; //按键下一个状态为释放状态

             }

             return 0;

        }

 

        case KEY_REALEASE_STATUS:      //按键释放状态

        {

            if(!KeyValue)      //按键释放

            {

                KeyStatus=KEY_SEARCH_STATUS;  //按键下一个状态为释放状态

                return KeyCurPress;  //返回当前按键

            }

            return 0;   

        }

        default: return 0;

    }

}

修改:发现
KEY_Scan() 修改为以下代码更简洁(对应修改后的工程项目文件也在下面):

u8 KEY_Scan(void)

{         

    static u8 KeyStatus=KEY_SEARCH_STATUS; //静态变量,保存按键状态

    static u8 KeyCurPress=0; //静态变量,保存当前按键的键值

           u8  HasKeyPressed ;

 

    /* 如果有按键按下,HasKeyPressed置1.否则,置0 */

    HasKeyPressed=(KEY0==0||KEY1==0||KEY2==0||KEY3==1)?1:0;

  

    switch(KeyStatus)

    {

        case KEY_SEARCH_STATUS:   //按键查询状态

        {
             if( HasKeyPressed )   //如果有按键按下(暂时不管到底是哪个Key按下)

                  KeyStatus=KEY_ACK_STATUS;  //则按键下一个状态为确认状态

             return 0;

        }

 

        case KEY_ACK_STATUS:    //按键确认状态

        {

             if(! HasKeyPressed )      //如果没有按键按下

                  KeyStatus=KEY_SEARCH_STATUS;   //按键状态返回为查询状态

             else //否则有按键按下

             {

                  if(KEY0==0) KeyCurPress=KEY_RIGHT;

                  else if(KEY1==0) KeyCurPress=KEY_DOWN;

                  else if(KEY2==0) KeyCurPress=KEY_LEFT;

                  else if(KEY3==1) KeyCurPress=KEY_UP;

                  KeyStatus=KEY_REALEASE_STATUS; //按键下一个状态为释放状态

             }

             return 0;

        }

 

        case KEY_REALEASE_STATUS:      //按键释放状态

        {

            if(! HasKeyPressed )      //按键释放

            {

                KeyStatus=KEY_SEARCH_STATUS; //按键下一个状态为释放状态

                return KeyCurPress;//返回当前按键

            }

            return 0;   

        }

        default: return 0;

    }

}


状态机实现 ALIETEK 实验3 按键输入.rar

1.51 MB, 下载次数: 935

实验3 状态机实现 实验3 按键输入 修改.rar

1.51 MB, 下载次数: 1214

正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-9 22:13:38 | 显示全部楼层
回复【2楼】miaoguoqiang:
---------------------------------
如果你想按键按下触发,那更简单了,把最后一个状态拿掉,中间修改一下,将case循环回去即可:

u8 KEY_Scan(void)
{         
    static u8 KeyStatus=KEY_SEARCH_STATUS; //静态变量,保存按键状态
    static u8 KeyCurPress=0; //静态变量,保存当前按键的键值
           u8 HasKeyPressed ;
 
    /* 如果有按键按下,HasKeyPressed置1.否则,置0 */
    HasKeyPressed=(KEY0==0||KEY1==0||KEY2==0||KEY3==1)?1:0;
  
    switch(KeyStatus)
    {
        case KEY_SEARCH_STATUS:   //按键查询状态
        {
             if( HasKeyPressed )   //如果有按键按下(暂时不管到底是哪个Key按下)
                  KeyStatus=KEY_ACK_STATUS;  //则按键下一个状态为确认状态
             return 0;
        }
 
        case KEY_ACK_STATUS:    //按键确认状态
        {
           KeyStatus=KEY_SEARCH_STATUS;  //按键返回至查询状态            
             if(HasKeyPressed )      //如果有按键按下
             {
                  if(KEY0==0) KeyCurPress=KEY_RIGHT;
                  else if(KEY1==0) KeyCurPress=KEY_DOWN;
                  else if(KEY2==0) KeyCurPress=KEY_LEFT;
                  else if(KEY3==1) KeyCurPress=KEY_UP;
                  return KeyCurPress; //返回当前按键 
             }
             return 0;
        }
        default: return 0;
   }
}
回复 支持 0 反对 1

使用道具 举报

27

主题

711

帖子

0

精华

版主

Rank: 7Rank: 7Rank: 7

积分
12581
金钱
12581
注册时间
2015-11-5
在线时间
2152 小时
发表于 2015-12-8 10:01:36 | 显示全部楼层
楼主,你上面的状态机确实没有实现真正意义的消抖啊,举个简单的例子,假如我在while(1)循环里是根据不同的按键来让不同的LED状态取反,整个while(1)循环时间远远不到1ms,但我们按键消抖一般都需要10ms,所以肯定不能以整体循环来作为延时的。我们公司项目也有用状态机来做,整个while(1)循环没有任何的等待,因此执行时间不会太长。

对于类似于按键这种消抖延时,可以采用一个类似于软件定时器来计算时间,例如开定时器2中断,1ms产生一次定时器中断,中断里面的内容十分简单,只是单纯的让节拍数+1,然后碰到delay_ms(10)这些地方时统一换成节拍计算,具体实现方法如下:
volatile u32 KEY_DELAY_CNT;      // 按键延时保存的旧的节拍数
volatile u32 TIM2_CNT = 0;       // 定时器2当前节拍数

void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)      // 检查TIM2中断标志
    {
        TIM2_CNT++;    // 节拍数+1
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);          // 清除TIM2中断标志
    }
}


switch(key_status)
{
    case KEY_PRESS:
        ...
        ...
        KEY_DELAY_CNT = TIM2_CNT;       // 记录当前TIM2的TICK数
        key_status = KEY_DELAY_10MS;    // 状态转换
        break;
        
    case KEY_DELAY_10MS:
        if( TIM2_CNT >= ( KEY_DELAY_CNT + 10 ) )     // 计算TIM2的当前TICK数是否比按键按下时的TICK数多10个(10个1ms中断,即10ms)
        {
            // 消抖完毕,执行LED取反等操作
            ...
            ...
            key_status = KEY_RELEASE;    // 状态转换
        }
        break;
        
    case KEY_RELEASE:
        ...
        ...
        break;
}


具体算法思想实现如上,按键按下和松开都跟LZ的几乎一致,但我多加了一个延时10ms的状态判断,并且该状态并没有直接delay来消耗CPU时间,而是通过一个定时器2来计算节拍,作用类似于操作系统的软件定时器,当然上面的节拍数并没有考虑溢出的情况,出现溢出的情况时其实也有解决的算法,可以参考FreeRTOS里面对延时溢出时的处理(采用2个链表,其中一个是正常延时链表,另一个则为延时溢出链表),有更多的问题可以加QQ详聊:410846867
拿来长岛冰茶换我半晚安睡
回复 支持 1 反对 0

使用道具 举报

105

主题

522

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1386
金钱
1386
注册时间
2012-10-23
在线时间
97 小时
发表于 2014-1-9 21:36:51 | 显示全部楼层
这样就只有释放才能执行一个按键的功能
回复 支持 反对

使用道具 举报

105

主题

522

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1386
金钱
1386
注册时间
2012-10-23
在线时间
97 小时
发表于 2014-1-9 22:34:05 | 显示全部楼层
回复【3楼】shr5791:
---------------------------------
这样不就没达到按键消抖了?靠程序运行一个一周的时间来消抖吗?如果程序运行周期只有1ms,就不能避免某些抖动
或者是中断里面每隔一定时间调用?
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-9 23:29:30 | 显示全部楼层
回复【4楼】miaoguoqiang:
---------------------------------
这要看你一个while(1)的执行周期了,如果短(比如原子的这个实验,while就很短),那就不需要了,如果 while(1)的执行周期很长,就应该定时扫描。不丢键就行。这样就能实时扫描了。
回复 支持 反对

使用道具 举报

13

主题

121

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
375
金钱
375
注册时间
2013-12-12
在线时间
13 小时
发表于 2014-1-10 00:10:16 | 显示全部楼层
2楼没说错啊,状态机不就是通过程序每执行一次调用一次key_scan()或是定时中断等来达到delay延时的效果,可以再加一个状态来添加长按的效果!
回复 支持 反对

使用道具 举报

120

主题

7878

帖子

13

精华

资深版主

Rank: 8Rank: 8

积分
12012
金钱
12012
注册时间
2013-9-10
在线时间
427 小时
发表于 2014-1-10 08:52:09 | 显示全部楼层
回复【楼主位】shr5791:
---------------------------------
这个方法好,但是while的时间短就不好了

目前有个作品在用这个方法了
现在,程序把烂铜烂铁变得智能化了,人呢,一旦离开了这烂铜烂铁就不知道干啥了
回复 支持 反对

使用道具 举报

头像被屏蔽

38

主题

382

帖子

0

精华

高级会员

Rank: 4

积分
596
金钱
596
注册时间
2012-12-5
在线时间
19 小时
发表于 2014-1-10 10:27:04 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-10 14:05:19 | 显示全部楼层
回复【7楼】Badu_Space:
---------------------------------
我到觉得while短那就更好了,扫描实时性更好。如果while一个循环时间太长,你就必须开定时器来定时调用key_scan了。
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-10 14:06:28 | 显示全部楼层
回复【8楼】toddchen:
---------------------------------
说得很好啊,看好一句话:“将CPU解放出来”。
回复 支持 反对

使用道具 举报

120

主题

7878

帖子

13

精华

资深版主

Rank: 8Rank: 8

积分
12012
金钱
12012
注册时间
2013-9-10
在线时间
427 小时
发表于 2014-1-10 14:13:50 | 显示全部楼层
回复【9楼】shr5791:
---------------------------------
是啊,这样每个事件占用CPU的时间短
现在,程序把烂铜烂铁变得智能化了,人呢,一旦离开了这烂铜烂铁就不知道干啥了
回复 支持 反对

使用道具 举报

120

主题

7878

帖子

13

精华

资深版主

Rank: 8Rank: 8

积分
12012
金钱
12012
注册时间
2013-9-10
在线时间
427 小时
发表于 2014-1-10 14:14:23 | 显示全部楼层
回复【10楼】shr5791:
---------------------------------
同感
现在,程序把烂铜烂铁变得智能化了,人呢,一旦离开了这烂铜烂铁就不知道干啥了
回复 支持 反对

使用道具 举报

5

主题

100

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
361
金钱
361
注册时间
2012-8-10
在线时间
40 小时
发表于 2014-1-10 16:37:11 | 显示全部楼层
回复【5楼】shr5791:

回复【4楼】miaoguoqiang:
---------------------------------
这要看你一个while(1)的执行周期了,如果短(比如原子的这个实验,while就很短),那就不需要了,如果
while(1)的执行周期很长,就应该定时扫描。不丢键就行。这样就能实时扫描了。

---------------------------------
如果while(1)的执行周期很短而且又不用定时扫描,那这种状态机按键扫描是这样做到消抖的呀?谢谢!
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-10 16:53:34 | 显示全部楼层
回复【13楼】hwl1023:
---------------------------------
比如,你的MCU上电后第一步就是要判断按键值,然后才根据按键值去执行不同的操作,这就是你说的情况了吧。如下:

u8 KeyVal=0;

main()
{
        while( ( KeyVal = Key_Scan() ) ==0 );  //其实这句中的while不也是个循环么,依然会消抖
        
        switch(KeyVal )
        {
                 //根据 KeyVal 去执行不同的操作
        }
}
回复 支持 反对

使用道具 举报

5

主题

100

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
361
金钱
361
注册时间
2012-8-10
在线时间
40 小时
发表于 2014-1-10 17:06:24 | 显示全部楼层
回复【14楼】shr5791:

回复【13楼】hwl1023:
---------------------------------
比如,你的MCU上电后第一步就是要判断按键值,然后才根据按键值去执行不同的操作,这就是你说的情况了吧。如下:
u8 KeyVal=0;
main()
{
        while( ( KeyVal = Key_Scan() ) ==0 );  //其实这句中的while不也是个循环么,依然会消抖
        
        switch(KeyVal )
        {
                 //根据 KeyVal 去执行不同的操作
        }
}

---------------------------------
while( ( KeyVal = Key_Scan() ) ==0 );  //其实这句中的while不也是个循环么,依然会消抖  /*这样做如果没有按键按下的话,程序就停在这里做不了其他事了呀*/
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-10 18:17:37 | 显示全部楼层
回复【15楼】hwl1023:
---------------------------------
晕,我这只是对你在12楼说的问题举个例子。我前面不是有“比如”两个字了嘛。我都被你讨论糊涂了。
回复 支持 反对

使用道具 举报

5

主题

100

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
361
金钱
361
注册时间
2012-8-10
在线时间
40 小时
发表于 2014-1-10 18:25:41 | 显示全部楼层
回复【16楼】shr5791:
---------------------------------
我是被你的例子搞糊涂了,因为实测了下发现按键有抖动,所以问下你是怎么消抖了。
回复 支持 反对

使用道具 举报

25

主题

683

帖子

0

精华

论坛大神

Rank: 7Rank: 7Rank: 7

积分
1351
金钱
1351
注册时间
2012-4-25
在线时间
195 小时
发表于 2014-1-11 10:16:43 | 显示全部楼层
单机编程无所谓 浪费CPU资源,你反正是 while一个大循环,你delay 10或100(但也不能太长),都没啥关系,有中断在呢。
1-1
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-11 13:49:15 | 显示全部楼层
回复【18楼】mygod:
---------------------------------
那要看你的系统是简单还是复杂了,如果涉及多任务并行,delay 10或100是绝对不允许的。
回复 支持 反对

使用道具 举报

6

主题

31

帖子

0

精华

初级会员

Rank: 2

积分
75
金钱
75
注册时间
2012-6-5
在线时间
0 小时
发表于 2014-1-12 16:17:28 | 显示全部楼层
多任务的话还是用操作系统吧
回复 支持 反对

使用道具 举报

8

主题

93

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
446
金钱
446
注册时间
2013-9-22
在线时间
0 小时
 楼主| 发表于 2014-1-12 16:25:53 | 显示全部楼层
回复【20楼】若如初见:
---------------------------------
呵呵,一遇到多任务处理就用操作系统不太现实吧。你做项目应该经常遇到多任务,你每次都用操作系统吗?如果MCU选的的是8051或者AVR,你打算用什么RTOS呢???推荐看看我以前发的一个帖子:
http://www.openedv.com/posts/list/22990.htm
回复 支持 反对

使用道具 举报

4

主题

44

帖子

0

精华

初级会员

Rank: 2

积分
80
金钱
80
注册时间
2014-11-18
在线时间
0 小时
发表于 2015-1-9 17:07:19 | 显示全部楼层
这个好,最近刚好遇见这种问题,看了以后很有帮助
我是伸手党 ←_←
回复 支持 反对

使用道具 举报

1

主题

108

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
352
金钱
352
注册时间
2012-11-8
在线时间
44 小时
发表于 2015-1-9 17:29:18 | 显示全部楼层
我对状态机编程的理解: 将事件的所有状态枚举化,触发事件(如外部中断,按键被按下,等待事件到达等)改变被枚举的事件状态,而事件状态则决定下一步对该事件的操作!
回复 支持 反对

使用道具 举报

32

主题

286

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1366
金钱
1366
注册时间
2014-3-27
在线时间
358 小时
发表于 2015-12-7 17:00:07 | 显示全部楼层
最近在用状态机编程,正好学习。
回复 支持 反对

使用道具 举报

13

主题

296

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
2067
金钱
2067
注册时间
2012-5-26
在线时间
292 小时
发表于 2015-12-8 09:23:17 | 显示全部楼层
关注一下。。
活着才是王道!健康是一切的前提!
回复 支持 反对

使用道具 举报

2

主题

5

帖子

0

精华

新手上路

积分
34
金钱
34
注册时间
2017-1-14
在线时间
7 小时
发表于 2017-2-9 10:15:46 | 显示全部楼层
FreeRTOS 发表于 2015-12-8 10:01
楼主,你上面的状态机确实没有实现真正意义的消抖啊,举个简单的例子,假如我在while(1)循环里是根据不同的 ...

此方法确实有改进,但是没有考虑按键被长按时计数器溢出问题。
计数器溢出则会导致某次按键检测不到的情况。
回复 支持 反对

使用道具 举报

27

主题

711

帖子

0

精华

版主

Rank: 7Rank: 7Rank: 7

积分
12581
金钱
12581
注册时间
2015-11-5
在线时间
2152 小时
发表于 2017-2-9 10:34:38 | 显示全部楼层
yszh0836 发表于 2017-2-9 10:15
此方法确实有改进,但是没有考虑按键被长按时计数器溢出问题。
计数器溢出则会导致某次按键检测不到的情 ...

我上面采用的判断语句是 if( TIM2_CNT >= ( KEY_DELAY_CNT + 10 ) )
这种判断确实有问题,其实大部分的人使用判断方式是 if( (TIM2_CNT - KEY_DELAY_CNT) > 10 )
这种方式可以避免溢出的问题,但延时时间不能大于u32的限制

拿来长岛冰茶换我半晚安睡
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-6-26 01:33

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表