前言¶
本文档介绍LiteOS的体系结构,并介绍如何进行LiteOS的开发和调试。
本文档主要适用于LiteOS的开发者。
本文档主要适用于以下对象:
物联网端侧软件开发工程师
物联网架构设计师
在本文中可能出现下列标志,它们所代表的含义如下。
概述¶
背景介绍¶
LiteOS是轻量级的实时操作系统,具备轻量级、低功耗、快速启动、组件丰富等关键能力。
LiteOS的基础内核包括不可裁剪的极小内核和可裁剪的其他模块。极小内核只包含任务管理和调度相关的基本功能。可裁剪的模块包括信号量、互斥锁、队列管理、事件管理、软件定时器、内存管理、中断管理、异常管理和系统时钟Tick。
LiteOS支持在单核环境上运行。
高实时性,高稳定性。
低功耗。支持动态加载、分散加载。
支持功能静态裁剪。
快速启动。
任务管理
提供任务的创建、删除、延迟、挂起、恢复等功能,支持锁定和解锁任务调度,支持任务按优先级高低的抢占调度以及同优先级时间片轮转调度。
内存管理
提供静态内存和动态内存两种算法,支持内存的申请与释放。目前支持的内存管理算法包括固定大小的BOX算法、动态申请的bestfit算法和bestfit_little算法。
提供内存统计、内存越界检测功能。
硬件相关
提供中断管理、异常管理、系统时钟Tick等功能。
中断管理:提供中断的创建、删除、使能、禁止及请求位清除功能。
异常管理:系统运行过程中发生异常后,跳转至异常处理模块,打印当前异常的函数调用栈信息或保存当前系统状态。
Tick:Tick是操作系统调度的基本时间单位,其时长由每秒的Tick数决定,由用户配置。
IPC通信
提供消息队列、事件、信号量和互斥锁功能。
消息队列:支持消息队列的创建、删除、发送和接收功能。
事件:支持读事件和写事件功能。
信号量:支持信号量的创建、删除、申请和释放功能。
互斥锁:支持互斥锁的创建、删除、申请和释放功能。
软件定时器
软件定时器提供了定时器的创建、删除、启动、停止功能。
低功耗
Tickless:Tickless机制是通过计算下一次有意义的时钟中断的时间,来减少不必要的时钟中断,从而降低系统功耗。打开Tickless功能后,系统会在CPU空闲时启动Tickless机制。
扩展内核/维测
CPU占用率:可以获取系统或者指定任务的CPU占用率。
Trace事件监测:实时获取事件发生的上下文,并写入缓冲区。支持自定义缓冲区,监测指定模块的事件,开启/停止Trace,清除/输出trace缓冲区数据等。
Perf:一款性能分析工具,可以通过Perf查找性能瓶颈、定位热点代码。LiteOS支持配置采样数据缓冲区、配置采样事件、启动/停止Perf、输出采样数据等功能。
Shell:LiteOS Shell使用串口或Telnet工具接收用户输入的命令,通过命令的方式调用、执行相应的应用程序。LiteOS Shell支持常用的基本调试功能,包含系统、文件、网络和动态加载等相关命令,同时支持用户添加自定义命令。
C++支持
LiteOS支持部分STL特性、异常和RTTI特性,其他特性由编译器支持。
支持的核¶
使用约束¶
LiteOS提供一套自有OS接口,同时也支持POSIX和CMSIS接口。请勿混用这些接口。混用接口可能导致不可预知的错误,例如:用POSIX接口申请信号量,但用LiteOS接口释放信号量。
开发驱动程序只能用LiteOS的接口,上层APP建议用POSIX接口。
基础内核¶
任务¶
概述¶
从系统角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。
LiteOS的任务模块可以给用户提供多个任务,实现任务间的切换,帮助用户管理业务程序流程。LiteOS的任务模块具有如下特性:
支持多任务。
一个任务表示一个线程。
抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。
相同优先级任务支持时间片轮转调度方式。
共有32个优先级[0, 31],最高优先级为0,最低优先级为31。
任务存在两种属性(detached、joinable)。默认情况下任务创建的属性为detached。
任务状态
LiteOS系统中的任务有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。
任务状态通常分为以下四种:
就绪(Ready):该任务在就绪队列中,只等待CPU。
运行(Running):该任务正在执行。
阻塞(Blocked):该任务不在就绪队列中,包含任务被挂起(suspend状态)、任务被延时(delay状态)、任务正在等待信号量、读写队列或者等待事件等。
退出态(Dead):该任务运行结束,等待系统回收资源,包含任务处于僵尸状态(zombie状态)、任务已经被回收(unused状态)。
任务状态迁移
任务状态迁移说明:
就绪态→运行态
任务创建后进入就绪态,发生任务切换时,就绪队列中最高优先级的任务被执行,从而进入运行态,同时该任务从就绪队列中移出。
运行态→阻塞态
正在运行的任务发生阻塞(挂起、延时、读信号量等)时,任务状态由运行态变成阻塞态,然后发生任务切换,取出并运行就绪队列中最高优先级任务。
阻塞态→就绪态(阻塞态→运行态)
阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪队列,从而由阻塞态变成就绪态;此时如果被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,该任务由就绪态变成运行态。
就绪态→阻塞态
任务也有可能在就绪态时被阻塞(挂起),此时任务状态由就绪态变为阻塞态,该任务从就绪队列中删除,不会参与任务调度,直到该任务被恢复。
运行态→就绪态
有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪队列中最高优先级任务会被取出并切换到运行态,那么原先运行的任务由运行态变为就绪态,并插入到就绪队列中。
运行态→退出态
运行中的任务运行结束,任务状态由运行态变为退出态。退出态包含任务运行结束的正常退出状态以及僵尸状态。例如,joinable属性的任务运行结束但是没有被回收资源,对外呈现的就是僵尸状态,即退出态。
阻塞态→退出态
阻塞的任务调用删除接口,任务状态由阻塞态变为退出态。
任务ID
任务ID在任务创建时通过参数返回给用户,是任务的重要标识。系统中的ID号是唯一的。用户可以通过任务ID对指定任务进行任务挂起、任务恢复、查询任务名等操作。
任务优先级
优先级表示任务执行的优先顺序。任务的优先级决定了在发生任务切换时即将要执行的任务,就绪队列中最高优先级的任务将得到执行。
任务入口函数
新任务得到调度后将执行的函数。该函数由用户实现,在任务创建时通过任务创建结构体设置。
任务栈
每个任务都拥有一个独立的栈空间,我们称为任务栈。栈空间里保存的信息包含局部变量、寄存器、函数参数、函数返回地址等。
任务上下文
任务在运行过程中使用的一些资源如寄存器等,称为任务上下文。当这个任务挂起时,其他任务继续执行,可能会修改寄存器等资源中的值。如果任务切换时没有保存任务上下文,可能会导致任务恢复后出现未知错误。
因此,LiteOS在任务切换时会将切出任务的任务上下文信息,保存在自身的任务栈中,以便任务恢复后从栈空间中恢复挂起时的上下文信息,从而继续执行挂起时被打断的代码。
任务控制块TCB
每个任务都含有一个任务控制块(TCB)。TCB包含了任务上下文栈指针(stack pointer)、任务状态、任务优先级、任务ID、任务名、任务栈大小等信息,可以反映出每个任务运行情况。
任务切换
任务切换包含获取就绪队列中最高优先级任务、切出任务上下文保存、切入任务上下文恢复等动作。
任务属性
任务存在两种属性(detached、joinable),任务的不同属性可以决定任务的终止方式。joinable属性即为非分离状态,这种状态的任务退出后,不会自动释放资源,需要被其他任务通过调用LOS_TaskJoin接口等待获取其返回值,才能被释放资源。detached属性即为分离状态,这种状态的任务退出后将自动回收资源,并且这个任务不能被等待,等待是没有意义的。
LiteOS包含两种调度模式,即全局队列调度器模式和多队列调度器模式,在不同场景下各有优势,当前系统默认使用全局调度器模式。
全局队列调度器模式:系统仅使用一个优先级队列保存准备运行的任务,所有核使用同一优先级队列调度,所有任务抢占同一个优先级队列,因此全局队列调度器模式拥有更高的实时性。
多队列调度器模式:仅多核使用,系统按照核数为每个核创建一个优先级队列,每个核仅使用自己的优先级队列进行调度,系统通过负载均衡算法将任务在不同的优先级队列上进行迁移,相比全局队列调度模式可以有效减少频繁的核间迁移。
用户创建任务时,系统会初始化任务栈,预置上下文。此外,系统还会将“任务入口函数”地址放在相应位置。这样在任务第一次启动进入运行态时,将会执行“任务入口函数”。
开发指导¶
任务创建后,内核可以执行锁定任务调度、解锁任务调度、挂起、恢复、延时等操作,同时也可以设置任务优先级,获取任务优先级。
LiteOS的任务管理模块提供下面几种功能,接口详细信息请参见API参考。
须知: 各个任务的任务栈大小可以在创建任务时进行针对性的设置,若设置为0,则使用默认任务栈大小(LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE)作为任务栈大小。
共享栈特性
共享栈特性说明:在原生系统中,每个任务都拥有自己的独立栈区,引入共享栈后,将会有部分任务共用同一片任务栈,称为共享栈,任务发生调度时,老任务的栈数据从共享栈拷出到堆,新任务的栈数据从堆拷回到共享栈。
共享栈使用说明:
LOSCFG_SHARED_STACK:共享栈总开关,开启后才能使用共享栈特性。
LOSCFG_SHARED_STACK_SIZE:配置共享栈的大小(Byte),宏默认值为16384,使用时可以根据实际情况调整,调整范围为:3072到65536,且需要能够整除16,建议配置为“参与共享的任务中任务栈的最大值”。
目前共享栈特性使用白名单策略,在名单内的任务为共享任务。配置方式如下(以hiwing为例):
在文件“Huawei_LiteOS/self_src/targets/hiwing/board.c”的g_taskBlackList数组中进行配置,一共3个字段,分别表示“任务名称”、“是否参与共享”、“是否在栈数据拷贝时进行压缩”。
示例如下:
/* 在此处设置共享栈白名单,名单内的任务参与共享 只有参与共享的任务才能参与压缩 */ const TaskBlackList g_sharedTask[LOSCFG_BASE_CORE_TSK_LIMIT + 1] = { // taskName 加入共享? 加入压缩? {"app_Task", TRUE, FALSE}, {"SerialShellTask", TRUE, FALSE}, {"SerialEntryTask", TRUE, FALSE}, {"TskInSharedBlackLits", TRUE, FALSE}, };
须知:如果任务创建时,指定任务使用创建时传入的栈,则该任务无法参与共享。
共享栈开启后,还可以开启LOSCFG_SHARED_TASK_STAT宏增加任务信息显示功能,但会增加内存开销约0.6KB。
第一组为总体性能数据,包括任务调度最大耗时、平均耗时以及调度耗时超过8000 cycleTime的次数。
第二组为单任务性能数据,包括任务名称、ID、调度次数、参与压缩拷贝次数(未参与或未开启压缩则为0)、压缩成功次数、压缩成功率、拷出到堆的最大耗时、拷入共享栈的最大耗时,以及拷贝耗时超过8000 cycleTime的次数。
加权公平调度特性
加权公平调度(后文简称WCFS)特性说明:
无WCFS的系统下,系统中存在一组优先级队列PriQueue,任务根据自己的优先级插入到不同的队列中,任务调度时,拿取优先级最高的队列的队首。
引入WCFS之后,任务将根据自己的优先级以及运行时间,加权计算得到一个虚拟运行时间,根据虚拟运行时间进行优先级排序,达到相对公平的状态。任务调度时,先从CFS队列中获取一个优先级最高的CFS任务CFSTask,然后获取优先级最高的RT任务(非CFS任务)RTTask,比较CFSTask与RTTask的优先级,选择其中优先级更高的执行,如果CFSTask与RTTask的优先级相同,则执行RTTask。RTTask任务和CFS任务都执行完时,CPU才会去执行Idle任务。
使用说明:
打开宏LOSCFG_WCFS_SCHEDULER即可使用CFS特性。目前加权公平调度特性使用黑名单策略,在名单内的任务不参与加权公平调度。配置方式如下(以hiwing为例):
在文件“LiteOS/Huawei_LiteOS/self_src/targets/hiwing/board.c”中的g_rtTaskName数组中进行配置,将不参与加权公平调度的线程名加入数组即可。
示例如下:
CHAR *g_rtTaskName[CFS_TASK_LIST_NUM] = { "app_Task", "SerialShellTask", "SerialEntryTask", };
其他:
目前LiteOS还提供UINT32 LOS_RemoveTaskFromCFS(UINT32 taskid);接口,该接口传入线程id,对应id的线程将被加入WCFS黑名单,成功则返回OK。
说明:
注意:如果该线程已经不参与WCFS,或者该线程为IdleTask,也返回OK。
LiteOS任务的大多数状态由内核维护,唯有自删除状态对用户可见,需要用户在创建任务时传入如下定义内容:
用户在调用创建任务接口时,可以将创建任务的TSK_INIT_PARAM_S参数的uwResved域设置为LOS_TASK_STATUS_DETACHED(detached属性)或者LOS_TASK_STATUS_JOINABLE(joinable属性)。
须知: 任务joinable属性受LOSCFG_TASK_JOINABLE开关影响。
打开菜单(如何打开请参见“打开menuconfig菜单”),LOSCFG_TASK_JOINABLE配置位于Kernel ---> Basic Config ---> Task --->Enable Join/Detach mechanism。
LOSCFG_TASK_JOINABLE打开,只有将任务状态设置为LOS_TASK_STATUS_JOINABLE才能设置为joinable属性的任务,joinable属性的任务不会自删除,需要用户通过调用LOS_TaskJoin函数去回收joinable属性任务的资源,或者通过调用LOS_TaskDetach接口设置任务为detached属性,detached属性的任务运行完成之后会自动回收资源。
LOSCFG_TASK_JOINABLE关闭,任务只有detached属性,不管TSK_INIT_PARAM_S参数的uwResved域是否设置为LOS_TASK_STATUS_JOINABLE。
LOS_TaskDelete接口无法回收joinable属性的任务资源,只能通过LOS_TaskJoin接口。
LiteOS任务的优先级当前支持范围为(LOS_TASK_PRIORITY_HIGHEST ~ LOS_TASK_PRIORITY_LOWEST),数值越小,优先级越高。
创建任务、删除任务、挂起任务、恢复任务、延时任务等操作存在失败的可能,失败时会返回对应的错误码,以便快速定位错误原因。
创建一个由用户自行传入任务栈的任务时,任务栈起始地址或任务栈大小没有按照LOSCFG_STACK_POINT_ALIGN_SIZE对齐。 |
|||
|
|||
检查是否对一个处于僵尸状态的任务进行删除、设置优先级/属性/CPU亲和性、suspend、resume,不要去操作一个僵尸状态的任务。 |
须知:
错误码定义见“错误码简介”错误码简介。8~15位代表的所属模块为任务模块,值为0x02。
任务模块中的错误码序号 0x16、0x1C,未被定义,不可用。
本部分以创建任务为例,讲解开发流程。
打开菜单,进入Kernel ---> Basic Config ---> Task菜单,完成任务模块的配置。
使能后,任务参数使用旧方式UINTPTR auwArgs[4],否则使用新的任务参数VOID *pArgs。建议关闭此开关,使用新的任务参数。
打开菜单,进入Kernel ---> Basic Config ---> Enable Scheduler菜单,完成任务调度相关配置。
锁任务调度LOS_TaskLock,防止高优先级任务调度。
创建任务LOS_TaskCreate,或静态创建任务LOS_TaskCreateStatic(需要打开LOSCFG_TASK_STACK_STATIC_ALLOCATION宏)。
解锁任务LOS_TaskUnlock,让任务按照优先级进行调度。
延时任务LOS_TaskDelay,任务延时等待。
挂起指定的任务LOS_TaskSuspend,任务挂起等待恢复操作。
恢复挂起的任务LOS_TaskResume。
无。
OsDbgTskInfoGet函数:调用OsDbgTskInfoGet函数,传参为taskId,将展示该任务的任务名、入口函数、taskId、优先级、当前状态、栈大小、使用峰值、sp指针位置、栈顶、CPUP状态(启用CPUP特性时打印)等。传参为0xFFFFFFFF,将展示所有任务的信息。
开启加权公平调度特性时,参与公平调度的任务优先级后,会带有[CFS]标志。
开启共享栈特性时,参与共享栈的任务的栈大小、栈顶、峰值相同。
注意事项¶
任务创建:
任务名是指针,并没有分配空间,在设置任务名时,禁止将局部变量的地址赋值给任务名指针。
创建任务时,会按照16Byte大小或者sizeof(UINTPTR) * 2对齐的方式为任务栈分配内存。确定任务栈大小的原则是:够用就行,多了浪费,少了任务栈溢出。
锁任务:
锁任务调度,并不关中断,因此任务仍可被中断打断。
锁任务调度必须和解锁任务调度配合使用。
任务优先级:
设置任务优先级时可能会发生任务调度。
LOS_CurTaskPriSet和LOS_TaskPriSet接口不能在中断中使用,也不能用于修改软件定时器任务的优先级。
LOS_TaskPriGet接口传入的task ID对应的任务未创建或者超过最大任务数,统一返回0xffff。
禁止操作:
挂起当前任务时,如果任务已经被锁定,则无法挂起。
idle任务及软件定时器任务不能被挂起或者删除。
在中断处理函数中或者在锁任务的情况下,执行LOS_TaskDelay会失败。
任务资源:
执行Idle任务时,会对之前已删除任务的任务控制块和任务栈进行回收。
可配置的系统最大任务数是指整个系统的任务总个数,而非用户能使用的任务个数。例如:系统软件定时器多占用一个任务资源,那么用户能使用的任务资源就会减少一个。
在删除任务时要保证任务申请的资源(如互斥锁、信号量等)已被释放。
编程实例¶
本实例介绍基本的任务操作方法,包含2个不同优先级任务的创建、任务延时、任务锁与解锁调度、挂起和恢复等操作,并阐述任务优先级调度的机制以及各接口的应用。
前提条件:在menuconfig菜单中完成任务模块的配置。
UINT32 g_taskHiId;
UINT32 g_taskLoId;
#define TSK_PRIOR_HI 4
#define TSK_PRIOR_LO 5
UINT32 Example_TaskHi(VOID)
{
UINT32 ret;
printf("Enter TaskHi Handler.\r\n");
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(g_taskLoId任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
printf("Delay Task Failed.\r\n");
return LOS_NOK;
}
/* 2个Tick时间到了后,该任务恢复,继续执行 */
printf("TaskHi LOS_TaskDelay Done.\r\n");
/* 挂起自身任务 */
ret = LOS_TaskSuspend(g_taskHiId);
if (ret != LOS_OK) {
printf("Suspend TaskHi Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskResume Success.\r\n");
return ret;
}
/* 低优先级任务入口函数 */
UINT32 Example_TaskLo(VOID)
{
UINT32 ret;
printf("Enter TaskLo Handler.\r\n");
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(背景任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
printf("Delay TaskLo Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskSuspend Success.\r\n");
/* 恢复被挂起的任务g_taskHiId */
ret = LOS_TaskResume(g_taskHiId);
if (ret != LOS_OK) {
printf("Resume TaskHi Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskDelete Success.\r\n");
return ret;
}
/* 任务测试入口函数,创建两个不同优先级的任务 */
UINT32 Example_TskCaseEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam;
/* 锁任务调度,防止新创建的任务比本任务高而发生调度 */
LOS_TaskLock();
printf("LOS_TaskLock() Success!\r\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_TaskHi;
initParam.usTaskPrio = TSK_PRIOR_HI;
initParam.pcName = "TaskHi";
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
/* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskHiId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_TaskHi create Failed!\r\n");
return LOS_NOK;
}
printf("Example_TaskHi create Success!\r\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_TaskLo;
initParam.usTaskPrio = TSK_PRIOR_LO;
initParam.pcName = "TaskLo";
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
/* 创建低优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskLoId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_TaskLo create Failed!\r\n");
return LOS_NOK;
}
printf("Example_TaskLo create Success!\r\n");
/* 解锁任务调度,此时会发生任务调度,执行就绪队列中最高优先级任务 */
LOS_TaskUnlock();
return LOS_OK;
}
编译运行得到的结果为:
LOS_TaskLock() Success!
Example_TaskHi create Success!
Example_TaskLo create Success!
Enter TaskHi Handler.
Enter TaskLo Handler.
TaskHi LOS_TaskDelay Done.
TaskHi LOS_TaskSuspend Success.
TaskHi LOS_TaskResume Success.
TaskHi LOS_TaskDelete Success.
内存¶
概述¶
内存管理模块用于管理系统的内存资源,它是操作系统的核心模块之一,主要包括内存的初始化、分配以及释放。
在系统运行过程中,内存管理模块通过对内存的申请/释放来管理用户和OS对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统的内存碎片问题。
LiteOS的内存管理分为静态内存管理和动态内存管理。
动态内存管理:在动态内存池中分配用户指定大小的内存块,支持bestfit(也称为dlink)和bestfit_little两种内存管理算法。
优点:按需分配。
缺点:内存池中可能出现碎片。
静态内存管理:在静态内存池中分配用户初始化时预设(固定)大小的内存块。
优点:分配和释放效率高,静态内存池中无碎片。
缺点:只能申请到初始化预设大小的内存块,不能按需申请。
动态内存管理,即在内存资源充足的情况下,根据用户需求从系统配置的一块比较大的连续内存(内存池,也是堆内存)中分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。
与静态内存相比,动态内存管理的优点是按需分配,缺点是内存池中容易出现碎片。
bestfit内存管理结构如图1所示。
第一部分
堆内存(也称内存池)的起始地址 及堆区域总大小。
第二部分
本身是一个数组,每个元素是一个双向链表,所有free节点的控制头都会被分类挂在这个数组的双向链表中。
假设内存允许的最小节点为2min字节,则数组的第一个双向链表存储的是所有size为2min<=size< 2min+1的free节点,第二个双向链表存储的是所有size为2min+1<=size< 2min+2的free节点,依次类推第n个双向链表存储的是所有size为2min+n-1<=size< 2min+n的free节点。每次申请内存的时候,会从这个数组检索最合适大小的free节点以分配内存。每次释放内存时,会将该内存作为free节点存储至这个数组以便下次再使用。
第三部分
占用内存池极大部分的空间,是用于存放各节点的实际区域。以下是LosMemDynNode节点结构体申明以及简单介绍:
typedef struct { union { LOS_DL_LIST freeNodeInfo; /* Free memory node */ struct { UINT32 magic; UINT32 taskId : 16; }; }; struct tagLosMemDynNode *preNode; UINT32 sizeAndFlag; } LosMemCtlNode; typedef struct tagLosMemDynNode { LosMemCtlNode selfNode; } LosMemDynNode;
当申请到的节点包含的数据空间首地址不符合对齐要求时,需要进行对齐,可以通过增加Gap域确保返回的指针符合对齐要求。
bestfit_little算法也是一种最佳适配算法,相比于bestfit算法,少掉了图1中的第二部分,因此体积更小,但是申请释放性能不如bestfit。一般建议在内存空间充裕的情况下,选择bestfit算法。
最佳适配算法使得每次分配内存时,都会选择内存池中最小最适合的内存块进行分配,而slab机制可以用于分配固定大小的内存块,从而减小产生内存碎片的可能性。
LiteOS内存管理中的slab机制支持配置slab class数目及每个class的最大空间。
现以内存池中共有4个slab class,每个slab class的最大空间为512Byte为例说明slab机制。这4个slab class是从内存池中按照最佳适配算法分配出来的。第一个slab class被分为32个16Byte的slab块,第二个slab class被分为16个32Byte的slab块,第三个slab class被分为8个64Byte的slab块,第四个slab class被分为4个128Byte的slab块。
初始化内存模块时,首先初始化内存池,然后在初始化后的内存池中按照最佳适配算法申请4个slab class,再逐个按照slab内存管理机制初始化4个slab class。
每次申请内存时,先在满足申请大小的最佳slab class中申请(比如用户申请20Byte内存,就在slab块大小为32Byte的slab class中申请),如果申请成功,就将slab内存块整块返回给用户,释放时整块回收。需要注意的是,如果满足条件的slab class中已无可以分配的内存块,则从内存池中按照最佳适配算法申请,而不会继续从有着更大slab块空间的slab class中申请。释放内存时,先检查释放的内存块是否属于slab class,如果是则还回对应的slab class中,否则还回内存池中。
静态内存实质上是一个静态数组,静态内存池内的块大小在初始化时设定,初始化后块大小不可变更。
静态内存池由一个控制块和若干相同大小的内存块构成。控制块位于内存池头部,用于内存块管理。内存块的申请和释放以块大小为粒度。
动态内存¶
开发指导¶
动态内存管理的主要工作是动态分配并管理用户申请到的内存区间。
动态内存管理主要用于用户需要使用大小不等的内存块的场景。当用户需要使用内存时,可以通过操作系统的动态内存申请函数索取指定大小的内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用。
LiteOS系统中的动态内存管理模块为用户提供下面几种功能,接口详细信息可以查看API参考。
须知:
动态内存提供了内存调测功能,具体使用方法见“内存调测方法”。
上述接口中,通过宏开关控制的都是内存调测功能相关的接口。
通过LOS_MemAllocAlign/LOS_MemMallocAlign申请的内存进行LOS_MemRealloc/LOS_MemMrealloc操作后,不能保障新的内存首地址保持对齐。
对于bestfit_little算法,不支持对LOS_MemAllocAlign申请的内存进行LOS_MemRealloc操作,否则将返回失败。
对于多内存池申请、释放功能仅支持在bestfit算法下使用。
说明: LiteOS提供了多模块内存统计功能,该功能基于普通内存接口的封装接口,增加模块ID作为入参。不同业务模块进行内存操作时,调用对应封装接口,可统计各模块的内存使用情况,并通过模块ID获取指定模块的内存使用情况。 应用场景:系统业务模块化清晰,用户需统计各模块的内存占用情况。 依赖:目前只有bestfit内存管理算法支持该功能,需要使能LOSCFG_KERNEL_MEM_BESTFIT。 使用方法:
该功能配置项为LOSCFG_MEM_MUL_MODULE,配置路径是Kernel ---> Memory Management ---> Dynamic Memory Management ---> Enable Memory module statistics;
每个业务模块配置唯一module ID,模块代码中在内存操作时调用对应接口,并传入相应模块ID;
通过LOS_MemMusedGet接口获取指定模块的内存使用情况,可用于模块内存占用优化分析。 注意事项:
模块ID由宏MEM_MODULE_MAX限定,当系统模块个数超过该值时,需通过配置LOSCFG_MEM_MODULE_MAX来修改MEM_MODULE_MAX的大小,配置路径:Kernel ---> Memory Management --->Enable Memory module statistics ---> Max Memory Module Number。
模块中所有内存操作都需调用LOS_MemM开头的接口,否则可能导致统计不准确。
本节介绍使用动态内存的典型场景开发流程。
在“los_config.h”文件中配置项动态内存池起始地址与大小。
OS_SYS_MEM_ADDR:一般使用默认值即可。
OS_SYS_MEM_SIZE:一般使用默认值即可。
打开菜单,进入Kernel ---> Memory Management菜单,完成动态内存管理模块的配置。
LOSCFG_MEM_MUL_POOL和LOSCFG_KERNEL_MEM_BESTFIT且关闭LOSCFG_EXC_INTERACTION
初始化LOS_MemInit。
初始一个内存池后如图,生成一个EndNode,并且剩余的内存全部被标记为FreeNode节点。
说明:
EndNode作为内存池末尾的节点,size为0。
申请任意大小的动态内存LOS_MemAlloc。
判断动态内存池中是否存在申请量大小的空间,若存在,则划出一块内存块,以指针形式返回;若不存在,返回NULL。
调用三次LOS_MemAlloc函数可以创建三个节点,假设分别为UsedA、UsedB、UsedC,大小分别为sizeA、sizeB、sizeC。因为刚初始化内存池的时候只有一个大的FreeNode,所以这些内存块是从这个FreeNode中切割出来的。

当内存池中存在多个FreeNode的时候进行malloc,将会适配最合适大小的FreeNode用来新建内存块,减少内存碎片。若新建的内存块不等于被使用的FreeNode的大小,则在新建内存块后,多余的内存又会被标记为一个新的FreeNode。
释放动态内存LOS_MemFree。回收内存块,供下一次使用。
假设调用LOS_MemFree释放内存块UsedB,则会回收内存块UsedB,并且将其标记为FreeNode。在回收内存块时,相邻的FreeNode会自动合并。

无。
注意事项¶
内存分配:
对齐分配内存接口LOS_MemAllocAlign/LOS_MemMallocAlign由于需要进行地址对齐,可能会额外消耗部分内存,因此存在一些遗失内存。当系统释放该对齐内存时,会同时回收因对齐导致的遗失内存。
重新分配内存接口LOS_MemRealloc/LOS_MemMrealloc如果分配成功,系统会自己判定是否需要释放原来申请的内存,并返回重新分配的内存地址。如果重新分配失败,原来的内存保持不变,并返回NULL。禁止使用pPtr = LOS_MemRealloc(pool, pPtr, uwSize),即:不能使用原来的旧内存地址pPtr变量来接收返回值。
内存释放:
对同一块内存多次调用LOS_MemFree/LOS_MemMfree时,第一次会返回成功,但对同一块内存多次重复释放会导致非法指针操作,结果不可预知。
其他限制:
由于动态内存管理需要管理控制块数据结构来管理内存,这些数据结构会额外消耗内存,故实际用户可使用内存总量小于配置项OS_SYS_MEM_SIZE的大小。
由于动态内存管理的内存节点控制块结构体LosMemDynNode中,成员sizeAndFlag的数据类型为UINT32,高两位为标志位,余下的30位表示内存结点大小,因此用户初始化内存池的大小不能超过1G,否则会出现不可预知的结果。
编程实例¶
普通内存使用实例¶
前提条件:在menuconfig菜单中完成动态内存的配置。
本实例执行以下步骤:
初始化一个动态内存池。
从动态内存池中申请一个内存块。
在内存块中存放一个数据。
打印出内存块中的数据。
释放该内存块。
#define TEST_POOL_SIZE (2*1024*1024)
UINT8 g_testPool[TEST_POOL_SIZE];
VOID Example_DynMem(VOID)
{
UINT32 *mem = NULL;
UINT32 ret;
ret = LOS_MemInit(g_testPool, TEST_POOL_SIZE);
if (LOS_OK == ret) {
dprintf("内存池初始化成功!\n");
} else {
dprintf("内存池初始化失败!\n");
return;
}
/*分配内存*/
mem = (UINT32 *)LOS_MemAlloc(g_testPool, 4);
if (NULL == mem) {
dprintf("内存分配失败!\n");
return;
}
dprintf("内存分配成功\n");
/*赋值*/
*mem = 828;
dprintf("*mem = %d\n", *mem);
/*释放内存*/
ret = LOS_MemFree(g_testPool, mem);
if (LOS_OK == ret) {
dprintf("内存释放成功!\n");
} else {
dprintf("内存释放失败!\n");
}
return;
}
编译运行得到的结果为:
内存池初始化成功!
内存分配成功
*mem = 828
内存释放成功!
多模块内存使用实例¶
前提条件:在menuconfig菜单中完成多模块内存统计的配置。
本实例执行以下步骤:
从动态内存池中为模块0申请一个内存块。
获取模块0的内存使用量。
从动态内存池中为模块1申请一个内存块。
获取模块1的内存使用量。
释放模块0和模块1的内存。
void test(void)
{
void *ptr = NULL;
void *ptrTmp = NULL;
UINT32 size = 0x10;
UINT32 module = 0;
UINT32 memUsed = 0;
ptr = LOS_MemMalloc(OS_SYS_MEM_ADDR, size, module);
if (ptr == NULL) {
PRINTK("module %d malloc failed\n", module);
} else {
PRINTK("module %d malloc successed\n", module);
}
memUsed = LOS_MemMusedGet(module);
PRINTK("module %d mem used size %d\n", module, memUsed);
module = 1;
ptrTmp = LOS_MemMalloc(OS_SYS_MEM_ADDR, size, module);
if (ptrTmp == NULL) {
PRINTK("module %d malloc failed\n", module);
} else {
PRINTK("module %d malloc successed\n", module);
}
memUsed = LOS_MemMusedGet(module);
PRINTK("module %d mem used size %d\n", module, memUsed);
module = 0;
(VOID)LOS_MemMfree(OS_SYS_MEM_ADDR, ptr, module);
module = 1;
(VOID)LOS_MemMfree(OS_SYS_MEM_ADDR, ptrTmp, module);
}
编译运行得到的结果为:
module 0 malloc successed
module 0 mem used size 32
module 1 malloc successed
module 1 mem used size 32
静态内存¶
开发指导¶
当用户需要使用固定长度的内存时,可以通过静态内存分配的方式获取内存,一旦使用完毕,通过静态内存释放函数归还所占用内存,使之可以重复使用。
LiteOS的静态内存管理主要为用户提供以下功能,接口详细信息可以查看API参考。
打印指定静态内存池所有节点信息(打印等级是LOS_INFO_LEVEL),包括内存池起始地址、内存块大小、总内存块数量、每个空闲内存块的起始地址、所有内存块的起始地址。 |
本节介绍使用静态内存的典型场景开发流程。
打开菜单,进入Kernel ---> Memory Management菜单,完成静态内存管理模块的配置。
规划一片内存区域作为静态内存池。
调用LOS_MemboxInit初始化静态内存池。
初始化会将入参指定的内存区域分割为N块(N值取决于静态内存总大小和块大小),将所有内存块挂到空闲链表,在内存起始处放置控制头。
调用LOS_MemboxAlloc接口分配静态内存。
系统将会从空闲链表中获取第一个空闲块,并返回该内存块的起始地址。
调用LOS_MemboxClr接口。
将入参地址对应的内存块清零。
调用LOS_MemboxFree接口。
将该内存块加入空闲链表。
无。
注意事项¶
如果静态内存池区域是通过动态内存分配方式获得的,在不需要该静态内存池时,应释放该段内存,以避免内存泄露。
编程实例¶
前提条件:在menuconfig菜单中完成静态内存的配置。
本实例执行以下步骤:
初始化一个静态内存池。
从静态内存池中申请一块静态内存。
在内存块存放一个数据。
打印出内存块中的数据。
清除内存块中的数据。
释放该内存块。
VOID Example_StaticMem(VOID)
{
UINT32 *mem = NULL;
UINT32 blkSize = 10;
UINT32 boxSize = 100;
UINT32 boxMem[1000];
UINT32 ret;
ret = LOS_MemboxInit(&boxMem[0], boxSize, blkSize);
if(ret != LOS_OK) {
printf("内存池初始化失败!\n");
return;
} else {
printf("内存池初始化成功!\n");
}
/*申请内存块*/
mem = (UINT32 *)LOS_MemboxAlloc(boxMem);
if (NULL == mem) {
printf("内存分配失败!\n");
return;
}
printf("内存分配成功\n");
/*赋值*/
*mem = 828;
printf("*mem = %d\n", *mem);
/*清除内存内容*/
LOS_MemboxClr(boxMem, mem);
printf("清除内存内容成功\n *mem = %d\n", *mem);
/*释放内存*/
ret = LOS_MemboxFree(boxMem, mem);
if (LOS_OK == ret) {
printf("内存释放成功!\n");
} else {
printf("内存释放失败!\n");
}
return;
}
编译运行得到的结果为:
内存池初始化成功!
内存分配成功
*mem = 828
清除内存内容成功
*mem = 0
内存释放成功!
中断¶
概述¶
中断是指出现需要时,CPU暂停执行当前程序转而执行新程序的过程。即在程序运行过程中,出现了一个必须由CPU立即处理的事务。此时,CPU暂时中止当前程序的执行转而处理这个事务,这个过程就叫做中断。
外设可以在没有CPU介入的情况下完成一定的工作,但某些情况下也需要CPU为其执行一定的工作。通过中断机制,在外设不需要CPU介入时,CPU可以执行其它任务;而当外设需要CPU时,将通过产生中断信号使CPU立即中断当前任务来响应中断请求。这样可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,有效提高系统实时性以及执行效率。
LiteOS的中断特性:
中断嵌套,即高优先级的中断可抢占低优先级的中断,且可配置。
使用独立中断栈,可配置。
可配置支持的中断优先级个数。
可配置支持的中断数。
中断底半部,且可配置。
与中断相关的硬件可以划分为三类:设备、中断控制器、CPU本身。
设备
发起中断的源,当设备需要请求CPU时,产生一个中断信号,该信号连接至中断控制器。
中断控制器
中断控制器是CPU众多外设中的一个,它一方面接收其它外设中断引脚的输入,另一方面,它会发出中断信号给CPU。可以通过对中断控制器编程来打开和关闭中断源、设置中断源的优先级和触发方式。常用的中断控制器有VIC(Vector Interrupt Controller)、GIC(General Interrupt Controller)、PLIC(Platform-Level Interrupt Controller)和CLIC(Core-Local Interrupt Controller)。在RISC-V中使用的中断控制器是PLIC和CLIC。
CPU
CPU会响应中断源的请求,中断当前正在执行的任务,转而执行中断处理程序。
中断号:
每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。
中断请求:
“紧急事件”需向CPU提出申请(发一个电脉冲信号),要求中断,及要求CPU暂停当前执行的任务,转而处理该“紧急事件”,这一申请过程称为中断请求。
中断优先级:
为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。
中断处理程序:
当外设产生中断请求后,CPU暂停当前的任务,转而响应中断申请,即执行中断处理程序。产生中断的每个设备都有相应的中断处理程序。
中断嵌套:
中断嵌套也称为中断抢占,指的是正在执行一个中断处理程序时,如果有另一个优先级更高的中断源提出中断请求,这时会暂时终止当前正在执行的优先级较低的中断源的中断处理程序,转而去处理更高优先级的中断请求,待处理完毕,再返回到之前被中断的处理程序中继续执行。
中断触发:
中断源向中断控制器发送中断信号,中断控制器对中断进行仲裁,确定优先级,将中断信号送给CPU。中断源产生中断信号的时候,会将中断触发器置“1”,表明该中断源产生了中断,要求CPU去响应该中断。
中断触发类型:
外部中断申请通过一个物理信号发送到NVIC/GIC,可以是电平触发或边沿触发。
中断向量:
中断服务程序的入口地址。
中断向量表:
存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。
中断共享:
当外设较少时,可以实现一个外设对应一个中断号,但为了支持更多的硬件设备,可以让多个设备共享一个中断号,共享同一个中断号的中断处理程序形成一个链表。当外部设备产生中断申请时,系统会遍历执行中断号对应的中断处理程序链表直到找到对应设备的中断处理程序。在遍历执行过程中,各中断处理程序可以通过检测设备ID,判断是否是这个中断处理程序对应的设备产生的中断。
核间中断:
对于多核系统,中断控制器允许一个CPU的硬件线程去中断其他CPU的硬件线程,这种方式被称为核间中断。核间中断的实现基础是多CPU内存共享,采用核间中断可以减少某个CPU负荷过大,有效提升系统效率。目前只有GIC中断控制器与LingLong中断控制器支持核间中断。
中断底半部:
区别于中断处理程序,中断底半部运行在任务上下文,用户可在中断处理程序中,将实时性要求不高,非紧急的业务添加到中断底半部执行。
LiteOS的中断嵌套:
GIC与NVIC的中断嵌套由硬件实现。
RISC-V的中断嵌套实现机制:中断嵌套下,中断A触发后会将当前的操作进行压栈,调用中断处理程序前,将MIE设置为1,允许新的中断被响应。在A执行中断处理程序的过程中,如果有更高优先级的中断B被触发,B会将当前的操作即中断A相关的操作进行压栈,然后执行B的中断处理程序。待B的中断处理程序执行完后,会暂时的将mstatus寄存器中的MIE域置为0,关闭中断响应,将中断A相关的操作进行出栈,将MIE设置为1,允许处理器再次响应中断,中断B结束,继续执行中断A。
LiteOS的中断底半部:
中断底半部依赖链表、任务与事件机制。
在中断处理程序中,用户添加底半部处理函数到队列中,待中断退出后,在中断底半部任务中逐个执行用户添加的底半部处理函数。
开发指导¶
当有中断请求产生时,CPU暂停当前的任务转而去响应外设请求。根据需要,用户通过中断申请,注册中断处理程序,可以指定CPU响应中断请求时所执行的具体操作。
LiteOS的中断模块为用户提供下面几种功能,接口详细信息可以查看API参考。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为中断模块,值为0x09。
打开菜单,进入Kernel ---> Interrupt Management菜单,完成中断模块的配置。
依赖CPU架构,目前Cortex-A、Cortex-R、ARM64、LingLong支持可选,RISC-V默认使用独立中断栈
调用中断创建接口LOS_HwiCreate创建中断。
调用LOS_HwiEnable接口使能指定中断。
调用LOS_HwiTrigger接口触发指定中断(该接口通过写中断控制器的相关寄存器模拟外部中断,一般的外设设备,不需要执行这一步)。
在中断处理函数中,调用LOS_HwiBhworkAdd接口添加中断底半部处理函数(如果无底半部处理函数,则可以不执行这一步)。
调用LOS_HwiDisable接口屏蔽指定中断,此接口根据实际情况使用,判断是否需要屏蔽中断。
调用LOS_HwiDelete接口删除指定中断,此接口根据实际情况使用,判断是否需要删除中断。
注意事项¶
中断处理:
中断处理程序耗时不能过长,否则会影响CPU对中断的及时响应。
中断响应过程中不能执行引起调度的函数。
中断共享:
中断共享机制,支持不同的设备使用相同的中断号注册同一中断处理程序,但中断处理程序的入参pDevId(设备号)必须唯一,代表不同的设备。即同一中断号,同一dev只能挂载一次;但同一中断号,同一中断处理程序,dev不同则可以重复挂载。
中断触发:
RISC-V himideerV200不支持LOS_HwiTrigger接口,因为RISC-V himideerV200中断控制器pending寄存器不可写;用户需要模拟中断触发进行测试,请使用软中断或外设中断验证中断触发流程。
中断寄存器状态清除:
RISC-V himideerV200开启中断嵌套时,对于电平触发方式,需要在中断处理程序结束时调用LOS_HwiClear接口,主动清除中断寄存器状态。即对于RISC-V himideerV200,LOS_HwiClear接口只有在中断嵌套场景生效;非嵌套场景用户无需清除中断控制器状态,LOS_HwiClear接口无清除中断状态效果。
中断底半部:
中断底半部用户处理函数中禁止调用延时接口,避免调用阻塞类接口。中断底半部任务运行时机具有不确定性,只有当中断底半部任务优先级最高,才会运行。
兼容接口:
LiteOS有兼容Linux的中断接口,request_irq、disable_irq、enable_irq、free_irq,详见“中断”。开发者可根据自身需要,选择适合的接口。
其他限制:
根据具体硬件,配置支持的最大中断数及可设置的中断优先级个数。
中断恢复LOS_IntRestore()的入参必须是与之对应的LOS_IntLock()的返回值(即关中断之前的CPSR值)。
Cortex-M系列处理器中0-15中断为内部使用,Cortex-A7中0-31中断为内部使用,因此不建议用户去申请和创建。
编程实例¶
本实例实现如下功能:
创建中断。
设置中断亲和性。
使能中断。
触发中断。
屏蔽中断。
删除中断。
前提条件:menuconfig菜单中配置中断使用最大数、配置可设置的中断优先级个数。
代码实现如下:
#include "los_hwi.h"
#include "los_typedef.h"
#include "los_task.h"
STATIC UINT32 g_BhworkCnt = 0;
STATIC VOID HwiBhfunc(VOID *data)
{
UINT32 *count = (UINT32 *data);
(*count)++;
}
STATIC VOID HwiUsrIrq(VOID)
{
UINT32 ret;
ret = LOS_HwiBhworkAdd(HwiBhfunc, &g_BhworkCnt);
if (ret != LOS_OK) {
printf("\n hwi bhwork add failed, %x \n", ret);
}
printf("\n in the func HwiUsrIrq \n");
}
/* cpu0 trigger, cpu0 response */
UINT32 It_Hwi_001(VOID)
{
UINT32 ret;
HWI_HANDLE_T irqNum = 26;
HWI_PRIOR_T irqPri = 0x3;
ret = LOS_HwiCreate(irqNum, irqPri, 0, (HWI_PROC_FUNC)HwiUsrIrq, 0);
if (ret != LOS_OK) {
return LOS_NOK;
}
#ifdef LOSCFG_KERNEL_SMP
ret = LOS_HwiSetAffinity(irqNum, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
if (ret != LOS_OK) {
return LOS_NOK;
}
#endif
ret = LOS_HwiEnable(irqNum);
if (ret != LOS_OK) {
return LOS_NOK;
}
ret = LOS_HwiTrigger(irqNum);
if (ret != LOS_OK) {
return LOS_NOK;
}
LOS_TaskDelay(1);
ret = LOS_HwiDisable(irqNum);
if (ret != LOS_OK) {
return LOS_NOK;
}
ret = LOS_HwiDelete(irqNum, NULL);
if (ret != LOS_OK) {
return LOS_NOK;
}
return LOS_OK;
}
异常接管¶
概述¶
异常接管是操作系统对运行期间发生的异常情况(芯片硬件异常)进行处理的一系列动作,例如打印异常发生时当前函数的调用栈信息、CPU现场信息、任务的堆栈情况等。
异常接管作为一种调测手段,可以在系统发生异常时给用户提供有用的异常信息,譬如异常类型、发生异常时的系统状态等,方便用户定位分析问题。
LiteOS的异常接管,在系统发生异常时的处理动作为:显示异常发生时正在运行的任务信息(包括任务名、任务号、堆栈大小等)以及CPU现场等信息。针对某些RISC-V架构的芯片,对内存size要求较高的场景,LiteOS提供了极小特性宏LOSCFG_EXC_SIMPLE_INFO(menuconfig菜单项为:Kernel ---> Exception Management ---> Enable Exception Simple Info),用于裁剪多余的异常提示字符串信息,但是仍然保留发生异常时的CPU执行环境的所有信息。
每个函数都有自己的栈空间,称为栈帧。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长。
以RISC-V 32位架构为例,每个栈帧中通常保存了返回地址(RA)、栈指针(SP)和帧指针(FP)的历史值,用于回溯函数调用关系。
堆栈分析:
RA寄存器(x1):返回地址寄存器,指向当前函数返回后要执行的下一条指令地址。
SP寄存器(x2):栈指针寄存器,指向当前栈帧的栈顶。
FP寄存器(x8):可用作帧指针寄存器(frame pointer),在编译时开启特定选项后,它指向当前函数的父函数栈帧的起始地址。RISC-V中x8也被称为s0或fp。
默认情况下,GCC编译器可能会将x8作为通用寄存器使用(例如用于保存局部变量),此时无法直接通过FP回溯栈帧。为了支持调用栈解析,需要在编译参数中添加“-fno-omit-frame-pointer”,强制编译器将x8用作帧指针。这样,每个函数入口会保存上一帧的FP和RA,形成链式结构。
当系统发生异常时,CPU会打印异常现场的寄存器值(包括ra、sp、fp 等)。通过当前栈帧中保存的fp可以找到父函数的栈帧,进而从父栈帧中取出其保存的ra(即父函数的返回地址)以及它的fp,如此迭代,即可追溯出完整的函数调用栈,帮助开发者快速定位异常原因。
堆栈分析原理如下所示,实际堆栈信息根据不同CPU架构有所差异,此处仅做示意。
假设函数调用链:main函数调用func1,func1又调用func2。此时正在执行func2,其栈帧如下(地址从高到低,每格4字节):
关键点说明:
sp(栈指针):始终指向当前栈帧的最低地址(即最后分配的空间)。在func2执行时,sp指向func2局部变量下方的空闲区域(或已占用的底部)。
fp(帧指针):指向当前栈帧中保存上一帧fp的位置。例如,func2的fp指向的地址存放着func1的fp值,而 func1的fp又指向main的fp位置,从而形成链表。
返回地址ra:每个函数在调用子函数前,会将当前的ra保存到自己的栈帧中(通常位于保存的fp上方4字节处,如图1中func2的ra在fp+4位置)。这样,当函数返回时,可以从栈帧中恢复ra。
异常回溯过程:
当发生异常时,CPU会保存当前寄存器值(包括sp、fp、ra 等)。假设当前在func2中发生异常,我们可以通过以下步骤回溯调用栈:
这种链式结构让调试器和异常处理程序能够精确还原函数调用序列,快速定位问题发生的上下文。
开发指导¶
异常接管对系统运行期间发生的芯片硬件异常进行处理,不同芯片的异常类型存在差异,具体异常类型可以查看芯片手册。
LiteOS的异常模块为用户提供下面几种功能,接口详细信息可以查看API参考。
异常接管一般的定位步骤如下:
打开编译后生成的镜像反汇编(asm)文件。
搜索PC指针(指向当前正在执行的指令)在asm中的位置,找到发生异常的函数。
重复3,得到函数间的调用关系,找到异常原因。
具体的定位方法会在实例中举例说明。
打开菜单,进入Kernel ---> Exception Management菜单,完成异常接管模块的配置。
LOSCFG_ARCH_ARM_CORTEX_A 或 LOSCFG_ARCH_ARM_CORTEX_R 或 LOSCFG_ARCH_ARM_926 |
注意事项¶
要查看调用栈信息,必须添加编译选项宏“-fno-omit-frame-pointer”支持stack frame,否则编译时FP寄存器是关闭的。
用户可通过调用ArchSetExcHook和ArchSetNMIHook接口来设定自定义的异常处理函数。然而,在异常处理函数内部必须使用LOS_TaskLock锁定任务调度机制,以避免因触发调度而导致二次异常或其他潜在问题的发生。
编程实例¶
通过错误释放内存,触发系统异常。系统异常被挂起后,能在串口中看到异常调用栈打印信息和关键寄存器信息,如下所示,其中Type表示异常类型,此处值为0x2表示非法指令 ,其它数值可以查看芯片手册。通过这些信息可以定位到异常所在函数和其调用栈关系。

定位步骤如下:
打开编译后生成的.asm 反汇编文件(默认生成在“LiteOS/out/<platform>“”目录下,其中的platform为具体的平台名)。
搜索mepc指针2616ecfa在.asm文件中的位置(去掉0x)。
mepc地址指向发生异常时程序正在执行的指令。在当前执行的二进制文件对应的asm文件中,查找mepc值2616ecfa,找到当前发生异常的文件,得到如下图所示结果。

从图中可以看到:
异常时CPU正在执行的指令unimp。
异常发生在“It_los_task_045.c”文件的“__asm volatile (".word 0x00000000")”语句。
接下来,需要查找谁调用了这个语句。
根据ra返回寄存器值查找调用栈。
从异常信息的backtrace begin开始,打印的是调用栈信息。在asm文件中查找traceback 0~2对应的ra,如下图所示。



可见,是FuncC调用了“__asm volatile (".word 0x00000000")”语句发生异常。依此方法,可得到异常时函数调用关系如下:FuncA(业务函数) ---> FuncB---> FuncC ---> __asm volatile (".word 0x00000000")。
最终,可以通过该方法排查出整个异常调用栈的信息。
错误处理¶
概述¶
错误处理指程序运行错误时,调用错误处理模块的接口函数,上报错误信息,并调用注册的钩子函数进行特定处理,保存现场以便定位问题。
通过错误处理,可以控制和提示程序中的非法输入,防止程序崩溃。
错误处理是一种机制,用于处理异常状况。当程序出现错误时,会显示相应的错误码。此外,如果注册了相应的错误处理函数,则会执行这个函数。
开发指导¶
调用API接口时可能会出现错误,此时接口会返回对应的错误码,以便快速定位错误原因。
错误码是一个32位的无符号整型数,31~24位表示错误等级,23~16位表示错误码标志(当前该标志值为0),15~8位代表错误码所属模块,7~0位表示错误码序号。
例如将任务模块中的错误码LOS_ERRNO_TSK_NO_MEMORY定义为致命级别的错误,模块ID为LOS_MOD_TSK,错误码序号为0,其定义如下:
#define LOS_ERRNO_TSK_NO_MEMORY LOS_ERRNO_OS_FATAL(LOS_MOD_TSK, 0x00)
#define LOS_ERRNO_OS_FATAL(MID, ERRNO) \
(LOS_ERRTYPE_FATAL | LOS_ERRNO_OS_ID | ((UINT32)(MID) << 8) | ((UINT32)(ERRNO)))
说明:
LOS_ERRTYPE_FATAL:错误等级为致命,值为0x03000000。
LOS_ERRNO_OS_ID:错误码标志,值为0x000000。
MID:所属模块,LOS_MOD_TSK的值为0x2。
ERRNO:错误码序号。 所以LOS_ERRNO_TSK_NO_MEMORY的值为0x03000200。
有时只靠错误码不能快速准确的定位问题,为方便用户分析错误,错误处理模块支持注册错误处理的钩子函数,发生错误时,用户可以调用LOS_ErrHandle接口以执行错误处理函数。
LiteOS的错误处理模块为用户提供下面几个接口,接口详细信息请参见API参考。
须知: 系统内部会在某些难以定位的错误处,主动调用注册的钩子函数(目前只在互斥锁模块和信号量模块中主动调用了钩子函数)。
注意事项¶
系统中有且仅有一个错误处理的钩子函数。当多次注册钩子函数时,最后一次注册的钩子函数会覆盖前一次注册的函数。
编程实例¶
本实例演示功能如下:
注册错误处理钩子函数。
执行错误处理函数。
代码实现如下:
#include "los_err.h"
#include "los_typedef.h"
#include <stdio.h>
void Test_ErrHandle(CHAR *fileName, UINT32 lineNo, UINT32 errorNo, UINT32 paraLen, VOID *para)
{
printf("err handle ok\n");
}
static UINT32 TestCase(VOID)
{
UINT32 errNo = 0;
UINT32 ret;
UINT32 errLine = 16;
LOS_RegErrHandle(Test_ErrHandle);
ret = LOS_ErrHandle("os_unspecific_file", errLine, errNo, 0, NULL);
if (ret != LOS_OK) {
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
HuaWei LiteOS # err handle ok
队列¶
概述¶
队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。
任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。如果将读队列和写队列的超时时间设置为0,则不会挂起任务,接口会直接返回,这就是非阻塞模式。
消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用。
LiteOS中使用队列实现任务异步通信,具有如下特性:
消息以先进先出的方式排队,支持异步读写。
读队列和写队列都支持超时机制。
每读取一条消息,就会将该消息节点设置为空闲。
发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。
一个任务能够从任意一个消息队列接收和发送消息。
多个任务能够从同一个消息队列接收和发送消息。
创建队列时所需的队列空间,默认支持接口内系统自行动态申请内存的方式,同时也支持将用户分配的队列空间作为接口入参传入的方式。
队列控制块:
typedef enum {
OS_QUEUE_READ =0,
OS_QUEUE_WRITE =1,
OS_QUEUE_N_RW =2
} QueueReadWrite;
/**
* Queue information block structure
*/
typedef struct
{
UINT8 *queueHandle; /* 队列指针 */
UINT8 queueState; /* 队列状态 */
UINT8 queueMemType; /* 创建队列时内存分配的方式 */
UINT16 queueLen; /* 队列中消息节点个数,即队列长度 */
UINT16 queueSize; /* 消息节点大小 */
UINT32 queueId; /* 队列ID */
UINT16 queueHead; /* 消息头节点位置(数组下标)*/
UINT16 queueTail; /* 消息尾节点位置(数组下标)*/
UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /* 数组下标0的元素表示队列中可读消息数,
数组下标1的元素表示队列中可写消息数 */
LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /* 读取或写入消息的任务等待链表,
下标0:读取链表,下标1:写入链表 */
LOS_DL_LIST memList; /* CMSIS-RTOS中的MailBox模块使用的内存块链表 */
} LosQueueCB;
每个队列控制块中都含有队列状态,表示该队列的使用情况:
OS_QUEUE_UNUSED:队列没有被使用。
OS_QUEUE_INUSED:队列被使用中。
每个队列控制块中都含有创建队列时的内存分配方式:
OS_QUEUE_ALLOC_DYNAMIC:创建队列时所需的队列空间,由系统自行动态申请内存获取。
OS_QUEUE_ALLOC_STATIC:创建队列时所需的队列空间,由接口调用者自行申请后传入接口。
表 1 队列运作原理描述表
图1对读写队列做了示意,图中只画了尾节点写入方式,没有画头节点写入,但是两者是类似的。
开发指导¶
队列用于任务间通信,可以实现消息的异步处理。同时消息的发送方和接收方不需要彼此联系,两者间是解耦的。
LiteOS中的队列模块提供下面几种功能,接口详细信息可以查看API参考。
获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务、等待mail操作的任务。 |
||
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知:
错误码定义见“错误码简介”。8~15位代表的所属模块为队列模块,值为0x06。
队列模块中的错误码序号0x11、0x14未被定义,不可用。
使用队列模块的典型流程如下:
打开菜单,进入Kernel ---> Enable Queue菜单,完成队列模块的配置。
创建队列。创建成功后,可以得到队列ID。
写队列。
读队列。
获取队列信息。
删除队列。
注意事项¶
队列创建:
系统支持的最大队列数是指:整个系统的队列资源总个数,而非用户能使用的个数。例如:系统软件定时器多占用一个队列资源,那么用户能使用的队列资源就会减少一个。
创建队列时传入的队列名和flags暂时未使用,作为以后的预留参数。
队列读写:
LOS_QueueReadCopy和LOS_QueueWriteCopy及LOS_QueueWriteHeadCopy是一组接口,LOS_QueueRead和LOS_QueueWrite及LOS_QueueWriteHead是一组接口,两组接口需要配套使用。
鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,用户必须保证调用LOS_QueueRead获取到的指针所指向的内存区域在读队列期间没有被异常修改或释放,否则可能导致不可预知的后果。
鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,也就意味着实际写和读的消息长度仅仅是一个指针数据,因此用户使用这组接口之前,需确保创建队列时的消息节点大小为一个指针的长度,避免不必要的浪费和读取失败。
其他:
队列接口函数中的入参timeout是相对时间。
当队列使用结束后,如果存在动态申请的内存,需要及时释放这些内存。
编程实例¶
创建一个队列,两个任务。任务1调用写队列接口发送消息,任务2通过读队列接口接收消息。
通过LOS_TaskCreate创建任务1和任务2。
通过LOS_QueueCreate创建一个消息队列。
在任务1 send_Entry中发送消息。
在任务2 recv_Entry中接收消息。
通过LOS_QueueDelete删除队列。
前提条件:在menuconfig菜单中完成队列模块的配置。
#include "los_task.h"
#include "los_queue.h"
static UINT32 g_queue;
#define BUFFER_LEN 50
VOID send_Entry(VOID)
{
UINT32 i = 0;
UINT32 ret = 0;
CHAR abuf[] = "test is message x";
UINT32 len = sizeof(abuf);
while (i < 5) {
abuf[len -2] = '0' + i;
i++;
ret = LOS_QueueWriteCopy(g_queue, abuf, len, 0);
if(ret != LOS_OK) {
dprintf("send message failure, error: %x\n", ret);
}
LOS_TaskDelay(5);
}
}
VOID recv_Entry(VOID)
{
UINT32 ret = 0;
CHAR readBuf[BUFFER_LEN] = {0};
UINT32 readLen = BUFFER_LEN;
while (1) {
ret = LOS_QueueReadCopy(g_queue, readBuf, &readLen, 0);
if(ret != LOS_OK) {
dprintf("recv message failure, error: %x\n", ret);
break;
}
dprintf("recv message: %s\n", readBuf);
LOS_TaskDelay(5);
}
while (LOS_OK != LOS_QueueDelete(g_queue)) {
LOS_TaskDelay(1);
}
dprintf("delete the queue success!\n");
}
UINT32 Example_CreateTask(VOID)
{
UINT32 ret = 0;
UINT32 task1, task2;
TSK_INIT_PARAM_S initParam;
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)send_Entry;
initParam.usTaskPrio = 9;
initParam.uwStackSize = LOS_TASK_MIN_STACK_SIZE;
initParam.pcName = "sendQueue";
#ifdef LOSCFG_KERNEL_SMP
initParam.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());
#endif
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
LOS_TaskLock();
ret = LOS_TaskCreate(&task1, &initParam);
if(ret != LOS_OK) {
dprintf("create task1 failed, error: %x\n", ret);
return ret;
}
initParam.pcName = "recvQueue";
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)recv_Entry;
ret = LOS_TaskCreate(&task2, &initParam);
if(ret != LOS_OK) {
dprintf("create task2 failed, error: %x\n", ret);
return ret;
}
ret = LOS_QueueCreate("queue", 5, &g_queue, 0, BUFFER_LEN);
if(ret != LOS_OK) {
dprintf("create queue failure, error: %x\n", ret);
}
dprintf("create the queue success!\n");
LOS_TaskUnlock();
return ret;
}
编译运行得到的结果为:
create the queue success!
recv message: test is message 0
recv message: test is message 1
recv message: test is message 2
recv message: test is message 3
recv message: test is message 4
recv message failure, error: 200061d
delete the queue success!
事件¶
概述¶
事件(Event)是一种任务间通信的机制,可用于任务间的同步。
多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。
一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。
多对多同步模型:多个任务等待多个事件的触发。
LiteOS提供的事件具有如下特点:
任务通过创建事件控制块来触发事件或等待事件。
事件间相互独立,内部通过一个32位无符号整型来实现,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。
事件仅用于任务间的同步,不支持数据传输。
若多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。
多个任务可以对同一事件进行读写操作。
支持事件读写超时机制。
事件控制块
/**
* Event control structure
*/
typedef struct tagEvent {
UINT32 uwEventID; /* 事件ID,每一位标识一种事件类型 */
LOS_DL_LIST stEventList; /* 读取事件的任务链表 */
} EVENT_CB_S, *PEVENT_CB_S;
uwEventID:用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共31种事件类型。(第25位系统保留。)
事件读取模式
在读事件时,可以选择读取模式。读取模式如下:
所有事件(LOS_WAITMODE_AND):逻辑与,基于接口传入的事件类型掩码eventMask,只有这些事件都已经发生才能读取成功,否则该任务将阻塞等待或者返回错误码。
任一事件(LOS_WAITMODE_OR):逻辑或,基于接口传入的事件类型掩码eventMask,只要这些事件中有任一种事件发生就可以读取成功,否则该任务将阻塞等待或者返回错误码。
清除事件(LOS_WAITMODE_CLR):这是一种附加读取模式,需要与所有事件模式或任一事件模式结合使用(LOS_WAITMODE_AND | LOS_WAITMODE_CLR或 LOS_WAITMODE_OR | LOS_WAITMODE_CLR)。在这种模式下,当设置的所有事件模式或任一事件模式读取成功后,会自动清除事件控制块中对应的事件类型位。
任务在调用LOS_EventRead接口读事件时,可以根据入参事件掩码类型eventMask读取事件的单个或者多个事件类型。事件读取成功后,如果设置LOS_WAITMODE_CLR会清除已读取到的事件类型,反之不会清除已读到的事件类型,需显式清除。可以通过入参选择读取模式,读取事件掩码类型中所有事件还是读取事件掩码类型中任意事件。
任务在调用LOS_EventWrite接口写事件时,对指定事件控制块写入指定的事件类型,可以一次同时写多个事件类型。写事件会触发任务调度。
任务在调用LOS_EventClear接口清除事件时,根据入参事件和待清除的事件类型,对事件对应位进行清零操作。
开发指导¶
事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。
LiteOS的事件模块为用户提供下面几种功能,接口详细信息可以查看API参考。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为事件模块,值为0x1C。
使用事件模块的典型流程如下:
打开菜单,进入Kernel ---> Enable Event菜单,完成事件模块的配置。
调用事件初始化LOS_EventInit接口,初始化事件等待队列。
写事件LOS_EventWrite或LOS_EventCondWrite,写入指定的事件类型或事件。
读事件LOS_EventRead或LOS_EventCondRead,读取指定的事件类型或条件事件。
清除事件LOS_EventClear,清除指定的事件类型,该接口只适用于事件类型的事件。
销毁事件LOS_EventDestroy。
无。
注意事项¶
在系统初始化之前不能调用读写事件接口。如果调用,系统会运行异常。
在中断中,可以对事件对象进行写操作,但不能进行读操作。
在锁定任务调度状态下,禁止任务阻塞于读事件。
为了区别LOS_EventRead接口返回的是事件还是错误码,事件掩码的第25位不能使用。
LOS_EventClear入参值是要清除的指定事件类型的反码(~events)。
LOS_EventWrite和LOS_EventRead成对使用,LOS_EventCondWrite和LOS_EventCondRead成对使用,这两组接口的区别在于,前面的用于读取或写入指定事件类型的事件;而后面的一组接口,用于读取或写入指定事件条件的事件,事件条件由用户实现,写事件任务不用关心事件条件,而是广播式告知读事件任务,即唤醒读任务,读任务判断读取的事件条件是否成功,这样可能会带来一定的性能开销,但是可简化写事件任务。
编程实例¶
基于事件类型的读写¶
示例中,任务Example_TaskEntry创建一个任务Example_Event,Example_Event读事件阻塞,Example_TaskEntry向该任务写事件。可以通过示例日志中打印的先后顺序理解事件操作时伴随的任务切换。
在任务Example_TaskEntry创建任务Example_Event,其中任务Example_Event优先级高于Example_TaskEntry。
在任务Example_Event中读事件0x00000001,阻塞,发生任务切换,执行任务Example_TaskEntry。
在任务Example_TaskEntry向任务Example_Event写事件0x00000001,发生任务切换,执行任务Example_Event。
Example_Event得以执行,直到任务结束。
Example_TaskEntry得以执行,直到任务结束。
前提条件:在menuconfig菜单中完成事件模块的配置。
代码实现如下:
#include "los_event.h"
#include "los_task.h"
#include "securec.h"
/* 任务ID */
UINT32 g_testTaskId;
/* 事件控制结构体 */
EVENT_CB_S g_exampleEvent;
/* 等待的事件类型 */
#define EVENT_WAIT 0x00000001
/* 用例任务入口函数 */
VOID Example_Event(VOID)
{
UINT32 ret;
UINT32 event;
/* 超时等待方式读事件,超时时间为100 ticks, 若100 ticks后未读取到指定事件,读事件超时,任务直接唤醒 */
printf("Example_Event wait event 0x%x \n", EVENT_WAIT);
event = LOS_EventRead(&g_exampleEvent, EVENT_WAIT, LOS_WAITMODE_AND, 100);
if (event == EVENT_WAIT) {
printf("Example_Event,read event :0x%x\n", event);
} else {
printf("Example_Event,read event timeout\n");
}
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
/* 事件初始化 */
ret = LOS_EventInit(&g_exampleEvent);
if (ret != LOS_OK) {
printf("init event failed .\n");
return -1;
}
/* 创建任务 */
(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Event;
task1.pcName = "EventTsk1";
task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = 5;
ret = LOS_TaskCreate(&g_testTaskId, &task1);
if (ret != LOS_OK) {
printf("task create failed .\n");
return LOS_NOK;
}
/* 写g_testTaskId 等待事件 */
printf("Example_TaskEntry write event .\n");
ret = LOS_EventWrite(&g_exampleEvent, EVENT_WAIT);
if (ret != LOS_OK) {
printf("event write failed .\n");
return LOS_NOK;
}
/* 清标志位 */
printf("EventMask:%d\n", g_exampleEvent.uwEventID);
LOS_EventClear(&g_exampleEvent, ~g_exampleEvent.uwEventID);
printf("EventMask:%d\n", g_exampleEvent.uwEventID);
/* 删除任务 */
ret = LOS_TaskDelete(g_testTaskId);
if (ret != LOS_OK) {
printf("task delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行结果如下:
Example_Event wait event 0x1
Example_TaskEntry write event .
Example_Event,read event :0x1
EventMask:1
EventMask:0
基于事件条件的读写¶
示例中,任务ExampleTaskEntry中创建两个读事件任务readTask1和readTask2和一个写事件任务writeTask,由writeTask依次将readTask1和readTask2等待的条件置为真,并写事件,readTask1和readTask2读取的事件符合预期,结束阻塞。可以通过示例日志中打印的先后顺序理解事件操作时伴随的任务切换。
在任务ExampleTaskEntry中创建两个读事件任务readTask1和readTask2;
任务readTask1等待的条件EventCondition1当前为假,当前未读取到目的事件,且未超时,任务阻塞;
任务readTask2等待的条件EventCondition2当前为假,当前未读取到目的事件,且未超时,任务阻塞;
在任务ExampleTaskEntry中创建写事件任务writeTask;
writeTask将readTask1等待的条件置为真,写事件;
readTask1和readTask2都得到唤醒,但因只有readTask1的条件为真,所以readTask1读事件成功,向下执行,readTask2读事件失败,继续阻塞;
writeTask将readTask2等待的条件置为真,写事件;
readTask2得到唤醒,条件为真,所以readTask2读事件成功,向下执行。
前提条件:在menuconfig菜单中完成事件模块的配置。
代码实现如下:
#include "los_event.h"
#include "los_task.h"
#include "securec.h"
/* 任务ID */
static UINT32 g_readTskId1;
static UINT32 g_readTskId2;
static UINT32 g_writeTskId;
/* g_readTskId1和g_readTskId2等待的条件 */
static int g_testData1 = 0;
static int g_testData2 = 0;
/* 事件控制结构体 */
static EVENT_CB_S g_event;
/* 写事件任务入口函数 */
static VOID WriteTaskFunc(VOID)
{
UINT32 ret;
g_testData1 = 1;
printf("\nWriteTaskFunc: write for ReadTaskFunc1\n\n");
ret = LOS_EventCondWrite(&g_event);
if (ret != LOS_OK) {
printf("WriteTaskFunc failed: ret = %u\n", ret);
return;
}
LOS_TaskDelay(2); /* 2: delay 2 ticks */
printf("\nWriteTaskFunc: write for ReadTaskFunc2\n\n");
g_testData2 = 1;
ret = LOS_EventCondWrite(&g_event);
if (ret != LOS_OK) {
printf("WriteTaskFunc failed: ret = %u\n", ret);
return;
}
LOS_TaskDelay(2); /* 2: delay 2 ticks */
return;
}
/* 读事件任务1预期想读取的事件条件 */
static BOOL EventCondition1(VOID)
{
BOOL ret;
ret = (g_testData1 == 1) ? TRUE : FALSE;
printf("EventCondition1: ret = %s\n", (ret ? "TRUE" : "FALSE"));
return ret;
}
/* 读事件任务1入口函数 */
static VOID ReadTaskFunc1(VOID)
{
UINT32 ret;
ret = LOS_EventCondRead(&g_event, EventCondition1(), LOS_WAIT_FOREVER);
if (ret != LOS_OK) {
printf("ReadTaskFunc1 read failed!\n");
return;
}
printf("ReadTaskFunc1 read succeeded!\n");
return;
}
/* 读事件任务2预期想读取的事件条件 */
static BOOL EventCondition2(VOID)
{
BOOL ret;
ret = (g_testData2 == 1) ? TRUE : FALSE;
printf("EventCondition2: ret = %s\n", (ret ? "TRUE" : "FALSE"));
return ret;
}
/* 读事件任务2入口函数 */
static VOID ReadTaskFunc2(VOID)
{
UINT32 ret;
ret = LOS_EventCondRead(&g_event, EventCondition2(), LOS_WAIT_FOREVER);
if (ret != LOS_OK) {
printf("ReadTaskFunc2 read failed!\n");
return;
}
printf("ReadTaskFunc2 read succeeded!\n");
return;
}
static VOID InitTaskParam(TSK_INIT_PARAM_S *task, char *taskName, TSK_ENTRY_FUNC entry, UINT16 prio, UINT16 affi)
{
task->pcName = taskName;
task->pfnTaskEntry = entry;
task->usTaskPrio = prio;
task->uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task->uwResved = LOS_TASK_STATUS_DETACHED;
#ifdef LOSCFG_KERNEL_SMP
task->usCpuAffiMask = affi;
#endif
}
static UINT32 ExampleTaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S readTask1 = {0};
TSK_INIT_PARAM_S readTask2 = {0};
TSK_INIT_PARAM_S writeTask = {0};
ret = LOS_EventInit(&g_event);
if (ret != LOS_OK) {
return ret;
}
InitTaskParam(&readTask1, "EventReadTsk1", (TSK_ENTRY_FUNC)ReadTaskFunc1, 5, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
ret = LOS_TaskCreate(&g_readTskId1, &readTask1);
if (ret != LOS_OK) {
return ret;
}
InitTaskParam(&readTask2, "EventReadTsk2", (TSK_ENTRY_FUNC)ReadTaskFunc2, 5, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
ret = LOS_TaskCreate(&g_readTskId2, &readTask2);
if (ret != LOS_OK) {
return ret;
}
InitTaskParam(&writeTask, "EventWriteTsk45", (TSK_ENTRY_FUNC)WriteTaskFunc, 5, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
ret = LOS_TaskCreate(&g_writeTskId, &writeTask);
if (ret != LOS_OK) {
return ret;
}
(VOID)LOS_TaskDelay(6); /* 6: delay 6 ticks */
(VOID)LOS_EventDestroy(&g_event);
return LOS_OK;
}
编译运行得到的结果为:
EventCondition1: ret = FALSE
EventCondition2: ret = FALSE
WriteTaskFunc: write for ReadTaskFunc1
EventCondition1: ret = TRUE
ReadTaskFunc1 read succeeded!
EventCondition2: ret = FALSE
WriteTaskFunc: write for ReadTaskFunc2
EventCondition2: ret = TRUE
EventCondition2: ret = TRUE
ReadTaskFunc2 read succeeded!
信号量¶
概述¶
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。
一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况:
0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。
正值,表示该信号量当前可被获取。
以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同:
用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量。二值信号量的计数值最大为1,是一种类似于互斥锁的机制。
用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。
信号量控制块:
/**
* Semaphore control structure.
*/
typedef struct {
UINT8 semStat; /* 是否使用标志位 */
UINT8 semType; /* 信号量类型 */
UINT16 semCount; /* 信号量计数 */
UINT32 semId; /* 信号量索引号 */
LOS_DL_LIST semList; /* 挂接阻塞于该信号量的任务 */
} LosSemCB;
信号量初始化:为配置的N个信号量申请内存(N值可以由用户自行配置,通过LOSCFG_BASE_IPC_SEM_LIMIT宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。
信号量创建:从未使用的信号量链表中获取一个信号量,并设定初值。
信号量申请:若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。
信号量释放:若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。
信号量删除:将正在使用的信号量置为未使用信号量,并挂回到未使用链表。
信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。
开发指导¶
在多任务系统中,信号量是一种非常灵活的同步方式,可以运用在多种场合中,实现锁、同步、资源计数等功能,也能方便的用于任务与任务,中断与任务的同步中。信号量常用于协助一组相互竞争的任务访问共享资源。
LiteOS的信号量模块为用户提供下面几种功能,接口详细信息可以查看API参考。
说明: 信号量有三种申请模式:无阻塞模式、永久阻塞模式、定时阻塞模式。
无阻塞模式:即任务申请信号量时,入参timeout等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。
永久阻塞模式:即任务申请信号量时,入参timeout等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。
定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
调整OS_SYS_MEM_SIZE以确保有足够的内存供信号量使用,或减小系统支持的最大信号量数LOSCFG_BASE_IPC_SEM_LIMIT。 |
||||
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为信号量模块,值为0x07。
信号量的开发典型流程:
打开菜单,进入Kernel ---> Enable Sem菜单,完成信号量的配置。
创建信号量LOS_SemCreate,若要创建二值信号量则调用LOS_BinarySemCreate。
申请信号量LOS_SemPend。
释放信号量LOS_SemPost。
删除信号量LOS_SemDelete。
无。
注意事项¶
由于中断不能被阻塞,因此不能在中断中使用阻塞模式申请信号量。
编程实例¶
本实例实现如下功能:
测试任务Example_TaskEntry创建一个信号量,锁任务调度,创建两个任务Example_SemTask1、Example_SemTask2,Example_SemTask2优先级高于Example_SemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务Example_TaskEntry释放信号量。
Example_SemTask2得到信号量,被调度,然后任务休眠20Tick,Example_SemTask2延迟,Example_SemTask1被唤醒。
Example_SemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被Example_SemTask2持有,Example_SemTask1挂起,10Tick后仍未得到信号量,Example_SemTask1被唤醒,试图以永久阻塞模式申请信号量,Example_SemTask1挂起。
20Tick后Example_SemTask2唤醒, 释放信号量后,Example_SemTask1得到信号量被调度运行,最后释放信号量。
Example_SemTask1执行完,40Tick后任务Example_TaskEntry被唤醒,执行删除信号量,删除两个任务。
前提条件:在menuconfig菜单中完成信号量的配置。
代码实现如下:
#include "los_sem.h"
#include "securec.h"
/* 任务ID */
static UINT32 g_testTaskId01;
static UINT32 g_testTaskId02;
/* 测试任务优先级 */
#define TASK_PRIO_TEST 5
/* 信号量结构体id */
static UINT32 g_semId;
VOID Example_SemTask1(VOID)
{
UINT32 ret;
printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n");
/* 定时阻塞模式申请信号量,定时时间为10ticks */
ret = LOS_SemPend(g_semId, 10);
/* 申请到信号量 */
if (ret == LOS_OK) {
LOS_SemPost(g_semId);
return;
}
/* 定时时间到,未申请到信号量 */
if (ret == LOS_ERRNO_SEM_TIMEOUT) {
printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n");
/*永久阻塞模式申请信号量*/
ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
printf("Example_SemTask1 wait_forever and get sem g_semId .\n");
if (ret == LOS_OK) {
LOS_SemPost(g_semId);
return;
}
}
}
VOID Example_SemTask2(VOID)
{
UINT32 ret;
printf("Example_SemTask2 try get sem g_semId wait forever.\n");
/* 永久阻塞模式申请信号量 */
ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n");
}
/* 任务休眠20 ticks */
LOS_TaskDelay(20);
printf("Example_SemTask2 post sem g_semId .\n");
/* 释放信号量 */
LOS_SemPost(g_semId);
return;
}
UINT32 ExampleTaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
TSK_INIT_PARAM_S task2;
/* 创建信号量 */
LOS_SemCreate(0,&g_semId);
/* 锁任务调度 */
LOS_TaskLock();
/* 创建任务1 */
(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1;
task1.pcName = "TestTsk1";
task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = TASK_PRIO_TEST;
ret = LOS_TaskCreate(&g_testTaskId01, &task1);
if (ret != LOS_OK) {
printf("task1 create failed .\n");
return LOS_NOK;
}
/* 创建任务2 */
(VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2;
task2.pcName = "TestTsk2";
task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
task2.usTaskPrio = (TASK_PRIO_TEST - 1);
ret = LOS_TaskCreate(&g_testTaskId02, &task2);
if (ret != LOS_OK) {
printf("task2 create failed .\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
ret = LOS_SemPost(g_semId);
/* 任务休眠40 ticks */
LOS_TaskDelay(40);
/* 删除信号量 */
LOS_SemDelete(g_semId);
/* 删除任务1 */
ret = LOS_TaskDelete(g_testTaskId01);
if (ret != LOS_OK) {
printf("task1 delete failed .\n");
return LOS_NOK;
}
/* 删除任务2 */
ret = LOS_TaskDelete(g_testTaskId02);
if (ret != LOS_OK) {
printf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
Example_SemTask2 try get sem g_semId wait forever.
Example_SemTask1 try get sem g_semId ,timeout 10 ticks.
Example_SemTask2 get sem g_semId and then delay 20ticks .
Example_SemTask1 timeout and try get sem g_semId wait forever.
Example_SemTask2 post sem g_semId .
Example_SemTask1 wait_forever and get sem g_semId .
读写信号量¶
概述¶
读写信号量(rwsem)与普通信号量的功能相似,常用于共享资源的互斥访问和任务同步。读写信号量将持有者分为读者和写者,如果一个持有者需要对该信号量保护的共享资源进行写操作,那么将这类持有者定义为写者;如果持有者只是访问共享资源,而不修改,那么将这类持有者定义为读者。
读写信号量支持一个读写信号量同时拥有不受上限的读者数,即读者不排他可并发;但是一个读写信号量同时只能有一个写者,写者是排他性的、独占性的,写写互斥、读写互斥。
读写信号量控制块:
typedef struct {
UINT8 rwsemState; /* 是否使用标志位 */
INT16 rwsemCount; /* 读写信号量拥有者的数量 */
UINT32 rwsemId; /* 读写信号量索引号 */
LOS_DL_LIST waitList; /* 等待队列,用于记录阻塞于该读写信号量的任务 */
} OsRwsemCB;
读写信号量资源初始化:为配置的N个读写信号量申请内存(N值可以由用户自行配置,通过LOSCFG_BASE_IPC_RWSEM_LIMIT宏实现),并把所有读写信号量初始化成未使用,加入到未使用链表中供系统使用。
创建读写信号量:从未使用链表中获取一个读写信号量。
申请读信号量:若该信号量还未被持有,或者正在被其他读者持有,返回成功;否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个读写信号量阻塞时,该任务会被挂到读写信号量等待队列的队尾。
申请写信号量:若该信号量还未被持有,返回成功;否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个读写信号量阻塞时,该任务会被挂到读写信号量等待队列的队尾。
释放读信号量:若没有任务等待该读写信号量,则直接返回;当信号量处于读状态,等待队列中的队首任务只能是写者,唤醒这一个写者,然后返回。
释放写信号量:若没有任务等待该读写信号量,则直接返回;如果等待队列的队首任务是写者,则唤醒这一个写者,然后返回;如果等待队列的队首任务是读者,则唤醒这个读者及后面连续的多个读者,直到等待队列为空,或者遇到写者停止。
降级写信号量为读信号量:信号量变更为读信号量,若没有任务等待该读写信号量或者等待队列的队首任务是写者,则直接返回;否则唤醒等待队列的队首读者及后面连续的多个读者,直到等待队列为空,或者遇到写者停止。
删除读写信号量:将正在使用的读写信号量设置为未使用的状态,并挂回到未使用链表。
说明:
读写信号量允许多个任务在同一时刻访问共享资源,但同一时刻只能有一个任务可修改共享资源,这样对于读任务多于写任务的场景,可有效提高系统的整体性能。
开发指导¶
读写信号量是基于普通信号量优化的一种多任务同步互斥机制,可以运用在多种场合中,实现锁、同步、资源计数等功能,也能方便的用于任务与任务同步中,适用于读任务多于写任务的场景。
LiteOS的读写信号量模块为用户提供下面几种功能,接口详细信息可以查看API参考。
说明: 读写信号量支持三种申请模式:无阻塞模式、永久阻塞模式、定时阻塞模式。
无阻塞模式:即任务申请读写信号量时,入参timeout等于0。若当前读写信号量可获取,则申请成功,否则立即返回申请失败。
永久阻塞模式:即任务申请读写信号量时,入参timeout等于0xFFFFFFFF。若当前读写信号量可获取,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到其他任务释放该读写信号量,使得该读写信号量可重新被获取,阻塞任务才会重新得以执行。
定时阻塞模式:即任务申请读写信号量时,0<timeout<0xFFFFFFFF。若当前读写信号量可获取,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,如果超时前其他任务释放该信号量,使得该读写信号量可重新被获取,则该任务可成功获取读写信号量继续执行,若超时前未获取到读写信号量,接口将返回超时错误码。
对于存在失败可能性的操作系统会返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为信号量模块,值为0x22。
读写信号量的典型开发流程如下:
打开菜单,进入Kernel ---> Enable Rwsem菜单,完成读写信号量的配置。
创建读写信号量LOS_RwsemCreate。
申请读信号量LOS_RwsemPendRead。
释放读信号量LOS_RwsemPostRead。
删除读写信号量LOS_RwsemDelete。
无。
注意事项¶
由于中断不能被阻塞,因此不能在中断中使用阻塞模式申请读写信号量。
编程实例¶
本实例实现如下功能:
测试任务ExampleTaskEntry创建一个读写信号量,并申请写信号量,锁任务调度,创建三个任务ExampleReadTask1、ExampleReadTask2和ExampleWriteTask2;ExampleReadTask1和ExampleReadTask2的优先级高于ExampleWriteTask2任务,并申请读信号量;ExampleWriteTask2申请写信号量。
ExampleTaskEntry解锁任务调度后,ExampleReadTask1、ExampleReadTask2获取读信号量失败,永久阻塞等待,ExampleWriteTask2等待1tick获取写信号量失败,改为永久阻塞等待获取写信号量。
ExampleTaskEntry降级写信号量为读信号量,ExampleReadTask1和ExampleReadTask2获取读信号量成功,ExampleWriteTask2继续阻塞。
在ExampleReadTask1、ExampleReadTask2和ExampleTaskEntry释放读信号量之后,ExampleWriteTask2获取写信号量成功。
ExampleWriteTask2释放写信号量。
前提条件:在menuconfig菜单中完成读写信号量的配置。
代码实现如下:
#include "los_rwsem.h"
#include "securec.h"
static UINT32 g_rwsemId; /* 读写信号量结构体id */
VOID ExampleReadTask1(VOID)
{
UINT32 ret;
/* 定时时间到,未申请到读信号量 */
printf("[ExampleReadTask1] try get rwsem as read, wait forever.\n");
/* 永久阻塞模式申请读信号量 */
ret = LOS_RwsemPendRead(g_rwsemId, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("[ExampleReadTask1] get rwsem success.\n");
LOS_TaskDelay(3);
printf("[ExampleReadTask1] post rwsem.\n");
LOS_RwsemPostRead(g_rwsemId);
return;
}
}
VOID ExampleReadTask2(VOID)
{
UINT32 ret;
/* 定时时间到,未申请到读信号量 */
printf("[ExampleReadTask2] try get rwsem as read, wait forever.\n");
/* 永久阻塞模式申请读信号量 */
ret = LOS_RwsemPendRead(g_rwsemId, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("[ExampleReadTask2] get rwsem success.\n");
LOS_TaskDelay(3);
printf("[ExampleReadTask2] post rwsem.\n");
LOS_RwsemPostRead(g_rwsemId);
return;
}
}
VOID ExampleWriteTask2(VOID)
{
UINT32 ret;
printf("[ExampleWriteTask2] try get rwsem as write, timeout 1 tick.\n");
/* 定时阻塞模式申请写信号量,定时时间为1tick */
ret = LOS_RwsemPendWrite(g_rwsemId, 1);
/* 申请到写信号量 */
if (ret == LOS_OK) {
LOS_RwsemPostWrite(g_rwsemId);
return;
}
/* 定时时间到,未申请到写信号量 */
if (ret == LOS_ERRNO_RWSEM_TIMEOUT) {
printf("[ExampleWriteTask2] timeout and try get rwsem as write, wait forever.\n");
/* 永久阻塞模式申请写信号量 */
ret = LOS_RwsemPendWrite(g_rwsemId, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("[ExampleWriteTask2] get rwsem success.\n");
LOS_TaskDelay(3);
printf("[ExampleWriteTask2] post rwsem.\n");
LOS_RwsemPostWrite(g_rwsemId);
return;
}
}
return;
}
static VOID InitTaskParam(TSK_INIT_PARAM_S *task, char *taskName, TSK_ENTRY_FUNC entry, UINT16 prio, UINT16 affi)
{
task->pcName = taskName;
task->pfnTaskEntry = entry;
task->usTaskPrio = prio;
task->uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task->uwResved = LOS_TASK_STATUS_DETACHED;
#ifdef LOSCFG_KERNEL_SMP
task->usCpuAffiMask = affi;
#endif
}
UINT32 ExampleTaskEntry(VOID)
{
UINT32 ret;
UINT32 readTsk1Id, readTsk2Id, writeTsk2Id;
TSK_INIT_PARAM_S readTask1 = {0};
TSK_INIT_PARAM_S readTask2 = {0};
TSK_INIT_PARAM_S writeTask2 = {0};
/* 创建读写信号量 */
ret = LOS_RwsemCreate(&g_rwsemId);
if (ret != LOS_OK) {
printf("[ExampleTaskEntry] create rwsem failed.\n");
return LOS_NOK;
}
ret = LOS_RwsemPendWrite(g_rwsemId, 0);
if (ret != LOS_OK) {
printf("[ExampleTaskEntry] get rwsem failed.\n");
return LOS_NOK;
}
printf("[ExampleTaskEntry] get rwsem success.\n");
/* 锁任务调度 */
LOS_TaskLock();
/* 创建读任务1 */
InitTaskParam(&readTask1, "resemReadTsk1", (TSK_ENTRY_FUNC)ExampleReadTask1, 5, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
ret = LOS_TaskCreate(&readTsk1Id, &readTask1);
if (ret != LOS_OK) {
printf("[ExampleTaskEntry] readTask1 create failed.\n");
return LOS_NOK;
}
/* 创建读任务2 */
InitTaskParam(&readTask2, "resemReadTsk2", (TSK_ENTRY_FUNC)ExampleReadTask2, 5, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
ret = LOS_TaskCreate(&readTsk2Id, &readTask2);
if (ret != LOS_OK) {
printf("[ExampleTaskEntry] readTask2 create failed.\n");
return LOS_NOK;
}
/* 创建写任务2 */
InitTaskParam(&writeTask2, "resemWriteTsk2", (TSK_ENTRY_FUNC)ExampleWriteTask2, 9, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
ret = LOS_TaskCreate(&writeTsk2Id, &writeTask2);
if (ret != LOS_OK) {
printf("[ExampleTaskEntry] writeTask2 create failed.\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
/* 让出CPU 5tick,等待readTask1、readTask2、writeTask2先执行并阻塞 */
LOS_TaskDelay(5);
/* 写信号量降级为读信号量 */
printf("[ExampleTaskEntry] downgrade rwsem.\n");
LOS_RwsemDowngradeWrite(g_rwsemId);
/* 任务休眠5ticks */
LOS_TaskDelay(5);
LOS_RwsemPostRead(g_rwsemId);
/* 任务休眠10ticks */
LOS_TaskDelay(10);
/* 删除读写信号量 */
LOS_RwsemDelete(g_rwsemId);
return LOS_OK;
}
编译运行所得结果如下:
[ExampleTaskEntry] get rwsem success.
[ExampleReadTask1] try get rwsem as read, wait forever.
[ExampleReadTask2] try get rwsem as read, wait forever.
[ExampleWriteTask2] try get rwsem as write, timeout 1 tick.
[ExampleWriteTask2] timeout and try get rwsem as write, wait forever.
[ExampleTaskEntry] downgrade rwsem.
[ExampleReadTask1] get rwsem success.
[ExampleReadTask2] get rwsem success.
[ExampleReadTask1] post rwsem.
[ExampleReadTask2] post rwsem.
[ExampleWriteTask2] get rwsem success.
[ExampleWriteTask2] post rwsem.
互斥锁¶
概述¶
互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。另外,互斥锁可以解决信号量存在的优先级翻转问题。
任意时刻互斥锁只有两种状态,开锁或闭锁。当任务持有时,这个任务获得该互斥锁的所有权,互斥锁处于闭锁状态。当该任务释放锁后,任务失去该互斥锁的所有权,互斥锁处于开锁状态。当一个任务持有互斥锁时,其他任务不能再对该互斥锁进行开锁或持有。
LiteOS提供的互斥锁具有如下特点:
通过优先级继承算法,解决优先级翻转问题。
多任务阻塞等待同一个锁的场景,支持基于任务优先级等待和FIFO两种模式。
多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占使用。互斥锁怎样来避免这种冲突呢?
用互斥锁处理临界资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个临界资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的完整性。
开发指导¶
多任务环境下往往存在多个任务竞争同一临界资源的应用场景,互斥锁可以提供任务间的互斥机制,防止两个任务在同一时刻访问相同的临界资源,从而实现独占式访问。
LiteOS的互斥锁模块为用户提供下面几种功能,接口详细信息可以查看API参考。
说明: 申请互斥锁有三种模式:无阻塞模式、永久阻塞模式、定时阻塞模式。
无阻塞模式:即任务申请互斥锁时,入参timeout等于0。若当前没有任务持有该互斥锁,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功,否则立即返回申请失败。
永久阻塞模式:即任务申请互斥锁时,入参timeout等于0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则,任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行。
定时阻塞模式:即任务申请互斥锁时,0<timeout<0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,超时前如果有其他任务释放该互斥锁,则该任务可成功获取互斥锁继续执行,若超时前未获取到该互斥锁,接口将返回超时错误码。 释放互斥锁:
如果有任务阻塞于该互斥锁,则唤醒被阻塞任务中优先级最高的,该任务进入就绪态,并进行任务调度。
如果没有任务阻塞于该互斥锁,则互斥锁释放成功。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为互斥锁模块,值为0x1D。
互斥锁典型场景的开发流程:
打开菜单,进入Kernel ---> Enable Mutex菜单,完成互斥锁的配置。
创建互斥锁LOS_MuxCreate。
申请互斥锁LOS_MuxPend。
释放互斥锁LOS_MuxPost。
删除互斥锁LOS_MuxDelete。
无。
注意事项¶
互斥锁不能在中断服务程序中使用。
LiteOS作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得互斥锁之后,应该尽快释放互斥锁。
持有互斥锁的过程中,不得再调用LOS_TaskPriSet等接口更改持有互斥锁任务的优先级。
互斥锁不支持多个相同优先级任务翻转的场景。
编程实例¶
本实例实现如下流程。
任务Example_TaskEntry创建一个互斥锁,锁任务调度,创建两个任务Example_MutexTask1、Example_MutexTask2。Example_MutexTask2优先级高于Example_MutexTask1,解锁任务调度,然后Example_TaskEntry任务休眠300Tick。
Example_MutexTask2被调度,以永久阻塞模式申请互斥锁,并成功获取到该互斥锁,然后任务休眠100Tick,Example_MutexTask2挂起,Example_MutexTask1被唤醒。
Example_MutexTask1以定时阻塞模式申请互斥锁,等待时间为10Tick,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。10Tick超时时间到达后,Example_MutexTask1被唤醒,以永久阻塞模式申请互斥锁,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。
100Tick休眠时间到达后,Example_MutexTask2被唤醒, 释放互斥锁,唤醒Example_MutexTask1。Example_MutexTask1成功获取到互斥锁后,释放锁。
300Tick休眠时间到达后,任务Example_TaskEntry被调度运行,删除互斥锁,删除两个任务。
前提条件:打开菜单完成互斥锁的配置。
代码实现如下:
/* 互斥锁句柄id */
UINT32 g_testMux;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_MutexTask1(VOID)
{
UINT32 ret;
printf("task1 try to get mutex, wait 10 ticks.\n");
/* 申请互斥锁 */
ret = LOS_MuxPend(g_testMux, 10);
if (ret == LOS_OK) {
printf("task1 get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxPost(g_testMux);
return;
} else if (ret == LOS_ERRNO_MUX_TIMEOUT ) {
printf("task1 timeout and try to get mutex, wait forever.\n");
/* 申请互斥锁 */
ret = LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("task1 wait forever, get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxPost(g_testMux);
return;
}
}
return;
}
VOID Example_MutexTask2(VOID)
{
printf("task2 try to get mutex, wait forever.\n");
/* 申请互斥锁 */
(VOID)LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
printf("task2 get mutex g_testMux and suspend 100 ticks.\n");
/* 任务休眠100Ticks */
LOS_TaskDelay(100);
printf("task2 resumed and post the g_testMux\n");
/* 释放互斥锁 */
LOS_MuxPost(g_testMux);
return;
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
TSK_INIT_PARAM_S task2;
/* 创建互斥锁 */
LOS_MuxCreate(&g_testMux);
/* 锁任务调度 */
LOS_TaskLock();
/* 创建任务1 */
memset(&task1, 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask1;
task1.pcName = "MutexTsk1";
task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = 5;
ret = LOS_TaskCreate(&g_testTaskId01, &task1);
if (ret != LOS_OK) {
printf("task1 create failed.\n");
return LOS_NOK;
}
/* 创建任务2 */
memset(&task2, 0, sizeof(TSK_INIT_PARAM_S));
task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask2;
task2.pcName = "MutexTsk2";
task2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task2.usTaskPrio = 4;
ret = LOS_TaskCreate(&g_testTaskId02, &task2);
if (ret != LOS_OK) {
printf("task2 create failed.\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
/* 休眠300Ticks */
LOS_TaskDelay(300);
/* 删除互斥锁 */
LOS_MuxDelete(g_testMux);
/* 删除任务1 */
ret = LOS_TaskDelete(g_testTaskId01);
if (ret != LOS_OK) {
printf("task1 delete failed .\n");
return LOS_NOK;
}
/* 删除任务2 */
ret = LOS_TaskDelete(g_testTaskId02);
if (ret != LOS_OK) {
printf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
task2 try to get mutex, wait forever.
task2 get mutex g_testMux and suspend 100 ticks.
task1 try to get mutex, wait 10 ticks.
task1 timeout and try to get mutex, wait forever.
task2 resumed and post the g_testMux
task1 wait forever,get mutex g_testMux.
软件定时器¶
概述¶
软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器。当经过设定的Tick数后,会触发用户自定义的回调函数。
硬件定时器受硬件的限制,数量上不足以满足用户的实际需求。因此为了满足用户需求,提供更多的定时器,LiteOS提供了软件定时器功能,支持如下特性:
创建软件定时器。
启动软件定时器。
停止软件定时器。
删除软件定时器。
获取软件定时器剩余Tick数。
可配置支持的软件定时器个数。
软件定时器是系统资源,在模块初始化的时候已经分配了一块连续内存。
软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。定时时间短的定时器总是比定时时间长的靠近队列头,满足优先触发的准则。
软件定时器以Tick为基本计时单位,当创建并启动一个软件定时器时,LiteOS会根据当前系统Tick时间及设置的定时时长确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。
当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,检查是否有定时器超时,若有则将超时的定时器记录下来。Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用已经记录下来的定时器的回调函数。
定时器状态:
OS_SWTMR_STATUS_UNUSED(定时器未使用)
系统在定时器模块初始化时,会将系统中所有定时器资源初始化成该状态。
OS_SWTMR_STATUS_TICKING(定时器处于计数状态)
在定时器创建后调用LOS_SwtmrStart接口启动,定时器将变成该状态,是定时器运行时的状态。
OS_SWTMR_STATUS_CREATED(定时器创建后未启动,或已停止)
定时器创建后,不处于计数状态时,定时器将变成该状态。
OS_SWTMR_STATUS_DELETING (定时器被删除了)
定时器类型是单次触发类型,在定时器超时后,定时器将变成该状态。
定时器模式:
软件定时器提供了三类模式:
单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动删除。
周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动停止定时器,否则将永远持续执行下去。
单次触发定时器,但这类定时器超时触发后不会自动删除,需要调用定时器删除接口删除定时器。
开发指导¶
创建一个单次触发的定时器,超时后执行用户自定义的回调函数。
创建一个周期性触发的定时器,超时后执行用户自定义的回调函数。
LiteOS的软件定时器模块为用户提供下面几种功能,接口详细信息可以查看API参考。
异步删除定时器,接口在入参检测后立即返回,不保障接口返回时软件定时器资源已经被释放,软件定时器资源将在目标定时器所有回调执行完毕后释放。 |
||
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为软件定时器模块,值为0x03。
软件定时器的典型开发流程:
注意事项¶
软件定时器的回调函数中不应执行过多操作,不建议使用可能引起任务挂起或者阻塞的接口或操作,如果使用会导致软件定时器响应不及时,可能会影响正常业务功能。
如果没有使能LOSCFG_BASE_CORE_SWTMR_IN_ISR宏,软件定时器将使用系统的一个任务资源。软件定时器任务的优先级设定为0,且不允许修改 。
系统可配置的软件定时器个数是指:整个系统可使用的软件定时器总个数,并非用户可使用的软件定时器个数。例如:系统多占用一个软件定时器,那么用户能使用的软件定时器资源就会减少一个。
创建单次不自删除属性的定时器,用户需要自行调用定时器删除接口删除定时器,回收定时器资源,避免资源泄露。
软件定时器的定时精度与系统Tick时钟的周期有关。
编程实例¶
在下面的例子中,演示如下功能:
软件定时器创建、启动、停止、删除操作。
单次软件定时器,周期软件定时器使用方法。
前提条件:在menuconfig菜单中完成软件定时器的配置。
代码实现如下:
UINT32 g_timerCount1 = 0;
UINT32 g_timerCount2 = 0;
VOID Timer1_CallBack(UINT32 arg)
{
UINT64 lastTick;
g_timerCount1++;
lastTick=(UINT32)LOS_TickCountGet();
dprintf("g_timerCount1=%d\n", g_timerCount1);
dprintf("tick_last1=%d\n", lastTick);
}
VOID Timer2_CallBack(UINT32 arg)
{
UINT64 lastTick;
lastTick=(UINT32)LOS_TickCountGet();
g_timerCount2++;
dprintf("g_timerCount2=%d\n", g_timerCount2);
dprintf("tick_last2=%d\n", lastTick);
}
VOID Timer_example(VOID)
{
UINT16 id1; // Timer1 id
UINT16 id2; // Timer2 id
UINT32 tick;
LOS_SwtmrCreate(1000, LOS_SWTMR_MODE_ONCE, Timer1_CallBack, &id1, 1);
LOS_SwtmrCreate(100, LOS_SWTMR_MODE_PERIOD, Timer2_CallBack, &id2, 1);
dprintf("create Timer1 success\n");
LOS_SwtmrStart(id1);
dprintf("start Timer1 sucess\n");
LOS_TaskDelay(200);
LOS_SwtmrTimeGet(id1, &tick);
dprintf("tick =%d\n", tick);
LOS_SwtmrStop(id1);
dprintf("stop Timer1 sucess\n");
LOS_SwtmrStart(id1);
LOS_TaskDelay(1000);
LOS_SwtmrDelete(id1);
dprintf("delete Timer1 sucess\n");
LOS_SwtmrStart(id2);
dprintf("start Timer2\n");
LOS_TaskDelay(1000);
LOS_SwtmrStop(id2);
LOS_SwtmrDelete(id2);
}
得到的结果为:
Create Timer1 success
start Timer1 sucess
tick =800
Stop Timer1 sucess
g_timerCount1=1201
tick_last1=1201
delete Timer1 success
Start Timer2
g_timerCount2=1
tick_last2=1301
g_timerCount2=2
tick_last2=1401
g_timerCount2=3
tick_last2=1501
g_timerCount2=4
tick_last2=1601
g_timerCount2=5
tick_last2=1701
g_timerCount2=6
tick_last2=1801
g_timerCount2=7
tick_last2=1901
g_timerCount2=8
tick_last2=2001
g_timerCount2=9
tick_last2=2101
g_timerCount2=10
tick_last2=2201
自旋锁¶
概述¶
在多核环境中,由于使用相同的内存空间,存在对同一资源进行访问的情况,所以需要互斥访问机制来保证同一时刻只有一个核进行操作。自旋锁就是这样的一种机制。
自旋锁是指当一个线程在获取锁时,如果锁已经被其它线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到获取到锁才会退出循环。因此建议保护耗时较短的操作,防止对系统整体性能有明显的影响。
自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。
开发指导¶
自旋锁可以提供任务之间的互斥访问机制,用来防止两个任务在同一时刻访问相同的共享资源。
LiteOS的自旋锁模块为用户提供下面几种功能,接口详细信息可以查看API参考。
自旋锁的开发典型流程:
自旋锁依赖于SMP,打开菜单,配置项的菜单路径为:Kernel ---> Enable Kernel SMP。
创建自旋锁:使用LOS_SpinInit初始化自旋锁,或者使用SPIN_LOCK_INIT初始化静态内存的自旋锁。
申请自旋锁:使用接口LOS_SpinLock/LOS_SpinTrylock/LOS_SpinLockSave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。
释放自旋锁:使用LOS_SpinUnlock/LOS_SpinUnlockRestore接口释放自旋锁。锁保护代码执行完毕后,释放对应的自旋锁,以便其他核申请自旋锁。
注意事项¶
主要注意函数入参的合法性由调用者保证。
同一个任务不能对同一把自旋锁进行多次加锁,否则会导致死锁。
自旋锁中会执行本核的锁任务操作,因此需要等到最外层完成解锁后本核才会进行任务调度。
LOS_SpinLock与LOS_SpinUnlock允许单独使用,即可以不进行关中断,但是用户需要保证使用的接口只会在任务或中断中使用。如果接口同时会在任务和中断中被调用,请使用LOS_SpinLockSave与LOS_SpinUnlockRestore,因为在未关中断的情况下使用LOS_SpinLock可能会导致死锁。
耗时的操作谨慎选用自旋锁,可使用互斥锁进行保护。
未开启SMP的单核场景下,自旋锁功能无效,只有LOS_SpinLockSave与LOS_SpinUnlockRestore接口有关闭恢复中断功能。
建议LOS_SpinLock和LOS_SpinUnlock、LOS_SpinLockSave和LOS_SpinUnlockRestore配对使用,避免出错。
编程实例¶
本实例实现如下流程。
任务Example_TaskEntry初始化自旋锁,创建两个任务Example_SpinTask1、Example_SpinTask2,分别运行于两个核。
Example_SpinTask1、Example_SpinTask2中均执行申请自旋锁的操作,同时为了模拟实际操作,在持有自旋锁后进行延迟操作,最后释放自旋锁。
300Tick后任务Example_TaskEntry被调度运行,删除任务Example_SpinTask1和Example_SpinTask2。
须知: 多核的运行时序不是固定的,因此也存在任务执行顺序不同的情况。
前提条件:在menuconfig中,将LOSCFG_KERNEL_SMP配置项打开,并设置多核core数量。
代码实现如下:
#include "los_spinlock.h"
#include "los_task.h"
/* 自旋锁句柄id */
SPIN_LOCK_S g_testSpinlock;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_SpinTask1(VOID)
{
UINT32 i;
UINTPTR intSave;
/* 申请自旋锁 */
dprintf("task1 try to get spinlock\n");
LOS_SpinLockSave(&g_testSpinlock, &intSave);
dprintf("task1 got spinlock\n");
for(i = 0; i < 5000; i++) {
asm volatile("nop");
}
/* 释放自旋锁 */
dprintf("task1 release spinlock\n");
LOS_SpinUnlockRestore(&g_testSpinlock, intSave);
return;
}
VOID Example_SpinTask2(VOID)
{
UINT32 i;
UINTPTR intSave;
/* 申请自旋锁 */
dprintf("task2 try to get spinlock\n");
LOS_SpinLockSave(&g_testSpinlock, &intSave);
dprintf("task2 got spinlock\n");
for(i = 0; i < 5000; i++) {
asm volatile("nop");
}
/* 释放自旋锁 */
dprintf("task2 release spinlock\n");
LOS_SpinUnlockRestore(&g_testSpinlock, intSave);
return;
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S stTask1;
TSK_INIT_PARAM_S stTask2;
/* 初始化自旋锁 */
LOS_SpinInit(&g_testSpinlock);
/* 创建任务1 */
memset(&stTask1, 0, sizeof(TSK_INIT_PARAM_S));
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SpinTask1;
stTask1.pcName = "SpinTsk1";
stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
stTask1.usTaskPrio = 5;
#ifdef LOSCFG_KERNEL_SMP
/* 绑定任务到CPU0运行 */
stTask1.usCpuAffiMask = CPUID_TO_AFFI_MASK(0);
#endif
ret = LOS_TaskCreate(&g_testTaskId01, &stTask1);
if(ret != LOS_OK) {
dprintf("task1 create failed .\n");
return LOS_NOK;
}
/* 创建任务2 */
memset(&stTask2, 0, sizeof(TSK_INIT_PARAM_S));
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SpinTask2;
stTask2.pcName = "SpinTsk2";
stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
stTask2.usTaskPrio = 5;
#ifdef LOSCFG_KERNEL_SMP
/* 绑定任务到CPU1运行 */
stTask1.usCpuAffiMask = CPUID_TO_AFFI_MASK(1);
#endif
ret = LOS_TaskCreate(&g_testTaskId02, &stTask2);
if(ret != LOS_OK) {
dprintf("task2 create failed .\n");
return LOS_NOK;
}
/* 任务休眠300Ticks */
LOS_TaskDelay(300);
/* 删除任务1 */
ret = LOS_TaskDelete(g_testTaskId01);
if(ret != LOS_OK) {
dprintf("task1 delete failed .\n");
return LOS_NOK;
}
/* 删除任务2 */
ret = LOS_TaskDelete(g_testTaskId02);
if(ret != LOS_OK) {
dprintf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
task2 try to get spinlock
task2 got spinlock
task1 try to get spinlock
task2 release spinlock
task1 got spinlock
task1 release spinlock
分级初始化¶
概述¶
分级初始化将Kernel的启动过程分成多个阶段,阶段之间存在依赖关系,后初始化的阶段可能会依赖前初始化阶段的接口。对于Kernel启动过程新增的调用接口,不再需要修改启动代码添加函数调用,可通过分级初始化注册宏进行接口注册,Kernel在启动时,按照顺序自动执行所有注册的初始化接口。
每一个需要在Kernel初始化时执行的函数,通过使用分级启动宏都会生成一个函数指针变量,指向该函数。
编译生成镜像时,链接脚本将上述的函数指针变量冠段。
Kernel初始化,从冠段地址取出所有函数指针,依次执行所有初始化函数。
开发指导¶
分级初始化,可以实现Kernel初始化模块与其他模块的充分解耦。Kernel初始化模块不再感知具体模块的初始化接口,初始化接口也不用关心如何被调用。
LiteOS中的分级初始化模块提供下面几种功能,接口详细信息可以查看API参考。
层级划分:分级初始化时,下表中越靠前的层级,优先级越高,越先得到执行。
优先级划分:同一层级的初始化函数之间,可能存在依赖关系。sync优先级只能用于设置同一层级内的函数优先级。sync值越小,优先级越高,越先执行。
确定初始化接口的依赖关系,进而确定层级及优先级。
调用LOS_SYS_INIT注册初始化接口。
注意事项¶
分级初始化适用于类型为typedef unsigned int (*SysInitcallFunc)(void)的初始化函数。
由于不会在代码中直接调用初始化函数,一些编译器可能会因此优化掉初始化函数,导致注册失败,建议使用-u链接选项。
同一个层级、sync优先级内,可以注册多个初始化接口,但这些接口之间不能存在依赖关系。
编程实例¶
在下面的实例中,描述了分级初始化注册接口的使用方法:
确定初始化接口的依赖关系、层级及优先级。
注册初始化接口。
#include "stdio.h"
#include "los_init.h"
#include "los_list.h"
#include "los_hwi.h"
#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif /* __cpluscplus */
#endif /* __cpluscplus */
UINT32 g_count;
LOS_DL_LIST g_DLlist;
UINT32 InitLevelTest1(VOID)
{
dprintf("InitLevelTest1 init\n");
g_count = 10;
LOS_ListInit(&g_DLlist);
return LOS_OK;
}
/* InitLevelTest1 has no dependence, set SYS_INIT_LEVEL_EARLY level */
LOS_SYS_INIT(InitLevelTest1, SYS_INIT_LEVEL_EARLY, SYS_INIT_SYNC_0);
STATIC VOID HwiUsrIrq(VOID)
{
g_count++;
}
UINT32 InitLevelTest2(VOID)
{
UINT32 ret;
HWI_HANDLE_T irqNum = 26;
HWI_PRIOR_T irqPri = 0x3;
dprintf("InitLevelTest2 init\n");
ret = LOS_HwiCreate(irqNum, irqPri, 0, (HWI_PROC_FUNC)HwiUsrIrq, 0);
if (ret != LOS_OK) {
return LOS_NOK;
}
return ret ;
}
/* InitLevelTest2 rely on hwi module, set SYS_INIT_LEVEL_RESERVE level */
LOS_SYS_INIT(InitLevelTest2, SYS_INIT_LEVEL_RESERVE, SYS_INIT_SYNC_0);
#ifdef __cplusplus
#if __cplusplus
}
#endif /* __cpluscplus */
#endif /* __cpluscplus */
编译运行得到的结果为:
InitLevelTest1 init
InitLevelTest2 init
其他¶
时间管理¶
概述¶
时间管理以系统时钟为基础,给应用程序提供所有和时间有关的服务。
系统时钟是由定时器/计数器产生的输出脉冲触发中断产生的,一般定义为整数或长整数。输出脉冲的周期叫做一个“时钟滴答”。系统时钟也称为时标或者Tick。
用户以秒、毫秒为单位计时,而操作系统以Tick为单位计时,当用户需要对系统进行操作时,例如任务挂起、延时等,此时需要时间管理模块对Tick和秒/毫秒进行转换。
LiteOS的时间管理模块提供时间转换、统计、延迟功能。
Cycle
系统最小的计时单位。Cycle的时长由系统主时钟频率决定,系统主时钟频率就是每秒钟的Cycle数。
Tick
Tick是操作系统的基本时间单位,由用户配置的每秒Tick数决定。
开发指导¶
用户需要了解当前系统运行的时间以及Tick与秒、毫秒之间的转换关系等。
LiteOS的时间管理提供下面几种功能,接口详细信息可以查看API参考。
时间转换存在出错的可能性,需要返回对应的错误码,以便快速定位错误原因。
在板级配置适配时配置有效的系统主时钟频率OS_SYS_CLOCK,打开菜单配置有效的LOSCFG_BASE_CORE_TICK_PER_SECOND。 |
时间管理的典型开发流程:
注意事项¶
时间管理不是单独的功能模块,依赖于OS_SYS_CLOCK和LOSCFG_BASE_CORE_TICK_PER_SECOND两个配置选项。
系统的Tick数在关中断的情况下不进行计数,故系统Tick数不能作为准确时间使用。
编程实例¶
在下面的例子中,介绍了时间管理的基本方法,包括:
时间转换:将毫秒数转换为Tick数,或将Tick数转换为毫秒数。
时间统计:每Tick的Cycle数、自系统启动以来的Tick数和延迟后的Tick数。
前提条件:
使用每秒的Tick数LOSCFG_BASE_CORE_TICK_PER_SECOND的默认值100。
配好OS_SYS_CLOCK系统主时钟频率。
时间转换:
VOID Example_TransformTime(VOID)
{
UINT32 ms;
UINT32 tick;
tick = LOS_MS2Tick(10000); // 10000ms转换为tick
dprintf("tick = %d \n",tick);
ms = LOS_Tick2MS(100); // 100tick转换为ms
dprintf("ms = %d \n",ms);
}
时间统计和时间延迟:
VOID Example_GetTime(VOID)
{
UINT32 cyclePerTick;
UINT64 tickCount;
cyclePerTick = LOS_CyclePerTickGet();
if(0 != cyclePerTick) {
dprintf("LOS_CyclePerTickGet = %d \n", cyclePerTick);
}
tickCount = LOS_TickCountGet();
if(0 != tickCount) {
dprintf("LOS_TickCountGet = %d \n", (UINT32)tickCount);
}
LOS_TaskDelay(200);
tickCount = LOS_TickCountGet();
if(0 != tickCount) {
dprintf("LOS_TickCountGet after delay = %d \n", (UINT32)tickCount);
}
}
编译运行得到的结果为:
时间转换:
tick = 1000
ms = 1000
时间统计和时间延迟:
LOS_CyclePerTickGet = 495000
LOS_TickCountGet = 1
LOS_TickCountGet after delay = 201
双向链表¶
概述¶
双向链表是指含有往前和往后两个方向的链表,即每个结点中除存放下一个节点指针外,还增加一个指向前一个节点的指针。其头指针head是唯一确定的。
从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,这种数据结构形式使得双向链表在查找时更加方便,特别是大量数据的遍历。由于双向链表具有对称性,能方便地完成各种插入、删除等操作,但需要注意前后方向的操作。
开发指导¶
LiteOS的双向链表模块为用户提供下面几种功能,接口详细信息可以查看API参考。
获取包含链表的结构体地址,接口的第一个入参表示的是链表中的某个节点,第二个入参是要获取的结构体名称,第三个入参是链表在该结构体中的名称。 |
|||
双向链表的典型开发流程:
调用LOS_ListInit/LOS_DL_LIST_HEAD初始双向链表。
调用LOS_ListAdd/LOS_ListHeadInsert向链表头部插入节点。
调用LOS_ListTailInsert向链表尾部插入节点。
调用LOS_ListDelete删除指定节点。
调用LOS_ListEmpty判断链表是否为空。
调用LOS_ListDelInit删除指定节点并以此节点初始化链表。
注意事项¶
需要注意函数入参的合法性由调用者保证。
需要注意节点指针前后方向的操作。
链表操作接口为底层接口,不对入参进行判空,需要使用者确保传参合法。
如果链表节点的内存是动态申请的,删除节点时,要注意释放内存。
编程实例¶
本实例实现如下功能:
初始化双向链表。
增加节点。
删除节点。
测试操作是否成功。
代码实现如下:
#include "stdio.h"
#include "los_list.h"
#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif /* __cpluscplus */
#endif /* __cpluscplus */
static UINT32 DLlist_sample(VOID)
{
LOS_DL_LIST DLlist = {NULL,NULL};
LOS_DL_LIST DLlistNode01 = {NULL,NULL};
LOS_DL_LIST DLlistNode02 = {NULL,NULL};
LOS_DL_LIST DLlistNode03 = {NULL,NULL};
dprintf("Initial head\n");
LOS_ListInit(&DLlist);
LOS_ListAdd(&DLlist, &DLlistNode01);
if (DLlistNode01.pstNext == &DLlist && DLlistNode01.pstPrev == &DLlist) {
dprintf("Add DLlistNode01 success \n");
}
LOS_ListTailInsert(&DLlist, &DLlistNode02);
if (DLlistNode02.pstNext == &DLlist && DLlistNode02.pstPrev == &DLlistNode01) {
dprintf("Tail insert DLlistNode02 success \n");
}
LOS_ListHeadInsert(&DLlistNode02, &DLlistNode03);
if (DLlistNode03.pstNext == &DLlist && DLlistNode03.pstPrev == &DLlistNode02) {
dprintf("Head insert DLlistNode03 success \n");
}
LOS_ListDelInit(&DLlistNode03);
LOS_ListDelete(&DLlistNode01);
LOS_ListDelete(&DLlistNode02);
if (LOS_ListEmpty(&DLlist)) {
dprintf("Delete success \n");
}
return LOS_OK;
}
#ifdef __cplusplus
#if __cplusplus
}
#endif /* __cpluscplus */
#endif /* __cpluscplus */
编译运行得到的结果为:
Initial head
Add DLlistNode01 success
Tail insert DLlistNode02 success
Head insert DLlistNode03 success
Delete success
原子操作¶
概述¶
在支持多任务的操作系统中,修改一块内存区域的数据需要“读取-修改-写入”三个步骤。然而同一内存区域的数据可能同时被多个任务访问,如果在修改数据的过程中被其他任务打断,就会造成该操作的执行结果无法预知。
使用开关中断的方法固然可以保证多任务执行结果符合预期,但这种方法显然会影响系统性能。
ARMv6架构引入了LDREX和STREX指令,以支持对共享存储器更缜密的非阻塞同步。由此实现的原子操作能确保对同一数据的“读取-修改-写入”操作在它的执行期间不会被打断,即操作的原子性。
LiteOS通过对ARMv6架构中的LDREX和STREX进行封装,向用户提供了一套原子操作接口。
LDREX Rx, [Ry]
读取内存中的值,并标记对该段内存为独占访问:
读取寄存器Ry指向的4Byte内存数据,保存到Rx寄存器中。
对Ry指向的内存区域添加独占访问标记。
STREX Rf, Rx, [Ry]
检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存:
有独占访问标记
将寄存器Rx中的值更新到寄存器Ry指向的内存。
标志寄存器Rf置为0。
没有独占访问标记
不更新内存。
标志寄存器Rf置为1。
判断标志寄存器
标志寄存器为0时,退出循环,原子操作结束。
标志寄存器为1时,继续循环,重新进行原子操作。
开发指导¶
有多个任务对同一个内存数据进行加减或交换操作时,使用原子操作保证结果的可预知性。
LiteOS的原子数据包含两种类型Atomic(有符号32位数)与 Atomic64(有符号64位数)。原子操作模块为用户提供下面几种功能,接口详细信息可以查看API参考。
须知: 原子操作中,操作数及其结果不能超过函数所支持位数的最大值。
无。
注意事项¶
需注意函数入参的合法性由调用者保证。
目前原子操作接口只支持整型数据。
原子操作只能保证操作的原子性,不能保证操作不会出现翻转溢出。
编程实例¶
调用原子操作相关接口,观察结果:
创建两个任务。
任务一用LOS_AtomicInc对全局变量加100次。
任务二用LOS_AtomicDec对全局变量减100次。
子任务结束后在主任务中打印全局变量的值。
#include "los_hwi.h"
#include "los_atomic.h"
#include "los_task.h"
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
Atomic g_sum;
Atomic g_count;
UINT32 Example_Atomic01(VOID)
{
int i = 0;
for(i = 0; i < 100; ++i) {
LOS_AtomicInc(&g_sum);
}
LOS_AtomicInc(&g_count);
return LOS_OK;
}
UINT32 Example_Atomic02(VOID)
{
int i = 0;
for(i = 0; i < 100; ++i) {
LOS_AtomicDec(&g_sum);
}
LOS_AtomicInc(&g_count);
return LOS_OK;
}
UINT32 Example_TaskEntry(VOID)
{
TSK_INIT_PARAM_S stTask1={0};
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic01;
stTask1.pcName = "TestAtomicTsk1";
stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
stTask1.usTaskPrio = 4;
stTask1.uwResved = LOS_TASK_STATUS_DETACHED;
TSK_INIT_PARAM_S stTask2={0};
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic02;
stTask2.pcName = "TestAtomicTsk2";
stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
stTask2.usTaskPrio = 4;
stTask2.uwResved = LOS_TASK_STATUS_DETACHED;
LOS_TaskLock();
LOS_TaskCreate(&g_testTaskId01, &stTask1);
LOS_TaskCreate(&g_testTaskId02, &stTask2);
LOS_TaskUnlock();
while(LOS_AtomicRead(&g_count) != 2);
dprintf("g_sum = %d\n", g_sum);
return LOS_OK;
}
编译运行得到的结果为:
g_sum = 0
位操作¶
概述¶
位操作是指对二进制数的bit位进行操作。程序可以设置某一变量为状态字,状态字中的每一bit位(标志位)可以具有自定义的含义。
开发指导¶
系统提供标志位的置1和清0操作,可以改变标志位的内容,同时还提供获取状态字中标志位为1的最高位和最低位的功能。用户也可以对系统的寄存器进行位操作。
LiteOS的位操作模块为用户提供下面几种功能,接口详细信息可以查看API参考。
注意事项¶
无。
编程实例¶
对数据实现位操作,本实例实现如下功能:
某一标志位置1。
获取标志位为1的最高bit位。
某一标志位清0。
获取标志位为1的最低bit位。
#include "los_bitmap.h"
#include "los_printf.h"
static UINT32 Bit_Sample(VOID)
{
UINT32 flag = 0x10101010;
UINT16 pos;
dprintf("\nBitmap Sample!\n");
dprintf("The flag is 0x%8x\n", flag);
pos = 8;
LOS_BitmapSet(&flag, pos);
dprintf("LOS_BitmapSet:\t pos : %d, the flag is 0x%0+8x\n", pos, flag);
pos = LOS_HighBitGet(flag);
dprintf("LOS_HighBitGet:\t The highest one bit is %d, the flag is 0x%0+8x\n", pos, flag);
LOS_BitmapClr(&flag, pos);
dprintf("LOS_BitmapClr:\t pos : %d, the flag is 0x%0+8x\n", pos, flag);
pos = LOS_LowBitGet(flag);
dprintf("LOS_LowBitGet:\t The lowest one bit is %d, the flag is 0x%0+8x\n\n", pos, flag);
return LOS_OK;
}
编译运行得到结果:
Bitmap Sample!
The flag is 0x10101010
LOS_BitmapSet: pos : 8, the flag is 0x10101110
LOS_HighBitGet:The highest one bit is 28, the flag is 0x10101110
LOS_BitmapClr: pos : 28, the flag is 0x00101110
LOS_LowBitGet: The lowest one bit is 4, the flag is 0x00101110
环形缓冲(ringbuf)¶
概述¶
环形缓冲(ringbuf)作为数据的环形缓冲机制,可以实现数据流以先入先出的方式进行缓存。
typedef struct {
UINT32 startIdx; /* ringbuf读下标 */
UINT32 endIdx; /* ringbuf写下标 */
UINT32 size; /* ringbuf总大小,单位为字节 */
UINT32 remain; /* ringbuf剩余可用大小,单位为字节 */
SPIN_LOCK_S lock; /* ringbuf自旋锁 */
RingbufStatus status /* ringbuf状态 */
RingbufFlag flag; /* Ringbuf flag */
CHAR *fifo; /* ringbuf缓冲区起始地址 */
} Ringbuf;
每个ringbuf有两种状态:
RBUF_UNINIT:ringbuf未初始化。
RBUF_INITED:ringbuf已初始化。
每个ringbuf有两种模式:
RBUF_NORMAL:ringbuf正常模式。
RBUF_OVERWRITE:ringbuf可覆盖写模式。
ringbuf控制块中维护着缓冲区的起始地址,和读/写指针的相对偏移。startIdx是读指针相对于ringbuf缓冲区起始地址的偏移,endIdx是写指针的相对偏移。startIdx与endIdx之间为缓冲的数据。ringbuf刚创建时,读写指针偏移均为0。
写入数据到ringbuf时,先判断ringbuf标志。标志为RBUF_NORMAL时,处于正常模式;标志为RBUF_OVERWRITE时,处于覆盖写模式。
正常模式:写入数据到ringbuf时,先判断ringbuf是否还有剩余可用内存。当还有可用内存时才会继续写入,endIdx增长,并减少ringbuf的剩余可用内存;否则丢弃写入数据,可根据LOS_RingbufWrite()的返回值判断写入的数据量。如果ringbuf已经存满数据,因为不会覆盖旧数据,所以无法继续写入。
可覆盖写模式:写入数据到ringbuf时,先判断ringbuf要写入的数据。当要写入数据大于size时,清空ringbuf,仅写入size长度数据;当要写入数据小于size且大于剩余内存remain时,清理一部分最早缓存的数据,再写入数据;当要写入数据小于剩余内存remain时,直接写入;最后均返回实际写入长度。
读取数据时,会按照先入先出的方式,读取最早缓存的数据,startIdx增长,并增加ringbuf的剩余可用内存。读取数据后,因为读指针向后移动,所以无法再次读取相同的数据。
endIdx增长到等于startIdx时,表示ringbuf已经写满,所以无法继续写入,需要读取出数据才能继续写数据到ringbuf。
开发指导¶
ringbuf用于数据缓冲,遵循先入先出的原则。
LiteOS中的ringbuf模块提供下面几种功能,接口详细信息可以查看API参考。
使用ringbuf模块开发的典型流程如下:
注意事项¶
ringbuf写接口行为由LOS_RingbufInit接口在初始化时决定。
通过LOS_RingbufRead接口读取缓冲区的数据后,该数据无法被再次读取。
LOS_RingbufPeek接口仅预读缓冲区数据,未消费该数据,该数据仍然可以被读取。
正常模式下,缓冲区写满时,无法继续写入,请检查LOS_RingbufWrite的返回值以确认实际写入的数据量。
使用完ringbuf后,需要用户自己释放申请的内存。
编程实例¶
创建一个ringbuf,两个任务。任务1定时写入数据到ringbuf,任务2在ringbuf数据达到一定量时,取出数据。
锁任务调度,通过LOS_TaskCreate创建任务1和任务2。
申请一块内存用作环形缓冲区。
通过LOS_RingbufInit初始化一个缓冲区。
解锁任务调度。
任务1往环形缓冲区中写入数据。
任务2判断数据达到水线后,读取数据。
static Ringbuf g_Ringbuf = {0};
#define READ_TRIGGER_THRESHOLD 32
#define RINGBUF_SIZE 128
VOID WriteTask(VOID)
{
UINT32 i = 0;
UINT32 ret = 0;
CHAR data[READ_TRIGGER_THRESHOLD * 2 + 1] = "LiteOS test data, send in WriteTask, receive & check in ReadTask";
for (;;) {
ret = LOS_RingbufWrite(&g_Ringbuf, data + i, READ_TRIGGER_THRESHOLD / 2);
if (ret != READ_TRIGGER_THRESHOLD / 2) {
PRINT_ERR("real write count is %u\n", ret);
}
i += ret;
if (i >= strlen(data)) {
break;
} else {
LOS_Msleep(1000); /* 1000: sleep 1000ms */
}
}
}
VOID ReadTask(VOID)
{
UINT32 i = 0;
UINT32 ret = 0;
UINT32 count = 0;
CHAR data[READ_TRIGGER_THRESHOLD * 2 + 1] = {0};
for (;;) {
count = LOS_RingbufUsedSize(&g_Ringbuf);
if (count >= READ_TRIGGER_THRESHOLD) {
ret = LOS_RingbufRead(&g_Ringbuf, data + i, count);
i += ret;
PRINTK("read data count %u \n", i);
if (i == READ_TRIGGER_THRESHOLD * 2) {
PRINTK("data receive done: %s\n", data);
break;
}
} else {
PRINTK("info: data count %u < %u\n", count, READ_TRIGGER_THRESHOLD);
}
LOS_Msleep(1000); /* 1000: sleep 1000ms */
}
}
UINT32 SampleTaskCreat(VOID)
{
UINT32 ret = 0;
UINT32 task1, task2;
CHAR *buff = NULL;
TSK_INIT_PARAM_S initParam;
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)WriteTask;
initParam.usTaskPrio = LOSCFG_BASE_CORE_TSK_DEFAULT_PRIO;
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
initParam.pcName = "WriteTask";
#ifdef LOSCFG_KERNEL_SMP
initParam.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());
#endif
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
LOS_TaskLock();
ret = LOS_TaskCreate(&task1, &initParam);
if(ret != LOS_OK) {
PRINT_ERR("create WriteTask failed, error: %x\n", ret);
return ret;
}
initParam.pcName = "ReadTask";
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ReadTask;
ret = LOS_TaskCreate(&task2, &initParam);
if(ret != LOS_OK) {
PRINT_ERR("create ReadTask failed, error: %x\n", ret);
return ret;
}
buff = (CHAR *)LOS_MemAlloc(m_aucSysMem0, RINGBUF_SIZE);
if (buff == NULL) {
PRINT_ERR("malloc falied");
return LOS_NOK;
}
ret = LOS_RingbufInit(&g_Ringbuf, buff, RINGBUF_SIZE, RBUF_NORMAL);
if (ret != LOS_OK) {
PRINT_ERR("ringbuf init falied");
return LOS_NOK;
}
PRINTK("create the ringbuf success!\n");
LOS_TaskUnlock();
return ret;
}
编译运行得到的结果为:
create the ringbuf success!
info: data count 16 < 32
read data count 32
info: data count 16 < 32
read data count 64
data receive done: LiteOS test data, send in WriteTask, receive & check in ReadTask
通用内存访问¶
概述¶
当寄存器或内存位于内存空间时,称为IO内存。LiteOS提供了一组通用接口来操作I/O内存。
开发指导¶
系统提供读写(1、2、4、8)字节寄存器操作,方便编写驱动代码。
LiteOS为用户提供下面几种功能,接口详细信息可以查看API参考。
须知: 如果32位ARM架构写64位寄存器通过str指令访问,会导致高32位寄存器地址和低32位寄存器地址的数据不能同时生效,因此通过strd指令实现。
注意事项¶
无。
扩展内核¶
动态加载¶
概述¶
动态加载是一种程序加载技术。
静态链接是在链接阶段将程序各模块文件链接成一个完整的可执行文件,运行时作为整体一次性加载进内存。动态加载允许用户将程序各模块编译成独立的.o文件和.so文件,而不将它们链接成一个可执行文件,在需要使用某模块时再动态地将其加载进内存。
LiteOS支持动态加载OBJ目标文件(.o文件)和SO共享目标文件(.so文件),需要目标文件、系统二进制镜像文件配合使用。
符号表在表现形式上是记录了符号名及其所在内存地址的数组。符号表在动态加载模块初始化时被载入到动态加载模块的符号管理结构中。在加载用户模块进行符号重定位时,动态加载模块通过查找符号管理结构得到相应符号所在地址。
对于so文件,可能以ZIP(压缩)或者NOZIP(非压缩)的形式存放在存储介质中,相应的有两种加载策略。
NOZIP策略:普通的so文件可以通过多次lseek与read获取相应的文件信息。首先根据so文件的 SegmentHeader信息,计算出对应的可Load段的大小,然后分配出相应的空间,将相应的可Load段加载到内存并完成动态加载工作。因为每次仅仅读取文件的一小部分,不存在内存浪费问题。
ZIP策略:读取ZIP压缩文件时,必须一次性将文件全部读取到内存中(mem1),但是第一次读取时并不知道对应的so文件中可Load段的大小,所以需要二次分配内存(mem2)。因为mem2中已经具备了本次动态加载所需的所有信息,如果让mem1和mem2并存就会造成内存浪费并导致内存峰值过高。解决办法是分两次读取ZIP文件,第一次读取的目的仅仅是计算最终所需的内存大小(计算完毕,立刻释放相应内存),第二次读取的目的才是根据相应信息完成动态加载工作。可见,ZIP加载策略是以牺牲指令为代价来避免内存峰值过高的一种策略。
开发指导¶
静态链接将程序各模块文件链接成一个整体,运行时一次性加载进内存,具有代码装载速度快等优点。但当程序规模较大,模块变更升级较为频繁时,会存在内存和磁盘空间浪费、模块更新困难等问题。
动态加载技术可以较好地解决上述静态链接中存在的问题,在程序需要执行外部模块中的代码时,动态地将外部模块加载进内存,不需要该模块时再卸载,可以实现公共代码的共享以及模块的平滑升级等功能。
LiteOS 的动态加载模块为用户提供下面几种功能,接口详细信息可以查看API参考。
设置so模块的动态加载参数,使用该接口需要打开LOSCFG_DYNLOAD_DYN_FROM_FS宏开关,即只支持从文件系统中加载so模块时才能设置其动态加载参数。 |
||
说明: LOS_DynMemPoolSet接口入参必须是经过LOS_MemInit初始化的内存池地址,即通过LiteOS内存管理算法管理,并且保证该内存池与系统内存池不重合。该接口需在加载.so或者.o文件前使用。
须知: 不同目标平台的内存保护/内存管理单元设计可能存在差异,在使用本章节提供的接口前,请务必阅读目标平台的相关文档以理解平台硬件能力和使用限制。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
动态加载主要有以下几个步骤:
准备编译环境
编译待加载的模块文件
编写动态加载的业务代码
编译系统镜像
准备系统环境
添加.o和.so模块编译选项。
.o模块的编译选项中需要添加“-nostdlib -fno-PIC”选项。
.so模块的编译选项中需要添加“-nostdlib -fPIC -shared”选项。
说明:
以下列出的编译选项,可以根据实际需要自行选择。
-z max-page-size=value
设置.o和.so模块可加载的program segment的对齐参数为value值,添加该选项,可以有效减少各相邻可加载的segment的虚拟地址之间由于对齐需要而产生的空白区域。如果不添加该选项,默认对齐参数为0x10000。
须知:IPC的动态加载需要用户保证所提供的模块文件中所有LD_SHT_PROGBITS、LD_SHT_NOBITS类型的section起始地址都是4字节对齐,否则拒绝加载该模块。
生成.o和.so模块时禁止链接编译器中的标准库(即不允许使用-ldl、-lpthread、-lc等),且务必使用编译选项“-nostdlib”。
.o和.so模块编译选项添加示例如下:
RM = -rm -rf CC = arm-himix100-linux-gcc SRCS = $(wildcard *.c) OBJS = $(patsubst %.c,%.o,$(SRCS)) SOS = $(patsubst %.c,%.so,$(SRCS)) all: $(SOS) $(OBJS): %.o : %.c @$(CC) -nostdlib -c $< -fno-PIC -o $@ $(SOS): %.so : %.c @$(CC) -nostdlib $< -fPIC -shared -o $@ clean: @$(RM) $(SOS) $(OBJS) .PHONY: all clean
修改系统的Makefile。
编译系统镜像的Makefile必须include根目录下的“config.mk”文件,并使用“config.mk”中的“LITEOS_CFLAGS”或“LITEOS_CXXFLAGS”编译选项,示例如下:
LITEOSTOPDIR ?= ../.. SAMPLE_OUT = . include $(LITEOSTOPDIR)/config.mk RM = -rm -rf LITEOS_LIBDEPS := --start-group $(LITEOS_LIBDEP) --end-group SRCS = $(wildcard sample.c) OBJS = $(patsubst %.c,$(SAMPLE_OUT)/%.o,$(SRCS)) all: $(OBJS) clean: @$(RM) *.o sample *.bin *.map *.asm $(OBJS): $(SAMPLE_OUT)/%.o : %.c $(CC) $(LITEOS_CFLAGS) -c $< -o $@ $(LD) $(LITEOS_LDFLAGS) -uinit_jffspar_param --gc-sections -Map=$(SAMPLE_OUT)/sample.map -o $ (SAMPLE_OUT)/sample ./$@ $(LITEOS_LIBDEPS) $(LITEOS_TABLES_LDFLAGS) $(LITEOS_DYNLDFLAGS) $(OBJCOPY) -O binary $(SAMPLE_OUT)/sample $(SAMPLE_OUT)/sample.bin $(OBJDUMP) -d $(SAMPLE_OUT)/sample >$(SAMPLE_OUT)/sample.asm
请严格按如下步骤进行编译。以makefile为例介绍。
编译.o和.so模块,并将运行所需的所有.o和.so文件拷贝到同一目录下。
说明:
以下列出的编译选项,可以根据实际需要自行选择。
-z max-page-size=value
设置.o和.so模块可加载的program segment的对齐参数为value值,添加该选项,可以有效减少各相邻可加载的segment的虚拟地址之间由于对齐需要而产生的空白区域。如果不添加该选项,默认对齐参数为0x10000。进入“RTOS_Lite/build/scripts/dynload_tools”目录执行“sym.sh”脚本,示例如下:
LITEOSTOPDIR ?= ../..
说明:sym.sh脚本的入参“/home/wmin/customer/out”,是.o和.so文件所在的目录的绝对路径。“out/dynload_sym”为指定“los_dynload_gsymbol.c”生成目录的绝对路径,建议指定路径为工程的编译生成目录。
注意如果所需加载的.o或.so被更新了,需要重新执行该脚本。该脚本会提取.o和.so文件中的所有系统符号(非用户定义的符号),以便在编译系统镜像时由编译器计算出对应符号的地址。
必须要在“RTOS_Lite/build/scripts/dynload_tools”目录下执行该脚本。
执行完“sym.sh”脚本后,确保“out/dynload_sym”目录下生成了“los_dynload_gsymbol.c”,并且保证该文件在编译阶段参与编译,以及生成的目标文件参与链接。否则,在加载.o和.so过程中会出现符号不能定位的问题。
(可选)进入“RTOS_Lite/build/scripts/dynload_tools”执行litesym生成小型化可加载的文件:
例如需要加载的文件为a.o, 当使能LOSCFG_DYNLOAD_LITE_SYMTAB时,需要将原始文件处理成自定义格式才能正确加载,可以用如下的命令生成。
$ python3 litesym /home/wmin/customer/out/a.o -o /home/wmin/customer/out/a_litesym.o
说明:使能LOSCFG_DYNLOAD_LITE_SYMTAB时,加载的文件必须是经过symtab脚本处理过的,否则加载会失败。
如果不需要使用LOSCFG_DYNLOAD_LITE_SYMTAB,不需要执行此脚本
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Dynamic Load Feature菜单,完成动态加载模块的配置。
LOSCFG_DYNLOAD_REL_FROM_MEM && !LOSCFG_DYNLOAD_REL_FROM_FS &&
须知:
如果同时使能LOSCFG_KERNEL_NX(数据段不可执行,依赖Cortex-A芯片,其在menuconfig中对应的菜单项为:Kernel ---> Enable Data Sec NX Feature)和动态加载模块,则只支持动态加载.so文件,其他形式的加载可能会有异常,同时需要配置动态加载使用的堆大小(位于系统动态内存池的尾部)设置动态加载使用的内存池地址(可选)。
调用LOS_DynMemPoolSet接口可以设置动态加载使用的内存池。如果不设置,默认使用系统内存池。
设置so文件的动态加载策略。
在不同的应用场景下,so文件可能以ZIP或者NOZIP的形式存放于存储介质中。如果需要从文件系统中加载so文件,由于压缩文件与非压缩文件读写操作的差异性,在初始化动态加载模块之前需要指明具体的加载策略。
DYNLOAD_PARAM_S dynloadParam = {ZIP}; // 设置ZIP或NOZIP策略 LOS_DynParamReg(&dynloadParam); // 设置具体的加载策略
说明:
以ZIP格式存储的so文件必须采用ZIP加载策略,而普通的so文件使用上述两种策略都可以加载成功,建议使用NOZIP策略。如果没有设置,默认采用NOZIP加载策略。使用相对路径(可选)。
如果在动态加载模块时想使用相对路径,可以通过LOS_PathAdd接口添加.so和.o文件所在的绝对路径:
ret = LOS_PathAdd("/yaffs/bin/dynload"); if (ret != LOS_OK) { printf("add relative path failed"); return 1; }
添加路径后,调用LOS_SoLoad、LOS_ObjLoad、LOS_MemLoad接口时传入文件名即可,动态加载会在添加的路径下查找指定文件。
说明:只有在调用LOS_PathAdd接口添加路径后,才能在调用动态加载模块接口时使用相对路径。
可以多次调用LOS_PathAdd接口添加多个相对路径。
如果添加的多个路径下有相同文件名的模块,则在加载模块时按照添加的先后依次在所有路径中查找,且只加载第一个查找到的文件。
加载用户模块。
动态加载模块支持加载.o和.so模块。
使用LOS_ObjLoad接口动态加载obj文件(注意只能从文件系统中加载obj模块):
if ((handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o")) == NULL) { printf("load module ERROR!!!!!!\n"); return 1; }
使用LOS_SoLoad接口动态加载so文件:
if ((handle = LOS_SoLoad("/yaffs/bin/dynload/foo.so")) == NULL) { printf("load module ERROR!!!!!!\n"); return 1; }
说明:
对于so文件的动态加载:如果模块A需要模块B,即模块A依赖模块B,如果明确指明了A.so依赖B.so(编译A.so时将B.so作为编译参数),那么加载A模块时会自动将B模块也加载进来。如果没有明确指明A模块与B模块的依赖关系,那么在加载A模块之前,必须保证B模块已经被成功加载。
对于不支持文件系统的平台,支持直接从指定内存空间动态加载so,可以调用LOS_MemLoad接口实现。
获取用户模块中的符号地址。
在特定用户模块中查找符号、
需要在某个特定用户模块中查找符号地址时,调用LOS_FindSymByName接口,并将LOS_FindSymByName的第一个参数置为需要查找的用户模块的句柄。
if ((magic = LOS_FindSymByName(handle, "os_symbol_table")) == NULL) { printf("symbol not found\n"); return 1; }
在全局符号表中查找符号
需要在全局符号表(即OS模块,包括本模块和所有其他用户模块)中查找某个符号地址时,调用LOS_FindSymByName接口,并将LOS_FindSymByName的第一个参数置NULL。
if ((funTestCase0 = LOS_FindSymByName(NULL, "printf")) == NULL) { printf("symbol not found\n"); return 1; }
使用获取到的符号地址。
LOS_FindSymByName返回符号地址(VOID *指针),对该符号地址转换类型后,可以使用该符号。下面针对数据类型符号和函数类型符号举例说明。
整数类型符号
现有待加载的test.c,有一全局变量UINT32 g_test = 0,可以通过如下代码获取g_test的地址。
const char *g_pscOsOSSymtblFilePath = "/yaffs/bin/dynload/test.so"; UINT32 * g_testPtr = NULL; INT8 *ptr = (INT8 *)NULL; if ((pOSSymtblHandler = LOS_SoLoad(g_pscOsOSSymtblFilePath)) == NULL) { return LOS_NOK; } if ((ptr = LOS_FindSymByName(pOSSymtblHandler, "g_test")) == NULL) { printf("g_uwTest not found\n"); return LOS_NOK; } g_testPtr = (UINT32 *)ptr; /* 强制类型转换成真实的指针类型 */
函数类型符号
foo.c中定义了一个无参的函数test_0和一个有两个参数的函数test_2,编译生成foo.o。
foo.c: int test_0(void) { return 0; } int test_2(int i, int j) { return 0; }
以下代码演示在demo.c中获取foo.o模块中的函数并调用。
demo.c: typedef int (* TST_CASE_FUNC)(); /* 无形参函数指针类型声明 */ typedef int (* TST_CASE_FUNC1)(UINT32); /* 单形参函数指针类型声明 */ typedef int (* TST_CASE_FUNC2)(UINT32, UINT32); /* 双形参函数指针类型声明 */ TST_CASE_FUNC funTestCase0 = NULL; /* 函数指针定义 */ TST_CASE_FUNC2 funTestCase2 = NULL; int ret; handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o"); funTestCase0 = LOS_FindSymByName(handle, "test_0"); if (funTestCase0 == NULL) { printf("can not find the function name\n"); return 1; } ret = funTestCase0(); funTestCase2 = LOS_FindSymByName(NULL, "test_2"); if (funTestCase2 == NULL){ printf("can not find the function name\n"); return 1; } ret = funTestCase2(42, 57);
卸载模块。
调用LOS_ModuleUnload接口卸载某个模块,将需要卸载的模块句柄作为参数传入该接口。对于已被加载过的obj或so文件的句柄,卸载时统一使用LOS_ModuleUnload接口。
ret = LOS_ModuleUnload(handle); if (ret != LOS_OK) { printf("unload module failed"); return 1; }
销毁动态加载模块。
不再需要动态加载功能时,调用LOS_LdDestroy接口,销毁动态加载模块。
须知:销毁动态加载模块时会自动卸载所有已被加载的模块,销毁后模块不能再使用。
销毁动态加载模块前需确认模块不再使用。
在RTOS_Lite源码根目录下执行make,编译系统镜像。
编译完成后,在根目录out/平台名的目录下,可以看到生成的系统镜像“vs_server.bin”文件。
说明: 如果待加载的.o和.so中包含了未定义的外部符号(既没有定义在这些.o和.so文件中,也不是一个合法的系统全局符号),在编译系统镜像文件时会提示相应错误,需排查错误信息,确保系统镜像编译正确。
.so文件(或.o文件)需要和系统镜像文件配合使用。
如果选择从文件系统加载模块,则模块文件必须放置在文件系统中,例如YAFFS、FAT等文件系统。
如果选择从指定内存空间加载,则忽略下文的2,只需要将模块文件烧写到指定内存空间即可。
建议操作顺序:
烧写系统镜像文件到开发板的flash中。
将.so文件(或.o文件)存储到开发板的flash中,这里分为两种情况:
如果模块文件保存在可热拔插的SD卡设备上,直接将SD卡插到开发板上即可。如果需要更新.so文件(或.o文件),可将SD卡插到电脑上更新。
如果模块文件保存在不可热插拔的存储设备上,可通过如下两种方式更新文件:
将模块文件编译进文件系统镜像,然后烧写文件系统镜像到开发板的flash中。
启动LiteOS系统后,通过tftp命令下载.so文件(或.o文件),示例命令如下:
tftp -g -l /yaffs/bin/dynload/foo.so -r foo.so 10.67.211.235
启动系统进行验证
注意事项¶
编译选项:
.o模块的编译选项中需要添加“-nostdlib -fno-PIC”选项。
.so模块的编译选项中需要添加“-nostdlib -fPIC -shared”选项。
安全性:
对于加载的模块文件,需要提供者保证它的来源可靠并且安全。强烈建议禁止应用系统从SD卡/U盘等外部高风险介质加载文件,如果确实需要,则由提供者/应用层保证文件的可靠性,如果由此产生损失或者问题,华为不承担任何责任。
如果被加载的文件有问题,可能导致包括但不限于设备损坏、数据泄露/被篡改等一系列的问题,对此华为不承担任何责任。
其他限制:
在编译系统镜像之前必须确保所需加载的.o与.so已经就绪,这样才能保证后续在编译系统镜像文件时,这些.o和.so所调用到的外部符号信息已经集成到最终的系统镜像中。
在加载.o和.so模块时,一旦发现该模块引用的外部符号被重复定义在其它没有依赖关系的模块中时,会拒绝对本次引用做重定位,导致加载该模块失败。为了避免使用异常,用户应该事先确保所有待加载的模块内部没有重复定义的符号(变量或函数接口)。
编程实例¶
实例描述
示例中,sample_foo.c中定义了一个无参的函数test_0和一个有两个参数的函数test_2,编译生成foo.o。在sample_Dynamic_loading.c中演示获取foo.o模块中的函数并调用。 sample_Dynamic_loading.c代码实现如下:
编程示例
typedefint (* TST_CASE_FUNC)(VOID); /* 无形参函数指针类型声明 */
typedefint (* TST_CASE_FUNC2)(UINT32, UINT32); /* 双形参函数指针类型声明 */
TST_CASE_FUNC funTestCase0 = NULL; /* 函数指针定义 */
TST_CASE_FUNC2 funTestCase2 = NULL;
int ret;
void *handle = NULL;
handle = LOS_ObjLoad("/jffs0/bin/dynload/dynload_test.o");
funTestCase0 = LOS_FindSymByName(handle, "test_0");
if (funTestCase0 == NULL) {
printf("can not find the function name\n");
return;
}
ret = funTestCase0(); /* 调用该函数指针 */
funTestCase2 = LOS_FindSymByName(NULL, "test_2");
if (funTestCase2 == NULL){
printf("can not find the function name\n");
return;
}
ret = funTestCase2(42, 57); /* 调用该函数指针 */
ret = LOS_ModuleUnload(handle);
if (ret != LOS_OK) {
printf("unload module failed");
return;
}
LOS_LdDestroy();
编译运行得到的结果为:
Huawei LiteOS#
test_0
test_2: 42 57
低功耗框架¶
概述¶
低功耗框架是低功耗模块的统一管理框架,提供对Tickless、系统休眠、休眠唤醒等多种低功耗能力的支持。
低功耗框架默认使能Tickless。此外,由于低功耗特性与具体业务和硬件有较强的相关性,为了更好地适配硬件、扩展功能,低功耗框架开放了相关功能接口方便开发者适配。同时,在默认的低功耗框架中也可以自定义具体策略。
注册低功耗框架休眠入口函数,系统idle时调用该休眠函数(可不注册,LOS_PowerMgrInit初始化时会注册默认入口,使用系统自带的低功耗框架)。 |
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为低功耗模块,值为0x21。
Tickless¶
概述¶
Tickless机制是一种新的定时机制,它使用动态时钟中断替代周期性时钟定时。
任务调度通常通过时钟中断触发。在周期性定时机制下,每一次时钟中断都会检测当前调度条件是否满足,但能触发任务调度的时钟中断往往只占很小一部分。软件定时器任务必须通过时钟中断触发才能实现,每一次时钟中断也会检测软件定时器是否到期,若到期则调度该定时器的回调函数,而大部分时钟中断发生时并没有软件定时器到期。Tickless机制是通过预期省略不必要的时钟中断,确定下一次“有意义”的时钟中断的产生时刻,而不让“无意义”的时钟中断产生。通过省去CPU空闲时期无意义的时钟中断,可以有效降低系统的功耗。
LiteOS的Tickless机制,采用的是“idle模式”,即在CPU空闲时期(idle任务中)开启Tickless机制。通过计算下一次任务调度的时间和下一次软件定时器到期的时间,将两者中的较小值设定为下一次时钟中断到来的时间,从而减少无意义的时钟中断。
开发指导¶
系统CPU长时间处于空闲状态,且系统对功耗要求较高。
Tickless的典型开发流程:
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Low Power Management Framework ---> Low Power Management Configure 菜单,使能Tickless。
当系统长时间不运行业务时,进入idle任务后根据可休眠时间自动进入tickless模式。
无。
注意事项¶
由于Tickless机制与系统时间都通过Tick中断定时器计时,因此在时间精度要求极高的场景下不建议使用。
各平台由于Systick寄存器位数差异,为避免寄存器溢出,在配置tickless休眠时间时,需要根据寄存器可接受最大数值、系统时钟频率、LOSCFG_BASE_CORE_TICK_PER_SECOND计算tickless可正常运行的最大tick数。例如cortex-m4平台由于SysTick->LOAD寄存器为24位,系统时钟频率为120M,配置LOSCFG_BASE_CORE_TICK_PER_SECOND为1000,则配置tickless休眠时间应不超过对应阈值:(2^24 - 1) / (120000000 / 1000) = 139 ticks。
系统休眠¶
概述¶
系统休眠是一种兼顾系统性能和能耗的低功耗机制,用户可通过自行配置休眠策略,在待机场景下调整系统时钟频率、调整外设工作模式或下电等,从而节省功耗,提升续航能力。
系统休眠的低功耗管控主要支持:
睡眠模式。
睡眠投票机制。
系统调频。
目前,系统休眠支持浅睡、深睡两种模式。系统休眠默认包含浅睡逻辑,若用户有自定义休眠逻辑或深睡的需求,可以注册低功耗流程结构体struct PowerMgrParameter。
typedef struct {
PowerMgrRunOps runOps; /* power manager run operations */
} PowerMgrParameter;
typedef struct {
#ifdef LOSCFG_KERNEL_DYN_FREQ
UINT32 (*changeFreq)(LosFreqMode freq); /**< Tune system frequency */
#endif
#if defined (LOSCFG_KERNEL_LIGHTSLEEP) || defined (LOSCFG_KERNEL_DEEPSLEEP)
LosSleepMode (*getSleepMode)(VOID); /**< GetSleepMode, provided for special needs before entering sleep */
#endif
#ifdef LOSCFG_KERNEL_LIGHTSLEEP
VOID (*enterLightSleep)(VOID); /**< Enter light sleep */
#endif
#ifdef LOSCFG_KERNEL_DEEPSLEEP
#ifdef LOSCFG_LOWPOWER_SLEEP_USERCONFIG
VOID (*userPreConfig)(VOID); /**< UserPreconfig, provided for special needs before entering sleep */
VOID (*userPostConfig)(VOID); /**< UserPostconfig, provided for special needs after wakeup */
#endif
VOID (*systemWakeup)(VOID); /**< System wakeup */
VOID (*suspendConfig)(VOID); /**< Supend device before entering deep sleep */
VOID (*resumeConfig)(VOID); /**< Resume device after wakeup from deep sleep */
#ifdef LOSCFG_KERNEL_SMP
VOID (*otherCoreResume)(VOID); /**< Other core Resume for multi-core scenes */
#endif
VOID (*enterDeepSleep)(VOID); /**< Enter deep sleep */
VOID (*setWakeUpTimer)(UINT32 timeout); /**< Set wakeup timer */
UINT32 (*withdrawWakeUpTimer)(VOID); /**< Withdraw wakeup timer */
#ifdef LOSCFG_KERNEL_RAM_SAVE
VOID (*contextSave)(VOID); /**< Context save */
VOID (*contextRestore)(VOID); /**< Context restore */
#endif
#endif
#ifdef LOSCFG_LOWPOWER_SLEEP_WFI
VOID (*enterWFI)(VOID); /**< Enter WFI mode */
#endif
} PowerMgrRunOps;
开发指导¶
用户可根据具体业务场景制定休眠策略,定义禁止/允许进入的睡眠模式、选择不同的睡眠模式。由于深睡等模式可能涉及到具体硬件和时钟,所以用户需自行实现相应函数以适配硬件。同时系统休眠模块在流程初始和结束阶段均有预留接口,对于有特殊需求的场景,可以进行睡眠流程的扩展。
系统休眠模块为用户提供如下功能,接口详细信息可以查看API参考。
默认的系统休眠流程如图1所示。
在进入系统休眠时,首先进行睡眠前期的配置,根据系统可睡眠时间及用户定义的休眠策略,获取睡眠模式。
若为深睡模式,则会执行深睡前期的配置,例如屏蔽中断、保存设备状态等。随后将挂起设备并设置唤醒源,系统进入深睡模式。
若为浅睡或其他模式,则执行对应流程。通常情况下,Tickless将配合系统休眠浅睡共同使用,以屏蔽Tick中断。用户也可使用自定义的Tick屏蔽和恢复机制。默认浅睡仅进入Tickless+WFI模式。
系统休眠的典型开发流程:
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Low Power Management Framework ---> Low Power Management Configure菜单。
在Menuconfig中配置系统休眠唤醒策略,包括最小浅睡/深睡阈值、最大睡眠阈值;如果开启深睡模式,根据需要选择是否开启自定义WFI、休眠前后处理、深睡tick补偿以及外部时钟频率等。
适配对应睡眠模式的休眠接口,详见“低功耗流程注册结构体”。
当系统业务繁忙时,调用LOS_PowerMgrSleepLock接口禁止进入指定的睡眠模式(深睡、浅睡);当系统业务空闲时,调用LOS_PowerMgrSleepUnLock接口允许进入指定的睡眠模式。注意这两接口应该是配套使用的。
系统待机时,根据业务投票及系统实际可休眠时长,自动进入相应休眠模式进行休眠。
注意事项¶
开启系统深睡模式时,建议在”低功耗流程注册结构体”的子结构体成员struct PowerMgrRunOps的enterDeepSleep钩子函数中将cache写回内存。
低功耗提供的调频机制,如果在系统启动之后和业务首次调频之前进入空闲状态,系统会默认把频率调到最高。
编程实例¶
本实例在cortex-M架构的CPU下实现如下功能:
注册低功耗流程框架,适配深睡、RTC使能等功能接口;
创建用户任务,并在任务中延时5000Ticks,使系统进入深睡模式。
前提条件:在menuconfig菜单中完成系统休眠深睡的配置,同时配置用户休眠阈值:LOSCFG_MIN_SLEEP_TIME、LOSCFG_MAX_SLEEP_TIME以及LOSCFG_MIN_DEEP_SLEEP_TIME。
部分代码实现如下:
注册低功耗框架
#ifdef LOSCFG_KERNEL_LOWPOWER #include "los_lowpower.h" #endif const PowerMgrParameter gPowerMgrParameter = { // 用户自定义系统休眠配置,成员函数用户自行按需实现 .runOps = { #ifdefLOSCFG_KERNEL_LIGHTSLEEP .enterLightSleep = EnterLightSleep, // 浅睡动作,此处设置为进入WFI模式 .getSleepMode = GetSleepMode, // 获取用户休眠模式 #endif #ifdefLOSCFG_KERNEL_DEEPSLEEP .suspendConfig = SuspendDevice, // 关闭外设动作 .resumeConfig = ResumeDevice, // 恢复外设 .enterDeepSleep = EnterDeepSleep, // 深睡动作,此处设置为STM32的standby模式 .setWakeUpTimer = SetWakeUpTimer, // 设置唤醒源,此处设置为根据入参(休眠时间)使能外部RTC .withdrawWakeUpTimer = WithdrawWakeUpTimer, // 取消唤醒源,此处设置为停止RTC计时 #endif } }; INT32 main(VOID) { UINT32 ret; #ifdef LOSCFG_KERNEL_LOWPOWER // 注册低功耗流程框架,若仅需默认的Tickless及浅睡模式,可调用LOS_PowerMgrInit(NULL)注册系统默认框架。 LOS_PowerMgrInit(gPowerMgrParameter); #endif HardwareInit(); printf("[main]: Hello, welcome to liteos !\n"); PRINT_RELEASE("********Hello LiteOS ********\n" "\nversion : %s\nopen-version : %s %u.%u.%u\n" "build data : %s %s\n\n" "**********************************\n", HW_LITEOS_VER, HW_LITEOS_SYSNAME, MAJ_V, MIN_V, REL_V, __DATE__, __TIME__); OsSetMainTask(); OsCurrTaskSet(OsGetMainTask()); ret = OsMain(); if (ret != LOS_OK) { return LOS_NOK; } RTC_Start(); g_kernelInit = 1; OsStart(); return LOS_OK; }
创建任务,并在任务入口函数中执行延时操作:
STATIC VOID task_demo1(VOID) { while (1) { osDelay(5000); // 任务延时5000Ticks } }
由于设置的深睡阈值为2000,任务延时5000Ticks,所以系统会进入深睡模式:

如上图所示,系统每隔5s将会从standby模式唤醒。
休眠唤醒¶
概述¶
休眠唤醒(RunStop)是一套保存系统现场镜像以及从现场镜像中快速恢复运行的机制。
用户在系统运行的某一时刻调用休眠唤醒接口,可以将系统此时的状态快照(CPU现场及内存)保存到Flash上。系统重启后,可以通过快照恢复系统,使系统从快照保存的状态处继续运行。
业务模块初始化并保持稳定运行后,调用休眠唤醒接口保存系统快照,这份快照包含了业务模块所需的所有CPU现场及内存状态。系统运行一段时间后,业务模块空闲时,主核主动下电进入低功耗模式。当有业务需要处理时,MCU发送特定数据包唤醒主核上电,硬件状态恢复后,系统从Flash快照处恢复运行,这样系统重新上电后无需重新初始化业务模块,从而实现业务模块的快速唤醒。
开发指导¶
如果希望系统运行一段时间后进入低功耗模式,当有业务到来时,系统能够快速恢复到低功耗模式前的状态,从而快速恢复业务处理能力。对于这种应用场景,可以通过休眠唤醒实现。
在某项目中,休眠唤醒被用于实现芯片的休眠唤醒:系统业务运行稳定后,保存当前系统状态的快照。当系统进入空闲状态时,主核主动下电进入低功耗模式,当业务消息到来时MMU对主核上电,系统从快照处快速启动,恢复执行。以WIFI芯片为例,RunStop可以实现主核在低功耗模式下WIFI状态不断连,以及WIFI状态的快速唤醒。
休眠唤醒模块为用户提供如下功能,接口详细信息可以查看API参考。
使能低功耗框架和休眠唤醒。
打开菜单,相关配置项的菜单路径为:Kernel ---> Enable Extend Kernel ---> Enable Low Power Management Framework ---> Low Power Management Configure。
休眠唤醒依赖Flash、Uboot和bestfit内存管理算法。Nand Flash的宏开关为LOSCFG_DRIVERS_MTD_NAND,spi_nor flash的宏开关为LOSCFG_DRIVERS_MTD_SPI_NOR
初始化低功耗框架。
入口函数main中,在OsStart前调用 LOS_PowerMgrInit 初始化低功耗框架为系统默认框架,示例代码如下:
#ifdef LOSCFG_KERNEL_LOWPOWER #include "los_lowpower.h" const PowerMgrRunOps runOps = { .getSleepMode = NULL, }; UINT32 OsPreSystemConfig(VOID) { PowerMgrParameter param; param.runOps = runOps; return LOS_PowerMgrInit(¶m); } #endif LITE_OS_SEC_TEXT_INIT int main(void) { ...... #ifdef LOSCFG_KERNEL_LOWPOWER ret = OsPreSystemConfig(); if (ret != LOS_OK) { return LOS_NOK; } #endif OsStart(); ...... }
-
业务代码入口函数为app_init,该函数位于源码根目录的“self_src/targets/board/$(LITEOS_PLATFORM)/os_adapt/os_adapt.c“文件中。
在快照业务代码后调用LOS_MakeImage将系统当前的状态快照保存到指定介质中,并通过MAKE_WOW_IMAGE宏控制非快照业务代码的编译,以实现快照镜像和全部镜像的编译,示例代码如下。
void wakeup_callback(void) { #ifdef LOSCFG_KERNEL_CPUP LOS_CpupReset(); #endif (VOID)LOS_HwiEnable(83); } #define NOR_ERASE_ALIGN_SIZE (64 * 1024) #define NOR_READ_ALIGN_SIZE 1 #define NOR_WRITE_ALIGN_SIZE 1 int flash_read(void *memaddr, size_t start, size_t size) { return spiflash_read(memaddr, start, size); } int flash_write(void *memaddr, size_t start, size_t size) { spiflash_erase(start, size); return spiflash_write(memaddr, start, size); } void app_init(void) { #ifdef LOSCFG_KERNEL_RUNSTOP RUNSTOP_PARAM_S stRunstopParam; // 定义一个LOS_MakeImage的参数变量 #endif #ifdef LOSCFG_DRIVERS_MTD_SPI_NOR (void)spinor_init(); mtd = get_mtd("spinor"); #endif #if defined(LOSCFG_KERNEL_RUNSTOP) && defined(LOSCFG_DRIVERS_MTD_SPI_NOR) memset(&stRunstopParam, 0, sizeof(RUNSTOP_PARAM_S)); /* LOS_MakeImage参数配置 */ stRunstopParam.pfFlashReadFunc = flash_read; // 指定介质读函数 stRunstopParam.pfFlashWriteFunc = flash_write; // 指定介质写函数 stRunstopParam.pfIdleWakeupCallback = idle_wakeup_callback; // 指定从快照恢复后在idle任务中需要执行的回调函数 stRunstopParam.pfImageDoneCallback = NULL; // 指定快照镜像生成后的回调函数,此处为NULL stRunstopParam.pfWakeupCallback = wakeup_callback; // 指定从快照恢复后需要执行的回调函数 stRunstopParam.uwFlashEraseAlignSize = NOR_ERASE_ALIGN_SIZE; // 介质的擦除对齐参数 stRunstopParam.uwFlashWriteAlignSize = NOR_WRITE_ALIGN_SIZE; // 介质的写对齐参数 stRunstopParam.uwFlashReadAlignSize = NOR_READ_ALIGN_SIZE; // 介质的读对齐参数 stRunstopParam.uwImageFlashAddr = 0x100000; // 烧写系统全镜像的介质起始地址 stRunstopParam.uwWowFlashAddr = 0x800000; // 保存快照的介质起始地址 LOS_MakeImage(&stRunstopParam); #endif #ifndef MAKE_WOW_IMAGE #ifdef LOSCFG_KERNEL_RUNSTOP PRINTK("Image length 0x%x\n", OsWowImageSizeGet()); #endif #endif /* MAKE_WOW_IMAGE */ }
配置WOW_SRC变量。
在源码根目录“build/make/wow_scatter.mk”文件中配置WOW_SRC变量,将变量定义为调用休眠唤醒函数的业务源文件路径,如下所示,其中LITEOSTOPDIR指代“RTOS_Lite/self_src”源码目录。
WOW_SRC := $(LITEOSTOPDIR)/targets/$(LITEOS_PLATFORM)/os_adapt/os_adapt.c编译系统镜像。
在根目录下执行make命令,编译全部业务代码。
RTOS_Lite$ make编译完成后,查看镜像段的排布,以验证快照镜像是否生成成功。进入系统镜像生成目录(例如Hi3556V200平台的镜像生成目录为“out/hi3556v200”,其他类推),可以看到生成的系统ELF文件vs_server,执行“readelf -S vs_server”命令查看section头信息,结果如图2所示。
图 2 系统ELF文件vs_server readelf结果

图2中显示了与休眠唤醒有关的段信息(包括段的名称、起始地址及偏移大小)。其中.wow_rodata为休眠唤醒镜像的只读数据段,.wow_text为代码段,.wow_data为数据段,.wow_bss为bss段。
查看休眠唤醒链接脚本“RTOS_Lite/self_src/tools/scripts/ld/wow.ld”的.text段,可以看到新增了wow.O(*.text*),它实现了将休眠唤醒的快照部分代码相关符号整合到同一个段中,如图3所示。
将全部镜像烧写到Flash。
进入串口工具界面,可以参考如下命令,将全部镜像烧写到Flash的0x100000地址位。
tftp 0x81000000 vs_server.bin;sf probe 0;sf erase 0x100000 0x300000;sf write 0x81000000 0x100000 0x300000;其中,“vs_server.bin”为系统镜像文件名,先将其烧写到内存中一段高地址位0x81000000。然后烧写到Flash,起始地址为0x100000,烧写长度为0x300000,即烧写的镜像文件大小不能超过3M,请根据实际的镜像大小调整长度值。
说明:
用户可以根据实际环境自主选择镜像烧写工具及烧写方法。加载全部镜像。
可以参考如下命令,从Flash的0x100000地址处读取全部镜像,加载到内存地址0x80200000处。
sf probe 0;sf read 0x80200000 0x100000 0x300000;执行如下命令,从内存地址0x80200000处启动系统。
go 0x80200000;根据系统反馈的快照镜像大小,从快照处恢复运行。
根据系统启动时打印的信息,可以获取休眠唤醒快照镜像大小,如图4红框中所示,本示例中快照大小为0x170000。快照被写到介质的0x800000地址处(从3中可以获得保存快照的介质的起始地址为0x800000)。
重启系统,可以参考如下命令在uboot中执行,从保存快照的介质的0x800000地址处读取0x170000大小的快照镜像,加载到内存地址0x80200000处,然后执行“go 0x80200000”从快照启动系统。
sf probe 0;sf read 0x80200000 0x800000 0x170000; go 0x80200000;
启动效果如图5所示。
系统成功从快照启动。
注意事项¶
用户在编写休眠唤醒业务时,需要保证“#ifndef MAKE_WOW_IMAGE”前涵盖了快照的所有代码和数据,否则从快照恢复后的系统中不会包含所有休眠唤醒的业务。
休眠唤醒依赖于存储介质,当前LiteOS依赖Flash,因此该功能依赖Flash,如果关闭Flash支持,休眠唤醒功能也会关闭。如果需要在其他存储介质上使能休眠唤醒,请参考当前Flash的配置进行适配。
CPU占用率¶
概述¶
LiteOS的CPUP(Central Processing Unit Percent,CPU占用率)分为系统CPU占用率、任务CPU占用率和中断CPU占用率。
系统CPU占用率是指周期时间内系统的CPU占用率,用于表示系统一段时间内的闲忙程度,也表示CPU的负载情况。
任务CPU占用率指单个任务的CPU占用率,用于表示单个任务在一段时间内的闲忙程度。
中断CPU占用率指周期时间内的中断占用率,用于表示系统一段时间内的中断处理时间。
LiteOS的CPUP采用任务级记录的方式,在任务切换时,记录任务的切入时间,在下一次任务切换时,记录任务的切出时间,以此计算出任务本次的运行时间,累加到任务的总占用时间上。
CPU占用率的计算方法:
系统CPU占用率=系统中除idle任务外其他任务运行总时间/系统运行总时间。
任务CPU占用率=任务运行总时间/系统运行总时间。
中断CPU占用率=中断运行总时间/系统运行总时间
开发指导¶
通过系统CPU占用率,判断当前系统负载是否超出设计规格。
通过系统中各个任务的CPU占用率,判断各个任务的CPU占用率是否符合设计的预期。
LiteOS的CPU占用率模块为用户提供下面几种功能,接口详细信息可以查看API参考。
使能LOSCFG_CPUP_INCLUDE_IRQ且设置入参flag为0时,获取所有中断的CPU占用率。设置入参flag为非0,或者关闭LOSCFG_CPUP_INCLUDE_IRQ后,获取所有任务的CPU占用率,这里的任务也包含了idle任务。 |
||
说明:
通过上述接口获取到的CPU占用率是千分比,所以CPU占用率的有效表示范围为0~1000。系统CPU占用率为1000,表示系统满负荷运转。任务CPU占用率为1000,表示在一段时间内系统一直在运行该任务。
获取CPU占用率有三种模式,通过入参mode设置:
CPUP_LAST_MULIT_RECORD(值为0,原为CPUP_LAST_TEN_SECONDS):表示获取最近多次采样记录内的CPU占用率,默认为10s。
CPUP_LAST_ONE_RECORD(值为1,原为CPUP_LAST_ONE_SECONDS): 表示获取最近1次采样记录的CPU占用率,默认为1s。
CPUP_ALL_TIME(值为0xffff),或除0和1之外的其他值:表示获取自系统启动以来的CPU占用率。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为CPUP模块,值为0x1E。
CPU占用率的典型开发流程:
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Cpup菜单,完成CPU占用率模块的配置。
获取系统CPU使用率LOS_HistorySysCpuUsage。
获取指定任务或中断的CPU使用率LOS_HistoryTaskCpuUsage。
获取所有任务或所有中断的CPU使用率LOS_AllCpuUsage。
无。
注意事项¶
CPU占用率对性能有一定影响,而一般只有在产品开发时需要了解各个任务的占用率,因此建议在发布产品时,关闭CPU占用率。
关闭配置项LOSCFG_CPUP_INCLUDE_IRQ后,系统中的中断耗时会被统计到中断发生的任务中,即被中断打断的任务中。
编程实例¶
本实例实现如下功能:
创建一个测试CPUP的任务。
获取系统最近1s内所有任务或中断的CPUP。
获取系统(除idle任务外)最近10s内的总CPU占用率。
获取CPUP测试任务的CPUP。
前提条件:打开菜单配置好CPU占用率模块。
代码实现如下:
#include <unistd.h >
#include "los_task.h"
#include "los_cpup.h"
#define MAXTASKNUM 32
UINT32 cpupUse;
UINT32 g_cpuTestTaskId;
VOID Example_CPUP(VOID)
{
printf("entry cpup test example\n");
while(1) {
usleep(100);
}
}
UINT32 Example_CPUP_Test(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S cpupTestTask;
CPUP_INFO_S cpupInfo[MAXTASKNUM];
/* 创建测试CPUP的任务 */
memset(&cpupTestTask, 0, sizeof(TSK_INIT_PARAM_S));
cpupTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_CPUP;
cpupTestTask.pcName = "TestCpupTsk"; /* 测试任务名称 */
cpupTestTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
cpupTestTask.usTaskPrio = 5;
cpupTestTask.uwResved = LOS_TASK_STATUS_DETACHED;
ret = LOS_TaskCreate(&g_cpuTestTaskId, &cpupTestTask);
if(ret != LOS_OK) {
printf("cpupTestTask create failed.\n");
return LOS_NOK;
}
usleep(100);
/* 系统中运行的任务或者中断数量 */
UINT16 maxNum = MAXTASKNUM;
/* 获取系统所有任务或中断最近1s的CPU占用率 */
cpupUse = LOS_AllCpuUsage(maxNum, cpupInfo, CPUP_LAST_ONE_SECONDS, 0);
printf("the system cpu usage in last 1s: %d\n", cpupUse);
/* 获取最近10s内系统(除idle任务外)总CPU占用率 */
cpupUse = LOS_HistorySysCpuUsage(CPUP_LAST_TEN_SECONDS);
printf("the history system cpu usage in last 10s: %d\n", cpupUse);
/* 获取指定任务在最近1s的CPU占用率,该测试例程中指定的任务为上面创建的CPUP测试任务 */
cpupUse = LOS_HistoryTaskCpuUsage(g_cpuTestTaskId, CPUP_LAST_ONE_SECONDS);
printf("cpu usage of the cpupTestTask in last 1s:\n TaskID: %d\n usage: %d\n", g_cpuTestTaskId, cpupUse);
return LOS_OK;
}
编译运行得到的结果为:
the system cpu usage in last 1s: 15
the history system cpu usage in last 10s: 3
cpu usage of the cpupTestTask in last 1s:
TaskID: 10
usage: 0
Trace¶
概述¶
Trace实时记录系统行为,类似系统“录像”功能。在系统发生异常后,能辅助用户查看历史事件,定位问题。
LiteOS的Trace采用静态代码打桩和缓冲区记录方式,在桩被执行时,获取事件发生的上下文,并写入到缓冲区。
在插桩函数的入参中需要提供监测的事件类型、事件操作的主体对象和事件的参数,这些信息会被写入缓冲区,同时缓冲区中也会记录事件发生的时间、系统中的任务信息等事件上下文信息。
开发指导¶
通过Trace了解系统运转的路径,理解系统运行机制。
通过Trace分析系统发生异常前的操作,定位死机问题。
LiteOS的Trace模块为用户提供下面几种功能,接口详细信息可以查看API参考。
说明:
LOS_TRACE_EASY(TYPE, IDENTITY, params...) 简易插桩。
一句话插桩,用户在目标源代码中插入该接口即可。
TYPE有效取值范围为[0, 0xF],表示不同的事件类型。
IDENTITY类型UINTPTR,表示事件操作的主体对象。
Params类型UINTPTR,表示事件的参数。 示例:
LOS_TRACE_EASY(1, userId0, userParam1, userParam2); LOS_TRACE_EASY(2, userId0); LOS_TRACE_EASY(1, userId1, userParam1); LOS_TRACE_EASY(2, userId1); LOS_TRACE_EASY(3, userParma1, userParam2, userParam3); // parmas 可复用IDENTITY字段(当IDENTITY不必要时)
LOS_TRACE(TYPE, IDENTITY, params...) 标准插桩。
相比简易插桩,支持动态过滤事件和参数裁剪,但使用上需要用户按规则来扩展。
TYPE用于设置具体的事件类型,可以在头文件los_trace.h中的enum LOS_TRACE_TYPE中自定义事件类型。定义方法和规则可以参考其他事件类型。
IDENTITY和Params的类型及含义同简易插桩。 示例:
1.在enum LOS_TRACE_MASK中定义事件掩码,即模块级别的事件类型。定义规范为TRACE_#MOD#_FLAG,#MOD#表示模块名,例如: TRACE_FS_FLAG = 0x2000 2.在enum LOS_TRACE_TYPE中定义具体事件类型。定义规范为#TYPE# = TRACE_#MOD#_FLAG | NUMBER,例如: FS_READ = TRACE_FS_FLAG | 0; // 读文件 FS_WRITE = TRACE_FS_FLAG | 1; // 写文件 3.定义事件参数。定义规范为#TYPE#_PARAMS(IDENTITY, parma1...) IDENTITY, ... 其中的#TYPE#就是上面2中的#TYPE#,例如: #define FS_READ_PARAMS(fp, fd, flag, size) fp, fd, flag, size 宏定义的参数对应于Trace缓冲区中记录的事件参数,用户可对任意参数字段进行裁剪; 当定义为空时,表示不监测该类型事件: #define FS_READ_PARAMS(fp, fd, flag, size) // 不监测文件读事件 4.在适当位置插入代码桩。定义规范为LOS_TRACE(#TYPE#, #TYPE#_PARAMS(IDENTITY, parma1...)) LOS_TRACE(FS_READ, fp, fd, flag, size); // 读文件的代码桩,#TYPE#之后的入参就是上面3中的FS_READ_PARAMS函数的入参。
LiteOS预置的Trace事件及参数均可以通过上述方式进行裁剪,参数详见“kernel_lite/include/los_trace.h”。
Trace Mask事件过滤接口LOS_TraceEventMaskSet(UINT32 mask),其入参mask仅高28位生效(对应LOS_TRACE_MASK中某模块的使能位),仅用于模块的过滤,暂不支持针对某个特定事件的细粒度过滤。例如:LOS_TraceEventMaskSet(0x202),则实际设置生效的mask为0x200(TRACE_QUE_FLAG),QUE模块的所有事件均会被采集。一般建议使用的方法为: LOS_TraceEventMaskSet(TRACE_EVENT_FLAG | TRACE_MUX_FLAG | TRACE_SEM_FLAG | TRACE_QUE_FLAG);
如果仅需要过滤简易插桩事件,通过设置Trace Mask为TRACE_MAX_FLAG即可。
Trace缓冲区大小有限,事件写满之后会覆盖写,用户可通过LOS_TraceRecordDump中打印的CurEvtIndex识别最新事件。
为快速定位可能导致Trace操作失败的原因,初始化Trace和启动Trace均需要返回对应的错误码。其他无返回值的接口如停止Trace、清除与Dump Trace 数据,错误原因均为Trace状态不合法,系统会直接打印错误原因。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为Trace模块,值为0x14。
Trace的典型开发流程:
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Trace Feature菜单,完成Trace的配置。
(可选)预置事件参数和事件桩(或使用系统默认的事件参数配置和事件桩)。
(可选)调用LOS_TraceStop停止Trace后,清除缓冲区LOS_TraceReset(系统默认已启动trace)。
(可选)调用LOS_TraceEventMaskSet设置需要监测的事件掩码(系统默认的事件掩码仅使能中断与任务切换),事件掩码参见“los_trace.h”中的LOS_TRACE_MASK定义。
在需要记录事件的代码起始点调用LOS_TraceStart。
在需要记录事件的代码结束点调用LOS_TraceStop。
调用LOS_TraceRecordDump输出缓冲区数据(函数的入参为布尔型,FALSE表示格式化输出,TRUE表示输出到Windows客户端)。
无。
注意事项¶
由于Trace会影响系统性能,同时考虑到一般只有在产品开发时才需要了解系统发生的事件,因此建议在产品发布时关闭Trace。
编程实例¶
本实例实现如下功能:
创建一个用于Trace测试的任务。
设置事件掩码。
启动Trace。
停止Trace。
格式化输出Trace数据。
前提条件:在menuconfig菜单中完成Trace模块的配置。
代码实现如下:
#include "los_trace.h"
UINT32 g_traceTestTaskId;
VOID Example_Trace(VOID)
{
UINT32 ret;
LOS_TaskDelay(10);
/* 开启trace */
ret = LOS_TraceStart();
if (ret != LOS_OK) {
dprintf("trace start error\n");
return;
}
/* 触发任务切换的事件 */
LOS_TaskDelay(1);
LOS_TaskDelay(1);
LOS_TaskDelay(1);
/* 停止trace */
LOS_TraceStop();
LOS_TraceRecordDump(FALSE);
}
UINT32 Example_Trace_test(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S traceTestTask;
/* 创建用于trace测试的任务 */
memset(&traceTestTask, 0, sizeof(TSK_INIT_PARAM_S));
traceTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Trace;
traceTestTask.pcName = "TestTraceTsk"; /* 测试任务名称 */
traceTestTask.uwStackSize = 0x800;
traceTestTask.usTaskPrio = 5;
traceTestTask.uwResved = LOS_TASK_STATUS_DETACHED;
ret = LOS_TaskCreate(&g_traceTestTaskId, &traceTestTask);
if(ret != LOS_OK){
dprintf("TraceTestTask create failed .\n");
return LOS_NOK;
}
/* 系统默认情况下已启动trace, 因此可先关闭trace,然后清除缓存区后,再重启trace */
LOS_TraceStop();
LOS_TraceReset();
/* 开启任务模块事件记录 */
LOS_TraceEventMaskSet(TRACE_TASK_FLAG);
return LOS_OK;
}
编译运行得到的结果为:
*******TraceInfo begin*******
clockFreq = 50000000
CurEvtIndex = 7
Index Time(cycles) EventType CurTask Identity params
0 0x366d5e88 0x45 0x1 0x0 0x1f 0x4 0x0
1 0x366d74ae 0x45 0x0 0x1 0x0 0x8 0x1f
2 0x36940da6 0x45 0x1 0xc 0x1f 0x4 0x9
3 0x3694337c 0x45 0xc 0x1 0x9 0x8 0x1f
4 0x36eea56e 0x45 0x1 0xc 0x1f 0x4 0x9
5 0x36eec810 0x45 0xc 0x1 0x9 0x8 0x1f
6 0x3706f804 0x45 0x1 0x0 0x1f 0x4 0x0
7 0x37070e59 0x45 0x0 0x1 0x0 0x8 0x1f
*******TraceInfo end*******
输出的事件信息包括:发生时间、事件类型、事件发生在哪个任务中、事件操作的主体对象、事件的其他参数。
Time(cycles):表示事件发生的时间,值是时钟的cycle数。
EventType:表示的具体事件可查阅头文件“los_trace.h”中的enum LOS_TRACE_TYPE。
CurTask:表示当前运行在哪个任务中,值为taskId。
Identity:表示事件操作的主体对象,可查阅头文件“los_trace.h”中的#TYPE#_PARAMS。
params:表示的事件参数可查阅头文件“los_trace.h“中的#TYPE#_PARAMS。
下面以序号为0的输出项为例,进行说明。
Index Time(cycles) EventType CurTask Identity params
0 0x366d5e88 0x45 0x1 0x0 0x1f 0x4 0x0
Time(cycles)可换算成时间,换算公式为cycles/clockFreq,单位为s。
0x45为TASK_SWITCH事件,即任务切换事件,当前运行任务的taskId为0x1。
Identity和params的含义需要查看TASK_SWITCH_PARAMS宏定义:
#define TASK_SWITCH_PARAMS(taskId, oldPriority, oldTaskStatus, newPriority, newTaskStatus) \ taskId, oldPriority, oldTaskStatus, newPriority, newTaskStatus
因为#TYPE#_PARAMS(IDENTITY, parma1...) IDENTITY, ...,所以Identity为taskId(0x0),第一个参数为oldPriority(0x1f)...
说明:
params的个数由menuconfig中Enable Trace Feature ---> Record max params配置,默认为3,超出的参数不会被记录,用户应自行合理配置该值。
综上所述,任务由0x1切换到0x0,0x1任务的优先级为0x1f,状态为0x4,0x0任务的优先级为0x0。
Perf¶
概述¶
Perf为性能分析工具,依赖PMU(Performance Monitoring Unit)对采样事件进行计数和上下文采集,统计出其算法的热点(hot spot)分布和热路径(hot path)。
基于事件采样原理,以性能事件为基础,当事件发生时,相应的事件计数器溢出发生中断,在中断处理函数中记录事件信息,包括当前的PC、当前运行的任务ID以及调用栈等信息。
开发指导¶
通过Perf查找性能瓶颈和定位热点代码,更高效地优化系统性能。
LiteOS的Perf模块为用户提供下面几种功能,接口详细信息可参见API参考。
须知: 采样数据缓冲区为环形buffer,buffer中读过的区域可以覆盖写,未被读过的区域不能被覆盖写。
为快速定位可能导致Perf操作失败的错误原因,Perf初始化和Perf采样配置接口均需要返回对应的错误码。具体描述如下表所示:
配置的Perf buffer size过大或过小,可通过修改“los_config.h”中LOS_PERF_BUFFER_SIZE解决。 |
||||
检查LOS_PerfConfig中事件类型、事件ID、事件周期是否合法。 |
||||
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为Perf模块,值为0x20。
Perf的典型开发流程:
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Perf Feature菜单,完成Perf的配置。
调用LOS_PerfConfig配置需要采样的事件。
Perf提供2种模式的配置, 及3大类型的事件配置:
2种模式:计数模式(仅统计事件发生次数)、采样模式(收集上下文如任务ID、PC、backtrace等)。
3种事件类型:CPU硬件事件(cycle、branch、icache、dcache等)、OS软件事件(task switch、mux pend、irq等)、高精度周期事件(cpu clock)。
在需要采样的代码起始点调用LOS_PerfStart。
在需要采样的代码结束点调用LOS_PerfStop。
调用输出缓冲区数据的接口LOS_PerfDataRead读取采样数据,并使用LiteOS Studio工具解析(单击调测工具里的性能分析tab页签)。
针对类型为硬件事件的采样,其依赖PMU硬件单元,且具有平台差异性。目前LiteOS Perf支持armv7 、armv8、risc-v、xea2 与LingLong架构的PMU。
注意事项¶
Perf用于性能调测,对系统性能有一定影响,建议在产品发布时,关闭Perf模块的裁剪开关LOSCFG_KERNEL_PERF。
编程实例¶
本实例实现如下功能:
配置采样事件。
启动Perf。
执行需要统计的算法。
停止Perf。
输出统计结果。
前提条件:在menuconfig菜单中完成Perf模块的配置。
打开菜单,进入Kernel ---> Enable Extend Kernel ---> Enable Perf Feature,然后选中“Enable Hardware Pmu Events for Sampling, Enable Perf Per-Task mode”和“Enable Perf Separated IRQ mode”,使能PMU对硬件事件监测,并使能perf分任务统计以及中断独立统计。
代码实现如下:
#include "los_perf.h"
#include "los_memory.h"
#define TEST_PERF_BUFFER_SIZE 1024
#define OUTPUT_BUFFER_SIZE 0x200
#ifndef LOSCFG_KERNEL_MEM_ALLOC
STATIC CHAR g_buffer[TEST_PERF_BUFFER_SIZE] = {0};
#endif
STATIC VOID OsPrintBuff(const CHAR *buf, UINT32 num)
{
UINT32 i;
dprintf("num: ");
for (i = 0; i < num; i++) {
dprintf(" %02u", i);
}
dprintf("\n");
dprintf("hex: ");
for (i = 0; i < num; i++) {
dprintf(" %02x", buf[i]);
}
dprintf("\n");
}
VOID PerfTestHWEvent(VOID)
{
UINT32 ret;
CHAR buf[OUTPUT_BUFFER_SIZE] = {0};
UINT32 len;
CHAR *buffer = NULL;
PerfConfigAttr attr1 = {
.eventsCfg = {
.type = PERF_EVENT_TYPE_HW,
.events = {
[0] = {PERF_COUNT_HW_CPU_CYCLES, 0xFFFF},
[1] = {PERF_COUNT_HW_BRANCH_INSTRUCTIONS, 0xFFFFFF00},
},
.eventsNr = 2,
.predivided = 1, /* cycle counter increase every 64 cycles */
},
.taskIds = {0},
.taskIdsNr = 0,
.needSample = 0,
.sampleType = PERF_RECORD_IP | PERF_RECORD_CALLCHAIN,
};
#ifdef LOSCFG_KERNEL_MEM_ALLOC
buffer = (CHAR *)LOS_MemAlloc(m_aucSysMem1, TEST_PERF_BUFFER_SIZE);
#else
buffer = g_buffer;
#endif
ret = LOS_PerfInit(buffer, TEST_PERF_BUFFER_SIZE);
if (ret != LOS_OK) {
PRINT_ERR("perf init error %u\n", ret);
return;
}
ret = LOS_PerfConfig(&attr1);
if (ret != LOS_OK) {
PRINT_ERR("perf config error %u\n", ret);
return;
}
PRINTK("------count mode------\n");
LOS_PerfStart(0);
test(); /* code snippets concerned by users */
LOS_PerfStop();
PRINTK("--------sample mode------ \n");
attr1.needSample = 1;
LOS_PerfConfig(&attr1);
LOS_PerfStart(2);
test(); /* code snippets concerned by users */
LOS_PerfStop();
len = LOS_PerfDataRead(buf, OUTPUT_BUFFER_SIZE); /* get sample data */
OsPrintBuff(buf, len); /* print data */
memset(buf, 0, sizeof(buf));
#ifdef LOSCFG_KERNEL_MEM_ALLOC
(VOID)LOS_MemFree(m_aucSysMem1, buffer);
#endif
LOS_PerfDeinit();
}
编译运行得到的结果为:
------count mode------
[EMG] perf count [tid 0 / Swt_Task]:
[EMG] [cycles] eventType: 0xff count: 0
[EMG] [branches] eventType: 0xc count: 0
[EMG] perf count [tid 1 / IdleCore000]:
[EMG] [cycles] eventType: 0xff count: 0
[EMG] [branches] eventType: 0xc count: 0
[EMG] perf count [tid 2 / app_Task]:
[EMG] [cycles] eventType: 0xff count: 40043481344
[EMG] [branches] eventType: 0xc count: 10002153167
[EMG] perf count [tid 3 / Swt_Task]:
[EMG] [cycles] eventType: 0xff count: 0
[EMG] [branches] eventType: 0xc count: 0
[EMG] perf count [tid 4 / IdleCore000]:
[EMG] [cycles] eventType: 0xff count: 100718720
[EMG] [branches] eventType: 0xc count: 5182967
[EMG] perf count [tid 5 / SerialShellTask]:
[EMG] [cycles] eventType: 0xff count: 0
[EMG] [branches] eventType: 0xc count: 0
[EMG] perf count [tid 6 / SerialEntryTask]:
[EMG] [cycles] eventType: 0xff count: 0
[EMG] [branches] eventType: 0xc count: 0
[EMG] perf count [irq@core 0]:
[EMG] [cycles] eventType: 0xff count: 891017
[EMG] [branches] eventType: 0xc count: 2948096
[EMG] perf count [irq@core 1]:
[EMG] [cycles] eventType: 0xff count: 889686
[EMG] [branches] eventType: 0xc count: 2948154
[EMG] time used: 44668000(us)
--------sample mode------
[EMG] time used: 44673000(us)
num: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 ...
hex: 00 ef ef ef 00 00 00 00 14 00 00 00 60 00 00 00 02 00 00 00 90 2d 20 80 02 00 00 00 d4 5f 20 80 54 55 21 80 90 2d 20 80 02 ...
针对计数模式,系统在perf stop后会打印如下信息:
当开启分任务统计功能后,会先打印每个任务的头信息,其后的事件计数为此任务的计数
perf count [tid 2 / app_Task]任务ID:2,任务名称:app_Task。
当关闭分任务统计功能后,会先打印每个核的头信息,其后的事件计数为此核的计数
perf count [core 0]核ID:core 0。
当开启中断独立统计功能后,会先打印每个核中断的头信息,其后的事件计数为此核中断的计数
perf count [irq@core 0]核ID:core 0。
事件计数的信息:
[cycles] eventType: 0xff count: 40043481344事件名称:cycles、事件类型:0xff、事件发生的次数:40043481344。
说明:
当采样事件为硬件PMU事件时,打印的事件类型为实际的硬件事件ID,非enum PmuHwId中定义的抽象类型。硬件事件ID详见各架构手册规定。针对采样模式,系统会将采样数据记录到用户指定的内存中,即LOS_PerfInit的入参。用户可以自行打印该内存的地址,然后通过JTAG口导出该片内存,再使用LiteOS Studio线下工具解析。用户也可以通过LOS_PerfDataRead将数据读到指定地址,进行查看或进一步处理。示例中OsPrintBuff为测试接口,其按字节打印Read到的采样数据,num表示第几个字节, hex表示该字节中的数值。
须知:在RISC-V架构中,使用采样模式需要在定时中断处理函数中调用OsPerfCheckEventCount()函数。一般使用Tick中断处理函数。
Perf独立中断统计模式下,PMU中断不会被单独统计。另外,由于Timed PMU同样依赖于中断工作,计数可能与预期不一致。如果需要使用Timed PMU,请关闭Perf独立中断。
C++支持¶
概述¶
C++作为目前使用最广泛的编程语言之一,支持类、封装、重载等特性,是在C语言基础上开发的一种面向对象的编程语言。
STL(Standard Template Library)标准模板库,是一些“容器”的集合,也是算法和其他一些组件的集合。其目的是标准化组件,使用标准化组件后可以不用重新开发,直接使用现成的组件。
开发指导¶
说明: 该函数有3个入参:
第一个参数:init_array段的起始地址。
第二个参数:init_array段的结束地址。
第三个参数:标记调用C++特性时的场景,包括BEFORE_SCATTER(在分散加载快速启动阶段使用C++特性)、AFTER_SCATTER(在分散加载非快速启动阶段使用C++特性)、NO_SCATTER(在非分散加载特性中使用C++特性,或者在分散加载中不使用C++特性)。
打开菜单,选择Kernel ---> Enable Extend Kernel ---> C++ Support,使能C++。
使用C++特性之前,需要调用函数LOS_CppSystemInit,初始化C++构造函数。
由于在分散加载应用场景下,C++初始化中涉及到的相关代码、数据段加载的时机会有所不同,所以在开启和不开启分散加载这两种情况下,C++初始化函数LOS_CppSystemInit的调用有所不同。其中构造函数的初始化存在init_array这个段中,段区间通过变量__init_array_start、__init_array_end传递。
不开启分散加载特性
在不开启分散加载特性的情况下,需要在调用C++代码前,以NO_SCATTER(表示并未开启分散加载特性)参数调用LOS_CppSystemInit:
LOS_CppSystemInit((unsigned long)&__init_array_start, (unsigned long)&__init_array_end, NO_SCATTER);开启分散加载特性
如果需要在分散加载的快速启动阶段调用相关C++代码:
在分散加载的快速启动阶段调用C++代码之前,以BEFORE_SCATTER(表示在分散加载快速启动阶段调用)参数调用LOS_CppSystemInit:
LOS_CppSystemInit((unsigned long)&__init_array_start, (unsigned long)&__init_array_end, BEFORE_SCATTER);在分散加载的非快速启动阶段,再以AFTER_SCATTER(表示在分散加载非快速启动阶段调用)参数调用LOS_CppSystemInit:
LOS_CppSystemInit((unsigned long)&__init_array_start, (unsigned long)&__init_array_end, AFTER_SCATTER);如果无需在分散加载的快速启动阶段调用相关C++代码:
以NO_SCATTER参数调用一次LOS_CppSystemInit:
LOS_CppSystemInit((unsigned long)&__init_array_start, (unsigned long)&__init_array_end, NO_SCATTER);
C函数与C++函数混合调用。
在C++中调用C程序的函数,代码需加入C++包含的宏:
#ifdef __cplusplus #if __cplusplus extern "C" { #endif /* __cplusplus */ #endif /* __cplusplus */ /* code */ ... #ifdef __cplusplus #if __cplusplus } #endif /* __cplusplus */ #endif /* __cplusplus */
注意事项¶
LiteOS的C++功能需要编译器适配才能支持,编译器编译链接C++代码时需要使用LiteOS提供的C库。
C++的cout函数依赖console功能。
编程实例¶
本实例实现了在LiteOS上运行C++的代码。
编写C++代码。
在运行C++代码之前,先在app_init函数里初始化C++构造函数。此处未开启分散加载特性,所以只需以NO_SCATTER参数调用一次LOS_CppSystemInit即可。
在LOS_CppSystemInit之后调用编写好的C++代码。
前提条件:打开菜单使能C++支持。
C++代码实现如下:
#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif /* __cplusplus */
#endif /* __cplusplus */
using namespace std;
class TestClass {
public:
TestClass(int arg);
~TestClass(void);
void PrintTest(void);
void StringTest(void);
void MapTest(void);
private:
int intTest;
string stringTest;
map<string, int> mapTest;
};
TestClass::TestClass(int arg)
{
cout << "TestClass is constructed here, arg = " << arg << endl;
intTest = arg;
}
TestClass::~TestClass(void)
{
cout << "TestClass is destructed" << endl;
}
void TestClass::PrintTest(void)
{
cout << __FUNCTION__ << " enter" << endl;
cout << " intTest = " << this->intTest << endl;
}
void TestClass::StringTest(void)
{
cout << __FUNCTION__ << " enter" << endl;
string a("Lite");
string b("OS");
string c("LiteOS");
if (a != b) {
cout << " " << a << " != " << b << endl;
}
a += b;
if (a == c) {
cout << " " << a << " == " << c << endl;
}
}
void TestClass::MapTest(void)
{
cout << __FUNCTION__ << " enter" << endl;
mapTest.insert(pair<string, int>("Huawei", 1));
mapTest.insert(pair<string, int>("LiteOS", 2));
mapTest.insert(pair<string, int>("Open", 3));
mapTest.insert(pair<string, int>("Source", 4));
cout << " show map key&value" << endl;
for (auto &it : mapTest) {
cout << " " << it.first << " " << it.second << endl;
}
mapTest["LiteOS"] = 8; /* 8: new value */
cout << " change value of \"LiteOS\" key" << endl;
for (auto &it : mapTest) {
cout << " " << it.first << " " << it.second << endl;
}
}
void CppTestEntry(void)
{
cout << "LiteOS cpp sample start" << endl;
TestClass test(123);
test.PrintTest();
test.StringTest();
test.MapTest();
cout << "LiteOS cpp sample stop" << endl;
}
#ifdef __cplusplus
#if __cplusplus
}
#endif /* __cplusplus */
#endif /* __cplusplus */
app_init中初始化C++构造函数后,调用C++代码,实现如下:
void app_init(void)
{
......
/* 初始化C++构造函数 */
LOS_CppSystemInit((UINT32)&__init_array_start, (UINT32)&__init_array_end, NO_SCATTER);
/* 调用C++代码 */
CppTestEntry();
......
}
运行CppTestEntry()函数,结果如下:
LiteOS cpp sample start
TestClass is constructed here, arg = 123
PrintTest enter
intTest = 123
StringTest enter
Lite != OS
LiteOS == LiteOS
MapTest enter
show map key&value
Huawei 1
LiteOS 2
Open 3
Source 4
change value of "LiteOS" key
Huawei 1
LiteOS 8
Open 3
Source 4
LiteOS cpp sample stop
TestClass is destructed
控制台¶
概述¶
控制台是用于输入/输出系统信息的设备,可以向用户发送文本输出,并从用户接收文本输入。
开发指导¶
LiteOS的控制台模块为用户提供下面几种功能,接口详细信息可以查看API参考。
注意事项¶
控制台功能依赖于文件系统。
控制台输出任务的优先级为30。
异常输出不经过控制台。
文件系统¶
功能概述¶
本章以Hi3556V200为例,介绍LiteOS目前支持的文件系统,包括VFS、FAT、YAFFS2和LITTLEFS。
VFS¶
概述¶
VFS是Virtual File System(虚拟文件系统)的缩写,是一个异构文件系统之上的软件粘合层,为用户提供统一的Unix文件操作接口。
由于不同类型的文件系统接口不统一,若系统中有多个文件系统类型,访问不同的文件系统则需要使用不同的非标准接口。但是,通过在系统中添加VFS层来提供统一的抽象接口,便可以屏蔽底层异构类型的文件系统差异,使得访问文件系统时无需关注底层的存储介质和文件系统类型,从而提高开发效率。
LiteOS中,VFS框架通过在内存中的树结构实现,树的每个结点都是一个inode结构体。设备注册和文件系统挂载后会根据路径在树中生成相应的结点。
VFS的两大主要功能如下:
查找节点。
统一调用(标准)。
通过VFS层,可以使用标准的Unix文件操作函数(如open、read、write等)实现对不同介质上不同文件系统的访问。
VFS框架内存中的inode的关键在于u和i_private字段,一个是函数方法结构体的指针,一个是数据指针。inode树结点有三种类型:
虚拟结点:作为VFS框架的虚拟文件,保持树的连续性,如/bin、/bin/vs、/dev。
设备结点:/dev目录下,对应一个设备,如/dev/mmc0。
挂载点:调用mount函数后生成,如/bin/vs/sd、/ramfs、/yaffs。
系统初始化过程中会调用los_vfs_init(),将“/”作为root_inode。
本设计中文件描述符分两种,采用动态内存数组的形式来对普通文件描述符进行管理,采用全局数组管理网络描述符,这两个数组在内存上并不连续。
File描述符,即普通文件描述符,其中0保留作为系统stdin标准输入、1保留作为系统stdout标准输出、2保留作为系统stderr标准错误输出。用户可以分配的文件描述符从3开始到CONFIG_NFILE_DESCRIPTORS-1。
Socket描述符,该描述符的分配从CONFIG_NFILE_DESCRIPTORS开始。
开发指导¶
改变文件系统中的文件属性,目前仅FAT文件系统支持,修改后的文件属性目前支持4种:F_RDO(只读)、F_HID(隐藏)、F_SYS(系统文件)、F_ARC(存档)。 |
||
说明: 关于文件属性:
当前只支持修改FAT文件系统的文件属性,其他文件系统对只读等属性有各自的处理方式。
F_RDO(只读)、F_HID(隐藏)、F_SYS(系统文件)和F_ARC(存档)这4种属性并不冲突,可以任意修改。
只读属性文件/目录不允许被删除。
只读属性文件/目录允许重命名。
只读文件不允许以O_CREAT、O_TRUNC,以及有含有写权限的方式打开。
在LiteOS中隐藏属性文件可见,在Windows中不可见(不显示隐藏文件属性的情况下)。
在LiteOS中文件加上隐藏属性,在Windows中只能通过命令行找到(在显示/不显示隐藏文件属性的情况下,该文件在Windows中都不可见)。
推荐驱动开发人员使用VFS框架来注册/卸载设备,在应用层使用open()、read()操作字符设备文件来调用驱动。
打开菜单,选择FileSystem ---> Enable VFS,使能VFS。
调用register_driver()、register_blockdriver()接口生成设备结点,调用mount()接口生成文件系统挂载结点。生成设备节点的开发流程详见适配文件操作的“开发指导”。
注意事项¶
VFS下所有文件系统创建的目录名和文件名长度最多为255个字符,若超过则创建失败。
VFS层支持的文件和目录最大全路径长度为259个字符,若超过,则创建失败
所有文件系统中的文件名和目录名中只仅支持“-” 与“_”两种特殊字符,使用其他特殊字符可能造成不可预知的后果,请谨慎为之。
VFS下的所有文件系统,在创建模式或只写模式打开文件,此时文件权限为open接口mode入参值默认为0666,因VFS下的文件系统对文件权限模式支持不全, 推荐使用0666入参。
VFS在优化迭代过程中,对部分POSIX接口(rename、dup、opendir等)的异常错误分支的errno返回值会有更改,以便更贴近于POSIX标准,业务在使用errno返回值时请谨慎处理。
对于块设备节点(例如“/dev/mmc0”),如果块设备驱动的bpos实现unlink回调函数且返回值为OK,则误调用unlink(“/dev/mmc0”)会导致块设备节点被删除和节点消失,并影响正常业务功能;如果想保持块设备节点常驻,不实现unlink回调即可。
ls命令会显示块设备节点文件的size,size值为块设备的容量值,如果不想显示对应的设备容量,块设备驱动的bpos不实现geometry回调函数即可。
VFS可以直接递归创建多级目录。
设备分为字符设备和块设备,为了设备上的数据安全,需挂载相应文件系统后通过文件系统接口操作数据。
los_vfs_init() 只能调用一次,多次调用将会造成文件系统异常。
open打开一个文件时,参数O_RDWR、O_WRONLY、O_RDONLY互斥,只能出现一个,若出现2个或以上作为open的参数,会出现不可预知的错误,禁止使用。
不建议open一个目录直接获取目录数据。如果要获取目录信息,可以调用opendir接口打开目录。
open()、read()、write()等为非线程安全接口,在多线程场景下,建议使用fopen()、fread()、fwrite()等接口。
挂载文件系统请严格按照手册进行,错误挂载可能损坏设备及系统。
挂载点必须为空目录,不能重复挂载至同一挂载点或挂载至其他挂载点下的目录或文件。
不要在休眠唤醒或者分散加载阶段挂载一个不存在的文件系统。
在unregister_driver卸载设备之前,要确保字符设备已经正确的close,即open和close的次数要对应,否则会卸载失败。
在umount之前,需确保此文件系统中的所有目录及文件全部关闭,否则umount会失败。如果强制umount,可能导致包括但不限于文件系统损坏、设备损坏等问题,对此华为不承担任何责任。
SD卡移除前,需确保所有目录及文件已经全部关闭,并进行umount操作。如果强制拔卡,可能导致包括但不限于SD数据丢失、SD卡损坏等问题,对此华为不承担任何责任。
FAT¶
概述¶
FAT文件系统是File Allocation Table(文件配置表)的简称,FAT文件系统有FAT12、FAT16、FAT32。FAT文件系统将硬盘分为MBR区、DBR区、FAT区、DIR区、DATA区等5个区域。
FAT文件系统支持多种介质,特别是在可移动存储介质(U盘、SD卡、移动硬盘等)上广泛使用。而且,FAT文件系统可以使嵌入式设备和Windows、Linux等桌面系统保持很好的兼容性,方便用户管理操作文件。
LiteOS的FAT文件系统的特点有:
代码量和资源占用小、可裁切、支持多种物理介质。
与Windows、Linux等系统保持兼容。
支持多设备、多分区识别等功能。
支持硬盘多分区,可以在主分区以及逻辑分区上进行文件操作。
支持识别出硬盘上其他类型的文件系统,比如NTFS。
支持基于FAT32 的虚拟分区特性。
当开启虚拟分区特性时,用户可通过调用虚拟分区的设置接口los_set_virpartparam(),对特定的物理分区配置虚拟分区的数量、空间百分比大小、虚拟分区的入口名等,并会根据设置的入口名,在根目录建立对应的文件夹作为虚拟分区入口。
在对应的虚拟分区内部进行操作,即视为在对应的虚拟分区内进行操作。
若所有虚拟分区的空间百分比总和未达到100%,则允许在虚拟分区入口目录外进行写入操作,此时所有虚拟分区入口外部的总可写入空间,即为百分比总和的剩余空间。若所有虚拟分区的空间百分比达到了100%,则在虚拟分区入口目录外部将会以空间不足的理由,拒绝写入操作。
开发指导¶
说明:
使能FAT文件系统缓存(Cache)异步刷新后,系统会创建一个任务刷Cache,线程被触发唤醒进行脏数据刷新的条件如下:
周期性5s唤醒。
脏数据停留时间超过3s。
脏数据量超过50%时,触发任何一个条件,线程都会立刻把脏数据写回磁盘。
在“open_source/FatFs/source/ffconf.h”文件中可以配置FAT文件系统,其中FF_FS_LOCK为最多支持同时打开的文件(文件夹)数。
虚拟分区的调配结构体定义如下所示:
#define _MAX_ENTRYLENGTH 16
#define _MAX_VIRVOLUMES 5
typedef struct virtual_partition_info
{
char *devpartpath;
int virpartnum;
double virpartpercent[_MAX_VIRVOLUMES];
char virpartname[_MAX_VIRVOLUMES][_MAX_ENTRYLENGTH + 1];
} virpartinfo;
入口名有效字符最多为_MAX_ENTRYLENGTH ,即16个字符,不得为空。传入的集合不可以有重名项。入口名不得含有非法名称字符。 |
须知:
在LiteOS中,虚拟分区特性仅支持特定的单个物理分区进行,可通过调配虚拟分区参数接口设置设备节点。
设置指定物理设备节点后,会对设置进行上锁,不允许再次更改设置。挂载时就会采用设置的参数进行配置。当卸载后,才会对设置进行解锁。
卸载后,需要通过调配虚拟分区参数接口重新设置虚拟分区参数。
当插入的物理分区中已经设置了虚拟分区,但是其配置参数与当前系统通过调配接口所设置的参数不一致时,则将按照已有的配置参数对此分区进行管理。
开启虚拟分区特性后,部分接口的行为会在虚拟分区下有部分变化。下述这些接口的行为变化只对于已成功应用了虚拟分区特性的物理分区,对于未能应用虚拟分区的基本FAT文件系统的物理分区,以及其他的文件系统,这些接口行为不涉及变化。
下述的接口只是在执行功能上的行为针对虚拟分区有所变化,函数的调用方式、参数等均不受影响。
挂载了基本FAT分区后,会尝试识别当前物理分区是否已经应用过虚拟分区,并尝试对该物理分区按照其配置的信息加载虚拟分区,检查并尝试修复虚拟分区入口;若当前物理分区未应用过虚拟分区并符合启动虚拟分区的条件,则会自动对该物理分区应用虚拟分区,并建立虚拟分区。 |
返回值为-1表示基本FAT分区挂载失败,此时会触发libc错误码指示发生了何种错误; 返回值为0表示基本FAT分区挂载成功,此时错误码若为0表示虚拟分区挂载成功,若错误码为VIRERR_MODIFIED、VIRERR_CHAIN_ERR、VIRERR_OCCUPIED、VIRERR_NOTCLEAR、VIRERR_NOTFIT、VIRERR_NOPARAM表示虚拟分区挂载失败 |
||
当FAT分区已挂载后,若格式化成为FAT32文件系统,则会在格式化完成后对该物理分区应用虚拟分区;若非FAT32文件系统,则会完成格式化操作,并清除该物理分区的虚拟分区;若FAT分区未挂载,无论何种文件系统,则均会完成格式化操作后清除该物理分区的虚拟分区。 |
返回值为-1表示基本FAT分区格式化失败,此时会触发libc错误码指示发生了何种错误; 返回值为0表示基本FAT分区格式化完成。若此时分区并未挂载,则错误码则会为VIRERR_NOTMOUNT;若格式化的分区非调配接口所设定设备节点路径,或者调配接口并未设置参数,则此时会指示VIRERR_NOPARAM。若分区已挂载,错误码为0表示应用或重新应用虚拟分区成功,或者错误码可能为VIRERR_NOTFIT。 |
||
请参阅虚拟分区的兼容性的说明。 |
|||
请参阅虚拟分区的兼容性的说明。 |
|||
应用虚拟分区发生错误,只会发生于挂载时或者格式化时。当基本FAT文件系统的对应操作完成后,进行虚拟分区的配置与应用。此时若发生某种问题或者触发某种条件,虚拟分区会将错误码传递给上层,以分辨应用虚拟分区时发生了什么情况。同时该分区将会以基本FAT文件系统进行管理,而不会在此基础上采用虚拟分区管理。
说明: 对于挂载和格式化操作的返回值,指示的是基本FAT文件系统的成功或失败。
当接口的返回值为0时,表示对应的基本FAT文件系统操作成功,此时可通过访问errno获取虚拟分区的错误码,以检查应用虚拟分区过程中是否有异常产生。
当接口的返回值为-1时,表示于对应的基本FAT文件系统操作失败,此时的errno为libc的错误码,表示基本FAT操作失败的原因,不再表示虚拟分区的提示信息。
虚拟分区特性是LiteOS独有的基于FAT的文件管理模式。在其他平台上对已经应用过虚拟分区的物理分区进行操作时,务必了解以下兼容性特征,以免使用时发生问题。
重新对存储介质分区、调整存储介质的大小等操作,会直接导致物理分区的虚拟分区特性失效。
在其他平台上对已经应用虚拟分区的物理分区建立文件夹时,需要注意:
若建立的文件夹在虚拟分区入口外,且全部虚拟分区的百分比总和为100%,则下次在LiteOS中在该文件夹内不允许创建新的文件或者文件夹。
在其他平台上对已经应用虚拟分区的物理分区写文件时,需要注意:
若写入的文件在虚拟分区入口外,且全部虚拟分区的百分比总和为100%,则下次在LiteOS中这些文件只允许以只读方式访问。
若写入的文件在虚拟分区入口内,而且文件整体的簇链均在对应的虚拟分区的簇链管理范围内,则下次在LiteOS中这些文件会以正常权限访问。
若写入的文件在虚拟分区入口内,而且文件整体的簇链有一部分或者整体不在虚拟分区的管理范围内,则下次在LiteOS中这些文件只允许以只读权限访问。
在其他平台上删除虚拟分区入口目录,会导致对应的虚拟分区意外丢失。下次在LiteOS中进行挂载时,会尝试重建丢失的虚拟分区。
若删除后用户创建一个同名的文件夹,且文件夹的整体簇链均落在了对应的虚拟分区的管理范围内,则下次在LiteOS中进行挂载时,该虚拟分区入口将被正常识别。
若删除后用户创建一个同名的文件夹,且整体簇链一部分或者整体落在了对应虚拟分区的管辖范围外,则下次在LiteOS中进行挂载时,会导致虚拟分区入口校验失败,拒绝此次的虚拟分区应用。
在其他平台上重命名虚拟分区入口目录,会导致对应的虚拟分区意外丢失。下次在LiteOS中进行挂载时,会尝试将丢失的虚拟分区重建。原有的文件不会丢失,但这些文件将不会再受到虚拟分区特性的管理,只允许以只读权限访问。
使用FAT功能,涉及以下几个步骤,其中对虚拟分区的操作为可选部分,只有在FAT使用虚拟分区功能的情况下才涉及,请根据实际业务情况选择(各步骤详细操作见下述分解):
打开菜单,进入FileSystem ---> Enable FAT菜单,完成FAT文件系统的配置。
说明:当虚拟分区特性启用后,整个LiteOS的FAT文件系统将优先采用虚拟分区特性进行管理,虚拟分区不支持FAT目录项缓存。
当启用FAT目录项缓存后,会消耗一部分内存用来缓存FAT文件系统的目录项,从而减少部分接口的再次访问耗时。
当启用FAT Trim功能后,可以有效减缓磁盘的write amplification行为,提升数据写入速率的稳定性。
当启用FTL功能后,FAT文件系统可以在Nor Flash上使用,使用add_mtd_partition添加分区生成spinorblk0ftlp0。
当使用format格式化设备后,mount即可得到一个干净的FAT文件系统。
FTL支持文件系统镜像的预制作,使用mkftl2image目录下的工具源码进行make编译,生成“mkfs.ftl”来进行制作,当修改压缩选项时,需重新生成“mkfs.ftl”。命令格式如下:mkfs.ftl [inDir] [partitionSize] [blockSize] [pageSize] [outFile]。
./mkfs.ftl appfs 0x400000 0x10000 0x200 appfs.ftl
在“open_source/FatFs/source/ffconf.h”文件中可以使能多分区和多设备。使能多分区后,可以操作设备上的多个分区,否则只能操作第一个分区。使能多设备后,可以将多个设备挂载成FAT文件系统。具体配置项为:
配置FF_MULTI_PARTITION为1,使能多分区功能。
配置FF_VOLUMES大于1时,使能多设备功能。
系统自动识别插入的SD卡,自动注册设备节点,如下所示。其中的mmcblk0和mmcblk1为卡0和卡1,是独立的主设备,mmcblk0p0和mmcblk0p1为卡0的两个分区,mmcblk1p0为卡1的分区,这些分区可作为分区设备使用。存在分区设备的情况下,禁止使用主设备。
LiteOS# ls
Directory /dev:
acodec 0
adec 0
aenc 0
ai 0
ao 0
console 0
fb0 0
hi_gpio 0
hi_mipi 0
hi_rtc 0
hi_tde 0
i2c-0 0
i2c-1 0
i2c-2 0
isp_dev 0
lcd 0
logmpp 0
mem 0
mmcblk0 0
mmcblk0p0 0
mmcblk0p1 0
mmcblk1 0
mmcblk1p0 0
可以在Shell中使用partinfo命令查看设备的分区信息:
LiteOS # partinfo /dev/sdap0
part info :
disk id : 3
part_id in system: 0
part no in disk : 0
part no in mbr : 1
part filesystem : 0C
part dev name : sdap0
part sec start : 2048
part sec count : 167794688
调用mount()函数实现设备节点的挂载。
ret = mount("/dev/mmcblk0p0", "/bin/vs/sd", "vfat", 0, NULL);
if (ret) {
dprintf("mount fat filesystem err %d\n", ret);
}
说明: mount()函数有五个参数:
第一个参数表示设备节点。
第二个参数表示挂载点。
第三个参数表示文件系统类型。
第四个参数表示挂载标志,默认为0。
最后一个参数表示私有数据,默认为NULL。
除了在源码中调用mount()函数,也可以在Shell中使用mount命令实现挂载,前面3个参数同mount()函数,最后两个参数不需要设置。得到如下回应信息,表明挂载成功:
LiteOS# mount /dev/mmcblk0p0 /bin/vs/sd vfat
mount ok
调用umount()函数卸载分区,只需要给出正确的挂载点即可。也可以在Shell中使用umount命令实现,得到如下回应信息,表明卸载成功:
LiteOS# umount /bin/vs/sd
umount ok
对于未在LiteOS中应用过虚拟分区特性的存储设备(如SD卡、U盘、移动硬盘等)物理分区,需要保证:
该物理分区为FAT32分区。
该物理分区下无任何文件。
对于曾经在LiteOS中已经应用过虚拟分区特性的存储设备物理分区,需要保证:
该物理分区下的虚拟分区入口目录没有被删除、重命名或者其他形式的破坏。
该物理分区的保留扇区内数据完整。
若有任一条件不满足,则无法对该物理分区应用虚拟分区特性,只能应用常规FAT文件系统。
虚拟分区特性针对的是某个特定场景下的物理分区,需要通过调配接口los_set_virpartparam()指定分区设备,并设置该分区的配置参数。调配虚拟分区参数接口的主要目的是允许用户建立符合场景的虚拟分区。
系统开机后,若想应用虚拟分区特性,需要先调用los_set_virpartparam(),对当前系统的虚拟分区数量、对应虚拟分区的关键参数进行配置。若未配置,则将导致后续虚拟分区应用失败。
对未在LiteOS中应用过虚拟分区特性的存储设备物理分区,在进行虚拟分区应用之前,可以通过调配接口设置需要的参数。正常情况下,当成功调配后,对该物理分区执行挂载操作,即可完成虚拟分区的应用。
若操作的物理分区已经应用过虚拟分区特性,只要该分区已设置的虚拟分区校验正确,则可忽略调配接口设置的参数,而无条件遵循该分区内部的参数对其管理。
在完成挂载FAT文件系统后会自动对已经挂载的分区进行虚拟分区的应用。若虚拟分区应用成功,则会启动虚拟分区对该物理分区进行管理;若虚拟分区应用失败,则会以普通FAT文件系统对该物理分区进行管理,并传出特定的错误码表明在应用虚拟分区时发生了何种错误。
虚拟分区成功挂载后,会在分区根目录下建立对应数量以及对应名称的目录,即作为虚拟分区入口。在虚拟分区入口内部创建文件,即相当于在对应的虚拟分区内创建文件。每个虚拟分区入口内的最大可用空间,即为设置的虚拟分区百分比大小。
卸载虚拟分区可以认为是挂载的逆过程,不再进行重复。
当对一个新物理分区应用虚拟分区时,执行挂载操作即可完成虚拟分区的创建,并且可以在LiteOS中进行重复使用。创建的虚拟分区的基本参数,以调配接口所设置的参数为准。
某些情况下,需要对某个分区重新应用虚拟分区时,可在已经挂载的分区上格式化该磁盘为FAT32文件系统。在格式化操作完成后,则会对该物理分区重新应用虚拟分区。
须知: 执行格式化操作会导致用户数据丢失,在执行此操作时请小心。
当某些情况下,需要撤销对物理分区使用虚拟分区特性,可以在未挂载的分区上进行格式化,即可清除该分区上的虚拟分区信息。
须知: 执行格式化操作会导致用户数据丢失,在执行此操作时请小心。
注意事项¶
FAT文件系统中,单个文件不能大于4GB。
带绝对路径的文件名,除去挂载点之后的长度总和不能大于252Byte。
目前只支持FAT32格式,如果使用其他文件格式,需要先将介质格式化为FAT32再对接到LiteOS。
FAT文件系统支持的操作有:open,close,read,write,seek,sync,opendir,closedir,readdir,rewinddir,statfs,virstatfs,unlink,mkdir,rmdir,rename,stat,lstat,stat64,utime,chattr,seek64,getlabel,set_label,format,fallocate,fallocate64,truncate,truncate64,fscheck。
以可写方式打开一个文件后,未close前再次打开会失败。同时打开同一文件,必须全部以只读方式打开。长时间打开一个文件,没有close时数据会丢失,必须close才能保存。
open打开一个文件,参数有O_TRUNC时,会将文件中的内容清空。
若open未使用mode入参,则无法通过mode入参来控制文件权限。
目前FAT文件系统不建议open带O_DIRECTORY属性的操作,打开目录请使用opendir()。
FAT文件系统的读写指针没有分离,所以以O_APPEND(追加写)方式打开文件后,读指针也在文件尾,读文件前需要用户手动置位。
FAT文件系统的stat及lstat函数获取的文件时间只是文件的修改时间,暂不支持获取文件创建时间和最后访问时间。微软FAT协议不支持1980年以前的时间。
在format操作之前,若FAT文件系统已挂载,需确保所有目录及文件已经全部关闭,否则format会失败。
FAT支持挂载只读属性:
当mount函数的入参为MS_RDONLY时,FAT将开启只读属性,所有带写入操作的接口(format接口除外),如write、mkdir、unlink,以及通过非O_RDONLY属性打开的文件,将均被拒绝,并传出EACCESS错误码。
当mount函数的入参为MS_NOSYNC时,FAT不会主动将Cache的内容写回存储器件。FAT的如下接口(open、close、 unlink、rename、mkdir、rmdir、chattr、truncate)不会自动进行sync操作,虽然可以提升速度,但是需要上层主动调用sync来同步数据,否则下电可能会导致数据丢失。
当前Cache的默认大小为16个块,每个块256个扇区。可以在“fs/include/vfs_config.h”文件中修改Cache的块数量CONFIG_FS_FAT_BLOCK_NUMS 和每个块的扇区数CONFIG_FS_FAT_SECTOR_PER_BLOCK,注意CONFIG_FS_FAT_SECTOR_PER_BLOCK必须为32的倍数,且不能大于2048。
当有两张SD卡插槽时,卡0和卡1不固定,先插入的为卡0,后插入的为卡1。
为避免SD卡使用异常和内存泄漏,在SD卡使用过程中拔卡,必须先关闭正处于打开状态的文件和目录,然后umount挂载节点。
YAFFS2¶
概述¶
YAFFS是Yet Another Flash File System的简称,是一种开源的、针对Nand Flash的嵌入式文件系统。在YAFFS中,最小的存储单位为page。
目前YAFFS文件系统有YAFFS和YAFFS2两个版本,主要区别在于page读写size的大小,YAFFS2可支持到2K per page,远高于YAFFS的512 bytes,因此YAFFS2能够更好地支持大容量的Nand Flash芯片。此外YAFFS2还有64bytes的SPARE区域,用于存储坏块信息、ECC校验等。
YAFFS2专为Nand Flash设计,适用于大容量的存储设备,同时也使得Nand Flash具有高效性和健壮性。
YAFFS2实现对2K per page的读写支持,同时在内存空间占用、垃圾回收速度、读写速度等方面均有大幅提升。
YAFFS2为文件系统提供了损耗平衡和掉电保护,保证在修改文件系统中的数据时,即使发生意外也不损坏数据。
LiteOS的YAFFS2文件系统支持多分区,采用双向链表结构实现。
开发指导¶
添加YAFFS2分区,该函数会自动为设备节点命名,对于YAFFS2,其命名规则是“/dev/nandblk”加上分区号。 |
||
说明:
add_mtd_partition添加YAFFS2分区时,系统会自动对起始地址和分区大小根据设备block大小进行对齐。该函数有四个参数:
第一个参数表示介质,支持“nand”和“spinor”。YAFFS2分区在“nand”上使用,littlefs在“spinor”上使用。
第二个参数表示分区的起始地址,以16进制的形式传入。
第三个参数表示分区大小,以16进制的形式传入。
最后一个参数表示分区号,有效值为0~19。
delete_mtd_partition函数有两个参数:
第一个参数是分区号。
第二个参数为介质类型,该函数与add_mtd_partition()函数对应。
使用LiteOS的YAFFS2文件系统,涉及以下几个步骤(各步骤详细操作见下述分解):
打开菜单,选择FileSystem ---> Enable YAFFS2,使能YAFFS2文件系统。
目前支持使用mkyaffs2image工具制作文件系统镜像,可以参考如下命令制作镜像:
./mkyaffs2image rootfs rootfs.yaffs2 1 2
说明: 用户可根据实际情况修改命令中参数值。
制作好镜像后,就可以烧录此镜像了。
创建分区的示例代码如下:
if (ret = add_mtd_partition("nand", 0x900000, 0x200000, 0) < 0) {
dprintf("add yaffs partition 0 failed, return %d\n", ret);
}
if (ret = add_mtd_partition("nand", 0xb00000, 0x200000, 1) < 0) {
dprintf("add yaffs partition 1 failed, return %d\n", ret);
}
创建成功后,在Shell中可以使用“partition nand”命令查看Nand Flash的分区信息:
LiteOS# partition nand
nand partition num:0, dev name:/dev/nandblk0, mountpt:(null), startaddr:0x0900000,length:0x0200000
nand partition num:1, dev name:/dev/nandblk1, mountpt:(null), startaddr:0x0b00000,length:0x0200000
调用mount()函数实现设备节点的挂载。
ret = mount("/dev/nandblk0", "/yaffs0", "yaffs", 0, NULL);
if (ret) {
dprintf("mount yaffs err %d\n", ret);
}
说明: mount()函数有五个参数:
第一个参数表示设备节点,这个参数需要和add_mtd_partition()函数对应起来。
第二个参数表示挂载点。
第三个参数表示文件系统类型。
第四个参数表示挂载标志,默认为0。
最后一个参数表示私有数据,默认为NULL。
除了在源码中调用mount()函数,也可以在Shell中使用mount命令实现挂载,前面3个参数同mount()函数,最后两个参数不需要给出,回显“mount ok”表明挂载成功:
LiteOS# mount /dev/nandblk1 /yaffs1 yaffs
mount ok
挂载成功后,在Shell中可以使用partition nand命令看到设备节点的挂载点信息:
LiteOS# partition nand
nand partition num:0, dev name:/dev/nandblk0, mountpt:/yaffs0, startaddr:0x0900000,length:0x0200000
nand partition num:1, dev name:/dev/nandblk1, mountpt:/yaffs1, startaddr:0x0b00000,length:0x0200000
调用umount()函数卸载分区,只需要正确给出挂载点即可。这一操作也可以在Shell中使用umount命令实现,回显“umount ok”表明命令执行成功:
LiteOS# umount /yaffs1
umount ok
LiteOS# umount /yaffs0
umount ok
卸载成功后,在Shell中可以使用“partition nand”命令看到设备节点的挂载点变为空。
LiteOS# partition nand
nand partition num:0, dev name:/dev/nandblk0, mountpt:(null), startaddr:0x0900000,length:0x0200000
nand partition num:1, dev name:/dev/nandblk1, mountpt:(null), startaddr:0x0b00000,length:0x0200000
调用delete_mtd_partition删除分区前要卸载分区。
ret = delete_mtd_partition(1, "nand");
if (ret != 0) {
printf("delte yaffs error\n");
} else {
printf("delete yaffs ok\n");
}
注意事项¶
对于YAFFS2的多分区特性,分区起始地址可以灵活配置,但分区间地址不能重叠,用户需要根据Flash使用情况,合理配置空间,防止与其它文件系统或其它不适合挂载YAFFS2文件系统的空间发生冲突。
最小可添加的分区为一个block大小,但是最小可挂载的分区为9个block大小(由YAFFS2特性决定),要注意区分这两个概念。
建议分区使用前先进行擦除操作。
LiteOS的YAFFS2文件系统只允许连续打开20个目录。
YAFFS2文件系统支持的操作有:open,close,read,write,seek,sync,dup,opendir,closedir,readdir,rewinddir,statfs,unlink,mkdir,rmdir,rename,stat,stat64,seek64。
open打开一个文件,参数有O_TRUNC时,必须同时拥有写的权限,才会将文件中的内容清空。
YAFFS2当前不支持获取文件的atime、mtime、ctime,使用stat接口获取出来的值为0。
目前YAFFS2不建议open带 O_DIRECTORY属性的操作,打开目录请使用opendir()。
YAFFS2支持挂载只读属性,当mount函数的入参为MS_RDONLY时,YAFFS2将开启只读属性,所有带写操作的接口,如write、mkdir、unlink,以及通过非O_RDONLY属性打开的文件,将均被拒绝,并传出EACCES错误码。
open打开一个文件,o_flags必须带有O_RDONLY、O_WRONLY、O_RDWR中的一种属性,o_flags设置的读写权限必须与创建文件时设置的mode匹配。
LITTLEFS¶
概述¶
LITTLEFS是LITTLE File System(微型文件系统)的缩写,是一种专门为微控制器设计的故障保护文件系统,主要应用于NOR Flash闪存,其特点包括:
可读写。
提供损耗平衡。
提供掉电安全保护,在修改文件系统中的数据时,即使发生意外也不损坏数据。
内存和代码很精简,占用很少的RAM和FLASH。
LITTLEFS是基于块的文件系统,磁盘被分成大小均匀的块阵列,作为存储的逻辑单元,这些块在使用时由公共块分配器进行分配。LITTLEFS使用小日志存储元数据,并使用较大的写时复制(COW)结构存储文件数据。
LiteOS的LITTLEFS主要应用于NOR Flash闪存的文件管理,且支持多分区。
开发指导¶
打开菜单,选择FileSystem ---> Enable LITTLEFS,使能LITTLEFS文件系统。
调用add_mtd_partition()函数添加LITTLEFS分区。
if (add_mtd_partition("spinor", 0x100000, 0x500000, 0) != 0) {
dprintf("add spinor partition failed\n");
}
说明: add_mtd_partition()添加LITTLEFS分区时,系统会自动根据设备block大小将分区起始地址和大小对齐。 该函数有四个参数:
第一个参数表示介质,支持“nand”和“spinor”。LITTLEFS分区在“spinor”上使用。
第二个参数表示分区的起始地址,以16进制的形式传入。
第三个参数表示分区大小,以16进制的形式传入。
最后一个参数表示分区号,有效值为0~19。
成功后,可以在Shell中使用“partition spinor”命令查看spinor flash分区信息,更多请详见“partition”。
LiteOS# partition spinor
spinor partition num:0, dev name:/dev/spinorblk0, mountpt:(null), startaddr:0x0100000, length:0x0500000
调用mount()函数实现设备节点的挂载。
ret = mount("/dev/spinorblk0", "/littlefs0", "littlefs", 0, NULL);
if (ret != 0) {
dprintf("ERROR:mount littlefs failed\n");
}
说明: mount()函数有五个参数:
第一个参数表示设备节点,这个参数须与add_mtd_partition()函数对应。
第二个参数表示挂载点。
第三个参数表示文件系统类型。
第四个参数表示挂载标志,默认为0。
最后一个参数表示私有数据,默认为NULL。
调用mount()函数是否成功,可以在Shell中执行“ls”查看当前根目录下的目录。
LiteOS# ls
Directory /:
bin <DIR>
dev <DIR>
littlefs0 <DIR>
挂载成功后,可以对NOR Flash进行读写操作。
调用umount()函数卸载分区,只需要给出正确的挂载点即可。
ret = umount("/littlefs0");
if (ret != 0) {
dprintf("umount littlefs err\n");
}
说明: umount()函数只有一个“表示挂载点”的参数。
调用delete_mtd_partition()删除已经卸载的分区。
delete_mtd_partition(0, "spinor");
说明: delete_mtd_partition()函数有两个参数:
第一个参数是分区号。
第二个参数为介质类型,该参数与add_mtd_partition()函数对应。
注意事项¶
如果以O_TRUNC方式open一个文件时,会将文件中的内容清空。
若open未使用mode入参,则无法通过mode入参来控制文件权限。
LITTLEFS文件系统的读写指针分离,以O_APPEND(追加写)方式打开文件后,读指针在文件头,写指针在文件尾。
LITTLEFS文件系统用于NOR Flash,最终调用NOR Flash驱动接口,因此使用LITTLEFS文件系统之前需要保证存在NOR Flash闪存,且驱动已初始化成功(spinor_init()返回0)。
目前LITTLEFS文件系统支持的操作有:open,close,read,write,seek,opendir,closedir,readdir,rewinddir,unlink,mkdir,rmdir,rename,stat,stat64,seek64。
当前LITTLEFS读写cache的默认大小为1024Byte,可以在“fs/littlefs/include/vfs_littlefs.h”文件中修改LFS_CACHE_SIZE的大小,注意必须为LFS_MIN_CACHE_SIZE的倍数,且不能大于65536Byte。
打开文件并写入数据,若在未close的情况下rename,数据会丢失。
目前access操作只能判断文件是否存在,不能判断文件是否具有指定的权限。
目前stat操作只能获取文件名、大小、文件类型(文件或目录),无法获取文件权限。
目前LITTLEFS不建议open带O_DIRECTORY属性的操作,打开目录请使用opendir()。
若同时打开同一文件,必须全部以只读方式打开。若以可写方式同时打开一个文件并写入数据,必须close才能保存,且只保存最后一次close的数据。
LITTLEFS支持挂载只读属性,当mount函数的入参mountflags为MS_RDONLY时,LITTLEFS将开启只读属性,所有带有写操作的接口,如write、mkdir、unlink,以及通过非O_RDONLY属性打开的文件,均将被拒绝,并传出EACCESS错误码。
兼容接口¶
概述¶
LiteOS提供了一套自有OS接口,同时也支持musl libc库、C++标准库、CMSIS接口和Linux接口。上层APP建议调用musl libc库等兼容接口,不建议直接调用LiteOS的自有OS接口。下面章节将列出LiteOS支持的兼容接口。
须知: LitesOS适配的libc库函数,遵循开源musl libc的实现形式,musl开源代码遵循标准C规范ISO/IEC 9899:1999和POSIX标准1003.1™-2008:作为最简单的底层库函数,内部不对指针类型参数做非空判断,由使用者保证入参的合法性。 其中LiteOS实现的获取时间类接口都是自系统起调度算起,与接口标准定义不一致,使用时需特别注意。
POSIX接口¶
POSIX支持接口¶
LiteOS提供一套POSIX适配接口,具体的规格参见下表。
POSIX NP支持接口¶
LiteOS还提供了一套POSIX NP(nonportable)适配接口,以支持部分SMP(多核)的NP接口。NP接口为非POSIX标准接口,作为POSIX接口的补充。以下接口只有在开启多核模式下可以操作亲和性,在单核模式下直接返回ENOERR。
POSIX不支持接口¶
LiteOS的POSIX接口中,有一些未支持,具体参见下表。
Libc/Libm/Libmat接口¶
Libc支持接口¶
LiteOS支持部分Libc接口,具体如下表所示。
设置文件流的缓冲区,当入参size小于0时,返回-1,并设置错误码。当入参buf为NULL,则会动态申请默认大小空间作为文件缓冲区 |
|||
根据LC_COLLATE来转换字符串src,若传入n的长度小于字符串src的长度,则不作转换,直接返回字符串src的长度 |
|||
Libm支持接口¶
LiteOS提供一套Libm开源接口,具体的规格参见下表。
须知: Libm不支持设置错误返回码。
Libmat支持接口¶
LiteOS支持部分Libmat接口,具体如下表所示。
打开菜单,进入Lib ---> Enable lib matrix菜单,完成矩阵模块的配置。
须知: 矩阵运算:
需要用户保证入参的矩阵是存在的,行列数是自身矩阵的行列数。
未做数据溢出判断,需要用户自己确保计算结果不会溢出 矩阵乘法: 需要用户保证入参的结果矩阵是空矩阵。
Libc/Libm/Libmat不支持接口¶
LiteOS的Libc/Libm/Libmat接口中,有一些未支持,具体参见下表。
须知: 自LiteOS V200R005C00版本起不再支持“stdlib.h”头文件中提供的atexit()。
CMSIS接口¶
CMSIS v1.0¶
本部分简要介绍LiteOS适配CMSIS v1.0接口的情况,更多关于接口声明、入参介绍、返回值类型、接口使用条件等详细介绍,请参见《CMSIS v1.0接口官方手册》。
CMSIS v1.0适配接口¶
LiteOS目前已支持大部分CMSIS v1.0接口,接口声明在“self_src/compat/cmsis/cmsis_os1.h”,接口描述详见下表。
CMSIS v1.0不支持接口¶
无。
CMSIS v1.0标准接口适配差异¶
考虑接口的易用性和LiteOS内部机制与CMSIS标准接口的差异,在适配CMSIS v1.0接口时,对部分接口进行了修改,详见下表。
增加一种定时器类型osTimerDelay,与osTimerOnce同为单次定时器,差别在于osTimerOnce超时后会删除定时器,但osTimerDelay不会,可以通过osTimerStart重复启动。 |
||
CMSIS v1.0非标准接口
CMSIS v2.0¶
本部分简要介绍LiteOS适配CMSIS v2.0接口的情况,更多关于接口声明、入参介绍、返回值类型、接口使用条件等详细介绍,请参见《CMSIS v2.0接口官方手册》。
CMSIS v2.0适配接口¶
LiteOS目前已支持大部分CMSIS v2.0接口,接口声明在“open_source/CMSIS/CMSIS/RTOS2/Include/cmsis_os2.h“”,接口描述详见下表。
CMSIS v2.0不支持接口¶
CMSIS v2.0不支持接口描述如下表所示。
挂起内核阻止调度,一般用于低功耗处理,目前LiteOS已提供Tickless、Runstop等低功耗机制,暂未适配该接口。 |
||
获取已创建的任务列表,目前未适配该接口,用户可以调用LiteOS的LOS_TaskInfoGet等维测接口获取任务状态。 |
||
CMSIS v2.0标准接口适配差异¶
考虑接口的易用性和LiteOS内部机制与CMSIS标准接口的差异,在适配CMSIS v2.0接口时,对部分接口进行了修改,详见下表。
CMSIS v2.0非标准接口
linux适配¶
LiteOS目前适配的是linux-4.19.90版本。Linux适配支持裁剪,如果需要使用Linux适配功能,可以打开菜单,选择Compat ---> Enable Linux以使能Linux适配模块。
完成量(completion)¶
概述¶
完成量(completion)是一种轻量级任务同步机制。完成量是对信号量的一种补充,Linux的信号量机制,在同一个信号量上允许并发执行up()和down(),在多CPU运行场景下,up()可能试图访问一个不存在的信号量,为了防止这种错误,专门设计了完成量这种机制。
当一个任务需要等待另一个任务完成某操作后才能执行时,通过完成量允许被等待任务在完成指定操作后通知等待任务,从而唤醒等待任务,实现任务同步。
开发指导¶
完成量应用于多任务同步场合,实现任务间的同步。
完成量的典型开发流程:
注意事项¶
完成量类似于信号量机制,参考信号量机制“注意事项”,即不能在中断中以阻塞模式(永久阻塞和定时阻塞)操作完成量,因为中断不能被阻塞。
需要用户保证完成量接口的完成量指针入参的合法性,即入参必须为合法的完成量指针。
编程实例¶
示例中,任务Example_TaskEntry创建了任务Example_Completion。Example_Completion等待完成量而阻塞,Example_TaskEntry唤醒该完成量。通过打印的先后顺序可以理解完成量操作时伴随的任务切换。
在任务Example_TaskEntry中创建任务Example_Completion,其中任务Example_Completion优先级高于Example_TaskEntry。
在任务Example_Completion中等待完成量,阻塞,发生任务切换,执行任务Example_TaskEntry。
在任务Example_TaskEntry中唤醒完成量,发生任务切换,执行任务Example_Completion。
Example_Completion得以执行,直到该任务结束。
Example_TaskEntry得以执行,直到任务结束。
前提条件:在menuconfig菜单中已经使能Linux适配模块。
代码实现如下:
#include "linux/completion.h"
#include "los_task.h"
/* 任务ID */
UINT32 g_testTaskId;
/* 完成量 */
struct completion exampleCompletion;
/* 用例任务入口函数 */
VOID Example_Completion(VOID)
{
UINT32 ret;
/* 以超时等待方式等待完成量,超时时间为100Ticks */
printf("Example_Completion wait completion\n");
ret = wait_for_completion_timeout(&exampleCompletion, 100);
if(ret == 0) {
printf("Example_Completion, wait completion timeout\n");
} else {
printf("Example_Completion, wait completion success\n");
}
return;
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task;
/* 完成量初始化 */
init_completion(&exampleCompletion);
/* 创建任务 */
memset(&task, 0, sizeof(TSK_INIT_PARAM_S));
task.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Completion;
task.pcName = "EventTsk1";
task.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task.usTaskPrio = 8;
ret = LOS_TaskCreate(&g_testTaskId, &task);
if(ret != LOS_OK) {
printf("task create failed \n");
return LOS_NOK;
}
/* 唤醒完成量 */
printf("Example_TaskEntry complete\n");
complete(&exampleCompletion);
printf("Delete Task.\n");
/* 删除任务 */
ret = LOS_TaskDelete(g_testTaskId);
if(ret != LOS_OK) {
printf("task delete failed\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
Example_Completion wait completion
Example_TaskEntry complete
Example_Completion, wait completion success
Delete Task.
工作队列(workqueue)¶
概述¶
工作队列提供一种机制:任何系统模块都可以将工作项挂载到工作队列,由工作队列按照FIFO方式处理挂载在上面的工作项。
工作队列通过工作队列任务处理挂载到工作队列中的工作项。队列处理任务允许被重新调度和睡眠,从而节省系统资源。
LiteOS的工作队列机制提供了丰富的对外接口以管理工作队列。当有工作项挂载到工作队列中时,工作队列处理任务将被唤醒去处理工作队列中的工作项,直到所有工作项都被处理完,工作队列处理任务将重新进入睡眠状态。
开发指导¶
工作队列运用在对时间要求不高的场景,原因是工作队列里的工作只有在工作队列处理任务得到运行时才能被逐一处理,而工作队列处理任务是否能被及时唤醒调度取决于其他内核任务,另外在中断里也不能调度,只有在退出中断后才能得到调度。
初始化一个延迟工作,将该工作与指定的处理函数绑定,该工作将延迟一段时间后才会被执行。当该工作被执行时,就会执行这个处理函数 |
|||
说明:
创建工作队列时,会初始化信号量,用于工作队列处理任务的睡眠和唤醒,以及初始化重要结构体并搭建工作队列链表。
销毁工作队列时,系统先对资源进行加锁操作,再删除工作队列处理任务,释放信号量资源,释放内存空间等,最后解锁。
有两种类型的工作:非延迟工作和延迟工作。工作可以立即得到挂载,而延迟工作需要延时指定时间后才能被挂载到工作队列中。
工作队列的典型流程:
打开菜单,选择Compat ---> Enable Linux ---> Enable workqueue,使能工作队列。
调用create_workqueue接口创建工作队列。
调用INIT_WORK/INIT_DELAYED_WORK初始化工作。
调用queue_work/queue_delayed_work接口挂载工作项到工作队列。
调用flush_work/flush_delayed_work接口立即执行工作。
调用cancel_delayed_work_sync/cancel_work_sync接口取消工作。
调用destroy_workqueue接口删除工作队列。
注意事项¶
挂载到工作队列的工作项由工作队列处理任务进行处理,而任务的调度运行在时间上具有不确定性,所以对时间要求高的工作不建议使用工作队列进行处理。
LiteOS使用工作队列名对工作队列进行标识,所以不能存在相同名称的工作队列。
可以使用schedule_work/schedule_delayed_work接口向系统默认的工作队列挂载工作,而不用再创建工作队列。
编程实例¶
本实例实现如下功能:
创建名称为wq_test的工作队列。
分配工作内存并且初始化工作。
向工作队列中挂载工作。
立即执行工作。
销毁工作队列。
前提条件:在menuconfig菜单中已经使能Linux适配模块。
代码实现如下:
#include "los_printf.h"
#include "linux/workqueue.h"
static void Work_Func(struct work_struct *work)
{
int i;
for (i = 0; i < 2; i++) {
dprintf("workqueue function is been called!..%d..%d..\n", i, work->work_status);
}
}
UINT32 Example_Workqueue(VOID)
{
struct workqueue_struct *wq;
struct work_struct *work;
UINT32 ret = LOS_OK;
wq = create_workqueue("test1008");
dprintf("create the workqueue successfully!\n\n");
work =(struct work_struct *)malloc(sizeof(struct work_struct));
if (!work) {
ret = LOS_NOK;
}
dprintf("create work ok!\n\n");
INIT_WORK(work, Work_Func);
dprintf("init the work ok!\n\n");
ret = queue_work(wq, work);
dprintf("mount the work into workqueue successfully!\n\n");
ret = flush_work(work);
dprintf("flush the work ok!\n\n");
destroy_workqueue(wq);
dprintf("destroy the work ok!\n\n");
return ret;
}
编译运行得到的结果为:
create the workqueue successfully!
create work ok!
init the work ok!
mount the work into workqueue successfully!
workqueue function is been called!..0..3..
workqueue function is been called!..1..3..
flush the work ok!
destroy the work ok!
等待队列(waitqueue)¶
概述¶
等待队列常用于线程等待资源。
当一个线程必须获取某些资源后才能进行下一步操作,且不占用CPU资源的时候,可以将该线程放入等待队列中,然后系统会切换到其他进程,等资源准备好后再唤醒等待队列。等待队列可以概括为两种等待情况:超时等待和循环等待。
LiteOS的等待队列机制提供了丰富的对外接口以管理等待队列,可以满足linux系统等待队列的绝大多数功能。
开发指导¶
与工作队列相比,等待队列运用在对时间要求较高的场景,尤其是超时等待机制,常见场景有:同步系统资源访问、中断底半部和异步事件通知等。
说明: 有两种类型的等待队列:
循环等待,只有等到资源后才会退出等待。
超时等待,即使未等到资源,在等待时间到达之后,也会退出等待。
等待队列的典型开发流程:
打开菜单,选择Compat ---> Enable Linux ---> Enable waitqueue,使能等待队列。
调用DECLARE_WAIT_QUEUE_HEAD/init_waitqueue_head接口初始化等待队列头。
调用wait_event/wait_event_interruptible/wait_event_interruptible_timeout等待资源。
调用wake_up/wake_up_interruptible/wake_up_interruptible_poll唤醒等待队列。
注意事项¶
LiteOS的等待队列不支持linux内核signal打断的机制。
LiteOS不支持在一个等待队列中增加多个等待项,一个等待队列中只能有一个等待项。
编程实例¶
本实例实现如下功能:
定义并初始化一个等待队列头。
创建一个任务,在任务中挂起事件,等待被唤醒。
唤醒事件。
前提条件:在menuconfig菜单中已经使能Linux适配模块。
代码实现如下:
#include "los_printf.h"
#include "los_task.h"
#include "linux/wait.h.h"
static UINT32 g_TestTaskID;
static DECLARE_WAIT_QUEUE_HEAD(g_waitQueue);
static BOOL g_condition = FALSE;
static VOID Example_Task(VOID)
{
UINT32 timeout = 10; // 10 ticks timeout
UINT32 ret;
ret = wait_event_interruptible_timeout(g_waitQueue, g_condition, timeout);
if (ret > 0) {
g_TestCount++;
}
}
UINT32 Example_Waitqueue(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S stTask = {0};
stTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Task;
stTask.usTaskPrio = TSK_PRIOR_HI;
stTask.pcName = "wait_task";
stTask.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
stTask.uwResved = LOS_TASK_STATUS_DETACHED;
g_TestCount = 0;
ret = LOS_TaskCreate(&g_TestTaskID, &stTask);
if (ret != LOS_OK) {
printf("Example_Task create Failed!\r\n");
return LOS_NOK;
}
LOS_TaskDelay(2);
g_condition = TRUE;
wake_up_interruptible(&g_waitQueue);
LOS_TaskDelay(1);
if (g_TestCount == 1) {
printf("Wakeup succeeded!\r\n");
return LOS_OK;
} else {
printf("Wakeup failed!\r\n");
return LOS_NOK;
}
}
编译运行得到的结果为:
Wakeup succeeded!
中断¶
概述¶
Linux内核中有专门的中断函数。为提高系统使用的友好度,LiteOS基于自身的中断机制,适配了Linux中的相关中断接口。
LiteOS的中断支持:
中断申请
中断删除
中断使能
中断屏蔽
中断底半部(基于workqueue)
中断顶半部和底半部的概念:
中断处理程序耗时应尽可能短,以满足中断的快速响应,为了平衡中断处理程序的性能与工作量,将中断处理程序分解为两部分:顶半部和底半部。
顶半部完成尽可能少的比较紧急的任务,它往往只是简单地读取寄存器中的中断状态并清除中断标志位,即进行“登记工作”,将耗时的底半部处理程序挂到系统的底半部执行队列中去。
开发指导¶
LiteOS中断模块适配的linux接口如下表,接口详细信息可以查看API参考。
说明: 中断底半部的功能,可利用workqueue实现,在上半部中初始化一个工作项,将其添加到workqueue中,系统会在空闲时执行workqueue里的底半部,详细使能用法请查阅“工作队列(workqueue)”。
注意事项¶
调用request_irq()申请中断时,中断处理程序入参要符合(int, void*)标准格式。直接调用LOS_HwiCreate()创建中断时,中断处理程序入参可以为NULL。
中断处理程序中禁止调用request_irq()和free_irq()接口。
当中断ID为共享中断时,不能通过LOS_HwiCreate()创建中断,且request_irq()入参dev指针不能为NULL,应与处理程序一一对应。直接调用LOS_HwiCreate()创建的中断,不能通过free_irq()删除。
编程实例¶
本实例实现如下功能:
申请中断。
删除中断。
前提条件:
在menuconfig菜单中使能Linux适配模块。
在menuconfig菜单中配置中断使用最大数和可设置的中断优先级个数。
代码实现如下:
#include "los_hwi.h"
#include "linux/interrupt.h"
#define HWI_NUM_INT50 50
void Uart_Irqhandle(int irq, void *dev)
{
printf("\nuart0:the function1 \n");
}
void Hwi_Test(void)
{
int a = 1;
void *dev = &a;
unsigned long flags = 0;
const char * name = "hwiTest";
request_irq(HWI_NUM_INT50, Uart_Irqhandle, flags, name, dev); // 创建中断
free_irq(HWI_NUM_INT50, dev); // 删除中断
}
高精度定时器¶
概述¶
高精度软件定时器利用一定的硬件定时器资源,通过算法实现更高精度的软件定时器,满足了对精确时间的需求。
LiteOS高精度软件定时器设计了一套软件架构,提供了微秒级的定时精度,以满足对精确时间有迫切需求的应用程序或内核驱动。
开发指导¶
内核软件定时器的定时精度为毫秒级别,定时精度并不高,无法满足对定时精度要求更高的场合。高精度软件定时器弥补了内核软件定时器在这方面的不足,提供了精度到达微秒级别的定时接口。
高精度软件定时器使用的典型流程:
打开菜单,选择Compat ---> Enable Linux ---> Enable Linux Hrtimer,使能高精度定时器。
定义一个hrtimer变量和一个ktime变量。
接着根据定时时间,给ktime变量赋值,分别为sec及usec字段。
然后调用hrtimer_create接口,创建高精度定时器,设置其定时器时间及回调函数。
最后调用hrtimer_start接口,启动高精度软件定时器。
调用hrtimer_cancel与hrtimer_forward接口取消或修改定时器的超时时间。
hrtimer_is_queued接口辅助用户判断高精度定时器是否已经创建。
注意事项¶
高精度软件定时器接口涉及的hrtimer变量,需要用户提供,接口不会为其申请任何资源。
hrtimer_init接口没有实现,只是适配linux接口,所以使用时可不调用。
目前高精度软件定时器只支持相对时间模式,不支持绝对时间模式。
高精度软件定时器依赖硬件,使用前需要确认硬件是否支持。
高精度定时器存在指令时间消耗,所以实际定时触发时间会大于设置的时间。
编程实例¶
本实例实现如下功能:
用户定义swtmr变量及time变量。
调用hrtimer_create接口创建高精度定时器,设置其定时器时间及回调函数。
调用hrtimer_start接口启动定时器。
调用hrtimer_forward接口修改定时时间,由原来的80ms修改为40ms。
调用LOS_Msleep函数使任务延迟50ms。
再调用hrtimer_cancel接口取消定时器,发现此时定时器已超时不存在了。
前提条件:已使能高精度定时器。
代码实现如下:
#include "los_base.h"
#include "linux/hrtimer.h"
static enum hrtimer_restart Hrtimer_Func(struct hrtimer *arg)
{
dprintf("The hrtimer is timeout!!!\n");
return HRTIMER_NORESTART;
}
static UINT32 TestCase(VOID)
{
struct hrtimer swtmr;
union ktime time;
union ktime interval = { 0 };
interval.tv.sec = 0;
interval.tv.usec = 40000;
int ret;
dprintf("----->test start<-----\n");
time.tv.sec = 0;
time.tv.usec = 80000;
ret = hrtimer_create(&swtmr, time, Hrtimer_Func);
if (ret == 0) {
dprintf("Hrtimer ctreate successfully!\n");
}
ret = hrtimer_start(&swtmr, time, HRTIMER_MODE_REL);
if (ret == 0) {
dprintf("Hrtimer start successfully!\n");
}
ret = hrtimer_forward(&swtmr, interval);
LOS_Msleep(50);
ret = hrtimer_cancel(&swtmr);
if (ret == 0) {
dprintf("Hrtimer already timeout!\n");
}
dprintf("----->test end<-----\n");
return LOS_OK;
}
编译运行得到的结果为:
----->test start<-----
Hrtimer ctreate successfully!
Hrtimer start successfully!
The hrtimer is timeout!!!
Hrtimer already timeout!
----->test end<-----
linux适配接口概览¶
LiteOS提供的linux适配接口如下表所示。
说明: 完全兼容表示功能与linux一致,错误码返回值要以实际代码为准,部分兼容表示部分功能与linux一致。
linux不支持接口概览¶
LiteOS的linux适配接口中,有一些未支持,包括但不限于下表所示。
RT-Thread适配¶
本部分简要介绍LiteOS适配RT-Thread v5.2.1接口的情况。
RT-Thread适配接口¶
RT-Thread接口适配差异¶
考虑接口的易用性和LiteOS内部机制与RT-Thread标准接口的差异,在适配RT-Thread接口时,对部分接口进行了修改,详见下表。
不支持RT-Thread自带的INIT_EXPORT,如果想系统启动自动初始化函数,可参考LiteOS的宏 LOS_SYS_INIT。 |
||
相比原生接口,新增支持命令RT_TIMER_CTRL_GET_STATE(获取定时器状态)、RT_TIMER_CTRL_GET_REMAIN_TIME(获得软件定时器剩余Tick数)。 |
||
FreeRTOS适配¶
本部分简要介绍LiteOS适配FreeRTOS v11.1.0接口的情况。
FreeRTOS适配接口¶
FreeRTOS接口适配差异¶
考虑接口的易用性和LiteOS内部机制与FreeRTOS标准接口的差异,在适配FreeRTOS接口时,对部分接口进行了修改,详见下表。
驱动开发指导¶
本章以Hi3556V200为例,使能开发者操作控制与ETH、USB2.0 DRD和SD/MMC/eMMC卡等驱动模块相连的LiteOS外围驱动设备。
说明: Hi3556V200/Hi3559V200无ETH模块。
基于驱动框架的开发指导¶
概述¶
如图1所示,驱动开发就是按一定的规格实现硬件功能并进行抽象,提供给开发者一套统一的设备访问接口,方便开发者进行上层业务开发。
驱动设备
系统将所有外部设备看成是一类特殊文件,称为“设备文件”。外部设备可分为三类:
字符设备:设备以字符形式发送和接收数据,不支持随机访问。
块设备:利用一块系统内存作为缓冲区,以整个数据缓冲区的形式传输数据。块设备主要针对Flash等慢速设备,能避免CPU耗费过多的时间等待操作完成。
网络设备:所有对网络硬件的访问都通过接口进行。接口提供一个对所有类型的网络硬件一体化的操作集合,来发送和接收基本数据。
驱动程序
内核与外部设备之间的接口。驱动程序向应用程序屏蔽了硬件在实现上的细节。在应用程序看来,外部设备只是一个设备文件,使应用程序可以像操作普通文件一样来操作外部设备,如open ()、close ()、read ()、write () 等。
驱动框架管理
对驱动设备和驱动程序进行系统管理的模块。
驱动框架涉及到2个比较重要的数据结构LosDevice(用于描述驱动设备)和LosDriver(用于描述驱动程序)。系统中有两个全局双向链表。一个是全局设备链表,管理挂载的所有驱动设备;另一个是全局驱动链表,管理所有挂载的驱动程序。
这两种数据结构介绍如下:
LosDevice
LosDevice代表一个驱动设备。结构体中重要的成员包括:
const CHAR *name:设备名,用以跟驱动配对。
struct LosDriver *driver:表示该驱动设备的驱动程序。
struct LosDeviceConfig cfg:设备io起始地址、io大小及中断号配置。
LOS_DL_LIST driverNode:对应的驱动节点,用以挂载到该驱动结构体中的deviceList上。
LOS_DL_LIST deviceItem:该驱动设备节点,用以挂载到全局设备链表中。
LosDriver
LosDriver代表一个设备驱动程序。结构体中重要的成员包括:
const CHAR *name:驱动名,用以跟设备配对。
LOS_DL_LIST deviceList:使用当前这个驱动程序的驱动设备的双向链表。
LOS_DL_LIST driverItem:该驱动程序节点,用以挂载到全局驱动链表里。
struct LosDriverOps ops:驱动实现的操作接口,如probe、remove等。
LosDevice和LosDriver之间的关系如图2所示。
LosDriver挂在全局驱动链表下,包含了一个deviceList链表,表示这个驱动程序操作(或控制)的所有设备。
LosDevice挂在全局设备链表下,包含了一个driver指针,表示这个设备对应的设备驱动程序。
一个驱动设备只对应一个驱动程序,而一个驱动程序可以支持多个驱动设备。当对应的设备/驱动挂载的时候,会根据名字去全局驱动链表/全局设备链表中寻找已经注册的对应的驱动/设备,执行注册过程。
开发指导¶
LiteOS的驱动框架为用户提供下面几种功能,接口详细信息可以查看API参考。
下面以注册USB驱动为例,讲解基于驱动框架的驱动开发流程。
打开菜单,选择Driver ---> Enable Driver Base,使能驱动框架。
注册驱动设备。
定义USB驱动设备实例。
struct LosDeviceRegs hiudc3_regs[] = { { .base = DWC_USB3_PORT1_BASE_ADDR, .size = DWC_USB3_PORT1_ADDR_OFFSET, }, }; static struct LosDevice hiudc3_device = { .name = "hi_udc3", .id = -1, .cfg = { .irqNum = NUM_HAL_INTERRUPT_USB_DEV, .numRegs = ARRAY_SIZE(hiudc3_regs), .regs = hiudc3_regs, }, };
将驱动设备注册到全局设备链表。
LOS_DeviceRegister(&hiudc3_device);预加载处理。
通过调用“linux/module.h”(在根目录“self_src/compat/linux/include”目录下)里面的预加载挂载函数,将驱动设备注册函数挂载到系统中。当系统启动时,系统会按照挂载函数优先级遍历预加载节点,从而注册驱动设备。系统提供如下预加载挂载函数,优先级从高到低:
pure_initcall(f) //0 core_initcall(f) //1 postcore_initcall(f) //2 arch_initcall(f) //3 subsys_initcall(f) //4 fs_initcall(f) //5 device_initcall(f) //6 late_initcall(f) //7
具体调用请参见“编程实例”。
须知:
执行预加载可能会影响到系统的某些功能,例如,当驱动程序初始化耗时较长时,会对快速启动(分散加载)的启动时间造成冲击。目前系统只支持优先级小于等于3的预加载函数。
注册设备驱动程序。
定义USB驱动程序实例。
static struct LosDriver hiudc3_driver = { .name = "hi_udc3", .ops = { .probe = hiudc3_probe, .remove = hiudc3_remove, }, .pmOps = { .suspend = hiudc3_suspend, .resume = hiudc3_resume, }, };
参数描述如表所示。
注册USB驱动程序。
LOS_DriverRegister(&hiudc3_driver);
将加载的驱动注册到文件系统。
为方便用户使用,将驱动实例注册到文件系统,使对驱动的操作抽象为对文件的操作。具体实现请参见“适配文件操作的驱动开发”。
注意事项¶
调用预加载处理的驱动函数需要在“build/make/liteos_tables_ldflags.mk”中的LITEOS_TABLES_DRIVER_LDFLAGS添加“-u”声明,否则可能会导致链接器优化掉驱动初始化模块,造成不可预估的错误。
LITEOS_TABLES_DRIVER_LDFLAGS := \
-ui2c_init \
-ugpio_init \
-uregulator_init \
-umtd_init_list \
编程实例¶
添加USB驱动。
前提条件:在menuconfig菜单中使能驱动框架。
在指定开发板的“board.c”目录下定义驱动设备。
/* 定义驱动设备实例 */ #ifdef LOSCFG_DRIVERS_USB3_DEVICE_CONTROLLER struct LosDeviceRegs hiudc3_regs[] = { { .base = DWC_USB3_PORT1_BASE_ADDR, .size = DWC_USB3_PORT1_ADDR_OFFSET, }, }; static struct LosDevice hiudc3_device = { .name = "hi_udc3", .id = -1, .cfg = { .irqNum = NUM_HAL_INTERRUPT_USB_DEV, .numRegs = ARRAY_SIZE(hiudc3_regs), .regs = hiudc3_regs, }, }; #endif int machine_init(void) { #ifdef LOSCFG_DRIVERS_USB3_DEVICE_CONTROLLER (void)LOS_DeviceRegister(&hiudc3_device); #endif } arch_initcall(machine_init); //预加载驱动设备注册。内核初始化执行时,会预加载驱动设备注册
在“self_src/drivers/usb/adapt_liteos/controller/usb_device”目录下定义USB设备驱动程序。
/* 驱动程序实现的操作接口 */ static int hiudc3_probe(struct LosDevice *pdev) { …… } static int hiudc3_remove(struct LosDevice *pdev) { …… } static int hiudc3_suspend(struct LosDevice *pst_dev) { …… } static int hiudc3_resume(struct LosDevice *pst_dev) { …… } /* 定义设备驱动程序实例 */ static struct LosDriver hiudc3_driver = { .name = "hi_udc3", .ops = { .probe = hiudc3_probe, .remove = hiudc3_remove, }, .pmOps = { .suspend = hiudc3_suspend, .resume = hiudc3_resume, }, }; /* 初始化驱动程序,即注册驱动程序 */ int hiudc3_init(void) { return (int)LOS_DriverRegister(&hiudc3_driver); } /* 注销驱动程序 */ int hiudc3_exit(void) { return (int)LOS_DriverUnregister(&hiudc3_driver); }
适配文件操作的驱动开发¶
概述¶
驱动开发就是把硬件的功能按操作系统的规格实现并抽象出来,提供给应用程序开发人员调用。
当在新的芯片上移植系统时,需要根据该芯片特性对支持的外部设备进行驱动开发。
LiteOS的驱动初始化函数主要初始化设备对应的驱动结构,提供给上层应用来注册设备驱动的控制节点。
开发指导¶
推荐驱动开发人员使用VFS框架来注册/卸载设备驱动,基于文件系统的驱动开发主要涉及下面几步:
打开菜单,选择FileSystem ---> Enable VFS,使能VFS。
在LiteOS上进行驱动开发的第一步,是编写驱动初始化函数。驱动初始化函数用于初始化设备所需的驱动结构,以及生成驱动的控制节点。
驱动初始化函数中需要调用设备驱动注册函数,用于注册并生成驱动节点。
register_driver(FAR const char *path, FAR const struct file_operations_vfs *fops, mode_t mode, FAR void *priv)
register_blockdriver(FAR const char *path, FAR const struct block_operations *bops, mode_t mode, FAR void *priv)
参数描述如下表所示:
编写完驱动初始化函数后,需要在适当的地方引导该初始化函数执行。用户可以在app_init函数里调用已编写好的驱动初始化函数引导设备初始化。
驱动初始化后,生成的设备驱动节点为应用提供操作设备的接口,下面以I2C设备驱动程序为例,说明用户程序与驱动操作函数的调用关系。
驱动操作函数集对于应用程序与驱动操作函数的调用关系非常重要。在编写驱动程序时,操作函数集需要实现硬件设备的各项机制,并在设备驱动注册时传入。操作函数集会成为应用程序需求的最终实现。
I2C设备驱动提供的操作函数集如下表所示:
open操作
应用程序打开节点文件时,系统最终会在该驱动节点注册过程中调用驱动操作函数集中的open函数。
open函数对目标驱动设备函数结构体先进行实例化,再进行必要的初始化。在成功打开节点文件后,应用程序能够获取到对应驱动节点的文件描述符,并通过该文件描述符对驱动程序进行访问。
read/write操作
read/write操作是常用的访问设备的接口。不同类型的设备与驱动提供的read/write操作的功能互有差异。对于i2c设备,应用程序通过调用read/write接口可以实现对I2C设备进行读写。
i2cdev_read(struct file * filep, char __user *buf, size_t count) i2cdev_write(struct file * filep, const char __user *buf, size_t count)
参数描述如下表所示:
ioctl操作
ioctl操作提供对驱动设备函数的配置管理。通过执行相应的命令,完成对设备属性的配置或访问。
I2C设备中,使用命令I2C_16BIT_REG、I2C_16BIT_DATA与I2C_TIMEOUT分别对应设置I2C设备的传输寄存器位宽、传输数据位宽与超时时间。
i2cdev_ioctl(struct file *filep, int cmd, unsigned long arg)参数描述如下表所示:
close操作
close操作对应着驱动操作函数集里的release函数。release函数对驱动程序的资源进行释放。
注意事项¶
无。
MMC驱动开发¶
概述¶
MMC(Multi Media Card)是 MMC协会订立、主要针对手机或平板电脑等产品的内嵌式存储器标准规格。MMC驱动用于处理对SD存储卡(Secure Digital Memory Card)和EMMC卡(Embedded Multi Media Card)的识别和读写等操作,并通过SDIO协议支持扩展外设(如蓝牙、WiFi、GPS等)。
LiteOS MMC驱动支持的设备包括TF卡、EMMC存储器和WiFi扩展外设。
TF(T-Flash)卡,又叫micro SD卡
TF卡主要通过挂载为FAT文件系统实现对设备的读写操作,挂载之前首先需要完成设备的识别,也可以通过裸读写接口对TF卡进行裸读写。
须知:
目前LiteOS不支持用户自定义TF卡分区。如有需要,用户需提前在PC端对TF卡做好分区操作。EMMC存储器
EMMC存储器主要通过挂载为FAT文件系统实现对设备的读写操作,挂载之前需要先调用接口自定义分区,然后进行设备的识别,也可以通过裸读写接口对EMMC存储器进行裸读写。
支持LiteOS调度模块启动前完成MMC驱动初始化,支持系统启动前(系统资源初始化后、第一次任务调度前)和异常阶段调用MMC异常读写接口对MMC存储进行读写和格式化,需打开宏LOSCFG_DRIVERS_MMC_RW_IN_BOOT_AND_EXC。
支持在LiteOS系统全局调度器启动前完成MMC低频切高频的tuning流程,支持EMMC器件在启动前运行在200MHZ频率。
支持RPMB(Replay Protected Memory Block)分区,通过RPMB操作接口对RPMB分区进行KEY的写入、Write Counter读取、RPMB数据区数据的读写(仅数据透传不涉及加解密),需打开宏LOSCFG_DRIVERS_MMC_RPMB。
提供发送CMD56命令的对外接口,获取EMMC存储器相关信息。
SDIO扩展外设
目前支持的外围设备只有WiFi模组。设备通过WiFi模组进行通信和数据传输,设备识别前需要根据硬件要求做好管脚复用配置。
LiteOS MMC驱动模块包括协议层和控制层:
协议层支持SD、EMMC、SDIO协议规范。
控制层支持Himci和Sdhci控制器,不同的芯片平台根据硬件IP选择对应的控制器驱动。
开发指导¶
LiteOS中的MMC设备分为嵌入式和非嵌入式两类:
嵌入式设备包括EMMC存储器和SDIO扩展外设,均不支持带电插拔。EMMC设备目前仅支持首次识别,不支持多次识别;SDIO扩展外设可以调用hisi_sdio_rescan接口实现对设备的卸载和加载识别(依赖于扩展外设的软件上下电)。
非嵌入式设备为TF卡。TF卡支持热插拔,通过线程轮询方式实现设备的动态卸载和加载。
须知: 除SDIO扩展外设可以调用sdio_rescan接口实现设备识别外,其他MMC设备的识别均在同一线程内实现,当存在多个MMC设备时,将根据控制器ID的顺序串行识别设备。
须知: MMC驱动初始化成功后,如果系统支持proc文件系统,会生成“proc/mci/mci_info”节点,通过“cat proc/mci/mci_info”可查看设备信息。此两个初始化接口不可混用,且MMC_HostInitById调用需保证单线程调用或者时间上互斥。
入参,是一个设备信息结构体,这里表示EMMC的分区信息,当前只支持传入全局变量struct disk_divide_info emmc |
||
设备信息结构体定义如下所示:
#define MAX_DIVIDE_PART_PER_DISK 16 // 每个设备可支持的最大逻辑分区个数
#define MAX_PRIMARY_PART_PER_DISK 4 // 每个设备可支持的最大主分区个数
struct disk_divide_info{
UINT64 sector_count; // 分区块个数
UINT32 part_count; // 分区号
UINT32 sector_size; // 分区大小
struct partition_info part[MAX_DIVIDE_PART_PER_DISK + MAX_PRIMARY_PART_PER_DISK]; // 分区信息结构体
};
TF卡裸读写接口说明
EMMC裸读写接口说明
MMC系统启动前和异常中读写/初始化接口说明
须知:
MMC异常(中断)上下文读写接口支持TF卡与EMMC但不支持SDIO设备,不推荐在非异常(中断)中调用,在调用前用户需保证系统不处于MMC逻辑执行流程中,否则可能会导致接口读写失败。
RPMB数据帧结构体
typedef struct {
uint8_t padding[RPMB_PADING_LEN];
uint8_t mac_key[RPMB_MAC_KEY_LEN];
uint8_t data[RPMB_DATA_LEN];
uint8_t rand[RPMB_RAND_LEN];
uint32_t write_cnt;
uint16_t address;
uint16_t block_cnt;
uint16_t op_result;
uint16_t req_resp;
} rpmb_data_t;
RPMB分区操作接口
入参,对RPMB分区的操作类型,支持四种操作类型设置key(RPMB_SET_KEY)、获取Write Counter(RPMB_READ_WRITE_COUNTER)、写数据(RPMB_WRITE_DATA)、读数据(RPMB_READ_DATA) |
||
LiteOS提供了一套完整的SDIO对外接口,包括发送命令、收发数据等。SDIO扩展外设通过调用这套接口实现对扩展外设的操作。
LiteOS基于控制器能力,提供数据的离散模式传输,并提供了一套专门的读写接口。SDIO扩展外设通过调用这套接口实现离散传输,即可以将多块buffer数据一次性发送或者一次性读取数据到多块buffer,以减少多块数据传输场景下的数据拷贝,提升数据传输性能。
mmc_sg结构体如下所示:
struct mmc_sg {
size_t length; // 数据长度,不能大于buffer的内存空间大小
void *data; // buffer地址,必须cache line对齐,其指向的buffer内存空间大小也必须cache line对齐
};
添加EMMC分区
size_t part0_start_sector = 16 * (0x100000/512); size_t part0_count_sector = 1024 * (0x100000/512); extern struct disk_divide_info emmc; add_mmc_partition(&emmc, part0_start_sector, part0_count_sector);
初始化驱动
MMC_HostInitById(emmc_host_id);
说明:
emmc_host_id为EMMC存储器对应的控制器编号。
emmc介质识别成功后会生成“/dev/mmcblkxPy”的节点,供文件系统挂载。(x是设备ID,一般是0或者1,y是分区ID,由设备分区个数决定,从0开始)。设备识别后,读写设备
裸读写
调用EMMC裸读写接口进行读写。
挂载为FAT文件系统进行读写
mount("/dev/mmcblk0p0", "/sd", "vfat", 0, NULL);挂载后可通过文件系统标准接口对设备进行读写等操作。
说明:
请根据实际情况传入“/dev/mmcblk0p0”为EMMC存储器被识别后注册的设备名。
初始化驱动
MMC_HostInitById(sd_host_id);
说明:
sd_host_id为TF卡对应的控制器编号。插入sd卡并识别成功后会生成“/dev/mmcblkxPy”的节点,供文件系统挂载。其中,x是设备ID,一般是0或者1,y是分区ID,由设备分区个数决定,从0开始。设备识别后,读写设备
裸读写
调用TF卡裸读写接口进行读写。
挂载为FAT文件系统读写
mount("/dev/mmcblk0p0", "/sd", "vfat", 0, NULL);挂载后可通过文件系统标准接口对介质进行读写等操作。
说明:
“/dev/mmcblk0p0”为TF卡被识别后注册的设备名,请根据实际情况传入。
管脚复用
配置IO寄存器,配置对应SDIO外围设备的CLK、CMD、DATA0、DATA1、DATA2、DATA3管脚寄存器。
初始化驱动
MMC_HostInitById(sdio_host_id);
说明:
sdio_host_id为SDIO扩展外设对应的控制器编号。SDIO wifi设备识别成功后会生成“/dev/sdio0”的节点。设备识别后获取func并使能func
struct sdio_func* func; func = sdio_get_func(func_num, manf_id, device_id); sdio_en_func(func);
设置当前block size
sdio_set_cur_blk_size(func, blk_size);注册中断处理函数
void handler_sample(struct sdio_func *func) { dprintf(“%s,%d func:0x%x\n”,__FUNCTION__,__LINE__,func); return; } sdio_require_irq(func, handler_sample);
后续就可以通过SDIO对外读写接口进行读写等操作了。
注意事项¶
正常操作过程中需要遵守的事项:
保证卡的金属片与卡槽硬件接触充分良好(如果接触不好,会出现检测错误或读写数据错误),测试薄的MMC卡时,必要时可以用手按住卡槽的通讯端测试。
读写过程中异常拔卡,驱动会直接卸载设备节点。在该场景下,用户必须释放正在操作的资源(比如打开的文件、目录等资源),并且对挂载点进行umount操作。否则,会造成资源泄漏或挂载点不能再次挂载。
SDIO外设业务调用sdio_require_irq注册中断后,在设备卸载前必须保证调用sdio_release_irq接口释放中断和相关资源,否则可能导致系统异常。
EMMC存储器进行驱动初始化前必须先进行添加分区,否则会导致设备识别时无法获取分区,设备识别后不支持动态添加和删除分区。
EMMC存储器挂载为FAT文件系统前需将dev设备格式化,否则可能导致挂载失败。
SDIO扩展外设进行驱动初始化前必须先进行管脚复用配置,否则会导致设备无法识别。
被禁止的操作:
读写SD卡时不要拔卡,否则会打印一些异常信息,并且可能会导致卡中文件或文件系统被破坏。
格式化SD卡时禁止拔卡,否则可能会造成SD卡永久性损坏。
上电状态下严禁拔掉SDIO扩展外设,否则可能会造成SDIO扩展外设或者硬件单板损坏。
不同平台控制器对应的介质可能不同,关于控制器编号与所支持的介质类型,请参考各芯片平台用户指南文档中关于EMMC/SD/SDIO控制器的章节。
USB驱动开发¶
概述¶
USB(Universal Serial Bus,通用串行总线)是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。LiteOS USB驱动主要包含协议层、Composite层和控制器驱动三部分。
协议层
协议层是设备具体实现层,支持上层业务通过USB口进行数据收发。
Composite层
Composite(复合设备)层连接协议层与控制器驱动,支持具体协议的加载、卸载及设备枚举过程。Composite作为虚拟设备,其他USB设备都需要挂载在Composite设备下面。由于Composite层封装了与控制端口交互的处理流程,USB设备只需要向Composite层注册相应的处理函数,具体协议的加载和设备枚举过程由Composite层执行。
控制器驱动
控制器驱动与硬件交互,主要支持数据收发和端口控制。
USB协议驱动加载和数据收发主要流程如下:
USB驱动加载
USB设备驱动的加载通过Composite层封装统一的处理流程,由Composite调用具体协议的加载函数进行驱动程序的加载。待设备与主机通过USB线建立连接后,Composite层统一处理与主机之间的枚举过程,通过协议层注册的处理函数,进行端口申请和协议私有数据初始化。
USB数据收发
USB设备加载成功后,业务层通过协议层进行数据收发,协议层直接调用控制器驱动提供的端口控制接口进行端口数据收发处理。
开发指导¶
功能¶
Hi35xx的USB功能支持Device模式,具体协议及功能参见下表。
说明: USB3.0 Device对接USB2.0 Host时,由于设备侧硬件限制不支持降速,无法识别。
LiteOS USB驱动由于受芯片硬件资源限制,支持的规格也不尽相同,详细请参见下表所示。
说明: Hi3516DV300/Hi3556V200/Hi3559V200的USB2.0口,是基于USB3.0的控制器操作的,编译时需要选择3.0的控制器。
配置USB模块的内存¶
USB模块的正常运行需要一段uncached内存,所以需要单独为USB模块配置内存。
配置内存可通过定义以下函数实现:
void board_config(void)
{
g_sys_mem_addr_end = 0x88000000;
g_usb_mem_addr_start = g_sys_mem_addr_end;
g_usb_mem_size = 0x100000;
}
须知:
不可以修改void board_config(void)的声明,若应用层不定义void board_config(void)函数,LiteOS会按默认值分配OS可见内存大小。建议根据实际情况自行定义。
g_sys_mem_addr_end配置的是OS可见内存的结束位置。
g_usb_mem_addr_start配置的是USB内存的起始位置。
g_usb_mem_size配置的是USB内存的大小。
若系统完全不支持USB相关功能,则可以将g_usb_mem_addr_start和g_usb_mem_size配置为0。
动态切换USB协议¶
动态切换时,先调用usb_deinit卸载当前使用的协议,再通过usb_init重新加载待切换的协议。
部分协议使用中不支持卸载(如:DFU、UVC、Camera、HID),需要先停用当前业务后再调用usb_deinit卸载。
模块编译¶
USB驱动的源码路径为drivers/usb。用户在LiteOS根目录下执行make即可编译出对应的USB模块库。编译成功后,会在“out/<platform>/lib”目录下生成名为“libusb_base.a”、“libusb_device.a”的库文件,目录中的platform为具体的平台名。
图1是LiteOS中USB驱动menuconfig配置项截图,在“Driver”配置项下,可以通过选中“Enable USB”开启 USB 驱动功能。
配置项如为图1所示的1~4,对应描述如下:
1表示是否打开USB驱动Device功能。
2表示Device控制器种类,分别有Dwc2.0和3.0(可视具体板子的 USB 口类别而定)。
3表示是否开启Device模式下的USB协议功能。
4表示Device模式下的具体USB协议功能,支持的USB协议如下,menuconfig配置项如图2所示:
Device U盘
UVC
UAC
Camera(UVC&UAC)
DFU升级
虚拟串口
虚拟网口ECM
虚拟网口RNDIS
复合设备(虚拟串口&虚拟网口)
HID
UAC&HID复合设备
须知: 初始化Device模式也是调用usb_init(DEVICE, xxx) ,根据业务诉求开启需要的协议,也可以全部开启。另外,依赖的外围组件也需要提前加载好,具体参见“Device端”的编程实例。
编程实例¶
Device端¶
Device虚拟U盘¶
在初始化函数中初始化SD卡驱动和USB驱动:
SD_MMC_Host_init();
usbd_set_device_info(DEV_MASS, &str_manufacturer, &str_product, &str_serial_number, dev_id);
usb_init(DEVICE, DEV_MASS); // dtype设置为DEV_MASS
说明:
在多卡情况下(系统同时挂载2张卡及以上),USB2.0 Device默认绑定第一张卡设备节点,例如同时存在“/dev/mmcblk0p0”,“/dev/mmcblk1p0”时,USB2.0 Device会绑定“/dev/mmcblk0p0”,因此Host端通过USB口只能访问到一张SD/MMC卡。
USB2.0 Device连接Host端后,Host端可以通过USB端口访问到系统存储介质的文件。在Host端访问存储介质过程中,系统端请勿对MMC/SD Card存储介质进行读写、格式化等操作,避免Host端访问存储介质异常。
Device U盘对接PC进行格式化时,仅支持Windows PC进行快速格式化操作,请勿使用Windows PC的一般格式化操作或Linux的高级格式化命令,避免对分区信息进行修改,导致Device卸载虚拟U盘后mount失败。如果进行了此操作,mount失败后可重新format存储介质来解决。
目前虚拟U盘支持的存储介质有4种,分别是:SD卡、MMC、RAM、FTL spi_nor。初始化流程一致,在加载USB之前,都需要先将对应介质的文件系统初始化,确保“dev/”目录下有可供虚拟U盘读写的节点。
应用层通过fmass_register_notify(void(*notify)(void* context, int status), void* context)函数注册notify回调,驱动会在识别到接入和拔出Host端时调用此回调函数,从而实现DeviceU盘拔插相关的处理。
请在USB模块初始化后,立马调用fmass_register_notify()实现Device回调函数的注册。
须知: Notify回调函数中,在USB Device链接的情况下,需通过fmass_partition_startup()将需要被PC端识别的SD卡分区节点启动,并且保证其接口传进来的设备节点是存在的,如果不存在,就默认将“/dev/mmcblk0p0”虚拟化为U盘,若“/dev/mmcblk0p0”节点不存在,则会失败。
开启LOSCFG_DRIVERS_USB_MASS_STORAGE_EXT_SER_FUNC,即可在虚拟U盘的基础上额外增加串口的功能。当前支持配置为单串口和双串口。
在正常操作过程中需要遵守的事项:
Device U盘将SD卡交由USB Host处理,因此在Device U盘被Host端识别后,会通过回调函数通知APP软件模块,APP必须将所有录像抓拍等操作SD卡的业务停下,并umount SD卡分区,待USB Host离线后再mount和恢复相关业务。
USB Host端进行读写访问时,不要拔出单板的SD/MMC卡,否则会打印一些异常信息,并且可能会导致卡住文件或文件系统被破坏。
在操作过程中出现异常时的操作:
若拔出Device U盘后,再极其快速地插入Device U盘,可能会导致Host端检测不到U盘,因为单板Device U盘的检测注册/注销过程需要一定的时间。
extern int fmass_register_notify(void(*notify)(void* context, int status), void* context);
extern int fmass_partition_startup(char* path);
void fmass_app_notify(void* conext, int status)
{
char *path = "/dev/mmcblk0p0";
if(status == 1)/*usb device connect*/
{
/* 停止录像抓拍等对/dev/mmcblk0p0分区的读写操作,并umount该分区 */
fmass_partition_startup(path); // startup fmass access partition
}
else
{
/* mount该分区,并通知其它模块该分区可以进行读写操作 */
}
}
void app_init()
{
uint32_t ret;
int handle;
const char manufacturer[38] = {
'H', 0, 'i', 0, 'S', 0, 'i', 0, 'l', 0, 'i', 0, 'c', 0, 'o', 0, 'n', 0,
'(', 0, 'S', 0, 'h', 0, 'a', 0, 'n', 0, 'g', 0, 'h', 0, 'a', 0, 'i', 0, ')', 0
};
struct device_string str_manufacturer = {
.str = manufacturer,
.len = 38
};
const char product[18] = {
'U', 0, 'S', 0, 'B', 0, 'D', 0, 'E', 0, 'V', 0, 'I', 0,
'C', 0, 'E', 0
};
struct device_string str_product = {
.str = product,
.len = 18
};
const char serial[16] = {
'2', 0, '0', 0, '2', 0, '0', 0, '0', 0, '6', 0, '2', 0,
'4', 0
};
struct device_string str_serial_number = {
.str = serial,
.len = 16
};
struct device_id dev_id = {
.vendor_id = 0x3361,
.product_id = 0x0108,
.release_num = 0x0318
};
SD_MMC_Host_init();
g_usb_mem_addr_start= g_sys_mem_addr_end;
g_usb_mem_size= 0x40000; //recommend 256K nonCache for usb
ret = usbd_set_device_info(DEV_MASS, &str_manufacturer, &str_product, &str_serial_number, dev_id);
if (ret != 0) {
dprintf("set fail!\n");
return;
}
ret = usb_init(DEVICE, DEV_MASS); // usb_init must after SD_MMC_Host_init
if(ret != 0) {
dprintf("usb init failed!\n");
return;
}
handle = fmass_register_notify(fmass_app_notify,NULL);
if (handle == -1) {
return;
}
}
制作FAT格式的文件系统镜像(以Ubuntu系统为例)。
制作FAT格式磁盘镜像。
mkfs.fat -s 64 -S 512 -f 2 -n "Internal SD" -F 32 -C usbdisk.tmp 2048其中2048表示磁盘空间大小,单位是KB。
挂载FAT格式磁盘镜像。
mount -o loop -t vfat usbdisk.tmp test/其中test为挂载目录。
在test目录下存放所需文件,然后将文件同步到磁盘。
sync test/卸载磁盘镜像。
umount test/制作文件系统镜像。
dd if=usbdisk.tmp of=udisk.img bs=1024 count=2048将“usbdisk.tmp”前面2MB的内容制作为文件系统镜像“udisk.img“”。
将制作好的文件系统镜像下载到flash上。
将“udisk.img”文件放到指定路径FAT_PART_IMAGE_PATH(“/littlefs/udisk.img”)。
调用ram_mass_storage_init接口注册RAM_DEV_NAME(/dev/uram)设备节点。
调用fmass_register_notify接口指定设备节点“/dev/uram”虚拟为U盘。
调用usbd_set_device_info和usb_init接口加载虚拟U盘协议。
须知:
本次制作的文件系统镜像申请的是2M容量,请根据实际业务需求调整容量大小,除了需要制定特定容量的文件系统镜像之外,还要修改对应的FAT_PART_SIZE大小。
Device UVC¶
Device UVC支持UVC1.0协议和UVC1.1协议版本的切换,打开菜单,进入Driver ---> Enable USB ---> Enable USB DWC Controller ---> Enable USB Gadget Support ---> USB Gadget Drivers ---> UVC Version可选择UVC1.0或UVC1.1版本。当前默认为UVC1.1协议,切换为UVC1.0可支持Windows xp系统,增加兼容性。
UVC上层应用在USB初始前设置UVC传输能力集,开启VI,打开UVC设备。
就绪后,等待底层UVC连接,启动VPSS和VENC编码并传输码流数据,直至UVC通道断开。
切换新码流格式时,应用须先关闭前一次编码并清空缓存,再启动编码。
方式1:直接调用device uvc相关接口控制数据传输。
通过fuvc_frame_descriptors_get接口设置UVC传输能力集(含:编码格式、分辨率和帧率)。
在初始化函数中调用usbd_set_device_info和usb_init接口实现USB驱动注册。
usbd_set_device_info(DEV_UVC, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_UVC); // dtype设置为DEV_UVC
开启VI后,调用uvc_open_device打开UVC设备,需指定分辨率和帧率。
通过uvc_wait_host等待板端与USB主机侧连接成功。
启动VPSS和VENC编码后,通过uvc_recv_pack开始执行视频数据传输。
通过uvc_video_stop停止视频数据传输,可调用uvc_close_device关闭USB设备。
方式2:打开device uvc注册的设备节点,并通过系统调用控制数据传输。
在初始化函数中调用usbd_set_device_info和usb_init接口实现USB驱动注册。
usbd_set_device_info(DEV_UVC, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_UVC); // dtype设置为DEV_UVC
开启VI后,调用open打开UVC设备节点“/dev/usb_uvc-0”,并通过调用select函数监视设备连接情况。
当板端与USB主机侧连接成功并可以开始传输数据时,select系统调用能够接收到来自host端的消息,此时调用ioctl接口发送VIDIOC_DQEVENT命令,并提供结构体struct usbd_uvc_video_event来接收数据。根据接收的数据将视频的分辨率、帧率等信息发送给VPSS和VENC。
启动VPSS和VENC编码后,通过uvc_recv_pack开始执行视频数据传输。同时通过系统调用ioctl发送VIDIOC_STREAMON命令通知USB设备可以开始进行数据传输。
通过调用VIDIOC_STREAMOFF可以停止视频传输,再通过系统调用close关闭USB设备。
须知: 方式1与方式2都有打开设备的操作,分别是直接调用接口uvc_open_device和调用系统接口open。当同时使用这两个接口时,只能打开一次设备,后调用的接口将无法打开设备且其各种操作都将失效。
Device UAC¶
UAC的数据处理流程,麦克风业务具体步骤如下:
在初始化函数中调用usbd_set_device_info和usb_init接口实现USB驱动注册。
usbd_set_device_info(DEV_UAC, str_manufacturer, str_product, str_serial_number, dev_id); usb_init(DEVICE, DEV_UAC); // dtype设置为DEV_UAC
通过fuac_get_opts接口获取当前Device UAC与Host协商的采样率、声道数和位深信息,计算每次要发送的音频长度和需要缓存的音频个数。
通过3计算出的数值,调用fuac_reqbuf_init接口初始化UAC内存池。
结束音频传输时,把5中已获取的空闲buf,通过调用fuac_send_message接口,全部归还给USB,然后调用fuac_reqbuf_deinit接口释放UAC内存池。
调用usb_deinit接口卸载协议,注意卸载协议前必须保证UAC内存池的所有buf全部归还给USB层,否则会卸载失败。
扬声器业务具体步骤如下:
Device UAV(Camera)¶
UAV是同时支持UAC、UVC,相关操作流程,参见上面UVC、UAC使用示例。区别在于初始化时,传入的协议类型:
usbd_set_device_info(DEV_CAMERA, &str_manufacturer, &str_product, &str_serial_number, dev_id);
usb_init(DEVICE, DEV_CAMERA); // dtype设置为 DEV_CAMERA
Device虚拟串口¶
虚拟串口配置说明如下:
方式一:初始化虚拟串口后,通过VFS 接口打开串口节点,并使用read、write、ioctl等接口控制串口。
当系统支持文件系统时,以下就是依赖文件系统的虚拟串口的初始化和使用流程:
在初始化函数中调用以下接口实现USB驱动注册:
usbd_set_device_info(DEV_SERIAL, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_SERIAL);
若需要基于USB虚拟的串口进行shell通信交互与调试信息打印,需要再进行console、Shell等与串口有依赖关系的模块初始化。
virtual_serial_init("/dev/ttyGS0"); system_console_init("/dev/serial");
若单板端写数据发送到USB主机,则直接调用write接口;若单板端读取从USB主机发来的数据,open串口后先调用ioctl接口,然后再调用read接口,ioctl用法如下:
ioctl(fd, CONSOLE_CMD_RD_BLOCK_SERIAL, 1);
须知:虚拟串口功能不能与Uart串口同时使用。
如果有对节点的多次open操作,需要在卸载前执行相同次数close操作,才能保证协议卸载时成功,否则会出现不可预知的错误。
虚拟串口最大支持读写4096Byte数据。
单串口模式下初始化后,在dev目录下会生成/dev/ttyGS0节点,在双串口模式下初始化后,在dev目录下会生成/dev/ttyGS0节点和/dev/ttyGS1节点。
方式二:直接调用非VFS模块的对外接口控制虚拟串口
当系统不支持文件系统时,虚拟串口和shell暂不兼容,初始化虚拟串口之后,需要调用usb_serial_ioctl接口,使虚拟串口可以正常读。用法如下:
usbd_set_device_info(DEV_SERIAL, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_SERIAL); usb_serial_ioctl(0,CONSOLE_CMD_RD_BLOCK_SERIAL, 1); // index参数默认使用0,当开启双串口时,可以选0或1。
然后根据业务需要调用usb_serial_read和usb_serial_write对虚拟串口进行读写操作。
须知:
非VFS模式的虚拟串口,使用对外接口时,入参index默认使用0,当仅支持一个虚拟串口时,只能用0,当开启双串口时,可以选0或1。
如果Host端为Windows PC,那么首先在PC端放置驱动文件“linux-cdc-acm.inf”,该文件在发布包中的路径:01.software/pc/usb_tools。
通过USB数据线将单板与Host端相连,PC端会自动加载驱动,第一次一般会失败,需要自行安装驱动,方法为:
右击计算机,进入管理界面。
打开设备管理器。
点开其他设备,会看到Gadget Serial(COMx),双击。
打开驱动程序界面,点击更新驱动程序,进入浏览计算机以查找驱动程序软件(R)。
把路径指向驱动文件“linux-cdc-acm.inf”所在的目录,点击下一步,计算机会自动进行安装驱动程序,安装成功后,关闭界面。
须知:“01. software/pc/usb_tools”目录下有“linux-cdc-acm.inf”文件,通常Win7无法自动加载虚拟串口驱动,需要手动加载;Win10通常会自动加载虚拟串口驱动,无需手动加载。
如果安装串口驱动失败,则需要修改“linux-cdc-acm.inf“文件,在[DeviceList]和[DeviceList.NTamd64]里添加“USB\VID_3361&PID_0102&REV_0318”,其中3361对应的是VID,0102对应的是PID,0318对应的是设备版本号,这3组数字是通过调用usbd_set_device_info接口来设置,请根据实际设备进行修改,需要和设置值保持一致。
单板端重新上电启动,在PC端就可以作为真正的串口来使用。
Device虚拟网口RNDIS¶
RNDIS目前可以不依赖lwip,支持对接不同的网络协议栈。RNDIS初始化方式分为两种。
第一种就是在开启了LWIP配置项后,步骤如下:
在初始化函数中调用以下接口实现TCP/IP协议栈初始化和USB驱动注册。
tcpip_init(NULL, NULL); // TCP/IP协议栈初始化 usbd_set_device_info(DEV_ETHERNET, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_ETHERNET);
USB虚拟网口默认未配置IP地址,请根据网络规划自行配置IP地址。
如果host端为Windows 7或者Windows10,那么首先在PC端放置linux.inf驱动文件,见发布包路径:01.software/pc/usb_tools。然后修改“linux.inf”驱动文件,在[RndisDevices.NTx86]、[RndisDevices.NTamd64]和[RndisDevices.NTia64]添加“USB\VID_3361&PID_0102&REV_0318”,其中3361对应的是VID,0102对应的是PID,0318对应的是设备版本号,这3组数字是通过调用usbd_set_device_info接口来设置,请根据实际设备进行修改,要和设置值保持一致。
通过USB数据线将单板与Host端相连。
如果Host端为windows7系统,通常自动加载驱动,一般会失败,需要自行安装驱动,具体操作为:
右击计算机,进入管理界面。
打开设备管理器。
点开其他设备,会看到“Linux USB Ethernet/RNDIS Gadget #3“”,双击。
打开驱动程序界面,点击更新驱动程序,进入浏览计算机以查找驱动程序软件(R)。
把路径指向“linux.inf”所在的目录,点击下一步,计算机会自动进行安装驱动程序,安装成功后,关闭界面。
如果Host端为Windows10系统,通常会误识别为虚拟串口,需要手动更新驱动,具体操作为:
右击计算机,进入管理界面。
打开设备管理器。
点开端口设备,会看到USB串行设备(COMX)。
打开驱动程序界面,点击更新驱动程序,进入浏览计算机以查找驱动程序软件(R)。
把路径指向“linux.inf”所在的目录,点击下一步,计算机会自动进行安装驱动程序,安装成功后,会生成虚拟网口设备,对应虚拟串口设备消失,关闭界面。如果安装过程中提示第三方INF不包含数字签名信息,此关闭方法可在网上搜索,此处不再赘述。
在单板端,配置IP并添加路由。
当单板和PC通过USB数据线相连时,会在PC端生成USB网络节点,具体位置:打开网络和共享中心--->更改适配器设置--->Linux USB Ethernet /RNDIS Gadget #x。
建立网桥:
右键“Linux USB E thernet /RNDIS Gadget #x”节点,点击添加到桥选项;
PC端连接大网的本地连接节点右键,选择添加到桥选项;
等待网桥建立完成,如图1所示。
此时在单板端,能正常访问大网,可作为真正的网口操作使用。
须知:
在Win10 PC桥接网络,若网桥无法获取到IP地址时,请确定一下桥接顺序,桥接时请先选择物理网卡,再选择虚拟网卡,然后桥接。若配置网桥失败,请单击确定,然后再选择物理网卡右键添加到桥(这个步骤很重要),因为造成配置网桥失败的原因就是物理网卡没有添加到桥。
当访问桥接的网络环境时需要权限,若出现能ping通IP地址但ping不通网关和服务器的问题,请排查单板端的MAC地址(IP和MAC地址有关联性)。
第二种初始化RNDIS的方式,就是在关闭LWIP的情况下,对接其他网络协议栈,步骤如下:
在初始化函数中进行USB驱动注册之前,需要调用RNDIS初始化其他网络协议栈钩子的函数。
struct usb_eth_operations { void (*eth_register)(void);// 加载时调用,注册网卡 void (*eth_unregister)(void);// 卸载时调用,注销网卡 void (*eth_link_change)(bool connected);// 连接状态改变时调用,通知网络协议栈断连状态 void (*eth_rx)(const uint8_t *buf, uint32_t buflen);// 收到数据时调用,将数据传入网络协议栈 };// 此结构体定义在f_eth.h头文件中,需要包含此头文件
创建struct usb_eth_operations类型ops,并使用其他网络协议栈api实现上述四个功能类型的函数,并将对应函数赋值给ops的各个函数指针成员。然后调用feth_init_ops函数初始化RNDIS内部的对应函数。
// 默认其他网络协议栈已经实现相应函数指针,参考self_src/drivers/usb/gadget/usbd_eth_common.c文件中lwip实现对应函数指针的方式 struct usb_eth_operations ops; ops.eth_register = other_net_reg; ops.eth_unregister = other_net_unreg; ops.eth_link_change = other_net_link; ops.eth_rx = other_net_rx; if (feth_init_ops(&ops) != 0) { dprintf("RNDIS init ops faild.\n"); return; } dprintf("RNDIS init ops success.\n");// 执行到这里说明已经初始化RNDIS与网络协议栈对接的钩子
调用以下接口进行USB驱动注册。
usbd_set_device_info(DEV_ETHERNET, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_ETHERNET);
至此已完成RNDIS的加载,其他网络协议栈需要发送数据到host,则调用feth_send_data接口即可。
feth_send_data(buf, ETH_SEND_BUF);// 在需要处直接调用发送接口发送即可与host对接后,如何识别问题,参照第一种RNDIS初始化方式中的方法即可。
须知: 当没开启lwip时,要求用feth_init_ops函数注册钩子函数,来对接一个网络协议栈,若没有注册钩子函数,RNDIS在加载后,只是一个无任何网络通信功能的虚拟网口,无法解析接收到的网络数据,也无法发送符合网络协议的网络数据。同时,RNDIS虽然也可以直接调用发送接口发送数据,但由于无法自己构造符合网络协议的网络数据,在host端将无任何反应。
Device虚拟网口ECM¶
虚拟网口ECM和虚拟网口RNDIS的加载过程是一样的,在与host对接之前的初始化加载流程可以参考RNDIS的加载过程,其中需要调整的代码如下,只是入参有区别,其他与RNDIS一致。
usbd_set_device_info(DEV_ECM, &str_manufacturer, &str_product, &str_serial_number, dev_id);
usb_init(DEVICE, DEV_ECM);
须知:
ECM对接的host是linux,要求linux开启支持ECM虚拟网口的配置。
ECM对接linux之后,需要先在linux系统中将对应网卡设置IP地址,然后再设置device端的ECM网卡IP地址,否则device端设置IP会失败。
ECM对接网络协议层的方式也与RNDIS一致,可以参考RNDIS。
Device复合设备(串口&网口)¶
复合设备支持单串口与网口复合以及双串口与网口复合,在配置项里,打开串口与网口复合的配置项后,默认是单串口与网口复合,若要双串口与网口复合,需要在串口模式中选择双串口即可。
以上两种复合设备的初始化过程一样,具体过程如下:
tcpip_init(NULL, NULL); // TCP/IP协议栈初始化
usbd_set_device_info(DEV_SER_ETH, &str_manufacturer, &str_product, &str_serial_number, dev_id);
usb_init(DEVICE, DEV_SER_ETH);
virtual_serial_init("/dev/ttyGS0");
system_console_init("/dev/serial");
复合设备中串口和网口的使用同上面“Device虚拟串口”和”Device虚拟网口RNDIS”使用示例。
须知:
虚拟串口功能不能与UART串口同时使用。
由于网口分为RNDIS网口和ECM网口,复合设备初始化的区别在于传入的设备枚举值不同,RNDIS与串口复合时,传入DEV_SER_ETH;ECM与串口复合时,传入DEV_SER_ECM。最终生成的复合设备,用法上与单个的串口或者网口用法一致,可以参考虚拟串口和虚拟网口的介绍
Device DFU¶
目前DFU支持两种升级方式,默认使用方式一。
方式一:通过分配足够大的空间来保存升级文件。
方式二:无需分配足够大的空间,边下载边处理数据。
方式一的升级步骤如下:
在初始化函数中调用以下接口实现USB驱动注册。
usbd_set_device_info(DEV_DFU, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_DFU);
升级开始前,调用usb_dfu_init_env_entities接口设置升级接口类型及环境变量。
调用usb_dfu_update_status,等待升级事件。
等到有升级事件后,调用usb_dfu_update_size_get获取升级文件大小,并对升级文件进行必要的校验,检查升级文件是否携带后缀字段(校验和、标志等关键字),如携带后缀比对校验和。
根据上述升级文件大小,为DFU缓存升级文件提供足够的内存空间,通过usb_dfu_read获取实际升级文件。
获取到实际升级文件后,即可对固件执行升级动作,写入指定Flash地址段。
最后需要调用usb_dfu_free_entities接口释放相关资源。
说明:
给升级的文件提供足够的内存空间主要与厂商提供的升级镜像大小有关,请预留足够的升级的空间。
方式二的升级步骤如下:
在初始化函数中调用以下接口来实现USB驱动注册。
usbd_set_device_info(DEV_DFU, &str_manufacturer, &str_product, &str_serial_number, dev_id); usb_init(DEVICE, DEV_DFU);
业务层需要自行实现弱函数usb_dfu_download_callback,该函数会在“控制传输完成回调函数”里被调用,将host端传来的数据作为入参传入该弱函数,在业务层直接处理每次传输进来的升级数据。
void usb_dfu_download_callback(constuint8_t *buf, uint32_t len); // 弱函数usb_dfu_download_callback的声明最后调用usb_dfu_wait_transmit_finish函数,等待传输完成。
说明:
弱函数usb_dfu_download_callback在中断里被调用,故自行实现的处理数据的代码中,不可使用引起调度的函数,如mutex锁的使用;
由于是边下载边升级,升级过程中需要确保USB中断不受影响,建议升级过程中关闭其他可能影响USB中断的模块或功能。
Device HID¶
HID上层应用在USB初始化之前,先调用usbd_set_device_info接口来设置厂商ID、产品ID、设备序列号以及字符串信息,然后调用hid_report_descriptor_info或者hid_add_report_descriptor接口来配置报告描述符信息,让USB主机识别为具体的HID设备,如键盘、鼠标、带有鼠标功能的键盘等
方式1:打开Device HID注册的设备节点,通过调用VFS模块的接口进行HID数据流传输。
调用usbd_set_device_info和hid_report_descriptor_info接口来填充各个描述符信息。
在初始化函数中调用usb_init接口实现USB驱动注册。
usb_init(DEVICE, DEV_HID); // dtype设置为 DEV_HID在上层应用中,调用open函数来打开指定的HID设备。
fd = open("/dev/hid", O_RDWR, 0);在上层应用中,按报告描述符规定的INPUT数据格式写到write函数中的入参buffer,长度也是按报告描述符规定的INPUT长度。若报告描述符定义了OUTPUT项目,则调用read函数去读取USB主机发给设备的数据,读取的大小是按报告描述符规定的OUTPUT长度,若要提高任务执行效率,可以调用poll机制,两者区别是read是永久阻塞模式,而poll机制是定时阻塞模式。
数据传输结束之后,若不使用设备,便调用close函数关闭设备。
方式2:直接调用非VFS模块的对外接口进行HID数据流传输。
调用usbd_set_device_info和hid_add_report_descriptor接口来填充各个描述符信息。
在初始化函数中调用usb_init接口实现USB驱动注册。
usb_init(DEVICE, DEV_HID); // dtype设置为 DEV_HID在上层应用中,按报告描述符规定的INPUT数据格式和长度信息,调用fhid_send_data接口发送HID数据。若报告描述符定义了OUTPUT项目,则调用fhid_recv_data接口去读取USB主机发给设备的数据,读取的HID数据长度与报告描述符规定的OUTPUT长度一致。
须知:
hid_report_descriptor_info接口只能设置一个Report,并和VFS模块的接口配套使用;hid_add_report_descriptor接口可以最多设置两个Report(循环调用接口进行设置,设置前需要配置LOSCFG_DRIVERS_USB_HID_REPORT_MAP_NUM),并和非VFS模块的接口配套使用。这两种方式不允许混用。如果需要重新设置Report,则需要加载并卸载Device HID协议才能设置。
如果报告描述符中没有Report ID项目(鼠标或者键盘报告表),默认的ID值是0,在数据传输过程中,第一个字节不需要填充0,若定义一个以上的Report ID(带有鼠标功能的键盘报告表),此时在传输报表中的第一个字节填充相应的Report ID,输入报表、输出报表与特征报表可以分享同一个Report ID。
如果设置的报告描述符含有Output Report特性,则需要打开LOSCFG_DRIVERS_USB_HID_OUTPUT_REPORT配置才能进行Output传输,当USB主机发给设备数据的时候,建议及时把数据读走,否则USB主机第二次发来的数据会覆盖第一次的数据。
若调用了open函数,此时无法卸载协议,只有调用close函数,才能卸载协议。当前Device HID只允许open一次,再次open就会失败。
上层应用中调用VFS模块的read和write接口或者非VFS模块的fhid_send_data和fhid_recv_data接口时,请确保传进来的buffer和len大小要一致,否则会造成发送或者读取的数据有误。在进行数据流传输前需要配置LOSCFG_DRIVERS_USB_HID_INPUT_REPORT_LEN,即发送hid数据的最大长度,该值必须要做对齐操作,否则会出现数据传输错误,当前对齐策略有两种,
如果传输的内存是non-cache属性,该值必须满足四字节对齐;
如果传输的内存是cache属性,该值必须满足USB_CACHE_ALIGN_SIZE对齐。
USB2.0控制器支持通过SOF中断发送hid数据(需打开配置项LOSCFG_DRIVERS_USB_HID_POLLING_REPORTS),在高速模式下其中断是每隔125μs上报,其回调弱函数void usb_sof_intr_callback(void)需要在业务层实现具体操作,该接口操作时间不能超过125μs,否则会影响传输速率和其他中断操作。
方式1和方式2不能同时使用,若使用方式1,需要打开VFS模块功能。HID业务运行过程中,不允许卸载HID协议,必须要停止应用层的业务操作后,才能卸载协议。
方式1和方式2的接收HID数据接口只能在任务中调用,不能在中断回调调用,发送HID数据接口可以在中断回调调用。
Device UAC&HID&串口复合设备¶
UAC&HID&串口复合设备是最多同时支持麦克风、扬声器、键盘和鼠标、串口功能,扬声器功能可以在配置项里打开或者关闭,而键盘和鼠标功能则是通过调用hid_report_descriptor_info或者hid_add_report_descriptor接口来配置报告描述符信息。相关操作流程,参见上面UAC、HID使用示例。区别在于初始化时,传入的协议类型:
usbd_set_device_info(DEV_UAC_HID, &str_manufacturer, &str_product, &str_serial_number, dev_id);
usb_init(DEVICE, DEV_UAC_HID); // dtype设置为 DEV_UAC_HID
串口功能可裁剪,默认关闭。通过打开LOSCFG_DRIVERS_USB_UAC_HID_EXT_SER_FUNC可开启额外的单串口功能。
API参考¶
该功能模块提供以下接口:
usbd_set_device_info¶
【描述】
设置device设备的VID、PID、设备版本号和厂商、产品、序列号字符串信息。
【语法】
uint32_t usbd_set_device_info(device_type dtype,
const struct device_string *str_manufacturer,
const struct device_string *str_product,
const struct device_string *str_serial_number,
struct device_id dev_id);
【参数】
【返回值】
【需求】
头文件:usb_init.h
库文件:libusb_base.a
【注意】
厂商ID号不可随意设置,需向USB协会申请,产品ID号和设备版本号可根据实际产品进行设置,如果同一个单板切换为不同的USB设备需要更换PID号,否则USB主机识别到的设备驱动会有问题。
字符串长度要和字符串大小一致,且长度范围2~252,传入的入参长度必须是偶数,否则USB主机无法识别字符串信息。
【举例】
须知: 以下示例仅供参考,不可直接使用,特别是VID号,请务必修改为客户自己的厂商信息。
const char manufacturer[38] = {
'H', 0, 'i', 0, 'S', 0, 'i', 0, 'l', 0, 'i', 0, 'c', 0, 'o', 0, 'n', 0,
'(', 0, 'S', 0, 'h', 0, 'a', 0, 'n', 0, 'g', 0, 'h', 0, 'a', 0, 'i', 0, ')', 0
};
struct device_string str_manufacturer = {
.str = manufacturer,
.len = 38
};
const char product[18] = {
'U', 0, 'S', 0, 'B', 0, 'D', 0, 'E', 0, 'V', 0, 'I', 0,
'C', 0, 'E', 0
};
struct device_string str_product = {
.str = product,
.len = 18
};
const char serial[16] = {
'2', 0, '0', 0, '2', 0, '0', 0, '0', 0, '6', 0, '2', 0,
'4', 0
};
struct device_string str_serial_number = {
.str = serial,
.len = 16
};
struct device_id dev_id = {
.vendor_id = 0x3361,
.product_id = 0x0102,
.release_num = 0x0318
};
uint32_t ret;
ret = usbd_set_device_info(DEV_UVC, &str_manufacturer, &str_product, &str_serial_number, dev_id);
if (ret != 0) {
dprintf("set fail!\n");
}
【相关主题】
无。
usb_init¶
【描述】
初始化USB模块。
【语法】
uint32_t usb_init(controller_type ctype, device_type dtype);
【参数】
|
||
【返回值】
【需求】
头文件:usb_init.h
库文件:libusb_base.a
【注意】
USB各协议使用时与其他模块的依赖关系,参考“编程实例”。
不支持重复执行,切换协议需要调用usb_deinit卸载当前协议。
【举例】
参考“编程实例”。
【相关主题】
usb_deinit¶
【描述】
卸载USB模块。
【语法】
uint32_t usb_deinit(void);
【参数】
无
【返回值】
【需求】
头文件:usb_init.h
库文件:libusb_base.a
【注意】
无
【举例】
参考“编程实例”。
【相关主题】
usb_is_devicemode¶
【描述】
判断当前USB Device是否已连接USB Host。
【语法】
bool usb_is_devicemode(void);
【参数】
无。
【返回值】
【需求】
头文件:usb_init.h
库文件:libusb_base.a
【注意】
此接口要在加载USB Device协议后再去调用。
【举例】
无。
【相关主题】
无。
usb_device_is_host_suspended¶
【描述】
检测USB Host是否进入休眠状态。
【语法】
bool usb_device_is_host_suspended(void);
【参数】
无。
【返回值】
【需求】
头文件:usb_init.h
库文件:libusb_base.a
【注意】
此接口要在加载USB2.0 控制器后再去调用。
【举例】
无
【相关主题】
usb_device_remote_wakeup¶
【描述】
远程唤醒已进入休眠状态的USB Host。
【语法】
int usb_device_remote_wakeup(void);
【参数】
无。
【返回值】
【需求】
头文件:usb_init.h
库文件:libusb_base.a
【注意】
此接口要在加载USB2.0 控制器后再去调用。
【举例】
无
【相关主题】
fmass_register_notify¶
【描述】
注册Device notify回调函数。
【语法】
int fmass_register_notify(void (*notify)(void *context, int status), void *context);
【参数】
【返回值】
【需求】
头文件:f_mass_storage.h
库文件:libusb_base.a
【注意】
允许注册多个notify回调函数,USB2.0 Device拔插事件发生时驱动会调用每一个notify函数。
【举例】
参考编程实例。
【相关主题】
fmass_unregister_notify¶
【描述】
注销Device notify回调函数。
【语法】
int fmass_unregister_notify(int handle);
【参数】
【返回值】
【需求】
头文件:f_mass_storage.h
库文件:libusb_base.a
【注意】
无
【举例】
参考“编程实例”。
【相关主题】
fmass_partition_startup¶
【描述】
启动被PC端识别的SD卡、MMC介质或RAM介质分区节点。
【语法】
int fmass_partition_startup(const char *path);
【参数】
【返回值】
【需求】
头文件:f_mass_storage.h
库文件:libusb_base.a
【注意】
无
【举例】
参考编程实例。
【相关主题】
ram_mass_storage_init¶
【描述】
使能FAT32文件系统镜像内存映射,并注册/dev/uram设备节点。
【语法】
int ram_mass_storage_init(void);
【参数】
无
【返回值】
【需求】
头文件:f_mass_storage_ram.h
库文件:libusb_base.a
【注意】
无。
【举例】
参考“编程实例”。
【相关主题】
ram_mass_storage_deinit¶
【描述】
禁用FAT32文件系统镜像的内存映射。
【语法】
int ram_mass_storage_deinit(void);
【参数】
无。
【返回值】
【需求】
头文件:f_mass_storage_ram.h
库文件:libusb_base.a
【注意】
无
【举例】
参考“编程实例”。
【相关主题】
fuvc_frame_descriptors_get¶
【描述】
设置UVC设备视频传输能力集。
【语法】
void fuvc_frame_descriptors_get(const struct fuvc_format_info *format_info, uint32_t frames_count);
【参数】
【返回值】
无。
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
参考“编程实例”。
【举例】
参考“编程实例”。
【相关主题】
无
uvc_open_device¶
【描述】
打开UVC设备。
【语法】
int uvc_open_device(uvc_t *hdl, const struct uvc_open_param *param);
【参数】
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
参考“编程实例”。
【举例】
参考“编程实例”。
【相关主题】
uvc_close_device¶
【描述】
关闭UVC设备。
【语法】
int uvc_close_device(uvc_t hdl);
【参数】
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
参考“编程实例”。
【举例】
参考“编程实例”。
【相关主题】
uvc_wait_host¶
【描述】
等待USB HOST连接。
【语法】
int uvc_wait_host(uvc_t hdl, int wait_option, int *connected);
【参数】
|
||
|
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
参考“编程实例”。
【举例】
参考“编程实例”。
【相关主题】
uvc_get_state¶
【描述】
获取UVC设备的运行状态。
【语法】
int uvc_get_state(uvc_t hdl, uint32_t *state);
【参数】
|
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
无。
【举例】
无。
【相关主题】
无。
uvc_recv_pack¶
【描述】
UVC零拷贝模式发送数据包。
【语法】
int uvc_recv_pack(const struct uvc_pack *pack);
【参数】
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
参考“编程实例”。
【举例】
参考“编程实例”。
【相关主题】
uvc_video_stop¶
【描述】
停止UVC视频传输。
【语法】
int uvc_video_stop(uvc_t hdl);
【参数】
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
参考“编程实例”。
【举例】
参考“编程实例”。
【相关主题】
uvc_format_info_get¶
【描述】
获取新码流格式详细信息。
【语法】
int uvc_format_info_get(struct uvc_format_info *info);
【参数】
struct uvc_format_info{ uint32_t width; // 分辨率,宽 uint32_t height; // 分辨率,高 uint32_t format; // 码流格式 uint32_t status; // 1-格式切换中;0-切换完成 } __attribute__((packed)); |
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
无。
【举例】
无。
【相关主题】
uvc_format_status_check¶
【描述】
码流格式切换事件。
【语法】
enum format_switch_status uvc_format_status_check(void);
【参数】
无。
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
无。
【举例】
无。
【相关主题】
hi_camera_register_cmd¶
【描述】
应用注册UVC扩展命令,提供底层驱动调用。
【语法】
int hi_camera_register_cmd(const struct uvc_camera_cmd *cmd);
【参数】
【返回值】
【需求】
头文件:f_uvc.h
库文件:libusb_base.a
【注意】
无。
【举例】
无。
【相关主题】
无。
uac_wait_host¶
【描述】
等待USB HOST连接。
【语法】
int uac_wait_host(int wait_option);
【参数】
|
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
fuac_reqbuf_init¶
【描述】
初始化UAC(音频)内存池。
【语法】
int fuac_reqbuf_init(uint32_t buf_count, uint32_t data_len);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
不能重复初始化,已初始化的内存池必须先调用释放接口再去初始化。
【举例】
无。
【相关主题】
fuac_reqbuf_deinit¶
【描述】
释放UAC(音频)内存池。
【语法】
int fuac_reqbuf_deinit(void);
【参数】
无。
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
释放之前必须先把申请到的内存全部归还给USB层,才能去释放,否则失败。一旦出现采样率或者声道数或者位深出现切换,则必须要释放内存然后重新初始化内存池。
【举例】
无。
【相关主题】
fuac_reqbuf_get¶
【描述】
获取空闲的UAC(音频)内存。
【语法】
uint8_t *fuac_reqbuf_get(uint32_t *buf_index);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
不使用音频内存时,必须调用fuac_send_message接口,把获取到的内存归还给USB。
【举例】
无。
【相关主题】
fuac_send_message¶
【描述】
音频数据传输发送。
【语法】
int fuac_send_message(const uint8_t *buf, uint32_t len, uint32_t buf_index);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
入参buf_index是通过调用fuac_reqbuf_get接口返回值的buf_index。fuac_reqbuf_get和fuac_send_message接口一一对应,顺序申请和顺序发送,不要乱序发送,否则Host出现声音不正常。
【举例】
无。
【相关主题】
fuac_receive_message¶
【描述】
音频数据传输接收。
【语法】
int fuac_receive_message(void *buf, int len, int timeout);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
fuac_opts_set¶
【描述】
设置UAC1.0音频参数。
【语法】
int fuac_opts_set(const struct uac_opts *opts);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
fuac_get_opts¶
【描述】
获取UAC1.0音频参数。
【语法】
int fuac_get_opts(struct uac_opts *opts);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
fuac_rate_get¶
【描述】
获取uac1.0音频采样率。
【语法】
uint32_t fuac_rate_get(void);
【参数】
无
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
fuac2_get_opts¶
【描述】
获取音频2.0协议的参数,包括通道数等。
【语法】
int fuac2_get_opts(struct uac2_opts *opts);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
fuac2_set_opts¶
【描述】
设置UAC2.0音频参数。
【语法】
int fuac2_set_opts(const struct uac2_opts *opts);
【参数】
【返回值】
【需求】
头文件:f_uac.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
usb_dfu_init_env_entities¶
【描述】
初始化配置DFU接口功能,配置环境变量(目前只支持RAM)。
【语法】
int usb_dfu_init_env_entities(const char *type, char *envstr, const char *devstr);
【参数】
【返回值】
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_dfu_update_status¶
【描述】
获取升级事件。
【语法】
uint32_t usb_dfu_update_status(void);
【参数】
无。
【返回值】
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_dfu_update_size_get¶
【描述】
获取升级文件实际大小。
【语法】
uint64_t *usb_dfu_update_size_get(void);
【参数】
无。
【返回值】
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_dfu_get_entity¶
【描述】
获取列表中指定的DFU结构体变量。
【语法】
struct usb_dfu_entity *usb_dfu_get_entity(int alter);
【参数】
【返回值】
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_dfu_read¶
【描述】
获取指定DFU列表中的一定大小的缓冲数据。
【语法】
int usb_dfu_read(struct usb_dfu_entity *dfu, void *buf, int size, uint32_t blk_seq_num);
【参数】
【返回值】
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_dfu_free_entities¶
【描述】
释放DFU相关资源。
【语法】
void usb_dfu_free_entities(void)
【参数】
无。
【返回值】
无。
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_dfu_wait_transmit_finish¶
【描述】
等待DFU传输完成。
【语法】
int usb_dfu_wait_transmit_finish(uint32_t timeout);
【参数】
【返回值】
【需求】
头文件:f_dfu.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
hid_report_descriptor_info¶
【描述】
配置报告描述符信息,设置报告数据格式和功能。
【语法】
int hid_report_descriptor_info(const void *report_desc, size_t report_desc_len);
【参数】
【返回值】
【需求】
头文件:f_hid.h
库文件:libusb_device.a
【注意】
报告描述符的定义要遵循标准HID协议,与VFS模块接口配套使用。
【举例】
请参考HID官方文档:《Device Class Definition for Human interface Devices》和《HID Usage Tables》。
【相关主题】
无。
hid_add_report_descriptor¶
【描述】
配置多个报告表描述符,设置报告表数据格式、功能和接口协议。
【语法】
int hid_add_report_descriptor(const uint8_t *report_desc, size_t report_desc_len, uint8_t protocol);
【参数】
【返回值】
【需求】
头文件:f_hid.h
库文件:libusb_device.a
【注意】
报告描述符的定义要遵循标准HID协议,与fhid_send_data和fhid_recv_data接口配套使用。
【举例】
请参考HID官方文档:《Device Class Definition for Human interface Devices》和《HID Usage Tables》。
【相关主题】
无。
fhid_send_data¶
【描述】
HID数据传输发送。
【语法】
ssize_t fhid_send_data (uint8_t report_index, const char *buf, size_t buflen);
【参数】
【返回值】
【需求】
头文件:f_hid.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
fhid_recv_data¶
【描述】
HID数据传输接收。
【语法】
ssize_t fhid_recv_data(uint8_t report_index, char *buf, size_t buflen);
【参数】
【返回值】
【需求】
头文件:f_hid.h
库文件:libusb_device.a
【注意】
由于此接口存在读数据阻塞机制,所以只能在任务中调用接口,不能在中断回调调用。
【举例】
无。
【相关主题】
无。
uvc_get_frame_buf¶
【描述】
获取帧缓冲区的地址。
【语法】
uint8_t *uvc_get_frame_buf(struct file *filep, uint32_t index, uint32_t offset);
【参数】
【返回值】
【需求】
头文件:usb_v4l2.h
库文件:libusb_base.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_serial_ioctl¶
【描述】
往USB虚拟串口发送ioctl命令。
【语法】
int usb_serial_ioctl(uint32_t index, int cmd, unsigned long arg);
【参数】
发送命令附带对应的参数,值为0x104命令附带的参数值,目前可以传入0和非0数,非0为控制读接口挂起等待读取数据;传0则是不挂起等待的读取数据。 |
【返回值】
【需求】
头文件:usbd_acm.h
库文件:libusb_device.a
【注意】
无
【举例】
usb_serial_ioctl(0, 0x104, 1); // 建议使用值为0x104的宏表示cmd命令,最后一个入参传1,表示控制虚拟串口读取数据时,等待挂起,直到接收到数据为止
usb_serial_ioctl(0, 0x104, 0); // 最后一个入参传0,表示控制虚拟串口调用读接口读取数据时,不管有没有收到数据,立即返回读结果
【相关主题】
无。
usb_serial_read¶
【描述】
读取来自主机往USB虚拟串口发送的数据。
【语法】
ssize_t usb_serial_read(uint32_t index, char *buffer, size_t buflen);
【参数】
【返回值】
【需求】
头文件:usbd_acm.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
usb_serial_write¶
【描述】
往USB虚拟串口发送的数据。
【语法】
ssize_t usb_serial_write(uint32_t index, const char *buffer, size_t buflen);
【参数】
【返回值】
【需求】
头文件:usbd_acm.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
feth_send_data¶
【描述】
往USB虚拟网口发送的数据。
【语法】
void feth_send_data(const uint8_t *buf, uint32_t buflen);
【参数】
【返回值】
无。
【需求】
头文件:f_eth.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
feth_init_ops¶
【描述】
初始化USB虚拟网口与网络协议栈对接的钩子。
【语法】
int feth_init_ops(struct usb_eth_operations* ops);
【参数】
【返回值】
【需求】
头文件:f_eth.h
库文件:libusb_device.a
【注意】
无。
【举例】
无。
【相关主题】
无。
feth_change_connect_status¶
【描述】
控制虚拟网口断连重连接口。
【语法】
void feth_change_connect_status(bool connect);
【参数】
【返回值】
无。
【需求】
头文件:f_eth.h
库文件:libusb_device.a
【注意】
此接口只是通知host,虚拟网口需要断开或连接,至于host端有没有真的将虚拟网口断开或连接,属于host的行为,device端并不知晓。
【举例】
无。
【相关主题】
无。
UART驱动开发¶
概述¶
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器),在嵌入式设计中,UART用来与PC进行通信,包括与监控调试器和其它器件,如EEPROM通信。
Huawei_LiteOS UART驱动模块的基本框架包括文件适配层、驱动抽象层、驱动实现层。
文件适配层:采用LOSCFG_FS_VFS管控,可裁剪,提供通过文件方式控制uart。
驱动抽象层:提供对外接口,提供通过struct LosDevice设备文件方式控制uart。
驱动实现层:uart控制器硬件实现层,不提供对外接口。
UART驱动主要开发流程如下:
UART驱动加载:
UART驱动的加载由驱动抽象层提供加载接口,系统在启动过程中注册驱动。
UART设备加载:
UART设备由系统根据需要及实际情况进行加载。
UART数据收发:
驱动抽象层提供裸读写接口,文件适配层提供VFS文件方式读写。
开发指导¶
裸读写UART¶
UART驱动抽象层提供如下的接口:
文件方式读写UART¶
参考“VFS”,支持open、close、read、write、ioctl操作。
模块配置¶
打开菜单,进入Driver → Uart Type菜单,完成UART模块的配置。
进入Driver → Main Uart Driver菜单,选择用于串口输出的默认串口驱动,可以选择如下配置:
编程实例¶
simple uart¶
使能LOSCFG_DRIVERS_SIMPLE_UART,simple uart只支持输出。
general uart¶
使能LOSCFG_DRIVERS_UART,general uart既支持输出,也支持输入。
裸读写UART¶
通过串口号获取UART设备。
打开串口。
写串口。
挂起串口。
恢复串口。
关闭串口。
#define BUF_SIZE 100
UINT32 TestCase(VOID)
{
struct LosDevice *uartDev = NULL;
int ret;
ssize_t rtRet;
char dataBuf[BUF_SIZE] = {"test uart\n"};
uartDev = UartDevGetByPort(0);
if (uartDev == NULL) {
return LOS_NOK;
}
ret = UartOpen(uartDev);
if (ret != 0) {
return LOS_NOK;
}
rtRet = UartWrite(uartDev, dataBuf, BUF_SIZE);
if (rtRet <= 0) {
goto OUT_CLOSE;
}
ret = UartSuspend(uartDev);
if (ret != 0) {
goto OUT_CLOSE;
}
ret = UartResume(uartDev);
if (ret != 0) {
goto OUT_CLOSE;
}
ret = UartClose(uartDev);
if (ret != 0) {
return LOS_NOK;
}
return LOS_OK;
OUT_CLOSE:
(VOID)UartClose(uartDev);
return LOS_NOK;
}
文件读写UART¶
使能LOSCFG_FS_VFS。
打开串口。
写串口。
挂起串口。
恢复串口。
关闭串口。
#define BUF_SIZE 100
#define UART_DEV_PATH "/dev/uartdev-0"
UINT32 TestCase(VOID)
{
int ret;
int fd;
char dataBuf[BUF_SIZE] = {"test uart\n"};
ssize_t writeLen;
fd = open(UART_DEV_PATH, O_RDWR);
if (fd == -1) {
return LOS_NOK;
}
writeLen = write(fd, dataBuf, BUF_SIZE);
if (writeLen <= 0) {
goto OUT_CLOSE;
}
ret = ioctl(fd, Pl011_IOCTL_SUSPEND, 0);
if (ret != 0) {
goto OUT_CLOSE;
}
ret = ioctl(fd, Pl011_IOCTL_RESUME, 0);
if (ret != 0) {
goto OUT_CLOSE;
}
ret = close(fd);
if (ret != 0) {
return LOS_NOK;
}
return LOS_OK;
OUT_CLOSE:
(VOID)close(fd);
return LOS_NOK;
}
配置工具¶
概述¶
LiteOS使用Kconfig文件配置系统。所用的Kconfig语言是一种菜单配置语言,config.in和Kconfig都由该语言编写而成。LiteOS使用python kconfiglib来解析、展示Kconfig文件。解析Kconfig文件后,会在根目录下生成/更新“.config”文件,同时在开发板的include文件夹下生成“menuconfig.h”。使用menuconfig前,需要先安装python和kconfiglib。
安装工具¶
安装python 2.7/3.2+。
下面以python3.10为例介绍安装方法。
命令行方式安装:
sudo apt-get install python3.10源码包编译安装:
通过官网下载python源码包。
解压源码包。
参考如下命令完成解压,将压缩包名替换为实际下载的源码包名:
tar -xf Python-3.10.2.tgz检查依赖。
解压后进入到目录中,执行“./configure”命令以检查编译与安装python所需的依赖:
cd Python-3.10.2 ./configure
如果没有报错就继续下一步操作,如果存在报错就根据提示安装依赖。
编译&安装python。
sudo make sudo make install
检查python版本并正确链接python命令。
python --version如果显示的不是刚刚安装的python版本,则需要执行以下命令来正确链接python命令。
获取python目录,例如对于python 3.10.2,执行如下命令。
which python3.10链接python命令到刚刚安装的python包。
将以下命令中的“python3.10-path“替换为“which python3.10”命令执行后的回显路径:
cd /usr/bin sudo rm python3 && sudo ln -s "python3.10-path" python3 sudo rm python && sudo ln -s python3 python
再次检查python版本。
python --version python3 --version
安装pip包管理工具。
如果pip命令不存在,可以下载pip源码包进行安装。pip依赖setuptools,如果setuptools不存在,也需要安装。
命令行方式安装:
sudo apt-get install python3-setuptools python3-pip -y sudo pip3 install --upgrade pip
源码包方式安装:
安装setuptools。
点击setuptools源代码包下载地址,可以参考下面的命令进行安装:
sudo unzip setuptools-50.3.2.zip cd setuptools sudo python setup.py install
须知:
setuptools最新版本不支持python 2.7,如果使用python 2.7,请下载setuptools 45.0.0版本以支持python 2.7。安装pip。
点击pip源代码包下载地址,可以参考下面的命令进行安装:
sudo tar -xf pip-20.2.4.tar.gz cd pip-20.2.4 sudo python setup.py install
安装kconfiglib库。
对于服务器可以联网的情况。
可以直接使用如下命令安装kconfiglib:
sudo pip install kconfiglib对于服务器不能联网的情况。
可以采用离线的方式安装。首先在其他能联网的环境上下载kconfiglib,可以下载kconfiglib的wheel文件“kconfiglib-14.1.0-py2.py3-none-any.whl”或源代码文件“kconfiglib-14.1.0.tar.gz”,这里以14.1.0版本为例。
wheel文件的安装,可以参考如下命令:
sudo pip install kconfiglib-14.1.0-py2.py3-none-any.whl源代码文件的安装,可以参考如下命令:
sudo tar -zxvf kconfiglib-14.1.0.tar.gz cd kconfiglib-14.1.0 sudo python setup.py install
使用指导¶
使用menuconfig配置工具前,请确保已经安装编译LiteOS的交叉编译器工具链,并加入环境变量。
为满足不同的使用场景,配置工具支持下述命令。根据使用场景,在根目录下执行下述其中一个命令即可。在执行命令前,先根据开发板拷贝“self_src/tools/build/config/”目录下的默认配置文件“${platform}.config”到根目录,并重命名为“.config”。除了“.config”文件外,还有一个重要的配置文件“build/menuconfig/config.in”文件(该文件包含了各个模块的Kconfig文件)。“.config”文件决定了各个配置项的默认值,“config.in”文件决定了展示图形化界面的配置项。
make defconfig¶
在LiteOS根目录下执行“make defconfig”命令会解析根目录下的“.config”文件(cmake当前不支持该功能),使所有的配置项尽可能使用其默认配置,并更新“.config”,同时在对应的LiteOS开发板工程的子目录include下生成“menuconfig.h”。
make allyesconfig¶
在LiteOS根目录下执行“make allyesconfig”命令会解析根目录下的“.config”文件(cmake当前不支持该功能),使所有的配置项尽可能使能,即设置为Y,并更新“.config”,同时在对应的LiteOS开发板工程的子目录include下生成“menuconfig.h”。
make allnoconfig¶
在LiteOS根目录下执行“make allnoconfig”命令会解析根目录下的“.config”文件(cmake当前不支持该功能),使所有的配置项尽可能禁用,即设置为“is not set”,并更新“.config”,同时在对应的LiteOS开发板工程的子目录include下生成“menuconfig.h”。
配置项说明¶
配置项简介¶
运行“make menuconfig”,进入LiteOS Configuration主界面,目前有Compiler、Targets、Kernel、Debug、SSP等选项。下面对Compiler,Targets和SSP选项进行介绍,其余选项在各个模块章节中均有介绍,这里不再赘述。
Compiler配置¶
Compiler选项中包含了两个子菜单:Compiler交叉编译器工具链和Optimize Option编译优化选项。

现有多种交叉编译器可选:包括“arm-himix410-linux”和“arm-none-eabi-”等,请选择实际使用的编译器。
有三种编译优化选项可选:Optimize None(不做编译优化)、Optimize Speed(优化编译速度)和Optimize Size(优化编译体积)。
Targets配置¶
Targets选项中包含Family(产品)、Board(芯片)、Enable Floating Pointer Unit(使能浮点数)、Enable Access Permission Control(使能访问权限控制)。

Family:选择产品,其中BVT、DPT、AIOT等都是产品名,SECRET是保密项目,QEMU和STM32是LiteOS内部使用的。
Board:选择具体芯片,目前支持Hi3161、Hi3372、Hi3322,Hi1A21等。
Enable Access Permission Control:使能访问权限控制,可以根据不同架构使能对应的内存管理单元,包含RiscV下的PMP。
打开菜单,进入Targets菜单,相关配置项介绍如下,可根据配置项名称搜到其具体所在位置。
须知: OS运行模式需要target进行适配,具体可以使用的模式以交付的模式为准。
SSP配置¶
选择“Stack Smashing Protector (SSP) Compiler Feature”选项,该选项可以打开和关闭堆栈保护功能。
-fno-stack-protector:关闭堆栈保护。
-fstack-protector:启用堆栈保护,若C或C++函数的局部变量中含有字符(char)数组(数组大小大于等于4个字节的字符数组),并且不会被优化掉,编译器会为函数插入保护代码。
-fstack-protector-strong:启用堆栈保护,若C或C++函数中有数组定义并且不会被优化掉,或者存在局部变量的地址引用时,编译器会为函数插入保护代码(默认选项)。
-fstack-protector-all:为所有C或C++函数插入保护代码,相比上一个,可能会大幅增加性能开销。
推荐使用“-fstack-protector”选项,在兼顾性能的同时,增强了安全性。
ArchStackGuardInit:liteos值提供伪随机数金丝雀值,需要厂商适配真随机,以保证安全性。

须知: LiteOS提供弱函数ArchStackGuardInit,采用伪随机数方式初始化“__stack_chk_guard”,如果用户需要达到真随机数,需重定义ArchStackGuardInit。另外部分架构没有随机种子,无法达成伪随机数,因此,用户必须自行验证“__stack_chk_guard”的随机性。
NX配置¶
“Enable Data Sec NX Feature”选项可以打开和关闭数据段不可执行功能。
选择该选项建议配合使用系统提供的链接脚本,不使用系统链接脚本时需要参考系统链接脚本编写私有链接脚本。
该选项只在支持MMU的架构上起效,不支持MMU的架构请在初始化过程中自行实现NX。

可维可测¶
Shell¶
概述¶
LiteOS提供Shell命令行,它能够以命令行交互的方式访问操作系统的功能或服务:它接收并解析用户输入的命令,并处理操作系统的输出结果。
LiteOS提供的Shell作为在线调试工具,支持常用的基本调试功能,包含系统、文件、网络、动态加载和Trace等相关命令。同时LiteOS的Shell可以根据需求新增定制命令。
开发指导¶
Shell命令可以通过串口,Telnet或者自定义工具输入。新增定制的命令,需重新编译链接后才能执行。
LiteOS提供的Shell命令参见后面“系统命令参考”章节。
LiteOS的Shell模块为用户提供下面几个接口,接口详细信息可以查看API参考。
说明:
静态注册命令方式一般用于注册系统常用命令,动态注册命令方式一般用于注册用户命令。
静态注册命令有5个入参,动态注册命令有4个入参。下面除去第一个入参是静态注册独有的,剩余的四个入参两个注册命令是一致的。第一个入参:命令变量名,用于设置链接选项(“build/make/liteos_tables_ldflags.mk”的LITEOS_TABLES_LDFLAGS变量)。例如变量名为ls_shellcmd,链接选项就应该设置为:LITEOS_TABLES_LDFLAGS += -uls_shellcmd。这个入参是静态注册独有的,动态注册中没有这个入参。
第二个入参:命令类型,目前支持两种命令类型。
CMD_TYPE_EX:不支持标准命令参数输入,会把用户填写的命令关键字屏蔽掉。例如:输入“ls /ramfs”,传入给命令处理函数的参数只有“/ramfs”,对应于命令处理函数中的argv[0],而ls命令关键字并不会被传入。
CMD_TYPE_STD:支持的标准命令参数输入,所有输入的字符都会通过命令解析后被传入。例如:输入“ls /ramfs”,“ls”和“/ramfs”都会被传入命令处理函数,分别对应于命令处理函数中的argv[0]和argv[1]。
第三个入参:命令关键字,是命令处理函数在Shell中对应的名称。命令关键字必须唯一,即两个不同的命令项不能有相同的命令关键字,否则只会执行其中一个。Shell在执行用户命令时,如果存在多个命令关键字相同的命令,只会执行在“help”命令中排在最前面的那个。
第四个入参:命令处理函数的入参最大个数。
静态注册命令暂不支持设置。
动态注册命令支持设置不超过32的入参最大个数,或者设置为XARGS(其在代码中被定义为0xFFFFFFFF)表示不限制参数个数。
第五个入参:命令处理函数名,即在Shell中执行命令时被调用的函数。
打开menuconfig,进入Debug ---> Enable a Debug Version ---> Enable Shell菜单,完成Shell的配置。
下面以注册系统命令ls为例,介绍新增Shell命令的典型开发流程。
定义Shell命令处理函数。
Shell命令处理函数用于处理注册的命令。例如定义一个命令处理函数osShellCmdLs,处理ls命令,并在头文件中声明命令处理函数原型。
int osShellCmdLs(int argc, const char **argv);
须知:
命令处理函数的参数与C语言中main函数的参数类似,包括两个入参:argc:Shell命令的参数个数。个数中是否包括命令关键字,和注册命令时的命令类型有关。
argv:为指针数组,每个元素指向一个字符串,该字符串就是执行shell命令时传入命令处理函数的参数。参数中是否包括命令关键字,和注册命令时的命令类型有关。
注册命令。
有静态注册命令和系统运行时动态注册命令两种注册方式。
静态注册ls命令:
#include "shcmd.h" SHELLCMD_ENTRY(ls_shellcmd, CMD_TYPE_EX, "ls", XARGS, (CMD_CBK_FUNC)osShellCmdLs);
动态注册ls命令:
#include "shell.h" osCmdReg(CMD_TYPE_EX, "ls", XARGS, (CMD_CBK_FUNC)osShellCmdLs);
对于静态注册命令方式,在“build/make/liteos_tables_ldflags.mk”中设置链接选项(LITEOS_TABLES_LDFLAGS变量)。
打开菜单使能Shell,详见配置项。
编译烧录系统后,可以执行新增的Shell命令。
注意事项¶
默认模式下支持英文输入。如果在UTF8格式下输入了中文字符,只能通过回退三次来删除。
支持Shell命令、文件名及目录名的Tab键联想补全,若有多个匹配项则补全共同字符,并打印多个匹配项。对于过多的匹配项(打印多于24行),将会进行打印询问(Display all num possibilities?(y/n)),可输入y选择全部打印,或输入n退出打印,选择全部打印并打印超过24行后,会进行“--More--”提示,此时回车键继续打印,q键退出(支持Ctrl+c退出)。
不建议使用Shell命令操作“/dev”目录下的设备文件,可能会引起不可预知的结果。
Shell属于调测功能,默认配置为关闭,正式商用产品中禁止使用该功能。
免责声明:华为不承担在正式商用产品中使用该功能的任何风险。
静态注册编程实例¶
本实例演示如何使用静态注册命令方式新增一个名为test的Shell命令。编译工具以Make为例介绍 。
定义一个新增命令所要调用的命令处理函数cmd_test。
使用SHELLCMD_ENTRY函数添加新增命令项。
在liteos_tables_ldflags.mk中添加链接该新增命令项参数。
打开菜单使能Shell。
重新编译代码后运行。
定义命令所要调用的命令处理函数cmd_test:
#include "shell.h" #include "shcmd.h" int cmd_test(void) { printf("hello everybody!\n"); return 0; }
添加新增命令项:
SHELLCMD_ENTRY(test_shellcmd, CMD_TYPE_EX, "test", 0, (CMD_CBK_FUNC)cmd_test);在链接选项中添加链接该新增命令项参数:
在“build/make/liteos_tables_ldflags.mk”中LITEOS_TABLES_LDFLAGS项下添加“-utest_shellcmd”。
打开菜单使能Shell,即设置LOSCFG_SHELL=y。
重新编译代码:
make clean;make
烧录新系统镜像后,重启系统。使用help命令查看当前系统中所有注册的命令,可以看到test命令已经注册。
动态注册编程实例¶
本实例演示如何使用动态注册命令方式新增一个名为test的Shell命令。编译工具以Make为例介绍 。
定义一个新增命令所要调用的命令处理函数cmd_test。
使用osCmdReg函数添加新增命令项。
打开菜单使能Shell。
重新编译代码后运行。
定义命令所要调用的命令处理函数cmd_test:
#include "shell.h" #include "shcmd.h" int cmd_test(void) { printf("hello everybody!\n"); return 0; }
在app_init函数中调用osCmdReg函数动态注册命令:
void app_init(void) { .... .... osCmdReg(CMD_TYPE_EX, "test", 0,(CMD_CBK_FUNC)cmd_test); .... }
打开菜单使能Shell,即设置LOSCFG_SHELL=y。
重新编译代码:
make clean;make
烧录新系统镜像后,重启系统。使用help命令查看当前系统中所有注册的命令,可以看到test命令已经注册。
系统命令参考¶
使能系统命令¶
使用Shell中的系统命令前,需要打开menuconfig使能Shell,详见“配置项”。
help¶
help命令用于显示当前操作系统内所有的Shell命令。
help
举例:输入“help”。
执行help,输出当前系统内的所有Shell命令:
LiteOS # help
*******************shell commands:*************************
cpup date dlock dmesg free help hwi
log memcheck mutex queue sem stack swtmr
systeminfo task uname watch
date¶
date命令用于查询及设置系统时间。
date
date --help
date +Format
date -s YY/MM/DD
date -s hh:mm:ss
date -r Filename(开源版本暂不支持该命令)
该命令依赖于LOSCFG_SHELL_EXTENDED_CMDS,该宏开关可在menuconfig菜单项中开启“Enable Shell Ext CMDs”使能。
Debug ---> Enable a Debug Version ---> Enable Shell ---> Enable Shell Ext CMDsdate参数缺省时,默认显示当前系统时间。
--help、+Format、-s、-r不能混合使用。
举例:
输入“date +%Y--%m--%d”。
执行“date +%Y--%m--%d”,按其指定格式打印当前系统时间:
LiteOS # date +%Y--%m--%d
2021--01--20
uname¶
uname命令用于显示操作系统的名称、系统编译时间、版本信息等。
uname [-a | -s | -t | -v | --help]
参数缺省时,默认显示操作系统名称。
uname的参数不能混合使用。
举例:
输入“uname -a”。
执行“uname -a”,获取系统信息:
LiteOS # uname -a
LiteOS V200R002C00SPC050B011 LiteOS 3.2.2 Mar 30 2019 16:09:28
task¶
task命令用于查询系统任务信息。
task_ _[ID]
参数缺省时,默认打印全部运行任务信息。
task后加ID,当ID参数在[0, 64]范围内时,返回指定ID号任务的任务名、任务ID、任务的调用栈信息(最大支持15层调用栈),其他取值时返回参数错误的提示。如果指定ID号对应的任务未创建,则提示。
如果在task命令中,发现任务是Invalid状态,请确保pthread_create创建函数时有进行如下操作之一,否则资源无法正常回收。
选择的是阻塞模式应该调用pthread_join()函数。
选择的是非阻塞模式应该调用pthread_detach()函数。
如果不想调用前面两个接口,就需要设置pthread_attr_t状态为PTHREAD_STATE_DETACHED,将attr参数传入pthread_create,此设置和调用pthread_detach函数一样,都是非阻塞模式。
举例1:输入“task 6”。
举例2:输入“task”。
执行“task 0xb”,查询ID号为b的任务信息:
LiteOS # task 0xb
TaskName = SerialEntryTask
TaskId = 0xb
*******backtrace begin*******
traceback 0 -- lr = 0x1d804 fp = 0xa86bc
traceback 1 -- lr = 0x1da40 fp = 0xa86e4
traceback 2 -- lr = 0x20154 fp = 0xa86fc
traceback 3 -- lr = 0x258e4 fp = 0xa8714
traceback 4 -- lr = 0x242f4 fp = 0xa872c
traceback 5 -- lr = 0x123e4 fp = 0xa8754
traceback 6 -- lr = 0x2a9d8 fp = 0xb0b0b0b
执行“task”,查询所有任务信息:
LiteOS # task
Name TaskEntryAddr TID Priority Status StackSize WaterLine StackPoint TopOfStack EventMask SemID CPUUSE CPUUSE10s CPUUSE1s MEMUSE
---- ------------ --- ------- -------- --------- -------- ---------- ---------- ------- ----- ------- --------- ------ -------
Swt_Task 0x40002770 0x0 0 QueuePend 0x6000 0x2cc 0x4015a318 0x401544e8 0x0 0xffff 0.0 0.0 0.0 0
IdleCore000 0x40002dc8 0x1 31 Ready 0x400 0x15c 0x4015a7f4 0x4015a550 0x0 0xffff 98.6 98.2 99.9 0
system_wq 0x400b80fc 0x3 1 Pend 0x6000 0x244 0x40166928 0x40160ab8 0x1 0xffff 0.0 0.0 0.0 0
SerialShellTask 0x40090158 0x5 9 Running 0x3000 0x55c 0x40174918 0x40171e70 0xfff 0xffff 1.2 1.7 0.0 48
SerialEntryTask 0x4008fe30 0x6 9 Pend 0x1000 0x2c4 0x40175c78 0x40174e88 0x1 0xffff 0.0 0.0 0.0 72
说明:
输出项说明:
Name:任务名。
TID:任务ID。
Priority:任务的优先级。
Status:任务当前的状态。
StackSize:任务栈大小。
WaterLine:该任务栈已经被使用的内存大小。
StackPoint:任务栈指针,表示栈的起始地址。
TopOfStack:栈顶的地址。
EventMask:当前任务的事件掩码,没有使用事件,则默认任务事件掩码为0(如果任务中使用多个事件,则显示的是最近使用的事件掩码)。
SemID:当前任务拥有的信号量ID,没有使用信号量,则默认0xFFFF(如果任务中使用了多个信号量,则显示的是最近使用的信号量ID)。
CPUUSE:系统启动以来的CPU占用率。
CPUUSE10s:系统最近10秒的CPU占用率。
CPUUSE1s:系统最近1秒的CPU占用率。
MEMUSE:截止到当前时间,任务所申请的内存大小,以字节为单位显示。MEMUSE仅针对系统内存池进行统计,不包括中断中处理的内存和任务启动之前的内存。 任务申请内存,MEMUSE会增加,任务释放内存,MEMUSE会减小,所以MEMUSE会有正值和负值的情况。
MEMUSE为0:说明该任务没有申请内存,或者申请的内存和释放的内存相同。
MEMUSE为正值:说明该任务中有内存未释放。
MEMUSE为负值:说明该任务释放的内存大于申请的内存。
系统任务说明(LiteOS系统初始任务有以下几种):
Swt_Task:软件定时器任务,用于处理软件定时器超时回调函数。
IdleCore000:系统空闲时执行的任务。
system_wq:系统默认工作队列处理任务。
SerialEntryTask:从底层buf读取用户的输入,初步解析命令,例如tab补全,方向键等。
SerialShellTask:接收命令后进一步解析,并查找匹配的命令处理函数,进行调用。
任务状态说明:
Ready:任务处于就绪状态。
Pend:任务处于阻塞状态。
PendTime:阻塞的任务处于等待超时状态。
Suspend:任务处于挂起状态。
Running:该任务正在运行。
Delay:任务处于延时等待状态。
SuspendTime:挂起的任务处于等待超时状态。
Invalid:非上述任务状态。
free¶
free命令可显示系统内存的使用情况,同时显示系统的text段、data段、rodata段、bss段大小。
free [_-k _| -m]
输入free显示内存使用情况,total表示系统动态内存池的总大小,used表示已使用的内存大小,free表示空闲的内存大小。text表示代码段大小,data表示数据段大小,rodata表示只读数据段大小,bss表示未初始化全局变量占用的内存大小。
free命令能够以三种单位来显示内存使用情况,包括Byte、KByte和MByte。
举例:
分别输入“free”、“free -k”、“free -m”。
以三种单位显示内存使用情况的输出:
LiteOS # free
total used free
Mem: 117631744 31826864 85804880
text data rodata bss
Mem: 4116480 423656 1204224 6659316
LiteOS # free -k
total used free
Mem: 114874 31080 83793
text data rodata bss
Mem: 4020 423 1176 6503
LiteOS # free -m
total used free
Mem: 112 30 81
text data rodata bss
Mem: 3 0 1 6
memcheck¶
检查动态申请的内存块是否完整,是否存在内存越界造成的节点损坏。
memcheck
打开内存完整性检查开关。
目前只有bestfit内存管理算法支持该命令,需要使能LOSCFG_KERNEL_MEM_BESTFIT。
Kernel ---> Memory Management ---> Dynamic Memory Management Algorithm ---> bestfit该命令依赖于LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK,使用时需要在菜单项中开启“Enable integrity check or not”。
Debug ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable integrity check or not
当内存池所有节点完整时,输出“memcheck over, all passed!”。
当内存池存在节点不完整时,输出被损坏节点的内存块信息。
举例:
输入“memcheck”。
没有内存越界时,执行“memcheck”输出内容如下:
LiteOS # memcheck system memcheck over, all passed!
发生内存越界时,执行“memcheck”输出内容如下:
LiteOS # memcheck [ERR][OsMemIntegrityCheck], 1145, memory check error! freeNodeInfo.pstPrev:(nil) is out of legal mem range[0x80353540, 0x84000000] broken node head: (nil) (nil) (nil) 0x0, pre node head: 0x7fc6a31b 0x8 0x80395ccc 0x80000110 --------------------------------------------- dump mem tmpNode:0x80395df4 ~ 0x80395e34 0x80395df4 :00000000 00000000 00000000 00000000 0x80395e04 :cacacaca cacacaca cacacaca cacacaca 0x80395e14 :cacacaca cacacaca cacacaca cacacaca 0x80395e24 :cacacaca cacacaca cacacaca cacacaca --------------------------------------------- dump mem :0x80395db4 ~ tmpNode:0x80395df4 0x80395db4 :00000000 00000000 00000000 00000000 0x80395dc4 :00000000 00000000 00000000 00000000 0x80395dd4 :00000000 00000000 00000000 00000000 0x80395de4 :00000000 00000000 00000000 00000000 --------------------------------------------- cur node: 0x80395df4 pre node: 0x80395ce4 pre node was allocated by task:SerialShellTask cpu0 is in exc. cpu1 is running. excType:software interrupt taskName = SerialShellTask taskId = 8 task stackSize = 12288 system mem addr = 0x80353540 excBuffAddr pc = 0x80210b78 excBuffAddr lr = 0x80210b7c excBuffAddr sp = 0x803b2d50 excBuffAddr fp = 0x80280368 R0 = 0x59 R1 = 0x600101d3 R2 = 0x0 R3 = 0x8027a300 R4 = 0x1 R5 = 0xa0010113 R6 = 0x80395e04 R7 = 0x80317254 R8 = 0x803b2de4 R9 = 0x4 R10 = 0x803afca4 R11 = 0x80280368 R12 = 0x1 CPSR = 0x600101d3
以上各关键输出项含义如下:
“mem check error”,表示检测到了内存节点被破坏。
“cur node:”,表示该节点内存被踩,并打印内存地址。
“pre node:”,表示被踩节点前面的节点,并打印节点地址。
“pre node was allocated by task:SerialShellTask”,表示在SerialShellTask任务中发生了踩内存。
“taskName”和“taskId”,分别表示发生异常的任务名和任务ID。
memused¶
memused命令用于查看当前系统used节点中保存的函数调用栈LR信息。通过分析数据可检测内存泄露问题。
memused
打开menuconfig菜单开启内存泄漏检测。
目前只有bestfit内存管理算法支持该功能,需要使能LOSCFG_KERNEL_MEM_BESTFIT。
Kernel ---> Memory Management ---> Dynamic Memory Management Algorithm ---> bestfit该命令依赖于LOSCFG_MEM_LEAKCHECK,可以在菜单项中开启“Enable Function call stack of Mem operation recorded”:
Debug ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable Function call stack of Mem operation recorded开启该菜单项后,会在内存操作时,将函数调用关系LR记录到内存节点中,若相同调用栈的内存节点随时间增长而不断增多,则可能存在内存泄露,通过LR可以追溯内存申请的位置。重点关注LR重复的节点。
须知:
此配置打开时,会影响内存占用和内存操作性能,因此仅在检测内存泄露问题时打开。
举例:
输入“memused”。
LiteOS # memused
node LR[0] LR[1] LR[2]
0x802d79e4: 0x8011d740 0x8011a990 0x00000000
0x802daa0c: 0x8011d5ec 0x8011d740 0x8011a990
0x802dca28: 0x8006e6f4 0x8006e824 0x8011d5ec
0x802e2a48: 0x8014daac 0x8014db4c 0x800f6da0
0x802e8a68: 0x8011d274 0x8011d654 0x8011d740
0x802e8a94: 0x8014daac 0x8014db4c 0x8011d494
0x802eeab4: 0x8011d4e0 0x8011d658 0x8011d740
0x802f4ad4: 0x8015326c 0x80152a20 0x800702bc
0x802f4b48: 0x8015326c 0x80152a20 0x800702bc
0x802f4bac: 0x801504d8 0x801505d8 0x80150834
0x802f4c08: 0x801504d8 0x801505d8 0x80150834
0x802f4c74: 0x801504d8 0x801505d8 0x80150834
0x802f4e08: 0x801504d8 0x801505d8 0x80150834
0x802f4e60: 0x801030e8 0x801504d8 0x801505d8
0x802f4e88: 0x80103114 0x801504d8 0x801505d8
0x802f4eb4: 0x801504d8 0x801505d8 0x80150834
0x802f4f7c: 0x801504d8 0x801505d8 0x80150834
0x802f5044: 0x801504d8 0x801505d8 0x80150834
0x802f510c: 0x800702bc 0x80118f24 0x00000000
hwi¶
hwi命令用于查询当前中断信息。
hwi
hwi status all
hwi status hwiNum [devId]
使能hwi命令,在菜单项打开宏开关LOSCFG_DEBUG_HWI:
Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Hwi Debugging输入“hwi”即显示当前中断号、中断次数及注册中断名称。
输入“hwi status all”显示当前所有已创建中断的状态。
输入“hwi status hwiNum [devId]”查询特定的中断状态;共享中断需要devId参数,非共享中断不需要参数devId。
若打开宏开关LOSCFG_CPUP_INCLUDE_IRQ,hwi命令还会显示各个中断的处理时间(cycles)、CPU占用率以及中断类型。该宏开关可在菜单项中开启“Enable Cpup include irq”使能:
Kernel ---> Enable Extend Kernel ---> Enable Cpup ---> Enable Cpup include irq关于该宏开关更详细的介绍,参见CPU占用率“开发流程”。
举例:
输入“hwi”。
输入“hwi status hwiNum devId”。
输入“hwi status hwiNum”。
输入“hwi status all”。
输入“hwi”,显示中断信息(LOSCFG_CPUP_INCLUDE_IRQ关闭)。
LiteOS # hwi InterruptNo Count Name 35: 1364: 36: 0: 40: 79: uart_pl011
输入“hwi”,显示中断信息(LOSCFG_CPUP_INCLUDE_IRQ打开)。
LiteOS # hwi InterruptNo Count Name CYCLECOST CPUUSE CPUUSE10s CPUUSE1s mode 3: 1333 122 0.0 0.0 0.0 normal 4: 0 0 0.0 0.0 0.0 normal 5: 59 uart_pl011 305 0.0 0.0 0.0 normal 12: 96 ETH 131 0.0 0.0 0.0 normal
输入“hwi status 3 0x1”,显示中断号3,设备号0x1共享中断的状态。
LiteOS # hwi status 3 0x1 InterruptNo DevId Priority Affinity Created Enable Pending ----------- ----- -------- -------- ------- ------ ------- 3 0x1 20 0x0 0 1 0
输入“hwi status 3”,显示中断号3的非共享中断的状态。
LiteOS # hwi status 3 InterruptNo DevId Priority Affinity Created Enable Pending ----------- ----- -------- -------- ------- ------ ------- 3 0x0 20 0x0 0 1 0
输入“hwi status all”,显示所有已创建中断的状态。
LiteOS # hwi status all InterruptNo DevId Priority Affinity Created Enable Pending ----------- ----- -------- -------- ------- ------ ------- 0 0x0 20 0x0 1 1 0 1 0x0 20 0x0 1 1 0 2 0x0 20 0x0 1 1 0 30 0x0 20 0x0 1 1 0 33 0x0 0 0x1 1 1 0
queue¶
queue命令用于查看队列的使用情况。
queue
该命令依赖于LOSCFG_DEBUG_QUEUE,该宏开关可在菜单项中开启“Enable Queue Debugging”使能。
Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Queue Debugging
举例:
输入“queue”。
执行“queue”后得到队列的使用情况:
LiteOS # queue
used queues information:
Queue ID <0x0> may leak, queue len is 0x10, readable cnt:0x0, writeable cnt:0x10, TaskEntry of creator:0x0x80007d5, Latest operation time: 0x614271
以上各输出项含义如下:
sem¶
sem命令用于查询系统内核信号量的相关信息。
sem [ID]
sem fulldata
查询所有在用的信号量信息,打印信息包括如下:SemID、Count、Original Count、Creater (TaskEntry)、Last Access Time |
参数缺省时,显示所有信号量的使用数及信号量总数。
sem后加ID,当ID参数在[0, 1023]范围内时,返回指定ID号的信号量使用数(如果指定ID号的信号量未被使用则提示),其他取值时返回参数错误的提示。
sem的参数ID和fulldata不可以混用。
参数fulldata依赖LOSCFG_DEBUG_SEMAPHORE,该宏开关可在菜单项中开启“Enable Semaphore Debugging”使能。
Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Semaphore Debugging
举例1:输入“sem 1”。
举例2:输入“sem”和“sem fulldata”。
执行“sem 1”,查询指令结果:
LiteOS # sem 1
SemID Count
---------- -----
0x00000001 0x1
No task is pended on this semphore!
执行“sem”和“sem fulldata”,查询所有在用的信号量信息:
LiteOS # sem
SemID Count
---------- -----
0x00000000 1
SemID Count
---------- -----
0x00000001 1
SemID Count
---------- -----
0x00000002 1
SemUsingNum : 3
LiteOS # sem fulldata
Used Semaphore List:
SemID Count OriginalCout Creater(TaskEntry) LastAccessTime
------ ----- ------------ ----------------- --------------
0x2 0x1 0x1 0x80164d70 0x3
0x0 0x1 0x1 0x0 0x3
0x1 0x1 0x1 0x0 0x3
mutex¶
mutex命令用于查看互斥锁的使用情况。
mutex
该命令依赖于LOSCFG_DEBUG_MUTEX,该宏开关可在菜单项中开启“Enable Mutex Debugging”使能。
Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Mutex Debugging
举例:
输入“mutex”。
执行“mutex”输出互斥锁使用情况:
LiteOS # mutex
used mutexs information:
Mutex ID <0x0> may leak, Owner is null, TaskEntry of creator: 0x8000711,Latest operation time: 0x0
以上各主要输出项含义如下:
swtmr¶
swtmr命令用于查询系统软件定时器的相关信息。
swtmr [ID]
使能swtmr命令,在菜单项打开宏开关LOSCFG_DEBUG_SWTMR:
Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Swtmr Debugging参数缺省时,默认显示所有软件定时器的相关信息。
swtmr后加ID号时,当ID在[0, 当前软件定时器个数-1]范围内时,返回对应ID的软件定时器的相关信息,其他取值时返回错误提示。
举例:
输入“swtmr”和“swtmr 0”。
执行“swtmr”查询软件定时器相关信息,输出如下:
LiteOS # swtmr
SwTmrID State Mode Interval Arg handlerAddr
---------- ------- ------- --------- ---------- --------
0x00000000 Ticking Period 1000 0x00000000 0x800442d
执行“swtmr 0”查询ID为0的软件定时器信息,输出如下:
LiteOS # swtmr 0
SwTmrID State Mode Interval Arg handlerAddr
---------- ------- ------- --------- ---------- --------
0x00000000 Ticking Period 1000 0x00000000 0x800442d
systeminfo¶
systeminfo命令用于显示当前操作系统的资源使用情况,包括任务、信号量、互斥锁、队列、软件定时器等。
对于信号量、互斥锁、队列和软件定时器,如果在系统镜像中已经裁剪了这些模块,那么说明系统没有使用这些资源,该命令也就不会显示这些资源的情况。
systeminfo
举例:
输入“systeminfo”。
执行“systeminfo”查看系统资源使用情况,输出如下:
LiteOS # systeminfo
Module Used Total
--------------------------------
Task 5 16
Sem 0 20
Mutex 1 20
Queue 1 10
SwTmr 1 16
以上各输出项含义如下:
log¶
log命令用于修改&查询系统的日志打印等级。
log level [levelNum]
须知: log命令的帮助信息中还显示了另外两个参数module和path,但当前暂不支持这两个参数。
该命令依赖于LOSCFG_SHELL_LK,可在菜单项中开启“Enable Shell lk”使能。
Debug ---> Enable a Debug Version ---> Enable Shell ---> Enable Shell lKlog level命令用于设置系统的日志打印等级,包括6个等级。
TRACE_EMG = 0
TRACE_COMMOM = 1
TRACE_ERROR = 2
TRACE_WARN = 3
TRACE_INFO = 4
TRACE_DEBUG = 5
若log level命令不加levelNum参数,则提示命令的使用方法。
举例:
输入“log level 4”。
执行“log level 4”,设置系统的日志打印等级为INFO级别:
LiteOS # log level 4
Set current log level INFO
dmesg¶
dmesg命令用于控制内核dmesg缓存区。
dmesg
dmesg [-c | -C | -D | -E | -L | -U]
dmesg -s <size>
dmesg -l <level>
dmesg > file(开源版本暂不支持该命令)
须知:
写入文件需确保已挂载文件系统。
关闭串口打印会影响Shell使用,建议先连接Telnet再尝试关闭串口。
该命令依赖于LOSCFG_SHELL_DMESG,使用时可在菜单项中开启“Enable Shell dmesg”。
Debug ---> Enable a Debug Version---> Enable Shell---> Enable Shell dmesg参数缺省时,默认打印缓存区内容。
各参数均不能混合使用。
举例:
输入“dmesg”后,接着输入“dmesg -C”和“dmesg”。
第一次执行“dmesg”后,可以看到输出了缓存区内容。接着执行“dmesg -C”清空缓存区内容,紧接着再次执行“dmesg”可以看到之前缓存区中的内容已经被清空:
LiteOS # dmesg
LiteOS # log level 4
Set current log level INFO
LiteOS # dmesg
LiteOS # dmesg -C
LiteOS # dmesg
LiteOS # dmesg
须知: “dmesg -C”是日志维测命令,只支持串口输出,不支持通过console输出到其他输出端口(Telnet、USB等)。
stack¶
stack命令用于显示当前操作系统内所有栈的信息。
stack
该功能默认开启。
举例:
输入“stack”。
执行“stack”命令查看系统内所有栈的信息:
LiteOS # stack
stack name cpu id stack addr total size used size
---------- ------ --------- -------- --------
udf_stack 3 0x3c800 0x28 0x0
udf_stack 2 0x3c828 0x28 0x0
udf_stack 1 0x3c850 0x28 0x0
udf_stack 0 0x3c878 0x28 0x0
abt_stack 3 0x3c8a0 0x28 0x0
abt_stack 2 0x3c8c8 0x28 0x0
abt_stack 1 0x3c8f0 0x28 0x0
abt_stack 0 0x3c918 0x28 0x0
fiq_stack 3 0x3ca40 0x40 0x0
fiq_stack 2 0x3ca80 0x40 0x0
fiq_stack 1 0x3cac0 0x40 0x0
fiq_stack 0 0x3cb00 0x40 0x0
svc_stack 3 0x3cb40 0x2000 0x524
svc_stack 2 0x3eb40 0x2000 0x524
svc_stack 1 0x40b40 0x2000 0x524
svc_stack 0 0x42b40 0x2000 0x528
irq_stack 3 0x3c940 0x40 0x0
irq_stack 2 0x3c980 0x40 0x0
irq_stack 1 0x3c9c0 0x40 0x0
irq_stack 0 0x3ca00 0x40 0x0
exc_stack 3 0x44b40 0x1000 0x0
exc_stack 2 0x45b40 0x1000 0x0
exc_stack 1 0x46b40 0x1000 0x0
exc_stack 0 0x47b40 0x1000 0x0
须知: stack是异常维测命令,只支持串口输出,不支持通过console输出到其他输出端口(USB等)。
cpup¶
cpup命令用于查询系统CPU的占用率,并以百分比显示占用率。
cpup [mode] [taskID]
|
||
参数缺省时,显示系统最近10s的CPU占用率。
只输入一个参数时,该参数为mode,显示系统相应时间的CPU占用率。
输入两个参数时,第一个参数为mode,第二个参数为taskID,显示指定任务ID的任务在相应时间的CPU占用率。
该功能需要使能CPU占用率模块,菜单路径为:
Kernel ---> Enable Extend Kernel ---> Enable Cpup想更多的了解CPU占用率模块,参见“CPU占用率”。
举例:
输入“cpup 1 1”。
执行“cpup 1 1”,显示ID为1的任务最近1s的CPU占用率:
LiteOS # cpup 1 1
TaskId 1 CpuUsage in 1s: 78.7
watch¶
watch命令用于周期性监测一个命令的运行结果。
watch [-c | -n | -t | --count | --interval | -no-title] <command>
watch --over
该命令依赖于LOSCFG_SHELL_EXTENDED_CMDS,该宏开关可在菜单项中开启“Enable Shell Ext CMDs”使能。
Debug ---> Enable a Debug Version ---> Enable Shell ---> Enable Shell Ext CMDscommand参数必须是Shell命令,对于非Shell命令,会有错误提示“command is not fount”。
如果要监测命令,command是必填参数。
--over参数不能与其他参数混合使用。
举例1:输入“watch -c 5 task 1”。
举例2:在不需要watch命令监测的情况下,执行“watch --over”。
每个周期间隔1秒的执行“task 1”命令,共执行5次,watch命令监测到的结果如下所示:
LiteOS # watch -c 3 task 1
LiteOS # Thu Jan 1 16:26:26 1970
TaskName = Swt_Task
TaskId = 0x1
*******backtrace begin*******
traceback 1 -- lr = 0x08004006 -- fp = 0x0800045e
traceback 2 -- lr = 0x08004000 -- fp = 0x0800194c
traceback 3 -- lr = 0x080040da -- fp = 0x08003e50
traceback 4 -- lr = 0x080015c2 -- fp = 0x080040a8
traceback 5 -- lr = 0x0800396e -- fp = 0x08001598
Thu Jan 1 16:26:27 1970
TaskName = Swt_Task
TaskId = 0x1
*******backtrace begin*******
traceback 1 -- lr = 0x08004006 -- fp = 0x0800045e
traceback 2 -- lr = 0x08004000 -- fp = 0x0800194c
traceback 3 -- lr = 0x080040da -- fp = 0x08003e50
traceback 4 -- lr = 0x080015c2 -- fp = 0x080040a8
traceback 5 -- lr = 0x0800396e -- fp = 0x08001598
Thu Jan 1 16:26:28 1970
TaskName = Swt_Task
TaskId = 0x1
*******backtrace begin*******
traceback 1 -- lr = 0x08004006 -- fp = 0x0800045e
traceback 2 -- lr = 0x08004000 -- fp = 0x0800194c
traceback 3 -- lr = 0x080040da -- fp = 0x08003e50
traceback 4 -- lr = 0x080015c2 -- fp = 0x080040a8
traceback 5 -- lr = 0x0800396e -- fp = 0x08001598
schedstatstart¶
schedstatstart命令用于开启调度统计功能。
schedstatstart
目前该功能仅支持有浮点运算的平台,且该命令依赖于LOSCFG_DEBUG_SCHED_STATISTICS,该宏开关可在菜单项中开启“Enable Scheduler Statistics Debugging”使能。
Debug ---> Enable a Debug Version ---> Enable DebugLiteOS Kernel Resource ---> Enable Scheduler Statistics Debugging。
schedstatstart、schedstatstop和schedstatinfo等3个命令一起配套使用。
举例:输入“schedstatstart”。
查看“输出说明”。
schedstatstop¶
schedstatstop命令用于关闭调度统计功能,并输出各个CPU的调度统计信息。
schedstatstop
同“使用指南”。
举例:输入“schedstatstop”。
查看“输出说明”。
schedstatinfo¶
schedstatinfo命令用于输出各个任务的CPU调度统计信息。
schedstatinfo
同“使用指南”。
举例:
输入“schedstatinfo”。
查看“输出说明”。
文件命令参考¶
使能文件系统命令¶
使用Shell中的文件命令前,需要打开menuconfig菜单使能Shell,详见“配置项”。
cd¶
cd命令用于改变当前目录。
cd [path]
未指定目录参数时,会跳转至根目录。
cd后加目录时,跳转至该目录。
目录以 /(斜杠)开头时,表示根目录。
.(点)表示当前目录。
..(点点)表示父目录。
举例:
输入“cd ..”。
pwd¶
pwd命令用于显示当前路径。
pwd
pwd 命令将当前目录的全路径名称(从根目录开始的绝对路径)写入标准输出。全部目录使用 /(斜线)分隔,第一个 / 表示根目录,最后一个目录是当前目录。
举例:
输入“pwd”。
mkdir¶
mkdir命令用于创建一个目录。
mkdir <dir>
mkdir后加所需要创建的目录名,则将在当前目录下创建目录。
mkdir后加带路径的目录名,则将在指定路径下创建目录。
举例:
输入“mkdir share”。
rmdir¶
rmdir命令用于删除指定的目录。
rmdir <dir>
rmdir命令只能用来删除目录。
rmdir一次只能删除一个目录。
rmdir只能删除空目录。
举例:输入“rmdir dir”。
rm¶
rm命令用于删除文件或者非空目录。
rm -r <dir>
rm_ _<file>
rm命令一次只能删除一个文件。
rm -r命令可以删除非空目录。
须知: 如果使用rm删除系统关键资源(例如/dev),会造成系统死机等未知影响,需慎重使用该命令。
举例:
输入“rm 1.c”。
输入“rm -r dir”。
touch¶
touch命令用于在当前目录下创建一个空文件。
touch <filename>
touch命令创建的空文件,其默认权限为可读写。
touch命令一次只能创建一个文件。
touch命令操作已存在的文件会成功,不会更新时间戳。
须知: 在系统重要资源路径下使用touch命令创建文件,会对系统造成死机等未知影响,如在“/dev”路径下执行“touch uartdev-0”,会产生系统卡死现象。
举例:
输入“touch file.c”。
cp¶
cp命令用于拷贝文件,创建一份副本。
cp <source path> <dest path>
同一路径下,源文件与目的文件不能重名。
源文件必须存在,且不为目录。
源文件路径支持“*”和“?”通配符,“*”代表任意多个字符,“?”代表任意单个字符。目的路径不支持通配符。当源文件路径可匹配多个文件时,目的路径必须为目录。
目的路径为目录时,该目录必须存在。此时目的文件以源文件名进行命名。
目的路径为文件时,所在目录必须存在。目的文件名就是目的路径中指定的文件名。
目前不支持多文件拷贝。参数大于2个时,只对前2个参数进行操作。
目的文件不存在时创建新文件,已存在则覆盖。
须知: 拷贝系统重要资源文件时,会造成死机等重大未知影响,如拷贝“/dev/uartdev-0”文件时,会产生系统卡死现象。
举例:
输入“cp 100HSCAM/FILE0087.MP4 .”。
dd¶
dd命令用于文件读写性能统计。
dd file=[PATHNAME] mode=[OPS] bs=[SIZE] count=[N]
dd命令可并发执行,一条dd命令对应一个线程,因此可以进行多线程的读写测试。
dd命令测试较大的文件读写时,存在耗时较长的情况,可能不会立即输出性能结果,这是正常现象。
bs字段和count字段可以不指定,系统将使用缺省值LOSCFG_DD_DEFAULT_BS 1024、LOSCFG_DD_DEFAULT_CNT 1。
bs字段的SIZE不宜过大,系统将申请SIZE大小的动态内存,过大的SIZE会导致内存申请失败,导致dd命令无法正常执行。
dd流程中仅会执行open、read/write、close操作,用户需自行保证操作dd前已正确挂载对应的文件系统,如有其他操作也需用户自行执行。
举例,测试文件写性能如下:
dd file=/app/sd/testfile mode=2 bs=32768 count=32768

输出写入文件的总字节数、总耗时以及速率。
ls¶
ls命令用于显示当前目录的内容。
ls [path]
|
|
ls可以显示文件的大小。
proc下ls无法统计文件大小,显示为0。
举例:
输入“ls”。
cat¶
cat用于显示文本文件的内容。
cat <pathname>
举例:
输入“cat w“”。
lsfd¶
lsfd命令用于显示当前已经打开的文件描述符及对应的文件名。
lsfd
lsfd命令显示当前已经打开文件的fd号以及文件的名字。
举例:
输入“lsfd”。
LiteOS # lsfd
fd filename
3 /dev/shell
format¶
format命令用于格式化磁盘。
format <dev_inodename> <sectors> <option> [label]
分配的单元内存或扇区大小,取值必须为2的幂数,FAT32下最大值为128,取其他值表示自动选择合适的簇大小。不同size的分区,可用的簇大小范围不同。 |
|
format指令用于格式化磁盘,设备名可以在dev目录下查找。format时必须安装存储卡。
format只能格式化U盘、SD卡和EMMC卡,无法格式化Nand Flash和Nor Flash。
sectors参数必须传入合法值,传入非法参数可能引发异常。
举例:
输入“format /dev/mmcblk0p0 128 2”。
LiteOS # format /dev/mmcblk0p0 128 2
Format to FAT32, 128 sectors per cluster.
format /dev/mmcblk0p0 Success
mount¶
mount命令用于将设备挂载到指定目录。
mount <device> <path> <name>
mount后加需要挂载的设备信息、指定目录以及设备文件格式,就能成功挂载文件系统到指定目录。
举例:
输入“mount /dev/mmc0 /bin/vs/sd vfat”。
将“/dev/mmc0”以VFAT文件系统类型挂载到“/bin/vs/sd”目录。
LiteOS # mount /dev/mmc0 /bin/vs/sd vfat
mount ok
umount¶
umount命令用于卸载已经挂载的文件系统。
umount <dir>
umount后加上需要卸载的文件系统对应的挂载目录,即将指定文件系统卸载。
举例:
输入“umount /bin/vs/sd”。
将挂载在“/bin/vs/sd”目录上的文件系统卸载。
LiteOS # umount /bin/vs/sd
umount ok
partinfo¶
partinfo命令用于查看系统识别的硬盘、U盘和SD卡的分区信息。
partinfo <dev_inodename>
举例:
输入“partinfo /dev/sdap0”。
LiteOS # partinfo /dev/sdap0
part info :
disk id : 3
part_id in system: 0
part no in disk : 0
part no in mbr : 1
part filesystem : 0C
part dev name : sdap0
part sec start : 2048
part sec count : 167794688
partition¶
partition命令用于查看Flash分区信息。
partition <nand | spinor>
当使能YAFFS文件系统时才可以查看Nand Flash分区信息。
当使能JFFS或ROMFS文件系统时才可以查看Spinor Flash分区信息。
举例:
输入partition nand。
查看Nand Flash统分区信息。
LiteOS # partition nand
nand partition num:0, blkdev name:/dev/nandblk0, mountpt:/yaffs0, startaddr:0x00e00000, length:0x00200000
statfs¶
statfs命令用于打印文件系统的信息,如该文件系统类型、总大小、可用大小等信息。
statfs <directory>
打印信息因文件系统而异。
以YAFFS文件系统为例:
输入“statfs /yaffs0”。
statfs got:
f_type = 1497497427
cluster_size = 2048
total_clusters = 704
free_clusters = 640
avail_clusters = 640
f_namelen = 255
/yaffs0
total size: 9830400 Bytes
free size: 5890048 Bytes
virstatfs¶
virstatfs命令用于打印虚拟分区下文件系统的信息,如总大小、可用大小等信息。
virstatfs_ _<directory>
virstatfs命令只有开启了虚拟分区特性时才有效(LOSCFG_FS_FAT_VIRTUAL_PARTITION=y)。
virstatfs的输入路径,必须是已经成功应用虚拟分区的物理分区上的虚拟分区入口目录,或者其子目录。如果输入路径是该物理分区的根目录,或者该物理分区未成功应用虚拟分区特性,或者路径指向非FAT32文件系统分区,virstatfs命令均会被拒绝执行。
举例:
输入“virstatfs /bin/vs/sd/virpart0”。

sync¶
sync命令用于同步缓存数据(FAT文件系统中的数据)到SD卡。
sync
sync命令用来刷新缓存,当没有SD卡插入时不进行操作,有SD卡插入时缓存信息会同步到SD卡。成功时无显示信息。
使用sync命令前,需要使能FAT文件系统,即设置LOSCFG_FS_FAT = y。
举例:
输入“sync”,有SD卡时同步到SD卡,无SD卡时不操作。
无。
writeproc¶
writeproc命令用于向proc文件系统中的文件写入内容。
writeproc <val> <operational mark> <path>
writeproc用于将指定内容写入proc文件系统中的文件,该文件必须是已经存在的文件,也可以是非用户创建的文件。
当操作符为“>>”,同时文件存在时,将指定内容写入该文件。
使用writeproc命令前,需要使能PROC文件系统,即设置LOSCFG_FS_PROC = y。
举例:
输入“writeproc 'sys=2' >> /proc/umap/logmpp”。
Trace命令参考¶
使能Trace命令¶
使用Shell中的Trace命令前,需要先打开menuconfig菜单使能Shell,详见“配置项”。
同时配置Trace模块,详见Trace开发流程中的“开发指导”,下文中的“离线模式”在menuconfig中的菜单项为:Kernel ---> Enable Extend Kernel ---> Enable Trace Feature ---> Trace work mode ---> Offline mode”。关于Trace模块的详细介绍,详见“Trace”。
trace_mask¶
设置事件过滤掩码。
trace_mask [MASK]
如果不设置事件掩码,或者执行该命令时参数缺省,则默认仅开启任务和中断事件记录。
trace_mask后加MASK,则开启对应模块的事件记录。
具体的模块掩码MASK参见“los_trace.h”中定义的LOS_TRACE_MASK。
举例:
输入“trace_mask 0”
输入“trace_mask 0xFFFFFFFF”
执行“trace_mask 0”,设置所有模块的事件都不记录,命令执行成功后,不会输出信息。
LiteOS # trace_mask 0
LiteOS #
执行“trace_mask 0xFFFFFFFF”,设置所有模块的事件都进行记录,命令执行成功后,不会输出信息。
LiteOS # trace_mask 0xFFFFFFFF
LiteOS #
trace_start¶
开启Trace。
trace_start
输入“trace_start”即开启系统Trace功能,离线模式下会记录系统发生的事件并保存在指定buffer中。
举例:开启系统Trace功能,输入“trace_start”。
在离线模式下,成功执行“trace_start”命令后,不会输出信息。
LiteOS # trace_start
LiteOS #
trace_stop¶
停止Trace。
trace_stop
输入“trace_stop”即终止系统Trace功能, 停止记录事件。
举例:停止系统Trace功能,输入“trace_stop”。
成功执行“trace_stop”命令后,不会输出信息。
LiteOS # trace_stop
LiteOS #
trace_dump¶
在离线模式下,dump出Trace buffer的信息。
trace_dump [1 | 0]
只能在离线模式下使用trace_dump命令。
参数缺省时将格式化打印Trace数据。
trace_dump命令打印的是trace_start和trace_stop之间的数据,所以需要先执行trace_stop停止Trace后,再执行trace_dump打印Trace buffer的信息。
举例:
输入“trace_dump”。
执行“trace_dump”命令,格式化打印缓存中的数据。
LiteOS # trace_dump
*******TraceInfo begin*******
clockFreq = 180000000
CurEvtIndex = 19
Index Time(cycles) EventType CurTask Identity params
0 0x7da8da5180 0x45 0x5 0x2 0x9 0x20 0x1f
1 0x7dde8c6980 0x45 0x2 0x5 0x1f 0x4 0x9
2 0x7e1431df20 0x45 0x5 0x2 0x9 0x20 0x1f
3 0x7e49e3f720 0x45 0x2 0x5 0x1f 0x4 0x9
4 0x7e7f896cc0 0x45 0x5 0x2 0x9 0x20 0x1f
5 0x7eb53b84c0 0x45 0x2 0x5 0x1f 0x4 0x9
6 0x7eeae0fa60 0x45 0x5 0x2 0x9 0x20 0x1f
7 0x7f20931260 0x45 0x2 0x5 0x1f 0x4 0x9
8 0x7f56388800 0x45 0x5 0x2 0x9 0x20 0x1f
9 0x7f8beaa000 0x45 0x2 0x5 0x1f 0x4 0x9
10 0x7fc19015a0 0x45 0x5 0x2 0x9 0x20 0x1f
11 0x7ff7422da0 0x45 0x2 0x5 0x1f 0x4 0x9
12 0x802ce7a340 0x45 0x5 0x2 0x9 0x20 0x1f
13 0x806299bb40 0x45 0x2 0x5 0x1f 0x4 0x9
14 0x80983f30e0 0x45 0x5 0x2 0x9 0x20 0x1f
15 0x80cdf148e0 0x45 0x2 0x5 0x1f 0x4 0x9
……
24 0x6c560a8d00 0x24 0x2 0x2d 0x0 0x0 0x0
25 0x6c8baf7600 0x25 0x2 0x2d 0x0 0x0 0x0
……
36 0x71fe6f2000 0x24 0x2 0x2d 0x0 0x0 0x0
37 0x7234140900 0x25 0x2 0x2d 0x0 0x0 0x0
38 0x7269c20250 0x45 0x2 0x1 0x1f 0x4 0x0
39 0x734055a650 0x45 0x1 0x2 0x0 0x8 0x1f
40 0x7380b52450 0x45 0x2 0x1 0x1f 0x4 0x0
……
48 0x77a6d3b300 0x24 0x2 0x2d 0x0 0x0 0x0
49 0x77dc789c00 0x25 0x2 0x2d 0x0 0x0 0x0
50 0x7812269550 0x45 0x2 0x1 0x1f 0x4 0x0
……
*******TraceInfo end*******
trace_reset¶
在离线模式下,清除Trace buffer中的事件数据。
trace_reset
只能在离线模式下使用trace_reset命令。
举例:
输入“trace_reset”。
执行“trace_reset”,清除事件数据:
LiteOS # trace_reset
LiteOS #
调度统计¶
LiteOS提供调度统计维测功能,该功能用于统计CPU的一些调度信息,包括idle任务启动时间、idle任务运行时长、调度切换次数、中断次数、核间迁移次数(多核)等。
在菜单项中使能Debug ---> Enable a Debug Version ---> Enable DebugLiteOS Kernel Resource ---> Enable Scheduler Statistics Debugging,可以开启调度统计维测功能(仅支持有浮点运算功能的平台)。
输入shell命令schedstatstart,开启调度统计功能。
输入shell命令schedstatstop,关闭调度统计功能,系统将输出各个CPU的调度统计信息。
输入shell命令schedstatinfo,系统将输出各个任务的CPU调度统计信息。
shell指令执行顺序:schedstatstart -> schedstatstop -> schedstatinfo。
调用schedstatstart后系统输出如下图所示:

调用schedstatstop后系统输出如下图所示:

参数描述如下表所示:
调用schedstatinfo后系统输出如下图所示:

参数描述如下表所示:
内存调测方法¶
概述¶
LiteOS提供内存维测功能,在菜单项中使能Debug ---> Enable a Debug Version ---> Enable MEM Debug,可以开启内存维测。
开启内存维测后,在LOS_MemFree和LOS_MemRealloc接口中会根据链表关系对目标节点及前后节点进行合法性校验,若检测到异常则立即挂起系统并抛出异常。除此之外,还提供了内存维测子功能,各功能可以独立使能,详见后续章节。
多内存池机制¶
系统中使用多个动态内存池时,需对各内存池进行管理和使用情况统计。
系统内存机制中通过链表实现对多个内存池的管理。内存池需回收时可调用对应接口进行去初始化。
通过多内存池机制,可以获取系统各个内存池的信息和使用情况,也可以检测内存池空间分配交叉情况,当系统两个内存池空间交叉时,第二个内存池会初始化失败,并给出空间交叉的提示信息。
打印系统中已初始化的所有内存池,包括内存池的起始地址、内存池大小、空闲内存总大小、已使用内存总大小、最大的空闲内存块大小、空闲内存块数量、已使用的内存块数量,仅打开LOSCFG_MEM_MUL_POOL时有效。 |
打开菜单开启多内存池机制。
功能依赖于LOSCFG_MEM_MUL_POOL,使用时在菜单项中开启“Enable Memory multi-pool control”:
Debug ---> Enable a Debug Version---> Enable MEM Debug---> Enable Memory multi-pool control调用LOS_MemInit接口进行内存池初始化,内存池回收时调用LOS_MemDeInit接口进行去初始化。
调用LOS_MemInfoGet获取指定内存池的信息和使用情况。
调用LOS_MemPoolList获取系统所有内存池信息和使用情况。
初始化内存池时,需保证各内存池空间无交叉,若交叉则会导致初始化失败。
malloc/free系列接口默认从OS系统内存池申请和释放内存,其它内存池的操作必须调用LiteOS内存接口(LOS_MemAlloc等),不能调用malloc/free系列接口及其相关封装接口。
内存池回收必须调用LOS_MemDeInit接口去初始化(回收前需确保池中内存块均已释放),否则二次初始化该内存池空间会失败,导致该内存池不能被重新使用。
内存池大小需根据业务实际情况合理分配。
void test(void)
{
UINT32 ret = 0;
UINT32 size = 0x100000;
VOID *poolAddr1 = LOS_MemAlloc(OS_SYS_MEM_ADDR, size);
ret = LOS_MemInit(poolAddr1, size);
if (ret != 0) {
PRINTK("LOS_MemInit failed\n");
return;
}
VOID *poolAddr2 = LOS_MemAlloc(OS_SYS_MEM_ADDR, size);
ret = LOS_MemInit(poolAddr2, size);
if (ret != 0) {
PRINTK("LOS_MemInit failed\n");
return;
}
PRINTK("\n********step1 list the mem poll\n");
LOS_MemPoolList();
LOS_MemDeInit(poolAddr1);
if (ret != 0) {
PRINTK("LOS_MemDeInit failed\n");
return;
}
PRINTK("\n********step2 list the mem poll\n");
LOS_MemPoolList();
LOS_MemDeInit(poolAddr2);
if (ret != 0) {
PRINTK("LOS_MemDeInit failed\n");
return;
}
PRINTK("\n********step3 list the mem poll\n");
LOS_MemPoolList();
}
log:
********step1 list the mem poll
pool0 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8017b2c0 0x100000 0x2e1fc 0xd1d20 0xd1d20 0x2b 0x1
pool1 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8027b2c0 0x7d84d40 0x7070c8 0x767db94 0x767db94 0x1026 0x1
pool2 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8078244c 0x100000 0x10 0xfff0c 0xfff0c 0x1 0x1
pool3 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8088245c 0x100000 0x10 0xfff0c 0xfff0c 0x1 0x1
********step2 list the mem poll
pool0 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8017b2c0 0x100000 0x2e1fc 0xd1d20 0xd1d20 0x2b 0x1
pool1 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8027b2c0 0x7d84d40 0x7070c8 0x767db94 0x767db94 0x1026 0x1
pool2 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8088245c 0x100000 0x10 0xfff0c 0xfff0c 0x1 0x1
********step3 list the mem poll
pool0 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8017b2c0 0x100000 0x2e1fc 0xd1d20 0xd1d20 0x2b 0x1
pool1 :
pool addr pool size used size free size max free node size used node num free node num
--------------- -------- ------- -------- -------------- ------------- ------------
0x8027b2c0 0x7d84d40 0x7070c8 0x767db94 0x767db94 0x1026 0x1
内存合法性检查¶
业务发生踩内存导致内存节点控制头被踩,长时间后才触发业务异常,业务逻辑复杂,难以定位发生踩内存的位置。
开启该功能后,在动态内存申请接口中增加内存合法性检查,对动态内存池中所有节点控制头的合法性进行检查,若已发生动态内存节点被踩,及时触发异常,输出error信息,缩小问题定位范围。
打开菜单开启内存合法性检查。
功能依赖LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK,使用时在菜单项中开启“Enable integrity check or not”:
Debug ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable integrity check or not发生踩内存后,下一次内存申请操作即触发异常,并给出被踩节点和前一节点信息,可初步分析定位是否是前一节点越界踩内存。踩内存发生范围为上一次内存申请和此次内存申请之间。检测到节点被踩会主动Panic并打印异常信息。
该功能开启时,系统内存申请操作的性能下降明显,建议仅在定位问题时开启,默认关闭。
通过构造超出内存长度的memset操作,构造越界踩内存,造成内存节点损坏,构造代码如下:
VOID SampleFunc(VOID *p)
{
memset(p, 0, 0x110); // 超出长度的memset,设置踩内存场景
}
UINT32 Test(UINT32 argc, CHAR **args)
{
void *p1, *p2;
p1 = LOS_MemAlloc((void*)OS_SYS_MEM_ADDR, 0x100);
p2 = LOS_MemAlloc((void*)OS_SYS_MEM_ADDR, 0x100);
dprintf("p1 = %p, p2 = %p \n", p1, p2);
SampleFunc(p1); // 因为p1和p2相邻,当memset的长度超过p1的内存大小,就会越界踩到p2内存
LOS_MemFree(OS_SYS_MEM_ADDR, (void *)p1);
LOS_MemFree(OS_SYS_MEM_ADDR, (void *)p2);
return 0;
}
执行上述代码后,执行Shell命令“memcheck”,其输出内容如下:

从上图可以看到打印了错误信息。
标记2所指“cur node:0x11b1ac”表示该节点内存被踩,“pre node:0x11b09c”表示被踩节点前面的节点。标记3所示“pre node was allocated by task:app_Task”表示在app_Task任务中发生了踩内存。
标记1打印的是p1和p2内存的起始地址,“p2 = 0x11b1bc”,减去控制头大小0x10,即p2-0x10=0x11b1ac,就是cur node打印出的地址,即p2内存被踩。从代码可以看到p1和p2是两个相邻的节点(这也可以从打印的p1和p2地址看出来,即p1+p1的size+控制头大小=p2,0x11b0ac+0x100+0x10=0x11b1bc),所以“pre node:0x11b09c”应该就是p1的地址,从标记1获取p1地址为“p1 = 0x11b0ac”,即pre node加上控制头大小0x10(0x11b09c+0x10=0x11b0ac)。
内存size检查¶
memset和memcpy操作动态内存,发生越界踩内存问题。
对于memset和memcpy操作,当入参为动态内存节点时,增加对内存节点实际大小与入参指定大小的检查,若指定大小大于节点实际大小时,输出error信息,并且取消该次memset或memcpy操作,所以能够防止操作越界。动态内存越界场景下,可开启该功能定位问题。
须知: 错误码定义见“错误码简介”。8~15位代表的所属模块为动态内存模块,值为0x01。
打开菜单开启内存size检查:
目前只有bestfit内存管理算法支持该功能,所以需要使能LOSCFG_KERNEL_MEM_BESTFIT。
Kernel ---> Memory Management ---> Dynamic Memory Management Algorithm ---> bestfit同时该功能依赖于LOSCFG_BASE_MEM_NODE_SIZE_CHECK,该宏开关可以通过在菜单项中开启“Enable size check or not”使能。
Debug ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable size check or not
开启该功能后,memset和memcpy性能下降,建议仅在需要定位越界问题时开启,默认关闭。
通过构造超出内存长度的memset和memcpy操作,构造越界踩内存问题,构造代码如下:
VOID test(VOID)
{
UINT32 size = 0x100;
VOID *poolAddr1 = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, size);
if (poolAddr1 == NULL) {
PRINTK("malloc poolAddr1 failed\n");
return;
} else {
PRINTK("malloc poolAddr1 %p successed\n", poolAddr1);
}
VOID *poolAddr2 = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, size);
if (poolAddr2 == NULL) {
PRINTK("malloc poolAddr2 failed\n");
return;
} else {
PRINTK("malloc poolAddr2 %p successed\n", poolAddr2);
}
LOS_MemCheckLevelSet(LOS_MEM_CHECK_LEVEL_LOW); // 开启对memset和memcpy的长度检测
PRINTK("memset poolAddr1 overflow\n");
memset(poolAddr1, 0x10, size * 2); // 超出长度的memset
PRINTK("memset poolAddr1\n");
memset(poolAddr1, 0x10, size); // 合理长度的memset
PRINTK("memcpy poolAddr2 overflow\n");
memcpy(poolAddr2, poolAddr1, size * 2); // 超出长度的memcpy
PRINTK("memcpy poolAddr2\n");
memcpy(poolAddr2, poolAddr1, size); // 合理长度的memcpy
LOS_MemCheckLevelSet(LOS_MEM_CHECK_LEVEL_DISABLE); // 关闭对memset和memcpy的长度检测
LOS_MemFree((VOID *)OS_SYS_MEM_ADDR, (VOID *)poolAddr1);
LOS_MemFree((VOID *)OS_SYS_MEM_ADDR, (VOID *)poolAddr2);
return 0;
}
log:
malloc poolAddr1 0x80349514 successed
malloc poolAddr2 0x80349624 successed
LOS_MemCheckLevelSet: LOS_MEM_CHECK_LEVEL_LOW
memset poolAddr1 overflow
[ERR] ---------------------------------------------
memset: dst inode availSize is not enough availSize = 0x100, memcpy length = 0x200
runTask->taskName = osMain
runTask->taskId = 64
*******backtrace begin*******
traceback 0 -- lr = 0x80209798 fp = 0x802c6930
traceback 1 -- lr = 0x80210fc4 fp = 0x802c6954
traceback 2 -- lr = 0x8020194c fp = 0x802c6994
traceback 3 -- lr = 0x80201448 fp = 0x802c699c
traceback 4 -- lr = 0x802012fc fp = 0x0
[ERR] ---------------------------------------------
memset poolAddr1
memcpy poolAddr2 overflow
[ERR] ---------------------------------------------
memcpy: dst inode availSize is not enough availSize = 0x100, memcpy length = 0x200
runTask->taskName = osMain
runTask->taskId = 64
*******backtrace begin*******
traceback 0 -- lr = 0x80209798 fp = 0x802c6930
traceback 1 -- lr = 0x8020dbc4 fp = 0x802c6954
traceback 2 -- lr = 0x8020194c fp = 0x802c6994
traceback 3 -- lr = 0x80201448 fp = 0x802c699c
traceback 4 -- lr = 0x802012fc fp = 0x0
[ERR] ---------------------------------------------
memcpy poolAddr2
由于开启size检测,非法的memset和memcpy操作被取消,输出error信息。“runTask->taskName = osMain”显示了该非法操作发生在osMain函数中,并打印寄存器lr和fp的值。此时可以打开编译后生成的 .asm反汇编文件,默认生成在“RTOS_Lite/out/<platform>”目录下,其中的platform为具体的平台名,通过对比“寄存器lr”的值,查看函数的嵌套调用。
内存泄露检测¶
业务运行中发生内存泄露,业务逻辑复杂或者长时间运行才出现。
申请内存和释放内存时,在内存节点控制头中记录函数调用栈,发生内存泄露时,通过分析used节点信息,可定位疑似内存泄露的位置。
多层栈回溯:
打开菜单开启内存泄漏检测。
目前只有bestfit内存管理算法支持该功能,需要使能LOSCFG_KERNEL_MEM_BESTFIT。
Kernel ---> Memory Management ---> Dynamic Memory Management Algorithm ---> bestfit同时该功能依赖于LOSCFG_MEM_LEAKCHECK,可以在菜单项中选择“Enable Function call stack of Mem operation recorded”开启该宏开关:
Debug ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable Function call stack of Mem operation recorded
配置调用栈回溯信息菜单配置路径:
Debug ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable Function call stack of Mem operation recordedLOSCFG_MEM_OMIT_LR_CNT:调用栈回溯忽略层级,默认配置为2。
LOSCFG_MEM_RECORD_LR_CNT:调用栈回溯记录个数,默认配置为3。
默认配置下,获取0~2层LR信息,忽略第0和第1两层(调用封装接口的节点0层和1层LR信息相同),记录第2层。
使用Shell命令“memused”获取used节点数据。
系统稳定运行后,若used节点个数随时间一直增加,极大可能存在内存泄露,对数据进行对比分析,重点关注LR重复的节点是否存在内存泄露,泄漏点可通过LR信息进行回溯查找。
打印log信息如下,memused命令说明详见“memused”:
LiteOS # memused node LR[0] LR[1] LR[2] 0x802d7b34: 0x8006d86c 0x8011c604 0x8011c758 0x802dab6c: 0x8006d16c 0x8006d8a0 0x8011c604
基于任务的内存节点统计:
打开菜单开启LOSCFG_MEM_DFX_SHOW_CALLER_RA宏。
Kernel ---> Memory Management ---> Enable Memory Task Usage Statistics ---> Enable Show where MemAlloc function is called目前bestfit和bestfit_little内存管理算法都支持该功能。
使用Shell命令“task_mem”获取used节点数据。
打印log信息如下:
HuaWei LiteOS # task_mem ========== pool info ========== pool addr pool size used size free size max free node size used node num free node num UsageWaterLine --------------- -------- ------- -------- -------------- ------------- ------------ ------------ 0x114770 0x7eb890 0x5c68 0x7e5ae8 0x7e5ae8 0x9 0x1 0x5fd0 ========== [all task heap] pool start: 0x114770 ========== Task:Swt_Task(taskId=00) malloc details: Current total pool alloc size=0 Bytes Current total slab alloc size=400 Bytes Current total node cost size=0 Bytes Task:IdleCore000(taskId=01) malloc details: callerRA=0x00101d50 : totalSize=00018528 Bytes, blockCount= 3 callerRA=0x00106e36 : totalSize=00000604 Bytes, blockCount= 1 callerRA=0x00108172 : totalSize=00002068 Bytes, blockCount= 1 callerRA=0x001031a2 : totalSize=00002456 Bytes, blockCount= 4 Current total pool alloc size=23656 Bytes Current total slab alloc size=0 Bytes Current total node cost size=180 Bytes
分析定位
callerRA: 内存申请调用者。
totalSize: 调用者申请的总size大小。
blockCount:调用者申请的内存块个数。
定位思路:多次执行taskmem命令,重点排查size和count一直在递增的callerRA;
如果callerRA指向用户自己封装的user_defined_malloc接口,则在user_defined_malloc前后加上callerRA统计打点,示例如下:
#include "los_memory.h" void *user_defined_malloc(size) { LOS_SaveCallerRa((uintptr_t)__builtin_return_address(0)); /* 函数主体 ...... */ LOS_ResetCallerRa(); }
再次编译运行后,可以通过命令查看调用user_defined_malloc函数的上层函数callerRA,由此找到内存泄漏点。
上述功能开启时会增加系统内存占用,影响内存操作性能,建议仅在定位问题时开启,默认关闭。
多层栈回溯内存占用size = 内存节点个数 * (LOSCFG_MEM_RECORD_LR_CNT - LOSCFG_MEM_OMIT_LR_CNT)* sizeof(指针)。
基于任务的内存节点统计内存占用size = 内存节点个数 * sizeof(unsigned int)。
内存重复释放检测¶
业务运行中出现内存重复释放,业务逻辑复杂或者长时间运行才出现。
释放内存时,在内存节点控制头中记录函数调用栈,发生内存泄露时,通过分析节点信息调用栈信息,可定位内存重复释放的位置。
打开LOSCFG_MEM_DFX_DOUBLE_FREE_CHECK宏
Kernel ---> Memory Management ---> Enable check mem double free check by record function called address
目前bestfit和bestfit_little内存管理算法都支持该功能。
当发生内存重复释放时,会打印log信息:(当开启LOSCFG_MEM_DEBUG时,系统会因为重复释放挂死)
Memory double free detected!
Current running taskId = 3, taskName = app_Task.
Memory first free function addr = 0x0x100860.
Memory double free function addr = 0x0x101710.
任务间通信调测方法¶
队列调测方法¶
队列是一种生产者消费者模型,生产者生产消息放入队列,等待被消费者使用。如果队列已满,生产者被挂起,如果队列已空,消费者被挂起。LiteOS中使用队列传递消息时,可以设置超时时间,队列的主要作用是实现任务间的异步通信。通过Shell命令“queue”或者使用OsShellCmdQueueInfoGe函数可以查看队列的使用情况。
“queue”命令和OsShellCmdQueueInfoGe函数依赖LOSCFG_DEBUG_QUEUE,使用时需要在menuconfig中开启“Enable Queue Debugging”。
Debug ---> Enable a Debug Version ---> Enable MMC Debug ---> Enable mmc dma descriptors dump
在Shell窗口中执行命令“queue”或者直接在任务中使用OsShellCmdQueueInfoGet函数,打印系统中的队列信息如下:

其输出项的含义见“队列”,调试过程中主要使用上图中的标识项“TaskEntry of creator”,即创建队列的接口函数地址(0x100830)。在.asm反汇编文件(make编译默认在“LiteOS/Huawei_LiteOS/out/<platform>”目录下,CMake编译默认在产品对应的目录下, 其中的platform为具体的平台名)中找到该地址,可以看到创建队列的函数名,比如这里的app_init(0x100830),见下图。

死锁检测机制¶
在许多应用中进程需要以独占的方式访问资源,当操作系统允许多个进程并发执行时可能会出现进程永远被阻塞现象,如两个进程分别等待对方所占的资源,于是两者都不能执行而处于永远等待状态,此现象称为死锁。死锁检测模块支持自旋锁、互斥锁、 二值信号量和线程锁(pthread mutex)四种类型锁的死锁,以及重复上锁、错误释放和锁的深度溢出的检测。
打开菜单,进入Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Lockdep Debugging菜单, 完成死锁检测模块的配置。
打开自旋锁检测后,检测到死锁时会打印死锁信息,死锁检测的打印信息示例如图1所示。
复制request addr的值(本例中为0x471d2),在系统镜像的.asm反汇编文件(make编译默认在“LiteOS/Huawei_LiteOS/out/<platform>”目录下,CMake编译默认在产品对应的目录下, 其中的platform为具体的平台名)中找到相应的地址,如图2所示,即可定位到调用的位置及调用函数(本例中为Task_B)。
根据图1中第二个蓝框的自旋锁,通过代码逻辑找到另一个任务持有该锁的情况,再结合代码,调整spinlock调用的时序,从而解决死锁问题。
须知: 自旋锁死锁一般发生在某个CPU卡住、任务不再发生调度时。若不开启自旋锁死锁检查,可以使用JLink等调试工具halt住CPU或者触发看门狗异常,查看PC值是否为自旋锁代码,以此确定是否发生了自旋锁死锁。
构造ABBA互斥锁死锁场景,具体如下:
有两个任务,分别为Task_A和Task_B,同时系统中还存在其他系统默认初始任务。在任务Task_A中依次申请Mutex1和Mutex0,Task_B任务依次申请Mutex1和Mutex0, 构成两个任务ABBA死锁场景。
代码如下:
#include "los_mux.h"
static UINT32 mutexTest[2];
UINT32 g_TestTaskID01, g_TestTaskID02;
static VOID Task_A(VOID)
{
UINT32 ret;
ret = LOS_MuxPend(mutexTest[0], LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
dprintf("%s pend muxid %d\n", __func__, mutexTest[0]);
}
LOS_TaskDelay(0x2);
ret = LOS_MuxPend(mutexTest[1], 6000); // 定时阻塞模式 6000tick
if (ret == LOS_ERRNO_MUX_TIMEOUT) {
dprintf("%s pend muxid %d\n", __func__, mutexTest[1]);
}
return;
}
static VOID Task_B(VOID)
{
UINT32 ret;
ret = LOS_MuxPend(mutexTest[1], LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
dprintf("%s pend muxid %d\n", __func__, mutexTest[1]);
}
LOS_TaskDelay(0x2);
ret = LOS_MuxPend(mutexTest[0], 6000); // 定时阻塞模式 6000tick
if (ret == LOS_ERRNO_MUX_TIMEOUT) {
dprintf("%s pend muxid %d\n", __func__, mutexTest[0]);
}
return;
}
UINT32 mutexDeadlock(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S stTask1 = {0};
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Task_A;
stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
stTask1.pcName = "Task_A";
stTask1.usTaskPrio = 9;
TSK_INIT_PARAM_S stTask2 = {0};
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Task_B;
stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_MIN_STACK_SIZE;
stTask2.pcName = "Task_B";
stTask2.usTaskPrio = 9;
ret = LOS_MuxCreate(&mutexTest[0]);
ret |= LOS_MuxCreate(&mutexTest[1]);
if (ret != LOS_OK) {
PRINT_ERR("create mutex error %u\n", ret);
}
ret = LOS_TaskCreate(&g_TestTaskID01, &stTask1);
ret |= LOS_TaskCreate(&g_TestTaskID02, &stTask2);
if (ret != LOS_OK) {
PRINT_ERR("create task error %u\n", ret);
}
LOS_TaskDelay(3000); // 任务休眠3000Ticks
ret = LOS_MuxDelete(mutexTest[0]);
ret = LOS_MuxDelete(mutexTest[1]);
LOS_TaskDelete(g_TestTaskID01);
LOS_TaskDelete(g_TestTaskID02);
return LOS_OK;
}
针对以上场景出现的问题,根据日志分析即可,具体操作步骤如下:
注:详细流程可参考自旋锁定位实例。
根据dump信息,可以看出任务Task_A与Task_B发生死锁,。
在.asm反汇编文件中,找到相应的任务函数,定位到互斥锁的pend位置及调用的接口。
根据上下文确认死锁原因,并调整申请锁的时序。
异常交互¶
操作系统对运行期间发生异常时,用户可以使用Shell命令进一步定位异常原因,查看系统状态。
系统发生异常时,保留Shell基础交互功能。
打开菜单开启异常交互功能。
该功能需要配置LOSCFG_EXC_INTERACTION=y,默认处于关闭状态。若关闭该选项,则异常交互失效。
开启该宏开关的菜单项为:
Debug ---> Enable a Debug Version ---> Enable Shell ---> Enable exc interaction输入“help”,查看当前支持的Shell命令。
系统异常时,异常交互功能只会使能串口中断、其他中断都会被关闭;用户创建的任务也会被删除,只保留异常交互所使用的基本任务。
异常交互属于调测功能,默认配置为关闭,正式商用产品中禁止使用该功能。
免责声明:华为不承担在正式商用产品中使用该功能的任何风险。
低功耗调测方法¶
低功耗调测使能¶
LiteOS提供低功耗维测功能,在菜单项中使能Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource--->Enable Lowpower Sleep Debugging,可以开启低功耗维测。使能低功耗维测先使能低功耗框架需要先打开menuconfig菜单使能Shell,详见“配置项”;同时启用低功耗框架,详见“低功耗框架”。该功能仅供用户开发阶段使用,正式版本商用前请关闭此功能。
系统状态信息查询¶
系统状态信息查询命令会打印出中断、定时器和任务的详细信息。阻止任务深睡事件查询、系统休眠唤醒打点命令配合该命令使用,方便用户定位。系统状态信息查询命令效果可参考系统命令hwi、swtmr、task,详见“系统命令参考”。
输入命令“pm sysinfo”。

打印中断与定时器信息,应先支持系统命令hwi、swtmr。详见“系统命令参考”。
低功耗动态开关¶
低功耗动态开关支持手动打开、关闭、状态查询功能,默认处于打开状态。
打开低功耗动态开关:lpm enable。
关闭低功耗动态开关:lpm disable。
查询低功耗动态开关的状态:lpm status。
无。
阻止系统深睡事件查询¶
阻止系统深睡事件是指用户钩子getSleepMode返回深睡模式,但是系统经判断无法进入深睡的事件。阻止系统深睡的原因可能是距离定时器或任务到期剩余的时间达不到深睡要求的最低时间阈值、任务投票阻止等。
开机以来阻止系统深睡事件查询命令:lpm blockinfo cpuid。cpuid指的是具体的CPU号。
阻止系统深睡事件统计信息清空命令:lpm resetblockinfo cpuid。
示例:
从下图中可以得到以下信息:
0核task id为3的任务阻止系统深睡2次,任务地址为0x8008ced。
0核swtmr id为2的定时任务阻止系统深睡1次,定时器任务地址为0x800fc91。
Normal,3)、(Light,3)、(Deep,3)三个投票各阻碍一次深睡。

当前只支持单核,因此cpuid当前只能为0,即“lpm blockinfo 0”或者“lpm resetblockinfo 0”。
统计信息根据定时器id或者任务id记录,任务或定时器销毁与创建可能存在id相同的情况,会被统计到一起,用户应避免此场景。如:某a时刻任务A id为8,执行完后销毁,阻止深睡3次;经过一段时间,任务B被创建,id同样为8,阻止深睡2次;那么统计信息中type为task,id为8的阻碍次数为3次,handler为任务B的handler。
被阻碍一次深睡会统计此刻所有的不满足深睡条件的因素。
系统休眠唤醒打点¶
系统休眠唤醒打点功能会对Normal、Light、Deep模式下的睡眠时戳信息进行统计。单项信息依次为cpu号、睡眠模式、睡眠开始时刻、睡眠结束时刻,实际睡眠时间,被唤醒的中断。默认记录条数为10条,深睡时唤醒中断为固定值4294967295。
系统休眠唤醒打点查询命令:lpm timestamp cpuid。cpuid指的是具体的CPU号。
修改最大记录条数可通过配置LOSCFG_LOWPOWER_RECORD_COUNT实现,最大值为1000。
示例:

当前只支持单核,因此cpuid当前只能为0,即“lpm timestamp 0”。
系统休眠唤醒打点统计的是实际睡眠时戳信息。如上图中用户配置浅睡时间为100,实际浅睡时间为99。
魔法键使用方法¶
系统未输出挂死相关信息,但是无响应时,可以通过魔法键查看中断是否有响应。在中断有响应的情况下,可以通过魔法键查看task信息中 的CPUP(CPU占用率),找到是哪个任务长时间占用CPU导致系统其他任务无响应(一般为比较高优先级任务一直抢占CPU,导致低优先级任务无响应)。
在UART中断和USB转虚拟串口中断中,嵌入魔法键检查功能,对特殊按键进行识别,输出相关信息。
打开菜单开启魔法键功能。
该功能需要配置LOSCFG_ENABLE_MAGICKEY=y,默认处于关闭状态,若关闭该选项,则魔法键失效,开启该宏开关的菜单项为:
Debug ---> Enable MAGIC KEY
说明:
可以在menuconfig中光标移动到该选项上,输入“?”,查看帮助信息:Answer Y to enable LiteOS Magic key. ctrl + r : Magic key check switch; ctrl + z : Show all magic op key; ctrl + t : Show task information; ctrl + p : System panic; ctrl + e : Check system memory pool.
输入“Ctrl + r ” 键,打开或者关闭魔法键检测功能。
在连接UART或者USB转虚拟串口的情况下,输入“Ctrl + r ” 键,打开魔法键检测功能,输出“Magic key on”;再输入一次后,则关闭魔法键检测功能,输出“Magic key off”。魔法键功能如下:
Ctrl + z:帮助键,输出相关魔法键简单介绍。
Ctrl + t:输出任务相关信息。
Ctrl + p:系统主动进入panic,输出panic相关信息后,系统会挂住。
Ctrl + e:系统进行简单的内存池完整性检查,检查发现错误则输出相关错误信息,检查正常则输出“system memcheck over, all passed!”。
魔法键检测功能打开情况下,如果需要通过UART或者USB转虚拟串口输入特殊字符需避免与魔法键值重复,否则魔法键会被误触发,而原有设计功能可能出现错误。
栈估算工具使用方法¶
对于内存资源受限的嵌入式设备,可以使用栈估算工具分析任务栈,达到精确使用栈空间的目的。
创建任务为任务设置任务栈大小时,可以使用栈估算工具分析任务栈,达到精确设置任务栈大小的目的。
系统划分内存设置中断栈大小时,可以使用栈估算工具分析中断栈,达到精确设置中断栈大小的目的。
可以使用栈估算工具分析任务栈溢出问题。
栈估算工具是基于python语言开发的函数栈静态分析工具,是一款离线工具。通过解析反汇编文件,计算得到任务运行中栈空间使用的最大值,为设置任务栈大小、栈溢出分析、栈内存优化等场景,提供静态分析的数据参考。同时栈估算工具还提供递归函数告警和函数调用关系树展示功能。
栈估算工具逐行解析反汇编文件,将跳转指令转化为函数调用关系,开栈指令解析为函数栈局部空间开销,最终使用深度优先搜索,遍历任务入口函数及其调用的所有子函数,得到任务栈内存使用的峰值。
栈估算工具名为“stackusage.py”,使用工具前,可以通过“python3 stackusage.py –h”命令查看帮助信息。栈估算工具支持python3.6及以上版本。如果没有安装python3.6及以上版本,可以参见官网python安装文档完成安装。

生成反汇编文件(可选)。
如果没有反汇编文件,可以通过elf文件生成,例如有“Huawei_LiteOS.elf”文件,可以参考如下命令生成名为“Huawei_LiteOS.asm”的反汇编文件:
arm-none-eabi-objdump -d Huawei_LiteOS.elf > Huawei_LiteOS.asm修改配置文件(可选)。
统计错误分支中的异常处理方法的栈开销,有时是无意义的。因此栈估算工具支持可选地忽略部分断言函数、异常分支处理函数、打印函数等,在统计过程中会将他们的栈开销设置为0。
在修改“cfg/config.ini”文件时,将统计时需要忽略的函数,添加至“Ignore_function”行末尾,如有多个函数可以以空格隔开。工具执行时将自动解析配置文件。

-
例如反汇编文件为“Huawei_LiteOS.asm”,想要使用工具估算OsIdleTask任务的任务栈大小,可以参考如下命令运行工具:
python3 stackusage.py -sp -t OsIdleTask -out Huawei_LiteOS.asm执行上面的命令后,其输出如下图所示:

工具输出结果分为四部分:函数调用关系配置文件提示、递归函数告警、计算结果输出、调用关系树状图。调用关系树状图中,显示了任务的调用关系,以及每一层调用函数栈的最大开销和局部栈开销,并以“*Rec”标记对递归调用的函数进行提示,以“*Ignore”标记对忽略统计的函数进行提示。
标注函数指针(可选)。
工具将无法自动解析的函数调用关系,保存至“cfg/config.ini”配置文件中。以下图为例,表示在LOS_LkPrint方法和OsIdleTask任务入口函数中,存在无法自动解析的函数调用关系,需要用户手动将方法中调用的函数名添加到“Remarks_here”行末尾,如果有多个函数则以空格隔开,如果没有需要添加的函数则标注为NULL。

完成函数调用关系配置文件的编辑后,需要按步骤2重新运行工具,以获取更新后的计算结果,此时配置文件中添加的函数调用关系已添加进调用关系树中。
计算任务栈大小。
从工具输出结果中可以看到估算的任务栈大小,比如3显示的OsIdleTask任务栈为476Byte,由于设定任务栈时需要增加任务栈初始化开销和任务切换寄存器压栈开销(建议增加300Byte的开销),所以OsIdleTask的最终任务栈大小计算为776Byte。
估算中断栈(可选)。
栈估算工具无法自动识别或解析中断入口函数和中断回调函数,用户需要在“cfg/config.ini”配置文件中手动配置,配置格式如下,Interrupt_entry表示中断入口,Interrupt_isr表示中断回调函数,冒号后面需要增加中断号、中断回调函数入口。

执行下面的命令,可以进行中断栈估算:
python3 stackusage.py -irq -out Huawei_LiteOS.asm估算结果如下:

中断入口栈开销72Byte,中断号58的回调函数在所有配置的中断号里栈开销最大,为372Byte,所以总共的中断栈开销是444Byte。
由于基于静态的反汇编文件解析的函数调用关系,无法准确获取到动态赋值的函数指针取值,因此缺失函数调用关系,可能导致计算偏差。使用时请补全函数指针配置文件中的指针取值,以得到更准确的计算结果。
由于静态解析时无法确定递归函数的递归深度,工具默认以调用深度为1进行统计。如果需要设置递归深度值,可在使用工具时增加“-r”入参设置。
解析结果需要增加任务栈初始化开销和任务切换时寄存器压栈开销。这些开销一般与系统可使用的寄存器数量相关,建议增加300Byte的开销。
栈估算工具支持解析ARM指令集和RISC-V指令集的反汇编文件,不支持其他指令集。
由于中断栈随着中断嵌套的深度增加而增加,而中断嵌套又与设置的中断优先级、系统运行的实际情况有关,因此栈估算工具无法静态地估算中断嵌套情景的栈开销。栈估算工具支持用户配置多个中断回调函数入口,统计结束后会给出一个最大的中断栈开销,用户可根据实际业务可能的嵌套情况,参考最大中断栈开销去设置中断栈大小。
调试案例¶
踩内存定位方法¶
异常信息定位踩内存方法¶
通过异常信息定位问题,参见异常接管的“编程实例”。
内存合法性检查定位踩内存方法¶
通过内存合法性检查定位问题,参见“内存合法性检查”。
内存size检查定位踩内存方法¶
通过内存size检查定位问题,参见“内存size检查”。
全局变量踩内存定位方法¶
调试过程中,发现一个全局变量只在一处赋值为0,但使用时打印发现变成一个非零异常值,大概率是该全局变量被踩。
如果已知一个全局变量被踩内存,可在“LiteOS/Huawei_LiteOS/out/<platform>/.map”文件中找到该全局变量所在的地址。注意该地址前面最近被使用的变量,排查是否前面变量操作不当引发踩内存,比如对该前面变量进行memcpy,memset操作时越界,溢出覆盖了当前全局变量。
这里列举一个测试的例子,在文件中定义了两全局变量,并且初始化。
UINT32 g_uwEraseMap[16] = {0};
UINT32 g_uwEraseCount = 0;
在.map文件中可以找到这些全局变量在bss段对应的位置。

若g_uwEraseMap被踩,在“.map”文件中找到其地址,再查找该地址前面的变量,即g_uwEraseCount。特别注意分析g_uwEraseCount变量的使用情况,观察是否存在某处,对变量g_uwEraseCount进行了越界操作。
task状态判断是否踩内存¶
Shell命令“task”,可以查看当前系统所有任务的状态。命令输出的stackSize、WaterLine、StackPoint、Top0fStack信息,可以作为判断任务栈是否踩内存的指标。
这里举例说明如何通过task命令判断是否踩内存,如下图所示,有一任务名为shellTask。

StackSize = 0x3000(创建该任务时分配的栈大小)
WaterLine = 0x2810(水线,目前为止该任务栈已经被使用的内存大小)
StackPoint = 0x80d10084 (任务栈指针, 指向该任务当前的地址)
Top0fStack = 0x80d0d768(栈顶)
MaxStackPoint = Top0fStack + StackSize = 0x80d10768(得到该任务栈最大的可访问地址)
若WaterLine > StackSize,则说明该任务踩内存。
若StackPoint > MaxStackPoint 或 StackPoint < Top0fStack,则说明该任务踩内存。
Trigger定位踩内存方法¶
Trigger方法主要用于定位固定地址踩踏的问题,如全局变量,已知被踩地址,可以快速找出被谁踩。
|
addr:被保护范围的起始地址,当size为4时,addr需要4字节对齐; 当调用ArchProtectionSetAddrRo(0x100000, 4)时,被保护的地址范围为 [0x100000, 0x100003] 共4Byte。 |
||
|
index:trigger索引,取值范围为[0, 3],当index == 0xffffffff时,标识取消所有trigger保护。 |
打开菜单LOSCFG_TRIGGER_ENABLE宏。
Targets ---> Enable Access Memory trigger.使用API来保护被踩地址。
UINT32 ret = ArchProtectionSetAddrRo(0x1405ca4, 4);对地址写后会触发如下打印:

其中:
task_name:写被保护地址的任务;
mepc:写被保护地址的指令地址,可从反汇编文件中找到对应代码所在行和函数;
caller:最近一层函数调用栈;
diff_addr:被踩的地址;
new_val:被踩地址被写入的新值;
old_val:被踩地址的旧值。
当前方案基于LiteOS内核设计,仅针对RISCV核且LiteOS内核没有开启LOSCFG_ARCH_RISCV_INT_AUTO_STACK特性。方案支持的平台为LINX131、LINX132、LINX170以及LINX-1XX_TEE。
LiteOS Kernel启动流程¶
图1为系统启动流程,部分模块的初始化会在模块宏开启时才生效,用户可以选择是否创建app任务(app_init),菜单配置路径为:
Debug ---> Enable TestSuite or AppInit ---> Choose TestSuite or AppInit 菜单。
LOSCFG_APPINIT_TESTSUIT开启后可以选择创建app或testsuite任务,如果不开启,则认为app和testsuite任务都不创建。
平台兼容性适配修改要点¶
在32位与64位模式下,涉及的主要差异点在于各个不同类型的数据位宽,开发过程中需要关注,详细如下表所示:
LiteOS的64位编译器使用的是LP64指令模式,从上表可以看出,指针、long和size_t类型都升级为64bit。
在64位模式下,相比于32位模式,由于位宽的差异会导致一些使用上的差异,请参考如下7个修改要点,对应用代码做相关差异性的检查。
指针的处理。
指针不建议强转成非指针类型,如long、int等非指针数据类型。指针可以强转成其他类型的指针和void*,但64位的编译器会报出告警,需要消除掉。
须知:
也不建议指针用于数学计算时临时强转成unsigned long或int,可以转换成char* 然后再加或减常数。long都要修改为int。
除非涉及到内核公共模块,long都要修改为int,不要使用long以及相关强转。
须知:
在某些64bit编程规范中,严格禁止使用long类型。否则很容易出错。仔细检查一切有关int、long、指针的赋值和比较语句。
虽然原则上不允许使用long,但存在引用的数据类型是Kernel内部原生定义为long的情况。这样在不能修改内部原生数据定义的情况下,需要注意检查引用处。
在新的编译器中,数据类型会按照如下规则自动转换:
int + long = long
unsigned + signed = unsigned
在这种原则下,下列代码:
long long a; int b; unsigned int c; b = -2; c = 1; a = b + c;
a的值是0x00000000FFFFFFFF, 而不是-1。原因是b+c是unsigned+signed,结果是个无符号数,再转成long long,只能在前面填充0。而如下代码:
long long a; int b; unsigned int c; b = -2; c = 1; a = (long long) b + c;
a的结果就是0xFFFFFFFFFFFFFFFF,因为b转为long后就是0xFFFFFFFFFFFFFFFE,与unsigned int c相加,就是上述结果。
要在常量表达式中指定数据类型。
如果想将一个常量作为64位来使用,必须清楚地在数值后跟上ll或者LL,否则,编译器有可能把它当作int来对待。例如下面这个函数:
long long SetBitN(long long value, unsigned bitNum) { long long mask; mask = 1 << bitNum; return value | mask; }
在64位下乍一看以为没问题,但其实1被当成int型处理了, 如果bitNum为超过32的数,实际无效,超过的部分会被截断,结果就是0。必须这样写:
long long SetBitN(long long value, unsigned bitNum) { long long mask; mask = 1LL << bitNum; return value | mask; }
另外再举例,以下两种操作会导致指针的高32比特被清0。
(void*)((long long)p & ~(16 - 1)) (void*)((long long)p & ~(alain - 1)) /* alain为u32 */
可以修改成:
(void*)((long long)p & ~((long long)16 - 1)) (void*)((long long)p & ~((long long)alain - 1)) /* alain为u32 */
也可以采用如下定义规避:
1LL // (long long) 1ULL // (unsigned long long)
格式化输入输出问题及解决办法。
与32位系统相比,64位系统会有如下问题:
32位系统可以用%x打印指针,但在64位系统会有截断问题。
解决办法:指针的输入输出需要用%p。
32位系统上,使用%d来打印int或long类型的值,但是在64位平台上会截断。
解决办法:long的输入输出需要用%ld。
关于size_t类型:在32bit平台上,它的原形是unsigned int,而在64bit平台上,它的原形是unsigned long。这导致使用printf等函数时,无论使用%u或者%llu打印都会报warning。目前可采用%lu打印方式,规避该问题。
输入的时候,注意buffer的长度,在64位下发生变化了,确保buffer的长度足够:
char * buf[9]; int a = 0; Sprint(buf, "%p", &a);
上述代码在32bit模式下是正确的,一个指针的值是0xC0001A38,输入后是8个字符。在64bit模式下,长度是16个字符。
最大数、无效数的宏定义。
特别注意代码中有无引用相关最大数,无效数的宏定义,在64bit下可能已经发生变化,如:
#define BYTES_IN_WORD 4 #define INVALID_DMA_ADDRESS 0xffffffff
这些是否在64bit下还是无效或最大,需要注意,如:
size_t count = BIG_NUMBER; for (unsigned int index = 0; index != count; index++) ………
size_t是64位的,而index是32位的,所以上边的循环会一直循环下去。
检查union中成员变量大小。
union中成员变量大小要一致,32、64位差异可能会导致原union中的某些成员变量变大,各成员变量平衡被打破。检查各联合体,确保联合体内各成员变量大小一致。
如下示例是一个常见的开源代码包,可在ILP32却不可在LP64环境下运行。代码假定长度为2的unsigned short数组,占用了与long同样的空间,可这在LP64平台上却不正确。
union{ unsigned long bytes; unsigned short len[2]; } size;
正确的方法是检查是否对结构体有特殊的应用,如果有,那么需要在所有代码中仔细检查联合体,以确认所有的数据成员在LP64中都为同等长度。
数据类型方面。
如果确定涉及到64bit计算和存储则定义为long long类型。如果32bit够用的话,数据类型建议使用int或unsigned int,以减少内存。不允许定义long类型。
类型定义使用体系结构无关的类型定义:u32,u16,u64,s16,s32,s64。
结构体定义:将最常用的数据字段都定义在一起相邻位置,这样便于一次取到Cache中,提升运行性能和效率。
由于对齐方式变为8字节了,结构体的排放方式需要注意新的对齐方式。
struct MyStruct { bool m_bool; char *m_pointer; int m_int; };
如上所示,结构体bool和char*之间会填充4个字节,故需要修改结构体排布方式,以节约内存使用,如下:
struct MyStructOpt { char *m_pointer; int m_int; bool m_bool; };
关于非对齐操作
不同芯片平台对不同介质的非对齐操作支持程度不一致。在使用非对齐操作前,请确认目标平台是否支持以非对齐方式访问。
关于联合和位域的使用
C语言标准中未定义联合和位域的比特序。在编译时,联合和位域的比特序由编译器实现决定。建议在程序开发中减少联合和位域的使用。
















































