145 Star 458 Fork 134

jiangxiaogang / KLite

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
README.md 33.73 KB
一键复制 编辑 原始数据 按行查看 历史
jiangxiaogang 提交于 2023-04-08 17:19 . 更新说明文档

KLite 参考手册

更新日期: 2023.04.08

一、简介

KLite 是由个人开发者于2015年编写的嵌入式操作系统内核,并以MIT协议开放源代码。
KLite的定位是一款入门级的嵌入式实时操作系统内核,以简洁易用为设计目标,旨在降低嵌入式RTOS的入门难度。
代码干净工整、架构清晰、函数接口简单易用、不使用条件编译、移植简单、无需配置和裁减。
可能是目前最简洁易用的RTOS。


功能特性:

  • 资源占用极小(ROM:2KB,RAM:0.5KB)
  • 优先级抢占
  • 时间片抢占(相同优先级)
  • 支持创建相同优先级的线程
  • 丰富的线程通信机制
  • 动态内存管理
  • 多编译器支持(GCC,IAR,KEIL)

作者简介: 蒋晓岗,男,现居成都,毕业于成都信息工程大学,从事嵌入式软件开发十年。

二、开始使用

KLite目前支持ARM7、ARM9、Cortex-M0、Cortex-M3、Cortex-M4、Cortex-M7内核。
以上平台如:全志F1C100S、STM32FXXX、NRF528XX等,请参考相关例程。
对于Cortex-M架构的MCU,实际上只需要修改cmsis.c里面的#include,比如#include "stm32f10x.h"

由于KLite不使用条件编译,因此可以预编译kernel源码为kernel.lib,并保留kernel.h,可以有效减少重复编译的时间。

main.c的推荐写法如下:

//只需要包含这一个头文件
#include "kernel.h"

//用于初始化应用程序的线程
void init(void *arg)
{
	//在这里完成外设和驱动初始化
	//并创建更多线程实现不同的功能
	//thread_create(...)
}

//空闲线程,只需调用kernel_idle即可
void idle(void *arg)
{
	kernel_idle();
}

//C语言程序入口
void main(void)
{
	static uint8_t heap[HEAP_SIZE]; /* 定义堆内存 */
	kernel_init(heap, sizeof(heap)); /* 系统初始化 */
	thread_create(idle, 0, 0); /* 创建idle线程 */
	thread_create(init, 0, 0); /* 创建init线程 */
	kernel_start(); /* 启动系统 */
}

三、核心功能

核心功能的源码在sources/kernel/目录,是KLite最核心的部分。
使用这些功能,只需要包含头文件

#include "kernel.h"

3.1 内核管理

  • void kernel_init(void *heap_addr, uint32_t heap_size);
    参数:heap_addr 动态分配起始地址
    参数:heap_size 动态分配内存大小
    返回:无
    描述:用于内核初始化在调用内核初始化时需保证中断处于关闭状态, 此函数只能执行一次,在初始化内核之前不可调用内核其它函数。

  • void kernel_start(void);
    参数:无
    返回:无
    描述:用于启动内核,此函正常情况下不会返回,在调用之前至少要创建一个线程

  • uint32_t kernel_version(void);
    参数:无
    返回:KLite版本号,BIT[31:24]主版本号,BIT[23:16]次版本号,BIT[15:0]修订号
    描述:此函数可以获取KLite的版本号

    命名规则:
    主版本:架构调整,比较大的修改,与旧版本可能会不兼容
    次版本:功能调整,主要涉及新增或删除功能
    修订号:细节优化或BUG修复


  • void kernel_idle(void);
    参数:无
    返回:无
    描述:处理内核空闲事务,回收线程资源此函数不会返回。必须单独创建一个线程来调用。

    由用户创建空闲线程是为了实现灵活配置空闲线程的stack大小,不使用宏定义来进行stack的配置。 如果使用宏定义来配置stack大小,那么代码编译成lib之后就无法修改了,失去了灵活性。


  • uint32_t kernel_idle_time(void);
    参数:无
    返回:系统空闲时间(毫秒)
    描述:获取系统从启动到现在空闲线程占用CPU的总时间,可使用此函数和kernel_tick_count()一起计算CPU占用率

  • void kernel_tick(uint32_t time);
    参数:滴答周期(毫秒)
    返回:无
    描述:此函数不是用户API,而是由CPU的滴答时钟中断程序调用,为系统提供时钟源。
    滴答定时器的周期决定了系统计时功能的细粒度,主频较低的处理器推荐使用10ms周期,主频较高则使用1ms周期。

    在这里把滴答时间转为毫秒单位,应用层就不必使用宏定义来进行单位转换,起到简化调用的目的。
    如果硬件定时器不能产生1ms的时钟,比如RTC=32768Hz,只能产生1024Hz的中断源,周期为0.97ms,这就很尴尬!
    方案一:软件修正误差,在1024个周期内,均匀地跳过24次中断。
    方案二:设置中断周期为125ms,这是在32768Hz时钟下能得到的最小整数时间。
    方案三:放弃毫秒为时间单位,老老实实用滴答数做为时间单位。


  • uint32_t kernel_tick_count(void);
    参数:无
    返回:系统运行时间(毫秒)
    描述:此函数可以获取内核从启动到现在所运行的总时间

