嵌入式_Ucos

1、UCOS简介:

1、UCOSII 是一个基于 ROM 运行的、可裁减的、抢占式、实时多任务内核,具有高度可移植性的实时操作系统(RTOS)。
UCOSII 具有执行效率高、占用空间小、实时性能优良和可扩展性强等特点, 最小内核可编译至 2KB 。
在这里插入图片描述
注:1、我们使用版本是v2.91,比早期版本多了 软件定时器,支持任务数最大达到 255 个。
2、有两个文件:os_dbg_r.c 和 os_dbg.c,没有在上图列出,也不将其加入到我们的工程中,这两个主要用于对 UCOS 内核进行调试支持,比较少用到。
3、UCOSII 的移植,只需要修改:os_cpu.h、os_cpu_a.asm 和 os_cpu.c三个文件即可,其中:os_cpu.h,进行数据类型的定义,以及处理器相关代码和几个函数原型;os_cpu_a.asm,是移植过程中需要汇编完成的一些函数,主要就是任务切换函数;os_cpu.c,定义一些用户HOOK 函数。
4、定时器的作用是为 UCOSII 提供系统时钟节拍,实现任务切换和任务延时等功能,时钟节拍由 OS_TICKS_PER_SEC(在 os_cfg.h 中定义)设置。
5、UCOSII 保留了最高4 个优先级和最低 4 个优先级的总共 8 个任务,用于拓展使用,但实际上,UCOSII 一般只占用最低 2 个优先级,分别用于空闲任务(倒数第一)和统计任务(倒数第二),所以剩下给我们使用的任务最多可达 255-2=253 个。

2、UCOS 是怎样实现多任务并发工作的呢?
答:类似于外部中断处理,当任务 A 正在执行时,如果需要释放 cpu 控制权,先对任务 A 进行现场保护,然后从任务就绪表中查找其他就绪任务去执行,等到任务 A 的等待时间到了,它重新获得cpu 控制权,先恢复任务 A 的现场,从而继续执行任务 A,这样看起来就好像两个任务同时执行了。实际上,任何时候,只有一个任务可以获得 cpu 控制权。
Ucos 任务的一般格式为:
void MyTask (void *pdata)
{
任务准备工作… While(1)//死循环
{
任务 MyTask 实体代码;
OSTimeDlyHMSM(x,x,x,x);//调用任务延时函数,释放 cpu 控制权,
}
}
3、ucos相关概念
任务优先级:ucos 中,优先级是任务的唯一标识。使用 CPU 的时候,优先级高(数值小)的任务比优先级低的任务具有优先使用权,即任务就绪表中总是优先级最高的任务获得 CPU 使用权,只有高优先级的任务让出 CPU 使用权(比如延时)时,低优先级的任务才能获得 CPU 使用权。UCOSII 不支持多个任务优先级相同,也就是每个任务的优先级必须不一样。

任务堆栈,就是存储器中的连续存储空间。为了满足任务切换和响应中断时保存 CPU 寄存器中的内容以及任务调用其他函数时的需要,每个任务都有自己的堆栈。在创建任务的时候,任务堆栈是任务创建的一个重要入口参数。

任务控制块(OS_TCB) ,用来记录任务堆栈指针,任务当前状态以及任务优先级等任务属性。UCOSII 的任何任务都是通过任务控制块(TCB)的东西来控制的,一旦任务创建了,任务控制块 OS_TCB 就会被赋值。每个任务控制块有 3 个最重要的参数:1,任务函数指针;2,任务堆栈指针;3,任务优先级;

任务就绪表,简而言之就是用来记录系统中所有处于就绪状态的任务。它是一个位图,系统中每个任务都在这个位图中占据一个进制位,该位置的状态(1 或者 0)就表示任务是否处于就绪状态。

任务调度:作用一是在任务就绪表中查找优先级最高的就绪任务,二是实现任务的切换。比如说,当一个任务释放 cpu 控制权后,进行一次任务调度,这个时候任务调度器首先要去任务就绪表查询优先级最高的就绪任务,查到之后,进行一次任务切换,转而去执行下一个任务。

