RT-Thread 中的多线程

前面有一篇我们写了一个按键控制 LED 显示的例子,现在我们尝试将该功能单独放入一个线程进行管理。

RT-Thread 线程管理和调度

RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如图 4-2 所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。

当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。

线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。

系统线程

系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。

1
2
3
4
5
msh >list_thread
thread   pri  status      sp     stack size max used left tick  error
-------- ---  ------- ---------- ----------  ------  ---------- ---
tshell    20  running 0x00000084 0x00001000    12%   0x00000004 000
tidle0    31  ready   0x00000044 0x00000100    34%   0x0000000a 000

空闲线程

空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。另外,空闲线程在 RT-Thread 也有着它的特殊用途:

若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。

空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

主线程

在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry(),用户的应用入口函数 main() 就是从这里真正开始的,系统调度器启动后,main 线程就开始运行,过程如下图,用户可以在 main() 函数里添加自己的应用程序初始化代码。

rt-thread/src/components.C 文件内:

140
141
142
143
144
int $Sub$$main(void)
{
    rtthread_startup();
    return 0;
}

系统启动后先从汇编代码 startup_stm32l475xx.s 开始运行,然后跳转到 C 代码执行该代码 $Sub$$main.在 rthtread_starup() 中执行了一些启动初始化工作:

216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
int rtthread_startup(void)
{
    rt_hw_interrupt_disable();

    /* 板级初始化:需在该函数内部进行系统堆的初始化 */
    rt_hw_board_init();

    /* 打印 RT-Thread 版本信息 */
    rt_show_version();

    /* 定时器初始化 */
    rt_system_timer_init();

    /* 调度器初始化 */
    rt_system_scheduler_init();

#ifdef RT_USING_SIGNALS
    /* 信号初始化 */
    rt_system_signal_init();
#endif

    /* 由此创建一个用户 main 线程 */
    rt_application_init();

    /* 定时器线程初始化 */
    rt_system_timer_thread_init();

    /* 空闲线程初始化 */
    rt_thread_idle_init();

    /* 启动调度器 */
    rt_system_scheduler_start();

    /* 不会执行至此 */
    return 0;
}

这部分启动代码,大致可以分为四个部分:

  1. 初始化与系统相关的硬件;
  2. 初始化系统内核对象,例如定时器、调度器、信号;
  3. 创建 main 线程,在 main 线程中对各类模块依次进行初始化;
  4. 初始化定时器线程、空闲线程,并启动调度器。

rt_application_init 函数创建了主线程。

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/* the system main thread */
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
#ifdef RT_USING_SMP
    rt_hw_secondary_cpu_up();
#endif
    /* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
    $Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
    main();
#endif
}

void rt_application_init(void)
{
    rt_thread_t tid;

#ifdef RT_USING_HEAP
    tid = rt_thread_create("main", main_thread_entry, RT_NULL,
                           RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
    RT_ASSERT(tid != RT_NULL);
#else
    rt_err_t result;

    tid = &main_thread;
    result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
                            main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
    RT_ASSERT(result == RT_EOK);

    /* if not define RT_USING_HEAP, using to eliminate the warning */
    (void)result;
#endif

    rt_thread_startup(tid);
}

点击查看大图 点击查看大图

线程管理

下图描述了线程的相关操作(具体函数的使用方法请查阅对应API文档):

RT-Thread 自动初始化机制

自动初始化机制是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。

在系统启动流程图中,有两个函数:rt_components_board_init() 与 rt_components_init(),其后的带底色方框内部的函数表示被自动初始化的函数,其中:

  1. “board init functions” 为所有通过 INIT_BOARD_EXPORT(fn) 申明的初始化函数。
  2. “pre-initialization functions” 为所有通过 INIT_PREV_EXPORT(fn)申明的初始化函数。
  3. “device init functions” 为所有通过 INIT_DEVICE_EXPORT(fn) 申明的初始化函数。
  4. “components init functions” 为所有通过 INIT_COMPONENT_EXPORT(fn)申明的初始化函数。
  5. “enviroment init functions” 为所有通过 INIT_ENV_EXPORT(fn) 申明的初始化函数。
  6. “application init functions” 为所有通过 INIT_APP_EXPORT(fn)申明的初始化函数。

用来实现自动初始化功能的宏接口定义详细描述如下表所示:

初始化顺序宏接口描述
1INIT_BOARD_EXPORT(fn)非常早期的初始化,此时调度器还未启动
2INIT_PREV_EXPORT(fn)主要是用于纯软件的初始化、没有太多依赖的函数
3INIT_DEVICE_EXPORT(fn)外设驱动初始化相关,比如网卡设备
4INIT_COMPONENT_EXPORT(fn)组件初始化,比如文件系统或者 LWIP
5INIT_ENV_EXPORT(fn)系统环境初始化,比如挂载文件系统
6INIT_APP_EXPORT(fn)应用初始化,比如 GUI 应用

初始化函数主动通过这些宏接口进行申明,如 INIT_BOARD_EXPORT(rt_hw_usart_init),链接器会自动收集所有被申明的初始化函数,放到 RTI 符号段中,该符号段位于内存分布的 RO 段中,该 RTI 符号段中的所有函数在系统初始化时会被自动调用。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int rt_hw_usart_init(void)  /* 串口初始化函数 */
{
     ... ...
     /* 注册串口 1 设备 */
     rt_hw_serial_register(&serial1, "uart1",
                        RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
                        uart);
     return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init);    /* 使用组件自动初始化机制 */

在新线程控制LED