3.2 内存管理


  • heap_t heap_create(void *addr, uint32_t size);
    参数: addr 待创建堆内存起始地址
    size 该堆内存的长度
    返回:堆内存对象,申请失败返回NULL
    描述:用户在指定内存创建一个用于动态管理的堆内存

    为系统中不同的模块创建一个独立的堆可以提高稳定性和运行效率。


  • void *heap_alloc(heap_t heap, uint32_t size);
    参数: heap 堆内存对象
    size 要申请的内存大小
    返回:申请成功返回内存指针,申请失败返回NULL
    描述:从堆中申请一段连续的内存,功能和标准库的malloc()一样

    严格来说调用此函数应该检查返回值是否为NULL, 但每次heap_alloc都要写检查返回值的代码可能有点繁琐或者容易有遗漏的地方, 所以此函数在返回NULL之前会调用一个HOOK函数,原型void heap_fault(void); 对于嵌入式系统来说申请内存失败是严重错误,所以我们可以偷懒只在heap_fault()函数中统一处理错误。


  • void heap_free(heap_t heap, void *mem);
    参数: heap 堆内存对象
    mem 要释放的内存指针
    返回:无
    描述:释放由heap_malloc()申请的内存,功能和标准库的free()一样

  • void heap_usage(heap_t heap, uint32_t *used, uint32_t *free);
    参数:
    heap 堆内存对象
    used 输出已使用内存数量(字节),此参数可以为NULL
    free 输出空闲内存数量(字节),此参数可以为NULL
    返回:无
    描述:获取内存用量信息,可使用此函数关注系统内存消耗,以适当调整内存分配

3.3 线程管理

  • thread_t thread_create(void(entry)(void), void *arg, uint32_t stack_size);
    参数:entry 线程入口函数
    参数:arg 线程入口函数的参数
    参数:stack_size 线程的栈大小(字节),为0则使用系统默认值(1024字节)
    返回:成功返回线程句柄,失败返回NULL
    描述:创建新线程,并加入就绪队列

    系统自动为新线程分配内存和栈空间,如果栈设置太小运行过程中可能会产生栈溢出


  • void thread_delete(thread_t thread);
    参数:thread 被删除的线程标识符
    返回:无
    描述:删除线程,并释放内存该函数不能用来结束当前线程如果想要结束当前线程请使用thread_exit()或直接使用return退出主循环

    不推荐直接删除线程,可能会造成系统不稳定,因为被删除线程可能进入了临界区未释放,需考虑清楚再使用


  • void thread_set_priority(thread_t thread, uint32_t prio);
    参数:thread 线程标识符
    参数:prio 新的优先级

    THREAD_PRIORITY_HIGHEST 最高优先级
    THREAD_PRIORITY_HIGHER 更高优先级
    THREAD_PRIORITY_HIGH 高优先级
    THREAD_PRIORITY_NORMAL 默认优先级
    THREAD_PRIORITY_LOW 低优先级
    THREAD_PRIORITY_LOWER 更低优先级
    THREAD_PRIORITY_LOWEST 最低优先级
    THREAD_PRIORITY_IDLE 空闲优先级

    返回:无
    描述:重新设置线程优先级,立即生效。


  • uint32_t thread_get_priority(thread_t thread);
    参数:thread 线程标识符
    返回:目标线程的优先级
    描述:获取指定线程的优先级

  • thread_t thread_self(void);
    参数: 无
    返回: 调用线程的标识符
    描述: 用于获取当前线程标识符

  • void thread_sleep(uint32_t time);
    参数:time 休眠时间(毫秒)
    返回:无
    描述:将当前线程休眠一段时间,释放CPU控制权

  • void thread_yield(void);
    参数:无
    返回:无
    描述:使当前线程立即释放CPU控制权,并进入就绪队列

  • void thread_exit(void);
    参数:无
    返回:无
    描述:退出当前线程,此函数不会立即释放线程占用的内存,需等待系统空闲时释放

  • void thread_suspend(void);

  • void thread_resume(thread_t thread);

    由于直接挂起其它线程不安全,因此移除了这两个函数。 推荐使用信号量来阻塞线程,达到挂起线程和唤醒线程的目的。
    这也是为什么C11标准和POSIX标准都没有定义线程挂起和恢复的接口。


  • uint32_t thread_time(thread_t thread)
    参数:thread 线程标识符
    返回:指定线程运行时间(毫秒)
    描述:获取线程自创建以来所占用CPU的时间在休眠期间的时间不计算在内.

    可以使用此函数来监控某个线程的CPU占用率

