完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
接下来,陆陆续续会将语雀的个人笔记搬移到csdn上。
该篇文档过长,建议目录查找关键位置。 UcosII是剥夺内核(每个系统节拍会切换到最高就绪优先级任务),系统运行取决于任务的优先级。任务优先级的关系:优先级越小,数值越大;优先级越大,数值越小。 移植 1、到ucos官方下载相应mcu型号的源码文件。 2、得到 EvalBoards 文件夹里面包含评估板相关文件,提取部分, APP.c就不需要了,钩子函数在 BSP.c文件,跟板子相关的函数可以删除了,特别注意时间戳所需要的函数实现要保留。 BSP_CPU_ClkFreq(); CPU_TS_TmrInit(); CPU_TS_TmrRd(); CPU_TS32_to_uSec(); 3、直接复制LIB跟CPU文件夹 4、在3个文件夹下删除不需要的编译器环境,RealView为Keil环境 5、修改编译器asm汇编文件 OS_CPU_A.ASM: keil环境下,查看函数定义前面是否是extern如果不是,改成extern。 这样修改的目的,是因为MDK编程环境不认识PUBLIC,要用EXPORT. 6、最重要的2个函数的连接: PendSV_Handler() 和 SysTick_Handler(); 在asm文件里面将 OS_CPU_PendSVHandler 更改为MCU启动文件中断向量表中的PendSV_Handler: 7、配置SysTick心跳 UcosII的心跳时间是由SysTick系统滴答来实现,具体心跳的间隔由配置文件OS_TICKS_PER_SEC宏来决定,关于时钟节拍的时间,如果中断时间越短,它的实时性越好,但是CPU做的无用功越多,同样,定时的时间越长,虽然CPU做的无用功越少,但是,它的实时性越差,根据CPU性能推荐10-100毫秒。 /********************************************************************* * 函数名称: Delay_init * 函数描述: 延时及ucosII心跳初始化 * 入 参: ucSysclk 系统时钟频率 * 出 参:无 * 返 回 值: 无 **********************************************************************/ void Delay_init(UINT8 ucSysclk) { #if SYSTEM_SUPPORT_OS //如果需要支持OS. UINT32 ulReload; #endif SysTick->CTRL&=~(1<<2); //SYSTICK使用外部时钟源 fac_us=ucSysclk/8; //不论是否使用OS,fac_us都需要使用 #if SYSTEM_SUPPORT_OS //如果需要支持OS. ulReload=ucSysclk/8; //每秒钟的计数次数 单位为M ulReload*=1000000/OS_DISPATCH_NUM; //根据delay_ostickspersec设定溢出时间 //reload为24位寄存器,最大值:16777216,在180M下,约合0.699s左右 fac_ms=1000/OS_DISPATCH_NUM; //代表OS可以延时的最少单位 SysTick->CTRL|=1<<1; //开启SYSTICK中断 SysTick->LOAD=ulReload; //每1/OS_DISPATCH_NUM秒中断一次 SysTick->CTRL|=1<<0; //开启SYSTICK #else fac_ms=((u32)SYSCLK*1000)/8; //非OS下,代表每个ms需要的systick时钟数 #endif } 8、修改SysTick中断 void SysTick_Handler(void) { if( OS_RUN == Valid ) { OSIntEnter(); //进入中断 OSTimeTick(); //调用ucos的时钟服务程序 OSIntExit(); //触发任务切换软中断 } } 配置文件 所有移植完成后,开始配置UcosII功能,配置文件为os_cfg.h: /* ---------------------- MISCELLANEOUS ----------------------- */ #define OS_APP_HOOKS_EN 0u /* Application-defined hooks are called from the uC/OS-II hooks 从uC/OS-II钩子调用应用程序定义的钩子*/ #define OS_ARG_CHK_EN 0u /* Enable (1) or Disable (0) argument checking 启用(1)或禁用(0)参数检查*/ #define OS_CPU_HOOKS_EN 1u /* uC/OS-II hooks are found in the processor port files uC/OS-II钩子可以在处理器端口文件中找到*/ #define OS_DEBUG_EN 0u /* Enable(1) debug variables 启用debug*/ #define OS_EVENT_MULTI_EN 0u /* Include code for OSEventPendMulti() 启用OSEventPendMulti()函数*/ #define OS_EVENT_NAME_EN 0u /* Enable names for Sem, Mutex, Mbox and Q 使能事件名称*/ #define OS_LOWEST_PRIO 63u /* Defines the lowest priority that can be assigned ... 最低优先级*/ /* ... MUST NEVER be higher than 254! */ #define OS_MAX_EVENTS 20u /* Max. number of event control blocks in your application 最大应用程序中事件控制块的数量*/ #define OS_MAX_FLAGS 5u /* Max. number of Event Flag Groups in your application 最大应用程序中的事件标志组数目*/ #define OS_MAX_MEM_PART 0u /* Max. number of memory partitions 最大内存分区数*/ #define OS_MAX_QS 5u /* Max. number of queue control blocks in your application 最大应用程序中队列控制块的数量*/ #define OS_MAX_TASKS 20u /* Max. number of tasks in your application, MUST be >= 2 最大应用程序中的任务数,必须是>= 2*/ #define OS_SCHED_LOCK_EN 1u /* Include code for OSSchedLock() and OSSchedUnlock() */ #define OS_TICK_STEP_EN 1u /* Enable tick stepping feature for uC/OS-View */ #define OS_TICKS_PER_SEC 200u /* Set the number of ticks in one second 设置一秒内的节拍数*/ #define OS_TLS_TBL_SIZE 0u /* Size of Thread-Local Storage Table 线程本地存储表的大小*/ /* --------------------- TASK STACK SIZE ---------------------- 任务堆栈大小*/ #define OS_TASK_TMR_STK_SIZE 128u /* Timer task stack size (# of OS_STK wide entries) 计时器任务栈大小(OS_STK范围条目的#)*/ #define OS_TASK_STAT_STK_SIZE 128u /* Statistics task stack size (# of OS_STK wide entries) 统计任务堆栈大小(OS_STK范围条目的#)*/ #define OS_TASK_IDLE_STK_SIZE 128u /* Idle task stack size (# of OS_STK wide entries) 空闲任务栈大小(OS_STK宽度条目的#)*/ /* --------------------- TASK MANAGEMENT ---------------------- 任务管理*/ #define OS_TASK_CHANGE_PRIO_EN 1u /* Include code for OSTaskChangePrio() */ #define OS_TASK_CREATE_EN 1u /* Include code for OSTaskCreate() */ #define OS_TASK_CREATE_EXT_EN 1u /* Include code for OSTaskCreateExt() */ #define OS_TASK_DEL_EN 1u /* Include code for OSTaskDel() */ #define OS_TASK_NAME_EN 1u /* Enable task names 启用任务名称*/ #define OS_TASK_PROFILE_EN 1u /* Include variables in OS_TCB for profiling 在OS_TCB中包含用于分析的变量*/ #define OS_TASK_QUERY_EN 1u /* Include code for OSTaskQuery() */ #define OS_TASK_REG_TBL_SIZE 1u /* Size of task variables array (#of INT32U entries) 任务变量数组的大小(INT32U项的#)*/ #define OS_TASK_STAT_EN 1u /* Enable (1) or Disable(0) the statistics task 启用(1)或禁用(0)统计任务*/ #define OS_TASK_STAT_STK_CHK_EN 1u /* Check task stacks from statistic task 从统计任务检查任务堆栈*/ #define OS_TASK_SUSPEND_EN 1u /* Include code for OSTaskSuspend() and OSTaskResume() */ #define OS_TASK_SW_HOOK_EN 1u /* Include code for OSTaskSwHook() */ /* ----------------------- EVENT FLAGS ------------------------ 事件标志*/ #define OS_FLAG_EN 1u /* Enable (1) or Disable (0) code generation for EVENT FLAGS 为事件标志启用(1)或禁用(0)代码生成*/ #define OS_FLAG_ACCEPT_EN 1u /* Include code for OSFlagAccept() */ #define OS_FLAG_DEL_EN 1u /* Include code for OSFlagDel() */ #define OS_FLAG_NAME_EN 1u /* Enable names for event flag group */ #define OS_FLAG_QUERY_EN 1u /* Include code for OSFlagQuery() */ #define OS_FLAG_WAIT_CLR_EN 1u /* Include code for Wait on Clear EVENT FLAGS 包括等待清除事件标志的代码*/ #define OS_FLAGS_NBITS 16u /* Size in #bits of OS_FLAGS data type (8, 16 or 32) OS_FLAGS数据类型的大小(8、16或32*/ /* -------------------- MESSAGE MAILBOXES --------------------- 消息邮箱*/ #define OS_MBOX_EN 1u /* Enable (1) or Disable (0) code generation for MAILBOXES 为邮箱启用(1)或禁用(0)代码生成*/ #define OS_MBOX_ACCEPT_EN 1u /* Include code for OSMboxAccept() */ #define OS_MBOX_DEL_EN 1u /* Include code for OSMboxDel() */ #define OS_MBOX_PEND_ABORT_EN 1u /* Include code for OSMboxPendAbort() */ #define OS_MBOX_POST_EN 1u /* Include code for OSMboxPost() */ #define OS_MBOX_POST_OPT_EN 1u /* Include code for OSMboxPostOpt() */ #define OS_MBOX_QUERY_EN 1u /* Include code for OSMboxQuery() */ /* --------------------- MEMORY MANAGEMENT -------------------- 内存管理*/ #define OS_MEM_EN 0u /* Enable (1) or Disable (0) code generation for MEMORY MANAGER 为内存管理器启用(1)或禁用(0)代码生成*/ #define OS_MEM_NAME_EN 1u /* Enable memory partition names 启用内存分区名称*/ #define OS_MEM_QUERY_EN 1u /* Include code for OSMemQuery() */ /* ---------------- MUTUAL EXCLUSION SEMAPHORES --------------- 互斥信号量*/ #define OS_MUTEX_EN 1u /* Enable (1) or Disable (0) code generation for MUTEX 启用(1)或禁用(0)互斥的代码生成*/ #define OS_MUTEX_ACCEPT_EN 1u /* Include code for OSMutexAccept() */ #define OS_MUTEX_DEL_EN 1u /* Include code for OSMutexDel() */ #define OS_MUTEX_QUERY_EN 1u /* Include code for OSMutexQuery() */ /* ---------------------- MESSAGE QUEUES ---------------------- 消息队列*/ #define OS_Q_EN 1u /* Enable (1) or Disable (0) code generation for QUEUES 为队列启用(1)或禁用(0)代码生成*/ #define OS_Q_ACCEPT_EN 1u /* Include code for OSQAccept() */ #define OS_Q_DEL_EN 1u /* Include code for OSQDel() */ #define OS_Q_FLUSH_EN 1u /* Include code for OSQFlush() */ #define OS_Q_PEND_ABORT_EN 1u /* Include code for OSQPendAbort() */ #define OS_Q_POST_EN 1u /* Include code for OSQPost() */ #define OS_Q_POST_FRONT_EN 1u /* Include code for OSQPostFront() */ #define OS_Q_POST_OPT_EN 1u /* Include code for OSQPostOpt() */ #define OS_Q_QUERY_EN 1u /* Include code for OSQQuery() */ /* ------------------------ SEMAPHORES ------------------------ 信号量*/ #define OS_SEM_EN 1u /* Enable (1) or Disable (0) code generation for SEMAPHORES 为信号量启用(1)或禁用(0)代码生成*/ #define OS_SEM_ACCEPT_EN 1u /* Include code for OSSemAccept() */ #define OS_SEM_DEL_EN 1u /* Include code for OSSemDel() */ #define OS_SEM_PEND_ABORT_EN 1u /* Include code for OSSemPendAbort() */ #define OS_SEM_QUERY_EN 1u /* Include code for OSSemQuery() */ #define OS_SEM_SET_EN 1u /* Include code for OSSemSet() */ /* --------------------- TIME MANAGEMENT ---------------------- 时间管理*/ #define OS_TIME_DLY_HMSM_EN 1u /* Include code for OSTimeDlyHMSM() */ #define OS_TIME_DLY_RESUME_EN 1u /* Include code for OSTimeDlyResume() */ #define OS_TIME_GET_SET_EN 1u /* Include code for OSTimeGet() and OSTimeSet() */ #define OS_TIME_TICK_HOOK_EN 1u /* Include code for OSTimeTickHook() */ /* --------------------- TIMER MANAGEMENT --------------------- 软件定时器*/ #define OS_TMR_EN 1u /* Enable (1) or Disable (0) code generation for TIMERS 启用(1)或禁用(0)软件定时器代码生成*/ #define OS_TMR_CFG_MAX 16u /* Maximum number of timers 最大定时器数*/ #define OS_TMR_CFG_NAME_EN 1u /* Determine timer names 确定计时器的名字*/ #define OS_TMR_CFG_WHEEL_SIZE 8u /* Size of timer wheel (#Spokes) 计时器轮尺寸(辐条数)*/ #define OS_TMR_CFG_TICKS_PER_SEC 100u /* Rate at which timer management task runs (Hz) 计时器管理任务运行的速率(Hz)*/ #define OS_TASK_TMR_PRIO 0u /* 软件定时器的优先级,设置为最高 */ 初始化 UcosII的初始化函数用于初始化配置,在os_cfg.h文件配置的功能会通过宏编译来选择性初始化。 |
|
|
|
核心调度函数
SysTick_Handler 函数用于根据时钟节拍来寻找延时完毕的任务,并将其设置成就绪状态,然后在就绪任务中获取最高优先级任务并调用OSIntExit函数来切换至该任务,在任何中断中,要用OSIntEnter()和OSIntExit()函数来表面进入和退出中断,OSIntExit函数会检查中断嵌套深度,并始终会在无中断嵌套后,才开始任务的调度。 OSIntEnter 这个函数用于通知uC/OS-II您将要为一个中断服务服务程序(ISR)。这允许uC/OS-II跟踪中断嵌套,从而只在最后一个嵌套ISR执行调度: void OSIntEnter (void) { if (OSRunning == OS_TRUE) { if (OSIntNesting < 255u) { OSIntNesting++; /* Increment ISR nesting level */ } } } OSTimeTick 函数通过while轮询TCB块任务有OSTCBDly延时的,发生1次心跳进行减1操作,并且判断延时是否到达,到达后会将相应的任务从新设置为就绪状态。 OSIntExit 通知uCOSII已经完成了对ISR的服务。当最后一个嵌套ISR已经完成,uC/OS-II将调用调度器来确定是否一个新的高优先级任务准备运行,最后OS_SchedNew(),查找当前最高优先级就绪任务,通过OSIntCtxSw中断级调度函数触发PendSV异常来进行调度: void OSIntExit (void) { #if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */ OS_CPU_SR cpu_sr = 0u; #endif if (OSRunning == OS_TRUE) { OS_ENTER_CRITICAL(); if (OSIntNesting > 0u) { /* Prevent OSIntNesting from wrapping */ OSIntNesting--; } if (OSIntNesting == 0u) { /* Reschedule only if all ISRs complete ... */ if (OSLockNesting == 0u) { /* ... and not locked. */ OS_SchedNew(); OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */ #if OS_TASK_PROFILE_EN > 0u OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */ #endif OSCtxSwCtr++; /* Keep track of the number of ctx switches */ #if OS_TASK_CREATE_EXT_EN > 0u #if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u) OS_TLS_TaskSw(); #endif #endif OSIntCtxSw(); /* Perform interrupt level ctx switch */ } } } OS_EXIT_CRITICAL(); } } OS_SchedNew 在UcosII中,为了节约CPU任务调度时间,不可能每次轮询查找所有任务那些就绪了,于是用了任务就绪组跟任务就绪位表,可以很方便的直接找出当前最高优先级的任务。 如当前有4个组有任务就绪,OSRdyGrp表示为0xF,将0xF代入OSUnMapTbl[OSRdyGrp]表,OSUnMapTbl数组内下标15的值为0,既找出了当前最高优先级的任务组,并赋值给y,再将y代入OSUnMapTbl[OSRdyTbl[y]]表中找出最高优先级任务的位位置(OSRdyTbl[y]里面保存了任务的位位置的偏移,创建任务确定的)最终得到OSPrioHighRdy(包含了任务在8*8的矩阵中的位置信息,高2位空,中3位为Y,低3位为X),合成最高优先级且已经就绪的任务横坐标和纵坐标。 OSRdyGrp就绪组信息和OSRdyTbl就绪位信息会在任务创建、等待完成、调度的时候填充进去,同样,也会在任务挂起、删除、等待、调度中将自己删除。 OSRdyGrp(1字节)表示任务就绪组,OSRdyGrp每一位代表1组,1组表示8个任务,Y是OSRdyGrp任务就绪组中的组位置,表示任务在0-8个组中那个组有任务就绪了,X是任务在OSRdyTbl[ptcb->OSTCBY]就绪位表中的位位置,表示任务在0-8位中那个位有任务就绪了,这个数组是按照就绪组序号存放的。 OSUnMapTbl[]:就绪表数组:数组中每一项是一个字节,第N项代表了第N个就绪组,字节数据代表了该优先级组中的优先级偏移(范围是从0~7,就是低三位),此时OSRdyGrp就绪组可能多个位被置位,表示可能多个优先级组有就绪任务; OSRdyTbl[]就绪表数组某个元素代表了某组中多个任务中的某个任务就绪,也是最低位0为最高优先级,OSRdyTbl这个要特别注意的是该变量为数组,参数是通过任务的优先级右移的值来决定下标位置,其实也就是按8个任务分为1组。即:OSRdyTbl[0]装的任务0-7,OSRdyTbl[1]装的任务8-15。 OSUnMapTbl[]表将2的8次方种可能全部算好了,为了节约时间,所以用查表的方式进行高就绪优先级任务的选择: INT8U const OSUnMapTbl[256] = { 0u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x00 to 0x0F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x10 to 0x1F */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x20 to 0x2F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x30 to 0x3F */ 6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x40 to 0x4F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x50 to 0x5F */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x60 to 0x6F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x70 to 0x7F */ 7u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x80 to 0x8F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x90 to 0x9F */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xA0 to 0xAF */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xB0 to 0xBF */ 6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xC0 to 0xCF */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xD0 to 0xDF */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xE0 to 0xEF */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u /* 0xF0 to 0xFF */ }; 如prio优先级为12的任务: ptcb->OSTCBY = (INT8U)(prio >> 3u); //Y 01 ptcb->OSTCBX = (INT8U)(prio & 0x07u); //X 04 ptcb->OSTCBBitY = (OS_PRIO)(1uL << ptcb->OSTCBY); //TCB_Y 02 ptcb->OSTCBBitX = (OS_PRIO)(1uL << ptcb->OSTCBX); //TCB_X 10 OSRdyGrp |= ptcb->OSTCBBitY; //02 OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; //10 代入查表: y=OSUnMapTbl[OSRdyGrp]; //OSRdyGrp值为02,OSUnMapTbl表中的02值为01,所以y值为01 x=OSUnMapTbl[OSRdyTbl[y]]); //OSRdyTbl[y]为OSRdyTbl[1],这里面存的是ptcb->OSTCBBitX的值0x10,OSUnMapTbl表中0x10的值为04。所以x等于04 任务优先级OSPrioHighRdy的组成为:1个字节,高2为为0,因为只有64个,中间3位为Y,低3位为X。 最后 OSPrioHighRdy = y <<3 + x; //OSPrioHighRdy = 0x0C。 得到的Y就是第几行,X就是第几位,任务优先级12的任务在第1行第4个。从0开始数。 找到最高优先级的就绪任务后,通过OSTCBPrioTbl[OSPrioHighRdy]会将相应的优先级任务的TCB块指针赋值给OSTCBHighRdy,然后调用OSIntCtxSw。 |
|
|
|
OSIntCtxSw
该函数为汇编函数,用于在中断结束时进行任务调度的函数,触发PendSV异常 ;******************************************************************************************************** ; PERFORM A CONTEXT SWITCH (From interrupt level) - OSIntCtxSw() ; ; Note(s) : 1)当OSIntExit()确定需要进行上下文切换时,OSIntCtxSw()被OSIntExit()调用 ;中断的结果。这个函数只会触发一个PendSV异常 ;当不再有活动中断且中断已启用时处理。 ;******************************************************************************************************** OSIntCtxSw LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch) LDR R1, =NVIC_PENDSVSET STR R1, [R0] BX LR PendSV_Handler PendSV是可悬挂任务,当有任务触发PendSV异常后,如果此时被更高异常中断打断,则PendSV异常挂起,当所有高于PendSV的中断执行完毕后,PendSV才执行。 PendSV异常主要用来执行上下文切换,保存当前任务的堆栈,加载即将执行任务的堆栈至系统寄存器,然后跳转至要执行任务的函数中,一般从加载的任务堆栈中将LR赋值给PC。 PendSV_Handler CPSID I ; Prevent interruption during context switch 防止上下文切换期间的中断 MRS R0, PSP ; PSP is process stack pointer PSP是进程栈指针 CBZ R0, PendSV_Handler_nosave ; Skip register save the first time 第一次跳过寄存器保存 SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack 将其余的regs r4-11保存在进程堆栈上 STM R0, {R4-R11} LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP; LDR R1, [R1] STR R0, [R1] ; R0 is SP of process being switched out R0是被切换的进程的SP ; At this point, entire context of process has been saved 此时,进程的整个上下文已经保存 PendSV_Handler_nosave PUSH {R14} ; Save LR exc_return value 保存LR exc_return值 LDR R0, =OSTaskSwHook ; OSTaskSwHook(); 调用钩子函数 BLX R0 POP {R14} LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy; 赋值最高优先级任务 LDR R1, =OSPrioHighRdy LDRB R2, [R1] STRB R2, [R0] LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy; 赋值最高优先级任务的TCB块指针 LDR R1, =OSTCBHighRdy LDR R2, [R1] STR R2, [R0] LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;从新任务中将栈顶赋值给SP LDM R0, {R4-R11} ; Restore r4-11 from new process stack 从新的进程堆栈恢复r4-11 ADDS R0, R0, #0x20 MSR PSP, R0 ; Load PSP with new process SP 加载新进程SP的PSP ORR LR, LR, #0xF4 ; Ensure exception return uses process stack 确保异常返回使用进程堆栈 CPSIE I BX LR ; Exception return will restore remaining context 异常返回将恢复剩余的上下文 END 该汇编函数主要完成: 1、将CPU的通用寄存器保存压入当前任务堆栈(每一个任务都有自己的堆栈,含SP变量等)。 2、将堆栈指针SP赋值给当前任务控制块中的OSTCBStkPtr中。 3、调用钩子函数(如果有)。 4、获得最高优先级的任务块,OSPrioCur = OSPrioHighRdy; OSTCBCur(当前TCB运行指针) = OSTCBHighRdy; 5、将新任务块中的OSTCBStkPtr(PSP栈顶指针)赋值给 SP; 6、将新的任务块中的R4-R11值出栈给CPU通用寄存器; 7、中断返回IRET(RETI),使PC指向当前TCB运行指针,完成调度。 OSTimeDly UcOSII的任务调度除了SysTick,还用OSTimeDly延时函数来进行调度。 该函数进入后首先判读有无中断嵌套和加锁,此函数首先将自己的就绪组及就绪位状态清除,目的是让自身任务挂起,并且将延时的时间放进当前任务的OSTCBDly里面(延时的时间通过SysTick心跳来进行减操作),然后进入**OS_Sched()**函数。 OS_Sched 该函数也会检查有无中断嵌套和加锁,然后调用OS_SchedNew函数来寻找当前最高优先级的就绪任务,然后将找到的高优先级TCB指针赋值给OSTCBHighRdy,然后判断是不是当前正在运行的,如果不是,将调用OS_TASK_SW宏,来调度任务。 OS_TASK_SW 该宏实际是调用了OSCtxSw汇编函数,该汇编函数跟OSIntCtxSw内容一致,都是用于触发PengSV异常(任务A切换到B时需要保存cpu的”现场数据“使系统能够返回A;同时需要加载B的”现场数据“到cpu中,以使B回到被切换时的状态。而只有cpu的特权模式允许上述操作,由用户级进入特权级的惟一途径是触发异常,包括中断) OSCtxSw LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch) LDR R1, =NVIC_PENDSVSET STR R1, [R0] BX LR OSStartHighRdy 该汇编函数用于UcosII启动时调用: ;******************************************************************************************************** ; START MULTITASKING ; void OSStartHighRdy(void) ; ; Note(s) : 1) This function triggers a PendSV exception (essentially, causes a context switch) to cause ; the first task to start. ; ; 2) OSStartHighRdy() MUST: ; a) 设置PendSV异常优先级最低 ; b) 设置初始PSP为0,告诉上下文切换器这是第一次运行 ; c) 将主堆栈设置为OS_CPU_ExceptStkBase ; d) 设置OSRunning(系统开始运行标志)为真; ; e) 触发PendSV; ; f) 启用中断,开始调度; ;******************************************************************************************************** OSStartHighRdy LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority LDR R1, =NVIC_PENDSV_PRI STRB R1, [R0] MOVS R0, #0 ; Set the PSP to 0 for initial context switch call MSR PSP, R0 LDR R0, =OS_CPU_ExceptStkBase ; Initialize the MSP to the OS_CPU_ExceptStkBase LDR R1, [R0] MSR MSP, R1 LDR R0, =OSRunning ; OSRunning = TRUE MOVS R1, #1 STRB R1, [R0] LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch) LDR R1, =NVIC_PENDSVSET STR R1, [R0] CPSIE I ; Enable interrupts at processor level OSStartHang B OSStartHang ; Should never get here |
|
|
|
任务运行流程
任务创建 任务创建通过OSTaskCreateExt()函数进行,主要初始化任务堆栈和初始化TCB块的信息: 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);//保存浮点寄存器的值 初始化任务堆栈 设置堆栈,任务函数,初始化ro-r15及系统xPSR状态寄存器,如使能FPU,还需初始化FPU寄存器。初始化ro-r15及系统xPSR状态寄存器。 添加进TCB任务控制块 OS_TCBInit(),该函数首先从空闲TCB指针申请了一块控制块(地址),依次在该申请的TCB块中加入了任务的堆栈指针(栈顶、栈底)、更新了空闲TCB指针、任务优先级、任务选项、并清除了任务的挂起状态并置为就绪状态,计算出了任务的X和Y(横坐标和纵坐标),然后将这个新申请的TCB块地址加到 OSTCBPrioTbl[Prio] TCB指针表中(基于优先级存放),然后再加入到OSTCBList双向链表中,更新OSRdyGrp任务就绪表,更新OSRdyTbl任务的就绪位表、更新OSTaskCtr数量任务计数器: ![](#align=left&display=inline&height=639&margin=[object Object]&originHeight=865&originWidth=1326&status=done&style=none&width=979) OS_TCBInit()函数初始化完毕后,会在OSTaskCreateExt()函数内部调用OS_Sched()函数进行任务调度,但由于os还未运行,此时是无法调度的。 开始任务 ucosII通过开始任务来启动其他应用任务,开始任务初始化完毕后,会调用OSStart()函数来启动任务调度。 该函数会判断OSStart标志是否为0,也就是os系统没有启动。 然后调用OS_SchedNew()函数找出最高优先级的任务优先级。 将当前的最高优先级就绪任务的TCB快指针确定后赋值给OSTCBCur(当前运行的TCB指针)。 最后调用OSStartHighRdy来启动调度。 功能说明 信号量(二值、计数型) 信号量总体运行规则是:创建1个事件控制块, 有任务请求信号量,就把该任务放入等待事件列表中,然后其他任务会发送信号量,就是把等待信号量的任务从挂起态设置为就绪态。 二值信号量就是独占任务,只能1个任务占用。 计数型或多值信号量,就是在初始化信号量的时候传入的共享资源cnt数(>1),表示该资源可以同时被cnt个任务同时占用,与二值信号量的区别是,使用后需要释放。 OSSemCreate() 信号量创建函数: 该函数创建1个具有信号量事件的类型OS_EVENT的控制块,并返回,用于请求和发送。 OSSemPend() 请求信号量: 该请求函数有3个参数,1个是创建信号量时返回的事件控制块指针,1个是超时时间(0是一直等待),1个是错误返回。 1、函数进入后,判断信号量计数是否大于1,如果是说明有信号量发送了,进行减一后,此时函数将在此返回,执行**OSSemPend(temp)**后面的逻辑代码。 2、再次进入这函数后,因为没有信号量发送或已经发送了,所以当前任务没有得到可用信号量,不会返回,此时当前运行的任务状态将被与等为信号量待定状态,同时将超时时间传入(超时时间是系统节拍,如果传入的值为真,则当前任务在系统滴答中断里面递减满足超时时间后,会将此任务恢复就绪,并再次执行)。 3、将事件控制块信息保存到当前任务的事件控制块指针中,将当前任务放入等候名单,然后清除当前任务的就绪组和就绪位信息。 4、执行调度函数,进行调度。 5、该信号量被发送信号量发送信号或超时恢复的时候,任务将从调度函数OS_Sched()的下一条指令执行(因为在进行系统调度的时候,入栈保存的LR寄存器指向下一条指令的地址)。 恢复任务后,检查状态,并将任务状态更改为准备运行。 OSSemPost() 发送信号量: 函数只有1个参数,即事件控制块指针。 1、验证事件控制块是否有等待事件。 2、通过OS_EventTaskRdy函数得到等待任务的X和Y,获取任务的优先级,取得TCB块指针,将等待任务的TCB块内的延时清0,清除任务的信号量待定状态,将任务从新加入到就绪组中,最后从事件等待列表中删除此任务。 3、信号量值加1。 4、执行调度函数,进行任务调度。 互斥信号量 互斥信号量用于降解优先级翻转的问题,当ABC的优先级分别为123时,C在某个时刻获得了信号量的使用权,在执行逻辑代码的时候,系统掉调度发生,因为B任务准备就绪,且优先级大于C,此时线程切换至B,在执行B代码的时候,A准备就绪,线程切换至A,A请求信号量,因为C被B打断,但C的信号量这个时候还没有释放,故A任务挂起,返回执行B,等B执行完后,才执行C,等C释放信号量后,A才得以运行。这一过程就是典型的发生了优先级翻转问题:B先于A运行。 解决这个问题就引入了互斥信号量,还是ABC,优先级为123,C在某个时刻获得了信号量的使用权,在执行逻辑代码的时候,系统掉调度发生,因为B任务准备就绪,且优先级大于C,此时线程切换至B,在执行B代码的时候,A准备就绪,线程切换至A,A请求信号量,在请求的过程中,发现优先级比C高,为了C不被其他任务打断,故将C的优先级提高到天花板,此时线程切换回C,等C执行完,释放信号量,恢复优先级后,线程切换至A,A执行完后,再执行B。 只要低优先级任务拿到信号量,高优先级任务再请求时,都会直接把低优先级的任务的优先级提升到天花板,无论有没有其他任务打断这个低优先级任务,直接提升。 OSMutexCreate() 互斥信号量创建函数参数1为需要继承的天花板优先级,参数2为错误返回。 函数进入后,判断了天花板优先级是否已经有任务占用了,没有的话,就开辟1个事件块,并将事件的参数进行初始化,将OSEventCnt置为可用的。 函数返回事件控制块首地址。 OSMutexPend() 请求互斥信号量函数,参数1为请求的信号量事件块,参数2超时时间,参数3为错误返回。 函数进来获取继承优先级 判断信号量是否已经被占用,没有占用就占用该信号量,将当前任务的TCB,优先级与继承优先级位或存入事件结构体后函数正常返回。 当发生互斥(信号量被已经被独占没有被释放,这个时候又有其他任务申请)后,函数经过②不会返回,表示信号量已经被其他任务占用了,此时先获取占用信号量任务的优先级及TCB块指针: 比较占有该信号量的任务的优先级比继承优先级小时才会进行反转: 随后会比较占用信号量任务的优先级是否比当前任务小。 如果是,从占用信号量任务的就绪列表和位列表将其删除,更改占用信号量任务的优先级为天花板,重新计算XY,放入就绪列表和就绪位表。 将当前任务添加到等候名单(挂起),并将当前这个任务的任务状态更改为信号量待定,然后调用线程切换到获取到天花板优先级任务中去。 |
|
|
|
OSMutexPost()
互斥信号量发送函数: INT8U OSMutexPost (OS_EVENT *pevent) { INT8U pip; INT8U prio; #if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr = 0; //方式3将把cpsr状态寄存器推入临时堆栈cpu_sr中,可以安全返回之前的中断状态 #endif if (OSIntNesting > 0) { return (OS_ERR_POST_ISR); } #if OS_ARG_CHK_EN > 0 if (pevent == (OS_EVENT *)0) { return (OS_ERR_PEVENT_NULL); } #endif if (pevent->OSEventType != OS_EVENT_TYPE_MUTEX) { return (OS_ERR_EVENT_TYPE); } OS_ENTER_CRITICAL(); pip = (INT8U)(pevent->OSEventCnt >> 8); //变相置顶值pip prio = (INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8); //持有mutex资源的task原始优先级 if (OSTCBCur != (OS_TCB *)pevent->OSEventPtr) { //因为解决了局部优先级翻转问题,所以OSTCBCur肯定要等于pevent->OSEventPtr,否则发生了不知名的异常 OS_EXIT_CRITICAL(); return (OS_ERR_NOT_MUTEX_OWNER); } if (OSTCBCur->OSTCBPrio == pip) { //task被提升了优先级到pip置顶值,也就是一个比该task优先级高,比pip低的进程需要访问互斥资源,即:存在B角色进程,那么使用OSMutex_RdyAtPrio()把本task从就绪控制矩阵中摘下来,同时将自己还原到prio优先级 OSMutex_RdyAtPrio(OSTCBCur, prio); } OSTCBPrioTbl[pip] = OS_TCB_RESERVED; if (pevent->OSEventGrp != 0) { /*OS_EventTaskRdy()函数将摘掉等待在pevent事件控制矩阵上的task中优先级最高的task 如果该task仅仅等待该pevent事件,那么将该task添加到就绪控制矩阵中 OSRdyGrp |= bity; OSRdyTbl[y] |= bitx;这样调度程序就会根据情况调度OS_Sched()该task了*/ prio = OS_EventTaskRdy(pevent, (void *)0, OS_STAT_MUTEX, OS_STAT_PEND_OK); /*保持处于高8位的pip置顶优先级值同时清除低8位数据*/ pevent->OSEventCnt &= OS_MUTEX_KEEP_UPPER_8; pevent->OSEventCnt |= prio; //获得执行权的task充当优先级翻转计算中的C角色 pevent->OSEventPtr = OSTCBPrioTbl[prio]; //task的控制块传给OSEventPtr指针,供优先级翻转计算使用 if (prio <= pip) { /*prio比pip小,那么说明比置顶值pip优先级还要高的进程竟然访问了共享资源, 那么这时可能会出现优先级翻转,因为这时mutex机制已经不起作用, 所以应该保证"变相置顶的方式"初始化时,自己内定的最高优先级pip务必大于所有能访问互斥资源的进程优先级[gliethttp]*/ OS_EXIT_CRITICAL(); OS_Sched(); return (OS_ERR_PIP_LOWER); } else { OS_EXIT_CRITICAL(); OS_Sched(); return (OS_ERR_NONE); } } //没有任何一个task悬停在本event事件控制矩阵上[gliethttp] pevent->OSEventCnt |= OS_MUTEX_AVAILABLE; //还原为初始值 pevent->OSEventPtr = (void *)0; //现在本task不悬停在任何event事件上 OS_EXIT_CRITICAL(); return (OS_ERR_NONE); } 该函数还是首先检查了中断嵌套和加锁,然后获取了继承优先级和任务原优先级,然后会判断当前任务的优先级是否被提升过,如果是,将通过函数OSMutex_RdyAtPrio恢复任务的优先级,然后判断有无任务正在等待互斥信号。 如果有的话就通过OS_EventTaskRdy获取等待任务的优先级,在获取优先级的同时会把获取的任务通过OS_EventTaskRemove函数从event等待列表中删除,并把等待的任务加入到就绪表中,如果没有event mutex的等待任务,则会直接设置event的OSEventCnt位OS_MUTEX_AVAILABLE和OSEventPtr的清0操作,然后调用OS_Sched来调度任务。 如果没有等待任务在event的等待列表中,则需要当前任务自己释放自己,也就是第三部分最后的4行操作。 消息邮箱 没有特别的,跟信号量唯一的区别是发送信号量时可以携带1个信息给等待信号量的任务**。** OSMboxCreate 邮箱任务创建,该函数返回1个OS_EVENT事件控制块首地址。 OSMboxPend 邮箱等待函数,该函数参数1为等待邮箱的事件块,参数2为超时时间,参数3为错误返回,进入函数后先判断是否在中断或加锁中,首次进入后,肯定没人给他发消息,所以该函数通过OS_EventTaskWait函数将自己挂起,如果是第二次或以上进入,一定是有人发消息了,所以再次进入后,会判断消息指针是否为空,如果不为空,则将消息指针清零,同时返回消息指针。 OSMboxPost 邮箱发送函数,该函数参数1为发送邮箱的事件块,参数2为要传递的消息,进入函数后先判断是否在中断或加锁中,然后判断有无正在等待的任务,如果有,同样通过OS_EventTaskRdy函数将等待任务中挂起态放入运行列表中,并且将消息赋值给等待任务的OSTCBMsg,随后将自身任务从就绪列表中删除,最后通过OS_Sched寻找高优先级就绪任务进行调度。 特别注意的是,该函数二次进入后是从OS_Sched函数后开始执行的,可以看到将直接得到消息,并将消息指针返回。 消息队列 可以存放N个信息的指针,当请求消息的任务得到消息了,这时候又有任务给他发,就会在消息的环形缓冲区累加地址,数据得以保存,不会丢失。 OSQCreate 消息队列创建函数,参数1为放消息的buff,参数2为消息个数,返回OS_EVENT类型的事件块指针。 OSQPend 消息队列请求函数,参数1为发送消息队列的事件块,参数2为超时时间,参数3为错误返回,进入函数后先判断是否在中断或加锁中,首次进入后,肯定没人给他发消息,所以该函数通过OS_EventTaskWait函数将自己挂起,如果是第二次或以上进入,一定是有人发消息了,所以再次进入后,会判断消息指针是否为空,如果不为空,则将消息指针清零,同时返回消息指针,跟消息邮箱不同的是,在返回之前会将存放消息队列的读指针进行加1,消息记录减1,并会判断读指针越界没有,如果越界将重新指向消息队列队头。 OSQPost 邮箱发送函数,该函数参数1为发送消息队列的事件块,参数2为要传递的消息,进入函数后先判断是否在中断或加锁中,然后判断有无正在等待的任务,如果有,同样通过OS_EventTaskRdy函数将等待任务中挂起态放入运行列表中,并且将消息赋值给等待任务的OSTCBMsg,随后将自身任务从就绪列表中删除,最后通过OS_Sched寻找高优先级就绪任务进行调度,再次进入后是在OS_Sched后,这里会维护一个消息队列的写指针,并且会判断写指针超出消息队列没,超出了将重新指向队列队头。 事件标志组 跟信号量、消息邮箱、队列的区别就是可以设置等待多项事件的标志,等所有标志都立起来的时候,才会生效。 软件定时器 软件定时器在功能开启后,会在ucos初始化时由OSTmr_Init函数创建1个软件定时器任务OSTmr_Task。 OSTmr_Task 软件定时器的任务执行函数,该函数是由SysTick心跳里面的OSTmrSignal函数中的OSTimeTickHook钩子函数来调用的,钩子函数会根据节拍来调用OSTmrSignal函数,OSTmrSignal函数里面会对OSTmr_Task函数发送信号量,OSTmr_Task便得以执行: static void OSTmr_Task (void *p_arg) { INT8U err; OS_TMR *ptmr; OS_TMR *ptmr_next; OS_TMR_CALLBACK pfnct; OS_TMR_WHEEL *pspoke; INT16U spoke; p_arg = p_arg; /* Prevent compiler warning for not using 'p_arg' */ for (;;) { OSSemPend(OSTmrSemSignal, 0u, &err); /* Wait for signal indicating time to update timers */ OSSchedLock(); OSTmrTime++; /* Increment the current time */ spoke = (INT16U)(OSTmrTime % OS_TMR_CFG_WHEEL_SIZE); /* Position on current timer wheel entry */ pspoke = &OSTmrWheelTbl[spoke]; ptmr = pspoke->OSTmrFirst; while (ptmr != (OS_TMR *)0) { ptmr_next = (OS_TMR *)ptmr->OSTmrNext; /* Point to next timer to update because current ... */ /* ... timer could get unlinked from the wheel. */ if (OSTmrTime == ptmr->OSTmrMatch) { /* Process each timer that expires */ OSTmr_Unlink(ptmr); /* Remove from current wheel spoke */ if (ptmr->OSTmrOpt == OS_TMR_OPT_PERIODIC) { OSTmr_Link(ptmr, OS_TMR_LINK_PERIODIC); /* Recalculate new position of timer in wheel */ } else { ptmr->OSTmrState = OS_TMR_STATE_COMPLETED; /* Indicate that the timer has completed */ } pfnct = ptmr->OSTmrCallback; /* Execute callback function if available */ if (pfnct != (OS_TMR_CALLBACK)0) { (*pfnct)((void *)ptmr, ptmr->OSTmrCallbackArg); } } ptmr = ptmr_next; } OSSchedUnlock(); } } 函数首次进入后,将自己挂起,根据时钟节拍接收信号量来执行。 当函数得到信号量的时候,首先上锁,并将软件定时器的计数自增,然后获取定时器条轮的入口地址,从条轮任务列表中取出任务的首地址,然后判断任务是否存在,存在的话先取出下个条轮任务的首地址,然后根据软件定时器的计时变量是否等于当前定时任务的定时时间,如果是,解锁,然后判断任务的定时器是否是需要重新装载,如果是,调用OSTmr_Link函数重新放入定时条轮,如不是,将指示该软件定时器为完成。然后调用回调执行任务,最后条轮指针指向下一个软件定时器入口。 OSTmrCreate 软件定时器创建函数,参数1为延时时间、参数2为周期还是单次、参数3是触发后手动添加时间还是自动重装,参数4是回调函数,参数5是回调参数,参数6是任务名字,参数7错误返回,函数将任务的参数加入时间队列后,返回OS_TMR类型的块指针。 OSTmrStart 软件定时器开始函数,参数1为要启动的OS_TMR类型的块指针,参数2为错误返回,函数首先进入后判断是否在中断中,然后上锁,根据定时器状态来执行,首次启动定时器为停止状态,调用OSTmr_Link函数将要启动的任务连接到时间条轮上。然后通过OSTmr_Task函数来执行任务。 定时器装载进定时器轮时就会确定其对应的OSTmrMacth值,这个值在定时器停止或重新装载之前保证不会变化,因此就能根据定时器控制块的OSTmrMacth字段值计算出这个定时器是在哪个轮辐之上,从而把大量的定时器分散于不同的轮辐之上,加速对定时器的操作。 当定时器轮需要接收一个定时器时,会先计算此定时器应该放在哪个轮辐之上,然后将其插入轮辐链表的第一个,如果此轮辐上还有其他定时器,则还会将原来第一个定时器的Prev指针指向新插入的这个定时器。 而当定时器轮卸下一个定时器时,则如果不是在轮辐的第一个,则可以通过Prev指针找到前一个定时器,这样就把删除定时器的时间控制在O(1)了。 任务同步 禁止任何中断和任务调度 OS_ENTER_CRITICAL() OS_EXIT_CRITICAL() 禁止任务调度 OSSchedLock() OSSchedUnlock() |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1618 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1545 浏览 1 评论
979 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
683 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1597 浏览 2 评论
1863浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
645浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
516浏览 3评论
532浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
505浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-22 16:01 , Processed in 1.007549 second(s), Total 86, Slave 70 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号