第5章 其 他 组 件 5.1时间模块 时间模块是MCU的基础,也是操作系统正常运转的必备条件。通常情况下,操作系统以Tick为基本时间单位。开发者可以通过menuconfig进入配置菜单,选择Kernel→Base Config→Task→(1000)Tick Value Per Second设置每秒的Tick数。 9min 5.1.1时间转换 系统以MCU的工作时钟为基础,提供一套时钟管理单元。操作系统以Tick为基本时间单位,也称“时钟滴答”,而用户以秒、毫秒为时间单位,用户需要了解Tick与秒、毫秒之间的转换关系。 系统的最小时间单位是Cycle,主时钟频率就是每秒的Cycle数。在targets/目标板/Src/system_stm32l4xx.c文件中定义了全局变量SystemCoreClock,此变量代表系统时钟。 1. 时间转换API LiteOS时间管理单元提供了若干转换函数,可将Tick和普通时间相互转换,见表51。 表51时间管理API 函数说明(头文件kernel/include/los_tick.h,错误码也参考此文件) LOS_Tick2MS将Tick转换为毫秒 返回值: UINT32,转换结果 参数: [IN] UINT32,待转换的Tick LOS_MS2Tick延时微秒,可被高优先级任务打断 返回值: 无 参数: [IN] UINT32,延时的微秒数 LOS_Udelay延时毫秒,可被高优先级任务打断 返回值: 无 参数: [IN] UINT32 usecs,延时的毫秒数 LOS_Mdelay将毫秒转换为Tick 返回值: UINT32,转换结果 参数: [IN] UINT32 msecs,待转换的毫秒数 LOS_TickCountGet获取从系统启动开始到目前的Tick数 返回值: UINT64,返回的Tick数 参数: 无 LOS_GetCpuCycle获取从系统启动开始到目前的Cycle数 返回值: 无 参数1: [OUT] UINT32 *puwCntHi,Cycle的高32位 参数2: [OUT] UINT32 *puwCntLo,Cycle的低32位 LOS_CurrNanosec获取从系统启动开始到目前的纳秒数 返回值: UINT64,64位结果 参数1: 无 开发者在使用时间管理函数之前务必配置每秒Tick数,若不配置,则默认每秒1000Tick。 注意: 针对STM32系列的芯片,开发者需使用介于2019.10~2021.10版本的交叉编译器,否则时间单元会出错。 2. 实战案例 1) 案例测试 配置好系统时钟OS_SYS_CLOCK和每秒的Tick数,对毫秒和Tick进行相互转换,并获取系统时钟。在mydemos文件夹下创建源文件time/time.c、头文件time/time.h,代码如下: //第5章/mydemos/time/time.c UINT32 demo_time(VOID) { //获取系统时钟 printf("bus clk is %d\n", get_bus_clk() ); //获取每秒的Tick printf("1 second have %d tick\n", LOSCFG_BASE_CORE_TICK_PER_SECOND); //毫秒转Tick printf("1000 ms is %d tick\n", LOS_MS2Tick(1000)); //Tick转毫秒 printf("1000 tick is ms %d\n", LOS_Tick2MS(1000)); //获取每个Tick的Cycle printf("1 tick have %d cycle", LOS_CyclePerTickGet()); return 0; } //第5章/mydemos/time/time.h #ifndef __TIME_H #define __TIME_H #include "sys_init.h" #include "los_tick.h" #include "hisoc/clock.h" #include "menuconfig.h" UINT32 demo_time(VOID); #endif 图51运行结果 参考3.2.4节修改Makefile,将源文件路径添加到LOCAL_SRCS,将头文件路径添加到LOCAL_INCLUDE。在源文件user_task.c中调用demo_time()函数,运行结果如图51所示。 2) 结果分析 这里使用默认配置每秒1000Tick,因此得到的转换结果为1000ms==1000Tick。系统时钟为80MHz,因此1Tick==1ms==80000Cycle。 17min 5.1.2软件定时器 在开发过程中通常会使用定时器执行周期性任务,而MCU的定时器数量有限,因此LiteOS提供了软件定时器功能。 软件定时器是基于Tick中断并由软件模拟的定时功能,当经过指定的Tick之后,会触发用户设置的回调函数。 1. 运行机制 系统使用队列维护软件定时器,在定时器队列中,时间短的定时器总是排在时间长的定时器的前面,这样可使时间短的定时器优先触发。在头文件Kernel/base/include/los_swtmr_pri.h中定义了软件定时器的核心结构体LosSwtmrCB,代码如下: //第5章/kernel/base/include/los_swtmr_pri.h typedef struct { SortLinkList sortList; UINT8 state; /* 定时器状态 */ UINT8 mode; /* 定时模式,单次、周期性 */ UINT8 overrun; /* 周期性定时器的执行次数 */ UINT16 timerId; /* 定时器ID */ UINT32 interval; /* 周期性定时器的周期 (unit: tick) */ UINT32 expiry; /* 单次定时器的触发时间 (unit: tick) */ #ifdef LOSCFG_KERNEL_SMP UINT32 cpuid; /* 在多核模式下,与定时器相关的CPU */ #endif UINTPTR arg; /* 定时器回调函数的参数 */ SWTMR_PROC_FUNC handler; /* 定时器回调函数 */ } LosSwtmrCB; 软件定时器以Tick为基本单位,系统需要一个队列和一个任务资源来维护软件定时器。定时器在创建时被加入一个计时的全局链表,当Tick中断发生时,扫描该链表中是否有超时的定时器,若有定时器超时,则执行该定时器对应的回调函数。 LiteOS定时器有以下3种模式: (1) 单次触发模式1,只触发一次,之后便自动删除。 (2) 单次触发模式2,只触发一次,但是不会自动删除。 (3) 周期触发模式,周期性触发,直到用户手动关闭为止。 注意: 软件定时器的任务优先级为0,并且不可以修改。由于软件定时器使用了队列和任务等资源,因此不使用的定时器要及时删除。 2. 软件定时API LiteOS软件定时器模块为用户提供了定时器的创建、删除、启动、停止等功能,见表52。 表52软件定时器API 函数说明(头文件kernel/include/los_swtmr.h,错误码也参考此文件) LOS_SwtmrCreate创建一个软件定时器 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回错误码 参数1: [IN] UINT32 interval,定时器超时时间 参数2: [IN] UINT8 mode,定时器模式 参数3: [IN] SWTMR_PROC_FUNC handler,定时器超时回调函数 参数4: [OUT] UINT16 *swtmrId,定时器ID 参数5: [IN] UINTPTR arg,超时回调函数的参数 LOS_SwtmrDelete删除一个软件定时器 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回错误码 参数: [IN] UINT16 swtmrId,指定要删除的定时器ID LOS_SwtmrStart启动定时器 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回错误码 参数: [IN] UINT16 swtmrId,指定要启动的定时器ID LOS_SwtmrStop停止定时器 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回错误码 参数: [IN] UINT16 swtmrId,指定要停止的定时器ID LOS_SwtmrTimeGet获取定时器剩余要超时的Tick数 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回错误码 参数1: [IN] UINT16 swtmrId,指定要启动的定时器ID 参数2: [OUT] UINT32 *tick,剩余的Tick返回这里 定时器超时回调函数只能传递一个整型参数,并且没有返回值。为了避免定时器响应不及时,不建议在回调函数中执行复杂操作,或者可能导致任务阻塞的操作。 3. 实战案例 1) 案例测试 创建两个软件定时器,定时器1为单次执行,定时器2为周期性执行。在mydemos文件夹下创造源文件swt/swt.c、头文件swt/swt.h,代码如下: //第5章/mydemos/swt/swt.c #include "swt.h" UINT16 swt1_id; UINT16 swt2_id; UINT16 swt2_cnt=0; //定时器1回调函数 void timer1_callback(UINT32 arg){ UINT32 tk; printf("timer1 timeout\n"); //获取timer2的剩余超时 LOS_SwtmrTimeGet(swt2_id, &tk); printf("timer2 still need %d tick overflow\n", tk); } //定时器2回调函数 void timer2_callback(UINT32 arg){ swt2_cnt++; printf("timer2 timeout\n"); //获取自系统启动经过的tick,注意这里是64位整数,需要使用%lld printf("system has pass %lld tick\n", LOS_TickCountGet()); if(swt2_cnt==arg){ printf("timer2 run %d times, stop timer2\n", swt2_cnt); LOS_SwtmrStop(swt2_id); } } UINT32 demo_swt(VOID){ //单次定时,超时之后会自动删除 LOS_SwtmrCreate(1000, LOS_SWTMR_MODE_ONCE, \ (SWTMR_PROC_FUNC)timer1_callback, &swt1_id, 1); //周期定时,除非用户手动停止并删除,否则持续执行 LOS_SwtmrCreate(3000, LOS_SWTMR_MODE_PERIOD, \ (SWTMR_PROC_FUNC)timer2_callback, &swt2_id, 3); //启动定时器 LOS_SwtmrStart(swt1_id); LOS_SwtmrStart(swt2_id); return 0; } 图52运行结果 参考3.2.4节修改Makefile,将源文件添加到变量LOCAL_SRCS,将头文件路径添加到变量LOCAL_INCLUDE。在源文件user_task.c中调用函数demo_swt(),运行结果如图52所示。 2) 结果分析 定时器1设置的超时为1000,定时器2设置的超时为3000。从结果看到当定时器1超时之后,定时器2还需要1999个tick才能超时,可见软件定时器在时间上误差很小。由于创建定时器2时为回调函数传入了整型数据3,因此可在其回调函数中通过判断入参停止该定时器。 13min 5.2原子操作和位操作 5.2.1计算机中的原子 1. 基本概念 原子即不可分割的最小单位,计算机中的原子操作是指某些指令在执行过程中不被打断,保证数据的可靠性。系统在修改内存数据时,要经过“读取→修改→写入”3个步骤,在多任务系统中此过程可能被打断,因此需要原子操作保证数据的可靠性。 2. 运行机制 由于在芯片中寄存器的速度要快于内存的速度,因此如非必要编译器会选择从寄存器读取数据。从寄存器中读取的数据有可能不是最新的数据,即可能为脏数据。 1) 从C到汇编 一句C代码往往会被编译为多句汇编代码,例如一个简单的赋值语句对应的汇编代码如下: //第5章/代码片段1 //c代码 int a,b; a = 1; b = a; //第5章/汇编代码,变量b的内容每次从寄存器获得 第1行 .word 20000450   ;变量a的地址 第2行 .word 20000454   ;变量b的地址 第3行 mov r0, #20000450  ;变量a的地址→r0 第4行 ldr r0, [r0, #0]   ;变量a的内容→r0 第5行 mov r1, r0   ;r0→r1,即a→r1 第6行 mov r2, #20000454  ;变量b的地址→r2 第7行 str r1, [r2, #0]   ; r1→变量b,即变量a→变量b 在上述代码片段1中,如果汇编的第4行到第5行之间发生中断并修改变量a的值,则地址20000450中的内容被修改,而第5行r1获取的仍然是地址20000450的旧值。 2) volatile关键字 LiteOS中定义的Atomic类型为volatile int,即在整数前增加了volatile关键字。被volatile修饰的变量意在告诉编译器此变量容易被修改,读取时应该从内存中重新加载,因此上述C代码对应的汇编代码如下: //第5章/代码片段2,变量b的内容每次从内存地址获取 第1行 .word 20000450   ;变量a的地址 第2行 .word 20000454   ;变量b的地址 第3行 mov r0, #20000450  ;变量a的地址→r0 第4行 ldr r0, [r0, #0]   ;变量a的内容→r0 第5行 mov r2, #20000454  ;变量b的地址→r2 第6行 str r0, [r2, #0]   ;变量a→变量b 在上述代码片段2中,变量b每次都从地址20000450读取变量a的值,因此得到的值永远是最新的数据。 3) LDREX、STREX指令 在C代码中,加法操作a=a+1要经历3个过程: 从内存取变量a,将变量a加1,将变量a存储到内存。如果在执行加法操作过程中有中断程序修改变量a的值,则结果就是未知的。ARMv6架构中引入了LDREX(读取内存)、STREX(将数据保存到内存)指令,这两条指令在操作内存时会设置内存独占标记,由此保证了内存操作的原子性。 注意: ARMv6之前的架构中没有LDREX、STREX指令,因此只能通过开关中断实现加减法的原子操作。 3. 原子操作API LiteOS原子操作目前仅支持整型数据,开发者可以对整型数据进行读、写、加、减等操作,见表53。 表53原子操作API 函数说明(头文件kernel/include/los_atomic.h) LOS_AtomicRead读取一个变量的内容,其本质就是读取被volatile修饰的变量,这意味着每次读取都要从内存获取,而非从寄存器获取 返回值: INT32,读到的数据 参数1: [IN] const Atomic *v,要读取的变量的地址 LOS_AtomicSet设置一个变量 返回值: INT32,设置的结果 参数1: [IN] Atomic *v,变量的地址 参数2: [IN] INT32 setVal,要设置的值 LOS_AtomicAdd对变量做加法,保证原子操作。通过LDREX、STREX指令实现 返回值: INT32,加法的结果 参数1: [IN] Atomic *v,变量的地址 参数2: [IN] INT32 setVal,要增加的值 LOS_AtomicSub对变量做减法,保证原子操作。通过LDREX、STREX指令实现 返回值: INT32,减法的结果 参数1: [IN] Atomic *v,变量的地址 参数2: [IN] INT32 setVal,要减去的值 LOS_AtomicInc对变量加1,保证原子操作。通过LDREX、STREX指令实现 返回值: 无 参数1: [IN] Atomic *v,变量的地址 LOS_AtomicDec对变量减1,保证原子操作。通过LDREX、STREX指令实现 返回值: 无 参数1: [IN] Atomic *v,变量的地址 此外,LiteOS还支持对64位整数进行原子操作,其API为LOS_Atomic64Read()、LOS_Atomic64Set()、LOS_Atomic64Add()、LOS_Atomic64Sub()等。 注意: CortexM0、CortexM1属于ARMv6M架构,CortexM3、CortexM4属于ARMv7M架构,这两种架构不支持LDREX指令,因此可使用开关中断实现变量加减法的原子操作。 4. 实战案例: 从汇编看原子操作 1) 案例测试 (1) 创建两个任务,任务1操作普通变量,任务2操作Atomic类型变量,通过汇编文件查看两个任务的不同之处。在mydemos文件夹下创建源文件atomic/atomic.c、头文件atomic/atomic.h,代码如下: //第5章/mydemos/atomic/atomic.c #include "atomic.h" //普通变量 int count_normal = 1; //Atomic类型变量,其实就是volatile int Atomic count_atomic = 1; //任务id UINT32 tid1; UINT32 tid2; UINT32 normal_int(VOID) { //变量每次都是1,编译器会进行优化操作,从寄存器读取变量值 while(count_normal) { ; } return LOS_OK; } UINT32 atomic_int(VOID) { //变量被volatile修饰,每次读取时都会从内存获取 while(count_atomic) { ; } return LOS_OK; } UINT32 my_task(UINT32 tid, char *name, UINT16 pri, UINT32 stack_size, TSK_ENTRY_FUNC func) { //参考4.1.4节 ... } UINT32 demo_atomic() { UINT32 ret; //创建任务 ret = my_task(&tid1, "read mem", 9, 0x800, normal_int); if(ret != LOS_OK) {printf("create normal task failed\n");return -1;} ret = my_task(&tid2, "write mem", 9, 0x800, atomic_int); if(ret != LOS_OK) {printf("create atomic task failed\n");return -1;} return ret; } (2) 参考3.2.4节修改Makefile,将源文件添加到LOCAL_SRCS,将头文件路径添加到LOCAL_INCLUDE。在源文件user_task.c中调用demo_atomic()函数,编译之后打开文件out/目标板/Huawei_LiteOS.asm,部分代码如下: ; 第5章/ Huawei_LiteOS.asm 0800b1a0 : 800b1a0: b480 push {r7} 800b1a2: 4b04 ldr r3, [pc, #16]  ; (800b1b4 ) 800b1a4: 6818 ldr r0, [r3, #0] 800b1a6: af00 add r7, sp, #0 800b1a8: 2800 cmp r0, #0 800b1aa: d1fd bne.n 800b1a8 800b1ac: 46bd mov sp, r7 800b1ae: f85d 7b04 ldr.w r7, [sp], #4 800b1b2: 4770 bx lr 800b1b4: 20000450 .word 0x20000450 0800b1b8 : 800b1b8: b480 push {r7} 800b1ba: 4b04 ldr r3, [pc, #16]  ; (800b1cc ) 800b1bc: af00 add r7, sp, #0 800b1be: 6818 ldr r0, [r3, #0] 800b1c0: 2800 cmp r0, #0 800b1c2: d1fc bne.n 800b1be 800b1c4: 46bd mov sp, r7 800b1c6: f85d 7b04 ldr.w r7, [sp], #4 800b1ca: 4770 bx lr 800b1cc: 2000044c .word 0x2000044c 2) 结果分析 从Huawei_LiteOS.asm汇编代码看到,函数normal_int()中首先将变量加载到寄存器r0,之后while循环使用cmp指令对比r0和0,如果非0,则跳转到地址800b1a8,接着重复调用cmp指令比较r0和0。函数Atomic_int()中首先将变量加载到寄存器r0,之后while循环使用cmp指令对比r0和0,如果非0,则跳转到地址800b1be,接着重新将变量的值加载到寄存器r0,然后调用cmp指令比较r0和0。得出结论,使用volatile修饰的变量,每次读取时都会从内存中将数据提取到寄存器,然后读取寄存器的值,这样保证读到的变量值不是脏数据。 11min 5.2.2位操作 位操作就是对二进制数中的某一位进行操作,C语言中使用符号“&”“|”“~”对数据进行位操作,它们分别代表按位与、按位或、按位取反。LiteOS提供了几个位操作的API,见表54,这些API的本质仍然是使用了C语言中的3种位操作符号。 表54位操作API 函数说明(头文件kernel/include/los_bitmap.h) LOS_BitmapSet将变量的某一位置1 返回值: 无 参数1: [IN] UINT32 *bitmap,变量的地址 参数2: [IN] UINT16 pos,要置1的位置 LOS_BitmapClr将变量的某一位清零 返回值: 无 参数1: [IN] UINT32 *bitmap,变量的地址 参数2: [IN] UINT16 pos,要清零的位置 LOS_LowBitGet找到二进制数值为1的最低一位并返回位索引 返回值: UINT16,索引值 参数1: [IN] UINT32 *bitmap,变量值 LOS_HighBitGet找到二进制数值为1的最高一位并返回位索引 返回值: UINT16,索引值 参数1: [IN] UINT32 *bitmap,变量值 19min 5.3双向循环链表 链表是一种常见的非顺序存储结构,它是操作系统中非常重要的一种数据结构。链表可分为单向链表、双向链表,又可分为循环链表、非循环链表,操作系统中常见的是双向循环链表。 5.3.1工作原理 链表由若干节点连接而成,每个节点的内部都有一个指针指向下一个节点,由此形成一个链式存储结构。双向链表的节点中不仅包含指向下一个节点的指针,还包含指向上一个节点的指针,通常将这两个指针称为“后继”和“前驱”。双向循环链表将链表的头部和尾部连接在一起,如图53所示。 图53双向链表 LiteOS双向链表结构定义在头文件kernel/include/los_list.h中,通过一系列宏定义向外提供了各种链表操作接口。开发者只要引用该头文件即可完成初始化链表、增加节点、删除节点、遍历链表等操作。其数据结构的代码如下: //第5章/kernel/include/los_list.h //链表结构 typedef struct LOS_DL_LIST { struct LOS_DL_LIST *pstPrev; //前驱 struct LOS_DL_LIST *pstNext; //后继 } LOS_DL_LIST; 1. 初始化链表 链表在使用前必须初始化头指针,此时链表中没有任何有用的数据节点,因此只需将头指针的前驱和后继都指向自己,代码如下: //第5章/kernel/include/los_list.h VOID LOS_ListInit(LOS_DL_LIST* list) { list->pstNext = list; list->pstPrev = list; } 开发者在使用时,只需先引入头文件los_list.h,然后调用函数LOS_ListInit(),这里需要传入的参数是一个指针。 2. 增加节点 向链表增加节点就会打断原来结构中的两条存储链,进而构造4条新的存储链,如图54所示。 图54新增节点 使用函数LOS_ListAdd(LOS_DL_LIST *list, LOS_DL_LIST *node)向链表list中添加新节点,其代码如下: //第5章/kernel/include/los_list.h //参数list为链表头,参数node为新增节点 VOID LOS_ListAdd(LOS_DL_LIST* list, LOS_DL_LIST* node) { node->pstNext = list->pstNext; //对应图54中的第④条链 node->pstPrev = list; //对应图54中的第①条链 list->pstNext->pstPrev = node; //对应图54中的第③条链 list->pstNext = node; //对应图54中的第②条链 } 从上述代码分析可知,函数LOS_ListAdd()是在链表的头部插入节点。在los_list.h文件中还提供了向尾部插入节点的函数LOS_ListTailInsert(),代码如下: //第5章/kernel/include/los_list.h LOS_ListTailInsert(LOS_DL_LIST* list, LOS_DL_LIST* node) { LOS_ListAdd(list->pstPrev, node); } 3. 删除节点 删除一个节点需要将其两侧的连接都打断,将该节点的前驱和后继直接连接起来,如图55所示。 图55删除节点 函数LOS_ListDelete(LOS_DL_LIST* node)可删除指定节点,只要将参数设为要删除的节点即可,代码如下: //第5章/kernel/include/los_list.h VOID LOS_ListDelete(LOS_DL_LIST* node) { node->pstNext->pstPrev = node->pstPrev; //对应图55中的第①条链 node->pstPrev->pstNext = node->pstNext; //对应图55中的第②条链 node->pstNext = NULL; //将删除的节点去除与链表的关联 node->pstPrev = NULL; } 4. 遍历链表 遍历链表就是对链表中所有节点逐次访问,链表不同于数组,无法使用下标直接访问。由于链表相邻节点之间是关联的,因此只要找到链表中任何一个节点就可完成链表的遍历。 LOS_DL_LIST结构中并不包含数据域,而开发者使用的链表结构则必定包含数据域,代码如下: typedef struct USER_DATA{ uint8_t mem1; uint8_t mem2; LOS_DL_LIST m_list; }USER_DATA; 假设一个链表中有若干USER_DATA节点data_1,data_2,…,data_n,则可通过其成员变量m_list完成链表的遍历。如果给出头节点head,则利用for循环可找到每个节点的m_list成员,代码如下: for(item = head->pstNext; //第1个m_list item != head; //结束条件 item = item->next) //下一个m_list 上述for循环只是找到了USER_DATA的m_list成员,而实际需要的则是USER_DATA这个节点本身,los_list.h提供了一种通过结构体成员找到整个结构体的方法,代码如下: //第5章/kernel/include/los_list.h #define LOS_DL_LIST_ENTRY(item, type, member) \ ((type*)(VOID*)((CHAR*)(item)-LOS_OFF_SET_OF(type, member))) //LOS_OFF_SET_OF(type, member)展开为((type *)0)->member //(type *)0将0地址强制转换为结构体地址,取其成员变量自然就可得到成员的偏移地址 LOS_DL_LIST_ENTRY的原理是通过某个成员的偏移地址找到整个结构体的地址,其中item是成员地址,type是结构体类型,member是成员名字。假设链表头是head,则返回节点data_1的代码如下: //通过成员变量返回结构体首地址 LOS_DL_LIST_ENTRY(head->pstNext, //第1个m_list USER_DATA, //结构体类型 m_list) //成员变量的名字 los_list.h提供了遍历链表的宏定义,其原理就是利用LOS_DL_LIST_ENTRY和for循环找到链表中的每个用户结构,代码如下: //第5章/kernel/include/los_list.h //item,遍历到的每个用户结构 //list,链表头 //用户结构体类型 //member,用户结构中的LOS_DL_LIST类型成员名称 #define LOS_DL_LIST_FOR_EACH_ENTRY(item, list, type, member) \ for(item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member); \ &(item)->member != (list); \ item = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member)) 如果要遍历USER_DATA链表中的每个节点,则代码如下: LOS_DL_LIST_FOR_EACH_ENTRY(item, &data_head, USER_DATA, m_list) 注意: los_list.h文件中的链表操作与平台和系统无关,也就是说开发者可将此头文件复制到其他C或C++项目中使用。 5.3.2实战案例: 学生管理系统 1. 案例描述 模拟一个简单的学生管理系统,可实现信息的增、删、改、查。本案例旨在练习链表操作,因此不涉及文件存储和用户交互,所有信息都在内存中。 2. 操作流程 1) 数据结构 在头文件mydemos/list/list.h中定义负责管理信息的结构类型,在结构体中存储两门功课的成绩和学生姓名,代码如下: //第5章 mydemos/list/list.h #ifndef __MYLIST_H #define __MYLIST_H #include "sys_init.h" #include "los_list.h" typedef struct STU_DATA{ LOS_DL_LIST s_list; uint8_t chinese; uint8_t math; uint8_t *name; }STU_DATA; VOID demo_list(VOID); #endif 2) 初始化数据 在源文件mydemos/list/list.c中定义全局链表头stu_head,并初始化链表,代码如下: //第5章 mydemos/list/list.c LOS_DL_LIST stu_head; static VOID stu_init(){ printf("init stu list\n"); LOS_ListInit(&stu_head); } VOID demo_list(VOID){ stu_init(); } 3) 插入数据 向链表中插入5个学生节点,每次都从尾部插入,代码如下: //第5章 mydemos/list/list.c static VOID stu_insert(uint8_t *name, uint8_t ch, uint8_t math){ STU_DATA *tmp; printf("inser %s chinese %d math %d ...", name, ch, math); //申请内存 tmp = (STU_DATA *)malloc(sizeof(STU_DATA)); if(tmp == NULL){ printf("failed\n"); return; } tmp->chinese = ch; tmp->math = math; //申请内存 tmp->name = (uint8_t *)malloc(sizeof(name)); if(tmp->name==NULL){ printf("failed\n"); return; } //复制内容 memcpy(tmp->name, name, sizeof(name)); //插入节点 LOS_ListHeadInsert(&stu_head, &(tmp->s_list)); printf("ok\n"); } VOID demo_list(VOID){ stu_init(); stu_insert("David", 80, 92); stu_insert("Mars", 82, 94); stu_insert("Alex", 84, 96); stu_insert("Tom", 86, 97); stu_insert("Jerry", 88, 98); } 4) 查找数据 查找过程应该遍历整个链表,因为满足条件的数据未必只有一条,代码如下: //第5章 mydemos/list/list.c static VOID stu_query_by_name(uint8_t *name){ STU_DATA *tmp; printf("query %s ...\n", name); //遍历链表 LOS_DL_LIST_FOR_EACH_ENTRY(tmp, &stu_head, STU_DATA, s_list){ //比较每个节点的name与入参是否相等 if(strcmp(name, tmp->name)==0){ printf("Chinese %d, Maths %d\n", tmp->chinese, tmp->math); } } printf("query %s over\n", name); } VOID demo_list(VOID){ ... stu_query_by_name("Tom"); } 5) 修改数据 修改分为两个过程,首先要找到满足条件的成员结构,然后修改结构中的其他数据,代码如下: //第5章 mydemos/list/list.c static VOID stu_modify_math(uint8_t *name, uint8_t math){ STU_DATA *tmp; printf("modify %s math score...\n", name); //遍历链表 LOS_DL_LIST_FOR_EACH_ENTRY(tmp, &stu_head, STU_DATA, s_list){ //比较每个节点的name与入参是否相等 if(strcmp(name, tmp->name)==0){ //更改数据 tmp->math = math; } } //重新查询,验证是否修改 stu_query_by_name(name); } VOID demo_list(VOID){ ... stu_modify_math("Tom", 90); } 6) 删除数据 删除某个成员信息,如果在创建成员时使用函数malloc()分配内存,则删除成员后需要使用函数free()释放内存,代码如下: //第5章 mydemos/list/list.c static VOID stu_delete_by_name(uint8_t *name){ STU_DATA *tmp, *node; printf("delete %s ...\n", name); //遍历链表 LOS_DL_LIST_FOR_EACH_ENTRY(tmp, &stu_head, STU_DATA, s_list){ //比较每个节点的name与入参是否相等 if(strcmp(name, tmp->name)==0){ //找到当前节点的前驱 node = LOS_DL_LIST_ENTRY(tmp->s_list.pstPrev, STU_DATA, s_list); //删除节点 LOS_ListDelete(&(tmp->s_list)); //释放内存 free(tmp->name); free(tmp); //由于当前节点被删除,因此tmp要重新赋值 //这样tmp->s_list.pstNext才有效,for循环才可继续执行 tmp = node; } } stu_query_by_name(name); } VOID demo_list(VOID){ ... stu_delete_by_name("Jerry"); } 3. 编译运行 将文件list.c和list.h的相对路径添加到文件targets/STM32L431_BearPi/Makefile中,具体参考3.2.4节。编译无误后运行结果如图56所示。 图56运行结果 5.4程序员利器Git 一个成熟的项目必定要经过各种版本的迭代,代码的版本控制曾是一个令无数程序员头疼的问题,GitHub平台成功解决了代码的版本控制问题。此外,很多大型项目由专人维护,开发者可以在项目的GitHub仓库中找到一些问题的解决方案。 5.4.1Git工具 GitHub是一个基于Git的分布式版本控制系统,Git最初是由Linux开发者Linus Torvalds开发的。尽管2013年国内解除了对GitHub的封锁,但由于要访问境外服务器,GitHub访问速度依然较慢,笔者推荐初学者使用国内代码托管平台Gitee。 Gitee与GitHub都基于Git工具,因此代码托管的首要任务是安装Git。 1. 安装Git Windows平台下可在Git官网(https://gitscm.com/download/win)下载Git工具,下载后双击安装文件即可安装。Linux/macOS平台下使用命令安装Git,命令如下: #Ubuntu系统安装Git sudo apt install git #macOS系统安装Git brew install git 图57打开Git命令行 2. 基础配置 无论将代码托管到GitHub还是Gitee,都需要在对应官网注册账号,并在自己的计算机中配置用户名和邮箱。尽管在Windows系统下可使用图形界面操作Git,笔者还是推荐使用命令行操作。在Windows系统中安装好Git工具之后,右击即可打开Git命令行,如图57所示。 打开命令行,配置注册过的用户名和邮箱地址,命令如下: git config -global user.name "weijie" git config -global user.email "xxx.com" 5.4.2代码管理 Git版本控制以仓库为基本单元,通常一个项目即一个仓库。用户在代码管理平台创建远程仓库,在PC端创建本地仓库,之后就可将本地项目代码上传到远程仓库。 1. 创建远程仓库 (1) 登录Gitee平台,进入个人主页后单击右上角的“+”创建一个仓库,如图58所示。 图58创建仓库 (2) 输入仓库信息后,单击“初始化readme文件”按钮,如图59所示。 图59初始化readme文件 2. 创建本地仓库 (1) 进入Test项目根目录下,打开Git命令行,为该项目初始化一个本地仓库,命令如下: #初始化Git环境 git init #添加远程仓库地址 #HTTPS地址参考图5-9 #wj-test是为远程仓库起的别名,用户可自己设置 git remote add wj-test https://gitee.com/wei-jie/test.git (2) 将远程仓库同步到本地,命令如下: git pull wj-test master (3) Windows环境下,每次拉取远程仓库都需要输入用户和密码,开发者可在Git命令行中做简单配置,只需输入一次用户名和密码。配置命令如下: git config -global credential.helper store 3. 同步代码 (1) 将文件添加到仓库,命令如下: #add就是添加 #点表示当前目录下的所有文件 #用户也可添加指定文件,例如"git add 1.c src/2.c" git add . 图510代码提交结果 (2) 提交本次修改,命令如下: #-m后的参数是为本次提交设置一个标记,通常以 #时间戳为标记 git commit -m "202212101900" (3) 将本地仓库同步到远程,命令如下: #将本地仓库上传到远程 git push wj-test master (4) 登录Gitee平台,进入Test仓库查看结果,如图510所示。 注意: 初始化仓库只需一次,以后每次提交代码都从指令git add开始。如果提交代码时使用SSH协议,则需要为计算机配置SSH证书,其他操作和HTTPS协议相同。 5.5本章小结 本章介绍了LiteOS时间管理、原子操作、位操作。时间管理模块严重依赖编译器,若在开发过程中时间转换失败,则可检查编译器版本是否满足要求。原子操作依赖处理器架构,不同的架构实现原子操作的方式不同。Git工具不仅可以对代码进行远程托管,还可进行团队协作,开发者应该熟练掌握Git工具的基本操作。