3.4 互斥锁


  • mutex_t mutex_create(void)
    参数:无
    返回:成功返回互斥锁标识符,失败返回NULL
    描述:创建一个互斥锁对象,支持递归锁

  • void mutex_delete(mutex_t mutex)
    参数:mutex 被删除的互斥锁标识符
    返回:无
    描述:删除一个互斥锁,并释放内存

    注意:在删除互斥锁的时候不会释放等待这个锁的线程,因此在删除之前请确认没有线程在使用它




3.5 信号量



  • void sem_delete(sem_t sem)
    参数:sem 信号量标识符
    返回:无
    描述:删除对象,并释放内存在没有线程使用它时才能删除,否则可能产生未知异常




3.6 条件变量


  • cond_t cond_create(void);
    参数:无
    返回:成功返回条件变量标识符,失败返回NULL
    描述:创建条件变量对象

  • void cond_delete(cond_t cond);
    参数:cond 条件变量标识符
    返回:无
    描述:删除条件变量




3.7 事件



  • void event_delete(event_t event);
    参数:event 事件标识符
    返回:无
    描述:删除事件对象,并释放内存,在没有线程使用它时才能删除,否则可能产生未知异常

  • void event_set(event_t event);
    参数:event 事件标识符
    返回:无
    描述:标记事件为置位状态,并唤醒等待队列中的线程,如果auto_resettrue,那么只唤醒第1个线程,并且将事件复位, 如果auto_resetfalse,那么会唤醒所有线程,事件保持置位状态

    参考:
    WIN32:https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-setevent




四、可选功能

可选功能的源码在sources/opt/目录,每个功能对应一个c文件和h文件。
c文件是功能的具体实现,h文件是模块声明的接口。
要使用哪个功能就包含对应的头文件和源代码。

4.1 事件组

#include "event_flags.h"

  • event_flags_t event_flags_create(void);
    参数:无
    返回:创建成功返回事件标识符,失败返回NULL
    描述:创建一个事件组对象

  • void event_flags_delete(event_flags_t event);
    参数:event 事件组标识符
    返回:无
    描述:删除事件组对象,并释放内存,在没有线程使用它时才能删除,否则可能产生未知异常

  • void event_flags_set(event_flags_t event, uint32_t bits);
    参数:event 事件组标识符
    返回:无
    描述:置位bits指定的事件标志位,并唤醒等待队列中想要获取bits的线程

  • void event_flags_reset(event_flags_t event, uint32_t bits);
    参数:event 事件组标识符
    返回:无
    描述:清除bits指定的事件标志位,此函数不会唤醒任何线程

  • uint32_t event_flags_wait(event_flags_t event, uint32_t bits, uint32_t ops);
    参数:
    event 事件组标识符
    bits 想要等待的标志位
    ops 等待标志位的行为

    EVENT_FLAGS_WAIT_ANY: 只要bits中的任意一位有效,函数立即返回;
    EVENT_FLAGS_WAIT_ALL: 只有bits中的所有位都有效,函数才能返回;
    EVENT_FLAGS_AUTO_RESET: 函数返回时自动清零获取到的标志位;

    返回:实际获取到的标志位状态
    描述:等待1个或多个事件标志位


  • uint32_t event_flags_timed_wait(event_flags_t event, uint32_t bits, uint32_t ops, uint32_t timeout);
    参数:
    event 事件组标识符
    bits 想要等待的标志位
    ops 等待标志位的行为
    timeout 等待时间(毫秒)
    返回:实际获取到的标志位状态
    描述:等待1个或多个事件标志位,直到timeout设定的时间。

