搜索
bottom↓
回复: 0

《MiniPro STM32H750 开发指南》 第六十三章

[复制链接]

出0入234汤圆

发表于 2022-10-13 14:59:37 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-10-13 14:59 编辑

1)实验平台:正点原子MiniPro STM32H750开发板
2)平台购买链接:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar
4)MiniPro STM32H750技术交流QQ群:756580169 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png
第六十三章 UCOSII实验3-消息队列、信号量集和软件定时器


上一章,我们学习了如何使用UCOSII的信号量和邮箱的使用,本章,我们将学习消息队列、信号量集和软件定时器的使用。
本章分为如下几个小节:
63.1 UCOSII消息队列、信号量集和软件定时器简介
63.2 硬件设计
63.3 程序设计
63.4 下载验证


63.1 UCOSII消息队列、信号量集和软件定时器简介
63.1.1 消息队列

消息队列可以视为消息邮箱的数组形式,消息邮箱一次传递一则消息,而消息队列可以在任务之间传递多条消息。消息队列的工作情况如图63.1.1所示:
第六十三章 UCOSII实验3302.png
图63.1.1.1 消息队列的工作情况图

        从上图可知,任务可以向消息队列中释放消息,只有任务才能从消息队列中请求消息,任务可以始终请求消息,也可以周期性地请求消息。消息队列具有一定的长度,其长度可包含的消息个数,如果向队列中释放消息的速度大于从队列中请求消息的速度,那么消息队列将会溢出。消息队列的数据结构如图63.1.2所示:
第六十三章 UCOSII实验3471.png
图 63.1.1.2 消息队列的数据结构图

消息队列由三个部分组成:事件控制块、消息队列和消息。当把事件控制块成员OSEventType的值设置为OS_EVENT_TYPE_Q时,该事件控制块描述的就是一个消息队列。
        从上图可以知道,消息队列相当于一个任务等待列表的消息邮箱数组,事件控制块成员OSEventPtr指向了一个叫做队列控制块(OS_Q)的结构,该结构管理了一个数组MsgTbl[],该数组中的元素都是一些指向消息的指针。
        队列控制块(OS_Q)的结构定义:
typedef struct os_q {                             /* 队列控制块 */
    struct os_q   *OSQPtr;                        /* 指向下一个空的队列控制块 */
    void         **OSQStart;                       /* 指向消息指针数组的起始地址 */
    void         **OSQEnd;                         /* 指向消息指针数组结束单元的下一个单元 */
    void         **OSQIn;                           /* 指向插入一条消息的位置 */
    void         **OSQOut;                          /* 指向被取出消息的位置 */
    INT16U         OSQSize;                        /* 数组的长度 */
    INT16U         OSQEntries;                     /* 已存放消息指针的元素数目 */
} OS_Q;
        其中,可以移动的指针为OSQIn和OSQOut,而指针OSQStart和OSQEnd只是一个标志(常指针)。当可移动的指针OSQIn或OSQOut移动到数组末尾,也就是与OSQEnd相等时,可移动的指针将会被调整到数组的起始位置OSQStart。也就是说,从效果上看,指针OSQEnd与OSQStart等值。于是,这个由消息指针构成的数组就头尾衔接起来形成了一个循环队列,如图63.1.3所示:
第六十三章 UCOSII实验31412.png
图63.1.1.3 消息指针数组构成的环形数据缓冲区

        在UCOSII初始化时,系统将按os_cfg.h文件中的OS_MAX_QS的数值定义OS_MAX_QS个队列控制块,并用队列控制块中的指针OSQptr将所有队列控制块链接为链表。由于这时候还没有使用它们,所以这个链表叫做空队列控制块链表。
消息队列相关的主要操作有:创建消息队列函数OSQCreate、请求消息队列函数OSQPend和向消息队列发送消息函数OSQPost。后面再对这几个函数进行讲解。
消息到这里就介绍完成了,想了解更多的朋友可以参考《嵌入式实时操作系统UCOSII原理及应用》第五章。
63.1.2 信号量集
        在实际应用中,任务常常需要与多个事件同步,即要根据多个信号量组合作用的结果来决定任务的运行方式。UCOSII为了实现多个信号量组合的功能定义了一种特殊的数据结构——信号量集。
信号量集所能管理的信号量都是一些二值信号,所有信号量集实质上是一种可以对多个输入的逻辑信号进行基本逻辑运算的组合逻辑,其示意图如图63.1.2.1所示:
第六十三章 UCOSII实验31898.png
图63.1.2.1 信号量集示意图

        不同于信号量、消息邮箱、消息队列等事件,UCOSII不使用事件控制块来描述信号量集,而使用了一个叫做标志组的结构OS_FLAG_GRP来描述。OS_FLAG_GRP结构如下:
typedef struct os_flag_grp {                         /* 标志组 */
    INT8U         OSFlagType;                          /* 信号量集的标志 */
    void         *OSFlagWaitList;                     /* 指向等待任务链表的指针 */
    OS_FLAGS      OSFlagFlags;                        /* 所有信号列表 */
} OS_FLAG_GRP;
        成员OSFlagFlags是一个指针,当一个信号量集被创建后,这个指针指向了这个信号量集的等待任务链表。
        与其他前面介绍过的事件不同,信号量集用一个双向链表来组织等待任务,每一个等待任务都是该链表中的一个节点(node)。标志组OS_FLAG_GRP的成员OSFlagWaitList就指向了信号量集的这个等待任务链表。等待任务链表节点OS_FLAG_NODE的结构如下:
