1.記事一覧
記事は複数回に分けて投稿する予定です。AMD/XilinxのARM SoC(ZynqMP Kria K26 SOM)で動作する、AMD/Xilinx公式認定Ubuntuやベアメタル開発を勉強していく覚書きです。
ZynqMP(APU:Cortex-A53,Arm64) 組込みLinux入門編
ZynqMP(RPU:Cortex-R5,Arm32) RPU入門編
- 【ZynqMP】1. Cortex-R5でHelloworld
- 【ZynqMP】2. Cortex-R5でRTOS+GPIO ←本記事
2.RPUでGPIOを動かしてみる(with FreeRTOS)
今回は新Vitis Unified IDEのFreeRTOS BSPを用いてAXI-GPIO IPを動かしてみます。ドライバはベアメタルのAXI gpio standalone driver(GPIOドライバ)とGIC Standalone driver(GICドライバ)を使用します。Xilinxではベアメタルをstandaloneと呼びます。
今回の内容
- タスクを複数生成してLEDやスイッチの制御
- タスク間通信・通知(Queue、Semaphore)
- 割込みを使う
- SDT(System Device Tree)対応のドライバを使う
2-1. FreeRTOS上でXilinxドライバはそのまま使える?
RTOSで専用でないデバイスドライバを使えるのか?と気になると思います。VitisがBSP管理するFreeRTOSでは無条件では無いですが、Xilinxベアメタルドライバを使えるようになっています。特に割込み周りですが、そもそもFreeRTOSは割込みハンドラを登録するシステムコールAPIは無く、ベクタテーブル管理はporting実装者やArch依存になります。VitisのFreeRTOSはGICドライバのIRQハンドラテーブルを参照しているので、GICドライバの設定関数をそのまま使うことが可能です。
ただし、FreeRTOSが割込み管理していない代わりに、ユーザ割込みハンドラ内でタスクスケジューラを制御するOS関数をユーザが実装するといったルールがあったりするのでサンプルアプリでその内容に触れます。
FreeRTOSが使用する割込みは、Cortex-R5(CR5)の場合、コンテキストスイッチ用にSVC例外、カーネルタイマとしてTTCタイマのIRQを使用しているようです。これらの割込みをユーザ側で使わないことと、これらの例外・IRQに対してユーザ割込みの優先度を考慮する必要があります。優先度に関してですが、Arch依存で全然ルールが違うのでCR5の場合はまだ私自身全然わかっていません。一応こちらに説明があります(ただしCA9)。
割込みハンドラ内でシステムコールAPIを呼ぶときは”configMAX_API_CALL_INTERRUPT_PRIORITYより、低い優先度であること”とあります。この値はPlatformのvitis-comp.jsonでは”freertos_max_api_call_interrupt_priority”と表記されていて、デフォルトは18です。GICは優先度の数値が小さいほうが優先度が高いので31~19の間で割込みハンドラを設定することになると思われます。
2-2. 今回作るサンプルアプリ
機能:
- スイッチを押すたびにユーザLEDがトグルする
- ハートビートLEDが1Hz点滅する(死活監視)
3タスク1割込み構成で動かしていきます。優先度は上が高くなるようにすると以下のとおりです。
- GPIO割込みハンドラ
- スイッチタスク
- ユーザLED制御タスク
- ハートビートタスク
3.環境
3-1. 新Vitis Unified IDE(2023.2)とSDT
前回の記事でVitisが新しくなったことに触れました。新Vitisではビルドシステムやハードウェアメタ情報のフォーマットが一新しています。
今回、ベアメタルドライバの仕様が影響します。ハードウェアメタ情報をSystem-Device-Tree(SDT)で管理するようになったためです。SDT対応版と旧版での差分は各ドライバのサンプルコードを見るとわかるようになっています。
- AXI-GPIO : Github
マクロ “SDT”でコンパイルスイッチが追加されています。ここではSDT対応のコードを参考にサンプルアプリを作ってみます。
3-2. 前提条件
今回の環境です。
開発PC | Ubuntu20.04 LTS |
開発ツール | AMD/Xilinx社 Vivado・Vitis Unified IDE v2023.2.1 |
Xilinx IP | Vivadoにて以下を主に使用 ・AXI-GPIO |
BSP | FreeRTOS V10.5.1 xiltimer v1_3 |
ドライバ | AXI gpio standalone driver(Github) GIC Standalone driver(Github) |
ターゲットボード | Xilinx社製 Kria KR260 ロボティクススターターキット(SK-KR260-G) SOM : Xilinx社 Kria K26 SOM (Zynq UltraScale+ MPSoCベース) APU : Arm CA53x4 1333MHz arm64 RPU : Arm CR5x2 533MHz armv7-R SDRAM : DDR4 4GB |
GPIO向け回路部品 | ・ブレッドボード ・ジャンパ線 ・10k抵抗1点 ・プッシュスイッチ1個 |
4.VivadoでPLのGPIOを作成
注意:Vivadoの詳しい操作方法は説明しません。要点だけ載せます。
キャリアボード上のLEDとPmodにタクトスイッチを配線します。LEDの場所は矢印のところにあります。
RPU側(Cortex-R5)からアクセスするのでPS-PLポートはHPM0 LPD側にします。FPD側でもアクセスは出来ます。バスやインターコネクトのアクセス経路が変わります。
AXI-GPIO IPを3つ用意します。
- 点滅LED(ハートビート)
- ユーザLED(スイッチ入力によりON/OFFトグル)
- スイッチ入力(Pmod0の1番ピン)
ブロックが配置できたら、AXI-GPIO IPのベースアドレスを確認します。
- 点滅LED : 0x 80000000
- ユーザLED : 0x 80010000
- プッシュSW : 0x 80020000
プッシュスイッチの配線は認定UbuntuでPLのGPIOを使うの時と同じプルアップ抵抗回路を使います。
制約条件は以下の通りです。
出来たらビットストリーム出力して、Vitis用にビットストリームを含む設定でエクスポートします。
5.Vitis Unified IDEでプロジェクト作成
5-1. Platformコンポーネントを作成
Platformコンポーネント作成時は以下の設定のようにします。
今回はAPU側でLinuxを同時に動かすことはないのですが、いずれやることになると思うので、対応としてtimer周りのリソース競合を解消しておきます。認定UbuntuではFAN制御サービスが稼働していて、PWM制御用にTTC0が使われています。そのためBSP側でこれを使用しないようにします。
まず、xiltimerライブラリが使用しているTTCを0以外にします。
続いて、FreeRTOSのカーネルタイマの割当をTTC0以外にします。
注意:今回TTC3に設定したのですが、TTC1が実際は使われていました。ソースではxiltimerのインターバルタイマAPIを使用していたので、xiltimerの設定値が有効になるようです。(あくまでVitis-2023.2の話です。)
標準出力stdoutと標準入力stdinの設定がstandalone設定だと出てきたのですが、freertosを選ぶとありませんでした。直接確かめていませんが旧Vitisでは設定項目があったらしいので、おそらくまだ未実装と思われます。
【追記】Vitis 2024.1でstdoutとstdinの設定出来るようになっていました。
作成したら、ビルドしておきます。
5-2. Applicationコンポーネント作成
テンプレートからFreeRTOS Hello Worldを選択し、作成します。先ほど作成したPlatformを選択して作成します。特に気をつける設定はなく、前回の記事のように作成します。
ソースフォルダにサンプルのfreertos_hello_world.cが出来ますが、削除して新しく作り直します。ちなみに空のプロジェクトはPlatform設定でfreertosを選んでいると作成できないようでした。
6.サンプルアプリ
6-1. main
コードを説明します。まずはmain()から。3タスクとQueueを生成してからスケジューラを起動します。
/* FreeRTOS */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/* Xilinx */
#include "xparameters.h"
#include "xgpio.h"
#include "xil_exception.h"
#include "xscugic.h"
#include "xinterrupt_wrap.h"
#include "xil_printf.h"
/* others */
#include "stdint.h"
#include <FreeRTOSConfig.h>
#include <portmacro.h>
static TaskHandle_t hb_led_task = NULL;
static TaskHandle_t led_task = NULL;
static TaskHandle_t switch_task =NULL;
static QueueHandle_t sw_event_queue = NULL;
/* main ---------------------------------------------------------*/
int main(void)
{
BaseType_t ret;
ret = xTaskCreate( HeartBeatLedTask,
(const char *)"HBLedTask",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY,
&hb_led_task);
if(ret != pdPASS){
xil_printf("xTaskCreate Error.");
return 0;
}
ret = xTaskCreate( LedTask,
(const char *)"LedTask",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
&led_task);
if(ret != pdPASS){
xil_printf("xTaskCreate Error.");
return 0;
}
ret = xTaskCreate( SwitchTask,
(const char *)"SwitchTask",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 2,
&switch_task);
if(ret != pdPASS){
xil_printf("xTaskCreate Error.");
return 0;
}
sw_event_queue = xQueueCreate(QUEUE_LENGTH, sizeof(SwitchEvents_t));
configASSERT(sw_event_queue);
vTaskStartScheduler();
while(1);
}
6-2. スイッチタスク
処理内容
- AXI-GPIOのIO初期化処理
- GICv1(Pl390)の割込み設定
- AXI-GPIOの割込み許可
- 以下ループ↓
- 1.スイッチ用GPIO割込みハンドラからの通知を待つ(セマフォTake)
- 2.スイッチの端子を読んで押下中であればQueueに押下イベントを送信
- 3.1にもどる
Queueの受信側はユーザLEDタスクです。ちなみにこの処理だとチャタるので注意。
SDT対応になってからGICの設定関数が1関数でセッティング出来るようになっています。従来は以下のようにユーザ側が設定していました。
- 割込みコントローラの選択(GIC or AXI-INTC)
- 割込み番号の選択(自分で調べる)
- 割込みトリガの選択(エッジorレベル)
- 割込み要因とGlobal割込み許可
SDTから上記パラメータを生成して、自動設定してくれます。
/* Switch Control -----------------------------------------------*/
static void SwitchTask(void *pvParameters);
static void GpioIntHandler(void *CallbackRef);
static XGpio user_sw_gpio;
#define USER_SW_MASK (0x00000001U)
#define SW_GPIO_CH (1U)
#define SW_GPIO_ACTIVE (0U)
#define QUEUE_LENGTH (4U)
typedef enum {
kSwErr = 0,
kSw1Pushed,
} SwitchEvents_t;
static void SwitchTask(void *pvParameters)
{
int gpio_ret;
XGpio_Config *ConfigPtr;
uint32_t gpio_io_reg = 0U;
const SwitchEvents_t sw_event_push = kSw1Pushed;
/* AXI-GPIO初期化 */
ConfigPtr = XGpio_LookupConfig(XPAR_XGPIO_2_BASEADDR);
gpio_ret = XGpio_Initialize(&user_sw_gpio, XPAR_XGPIO_2_BASEADDR);
if (gpio_ret != XST_SUCCESS) {
xil_printf("led gpio driver init error.");
configASSERT(FALSE);
}
XGpio_SetDataDirection(&user_sw_gpio, SW_GPIO_CH, USER_SW_MASK);
/* GICv1のAXI-GPIO関連設定 */
gpio_ret = XSetupInterruptSystem( &user_sw_gpio,
&GpioIntHandler,
ConfigPtr->IntrId,
ConfigPtr->IntrParent,
XINTERRUPT_DEFAULT_PRIORITY);
if (gpio_ret != XST_SUCCESS) {
xil_printf("switch gpio driver init error.");
configASSERT(FALSE);
}
/* AXI-GPIOのピン入力割込有効 */
XGpio_InterruptEnable(&user_sw_gpio, USER_SW_MASK);
XGpio_InterruptGlobalEnable(&user_sw_gpio);
while(1){
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
gpio_io_reg = XGpio_DiscreteRead(&user_sw_gpio, SW_GPIO_CH);
if((gpio_io_reg & USER_SW_MASK) == SW_GPIO_ACTIVE){
xQueueSend(sw_event_queue,
&sw_event_push,
0UL);
}
}
}
6-3. GPIO割込みハンドラ
ハンドラ内の処理内容
- AXI-GPIO割込み要因のリセット
- スイッチタスクにセマフォをGive
- 再スケジュールの有無の制御
セマフォを呼びタスクの優先順位が変更される場合、スケジューラを実行して実行コンテキストを切り替える必要があるとのことで”portYIELD_FROM_ISR”を呼ぶ必要があります。
/* GPIO割込みハンドラ */
static void GpioIntHandler(void *CallbackRef)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
XGpio *GpioPtr = (XGpio *)CallbackRef;
/* AXI-GPIOの割込み要因クリア */
XGpio_InterruptClear(GpioPtr, SW_GPIO_CH);
/* スイッチタスクに通知 */
vTaskNotifyGiveFromISR(switch_task, &xHigherPriorityTaskWoken);
/* スケジューラ制御 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
6-4. ユーザLED制御タスク
処理内容
- GPIO初期化
- 以下ループ
- 1.Queueにデータが入るまで待つ
- 2.ボタン押下イベントならLEDをトグル
- 3.1に戻る
/* User LED Control ----------------------------------------------*/
static void LedTask(void *pvParameters);
static XGpio user_led_gpio;
#define USER_LED_MASK (0x00000001U)
#define LED_GPIO_CH (1U)
static void LedTask(void *pvParameters)
{
SwitchEvents_t sw_ev = kSwErr;
BaseType_t ret;
int gpio_ret;
LedStatus_t led_status = kLedOff;
/* AXI-GPIO初期化 */
gpio_ret = XGpio_Initialize(&user_led_gpio, XPAR_XGPIO_1_BASEADDR);
if (gpio_ret != XST_SUCCESS) {
xil_printf("gpio driver init error.");
configASSERT(FALSE);
}
XGpio_SetDataDirection(&user_led_gpio, LED_GPIO_CH, ~USER_LED_MASK);
XGpio_DiscreteClear(&user_led_gpio, LED_GPIO_CH, USER_LED_MASK);
while(1){
/* Switchタスクからの押下イベント待ち */
ret = xQueueReceive(sw_event_queue,
(SwitchEvents_t*) &sw_ev,
portMAX_DELAY);
if(ret != pdPASS){
xil_printf("xTaskReceive Error.");
configASSERT(FALSE);
}else {
switch(sw_ev){
case kSwErr:
xil_printf("Switch events Error.");
configASSERT(FALSE);
break;
case kSw1Pushed:
/* スイッチが押下されたのでLEDをトグル */
if(led_status == kLedOn){
led_status = kLedOff;
XGpio_DiscreteClear(&user_led_gpio,
LED_GPIO_CH,
USER_LED_MASK);
}else{
led_status = kLedOn;
XGpio_DiscreteWrite(&user_led_gpio,
LED_GPIO_CH,
USER_LED_MASK);
}
break;
default:
xil_printf("Switch events Error.");
configASSERT(FALSE);
break;
}
}
}
}
6-5. ハートビートタスク(死活監視)
LEDが1Hz点滅するだけです。だけですが、もし今までのすべての処理をシングルスレッドでやるとなったら結構面倒になるはずです。
/* Heart Beat LED ----------------------------------------------- */
static void HeartBeatLedTask(void *pvParameters);
static XGpio beat_led_gpio;
#define BEAT_LED_MASK (0x00000001U)
#define BEAT_LED_GPIO_CH (1U)
typedef enum {
kLedOff = 0,
kLedOn
} LedStatus_t;
static void HeartBeatLedTask(void *pvParameters)
{
int gpio_ret;
LedStatus_t led_status = kLedOff;
/* AXI-GPIO初期化 */
gpio_ret = XGpio_Initialize(&beat_led_gpio, XPAR_XGPIO_0_BASEADDR);
if (gpio_ret != XST_SUCCESS) {
xil_printf("gpio driver init error.");
configASSERT(FALSE);
}
XGpio_SetDataDirection(&user_led_gpio, BEAT_LED_GPIO_CH, ~BEAT_LED_MASK);
XGpio_DiscreteClear(&user_led_gpio, BEAT_LED_GPIO_CH, BEAT_LED_MASK);
while(1)
{
/* LED点滅 */
vTaskDelay(500/portTICK_PERIOD_MS);
if(led_status == kLedOff){
led_status = kLedOn;
XGpio_DiscreteWrite(&beat_led_gpio,
BEAT_LED_GPIO_CH,
BEAT_LED_MASK);
}else{
led_status = kLedOff;
XGpio_DiscreteClear(&beat_led_gpio,
BEAT_LED_GPIO_CH,
BEAT_LED_MASK);
}
}
}