4.2 软定时器

#include "soft_timer.h"

  • soft_timer_t soft_timer_create(void (*handler)(void *), void *arg);
    参数:
    handler 定时器回调函数
    arg 回调函数的参数
    返回:创建成功返回软件定时器标识符,失败返回NULL
    描述:创建一个软件定时器。

  • void soft_timer_delete(soft_timer_t timer);
    参数:
    times 定时器标识符
    返回:无
    描述:删除软件定时器。

  • void soft_timer_start(soft_timer_t timer, uint32_t timeout);
    参数:
    times 定时器标识符
    返回:无
    描述:启动软件定时器。

  • void soft_timer_stop(soft_timer_t timer);
    参数:
    times 定时器标识符
    返回:无
    描述:停止软件定时器。

  • void soft_timer_service(void);
    参数:无
    返回:此函数在正常情况下不会返回。
    描述:处理软件定时器事件。

    如果要使用软件定时器功能,需要用户创建一个线程,调用这个函数,用来承载定时器的执行。
    这样做的目的是让用户控制软件定时器的线程优先级和栈空间。

4.3 块内存池

#include "mpool.h"

  • mpool_t mpool_create(uint32_t block_size, uint32_t block_count);
    参数:
    block_size 内存块大小
    block_count 内存块总数
    返回:创建成功返回标识符,失败返回NULL
    描述:创建块内存池。

  • void mpool_delete(mpool_t mpool);
    参数:
    mpool 标识符
    返回:无
    描述:删除块内存池。

  • void *mpool_alloc(mpool_t mpool);
    参数:
    mpool 标识符
    返回:申请成功返回内存指针,申请失败返回NULL
    描述:从内存池中申请一块内存。

  • void mpool_free(mpool_t mpool, void *block);
    参数:
    mpool 标识符
    block 内存块指针
    返回:无
    描述:释放内存

4.4 数据队列

#include "queue.h"

queue_t queue_create(uint32_t item_size, uint32_t queue_depth);
参数:
item_size 队列数据块大小
queue_depth 队列深度
返回:创建成功返回标识符,失败返回NULL
描述:创建一个数据队列。


void queue_delete(queue_t queue);
参数:
queue 标识符
返回:无
描述:删除数据队列。


void queue_clear(queue_t queue);
参数:
queue 标识符
返回:无
描述:清空数据队列。


bool queue_send(queue_t queue, void *item, uint32_t timeout);
参数:
queue 标识符
item 数据指针
timeout 超时时间(毫秒)
返回:成功返回true,超时返回false
描述:向队列中发送一条数据。


bool queue_recv(queue_t queue, void *item, uint32_t timeout);
参数:
queue 标识符
item 数据指针
timeout 超时时间(毫秒)
返回:成功返回true,超时返回false
描述:从队列中取出一条数据。

4.5 消息邮箱

消息邮箱按照FIFO机制取出消息。取出的消息长度和发送的消息长度一致。
如果输入的buf长度小于消息长度,则丢弃超出buf长度的部分内容。

#include "mailbox.h"

mailbox_t mailbox_create(uint32_t size);
参数:
size 缓冲区长度
返回:创建成功返回标识符,失败返回NULL
描述:创建缓冲区。


void mailbox_delete(mailbox_t mbox);
参数:
mbox 标识符
返回:无
描述:删除缓冲区。


void mailbox_clear(mailbox_t mbox);
参数:
mbox 标识符
返回:无
描述:清空缓冲区。


uint32_t mailbox_post(mailbox_t mbox, void *buf, uint32_t len, uint32_t timeout);
参数:
mbox 标识符
buf 数据指针
len 数据长度
timeout 超时时间(毫秒)
返回:实际写入数据长度
描述:向缓冲区写入指定长度的数据。