typedef struct os_flag_node {                       /*  等待任务链表节点  */
    void         *OSFlagNodeNext;                     /*  指向下一个节点的指针 */
    void         *OSFlagNodePrev;                     /*  指向前一个节点的指针 */
    void         *OSFlagNodeTCB;                      /*  指向对应任务控制块的指针 */
    void         *OSFlagNodeFlagGrp;                 /*  反向指向信号量集的指针 */
    OS_FLAGS      OSFlagNodeFlags;                   /*  信号过滤器 */
    INT8U         OSFlagNodeWaitType;                /*  定义逻辑运算关系的数据 */
} OS_FLAG_NODE;
        其中OSFlagNodeWaitType是定义逻辑运算关系的一个常数(根据需要设置),其可选值和对应的逻辑关系如表63.1.2.1所示:
324FE766-0B0A-4f38-B372-AB968AF5AB3C.png
表63.1.2.1 OSFlagNodeWaitType可选值及其意义

OSFlagFlags、OSFlagNodeFlags、OSFlagNodeWaitType三者的关系如图63.1.2.2所示:
第六十三章 UCOSII实验33282.png
图63.1.2.2 标志组与等待任务共同完成信号量集的逻辑运算及控制

        为了方便说明,我们将OSFlagFlags定义为8位,但是UCOSII支持8位/16位/32位定义,这个通过修改OS_FLAGS的类型来确定(UCOSII默认设置OS_FLAGS为16位)。
        上图清楚表达了信号量集各成员的关系:OSFlagFlags位信号量表,通过发送信号量集任务设置;OSFlagNodeFlags为信号滤波器,由请求信号量集的任务设置,用于选择性的挑选OSFlagFlags中的部分(或全部)位作为有效信号;OSFlagNodeWaitType定义有效信号的逻辑运算关系,也是由请求信号量集的任务设置,用于选择有效的组合方式(0/1?与/或?)。
        举个简单的例子,假设请求信号量集的任务设置OSFlagNodeFlags的值为0x0F,同时设置OSFlagNodeWaitType的值为WAIT_SET_ANY,那么只要OSFlagFlags的低四位的任何一位为1,请求信号量集的任务将得到有效的请求,从而执行相关操作;如果第四位都为0,那么请求信号量集的任务将得到无效的请求。
        信号量集相关的主要操作有:创建一个信号量集函数OSFlagCreate,请求一个信号量集函数OSFlagPend,向信号量集发送信号函数OSFlagPost。后面再对这几个函数进行讲解。
信号量集就介绍到这里,更详细的介绍,请参考《嵌入式实时操作系统UCOSII原理及应用》第六章。
63.1.3 软件定时器
UCOSII从V2.83版本以后,加入了软件定时器,这使得UCOSII的功能更加完善,在其上的应用程序开发与移植也更加方便。在实时操作系统中一个好的软件定时器实现要求有较高的精度、较小的处理器开销,且占用较少的储存器资源。
通过前面的学习,我们知道UCOSII通过OSTimTick函数对时钟节拍进行加1操作,同时遍历任务控制块,以判断任务延时是否到时。软件定时器同样由OSTimTick提供时钟,但是软件定时器的时钟还受OS_TMR_CFG_TICKS_PER_SEC设置的控制,也就是在UCOSII的时钟节拍上面在做了一次“分频”,软件定时器的最快时钟节拍就等于UCOSII的系统时钟节拍。这也决定了软件定时器的精度。
软件定时器定义了一个单独的计数器OSTmrTime,用于软件定时器的计时,UCOSII并不在OSTimTick中进行软件定时器的到时判断与处理,而是创建了一个高于应用程序中所有其他任务优先级的定时器管理任务OSTmr_Task,在这个任务中进行定时器的到时判断和处理。时钟节拍函数通过信号量给这个高优先级任务发信号。这种方法缩短了中断服务程序的执行时间,但也使得定时器到时处理函数的响应收到中断推出时恢复现场和任务切换的影响。
UCOSII中软件定时器的实现方法是,将定时器按定时事件分组,使得每次时钟节拍到来时只对部分定时器及逆行比较操作,缩短了每次处理的时间。但这就需要动态地维护一个定时器组。定时器组的维护只是在每次定时器到时的时候才发生,而且定时器从组中移除和再插入操作不需要排序。这是一种比较高效的算法,减少了维护所需的操作时间。
UCOSII软件定时器实现了3类链表的维护:
OS_EXT  OS_TMR            OSTmrTbl[OS_TMR_CFG_MAX];         /* 定时器控制块数组OS_EXT */
OS_EXT  OS_TMR           *OSTmrFreeList;                      /* 空闲定时器控制块链表指针  */
OS_EXT  OS_TMR_WHEEL     OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE];         /* 定时器轮 */
其中OS_TMR为定时器控制块,定时器控制块是软件定时器管理的基本单元,包含软件定时器的名称、定时时间、在链表中的位置、使用状态、使用方式,以及到时回调函数及其参数等基本信息。
OSTmrTbl[OS_TMR_CFG_MAX]:以数组的形式静态分配定时器控制块所需的RAM空间,并存储所有已建立的定时器控制块,OS_TMR_CFG_MAX为最大软件定时器的个数。
OSTmrFreeList:为空闲定时器控制块链表头指针。空闲态的定时器控制块(OS_TMR)中,OSTmrnext和OSTmrPrev两个指针分别指向空闲控制块的前一个和后一个,组织了空闲控制块双向链表。建立定时器时,从这个链表中搜索空闲定时器控制块。
        OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]:该数组的每个元素都是已开启定时器的一个分组,元素中记录了指向该分组中第一个定时器控制块的指针,以及定时器控制块的个数。运行态的定时器控制块(OS_TMR)中,OSTmrnext和OSTmrPre两个指针同样也组织了所在分组中定时器控制块的双向链表。软件定时器管理所需的数据结构示意图如图63.1.3.1所示:
第六十三章 UCOSII实验35388.png
图63.1.3.1 软件定时器管理所需的数据结构示意图

