RT-Thread入门
1.初识RT-Thread
嵌入式系统是一种完全嵌入在装置或设备内部,为满足特定需求而设计的计算机系统,譬如生活中常见的嵌入式系统就有:电视机顶盒、路由器、电冰箱、微波炉与移动电话等。
嵌入式操作系统是应用于嵌入式系统的软件。
2.动态内存堆的使用
- 裸机系统动态内存
动态内存配置
动态内存使用- RTT动态内存
动态内存配置
动态内存使用(源码解析)- 动态内存注意事项
内存复位
内存泄漏- 其他相关API
rt_realloc
rt_calloc
2.1 简述堆栈
在单边机应用中,我们经常提到堆栈这个词,实际上堆和栈是两个不同的概念。
栈(stack):由编译器自动分配释放
堆(heap):一般由程序员分配和释放
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{int b; //栈char s[] = "abc"; //栈char *p2; //栈char *p3 = "123456"; //123456\0在常量区,p3在栈上。static int c = 0; //全局静态初始化区p1 = (char*)malloc(10); //堆p2 = (char*)malloc(20); //堆
}
全局初始化区
全局静态初始化区
全局未初始化区
常量区
栈区
堆区
2.2 MDK裸机系统动态内存配置和使用
使用方式1:startup_stm32f103xe.s
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>Stack_Size EQU 0x00000400AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>Heap_Size EQU 0x00000200AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
栈空间的设置和堆空间的设置
使用方式2:
char *p;
p = (char*)malloc(10)
free(p);
2.3 RT-Thread动态内存配置和使用
与裸机系统下使用相差不大。
RT-Thread中,使用动态内存前,需要使用动态内存函数rt_system_heap_init
配置好动态内存区。
之后,使用rt_malloc
及rt_free
函数获得动态内存及释放动态内存。
char*p;
p = (char *)rt_malloc(10);
rt_free(p);
2.3.1 rt_system_heap_init
board.c
rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
参数为起始地址和结束地址,两地址之间的空间作为动态内存的空间
可以从芯片总的内存空间中提取一部分作为动态内存空间。
起始地址 HEAP_BEGIN
#define HEAP_BEGIN ((void *)&Image$$RW_IRAM1$$ZI$$Limit)
Image R W I R A M 1 RW_IRAM1 RWIRAM1ZI$$Limit 是一个链接器导出的符号。代表ZI段的结束,也就是程序执行区的RAM结束后的地址,另一个角度,也就是我们执行区RAM未使用的区域的起始地址。
从工程的map文件中,可以看到:
Total RO Size (Code + RO Data) 63528 ( 62.04kB)Total RW Size (RW Data + ZI Data) 22576 ( 22.05kB)Total ROM Size (Code + RO Data + RW Data) 63676 ( 62.18kB)
整个工程中用到的静态RAM空间用掉了22.05K
芯片总RAM有64K,除去用掉的22.05K的空间,剩余的空间的起始地址,在MDK中使用 宏
Image$$RW_IRAM1$$ZI$$Limit
表示。
结束地址 HEAP_END
#define HEAP_END STM32_SRAM_END
#define STM32_SRAM_END (0x20000000 + STM32_SRAM_SIZE * 1024)
使用片内RAM的结束地址作为动态内存的结束地址。
2.4 动态内存堆使用注意点
内存复位
当我们每次申请到新的内存块之后,建议对所申请到的内存块进行清零操作。
p = (char *)rt_malloc(10);
if(p!=RT_NULL)
{rt_memset(p,0,10); //对申请到的内存清零
}
内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存,由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
我们在使用动态内存时需要注意:rt_malloc
需要和 rt_free
配套使用。
2.5 其他动态内存相关的API
void *rt_realloc(void *rmem,rt_size_t newsize)
在已分配内存块的基础上重新分配内存块的大小(增加或减少)
在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)
void *rt_calloc(rt_size_t count,rt_size_t size)
从内存堆中分配连续内存地址的多个相同大小的内存块
2.6 动态内存使用的Demo代码
int i = 0;char *ptr = RT_NULL; //内存块指针for(i=0;;i++){ptr = rt_malloc(1<<i); //每次申请 (1<<i)大小的内存if(ptr != RT_NULL){rt_kprintf("rt_malloc OK! size=%d\n",(1<<i));rt_memset(ptr,0,(1<<i)); //清空申请到的内存rt_kprintf("rt_memset OK!\n");rt_free(ptr); //释放申请到的内存rt_kprintf("rt_free OK!\n");}else{rt_kprintf("rt_malloc Err!\n");return -1;}}
3.线程的创建
3.1 线程的概念
RT-Thread名为实施线程RTOS,那么什么叫线程?
- 人们在生活中处理复杂问题时,惯用的方法是“分而治之”,即把一个大问题分解成多个相对简单、比较容易解决的小问题。小问题逐个被解决了,大问题也就随之解决了。同样,在设计一个较为复杂的应用程序时,也通常吧一个大型任务分解成多个小任务,然后通过运行这些小任务,最终达到完成大任务的目的。
- 在RT-Thread中,与上述小任务对应的程序实体就叫做“线程”(或任务),RT-Thread就是一个能对这些小“线程”进行管理和调度的多”线程“操作系统。
- 线程是实现任务的载体,它是RT-Thread中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级。
3.2 线程的组成
RT-Thread中,线程由三部分组成:线程代码(入口函数)、线程控制块、线程堆栈。
线程代码
//无限循环结构
void thread_entry(void *parameter)
{while(1){/* 等待事件发生 *//* 处理事件 */}
}//顺序执行结构
void thread_entry(void *parameter)
{/* 事务1处理 *//* 事务2处理 *//* 事务N处理 */
}
无限循环结构中一般加入让出CPU的API调用,否则这个线程一直占用CPU,其他线程无法被执行。
线程控制块
线程控制块是操作系统用于管理线程的一个数据结构。它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包括线程与线程之间连接用的链表结构,线程等待事件集合等。
struct rt_thread
struct tr_thread *rt_thread_t
线程栈
RT-Thread每个线程都具有独立的栈空间,当进行线程切换时,系统会将当前线程的上下文保存在线程栈中,当线程要恢复运行时,再从线程栈中读取上下文信息,恢复线程的运行。
线程上下文是指线程执行的环境,具体说就是各个变量和数据,包括所有寄存器变量、堆栈信息、内存信息等。
线程栈在形式上是一段连续的内存空间,我们可以通过定义一个数组或者申请一段动态内存来作为线程的栈。
3.3 线程创建
创建线程
创建动态线程
rt_thread_t rt_thread_create(const char *name,void (*entry)(void *parameter),void *parameter,rt_uint32_t stack_size,rt_uint8_t priority,rt_uint32_t tick);
创建静态线程
rt_err_t rt_thread_init(struct rt_thread *thread,const char *name,void (*entry)(void *parameter),void *parameter,void *stack_start,rt_uint32_t stack_size,rt_uint8_t priority,rt_uint32_t tick);
动态线程的线程控制块和线程栈都是自动分配的。
创建静态线程需要提前定义好线程控制块和线程栈。
启动线程
rt_err_t rt_thread_startup(rt_thread_t thread);
调用此函数后,创建的线程会被加入到线程的就绪队列,执行调度。
3.4 静态线程VS动态线程
相关资源分配形式
静态线程需要将线程控制块与线程栈先定义出来,动态线程不需要,动态线程的线程控制块和线程栈是自动分配的。
运行效率
1.如果动态线程和静态线程的线程控制块和线程栈都处于芯片的RAM中,则动态线程和静态线程的运行效率没有区别。
2.如果系统使用了外扩的外部RAM,动态线程的线程控制块和线程栈是处于外部RAM中。则动态线程的运行效率有所下降。
3.两种方式创建的线程,本质上没有区别。
3.5 线程创建Demo
/*
创建两个线程,一个动态线程,一个静态线程
*/#define THREAD_PORITY 25
#define THREAD_SLICE 5
#define THREAD_STCK_SIZE 512//动态线程
rt_thread_t tid1 = RT_NULL; //静态线程
struct rt_thread thread2; //静态线程
char thread2_stack[THREAD_STCK_SIZE];/*** @brief 线程1的入口函数* * @param parameter [IN]线程的参数*/
void my_thread1_entry(void *parameter)
{uint32_t count = 0;while(1){count ++;rt_kprintf("thread1 count = %d\r\n",count);rt_thread_mdelay(100);}}/*** @brief 线程2的入口函数* * @param parameter [IN]线程的参数*/
void my_thread2_entry(void *parameter)
{uint32_t count = 0;for(count=0;count<10;count++){rt_kprintf("thread2 count = %d\r\n",count);}rt_kprintf("thread2 exit\r\n");
}int main(void)
{ //创建动态线程tid1 = rt_thread_create("thread1",my_thread1_entry,RT_NULL,THREAD_STCK_SIZE,THREAD_PORITY,THREAD_SLICE);if(tid1 != RT_NULL){rt_thread_startup(tid1); //将线程添加到线程就绪队列rt_kprintf("thread1 creater OK!\r\n");}else{rt_kprintf("thread1 creater Err!\r\n");}//创建静态线程rt_thread_init(&thread2,"thread2",my_thread2_entry,RT_NULL,&thread2_stack[0],THREAD_STCK_SIZE,THREAD_PORITY-1,THREAD_SLICE);rt_thread_startup(&thread2); //将线程2添加到线程就绪列表return 0;
}
运行结果:
thread1 creater OK!
msh >thread2 count = 0
thread2 count = 1
thread2 count = 2
thread2 count = 3
thread2 count = 4
thread2 count = 5
thread2 count = 6
thread2 count = 7
thread2 count = 8
thread2 count = 9
thread2 exit
thread1 count = 1
thread1 count = 2
thread1 count = 3
thread1 count = 4
thread1 count = 5
thread1 count = 6
…
4.跑马灯线程实例
4.1 线程状态
线程状态转换图