4、5种状态:每个任务都处在以下 5 种状态之一的状态下,这 5种状态是:睡眠状态、 就绪状态、 运行状态、 等待状态(等待某一事件发生)和中断服务状态。
睡眠状态:任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。
就绪状态:系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准备好了,但由于该任务的优先级比正在运行的任务的优先级低, 还暂时不能运行,这时任务的状态叫做就绪状态。
运行状态:该任务获得 CPU 使用权,并正在运行中。
等待状态:正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把 CPU 的使用权让给别的任务而使任务进入等待状态。
中断服务状态:一个正在运行的任务 一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态。
在这里插入图片描述
5、与任务相关的几个函数:
1) 建立任务函数
UCOSII 提供了我们 2 个建立任务的函数:OSTaskCreat 和 OSTaskCreateExt,我们一般用 OSTaskCreat 函数来创建任务,
该函数原型为:OSTaskCreate(void(task)(voidpd),voidpdata,OS_STKptos,INTU prio)。
该函数包括 4 个参数:task:是指向任务函数的指针;pdata:是任务开始执行时,传递给任务参数的指针;ptos:是分配给任务堆栈的栈顶指针(每个任务都有自己的堆栈,堆栈必须申明为 OS_STK 类型,并且由连续的内存空间组成。可以静态分配堆栈空间,也可以动态分配堆栈空间。);prio 是分配给任务的优先级。
2) 任务删除函数
所谓任务删除,其实是把任务置于睡眠状态,并不是把任务代码给删除了。UCOSII提供的任务删除函数原型为:
INT8U OSTaskDel(INT8U prio);其中参数 prio 就是我们要删除的任务的优先级,可见该函数是通过任务优先级来实现任务删除的。
特别注意:任务不能随便删除,必须在确保被删除任务的资源被释放的前提下才能删除!
3) 请求任务删除函数
前面提到,必须确保被删除任务的资源被释放的前提下才能将其删除,所以我们通过向被删除任务发送删除请求,来实现任务释放自身占用资源后再删除。UCOSII 提供的请求删除任务函数原型为:INT8U OSTaskDelReq(INT8U prio);同样通过优先级来确定被请求删除任务。
4) 改变任务的优先级函数
UCOSII 在建立任务时,会分配给任务一个优先级,但是这个优先级可以通过调用 UCOSII 提供的函数修改。UCOSII 提供的任务优先级修改函数原型为:INT8U OSTaskChangePrio(INT8U oldprio,INT8U newprio);
5) 任务挂起函数
任务挂起和任务删除有点类似,但是又有区别,任务挂起只是将被挂起任务的就绪标志删除,并做任务挂起记录,并没有将任务控制块从任务控制块链表里面删除,也不需要释放其资源,而任务删除则必须先释放被删除任务的资源,并将被删除任务的任务控制块也给删了。被挂起的任务,在恢复(解挂)后可以继续运行。UCOSII 提供的任务挂起函数原型为:INT8U OSTaskSuspend(INT8U prio);
6) 任务恢复函数
通过该函数将被挂起的任务恢复,让调度器能够重新调度该函数。UCOSII 提供的任务恢复函数原型为:
INT8U OSTaskResume(INT8U prio);
7) 任务信息查询
在应用程序中我们经常要了解任务信息,查询任务信息函数原型为:INT8U OSTaskQuery(INT8U prio,OS_TCB *pdata);
这个函数获得的是对应任务的 OS_TCB 中内容的拷贝。

注:从上面这些函数我们可以看出,对于每个任务,有一个非常关键的参数就是任务优先级 prio,在UCOS 中,任务优先级可以用来作为任务的唯一标识,所以任务优先级对任务而言是唯一的,而且是不可重复的。