OS_TMR_CFG_WHEEL_SIZE定义了OSTmrWheelTbl的大小,同时这个值也是定时器分组的依据。按照定时器到时值与OS_TMR_CFG_WHEEL_SIZE相除的余数进行分组:不同余数的定时器放在不同分组中;相同余数的定时器处在同一组中,由双向链表连接。这样,在余数值为0~OS_TMR_CFG_WHEEL_SIZE - 1的不同定时器控制块,正好分别对应了数组元素OSTmrWheelTbl[0]~OSTmrWheelTbl[OS_TMR_CFGWHEEL_SIZE - 1]的不同分组。每次时钟节拍到来时,时钟数OSTmrTime值加 1,然后也进行求余操作,只有余数相同的那组定时器才有可能到时,所以只对该组定时器进行判断。这种方法比循环判断所有定时器更高效。随着时钟数的累加,处理的分组也由0~OS_TMR_CFG_WHEEL_SIZE - 1循环。这里,我们推荐OS_TMR_CFG_WHEEL_SIZE的取值为2的N次方,以便采用移位操作计算余数,缩短处理时间。
        信号量唤醒定时器管理任务,计算出当前索要处理的分组后,程序遍历该分组中所有控制块,将当前OSTmrTime值与定时器控制块中的到时值(OSTmrMatch)相比较。若相等(即到时),则调用该定时器到时回调函数;若不相等,则判断该组中下一个定时器控制块。如此操作,直到该分组链表的结尾。软件定时器管理任务的流程如图63.1.3.2所示:
第六十三章 UCOSII实验36061.png
图63.1.3.2 软件定时器管理任务流程

        当运行完软件定时器的到时处理函数之后,需要进行该定时器控制块在链表中的移除和再插入操作。插入前需要重新计算定时器下次到时时所处的分组。计算公式如下:
定时器下次到时的OSTmrTime值(OSTmrMatch) = 定时器定时值 + 当前OSTmrTime值
新分组 = 定时器下次到时的OSTmrTime值(OSTmrMatch) % OS_TMR_CFG_WHEEL_SIZE
软件定时器相关的主要操作有:创建软件定时器函数OSTmrCreate,开启软件定时器函数OSTmrStart,停止软件定时器函数OSTmrStop。
63.2 硬件设计
1. 例程功能
本实验我们在UCOSII里面创建7个任务:开始任务、LED任务、触摸屏任务、队列消息显示任务、信号量集任务、按键扫描任务和主任务,开始任务用于创建邮箱、消息队列、信号量集以及其他任务,之后挂起;触摸屏任务用于在屏幕上画图,测试CPU使用率;队列消息显示任务请求消息队列,在得到消息后显示收到的消息数据;信号量集任务用于测试信号量集,采用OS_FLAG_WAIT_SET_ANY的方法,任何按键按下(包括TPAD),该任务都会控制蜂鸣器发出“滴”的一声;按键扫描任务用于按键扫描,优先级最高,将得到的键值通过消息邮箱发送出去;主任务创建3个软件定时器(定时器1,100ms溢出一次,显示CPU和内存使用率;定时2,200ms溢出一次,在固定区域不停的显示不同颜色;定时3,100ms溢出一次,用于自动发送消息到消息队列),并通过查询消息邮箱获得键值,根据键值执行LED1控制、控制软件定时器3的开关、触摸区域清屏、触摸屏校和软件定时器2的开关控制等。
通过按KEY0,可以启动/停止tmr2,从而控制屏幕的刷新,还会清除TOUCH区域的输入。
通过按KEY1则可以启动tmr3控制消息队列发送,可以在LCD上面看到Q和MEM的值慢慢变大(说明队列消息在增多,占用内存也随着消息增多而增大),在QUEUE MSG区,开始显示队列消息,再按一次KEY1停止tmr3,此时可以看到Q和MEM逐渐减小。当Q值变为0的时候,QUEUE MSG也停止显示(队列为空)。
通过KEY_UP按键,可以进行触摸屏校准(仅电阻屏有效)。
2. 硬件资源
1)RGB灯
     RED   : LED0 - PB4
     GREEN : LED1 - PE6
2)独立按键
KEY0  – PA1
      KEY1  – PA15
WK_UP – PA0
3) 正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)
5)触摸屏(电阻式/电容式)
6)蜂鸣器(PE4)
63.3 程序设计
62.3.1 UCOSII相关函数介绍

消息队列函数
在这里对本实验用到的UCOSII消息队列函数进行介绍,相关代码存放在os_q.c中。
1. OSQCreate函数
创建消息队列函数,其声明如下:
OS_EVENT  *OSQCreate (void **start, INT16U size)
函数描述:
用于创建一个消息队列
函数形参:
start:存放消息缓冲区指针数组的地址           
size:该数组的大小
函数返回值:
返回一个指向消息队列控制块的指针。如果没有空闲的控制块,返回空指针。
2. OSQPend函数
请求消息队列函数,其声明如下:
void  *OSQPend (OS_EVENT  *pevent, INT32U timeout, INT8U *perr)
函数描述:
用于向消息队列请求消息。
函数形参:
pevent:要访问的消息队列事件控制块的指针      
timeout:等待时限
perr:错误信息        
OS_ERR_NONE:调用成功,消息被正确地接受
OS_ERR_TIMEOUT :消息没有在指定数目的时钟周期内被接收
OS_ERR_PEND_ABOUT:取消队列等待
OS_ERR_EVENT_TYPE:没有传递一个指向队列的指针
OS_ERR_PEVENT_NULL:pevent是一个空指针
OS_ERR_PEND_ISR :从中断调用该函数,导致任务挂起
OS_ERR_PEND_LOCKED:调度器上锁了
函数返回值:

