# bcos **Repository Path**: yuankes/bcos ## Basic Information - **Project Name**: bcos - **Description**: 一个自己开发的嵌入式操作系统 - **Primary Language**: C - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-11-07 - **Last Updated**: 2024-11-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # bcos #### 介绍 这是一个嵌入式实时操作系统,因大多数时候是周末没事的时候在被窝里开发的,故取名bcos(bed clothes os)。该操作系统支持多任务抢占式优先级调度,定时tasklet等功能。目前的硬件环境是STM32F103ZE,可以移植到大多数的ARM Cortex-M3的单片机上。开发环境使用的是keil,其他开发环境或编译器移植起来难度也不大。 本文档分为应用篇和原理篇,应用篇主要介绍bcos各功能模块的使用方法和注意事项,原理篇根据bcos的各功能模块由浅入深的讲解bcos的实现过程和原理。可以让读者深入了解嵌入式RTOS的相关知识。 ## 链表 ### 1.概述 链表是在程序设计过程中经常使用的数据结构,bcos系统内部的调度和tasklet的实现都是基于链表。所以,对链表的支持是bcos与生俱来的特性。bcos的链表设计参考了Linux内核链表的设计思想,如果用户想使用链表只需要在自己的数据结构中包含链表头,然后便可以开始使用链表来实现自己的数据结构了。 ### 2.双向循环链表 下面展示了双向循环链表的所有接口并附有简单的解释。每个接口的参数都易于理解,读者很容易理解并使用接口实现利用双向循环链表进行数据的管理。 链表头: ``` struct list_head{ struct list_head *next, *prev; }; ``` 初始化链表: ``` //静态初始化链表 #define LIST_HEAD_INIT(name) \ { \ &(name), \ &(name), \ } //定义并静态初始化链表 #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) //动态初始化链表 static inline void INIT_LIST_HEAD(struct list_head *list) ``` 链表节点的添加 ``` //将新节点添加到head后面,如果head为链表头则将新节点添加到链表的最前面 static inline void list_add(struct list_head *new_, struct list_head *head) //将新节点添加到head前面,如果head为链表头则将新节点添加到链表的最后面 static inline void list_add_tail(struct list_head *new_, struct list_head *head) ``` 链表节点的删除 ``` //将节点从链表中删除并将该节点的prev和next指针赋值为NULL static inline void list_del(struct list_head *entry) //将节点从链表中删除并将该节点的prev和next指针指向它自己 static inline void list_del_init(struct list_head *entry) ``` 链表遍历 ``` //从链表头遍历到链表尾 /** * list_for_each_entry_safe - iterate over list of given type safe against * removal of list enty. * @pos: the type * to use as a loop sursor. * @n: another type * to use as temporary storage. * @head: the head for your list. * @member: the name of the list_struct within the strut. */ #define list_for_each_entry_safe(pos, n, head, member) //从链表尾逆向遍历到链表头 /** * list_for_each_entry_safe_reverse * @pos: the type * to use as a loop cursor. * @n another type * to use as temporary storage. * @head: the head for your list. * @member: the name of the list_struct within the struct. * * Iterate backwards over list of given type, safe against removal * of list entry. */ #define list_for_each_entry_safe_reverse(pos, n, head,member) ``` 获取链表的第一个节点 ``` /** * list_first_entry - get the first element from a list. * @ptr: the list head to take the element from. * @type: the type of the struct this is embedded in. * @member: the name of the list_struct within the struct. * * Note, that list is expected to be not empty. */ #define list_first_entry(ptr, type, member) ``` 判断是否是链表最后一个节点 ``` /** * list_is_last - tests whether @list is the last entry in list @head * @list: the entry to test. * @head: the head of the list. */ static inline int list_is_last(const struct list_head *list, const struct list_head *head) ``` 判断链表是否为空 ``` /** * list_empty - tests whether a list is empty. * @head: the list to test. */ static inline int list_empty(const struct list_head *head) ``` 以链表中某个元素为依据,升序的插入节点 ``` /** * 在列表中升序插入,值更小的靠近链表头 */ #define list_ascending_order_add(new_, pos, n, head, member, condition_member) ``` ### 3.单向循环链表 单向循环链表的实现与双向循环链表类似,它的接口实现与双向循环链表也基本类似。接口如下: 链表头及初始化: ``` typedef struct slist_s { struct slist_s *next; }slist_t; //链表的静态初始化 #define SLIST_HEAD_INIT(name) {&name} //链表的动态初始化 static inline void slist_init_head(slist_t *head) ``` 链表添加一个节点 ``` //将新节点添加到head的后面 static inline void slist_add(slist_t *new_, slist_t *head) ``` 链表删除节点: ``` //仅将节点从链表中移除,需要提供将被删除节点的上一个节点 static inline void slist_del(slist_t *prev, slist_t *node) //将节点从链表中移除并初始化(将节点的next指针指向节点自己) static inline void slist_del_init(slist_t *prev, slist_t *node) ``` 判断节点是否是链表的最后一个节点 ``` /* * list:被判断的链表节点 * head:链表头 */ static inline int slist_is_last(const slist_t *list, const slist_t *head) ``` 判断链表是否为空 ``` static inline int slist_empty(const slist_t *head) ``` 对链表进行遍历 ``` #define slist_for_each_entry_safe(pos, n, head, member) ``` 单向循环链表的使用示例具体可以参考内存管理模块的实现[内存管理](https://gitee.com/wang-xiujie/bcos/wikis/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86)。 ## 内存管理 ### 1.概述 bcos的内存管理模块主要是解决用malloc和free两个库函数进行内存申请和释放的过程中内存碎片化的问题。bcos的内存管理模块用链表的方式管理内存池,会自动的将相连的内存空间组合成一整片的内存空间,尽最大的可能减少内存碎片化的问题。 ### 2.内存管理模块的接口 在使用内存管理模块前需要对内存管理模块及内存池进行初始化,另外,bcos_mem.h文件中的宏BCOS_HEAP_MEM_SIZE定义了内存池的大小,用户需要根据单片机的实际RAM大小进行设置。正常情况下系统启动后默认初始化了内存管理模块,对于某些RAM空间比较小的单片机一般不需要内存管理模块用户也可选择不移植内存管理模块。内存的申请和释放接口与malloc和free的参数和返回设计成一样的目的就是方便用户的理解和使用。 ``` void bcos_mem_init(void); ``` 申请内存: ``` void *bcos_malloc(size_t size); ``` 释放内存: ``` void bcos_free(void *memp); ``` ## 任务使用 ### 1.任务的优先级 在bcos系统中任务优先级是一个非常重要的概念,它直接影响到用户创建的任务的重要性和其被调度执行的顺序。在bcos中优先级被定义为一个8位的无符号的整数,其中在系统初始化后系统会自动创建两个任务,它们分别是空闲任务和mainloop任务。空闲任务的优先级是0xFF而mainloop任务的优先级仅仅比空闲任务的优先级高一点点是0xFE。所以,用户创建任务时优先级可以在0x00到0xFD之间任选一个就可以。当然,两个任务的优先级尽量不要相同,如果用户创建了两个优先级相同的任务也没关系系统仍然可以正常运行,只是这两个优先级相同的任务如果同时处于就绪状态时其执行顺序是不确定的。 ### 2.任务的创建 bcos任务的创建为用户准备了两个接口 ``` /* * 创建任务,需要静态定义任务控制块和栈空间 * 任务优先级可以是0x00 - 0xFD * * @param: * tcb: 任务控制块指针 * task: 任务处理函数 * p_arg: 任务函数参数 * ptos: 栈底指针 */ void bcos_task_create(bcos_tcb_t *tcb, void (*task)(void *ptr), void *p_arg, BC_OS_STK *ptos); /* * 创建任务,任务控制块和栈空间用动态分配的方式 * * @param: * task: 任务处理函数指针 * p_arg: 任务处理函数参数 * prio: 任务优先级 * task_size: 任务栈的深度 * * @return * 返回任务控制块指针,如果指针不为NULL则任务创建成功,否在任务创建失败 */ bcos_tcb_t *bcos_task_malloc(void (*task)(void *ptr), void *p_arg, uint8_t prio, uint16_t stack_size) ``` bcos_task_create这个接口是静态创建任务的接口,用户需要手动的创建任务控制块和栈空间并进行初始化。另外,对于使用了内存管理模块的用户,bcos系统提供了一种可以动态分配任务控制块和栈空间的任务创建接口,它就是bcos_task_malloc。 ### 3.任务的销毁 任务的销毁是与任务创建相逆的过程,对于不希望再被执行的任务用户有权利将其销毁。对于使用bcos_task_malloc创建的任务,任务销毁后任务控制块和栈空间会被释放到内存池中供系统再次使用,可以提高系统的性能。 接口如下: ``` /* 销毁任务,如果任务的任务控制块和栈空间是动态分配的则将这些内存空间释放 */ void bcos_task_destroy(bcos_tcb_t *tcb); ``` 无论是使用bcos_task_create还是bcos_task_malloc创建的任务,在销毁时都使用这一个接口。该接口会自动判断这个任务的任务控制块和栈空间是否是通过动态分配得到的,如果是则将其释放。 ### 4.使用示例 动态分配tcb和栈的方式创建和销毁任务 ``` bcos_tcb_t *task_led_tcb = NULL; void tasklet_handler(void *ptr); bcos_tasklet_t destroy_tasklet = BCOS_TASKLET_STATIC_INIT(destroy_tasklet, tasklet_handler, NULL); void tasklet_handler(void *ptr) { bcos_task_destroy(task_led_tcb); } void task_led(void *ptr) { uint8_t counter = 0; while(1) { led0_on(); bcos_delay_ms(200); led0_off(); bcos_delay_ms(200); counter++; if(counter > 40) { bcos_task_destroy(task_led_tcb); } } } void mainloop(void *ptr) { led_init(); SysTick_Config(SystemCoreClock/10000); task_led_tcb = bcos_task_malloc(task_led, NULL, 5, 80); //4S后用tasklet的方式销毁任务 bcos_tasklet_post(&led_tasklet, 4000); while(1) { led1_on(); bcos_delay_ms(100); led1_off(); bcos_delay_ms(100); } } ``` 静态创建tcb和栈空间的方式创建任务 ``` //定义栈空间 BC_OS_STK task_led_stk[80]; //定义tcb bcos_tcb_t task_led_tcb = BCOS_TCB_STATIC_INIT(task_led_tcb, 5); //优先级是5 void tasklet_handler(void *ptr); bcos_tasklet_t destroy_tasklet = BCOS_TASKLET_STATIC_INIT(destroy_tasklet, tasklet_handler, NULL); void tasklet_handler(void *ptr) { bcos_task_destroy(task_led_tcb); } void task_led(void *ptr) { uint8_t counter = 0; while(1) { led0_on(); bcos_delay_ms(200); led0_off(); bcos_delay_ms(200); counter++; //循环40次后销毁,这里与tasklet的销毁的区别是自己销毁自己 if(counter > 40) { bcos_task_destroy(task_led_tcb); } } } void mainloop(void *ptr) { led_init(); SysTick_Config(SystemCoreClock/10000); bcos_task_create(&task_led_tcb, task_led, NULL, &task_led_stk[80-1]); //别的任务销毁某任务 bcos_tasklet_post(&destroy_tasklet, 4000); while(1) { led1_on(); bcos_delay_ms(100); led1_off(); bcos_delay_ms(100); } } ``` ## 延时和挂起 ### 1.概述 在任务中延时和任务挂起是嵌入式操作系统中的常规需求,bcos提供了任务延时和挂起的接口。用户调用任务延时接口和任务挂起任务挂起接口后调用这两个接口的任务会被系统调度到任务等待队列中,直到任务延时超时或用户调研任务恢复函数后才会重新被添加到就绪队列中调度执行。在正常的bcos的使用过程中每一个任务至少要有1ms的延时调用,否则该任务会一直占用cpu的执行权其他任务无法被调度执行。 ### 2.任务延时、挂起、恢复接口 ``` /* * 系统延时函数,调研该函数会触发系统调度,当前任务会放弃cpu的占有权。 * 有两种情况不可以调用系统延时函数: * 1.由于tasklet的处理函数在空闲任务中执行,当tasket的处理函数被执行时 * 就绪队列中只有空闲任务。系统无法放弃空闲任务的执行权,所以在tasklet * 的处理函数中不允许调用系统延时函数。 * 2.在中断处理函数中也不允许调用系统延时函数,由于中断处理函数执行在特权模式 * 其栈指针是MSP,此时cpu不是执行在任务中。 * * @tick£:系统滴答数,正常情况下一个系统滴答1ms */ void bcos_delay_ms(BC_OS_TICK tick); /* * 任务挂起 * * 该接口以宏的形式实现 */ #define bcos_task_suspend(); /* * 任务恢复 * * 该函数有两个功能: * 1.该函数与bcos_task_suspend()操作相反,可以取消任务挂起; * 2.该函数可以取消任务延时,让任务马上恢复执行; * 该函数的执行会触发一次任务调度 * * @param * tcb:任务TCB指针 */ void bcos_task_resume(bcos_tcb_t *tcb); ``` ## Tasklet的使用 ### 0.简介 tasklet的实现主要是为了满足嵌入式系统开发过程中经常会遇到定时周期任务、延时任务等需求。在bcos系统中,tasklet在空闲任务中执行,空闲任务在系统中的优先级最低。所以,当系统中没有其他任务执行时,空闲任务会检查tasklet队列中是否有超时的任务需要执行。用户在使用tasklet时一定要注意这一点。tasklet的回调函数的执行与我们预期执行的时间的误差取决于系统中其他线程任务的繁忙程度,当然由于空闲任务的优先级最低,所以在tasklet回调函数执行的过程中可能会被其他高优先级的就绪任务抢占。 ### 1.接口 tasklet的数据结构: ``` typedef struct bcos_tasklet_s { /* 链表头 */ struct list_head list; /* 回调函数 */ void (*handler)(void *ptr); /* 任务参数 */ void *ptr; /* 任务延时间戳 */ BC_OS_TICK stamp; }bcos_tasklet_t; ``` 对数据结构静态初始化接口: ``` /* 静态初始化tasklet */ #define BCOS_TASKLET_STATIC_INIT(name, handler, ptr) \ { \ LIST_HEAD_INIT(name.list), \ handler, \ ptr, \ 0, \ } ``` 将tasklet提交给系统处理: ``` /* * 将tasklet节点添加到tasklet执行队列, 如果时立即执行的tasklet,系统会 * 尽快调度执行,如果是延时任务,系统会在超时时触发。 * * @prarm: * tasklet: tasklet节点的指针; * ms: 多少毫秒后执行处理函数; * * @return: none; */ void bcos_tasklet_post(bcos_tasklet_t *tasklet, BC_OS_TICK ms); ``` 取消tasklet任务: ``` /* 取消tasklet任务 */ void bcos_tasklet_cancel(bcos_tasklet_t *tasklet) ``` ### 2.作为一个定时任务 在嵌入式系统开发中经常会有定时任务的需求,bcos的tasklet恰恰提供了这样的功能,只需要定义一个tasklet并初始化然后在合适的时机将它提交给系统就可以了。下面的例子就是一个周期性的定时任务的实现。 示例如下: ``` //申明tasklet处理函数 void tasklet_handler(void *ptr); //创建tasklet并初始化 bcos_tasklet_t led_tasklet = BCOS_TASKLET_STATIC_INIT(led_tasklet, tasklet_handler, NULL); //tasklet处理函数的实现,主要实现一个led的翻转功能并重新提交tasklet void tasklet_handler(void *ptr) { static uint8_t flag = 0; if(flag) { flag = 0; led0_on(); } else { flag = 1; led0_off(); } //200ms后重新执行该任务 bcos_tasklet_post(&led_tasklet, 200); } //bcos系统启动后会创建并首先调度到mainloop任务 void mainloop(void *ptr) { led_init(); SysTick_Config(SystemCoreClock/10000); bcos_tasklet_post(&led_tasklet, 200); while(1) { led1_on(); bcos_delay_ms(100); led1_off(); bcos_delay_ms(100); } } ``` ### 3.作为中断处理的底半部 设备的中断会打断内核任务中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。为了在中断执行时间尽量短和完成中断到来后的任务需求之间找到平衡点bcos系统的tasklet借鉴Linux系统中中断处理的机制,为实现中断底半部提供了系统的支持。 中断处理程序被分解为两部分实现:顶半部和底半部 ![输入图片说明](https://images.gitee.com/uploads/images/2022/0411/204800_ae4da93f_9200444.png "屏幕截图.png") 由于中断的底半部处理程序执行在线程上下文,加上tasklet在系统中优先级最低,对于非常耗时的底半部处理历程来讲是可以被抢占的。这样系统中其他重要的任务也可被调度执行,这样可以大大提高系统的吞吐量。 下面以按键的消抖程序作为中断底半部实现的tasklet示例: ``` void tasklet_handler(void *ptr) { //按键的处理逻辑 } bcos_tasklet_t key_tasklet = BCOS_TASKLET_STATIC_INIT(key_tasklet, tasklet_handler, NULL); void gpio_irq_handler(void) { //GPIO的上升沿中断 if (gpio_level() == High) { //将按键tasklet从tasklet列表中删除并重新提交 bcos_tasklet_cancel(&key_tasklet); bcos_tasklet_post(&key_tasklet, 200); } } ```