前言
一、Keil MDK工程的搭建
1.安装RT-Thread软件包
2.使用RT-Thread软件包新建Keil工程
3.配置rtconfig.h
4.解决RT-Thread中#error提示的TODO
5. 解决Linker的错误
二、信号量在线程同步中的应用
三、互斥信号量在线程同步中的应用
总结
参考资料
非常有幸能够参与RT-Thread Nano学习营的线下学习。本来报名计划参加线下活动,但是临时家里有事未能如期参加,很是遗憾。与FreeRTOSv9相比,个人认为RT-Thread在本地化方面做的更加优秀。
学习营推荐的开发环境是RT-Thread Studio,非常的快捷高效。但是本人一直习惯Keil或VS Code,所以直接使用Keil作为开发环境。
打开Keil,选择“Pack Installer”选项卡,然后在线安装RT-Thread软件包。
此外,如果无法在线安装,也可以到Keil官网下载,然后离线导入即可。
为了方便调试,勾选“enable kernel debug configuration”选项。
为了能够使用rt_kprintf()函数在UART窗口打印调试信息,勾选“Using Console”选项。
#error "TODO 1: OS Tick Configuration."
在board.c中添加系统滴答定时器的配置。
#include
#include
// 省略
// void rt_os_tick_callback(void)
void SysTick_Handler(void)
{
rt_interrupt_enter();
rt_tick_increase();
rt_interrupt_leave();
}
/**
* This function will initial your board.
*/
void rt_hw_board_init(void)
{
// #error "TODO 1: OS Tick Configuration."
/*
* TODO 1: OS Tick Configuration
* Enable the hardware timer and call the rt_os_tick_callback function
* periodically with the frequency RT_TICK_PER_SECOND.
*/
SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
/* Call components board initial (use INIT_BOARD_EXPORT()) */
// 以下省略
Error: L6218E: Undefined symbol assert_param (referred from misc.o)
.\Objects\SemaphoreUsage.axf: Error: L6218E: Undefined symbol $Super$$main (referred from components.o).
Keil MDK支持的符号$Sub$$和$Super$$,$Super$$main和$Sub$$main是两个特殊的函数。$Super$$main是主函数的入口点,$Sub$$main是副函数的入口点。当程序启动时,Super$$main将被调用,它将初始化硬件并调用$Sub$$main。$Sub$$main是用户定义的主函数,它包含程序的主要逻辑。此处直接将自己的main函数加入工程即可通过。
信号量是一种同步机制,用于限制对共享资源的访问。在RT-Thread中,信号量定义在“rtsem.h”头文件中。它们用于控制对共享资源(如内存缓冲区、I/O设备和其他系统资源)的访问。
信号量的基本功能是提供一种等待资源可用的方式。当线程请求访问资源时,它必须首先获取与该资源关联的信号量。如果信号量可用,则线程可以继续访问资源。如果信号量不可用,则线程将被阻塞,直到信号量变为可用。
信号量可用于各种场景,例如保护共享数据结构、控制对I/O设备的访问和同步线程。它们在实时系统中特别有用,因为必须满足时间限制。但是,使用信号量时可能会引入额外的开销和复杂性,也可能引起死锁、优先级反转和竞争等问题。必须确保在获取信号量后释放它们,以防止死锁和其他同步问题。
信号量结构体在“rtthread/include/rtdef.h”中定义。
rt_sem_t 是一个指向 rt_semaphore 结构体的指针类型。rt_semaphore 是一个信号量结构体,用于同步线程之间的操作。rt_semaphore 结构体的成员变量如下:
- parent:继承自 rt_ipc_object 结构体,包含了 rt_object 结构体的成员变量,如 name、type 和 flag,以及 rt_ipc_object 结构体的成员变量,如 suspend_thread。
- value:信号量的值,用于控制线程的访问。
- reserved:保留字段。
RT-Thread中信号量的实现源代码位于“rtthread/include/rtthread.h”和“rtthread/src/ipc.c”中。
下面的例子中,定义了三个信号量sem1、sem2和sem3,并将sem1初始化为1,sem2和sem3初始化为0。然后,我们创建了三个线程thread1、thread2和thread3,并分别在这些线程的入口函数中实现了获取和释放信号量的逻辑。在main函数中,我们初始化了三个信号量,并创建了三个线程,并启动这些线程。
#include
typedef enum {
FLAG_THREAD_IDLE,
FLAG_THREAD_1,
FLAG_THREAD_2,
FLAG_THREAD_3
} ThreadFlag_t;
/* 定义三个信号量 */
static rt_sem_t sem1;
static rt_sem_t sem2;
static rt_sem_t sem3;
static rt_uint32_t seed = 1;
/* 记录当前线程的标识 */
ThreadFlag_t xCurrentThreadFlag;
static rt_uint32_t rt_hw_rand(void)
{
seed = (seed * 1103515245 + 12345) % (1 << 31);
return seed;
}
static void rt_hw_rand_init(void)
{
seed = rt_tick_get();
}
/* 线程1的入口函数 */
static void thread1_entry(void* parameter)
{
while (1)
{
/* 获取信号量1 */
rt_sem_take(sem1, RT_WAITING_FOREVER);
xCurrentThreadFlag = FLAG_THREAD_1;
/* 释放信号量2 */
rt_sem_release(sem2);
}
}
/* 线程2的入口函数 */
static void thread2_entry(void* parameter)
{
while (1)
{
/* 获取信号量2 */
rt_sem_take(sem2, RT_WAITING_FOREVER);
xCurrentThreadFlag = FLAG_THREAD_2;
/* 释放信号量3 */
rt_sem_release(sem3);
}
}
/* 线程3的入口函数 */
static void thread3_entry(void* parameter)
{
while (1)
{
/* 获取信号量3 */
rt_sem_take(sem3, RT_WAITING_FOREVER);
xCurrentThreadFlag = FLAG_THREAD_3;
/* 阻塞一段随机的时间 */
rt_thread_mdelay(rt_tick_from_millisecond(rt_hw_rand() % 1));
/* 释放信号量1 */
rt_sem_release(sem1);
}
}
/* Idle线程的hook钩子函数 */
void resetCurrentThreadFlag(void)
{
xCurrentThreadFlag = FLAG_THREAD_IDLE;
}
int main(void)
{
/* 初始化随机数 */
rt_hw_rand_init();
xCurrentThreadFlag = FLAG_THREAD_IDLE;
rt_thread_idle_sethook(resetCurrentThreadFlag);
/* 初始化信号量, sem1初始化为1,sem2和sem3初始化为0 */
rt_sem_init(sem1, "sem1", 1, RT_IPC_FLAG_FIFO);
rt_sem_init(sem2, "sem2", 0, RT_IPC_FLAG_FIFO);
rt_sem_init(sem3, "sem3", 0, RT_IPC_FLAG_FIFO);
if (sem1 != RT_NULL && sem2 != RT_NULL && sem3 != RT_NULL)
{
/* 创建三个线程 */
rt_thread_t thread1 = rt_thread_create("thread1", thread1_entry, RT_NULL, 1024, 8, 10);
rt_thread_t thread2 = rt_thread_create("thread2", thread2_entry, RT_NULL, 1024, 8, 10);
rt_thread_t thread3 = rt_thread_create("thread3", thread3_entry, RT_NULL, 1024, 8, 10);
if (thread1 != RT_NULL && thread2 != RT_NULL && thread3 != RT_NULL)
{
/* 启动三个线程 */
rt_thread_startup(thread1);
rt_thread_startup(thread2);
rt_thread_startup(thread3);
return RT_EOK;
}
/* 删除已创建的信号量 */
rt_sem_delete(sem1);
rt_sem_delete(sem2);
rt_sem_delete(sem3);
}
return RT_ERROR;
}
本例中,使用全局变量xCurrentThreadFlag实时保存当前正在执行的线程。通过Keil MDK的逻辑分析仪工具可观察到如下图形:
互斥信号量是RT-Thread中的一种用于线程同步的机制,它可以保证在同一时间只有一个线程可以访问共享资源。在RT-Thread中,互斥信号量可以用于以下场景:
1. 保护共享资源:当多个线程需要访问同一个共享资源时,互斥信号量可以确保在同一时间只有一个线程可以访问该资源,从而避免数据竞争和死锁等问题。
2. 保护临界区:当多个线程需要访问同一个临界区时,互斥信号量可以确保在同一时间只有一个线程可以进入该临界区,从而避免数据竞争和死锁等问题。
在RT-Thread中,互斥信号量的API包括:
- rt_mutex_init:初始化互斥信号量。
- rt_mutex_detach:删除互斥信号量。
- rt_mutex_take:获取互斥信号量。
- rt_mutex_release:释放互斥信号量。
注意:使用互斥信号量的前提是在rtconfig.h中定义RT_USING_MUTEX宏。
在这个示例中,我们创建了一个互斥信号量 mutex,以确保两个线程不会同时访问共享资源。thread1 和 thread2 分别访问共享资源,并在访问之前和之后使 rt_mutex_take() 和 rt_mutex_release() 函数获取和释放互斥信号量。shared_resource 是一个整数类型的共享资源,thread1 和 thread2 分别对其进行加和减的操作。
#include
#define __SIMULATOR__ //当前在使用Simulator
#ifdef __SIMULATOR__
#define DEBUG_PRINTF(str) printf(str)
#else
#define DEBUG_PRINTF(str) rt_kprintf(str)
#endif
// 定义互斥信号量
#define MUTEX_NAME "mutex"
static rt_mutex_t mutex;
// 定义线程
#define THREAD1_NAME "thread1"
#define THREAD2_NAME "thread2"
static rt_thread_t thread1;
static rt_thread_t thread2;
// 声明共享资源
int shared_resource = 0;
typedef enum {
FLAG_THREAD_IDLE,
FLAG_THREAD_1,
FLAG_THREAD_2
} ThreadFlag_t;
ThreadFlag_t xCurrentThreadFlag;
void resetCurrentThreadFlag(void)
{
if (xCurrentThreadFlag != FLAG_THREAD_IDLE)
{
DEBUG_PRINTF("Thread has been blocked, and the IDLE starts!\n");
}
xCurrentThreadFlag = FLAG_THREAD_IDLE;
}
// 线程1入口函数
void thread1_entry(void* parameter)
{
while (1)
{
xCurrentThreadFlag = FLAG_THREAD_1;
DEBUG_PRINTF("The step 1 of thread 1 is running!\n");
// 获取互斥信号量
if (rt_mutex_take(mutex, RT_WAITING_FOREVER) != RT_EOK)
{
DEBUG_PRINTF("thread1 take mutex failed.\n");
return;
}
DEBUG_PRINTF("Thread 1 takes mutex!\n");
// 访问共享资源
shared_resource++;
// 阻塞当前任务
rt_thread_mdelay(100);
DEBUG_PRINTF("The step 1 of thread 1 is running!\n");
xCurrentThreadFlag = FLAG_THREAD_1;
shared_resource--;
// 释放互斥信号量
rt_mutex_release(mutex);
DEBUG_PRINTF("Thread 1 releases mutex!\n");
rt_thread_mdelay(100);
DEBUG_PRINTF("The step 3 of thread 1 is running!\n");
}
}
// 线程2入口函数
void thread2_entry(void* parameter)
{
while (1)
{
xCurrentThreadFlag = FLAG_THREAD_2;
DEBUG_PRINTF("The step 1 of thread 2 is running!\n");
// 获取互斥信号量
if (rt_mutex_take(mutex, RT_WAITING_FOREVER) != RT_EOK)
{
DEBUG_PRINTF("thread2 take mutex failed.\n");
return;
}
DEBUG_PRINTF("Thread 2 takes mutex!\n");
// 访问共享资源
shared_resource--;
// 阻塞当前任务
rt_thread_mdelay(100);
DEBUG_PRINTF("The step 2 of thread 2 is running!\n");
xCurrentThreadFlag = FLAG_THREAD_2;
shared_resource++;
// 释放互斥信号量
rt_mutex_release(mutex);
DEBUG_PRINTF("Thread 2 releases mutex!\n");
rt_thread_mdelay(200);
DEBUG_PRINTF("The step 3 of thread 2 is running!\n");
}
}
int main(void)
{
xCurrentThreadFlag = FLAG_THREAD_IDLE;
rt_thread_idle_sethook(resetCurrentThreadFlag);
// 创建互斥信号量
mutex = rt_mutex_create(MUTEX_NAME, RT_IPC_FLAG_FIFO);
if (mutex == RT_NULL)
{
DEBUG_PRINTF("create mutex failed.\n");
return -1;
}
// 创建线程
thread1 = rt_thread_create(THREAD1_NAME, thread1_entry, RT_NULL, 1024, 10, 10);
if (thread1 == RT_NULL)
{
DEBUG_PRINTF("create thread1 failed.\n");
return -1;
}
thread2 = rt_thread_create(THREAD2_NAME, thread2_entry, RT_NULL, 1024, 10, 10);
if (thread2 == RT_NULL)
{
DEBUG_PRINTF("create thread2 failed.\n");
return -1;
}
// 启动线程
rt_thread_startup(thread1);
rt_thread_startup(thread2);
return 0;
}
在上面的示例中,如果不适用互斥信号量,会出现下图的情形。
T1时刻:Thread1开始执行,并将shared_resource自加1,从0变为1. 然后调用t_thread_mdelay(3)阻塞了自己,开始执行Idle线程。
T2时刻:Thread2开始执行,并将shared_resource自减1,从1变为0. 然后调用rt_thread_mdelay(1)阻塞了自己,开始执行Idle线程。
T3时刻:Thread2开始执行,并将shared_resource自加1,从0变为1. 然后调用t_thread_mdelay(20)阻塞了自己,开始执行Idle线程。
T4时刻:Thread2开始执行,并将shared_resource自减1,从1变为0. 然后调用rt_thread_mdelay(10)阻塞了自己,开始执行Idle线程。
可见,两个线程对shared_resource 的加减操作互相穿插了,这中情况就可以通过应用互斥量解决(效果如下图)。
使用信号量后串口的输出结果如下:
The step 1 of thread 1 is running!
Thread 1 takes mutex!
The step 1 of thread 2 is running!
Thread has been blocked, and the IDLE starts!
The step 1 of thread 1 is running!
Thread 1 releases mutex!
Thread 2 takes mutex!
Thread has been blocked, and the IDLE starts!
The step 3 of thread 1 is running!
The step 1 of thread 1 is running!
The step 2 of thread 2 is running!
Thread 2 releases mutex!
Thread 1 takes mutex!
Thread has been blocked, and the IDLE starts!
The step 1 of thread 1 is running!
Thread 1 releases mutex!
Thread has been blocked, and the IDLE starts!
The step 3 of thread 2 is running!
The step 1 of thread 2 is running!
Thread 2 takes mutex!
The step 3 of thread 1 is running!
The step 1 of thread 1 is running!
Thread has been blocked, and the IDLE starts!
RT-Thread是一个非常优秀的开源的实时操作系统,特别适合应用于嵌入式系统的软件开发。它采用了分层的设计,包括内核层、组件层和应用层。并且支持多种处理器架构,包括ARM、MIPS、RISC-V等。与FreeRTOS相比,RT-Thread的内存管理采用了动态内存池的方式,可以有效地避免内存碎片问题。
这次学习营主要学习的是裁剪版的RT-Thread Nano,它只有纯净的内核,仅仅包含线程、信号量、邮箱和定时器等基本功能,不包括文件系统、网络协议栈、图形界面等高级功能。相比于RT-Thread标准版,RT-Thread Nano的优势在于它更加轻量级,适用于那些资源受限的嵌入式系统。如果系统的需求仅仅要求基本的操作系统功能,那么RT-Thread Nano将是一个更好的选择。
通过这次RT-Thread Nano学习营的线下学习,初次尝试了RT-Thread在Keil MDK上的环境搭建、工程创建和配置,而且对RT-Thread的进程管理和IPC的应用有了一定的了解,后期准备细读以下RT-Thread nano内核源码。