uint32_t mailbox_wait(mailbox_t mbox, void *buf, uint32_t len, uint32_t timeout);
参数:
mbox 定时器标识符
buf 数据指针
len 数据长度
timeout 超时时间(毫秒)
返回:实际读出数据长度
描述:从缓冲区中读出指定长度的数据。

五、其它函数

其它函数是与操作系统本身无关的,但在实现过程中引用的通用功能。

5.1 通用链表

#include "list.h"

5.2 通用FIFO

#include "fifo.h"

六、硬件移植

KLite核心代码完全使用C语言实现,由于不同CPU平台的差异性,有一些功能依赖于目标CPU平台,这些无法统一实现的函数,可能需要使用汇编才能实现 这些函数的实现代码放在sources/port/目录。


  • void cpu_sys_init(void);
    描述:初始化与操作系统有关的功能,在kernel_init()阶段被调用
    参数:无
    返回:无

  • void cpu_sys_start(void);
    描述:启动与操作系统有关的功能,在kernel_start()阶段被调用通常用于启动滴答时钟定时器,并打开中断滴答时钟使用硬件定时器中断,中断周期通常为1ms, 也可以为其它任意值,只需要在中断服务程序中调用一次kernel_tick(1)即可这里的参数1则代表时钟周期,如果是10ms中断一次,则应该传入10作为参数。 参数:无
    返回:无

  • void cpu_sys_sleep(uint32_t time);
    描述:实现低功耗休眠的接口,操作系统空闲时调用
    参数:time 休眠的最长时间,单位毫秒
    返回:无

  • void cpu_enter_critical(void);
    描述:实现操作系统进入临界区的接口,需要支持递归
    参数:无
    返回:无

  • void cpu_leave_critical(void);
    描述:实现操作系统退出临界区的接口,需要支持递归
    参数:无
    返回:无

  • void cpu_contex_switch(void);
    描述:实现切换线程上下文的接口,需要使用可挂起的中断来实现
    参数:无
    返回:无

  • void *cpu_contex_init(void *stack_base, void *stack_top, void *entry, void *arg, void *exit);
    描述:初始化线程栈的接口
    参数:
    stack_base 栈底指针
    stack_top 栈顶指针
    entry 线程入口函数地址
    arg 线程入口函数参数
    exit 线程出口函数地址
    返回:新的栈指针

    对于ARM处理器使用的是满递减栈,因此返回的是新的栈顶指针。

七、设计思路

核心思想是通过调度器(sched.c)维护3个TCB链表(队列):

  • 就绪链表(m_ready_list): 此版本使用了8条就绪表,每个优先级对应一个表。

    在早期的版本中,为了使代码实现简单,使用的是一条双向排序链表,高优先级在头,低优先级在尾
    但这种方案排序时间会随着优先级数量和线程数量上升,一致性较差。
    为了使KLite有更广泛的应用,因此现在替换为目前主流的优先级列表和位图搜索算法来调度。
    理论上可以支持32级优先级(就绪表),但经过反复推敲,8个优先级已经能满足绝大多数的场景了。 因此目前的设定最多8个优先级,如果想改为32级也很简单就能实现。

  • 睡眠链表(m_sleep_list): 当线程休眠时,会加入此表,在每一次滴答周期内都会检查有否有线程休眠结束,将其移到就绪表中。

  • 等待链表(tcb->list_wait): 用于线程等待对象的阻塞。

每当需要切换线程时,就从就绪表中取出优先级最高的线程,将sched_tcb_next的值修改为新线程,调用硬件接口完成上下文切换。
硬件切换接口cpu_contex_switch()挂起指定中断,在该中断服务程序中完成线程切换。
切换过程:将当前上下文保存到sched_tcb_now->sp栈中,并更新sp值,
然后从sched_tcb_next->sp中取出上下文,
最后将sched_tcb_next赋值到sched_tcb_now

八、结语

KLite并不像其它物联网操作系统一样提供一站式的全家桶服务,它仅仅是个多线程调度内核,
但我们可以灵活选择适合自己的文件系统、网络协议栈、图形界面等更丰富的功能。

如果您在使用中发现任何BUG或者有好的改进建议,欢迎加入QQ群(317930646)或发送邮件至kerndev@foxmail.com

C
1
https://gitee.com/kerndev/klite.git
git@gitee.com:kerndev/klite.git
kerndev
klite
KLite
master

搜索帮助