注意事项:
如果队列中有消息,该消息传递给任务,并从队列中清除该消息;如果队列中没有消息,则调用该函数的任务被挂起等待,直到有消息或者等待超时。当有多个任务请求到同一消息队列时,UCOSII进行任务调度,当前任务和所有请求该消息队列的任务中最高优先级的任务得到运行。
3. OSQPost函数
向消息队列发送消息函数,其声明如下:
INT8U  OSQPost (OS_EVENT  *pevent, void *pmsg)
函数描述:
用于向消息队列发送消息
函数形参:
pevent:消息队列指针                             
pmsg:消息指针
函数返回值:
OS_ERR_NONE:函数调用成功,成功发送消息到消息队列
OS_ERR_Q_FULL:队列已满
OS_ERR_EVENT_TYPE:pevent不是指向消息队列的指针
OS_ERR_PEVENT_NULL:pevent是一个空指针
信号量集函数
在这里对本实验用到的UCOSII信号量集函数进行介绍,相关代码存放在os_flag.c中。
1. OSFlagCreate函数
创建信号量集函数,其声明如下:
OS_FLAG_GRP  *OSFlagCreate (OS_FLAGS  flags, INT8U    *perr)
函数描述:
用于创建一个信号量集
函数形参:
flags:信号的初始值                                
perr:错误信息
OS_ERR_NONE:函数调用成功
OS_ERR_CREATE_ISR:在中断中创建信号量集
OS_ERR_FLAG_GRP_DEPLETED:没有更多的事件标志组
函数返回值:
这个信号量集的标志组的指针,应用程序可以用这个指针对信号量集进行相对应的操作。
2. OSFlagPend函数
请求信号量集函数,其声明如下:
OS_FLAGS  OSFlagPend (OS_FLAG_GRP  *pgrp,
                          OS_FLAGS      flags,
                          INT8U         wait_type,
                          INT32U        timeout,
                          INT8U        *perr)
函数描述:
用于请求一个信号量集
函数形参:
pgrp:消息邮箱的指针           
flags :消息指针
wait_type:逻辑运算类型                 
        OS_FLAG_WAIT_CLR_ALL:等待“mask”中的所有位被清除
        OS_FLAG_WAIT_SET_ALL:等待“mask”中的所有位被设置
        OS_FLAG_WAIT_CLR_ANY:等待“mask”中的任何位被清除
        OS_FLAG_WAIT_SET_ANY:等待“mask”中的任何位被设置
timeout:等待时延
perr:错误信息
        OS_ERR_NONE:指定比特数已经被设置“超时”
        OS_ERR_PEND_ISR:从中断挂起
        OS_ERR_FLAG_INVALID_PGRP:pgrp是一个空指针
        OS_ERR_TIMEOUT:指定比特数未设置“超时”
        OS_ERR_PEND_ABORT:取消标志等待
        OS_ERR_FLAG_WAIT_TYPE:没有指定正确的“wait_type”参数
函数返回值:
返回标志组成员OSFlagFlags的值
3. OSFlagPost函数
向信号量集发送信号函数,其声明如下:
OS_FLAGS  OSFlagPost (OS_FLAG_GRP  *pgrp,
                          OS_FLAGS      flags,
                          INT8U         opt,
                          INT8U        *perr)
函数描述:
用于向信号量集发送信号
函数形参:
pgrp:信号量集指针                                               
flags:选择所要发送的信号
opt:信号有效的选项                                                        
perr:错误信息
        OS_ERR_NONE:函数调用成功
        OS_ERR_FLAG_INVALID_PGRP:pgrp是一个空指针
        OS_ERR_EVENT_TYPE:没有指向时间标志组
        OS_ERR_FLAG_INVALID_OPT:使用无效选项
函数返回值:
返回标志组成员OSFlagFlags的值
注意事项:
所谓任务向信号量集发信号,就是对信号量集标志组中的信号进行置“1”(置位)或者置“0”(复位)的操作。至于对信号量几种的哪些信号进行操作,用函数中的参数flags来指定;对指定的信号是置“1”还是置“0”,用函数中的参数opt来指定(opt = OS_FLAG_SET为置“1”操作;opt = OS_FLAG_CLR为置“0”操作)。
软件定时器函数
在这里对本实验用到的UCOSII软件定时器函数进行介绍,相关代码存放在os_tmr.c中。
1. OSTmrCreate函数
创建软件定时器函数,其声明如下:
OS_TMR  *OSTmrCreate (INT32U           dly,
                          INT32U           period,
                          INT8U            opt,
                          OS_TMR_CALLBACK  callback,
                          void            *callback_arg,
                         INT8U           *pname,
                         INT8U           *perr)
函数描述:
用于创建软件定时器
函数形参:
dly:用于初始化定时时间                    
period:软件定时器的周期溢出时间
opt:设置软件定时器工作模式
OS_TMR_OPT_ONE_SHOT:计时器只倒计时一次
OS_TMR_OPT_PERIODIC:定时器倒计时,然后重新加载
callback_arg:回调函数的参数
pname:软件定时器的名字                  
perr:错误信息
OS_ERR_NONE:函数调用成功
OS_ERR_TMR_INVALID_DLY:执行无效延时操作
OS_ERR_TMR_INVALID_PERIOD:执行操作时输入了不合法的时间
OS_ERR_TMR_INVALID_OPT:使用无效选项
OS_ERR_TMR_ISR:在中断中创建
OS_ERR_TMR_NON_AVAIL:定时器池中没有空闲的定时器
函数返回值:
返回定时器控制块
注意事项:
软件定时器的回调函数有固定格式,我们必须按照这个格式编写,软件定时器的回调函数格式为:void (*OS_TMR_CALLBACK)(void *ptmr, void *parg)。其中,函数名我们可以自己随意设置,而ptmr这个参数,软件定时器用来传递当前定时器的控制块指针,所以我们一般设置其类型为OS_TMR*类型,第二个参数parg为回调函数的参数,这个就可以根据自己需要设置了,你也可以不用,但是必须有这个参数。
2. OSTmrStart函数
开启软件定时器函数,其声明如下:
BOOLEAN  OSTmrStart (OS_TMR   *ptmr, INT8U  *perr)
函数描述:
用于开启某个软件定时器
函数形参:
ptmr:要开启的软件定时器指针            
perr:错误信息
        OS_ERR_NONE:函数调用成功
        OS_ERR_TMR_INVALID:定时器无效
        OS_ERR_TMR_INVALID_TYPE:ptmr不是指向OS_TMR
        OS_ERR_TMR_ISR:从中断中创建
        OS_ERR_TMR_INACTIVE:定时器没有被创建