4.2 系统滴答时钟
每个操作系统中都存在一个“系统心跳”时钟,是操作系统中最小的时钟单位。这个时钟负责系统和时间相关的一些操作。作为操作系统运行的时间尺度,心跳时钟是由硬件定时器的定时中断产生。
系统的心跳时钟我们也称之为系统滴答或时钟节拍,系统滴答的频率需要我们根据CPU的处理能力来决定。
时钟节拍使得内核可以将线程延时若干个整数倍时钟节拍,以及线程等待事件发生时,提供等待超时的依据。
频率越快,内核函数介入系统运行的几率就越大,内核占用的处理器时间就越长,系统的负荷就变大;频率太小,时间处理精度又不够。
我们在STM32平台上一般设置系统滴答的频率为100Hz,即每个滴答的时间是10ms。
4.2.1 STM32上系统滴答的配置
//board.c
void SystemClock_Config(void)
{...//设置系统中断时间HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / RT_TICK_PER_SECOND);...
}//系统定时器中断
void SysTick_Handler(void)
{...rt_tick_increase();...
}//rt_config.h
#define RT_TICK_PER_SECOND 100 //配置滴答频率为100Hz
rt_thread_mdealy(500)
延时500ms。这个API的延时就是由系统滴答完成的。
4.3 GPIO驱动架构操作IO
IO初始化
void rt_pin_mode(rt_base_t pin,rt_base_t mode);
pin:引脚索引
mode:引脚模式PIN_MODE_OUTPUTPIN_MODE_INPUTPIN_MODE_INPUT_PULLUPPIN_MODE_INPUT_PULLDOWNPIN_MODE_OUTPUT_OD
IO写入
void rt_pin_write(rt_base_t pin,rt_base_t value);
pin:引脚索引
value:引脚电平PIN_HIGHPIN_LOW
IO读出
int rt_pin_read(rt_base_t pin);
pin:引脚索引
返回值:引脚电平
4.4 跑马灯Demo
/*
跑马灯
*/
#include <rtthread.h>
#include <rtdevice.h>
#include <drv_gpio.h>//线程信息
#define THREAD_PORITY 25
#define THREAD_SLICE 5
#define THREAD_STCK_SIZE 512//线程控制块指针
rt_thread_t led_tid = RT_NULL;//LED引脚
#define MY_LED_PIN 14/*** @brief 线程的入口函数* * @param parameter [IN]线程的参数*/
void led_entry(void *parameter)
{//设置LED引脚为推挽输出rt_pin_mode(MY_LED_PIN,PIN_MODE_OUTPUT); while(1){rt_pin_write(MY_LED_PIN,PIN_HIGH); //引脚置高rt_thread_delay(50); //延时50个时钟节拍 rt_thread_sleep(50)rt_pin_write(MY_LED_PIN,PIN_LOW); //引脚置低rt_thread_mdelay(500); //延时500ms}}int main(void)
{ //创建线程led_tid = rt_thread_create("led",led_entry,RT_NULL,THREAD_STCK_SIZE,THREAD_PORITY,THREAD_SLICE);if(led_tid != NULL){rt_thread_startup(led_tid); //加入到线程就绪队列}return 0;
}
4.5 线程栈大小分配
先将线程栈大小设置一个固定值(比如2048),在线程运行时通过查看线程栈的使用情况,了解线程栈使用的实际情况,根据实际情况设置合理的线程栈大小。
msh >list_thread
thread pri status sp stack size max used left tick error
------ --- ------- ---------- ---------- ------ ---------- ---
led 25 suspend 0x00000074 0x00000200 24% 0x00000005 000
tshell 20 ready 0x00000080 0x00001000 07% 0x00000005 000
tidle 31 ready 0x00000054 0x00000100 35% 0x00000010 000
msh >
一般将线程栈最新大使用量设置为70%。
5.线程的时间片轮询调度
5.1 线程优先级
优先级和时间片是线程的两个重要参数