6、UCOSII移植过程:
①向工程中添加文件夹(CONFIG、CORE、PORT)
②文件夹中添加文件:CORE中添加ucos源码对应文件夹中移植来的全部.c文件;CONFIG中加入includes.h和os_cfg.h;PORT中加入os_cpu.h、os_cpu_a.asm、os_cpu_c.c、os_dbg.c、os_dbg_r.c。
在这里插入图片描述
③工程中加入对应文件,注意core中去掉ucosii.c;PORT中加入os_cpu_c.c和os_cpu_a.asm和os_cpu.h;CONFIG加入includes.h、os_cfg.h;
④ucos_ii.h中注释掉include <app_cfg.h>;删除stm32f10x_it.c文件中的PendSV_Handler和SysTick_Handler函数
⑤注意修改os_cpu_c.c文件中192行为:
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK stk;
(void)opt; /
‘opt’ is not used, prevent warning /
stk = ptos; /
Load stack pointer /
/
Registers stacked as if auto-saved on exception */
(stk) = (INT32U)0x01000000L; / xPSR */
(–stk) = (INT32U)task; / Entry Point /
(–stk) = (INT32U)0xFFFFFFFEL; / R14 (LR) (init value will cause fault if ever used)
/
(–stk) = (INT32U)0x12121212L; / R12 */
(–stk) = (INT32U)0x03030303L; / R3 */
(–stk) = (INT32U)0x02020202L; / R2 */
(–stk) = (INT32U)0x01010101L; / R1 */
(–stk) = (INT32U)p_arg; / R0 : argument /
/
Remaining registers saved on process stack */
(–stk) = (INT32U)0x11111111L; / R11 */
(–stk) = (INT32U)0x10101010L; / R10 */
(–stk) = (INT32U)0x09090909L; / R9 */
(–stk) = (INT32U)0x08080808L; / R8 */
(–stk) = (INT32U)0x07070707L; / R7 */
(–stk) = (INT32U)0x06060606L; / R6 */
(–stk) = (INT32U)0x05050505L; / R5 */
(–stk) = (INT32U)0x04040404L; / R4 */
return (stk);
}
⑥修改sys.h头文件中的SYSTEM_SUPPORT_UCOS 0 改成1!!
7、注意点:
1、我们在创建 start_task 之前首先调用 ucos 初始化函数 OSInit(),该函数的作用是初始化 ucos 的所有变量和数据结构,该函数必须在调用其他任何 ucos 函数之前调用。在start_task 创建之后,我们调用 ucos 多任务启动函数 OSStart(),调用这个函数之后,任务才真正开始运行。我们在 main 函数只创建了 start_task一个任务,然后在 start_task 再创建另外两个任务,在创建之后将自身(start_task)挂起。这里,我们单独创建 start_task,是为了提供一个单一任务,实现应用程序开始运行之前的准备工作(比如:外设初始化、创建信号量、创建邮箱、创建消息队列、创建信号量集、创建任务、初始化统计任务等)。
2、经常有一些代码段必须不受任何干扰地连续运行,这样的代码段叫做临界段(或临界区)。因此,为了使临界段在运行时不受中断所打断,在临界段代码前必须用关中断指令使 CPU 屏蔽中断请求,而在临界段代码后必须用开中断指令解除屏蔽使得 CPU 可以响应中断请求。UCOSII 提供 OS_ENTER_CRITICAL 和 OS_EXIT_CRITICAL 两个宏来实现。因为临界段代码不能被中断打断,将严重影响系统的实时性,所以临界段代码越短越好!
3、一个任务里面一般是必须有延时函数的,以释放 CPU 使用权,否则可能导致低优先级的任务因高优先级的任务不释放 CPU 使用权而一直无法得到 CPU 使用权,从而无法运行。

8、UcosII信号量和邮箱
系统中多任务运行,需要互相无冲突地访问同一个共享资源,或者互相支持和依赖,甚至有时要互相加以必要的限制和制约,才能保证任务的顺利运行。因此,操作系统必须具有对任务运行进行协调的能力,从而使任务之间可以无冲突、流畅地同步运行。
例如,任务 A 和任务 B 共享一台打印机,如果系统已经把打印机分配给了任务 A,则任务B 因不能获得打印机的使用权而应该处于等待状态,只有当任务 A 把打印机释放后,系统才能唤醒任务 B 使其获得打印机的使用权。如果这两个任务不这样做,那么会造成极大的混乱 。

任务间的同步依赖于任务间的通信。在 UCOSII 中,是使用信号量、邮箱(消息邮箱)和消息队列这些被称作事件的中间环节来实现任务之间通信的。两个任务通过事件进行通讯,为了把描述事件的数据结构统一起来,UCOSII 使用叫做事件控制块(ECB)的数据结构来描述诸如信号量、邮箱(消息邮箱)和消息队列这些事件。事件控制块中包括等待任务表在内的所有有关事件的数据。
typedef struct
{
INT8U OSEventType; //事件的类型
INT16U OSEventCnt; //信号量计数器
void *OSEventPtr; //消息或消息队列的指针
INT8U OSEventGrp; //等待事件的任务组
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];//任务等待表
#if OS_EVENT_NAME_EN > 0u
INT8U *OSEventName; //事件名
#endif
} OS_EVENT; //事件控制块结构体定义
(1)信号量:目的是为了给共享资源设立一个标志,表示该共享资源的占用情况。
(一个任务在访问共享资源之前,就可以先对这个标志进行查询,从而在了解资源被占用的情况之后,再来决定自己的行为。)
信号量可以分为两种:一种是二值型信号量——互斥型信号量,另外一种是 N 值信号量——计数型信号量。