OS_ERR_TMR_INVALID_STATE:定时器处于无效状态
函数返回值:
返回布尔值 OS_TRUE:软件定时器开启    OS_FALSE:错误发生
3. OSTmrStop函数
停止软件定时器函数,其声明如下:
BOOLEAN  OSTmrStop (OS_TMR  *ptmr,
                       INT8U    opt,
                       void    *callback_arg,
                       INT8U   *perr)
函数描述:
用于停止某个软件定时器
函数形参:
ptmr:要停止的软件定时器指针
opt:停止选项
OS_TMR_OPT_NONE,直接停止,不做任何其他处理
OS_TMR_OPT_CALLBACK,停止,用初始化的参数执行一次回调函数
OS_TMR_OPT_CALLBACK_ARG,停止,用新的参数执行一次回调函数
callback_arg:新的回调函数参数
perr:错误信息
        OS_ERR_NONE:函数调用成功
        OS_ERR_TMR_INVALID:定时器无效
        OS_ERR_TMR_INVALID_TYPE:ptmr不是指向OS_TMR
        OS_ERR_TMR_ISR:从中断中创建
        OS_ERR_TMR_INACTIVE:定时器没有被创建
        OS_ERR_TMR_INVALID_OPT:操作时使用无效选项
        OS_ERR_TMR_STOPPED:定时器已经停止
        OS_ERR_TMR_INVALID_STATE:定时器处于无效状态
        OS_ERR_TMR_NO_CALLBACK:定时器没有定义回调函数
函数返回值:
返回布尔值 OS_TRUE:软件定时器停止    OS_FALSE:软件定时器停止失败
63.3.2 程序流程图
393FA667-8307-4936-9287-3E6278387F0A.png
图63.3.2.1 UCOSII消息队列、信号量集和软件定时器实验

63.3.3 程序解析
main.c代码
在main.c文件下,除了main函数之外,还有UCOSII任务的一些配置以及7个任务函数。我们先看一下UCOSII任务的一些宏定义,如下代码所示:
/*****************************************************************************/
/* UCOSII任务设置 */

/* START 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define START_TASK_PRIO                 10           /* 开始任务的优先级设置为最低 */
#define START_STK_SIZE                  128          /* 堆栈大小 */
OS_STK START_TASK_STK[START_STK_SIZE];             /* 任务堆栈 */
void start_task(void *pdata);                        /* 任务函数 */

/* 触摸屏任务 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define TOUCH_TASK_PRIO                 7            /* 优先级设置(越小优先级越高) */
#define TOUCH_STK_SIZE                  128           /* 堆栈大小 */
OS_STK TOUCH_TASK_STK[TOUCH_STK_SIZE];            /* 任务堆栈 */
void touch_task(void *pdata);                       /* 任务函数 */

/* LED 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define LED_TASK_PRIO                   6            /* 优先级设置(越小优先级越高) */
#define LED_STK_SIZE                    128          /* 堆栈大小 */
OS_STK LED_TASK_STK[LED_STK_SIZE];                 /* 任务堆栈 */
void led_task(void *pdata);                         /* 任务函数 */

/* 队列消息显示 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define QMSGSHOW_TASK_PRIO              5               /* 优先级设置(越小优先级越高) */
#define QMSGSHOW_STK_SIZE               128             /* 堆栈大小 */
OS_STK QMSGSHOW_TASK_STK[QMSGSHOW_STK_SIZE];         /* 任务堆栈 */
void qmsgshow_task(void *pdata);                       /* 任务函数 */

/* 主 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define MAIN_TASK_PRIO                  4               /* 优先级设置(越小优先级越高) */
#define MAIN_STK_SIZE                   128             /* 堆栈大小 */
OS_STK MAIN_TASK_STK[MAIN_STK_SIZE];                 /* 任务堆栈 */
void main_task(void *pdata);                          /* 任务函数 */

/* 信号量集 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define FLAGS_TASK_PRIO                 3               /* 优先级设置(越小优先级越高) */
#define FLAGS_STK_SIZE                  128             /* 堆栈大小 */
OS_STK FLAGS_TASK_STK[FLAGS_STK_SIZE];               /* 任务堆栈 */
void flags_task(void *pdata);                          /* 任务函数 */

