【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用

 

 

文章目录

前言

一、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在本地化方面做的更加优秀。


 

一、Keil MDK工程的搭建

学习营推荐的开发环境是RT-Thread Studio,非常的快捷高效。但是本人一直习惯Keil或VS Code,所以直接使用Keil作为开发环境。

1.安装RT-Thread软件包

  打开Keil,选择“Pack Installer”选项卡,然后在线安装RT-Thread软件包。

【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第1张图片

 

此外,如果无法在线安装,也可以到Keil官网下载,然后离线导入即可。

【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第2张图片

 2.使用RT-Thread软件包新建Keil工程

【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第3张图片

 3.配置rtconfig.h

为了方便调试,勾选“enable kernel debug configuration”选项。

为了能够使用rt_kprintf()函数在UART窗口打印调试信息,勾选“Using Console”选项。

【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第4张图片

4.解决RT-Thread中#error提示的TODO

#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()) */


// 以下省略

 5. 解决Linker的错误

Error: L6218E: Undefined symbol assert_param (referred from misc.o)

  •  引入头文件stm32f10x_conf.h
    #include 

     

  •  在工程Options的C/C++选项卡里,定义预处理宏“USE_STDPERIPH_DRIVER”【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第5张图片

 .\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 Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第6张图片


三、互斥信号量在线程同步中的应用

互斥信号量是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;
}

 在上面的示例中,如果不适用互斥信号量,会出现下图的情形。

【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第7张图片

 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 的加减操作互相穿插了,这中情况就可以通过应用互斥量解决(效果如下图)。

【开放原子训练营(第二季)RT-Thread Nano学习营 学习笔记】Keil工程的搭建及信号量在线程同步中的应用_第8张图片

使用信号量后串口的输出结果如下:

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内核源码。

 

参考资料

  1. RT-Thread nano 源码仓库 rtthread-nano/rt-thread at master · RT-Thread/rtthread-nano (github.com)
  2. RT-Thread官网的文档和视频  https://www.rt-thread.org

 

你可能感兴趣的:(RT-Thread,学习,笔记)