1) 创建信号量函数
OS_EVENT *OSSemCreate (INT16U cnt);
返回值为已创建信号量的指针,参数 cnt 是信号量计数器(OSEventCnt)的初始值。
2) 请求信号量函数
void OSSemPend ( OS_EVENT *pevent, INT16U timeout, INT8U *err);
参数 pevent 是被请求信号量的指针,timeout 为等待时限(防止任务因得不到信号量而处于长期的等待状态,如果被设置为 0,则表明任务的等待时间为无限长),err 为错误信息。
3) 发送信号量函数(释放信号量)——任务获得信号量,并在访问共享资源结束以后,必须要释放信号量!
INT8U OSSemPost(OS_EVENT *pevent);
pevent 为信号量指针,函数调用成功后,返回值为 OS_ON_ERR,否则会根据具体错误返回 OS_ERR_EVENT_TYPE、OS_SEM_OVF。
注意:OSSemPost 函数在对信号量的计数器操作之前,首先要检查是否还有等待该信号量的任务。如果没有,就把信号量计数器
OSEventCnt 加一;如果有,则调用调度器 OS_Sched( )去运行等待任务中优先级别最高的任务。
4) 删除信号量函数
OS_EVENT *OSSemDel (OS_EVENT *pevent,INT8U opt, INT8U *err);
pevent 为要删除的信号量指针,opt 为删除条件选项,err 为错误信息。

(2)邮箱:用来传递消息缓冲区指针的数据结构叫做邮箱(消息邮箱)。
(任务与任务之间通过传递数据——“消息” 的方式来进行通信,在内存中创建了一个存储空间作为该数据的缓冲区(消息缓冲区)。在任务间传递数据(消息)的最简单办法就是传递消息缓冲区的指针,我们通过事件控制块的 OSEventPrt 来传递消息缓冲区指针,同时使事件控
制块的成员 OSEventType 为常数 OS_EVENT_TYPE_MBOX,则该事件控制块就叫做消息邮箱。)
1) 创建邮箱函数
OS_EVENT *OSMboxCreate (void *msg);
参数 msg 为消息的指针,返回值为消息邮箱的指针。
注:调用函数前需先定义 msg 的初始值。一般情况下,初始值为NULL;但也可以先定义一个邮箱,然后把这个邮箱的指针作为参数传递到
OSMboxCreate 中,使之一开始就指向一个邮箱。
2) 向邮箱发送消息函数
INT8U OSMboxPost (OS_EVENT *pevent,void *msg);
pevent 为消息邮箱的指针,msg 为消息指针。
3) 请求邮箱函数
void *OSMboxPend (OS_EVENT *pevent, INT16U timeout, INT8U *err);
pevent 为请求邮箱指针,timeout 为等待时限,err 为错误信息。
注:这个函数的主要作用就是查看邮箱指针 OSEventPtr 是否为 NULL,如果不是 NULL 就把邮箱中的消息指针返回给调用函数
的任务,同时用 OS_NO_ERR 通过函数的参数 err 通知任务获取消息成功;如果邮箱指针OSEventPtr 是 NULL,则使任务进入等待状态,并引发一次任务调度。
4) 查询邮箱状态函数
INT8U OSMboxQuery(OS_EVENT *pevent,OS_MBOX_DATA *pdata);
pevent 为消息邮箱指针,pdata 为存放邮箱信息的结构。
5) 删除邮箱函数
OS_EVENT *OSMboxDel(OS_EVENT *pevent,INT8U opt,INT8U *err);
pevent 为消息邮箱指针,opt 为删除选项,err 为错误信息。

