RT_Thread——快速入门
文章目录
- 一、RT-Thread 目录结构
- 二、核心文件
- 三、移植时涉及的文件
- 3.1 CPU 部分
- 3.2 BSP 部分
- 四、内存管理
- 五、启动流程及main函数
- 5.1 启动流程
- 5.2 关键函数速览
- 5.3 main 函数示例
- 六、数据类型和编程规范
- 6.1 数据类型
- 6.2 函数名
- 6.3 结构体定义
- 6.4 注释规范
- 七、使用模拟器运行第1个程序
- 八、使用逻辑分析仪
- 九、与RTOS对比
- 十、RTT栈回溯示例和栈大小的确认
- 10.1 栈回溯示例和原理
- 10.2 栈大小的确认
一、RT-Thread 目录结构
以RT-Thread 源码(V3.1.5)为例,它的目录如下:
主要涉及3个目录:
- bsp
- bsp目录下是各单板的板级支持包(Board support package),即针对某个特定单板兼容适配的工程。比如ST官方公板:stm32f407-st-discovery
- 移植时,需要针对特定单板适配串口端口、GPIO等
- libcpu
- 为了在不同CPU架构芯片上运行,RT-Thread提供libcpu 抽象层对内核提供统一接口,包括全局中断开关、线程栈初始化等
- 移植时,需要针对特地CPU架构实现全局中断开关、线程上下文切换等
- src
- RT-Thread 的核心文件
二、核心文件
以理解、使用RT-Thread 为目标的话,最核心的文件只有5个:
- src\thread.c
- src\timer.c.
- src\scheduler.c
- src\ipc.c
- include\rtservice.h
这些文件的作用列表如下:
三、移植时涉及的文件
移植RT-Thread,包含CPU 部分移植和BSP 部分移植。
3.1 CPU 部分
CPU 部分移植就是移植libcpu 目录,主要涉及cpuport.c
和context_xx.S
。
比如:libcpu/arm/cortex-m3/cpuport.c
和 context_rvds.S
,这表示 Cortex-M3 架构在 RVDS 或Keil 工具上的移植文件
- cpuport.c:主要用来初始化线程的栈
- context_rvds.S:主要线程切换、全局中断开关
如果RT-Thread 还没支持你使用的CPU,你才需要实现这两个文件。
一般来说,对于大部分的CPU, RT-Thread都已经支持,不需要我们移植。
3.2 BSP 部分
BSP 就是Board Support Packet,板级支持包,就是开发板的相关文件。
BSP 部分主要涉及 main.c
、board.c
和 rtconfig.h
比如:bsp/stm32/stm32f103-simulator
目录下,包含 applications/main.c
、 board/board.c
和 rtconfig.h
- main.c:工程的主函数入口
- board.c:该单板的系统时钟相关配置
- rtconfig.h:内核配置文件,配置裁剪系统功能
四、内存管理
RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。
总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况。
总结起来就是:
- 内存堆管理:它可以分为下面三种,三种只能选择一种
- 针对小内存块的分配管理(小内存管理算法):src\mem.c
- 针对大内存块的分配管理(slab 管理算法):src\slab.c
- 针对多内存堆的分配情况(memheap 管理算法):src\memheap.c
- 内存池管理:先从内存堆里申请空间,然后进行二次管理,对应文件: src\mempool.c
注意:
- src\mem.c、src\slab.c、src\memheap.c 这三个文件只能选择一个文件
- src\mempool.c:先从堆里分配得到一块内存,把这块内存当做内存池
五、启动流程及main函数
5.1 启动流程
我们习惯从 main 函数开始阅读源码,但是RT-Thread 的主要启动流程反而不在 main 函数里。
以工程 RT-Thread_01_create_task
为例:
- 第1个文件:startup_stm32f103xe.s
- 调用
SystemInit
:系统初始化,比如初始化时钟 - 调用
__main
:重定位,比如把代码段从Flash
复制到内存,然后执行$Sub$$main
- 调用
- 第2个文件:src\components.c
- 执行一系列的初始化
- 调用
rt_application_init
- 初始化RTT 组件(包括创建SHELL 线程):因为有SHELL 线程,所以即使 main 函数为空,也是可以操作开发板的
- 调用
main
函数:可以写为空函数
- 启动调度器:
rt_system_scheduler_start
启动过程如下图所示:
上图中,位置①处创建了SHELL线程,用户可以在串口上输入各种命令;所以位置②的main函数写为空函数也是可以的。
5.2 关键函数速览
$ Sub $$main
函数
int $Sub$$main(void)
{ rtthread_startup(); return 0;
}
rtthread_startup
函数- 初始化单板
- 初始化定时器、调度器、信号
- 创建用户进程:
rt_application_init
- 启动调度器
rt_application_init
函数
tid = rt_thread_create("main", main_thread_entry, RT_NULL, RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
main_thread_entry
函数做两件事- 调用
rt_components_init
函数:这会初始化一系列的组件,比如创建SHELL线程 - 调用
$ Super $$main
函数:就是调用main
函数
- 调用
void main_thread_entry(void *parameter)
{ extern int main(void); extern int $Super$$main(void); #ifdef RT_USING_COMPONENTS_INIT /* RT-Thread components initialization */ rt_components_init(); #endif /* invoke system main function */ #if defined(__CC_ARM) || defined(__CLANG_ARM) $Super$$main(); /* for ARMCC. */ #elif defined(__ICCARM__) || defined(__GNUC__) main(); #endif
}
- 从
$ Sub $$main
到main
的调用过程如下
$Sub$$main() rtthread_startup() rt_application_init() main_thread_entry() $Super$$main() main()
5.3 main 函数示例
在rt_components_init
函数中已经创建了SHELL 线程,用户可以通过串口跟它交互。
所以,即使main
函数为空,我们还是可以通过串口操作开发板的。
每个工程都有一个main.c
文件:
- 可以把
main
函数写为空函数 - 可以在
main
函数里面创建线程 - 也可以在
main.c
任意位置使用宏MSH_CMD_EXPORT
定义命令,然后在串口中执行这个命令
main.c
示例代码如下:
六、数据类型和编程规范
6.1 数据类型
RT-Thread的数据类型定义在rtdef.h
中,如下所示,涉及8位、16位、32位、64位的定义。
格式为都以rt_
开头,然后指明是否是有符号类型和位数,最后以_t
结尾。
/* RT-Thread basic data type definitions */
##ifndef RT_USING_ARCH_DATA_TYPE
typedef signed char rt_int8_t; /**< 8bit integer type */
typedef signed short rt_int16_t; /**< 16bit integer type */
typedef signed int rt_int32_t; /**< 32bit integer type */
typedef unsigned char rt_uint8_t; /**< 8bit unsigned integer type */
typedef unsigned short rt_uint16_t; /**< 16bit unsigned integer type */
typedef unsigned int rt_uint32_t; /**< 32bit unsigned integer type */ ##ifdef ARCH_CPU_64BIT
typedef signed long rt_int64_t; /**< 64bit integer type */
typedef unsigned long rt_uint64_t; /**< 64bit unsigned integer type */
##else
typedef signed long long rt_int64_t; /**< 64bit integer type */
typedef unsigned long long rt_uint64_t; /**< 64bit unsigned integer type */
##endif
##endif
随后还定义特殊数据类型。
比如rt_base_t
是基础数据类型,和芯片位数
相关,在Cortex-M3等32位架构中,它就是32位的;在A57等64位架构中,它就是64位的。
rt_err_t
用于记录错误编号,rt_time_t
用于时间戳,rt_tick_t
用于周期性时钟tick计数。
typedef int rt_bool_t; /**< boolean type */
typedef long rt_base_t; /**< Nbit CPU related date type */
typedef unsigned long rt_ubase_t; /**< Nbit unsigned CPU related data type */ typedef rt_base_t rt_err_t; /**< Type for error number */
typedef rt_uint32_t rt_time_t; /**< Type for time stamp */
typedef rt_uint32_t rt_tick_t; /**< Type for tick count */
typedef rt_base_t rt_flag_t; /**< Type for flags */
typedef rt_ubase_t rt_size_t; /**< Type for size number */
typedef rt_ubase_t rt_dev_t; /**< Type for device */
typedef rt_base_t rt_off_t; /**< Type for offset */
6.2 函数名
函数名使用英文小写,单词之间使用 _ 连接,通常以rt_
开头,比如:
int rt_hw_spi_init(void)
如果函数入口参数是空,必须使用 void 作为入口参数。
内部静态函数,则以 _ 开头,不用rt_
开头,比如:
static rt_err_t _uart_configure()
6.3 结构体定义
函数名使用英文小写,单词之间使用_连接,比如:
struct rt_list_node
{ struct rt_list_node *next; struct rt_list_node *prev;
};
结构体的类型定义,使用结构体名称加上 _t 的形式,例如:
typedef struct rt_list_node rt_list_t;
6.4 注释规范
注释以 /**
开头,以 */
结尾,中间写入函数注释,组成元素如下,每个元素描述之间空一行,且首列对齐。
@brief
+ 简述函数作用。在描述中,着重说明该函数的作用@note
+ 函数说明。在上述简述中未能体现到的函数功能或作用的一些点,可以做解释说明@see
+ 相关 API 罗列。若有与当前函数相关度较高的 API,可以进行列举@param
+ 以参数为主语 + be 动词 + 描述,说明参数的意义或来源@return
+ 枚举返回值 + 返回值的意思,若返回值为数据,则直接介绍数据的功能@warning
+ 函数使用注意要点。在函数使用时,描述需要注意的事项,如使用环境、使用方式等
注释模版请参见:rt-thread/src/ipc.c 源码文件。
七、使用模拟器运行第1个程序
双击"RT-Thread_01_create_task\bsp\stm32\stm32f103-simulator\project.uvprojx"打开第一个示例。
打开之后,首先要编译工程,才能使用模拟器运行,点击"Build"图标进行编译,如下图所示:
编译完成后,点击"Debug"按钮进行仿真,如下图所示:
第一个程序里面创建了两个任务,两个任务一直打印各自的信息。
这里需要打开串口显示模拟窗口,显示任务的打印内容。
点击左上角菜单的“View”,然后选择“Serial Windows”,点击“UART #1”,如下图所示:
最后,点击“Run”运行程序,右下角串口显示窗口将打印两个任务的信息。
如果想退出模拟器仿真,再次"Debug"按钮退出,如下图所示:
八、使用逻辑分析仪
此程序有两种输出方式:
- 串口:查看打印信息
- 逻辑分析仪:观察全局变量的波形,根据波形解析任务调度情况 下面举例说明逻辑分析仪的用法。
双击"RT-Thread_06_taskdelay\bsp\stm32\stm32f103-simulator\project.uvprojx"打开该示例。
打开之后,首先要编译工程,点击"Build"图标进行编译。
编译完成后,点击"Debug"按钮进行仿真。
本实例使用模拟器的逻辑分析仪观察现象。
首先在“main.c”的主函数加入断点,在代码行前的灰色处,点击一下就会有一个红色小点,就是设置的“断点”。
然后点击“Run”运行,程序运行到断点位置,就会停下来等待下一步操作:
- 在代码中找到全局变量flag
- 鼠标选中flag,然后点击鼠标右键,在弹出的菜单里选择"Add ‘flag’ to…",选择“Analyzer”,如下图所示:
此时在代码框上面,就会出现逻辑分析仪“Logic Analyzer”显示窗口,里面分析的就是变量flag。
点击这个flag,然后右键,选择“Bit”,以便观察,如下图所示:
再点击一下“Run”,继续运行,此时逻辑分析仪窗口显示变量flag 的bit 值变化,如下图所示:
在逻辑分析仪窗口,可以使用鼠标滚轮放大、缩小波形。
九、与RTOS对比
A执行一半轮到B执行,B一开始执行发现获得不到信号量就会从就绪链表中退出进入休眠状态,导致定时器中断时无法让B运行,所以在后续会一直执行A,直到A运行完毕释放信号量,B才会被唤醒,才能够正常运行。
在RTOS中,prvSetupHardware()
会先去完成硬件相关初始化,接着创建任务,此时创建任务后会将任务放入一个就绪链表中,并去启动调度器;而在RTT中,在main函数前就完成硬件相关初始化,并去创建任务,注意此时与RTOS不一样的是它创建完任务并没有直接将任务放入就绪链表中,而是通过启动线程才能将任务放入就绪链表
中,最后在其他地方去启动调度器。
由上图可知,RTOS中启动后会运行main函数,main函数再去启动调度器;而RTT是在启动后创建了一个线程,线程会启动调度器,该线程才能得以运行,接着去运行main函数。
十、RTT栈回溯示例和栈大小的确认
10.1 栈回溯示例和原理
在执行上述函数时,发现串口打印到PC地址时会奔溃,要想找到发生奔溃的位置,我们首先需要找到错误位置。由于是在打印PC地址时发生奔溃,说明PC地址对应的函数中出现问题,接下来我们就需要去查看反汇编,找到改地址对应的函数:
接着我们需要去分析栈,我们需要去改下部分代码,去打印正常栈的地址,以便区别异常栈:
unsigned int *app_sp;
unsigned int *app_sp_top;int i;
app_sp = (unsigned int *)(context + 1); /* context(异常栈大小) + 16*4 */ //异常栈大小+1为正常栈位置rt_kprintf("app stacks: \r\n");
app_sp_top = (unsigned int *)((unsigned int)rt_thread_self()->stack_addr + rt_thread_self()->stack_size);
i = 0;
while (app_sp_top >= app_sp)
{rt_kprintf("%08x ", *app_sp_top);app_sp_top--;i++;if (i % 16 == 0)rt_kprintf("\r\n");
}
打印出来的地址如下:
可见fputc函数的栈指针指向0x0800128f,要想知道调用fputc的函数地址,只需在fputc函数的地址减一即可找到:
所以我们找到调用fputc的函数是printf函数,而printf函数的栈指针指向0x05501dbb,要想知道调用printf的函数地址,只需在printf函数的地址减一即可找到:
说明是在main函数中调用printf导致的奔溃,解决办法是将printf改成rtt中封装的打印函数rtt_kprintf。
while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */rt_thread_mdelay(500);HAL_GPIO_TogglePin(LED_B_GPIO_Port,LED_B_Pin);//printf("hello\r\n");rt_kprintf("hello\r\n");}
10.2 栈大小的确认
①:调用test_stack函数时,一进去LR就会把返回地址保存进栈中
②:接着我们定义了一个大小为100的int型数组,其大小为100*4=400,于是SP栈指针指向SP-404的位置,分成一个栈给数组(多分配出的4个字节可能是编译器问题)
③:接下来在r0为存在sp+4的位置,由图可见,此时除去r0,r0往上的栈才是真正数组的栈(这里的汇编的上一行r1表示的是strcpy函数里面的参数字符串,保存在Flash中)
④:调用strcpy函数进行数组赋值
⑤:SP栈指针回到LR寄存器的位置
⑥:调用POP指令后SP栈指针指向栈的最顶端
这里说明了在调用函数test_stack时,会分配给其对应的栈,当函数执行完毕,该栈会被回收。
在上述函数中,会在栈的起始放入‘#’,以此来区别别的栈,当放入的不为‘#’时,说明此处为栈的截至位置,则可以知道栈的大小,如果要想知道一个线程用了多少栈,则与下面函数有关:
红框内说明当栈内为‘’#时,指针会一直从低处到高处增加,并计算栈大小。