/* 按键扫描 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define KEY_TASK_PRIO                   2               /* 优先级设置(越小优先级越高) */
#define KEY_STK_SIZE                    128             /* 堆栈大小 */
OS_STK KEY_TASK_STK[KEY_STK_SIZE];                   /* 任务堆栈 */
void key_task(void *pdata);                           /* 任务函数 */
/*****************************************************************************/
/**
* @brief       开始任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void start_task(void *pdata)
{
    OS_CPU_SR cpu_sr = 0;
    uint8_t err;
    pdata = pdata;
    msg_key = OSMboxCreate((void *)0);         /* 创建消息邮箱 */
    q_msg = OSQCreate(&MsgGrp[0], 256);        /* 创建消息队列 */
    flags_key = OSFlagCreate(0, &err);         /* 创建信号量集 */
    OSStatInit();                                  /* 开启统计任务 */
    OS_ENTER_CRITICAL();                          /* 进入临界区(关闭中断) */
   
    /* LED任务 */
    OSTaskCreateExt((void(*)(void *) )led_task,
                    (void *          )0,
                    (OS_STK *        )&LED_TASK_STK[LED_STK_SIZE - 1],
                    (INT8U          )LED_TASK_PRIO,
                    (INT16U         )LED_TASK_PRIO,
                    (OS_STK *        )&LED_TASK_STK[0],
                    (INT32U         )LED_STK_SIZE,
                    (void *          )0,
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR |  OS_TASK_OPT_SAVE_FP);
    /* 触摸任务 */
    OSTaskCreateExt((void(*)(void *) )touch_task,
                    (void *          )0,
                    (OS_STK *        )&TOUCH_TASK_STK[TOUCH_STK_SIZE - 1],
                    (INT8U          )TOUCH_TASK_PRIO,
                    (INT16U         )TOUCH_TASK_PRIO,
                    (OS_STK *        )&TOUCH_TASK_STK[0],
                    (INT32U         )TOUCH_STK_SIZE,
                    (void *          )0,
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR |  OS_TASK_OPT_SAVE_FP);
    /* 消息队列显示任务 */
    OSTaskCreateExt((void(*)(void *) )qmsgshow_task,
                    (void *          )0,
                    (OS_STK *        )&QMSGSHOW_TASK_STK[QMSGSHOW_STK_SIZE - 1],
                    (INT8U          )QMSGSHOW_TASK_PRIO,
                    (INT16U         )QMSGSHOW_TASK_PRIO,
                    (OS_STK *        )&QMSGSHOW_TASK_STK[0],
                    (INT32U         )QMSGSHOW_STK_SIZE,
                    (void *          )0,
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR |  OS_TASK_OPT_SAVE_FP);
    /* 主任务 */
    OSTaskCreateExt((void(*)(void *) )main_task,
                    (void *          )0,
                    (OS_STK *        )&MAIN_TASK_STK[MAIN_STK_SIZE - 1],
                    (INT8U          )MAIN_TASK_PRIO,
                    (INT16U         )MAIN_TASK_PRIO,
                    (OS_STK *        )&MAIN_TASK_STK[0],
                    (INT32U         )MAIN_STK_SIZE,
                    (void *          )0,
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR |  OS_TASK_OPT_SAVE_FP);
    /* 信号量集任务 */
    OSTaskCreateExt((void(*)(void *) )flags_task,
                    (void *          )0,
                    (OS_STK *        )&FLAGS_TASK_STK[FLAGS_STK_SIZE - 1],
                    (INT8U          )FLAGS_TASK_PRIO,
                    (INT16U         )FLAGS_TASK_PRIO,
                    (OS_STK *        )&FLAGS_TASK_STK[0],
                    (INT32U         )FLAGS_STK_SIZE,
                    (void *          )0,
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR |  OS_TASK_OPT_SAVE_FP);
    /* 按键任务 */
    OSTaskCreateExt((void(*)(void *) )key_task,
                    (void *          )0,
                    (OS_STK *        )&KEY_TASK_STK[KEY_STK_SIZE - 1],
                    (INT8U          )KEY_TASK_PRIO,
                    (INT16U         )KEY_TASK_PRIO,
                    (OS_STK *        )&KEY_TASK_STK[0],
                    (INT32U         )KEY_STK_SIZE,
                    (void *          )0,
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR |  OS_TASK_OPT_SAVE_FP);
    OS_EXIT_CRITICAL();                     /* 退出临界区(开中断) */
    OSTaskSuspend(START_TASK_PRIO);        /* 挂起开始任务 */
}

/**
* @brief       LED任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void led_task(void *pdata)
{
    uint8_t t;

    while (1)
    {
        t++;
        delay_ms(10);
        if (t == 8)LED0(1);         /* LED0灭 */
        if (t == 100)               /* LED0亮 */
        {
            t = 0;
            LED0(0);
        }
    }
}

/**
* @brief       触摸屏任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void touch_task(void *pdata)
{
    uint32_t cpu_sr;
    uint16_t lastpos[2];    /* 最后一次的数据 */

    while (1)
    {
        tp_dev.scan(0);

        if (tp_dev.sta & TP_PRES_DOWN)  /* 触摸屏被按下 */
        {
          if(tp_dev.x[0]<(130-1)&&tp_dev.y[0]<lcddev.height&&tp_dev.y[0]>(220+1))
            {
                if (lastpos[0] == 0XFFFF)
                {
                    lastpos[0] = tp_dev.x[0];
                    lastpos[1] = tp_dev.y[0];
                }
                OS_ENTER_CRITICAL();/*进入临界段,防止其他任务,打断LCD操作,导致液晶乱序*/
                lcd_draw_bline(lastpos[0], lastpos[1], tp_dev.x[0], tp_dev.y[0],
2,RED); /*画线*/
                OS_EXIT_CRITICAL();
                lastpos[0] = tp_dev.x[0];
                lastpos[1] = tp_dev.y[0];
            }
        }
        else
        {
            lastpos[0] = 0XFFFF;
            delay_ms(10);   /* 没有按键按下的时候 */
        }
    }
}

/**
* @brief       队列消息显示任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void qmsgshow_task(void *pdata)
{
    char *p;
    uint8_t err;

    while (1)
    {
        p = OSQPend(q_msg, 0, &err); /* 请求消息队列 */
        lcd_show_string(5, 170, 240, 16, 16, p, RED); /* 显示消息 */
        myfree(SRAMIN, p);
        delay_ms(500);
    }
}

