目录
前言
快速入门
工程说明
调试命令
系统启动步骤
用户入口代码
内核基础
线程调度
时钟管理
线程间同步
线程间通信
内存管理
I/O设备管理
程序内存分布
自动初始化机制
内核对象模型
静态与动态对象
内核对象管理架构
遍历内核对象
内核配置与裁剪
第8章 线程的定义与线程的切换
定义线程栈:
rt_uint8_t
ALIGN
RT_ALIGN_SIZE
链表
线程栈初始化:rt_hw_stack_init() 函数
将线程插入到双向就绪列表
实现调度器
系统调度
main函数
第9章 临界段的保护
第10章 对象容器的实现
新手入门官方文档:
Keil模拟器STM32F103 (rt-thread.org)https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/tutorial/quick-start/stm32f103-simulator/stm32f103-simulator官方API参考手册:
RT-Thread API参考手册: event_sample.chttps://www.rt-thread.org/document/api/event_sample_8c-example.html#a10 以下内容是随着学习随时整理的,有些来自于野火出的《RT-Thread内核实现与应用开发》的学习笔记,有些来自官网的东西,自认为难以记忆与重点的就记下来。
大家可以浏览认识,至于大家想深入了解希望能够自己去看相关文档。
可以去官方有相关文档说明
首先将KEIL下载下来,官网上有相关例程
打开调试公共点击运行(F5)代码,打开串口UART#1,点击Tab键或者help+回车,查看系统支持的命令。
以 MDK-ARM 为例,MDK-ARM 的用户程序入口为 main() 函数,位于 main.c 文件中。系统启动后先从汇编代码 startup_stm32f103xe.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统功能初始化,最后进入用户程序入口 main()。
如果使用到了ARM内核则重新定义启动当时,最终跳到了rtthread_startup(); 是RT-Thread规定的统一启动入口。 启动程序函数初始化如下图所示:
启动代码分为四部分:
为了在进入 main 程序之前,完成系统功能初始化,可以使用 $sub$$
和 $super$$
函数标识符在进入主程序之前调用另外一个例程,这样可以让用户不用去管 main() 之前的系统初始化操作。
内核的组成部分、系统如何启动、内存分布情况以及内核配置方法。
实时内核的实现包括:对象管理、线程管理及调度器、线程间通信管理、时钟管理及内存管理等等,内核最小的资源占用情况是 3KB ROM,1.2KB RAM。
线程是 RT-Thread 操作系统中最小的调度单位,线程调度算法是基于优先级的全抢占式多线程调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
RT-Thread 的时钟管理以时钟节拍为基础,时钟节拍(滴答始终)是 RT-Thread 操作系统中最小的时钟单位。RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止定时器否则将永远持续执行下去。
另外,根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以设置为 HARD_TIMER 模式或者 SOFT_TIMER 模式。
通常使用定时器定时回调函数(即超时函数),完成定时服务。用户根据自己对定时处理的实时性要求选择合适类型的定时器。
RT-Thread 采用信号量、互斥量与事件集实现线程间同步。线程通过对信号量、互斥量的获取与释放进行同步;互斥量采用优先级继承的方式解决了实时系统常见的优先级翻转问题,信号量会导致线程阻塞即当低优先级持有信号量时,高优先级无法获取信号量。
RT-Thread 支持邮箱和消息队列等通信机制。邮箱中一封邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。邮箱效率较消息队列更为高效。邮箱和消息队列的发送动作可安全用于中断服务例程中。通信机制支持线程按优先级等待或按先进先出方式获取。
RT-Thread 支持静态内存池管理及动态内存堆管理。当静态内存池具有可用内存时,系统对内存块分配的时间将是恒定的;当静态内存池为空时,系统将申请内存块的线程挂起或阻塞掉 (即线程等待一段时间后仍未获得内存块就放弃申请并返回,或者立刻返回。等待的时间取决于申请内存块时设置的等待时间参数),当其他线程释放内存块到内存池时,如果有挂起的待分配内存块的线程存在的话,则系统会将这个线程唤醒。
动态内存堆管理模块在系统资源不同的情况下,分别提供了面向小内存系统的内存管理算法及面向大内存系统的 SLAB 内存管理算法。
还有一种动态内存堆管理叫做 memheap,适用于系统含有多个地址且不连续的内存堆。使用 memheap 可以将多个内存堆 “粘贴” 在一起,让用户操作起来像是在操作一个内存堆。
RT-Thread 将 PIN、I2C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。实现了按名称访问的设备管理子系统,可按照统一的 API 界面访问硬件设备。在设备驱动接口上,根据嵌入式系统的特点,对不同的设备可以挂接相应的事件。当设备事件触发时,由驱动程序通知给上层的应用程序。
一般MCU包含的存储空间有:片内Flash(硬盘)与片内RAM(内存)。编译器会将一个程序分类为好几个部分,分别存储在 MCU 不同的存储区。
Keil工程在编译完成后,会相应的程序提示占用空间
编译完后工程会生成一个.map文件,说明了各个函数占用的吃尺寸和地址,在文件最后几行与场面的关系。
STM32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段,并将这块 RAM 区域清零。
动态内存的申请:msg_ptr 指针指向的 128 字节内存空间位于动态内存堆空间中。
而一些全局变量则是存放于 RW 段和 ZI 段中,RW 段存放的是具有初始值的全局变量(而常量形式的全局变量则放置在 RO 段中,是只读属性的),ZI 段存放的系统未初始化的全局变量。
自动初始化机制是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。
静态内核对象通常放在 RW 段和 ZI 段中,在系统启动后在程序中初始化;
动态内核对象则是从内存堆中创建的,而后手工做初始化,最后需要释放。
静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。这两种方式各有利弊,可以根据实际环境需求选择具体使用方式。
静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。这两种方式各有利弊,可以根据实际环境需求选择具体使用方式。
对象控制块
struct rt_object
{
/* 内核对象名称 */
char name[RT_NAME_MAX];
/* 内核对象类型 */
rt_uint8_t type;
/* 内核对象的参数 */
rt_uint8_t flag;
/* 内核对象管理链表 */
rt_list_t list;
};
struct rt_object_information
{
/* 对象类型 */
enum rt_object_class_type type;
/* 对象链表 */
rt_list_t object_list;
/* 对象大小 */
rt_size_t object_size;
};
rt_thread_t thread = RT_NULL;
struct rt_list_node *node = RT_NULL;
struct rt_object_information *information = RT_NULL;
information = rt_object_get_information(RT_Object_Class_Thread);
rt_list_for_each(node, &(information->object_list))
{
thread = (rt_thread_t)rt_list_entry(node, struct rt_object, list);
/* 比如打印所有thread的名字 */
rt_kprintf("name:%s\n", thread->name);
}
rt_mutex_t mutex = RT_NULL;
struct rt_list_node *node = RT_NULL;
struct rt_object_information *information = RT_NULL;
information = rt_object_get_information(RT_Object_Class_Mutex);
rt_list_for_each(node, &(information->object_list))
{
mutex = (rt_mutex_t)rt_list_entry(node, struct rt_object, list);
/* 比如打印所有mutex的名字 */
rt_kprintf("name:%s\n", mutex->parent.parent.name);
}
配置主要是通过修改工程目录下的 rtconfig.h 文件来进行,用户可以通过打开 / 关闭该文件中的宏定义来对代码进行条件编译,最终达到系统配置和裁剪的目的
注:在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。
线程的构成请查看实时手册
RT-Thread API参考手册: event_sample.chttps://www.rt-thread.org/document/api/event_sample_8c-example.html#a10
这些函数均在 rtservice.h 中实现,rtservice.h 第一次使用需要自行在 rtthread/3.0.3/include 文件夹下新建,然后添加到工程的 rtt/source 组中。
线程创建好之后,我们需要把线程添加到就绪列表里面,表示线程已经就绪,系统随时可以调度。就绪列表在 scheduler.c 中定义(scheduler.c 第一次使用需要在 rtthread3.0.3src 目录下新建,然后添加到工程的 rtt/source 组中)。
调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。从代码上来看,调度器无非也就是由几个全局变量和一些可以实现线程切换的函数组成,全部都在 scheduler.c 文件中实现。
调度器启动由函数 rt_system_scheduler_start() 来完成
系统调度就是在就绪列表中寻找优先级最高的就绪线程,然后去执行该线程。但是目前我们还不 支持优先级,仅实现两个线程轮流切换,系统调度函数 rt_schedule。
线程的创建,就绪列表的实现,调度器的实现均已经讲完,现在我们把全部的测试代码都放到 main.c 里面
在 rtt 中,每当用户创建一个对象,如线程,就会将这个对象放到一个叫做容器的地方。
C 语言知识:如果枚举类型的成员值没有具体指定,那么后一个值是在前一个成员值基础上加1。