(3)消息队列:(消息队列可以在任务之间传递多条消息)
(消息队列由三个部分组成:事件控制块、消息队列和消息。当把事件控制块成员 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; //指向插入一条消息的位置。当它移动到与 OSQEnd 相等时,被调整到指向数组的起始单元
void OSQOut; //指向被取出消息的位置。当它移动到与 OSQEnd 相等时,被调整到指向数组的起始单元
INT16U OSQSize; //数组的长度
INT16U OSQEntries; //已存放消息指针的元素数目
} OS_Q;
注意:OSQIn 和 OSQOut为可移动的指针,OSQStart 和 OSQEnd 只是一个标志指针。当 OSQIn 或 OSQOut 移动到数组末尾,即与OSQEnd 相等时,可移动的指针将会被调整到数组的起始位置 OSQStart。也就是说,从效果上来看,指针 OSQEnd与 OSQStart 等值。于是,这个由消息指针构成的数组就头尾衔接起来形成了一个循环的队列。
在这里插入图片描述
1) 创建消息队列函数
OS_EVENT *OSQCreate(void
start,INT16U size);
:start 为存放消息缓冲区指针数组的地址,size 为该数组大小。该函数的返回值为消息队列指针。
注意:创建一个消息队列首先需要定义一指针数组,然后把各个消息数据缓冲区的首地址存入这个数组中,然后再调用函数 OSQCreate 来创建消息队列。
2) 请求消息队列函数——目的是为了从消息队列中获取消息。
void
OSQPend(OS_EVENTpevent,INT16U timeout,INT8U err) ;
:pevent 为所请求的消息队列的指针,timeout 为任务等待时限,err 为错误信息。
3) 向消息队列发送消息函数
任务可以通过调用函数 OSQPost 或 OSQPostFront 两个函数来向消息队列发送消息。
函数 OSQPost 以 FIFO(先进先出)的方式组织消息队列,函数 OSQPostFront 以 LIFO(后进先出)的方式组织消息队列。
INT8U OSQPost(OS_EVENT
pevent,void msg)
INT8U OSQPost(OS_EVENT
pevent,void
msg);
:pevent 为消息队列的指针,msg 为待发消息的指针。
(4)信号量集:信号量集实质上是一种可以对多个输入的逻辑信号进行基本逻辑运算的组合逻辑
(5)软件定时器
在实时操作系统中一个好的软件定时器实现要求有较高的精度、较小的处理器开销,且占用较少的存储器资源。

UCOSII 通过 OSTimTick 函数对时钟节拍进行加 1 操作,同时遍历任务控制块,以判断任务延时是否到时。软件定时器同样由 OSTimTick 提供时钟,但是软件定时器的时钟还受 OS_TMR_CFG_TICKS_PER_SEC 设置的控制,也就是在 UCOSII 的时钟节拍上面再做了一次“分频”,软件定时器的最快时钟节拍就等于 UCOSII 的系统时钟节拍。这也决定了软件定时器的精度。

软件定时器定义了一个单独的计数器 OSTmrTime用于软件定时器的计时,UCOSII 并不在 OSTimTick 中进行软件定时器的到时判断与处理,而是创建了一个高于应用程序中所有其他任务优先级的定时器管理任务 OSTmr_Task,在这个任务中进行定时器的到时判断和处理。时
钟节拍函数通过信号量给这个高优先级任务发信号。这种方法缩短了中断服务程序的执行时间,但也使得定时器到时处理函数的响应受到中断退出时恢复现场和任务切换的影响。
软件定时器的实现方法是,将定时器按定时 时间分组,使得每次时钟节拍到来时只对部分定时器进行比较操作,缩短了每次处理的时间。但这就需要动态地维护一个定时器组。定时器组的维护只是在每次定时器到时才发生,而且定时器从组中移除和再插入操作不需要排序。

UCOSII 软件定时器实现了 3 类链表的维护:
OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; //定时器控制块数组
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 和 OSTmrPrev 两个指针同样也组织了所在分组中定时器控制块的双向链表。

1) 创建软件定时器函数
OS_TMR *OSTmrCreate (INT32U dly, INT32U period, INT8U opt,
OS_TMR_CALLBACK callback,void *callback_arg, INT8U *pname, INT8U *perr);
dly,用于初始化定时时间,对单次定时(ONE-SHOT 模式)的软件定时器来说,这就是该定时器的定时时间,而对于周期定时(PERIODIC 模式)的软件定时器来说,这是该定时器第一次定时的时间,从第二次开始定时时间变为 period。

period,在周期定时(PERIODIC 模式),该值为软件定时器的周期溢出时间。
opt,用于设置软件定时器工作模式。OS_TMR_OPT_ONE_SHOT(单词定时器)或 OS_TMR_OPT_PERIODIC(周期定时器。)
callback,为软件定时器的回调函数,当软件定时器的定时时间到达时,会调用该函数。
callback_arg,回调函数的参数。
pname,为软件定时器的名字。
perr,为错误信息。
软件定时器的回调函数格式为:
void (*OS_TMR_CALLBACK)(void *ptmr, void parg);
其中,函数名我们可以自己随意设置,而 ptmr 这个参数,软件定时器用来传递当前定时器的控制块指针,所以我们一般设置其类型为 OS_TMR
类型,第二个参数(parg)为回调函数的参数,你也可以不用,但是必须有这个参数。
2) 开启软件定时器函数
BOOLEAN OSTmrStart (OS_TMR *ptmr, INT8U *perr);
其中 ptmr 为要开启的软件定时器指针,perr 为错误信息。
3) 停止软件定时器函数
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,错误信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值