/**
* @brief       主任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void main_task(void *pdata)
{
    uint32_t key = 0;
    uint8_t err;
    uint8_t tmr2sta = 1;            /* 软件定时器2开关状态 */
    uint8_t tmr3sta = 0;            /* 软件定时器3开关状态 */
    uint8_t flagsclrt = 0;          /* 信号量集显示清零倒计时 */
    tmr1 = OSTmrCreate(10, 10, OS_TMR_OPT_PERIODIC, (OS_TMR_CALLBACK)tmr1_callback, 0, "tmr1", &err); /* 100ms执行一次 */
    tmr2 = OSTmrCreate(10, 20, OS_TMR_OPT_PERIODIC, (OS_TMR_CALLBACK)tmr2_callback, 0, "tmr2", &err); /* 200ms执行一次 */
    tmr3 = OSTmrCreate(10, 10, OS_TMR_OPT_PERIODIC, (OS_TMR_CALLBACK)tmr3_callback, 0, "tmr3", &err); /* 100ms执行一次 */
    OSTmrStart(tmr1, &err); /* 启动软件定时器1 */
    OSTmrStart(tmr2, &err); /* 启动软件定时器2 */

    while (1)
    {
        key = (uint32_t)OSMboxPend(msg_key, 10, &err);
        if (key)
        {
            flagsclrt = 51; /* 500ms后清除 */
           OSFlagPost(flags_key,1<<(key-1),OS_FLAG_SET,&err);/*设置对应的信号量为1*/
        }
        if (flagsclrt)   /* 倒计时 */
        {
            flagsclrt--;
            if (flagsclrt==1)lcd_fill(140, 162, 239, 162 + 16, WHITE); /*清除显示*/
        }

        switch (key)
        {
            case KEY0_PRES:/* 软件定时器2 开关,并清屏 */
                tmr2sta = !tmr2sta;
                if (tmr2sta)
                {
                    OSTmrStart(tmr2, &err); /* 开启软件定时器2 */
                }
                else
                {
                    OSTmrStop(tmr2, OS_TMR_OPT_NONE, 0, &err);   /*关闭软件定时器2*/
                    lcd_show_string(148, 262, 240, 16, 16, "TMR2 STOP", RED);   
                }
                lcd_fill(0, 221, 129, lcddev.height - 1, WHITE); /* 顺便清屏 */
                break;

            case KEY1_PRES:/* 控制软件定时器3 */
                tmr3sta = !tmr3sta;
                if (tmr3sta)
                {
                    OSTmrStart(tmr3, &err);
                }
                else
                {
                    OSTmrStop(tmr3, OS_TMR_OPT_NONE, 0, &err);  /*关闭软件定时器3*/
                }
                break;

            case WKUP_PRES:/* 校准 */
                OSTaskSuspend(TOUCH_TASK_PRIO);                    /* 挂起触摸屏任务 */
                OSTaskSuspend(QMSGSHOW_TASK_PRIO);                /* 挂起队列信息显示任务 */
                OSTmrStop(tmr1, OS_TMR_OPT_NONE, 0, &err);/* 关闭软件定时器1 */

               if(tmr2sta)OSTmrStop(tmr2,OS_TMR_OPT_NONE,0,&err);/*关闭软件定时器2*/

                if ((tp_dev.touchtype & 0X80) == 0)
                {
                    tp_adjust();
                }
               
                OSTmrStart(tmr1, &err);                         /* 重新开启软件定时器1 */
                if (tmr2sta)OSTmrStart(tmr2, &err);          /* 重新开启软件定时器2 */
                OSTaskResume(TOUCH_TASK_PRIO);                /* 解挂 */
                OSTaskResume(QMSGSHOW_TASK_PRIO);            /* 解挂 */
                ucos_load_main_ui();                           /* 重新加载主界面 */
                break;
        }
        delay_ms(10);
    }
}

/**
* @brief       信号量集处理任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void flags_task(void *pdata)
{
    uint16_t flags;
    uint8_t err;

    while (1)
    {
     flags=OSFlagPend(flags_key,0X0007,OS_FLAG_WAIT_SET_ANY,0,&err);/*等待信号量*/

        if (flags&0X0001)lcd_show_string(140,162,240,16,16,"KEY0 DOWN  ",RED);
        if (flags&0X0002)lcd_show_string(140,162,240,16,16,"KEY1 DOWN  ",RED);
        if (flags&0X0004)lcd_show_string(140,162,240,16,16,"KEY_UP DOWN  ",RED);

        LED1(0);
        delay_ms(50);
        LED1(1);
        OSFlagPost(flags_key, 0X0007, OS_FLAG_CLR, &err); /* 全部信号量清零 */
    }
}

/**
* @brief       按键扫描任务
* @param       pdata : 传入参数(未用到)
* @retval      无
*/
void key_task(void *pdata)
{
    uint8_t key;
    while (1)
    {
        key = key_scan(0);
        if (key)OSMboxPost(msg_key, (void *)key); /* 发送消息 */
        delay_ms(10);
    }
}

/**
* @brief       软件定时器1的回调函数
*   @note      每100ms执行一次,用于显示CPU使用率和内存使用率
* @param       ptmr : 软件定时器指针
* @param       p_arg: 参数指针(未用到)
* @retval      无
*/
void tmr1_callback(OS_TMR *ptmr, void *p_arg)
{
    static uint16_t cpuusage = 0;
    static uint8_t tcnt = 0;
    if (tcnt == 5)
    {
        lcd_show_xnum(202, 10, cpuusage / 5, 3, 16, 0, BLUE); /* 显示CPU使用率 */
        cpuusage = 0;
        tcnt = 0;
    }

    cpuusage += OSCPUUsage;
    tcnt++;
lcd_show_xnum(202,30,my_mem_perused(SRAMIN)/10,3,16,0,BLUE);/*显示内存使用率*/
lcd_show_xnum(202,50, ((OS_Q *)(q_msg->OSEventPtr))->OSQEntries,
3, 16, 0X80, BLUE); /* 显示队列当前的大小 */
}

/**
* @brief       软件定时器2的回调函数
*   @note      每200ms执行一次
* @param       ptmr : 软件定时器指针
* @param       p_arg: 参数指针(未用到)
* @retval      无
*/
void tmr2_callback(OS_TMR *ptmr, void *p_arg)
{
    static uint8_t sta = 0;
    switch (sta)
    {
        case 0:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, RED);
            break;
        case 1:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, GREEN);
            break;
        case 2:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, BLUE);
            break;
        case 3:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, MAGENTA);
            break;
        case 4:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, CYAN);
            break;
        case 5:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, YELLOW);
            break;
        case 6:
            lcd_fill(131, 221, lcddev.width - 1, lcddev.height - 1, BRRED);
            break;
    }
    sta++;
    if (sta > 6)sta = 0;
}