前面我们在潘多拉STM32L4上实现了一个按键控制 LED 和蜂鸣器的例子, 现在我们让该功能独立存在于一个文件中并自动启动独立线程执行。

stm32l475-atk-pandora\applications\ 目录创建文件 key_control_led.c 文件,然后修改 SConscript 配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from building import *

cwd     = GetCurrentDir()

src = Split('''
main.c
key_control_led.c
''')

CPPPATH = [str(Dir('#')), cwd]

group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH)

Return('group')

接下来打开 Evn 生成新的 MDK5 工程 scons --target=mdk5:

1
2
3
4
> scons --target=mdk5
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...

实现的代码如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>

#define THREAD_STACK_SIZE 200 //线程栈大小(字节)

#define THREAD_TIMESLICE 40   //占用的滴答时钟数

#define NULL_LED -1
#define BEEP_TIME 3


#define LED_PIN_RED 		GET_PIN(E, 7)  //红灯管脚
#define LED_PIN_GREEN 	    GET_PIN(E, 8)  //绿灯管脚
#define LED_PIN_BLUE 		GET_PIN(E, 9)  //蓝灯管脚

#define KEY_OPEN_RED        GET_PIN(D, 10)  //点亮红灯按钮
#define KEY_OPEN_GREEN      GET_PIN(D, 9)   //点亮绿灯按钮
#define KEY_OPEN_BLUE       GET_PIN(D, 8)   //点亮蓝灯按钮

#define BEEP_PIN 			GET_PIN(B, 2)   //蜂鸣器控制管脚


static rt_thread_t key_led_thread = RT_NULL;
static rt_uint8_t thread_priority = 20;


//根据下标点亮LED
void change_lighting_led(int lightIndex)
{
    rt_base_t rgbs[] = {LED_PIN_RED, LED_PIN_GREEN, LED_PIN_BLUE};
    for(int i = 0; i < 3; i++)
    {
        if(i == lightIndex)
        {
            rt_pin_write(rgbs[i], PIN_LOW);
        }
        else
        {
            rt_pin_write(rgbs[i], PIN_HIGH);
        }
    }
}

//读取按键
void read_key_lighting_led()
{
    int beep_time_count = 0;
    rt_base_t keys[] = {KEY_OPEN_RED, KEY_OPEN_GREEN, KEY_OPEN_BLUE};
    
    rt_pin_write(BEEP_PIN, PIN_LOW);
    
    change_lighting_led(NULL_LED); //熄灭所有LED
    
    while (1)
    {
        for(int i = 0; i < 3; i++)
        {
            if(rt_pin_read(keys[i]) == PIN_LOW)
            {
                rt_thread_mdelay(50);  //去抖动
                if(rt_pin_read(keys[i]) == PIN_LOW)
                {
                    change_lighting_led(i);  //切换点亮的LED
                    beep_time_count = 0;
                
                    if(beep_time_count < BEEP_TIME)
                    {
                        rt_pin_write(BEEP_PIN, PIN_HIGH); 
                    }
                    else
                    {
                        rt_pin_write(BEEP_PIN, PIN_LOW); 
                    }
                }
            }
        }
        
        beep_time_count++;
        if(beep_time_count >= BEEP_TIME)
        {
            rt_pin_write(BEEP_PIN, PIN_LOW); 
        }

        rt_thread_mdelay(50);
    }
}

//线程函数
void key_control_led_entry(void *param)
{
    //设置管脚的模式
    rt_pin_mode(LED_PIN_RED, PIN_MODE_OUTPUT);
    rt_pin_mode(LED_PIN_GREEN, PIN_MODE_OUTPUT);
    rt_pin_mode(LED_PIN_BLUE, PIN_MODE_OUTPUT);

    rt_pin_mode(KEY_OPEN_RED, PIN_MODE_INPUT);
    rt_pin_mode(KEY_OPEN_GREEN, PIN_MODE_INPUT);
    rt_pin_mode(KEY_OPEN_BLUE, PIN_MODE_INPUT);

    rt_pin_mode(BEEP_PIN, PIN_MODE_OUTPUT);

    read_key_lighting_led();
}

//创建键盘控制LED线程
int create_key_control_led_thread(void)
{
    key_led_thread = rt_thread_create("led_test", key_control_led_entry, 
                    RT_NULL, THREAD_STACK_SIZE, thread_priority, THREAD_TIMESLICE);
    if(key_led_thread != RT_NULL)
    {
         rt_thread_startup(key_led_thread);
    }
    return 0;
}

INIT_APP_EXPORT(create_key_control_led_thread);

此时的 main 函数是空的,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>


int main(void)
{

    return RT_EOK;
}

此例中我们使用的是 rt_thread_create 函数进行动态创建,另外还有一个函数 rt_thread_init 可以初始化一个静态线程对象,有一点区别可以看源码,我们一般使用 rt_thread_create 函数创建线程。

1
2
3
4
5
6
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_uint8_t 的类型,所以不可用宏定义去定义优先级。

还有一个参数在使用上可能比较疑惑,那就是 stack_size 参数,设置多大合适呢?一般情况下我们先设置一个值,然后在 FinSH 控制台中使用 list_thread 命令查看使用情况:

可以看到 led_test 线程的优先级是 20 状态是 suspend(挂起)状态,栈的起始地址是 0x00000090 栈大小是 0x000000c8 使用率是 72% 线程剩余的运行节拍数是 0x28.

我们设置的这个栈大小就比较合适,一般情况下使用率在 70% 附近比较理想,当然可以根据情况调整,不要浪费资源。