この記事では、[WildfireEmbedFire]「RTスレッドカーネルの実装とアプリケーション開発-STM32に基づく」について説明します。これは個人的な学習ノートとしてのみ使用されます。詳細な内容と手順については、元のテキストを確認してください(Wildfire Data Download Centerからダウンロードできます)。
スレッドの基本概念
RTスレッドスレッドは、独立したスレッドのコレクションと考えることができます。各スレッドは独自の環境で実行されます。常に1つのスレッドのみが実行されており、RT-Threadスケジューラーが実行するスレッドを決定します。スケジューラは各スレッドを継続的に起動および停止し、すべてのスレッドが同時に実行されているようです。スレッドとして、スケジューラーのアクティビティーについて何も知る必要はありません。スレッドの切り替え時にコンテキスト(レジスター値、スタックの内容)を保存するのはスケジューラーの主な責任です。これを実現するには、各RTスレッドスレッドに独自のスタックが必要です。スレッドが切り取られると、その実行環境がスレッドのスタックに保存されるため、スレッドが再度実行されたときに、最後に実行されていた環境をスタックから正しく復元できます。
RT-Threadのスレッドモジュールは、ユーザーに複数のスレッドを提供し、スレッド間の切り替えと通信を実現し、ユーザーがビジネスプログラムプロセスを管理するのに役立ちます。このようにして、ユーザーはビジネス機能の実現により多くのエネルギーを注ぐことができます。- オリジナル
スレッドスケジューラの基本概念
RT-Threadで提供されるスレッドスケジューラは、優先度ベースの完全プリエンプティブスケジューリングです。システムでは、割り込み処理機能、スケジューラのロック部分のコード、および割り込みを無効にするコードを除いて、システムの他の部分はできません。スレッドスケジューラ自体を含め、すべてがプリエンプティブになります。システムは、0から255までの合計256の優先度をサポートします。値が小さいほど、優先度が高くなります。0が最高の優先度であり、255は、通常はユーザーが使用しないアイドルスレッドに割り当てられます。リソースが限られている一部のシステムでは、実際の状況に応じて、8または32の優先順位のみをサポートするシステム構成を選択できます。システムでは、現在のスレッドよりも優先度の高いスレッドの準備ができると、現在のスレッドがすぐにスワップアウトされ、優先度の高いスレッドがプロセッサをプリエンプトして実行します。
-RT-Thread公式中国語マニュアル
スレッド状態の概念
スレッドの実行プロセスでは、プロセッサで一度に実行できるスレッドは1つだけです。実行中のプロセスとは別に、スレッドには、実行状態、非実行状態など、さまざまな異なる実行状態があります。RT-Threadリアルタイムオペレーティングシステムでは、スレッドには5つの状態が含まれており、オペレーティングシステムは実行状況に応じて状態を動的に自動的に調整します。RT-Threadの5つのスレッド状態は次のとおりです。
-RT-Thread公式中国語マニュアル
調子 | 説明 |
---|---|
RT_THREAD_INIT | スレッドの初期状態。スレッドが作成されたばかりで実行が開始されていないときはこの状態です。この状態では、スレッドはスケジューリングに参加しません。 |
RT_THREAD_SUSPEND | 一時停止およびブロックされました。スレッドはこの時点で一時停止されています。リソースが利用できないために待機が一時停止されているか、スレッドが一定期間アクティブに遅延して一時停止されている可能性があります。この状態では、スレッドはスケジューリングに参加しません。 |
RT_THREAD_READY | 準備完了状態。スレッドは実行中です。または、現在のスレッドの実行が終了してプロセッサを生成した後、オペレーティングシステムは実行する最も優先度の高いスレッドを探します。 |
RT_THREAD_RUNNING | 実行状態。スレッドは現在実行中です。シングルコアシステムでは、rt_thread_self()関数によって返されるスレッドのみがこの状態になります。マルチコアシステムでは、この制限の対象にはなりません。 |
RT_THREAD_CLOSE | スレッドの終了状態。スレッドの実行が終了すると、この状態になります。この状態のスレッドは、スレッドのスケジューリングに参加しません。 |
スレッド状態の移行
RT-Threadは、一連のオペレーティングシステムコールインターフェイスを提供するため、スレッドは5つの実行状態を切り替えることができます。次の図は、いくつかの状態間の切り替えの概略図です。
スレッドは、関数を呼び出すことによって
rt_thread_create/init
初期状態(RT_THREAD_INIT)にrt_thread_startup
入り、次に関数を呼び出すことによって準備完了状態(RT_THREAD_READY )に入ります。準備完了状態のスレッドが、、およびその他の関数を呼び出すrt_thread_delay
とき、またはリソースを取得できないため、スレッドは次のようになります。一時停止状態(RT_THREAD_SUSPEND);一時停止状態のスレッドは、タイムアウトを待機した後、または他のスレッドがリソースを解放したためにリソースを取得できなかった場合、準備完了状態に戻ります。一時停止状態のスレッドが呼び出されると、閉じた状態(RT_THREAD_CLOSE )に変わりますが、実行中のスレッドは、スレッドの最後の部分で関数を実行することにより、閉じた状態( RT_THREAD_CLOSE )に変わります。実行は終了します。rt_sem_take
rt_mb_recv
rt_thread_delete/detach
rt_thread_exit
-RT-Thread公式中国語マニュアル
共通のスレッド機能
以下は、一般的に使用されるスレッド操作関数です。
スレッド中断関数rt_thread_suspend()
スレッドがrt_thread_delayを呼び出すと、呼び出し元のスレッドはアクティブに一時停止します。rt_sem_takeやrt_mb_recvなどの関数を呼び出すと、使用できないリソースによって呼び出し元のスレッドも一時停止します。一時停止状態のスレッドで、待機しているリソースが(設定された待機時間を超えて)タイムアウトした場合、スレッドはこれらのリソースを待機せず、準備完了状態に戻ります。または、他のスレッドがスレッドを解放すると、リソースを待機している間、スレッドも準備完了状態に戻ります。
-RT-Thread公式中国語マニュアル
注:通常、この関数を使用してスレッド自体を一時停止することはできません。rt_thread_suspend関数を使用して現在のスレッドを一時停止する必要がある場合は、rt_thread_suspend()関数を呼び出した直後にrt_schedule()関数を呼び出して手動で実行する必要があります。スレッドコンテキストの切り替え。
- オリジナル
使用例(オリジナルコード一覧)
rt_kprintf("挂起 LED1 线程!\n");
uwRet = rt_thread_suspend(led1_thread);/* 挂起 LED1 线程 */
if (RT_EOK == uwRet)
{
rt_kprintf("挂起 LED1 线程成功!\n");
}else
{
rt_kprintf("挂起 LED1 线程失败!失败代码:0x%lx\n",uwRet);
}
スレッド再開関数rt_thread_resume()
スレッド回復とは、中断されたスレッドを再び準備完了状態にすることです。再開されたスレッドが、すべての準備完了状態のスレッドの中で最も優先度の高いリンクリストの最初の位置にある場合、システムはスレッドコンテキストを切り替えます。
-RT-Thread公式中国語マニュアル
使用例(オリジナルコード一覧)
rt_kprintf("恢复 LED1 线程!\n");
uwRet = rt_thread_resume(led1_thread);/* 恢复 LED1 线程! */
if (RT_EOK == uwRet)
{
rt_kprintf("恢复 LED1 线程成功!\n");
}else
{
rt_kprintf("恢复 LED1 线程失败!失败代码:0x%lx\n",uwRet)
}
スレッド関連のインターフェースの概要
「RT-Thread公式中国語マニュアル」より抜粋
関数 | スレッドセーフ | 割り込みルーチン | 機能詳細 |
---|---|---|---|
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); |
安全性 | 呼び出し不可 | この関数を呼び出すと、システムは動的ヒープメモリからスレッドハンドル(つまり、TCB、スレッド制御ブロック)を割り当て、パラメータで指定されたスタックサイズに従って動的ヒープメモリから対応するスペースを割り当てます。割り当てられたスタックスペースは、rtconfig.hで設定されたRT_ALIGN_SIZEメソッドに従って調整されます。 |
rt_err_t rt_thread_delete (rt_thread_tスレッド); |
安全性 | 呼び出し可能 | この関数を呼び出した後、スレッドオブジェクトはスレッドキューから削除され、カーネルオブジェクトマネージャーから削除されます。スレッドが占有していたスタックスペースも解放され、再利用されたスペースは他のメモリ割り当てに再利用されます。実際、rt_thread_delete関数を使用してスレッドインターフェイスを削除すると、対応するスレッドの状態がRT_THREAD_CLOSE状態に変更され、rt_thread_defunctキューに入れられます。実際の削除アクション(スレッド制御ブロックの解放とスレッドスタックの解放)を実行する必要があります。次回アイドルスレッドがアイドル状態になると、最後のスレッド削除アクションはアイドルスレッドによって完了します。rt_thread_initで初期化された静的スレッドは、このインターフェースを使用して削除することはできません。 |
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_thread_init関数は、静的スレッドオブジェクトを初期化するために使用されます。スレッドハンドル(またはスレッド制御ブロックポインタ)とスレッドスタックは、ユーザーが提供します。静的スレッドとは、スレッド制御ブロックとスレッド実行スタックが一般にグローバル変数として設定され、コンパイル時に決定および割り当てられ、カーネルがメモリスペースを動的に割り当てる責任を負わないことを意味します。ユーザーが提供するスタックヘッドアドレスはシステムアラインメントする必要があることに注意してください(たとえば、ARMでは4バイトアラインメントが必要です)。 |
rt_err_t rt_thread_detach (rt_thread_tスレッド); |
安全性 | 呼び出し可能 | この関数インターフェースはrt_thread_delete()関数に対応します。rt_thread_delete()関数によって操作されるオブジェクトはrt_thread_create()によって作成されるハンドルであり、rt_thread_detach()関数によって操作されるオブジェクトはrt_thread_init()によって初期化されるスレッド制御ブロックです。関数。同様に、スレッド自体がこのインターフェースを呼び出してスレッド自体から切り離してはなりません。 |
rt_err_t rt_thread_startup (rt_thread_tスレッド); |
安全性 | 呼び出し可能 | この関数が呼び出されると、スレッドの状態は準備完了状態に変更され、スケジューリングのために対応する優先キューに配置されます。新しく開始されたスレッドの優先度が現在のスレッドよりも高い場合、すぐにこのスレッドに切り替わります。 |
rt_thread_t rt_thread_self (void); |
安全性 | 呼び出し可能 | プログラムの実行中に、同じコードが複数のスレッドによって実行される場合があります。実行中、現在実行中のスレッドのハンドルは、次の関数インターフェイスを介して取得できます。注:現在の実行スレッドを正確に取得できないため、割り込みサービスルーチンでこの関数を呼び出さないでください。スケジューラーが開始されていない場合、このインターフェースはRT_NULLを返します。 |
rt_err_t rt_thread_yield (void); |
安全性 | 呼び出し可能 | 调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。 |
rt_err_t rt_thread_sleep (rt_tick_t tick); rt_err_t rt_thread_delay (rt_tick_t tick); |
安全 | 不可调用 | 这两个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。这个函数接受一个参数,该参数指定了线程的休眠时间(单位是OS Tick时钟节)。 |
rt_err_t rt_thread_suspend (rt_thread_t thread); |
安全 | 可调用 | 当线程调用rt_thread_delay,调用线程将主动挂起,当调用rt_sem_take,rt_mb_recv等函数时,资源不可使用也将导致调用线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其它线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。• 注:通常不应该使用这个函数来挂起线程本身,如果确实需要采用rt_thread_suspend函数挂起当前任务,需要在调用rt_thread_suspend()函数后立刻调用rt_schedule()函数进行手动的线程上下文切换。 |
rt_err_t rt_thread_resume (rt_thread_t thread); |
安全 | 可调用 | 线程恢复就是让挂起的线程重新进入就绪状态,如果被恢复线程在所有就绪态线程,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。 |
rt_err_t rt_thread_control (rt_thread_t thread, rt_uint8_t cmd, void* arg); |
安全 | 可调用 | 指示控制命令cmd当前支持的命令包括 RT_THREAD_CTRL_CHANGE_PRIORITY - 动态更改线程的优先级;RT_THREAD_CTRL_STARTUP - 开始运行一个线程,等同于rt_thread_startup()函数调用; RT_THREAD_CTRL_CLOSE - 关闭一个线程,等同于rt_thread_delete()函数调用。 |
void rt_thread_idle_init (void); |
不安全 | 不可调用 | 系统运行过程中必须存在一个最终可运行的线程,可以调用该函数初始化空闲线程 |
void rt_thread_idle_sethook (void (*hook)(void)); |
不安全 | 不可调用 | 当空闲线程运行时会自动执行设置的钩子函数,由于空闲线程具有系统的最低优先级,所以只有在空闲的时候才会执行此钩子函数。空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如rt_thread_delay() , rt_sem_take() 等可能会导致线程挂起的函数都不能使用。 |
线程设计
程序上下文
线程的5种状态构成了程序运行的上下文状态,RT-Thread中程序运行的上下文包括:
- 中断服例程:它运行在非线程的执行环境下,中断服务程序最好保持精简短小,因为中断服务是一种高于任何线程的存在;
- 普通线程:不应该使用死循环;
- 空闲线程:通常这个空闲线程钩子能够完成
一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。
真正的系统资源回收工作在idle线程(空闲线程)完成,所以。对于空闲线程钩子上挂接的程序,它应该:
- 不会挂起的idle线程;
- 不应该陷入死循环,需要留出部分时间用于系统处理僵尸线程的系统资源回收。
线程设计要点
在线程设计时,我们需要考虑到:
-
上下文环境
对于工作内容,首先需要考虑运行环境,多个工作内容是否有重叠,能否一起处理。例如键盘事件:正常 -
线程的状态跃迁
状态跃迁指的是线程运行状态的变化,在进行线程设计时,应该保证线程在不活跃的时候,必须让出处理器,即让它进入阻塞状态。 -
线程运行时间长度
线程运行时间长度被定义为,在线程所关心的一种事件或多种事件触发状态下,线程由阻塞态跃迁为就绪态执行设定的工作,再从就绪态跃迁为阻塞态所需要的时间(一般还应加上这段时间内,这个线程不会被其它线程所抢占的先决条件)。线程运行时间长度将和线程的优先级设计密切相关,同时也决定着设计的系统是否能够满足预计的实时响应的指标。
例如,对于事件A对应的服务线程Ta,系统要求的实时响应指标是1ms,而Ta的最大运行时间是500us。此时,系统中还存在着以50ms为周期的另一线程Tb,它每次运行的最大时间长度是100us。在这种情况下,即使把线程Tb的优先级抬到比Ta更高的位置,对系统的实时性指标也没什么影响(因为即使在Ta的运行过程中,Tb抢占了Ta的资源,但在规定的时间内(1ms),Ta也能够完成对事件A的响应)。——RT-Thread官方中文手册
线程管理实验
此实验参考原文对应实验代码,只包含
man()
函数,按键驱动及其他外设初始化相关代码下面未给出。
要想按键线程可以控制LED线程,前者的线程优先级必须高于后者。
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t led0_thread = RT_NULL;
static rt_thread_t key_thread = RT_NULL;
/******************************************************************************
* @ 函数名 : led0_thread_entry
* @ 功 能 : LED0线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void led0_thread_entry(void *parameter)
{
while(1)
{
LED0(ON);
rt_thread_delay(500); // 500个tick(500ms)
rt_kprintf("led0_thread 正在运行, LED0_ON\r\n");
LED0(OFF);
rt_thread_delay(500);
rt_kprintf("led0_thread 正在运行, LED0_OFF\r\n");
}
}
/******************************************************************************
* @ 函数名 : key_thread_entry
* @ 功 能 : 按键线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void key_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
while(1)
{
// KEY0 被按下
if(Key_Scan(KEY0_GPIO_PORT, KEY0_GPIO_PIN) == KEY_ON)
{
rt_kprintf("挂起 led0_thread。\n");
uwRet = rt_thread_suspend(led0_thread); // 挂起 led0_thread
if(uwRet == RT_EOK)
{
rt_kprintf("挂起线程成功!\n");
}
else
{
rt_kprintf("挂起线程失败!\n");
}
}
// WK_UP 被按下
if(Key_Scan(WK_UP_GPIO_PORT, WK_UP_GPIO_PIN) == KEY_ON)
{
rt_kprintf("恢复 led0_thread。\n");
uwRet = rt_thread_resume(led0_thread); // 恢复 led0_thread
if(uwRet == RT_EOK)
{
rt_kprintf("恢复线程成功!\n");
}
else
{
rt_kprintf("恢复线程失败!\n");
}
}
rt_thread_delay(20);
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个动态线程
led0_thread = // 线程控制块指针
rt_thread_create("led0", // 线程名字
led0_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(led0_thread != RT_NULL)
rt_thread_startup(led0_thread);
else
return -1;
// 创建一个按键线程
key_thread = // 线程控制块指针
rt_thread_create("key", // 线程名字
key_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
4, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(key_thread != RT_NULL)
rt_thread_startup(key_thread);
else
return -1;
}
遇到个小坑,野火的按键初始化中GPIO模式设置为浮空输入,这应该是因为野火开发板有硬件下拉,但正点原子没有硬件下拉,所以要设置为下拉输入;另外,野火的按键扫描函数没加消抖。
实验现象
キーKEY0
が押されると、led0_threadスレッドが一時停止され、キーWK_UP
が押されると、led0_threadスレッドが実行を継続します。(しかし、ボタンを短く押すと一時停止したり、再開に失敗したりすることがある理由はわかりません)