/**
* @brief       软件定时器3的回调函数
*   @note      每300ms执行一次
* @param       ptmr : 软件定时器指针
* @param       p_arg: 参数指针(未用到)
* @retval      无
*/
void tmr3_callback(OS_TMR *ptmr, void *p_arg)
{
    uint8_t *p;
    uint8_t err;
    static uint8_t msg_cnt = 0;        /* msg编号 */
    p = mymalloc(SRAMIN, 13);           /* 申请13个字节的内存 */
    if (p)
    {
        sprintf((char *)p, "ALIENTEK %03d", msg_cnt);
        msg_cnt++;
        err = OSQPost(q_msg, p);        /* 发送队列 */
        if (err != OS_ERR_NONE)         /* 发送失败 */
        {
            myfree(SRAMIN, p);          /* 释放内存 */
            OSTmrStop(tmr3, OS_TMR_OPT_NONE, 0, &err);  /* 关闭软件定时器3 */
        }
    }
}
上面就是对创建的start_task、led_task、touch_task、qmsgshow_task、main_task 、flags_task和 key_task等7个任务的参数进行配置,例如优先级、堆栈大小和任务函数的声明及定义。此外还有3个软件定时器及其回调函数。
软件定时器tmr1、tmr2和tmr3,tmr1用于显示CPU使用率和内存使用率,每100ms执行一次;tmr2用于在LCD的右下角区域不停的显示各种颜色,每200 ms执行一次;tmr3用于定时向队列发送消息(用到了对台内存申请),每100ms发送一次。
在本实验中,我们还是使用消息邮箱msg_key在按键任务和主任务之间传递键值数据,我们创建信号量集flags_key,在主任务里面将按键键值通过信号量集传递给信号量集处理任务flags_task,实现按键信息的显示以及LED1的提示性闪灯。
此外我们还创建了一个大小为256的消息队列q_msg,通过软件定时器tmr3的回调函数向消息队列发送消息,然后在消息队列显示任务qmsgshow_task里面请求消息队列,并在LCD上面显示得到的消息。消息队列还用到了动态内存管理。
在主任务main_task里面,我们实现了前面介绍的功能:KEY0控制软件定时器2的开关,间接控制消息队列的发送;KEY1控制软件定时器3的开关,同时清除LCD触摸屏区域的数据;WK_UP用于触摸屏校准,在校准的时候,要先挂起触摸屏任务、队列消息显示任务,并停止软件定时器tmr1和tmr2,否则可能对校准时的LCD显示造成干扰;
其他任务做的事情在前面程序流程图里面已经有说明,这里就不多说了。
下面看一下main主函数的代码:
int main(void)
{
    sys_cache_enable();                    /* 打开L1-Cache */
    HAL_Init();                                     /* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4);        /* 设置时钟, 480Mhz */
    delay_init(480);                               /* 延时初始化 */
    usart_init(115200);                           /* 串口初始化为115200 */
    mpu_memory_protection();                     /* 保护相关存储区域 */
    lcd_init();                                     /* 初始化LCD */
    led_init();                                     /* 初始化LED */
    key_init();                                     /* 初始化按键 */   
    beep_init();                                    /* 初始化蜂鸣器 */
    tp_dev.init();                                 /* 触摸屏初始化 */
    ucos_load_main_ui();                          /* 加载主界面 */
    my_mem_init(SRAMIN);                          /* 初始化内部内存池 */

    OSInit();                                           /* UCOS初始化 */
    OSTaskCreateExt((void(*)(void *) )start_task,        /* 任务函数 */
                    (void *          )0,                        /* 传递给任务函数的参数 */
/* 任务堆栈栈顶 */
                    (OS_STK *        )&START_TASK_STK[START_STK_SIZE - 1],
                    (INT8U          )START_TASK_PRIO,                /* 任务优先级 */
                    (INT16U         )START_TASK_PRIO,/* 任务ID,这里设置为和优先级一样 */
                    (OS_STK *        )&START_TASK_STK[0],        /* 任务堆栈栈底 */
                    (INT32U         )START_STK_SIZE,                /* 任务堆栈大小 */
                    (void *          )0,                              /* 用户补充的存储区 */
                    (INT16U         )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR
| OS_TASK_OPT_SAVE_FP);
/* 任务选项,为了保险起见,所有任务都保存浮点寄存器的值 */
    OSStart();    /* 开始任务 */
}
这一章的main函数与上一章节的相差不大,可以参考流程图去理解,这里就不作展开了。
63.4 下载验证
将程序下载到开发板后,可以看到LCD显示界面如图63.4.1所示:
第六十三章 UCOSII实验328555.png
图63.4.1 初始化界面

从图中可以看出,默认状态下,CPU使用率为4%左右。比上一章多出一些,这主要是key_task里面增加不停的刷屏(tmr2)操作导致的。
通过按KEY0,可以启动/停止tmr2,从而控制屏幕的刷新,同时清除LCD触摸屏区域数据。
通过按KEY1则可以启动tmr3控制消息队列发送,可以在LCD上面看到Q和MEM的值慢慢变大(说明队列消息在增多,占用内存也随着消息增多而增大),在QUEUE MSG区,开始显示队列消息,再按一次KEY1停止tmr3,此时可以看到Q和MEM逐渐减小。当Q值变为0的时候,QUEUE MSG也停止显示(队列为空)。
通过KEY_UP按键,可以进行触摸屏校准(仅电阻屏,电容屏无需校准)。
在TOUCH区域,可以输入手写内容。
任何按键按下,蜂鸣器都会发出“滴”的一声,提示按键被按下,同时在FLAGS区域显示按键信息。

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-24 10:31

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

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