1. はじめに
前回紹介したリポジトリを使って、GPIO へのアクセスや移植性を維持した運用例について例示していきたいと思います。
公式のLチカを見たとき、「コレジャナイ」感、ありませんでした?
そして、DeviceTree とマクロの仕組みを頑張って読み解き、理解し始めたとき、
改めて「やりたいことは理解できるけど、やっぱりコレジャナイ」感…ワカリマス。
なので、おそらく多くの方が求めていたであろう、直感的でわかりやすい GPIO を叩き方の紹介です。
今回は nucleo_f401re を例に話を進めますが、引き続き bbc_microbit / bbc_microbit_v2 でも確認できます。
2. まずは公式のLチカを改めて眺める
初めてだと見慣れない多重マクロに面を食らうと思いますが、落ち着いて読むと Arudino とやってることはほとんど同じです。
- 関数の外で定義・宣言 == Arudino の各ヘッダー内部の実装
- ループの外で初期化 == Arduino の setup() 相当
- 以降のループや割込みイベント等で随時 GPIO を叩く == Arduino の loop() 相当
違いといえば、PA_0 とか、Serial だとか、使うつもりがなくても あらかじめ宣言されている所を、
明示的に宣言して確保する手続きを踏んでいるぐらいです(そこが重いんですが)。
以下は流し読みでOKです。
/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS 1000
/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)
/*
* A build error on this line means your board is unsupported.
* See the sample documentation for information on how to fix this.
*/
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void)
{
int ret;
bool led_state = true;
if (!gpio_is_ready_dt(&led)) {
return 0;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
return 0;
}
while (1) {
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
return 0;
}
led_state = !led_state;
printf("LED state: %s\n", led_state ? "ON" : "OFF");
k_msleep(SLEEP_TIME_MS);
}
return 0;
}
3. 実際の運用を想定した作り(仮)
では、ZephyrOpsPlaybookを使って、nucleo_f401re の LED を明滅させてみます。
既に playbook/scripts/west_env.bat がある場合はそのファイルでBOARD_TYPE=nucleo_f401re
を指定した上で rebuild & flash を実施。
(west_env.bat がない場合はこちら)
#include <zephyr/kernel.h>
#include "boards/unique.h"
int main(void)
{
gpio_pin_configure(GREEN_LED.port, GREEN_LED.pin, GPIO_OUTPUT_ACTIVE);
while (true) {
gpio_pin_toggle(GREEN_LED.port, GREEN_LED.pin);
k_msleep(1000);
}
return 0;
}
スッキリ…!
ネタばらし
上記ができるように、ヘッダー側で下記を定義をしています。
また、事前に device_is_ready() でポートの状態を確認した上で gpio_pin_configure() を呼ぶべきでしょう。
struct gpio_port_pin {
const struct device *const port;
const gpio_pin_t pin;
};
#define GPIO_PORT_PIN(_port, _pin) ((struct gpio_port_pin){ .port = DEVICE_DT_GET(DT_NODELABEL(_port)), .pin = (_pin) })
#define GPIOA_PIN05 GPIO_PORT_PIN(gpioa, 5) // PA_5, D13, SPI1_SCLK, LED1, PWM2/1, ADC1/5
#define GREEN_LED GPIOA_PIN05
さらにネタばらし
struct gpio_port_pin
は blinky でも使用している struct gpio_dt_spec
から gpio_dt_flags_t dt_flags;
を削除した下位互換です。
flag を使って pin の設定を動的に変える処理が多い設計であれば struct gpio_dt_spec
をそのまま使う方が良いと思います。
struct gpio_dt_spec {
/** GPIO device controlling the pin */
const struct device *port;
/** The pin's number on the device */
gpio_pin_t pin;
/** The pin's configuration flags as specified in devicetree */
gpio_dt_flags_t dt_flags;
};
4. 課題
先述の実装はシンプルではありますが、例えば、2023年度モデル用の基板と2024年度モデルの基板はほとんど共通で、pin-assign の一部が異なる、といったケースが非常によくあるかと思います。
そういったケースに備え、基板やモデル依存の処理は分けて実装する必要が出てきます。
5. 公式 Zephyr の管理方法
ここで、公式 Zephyr の管理方法を見てみます。
公式では各 SoC やモデルを boards という単位で依存部分を分けて管理しています。
(他にもシリーズモノは zephyr/dts や zephyr/soc にも派生して管理しています)
.
├── playbook/ # ZephyrOpsPlaybook リポジトリ
└── zephyr/ # 公式の Zephyr リポジトリ
└── boards/ # boards 依存の処理
├── bbc/ # ベンダー名: BBC
│ ├── microbit/ # ボード名: microbit
│ │ ├── bbc_microbit.dts # microbit の DeviceTree 本体
│ │ ├── bbc_microbit_defconfig # microbit の デフォルトの prj.conf
│ │ │ :
│ │ └── board.h # microbit 固有の定義
│ └── microbit_v2/ # ボード名: microbit_v2
│ └── 以下同様
├── st/ # ベンダー名: STM
│ └── nucleo_f401re/ # ボード名: nucleo_f401re
│ └── 以下同様
そして、ZephyrOpsPlaybook も同様に boards 以下に分けて管理を行っています。
6. ZephyrOpsPlaybook での実装例
実際に、main.c からの流れを追っていきます。
playbook/core/main.c
まずは playbook/include/boards/unique.h を include しており、
その後 main 関数の冒頭で、ボード依存の初期化処理 uni_board_init() を呼んでいます。
#include <zephyr/kernel.h>
#include "boards/unique.h"
int main(void)
{
uni_board_init();
while (true) {
k_msleep(1000);
}
return 0;
}
playbook/include/boards/unique.h
ここからはボード依存を含むことがわかるように、boards 以下のディレクトリに配置しています。
このヘッダーは、共通処理側からアクセスできるように unique.h というボカシた名称にしています。
#pragma once
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
struct gpio_port_pin {
const struct device *const port;
const gpio_pin_t pin;
};
#define GPIO_PORT_PIN(_port, _pin) ((struct gpio_port_pin){ .port = DEVICE_DT_GET(DT_NODELABEL(_port)), .pin = (_pin) })
// ここで board 毎に定義されている CONFIG で board 依存のヘッダーを include
#if defined(CONFIG_BOARD_BBC_MICROBIT)
#include "boards/bbc/microbit/bbc_microbit.h"
#elif defined(CONFIG_BOARD_BBC_MICROBIT_V2)
#include "boards/bbc/microbit_v2/bbc_microbit_v2.h"
#elif defined(CONFIG_BOARD_NUCLEO_F401RE)
#include "boards/st/nucleo_f401re.h"
#endif
playbook/include/boards/st/nucleo_f401re.h
ここで、ボード依存の定義を書き連ねる。
参照したデータシート:
um1724-stm32-nucleo64-boards-mb1136-stmicroelectronics.pdf 58ページ目
#pragma once
#include "drivers/button/drv_button_nucleo.h"
// CN7 odd
#define GPIOC_PIN10 GPIO_PORT_PIN(gpioc, 10) // PC_10, SPI3 SCLK
#define GPIOC_PIN12 GPIO_PORT_PIN(gpioc, 12) // PC_12, SPI3_MOSI
// VDD
// BOOT0
// -
// -
#define GPIOA_PIN13 GPIO_PORT_PIN(gpioa, 13) // PA_13
#define GPIOA_PIN14 GPIO_PORT_PIN(gpioa, 14) // PA_14
#define GPIOA_PIN15 GPIO_PORT_PIN(gpioa, 15) // PA_15, PWM2/1, SPI1_SSEL
// GND
#define GPIOB_PIN07 GPIO_PORT_PIN(gpiob, 7) // PB_7, UART1_RX, PWM4/2, I2C1_SDA
#define GPIOC_PIN13 GPIO_PORT_PIN(gpioc, 13) // PC_13, BUTTON1
#define GPIOC_PIN14 GPIO_PORT_PIN(gpioc, 14) // PC_14
#define GPIOC_PIN15 GPIO_PORT_PIN(gpioc, 15) // PC_15
#define GPIOH_PIN00 GPIO_PORT_PIN(gpioh, 0) // PH_0
#define GPIOH_PIN01 GPIO_PORT_PIN(gpioh, 1) // PH_1
// VBAT
#define GPIOC_PIN02 GPIO_PORT_PIN(gpioc, 2) // PC_2, SPI2_MISO, ADC1/12
#define GPIOC_PIN03 GPIO_PORT_PIN(gpioc, 3) // PC_3, SPI2_MOSI, ADC1/13
// CN7 even
#define GPIOC_PIN11 GPIO_PORT_PIN(gpioc, 11) // PC_11
#define GPIOD_PIN02 GPIO_PORT_PIN(gpiod, 2) // PD_2
// E5V
// GND
// -
// IOREF
// RESET
// +3.3V
// +5V
// GND
// GND
// VIN
// -
#define GPIOA_PIN00 GPIO_PORT_PIN(gpioa, 0) // PA_0, A0, ADC1/0, PWM2/1, UART2_CTS
#define GPIOA_PIN01 GPIO_PORT_PIN(gpioa, 1) // PA_1, A1, ADC1/1, PWM2/2, UART2_RTS
#define GPIOA_PIN04 GPIO_PORT_PIN(gpioa, 4) // PA_4, A2, ADC1/4, SPI1_SSEL
#define GPIOB_PIN00 GPIO_PORT_PIN(gpiob, 0) // PB_0, A3, ADC1/8, PWM1/2N
#define GPIOC_PIN01 GPIO_PORT_PIN(gpioc, 1) // PC_1, A4, ADC1/11
#define GPIOC_PIN00 GPIO_PORT_PIN(gpioc, 0) // PC_0, A6, ADC1/10
// CN10 odd
#define GPIOC_PIN09 GPIO_PORT_PIN(gpioc, 9) // PC_9, PWM3/4, I2C3_SDA
#define GPIOB_PIN08 GPIO_PORT_PIN(gpiob, 8) // PB_8, D15, PWM4/3, I2C1_SCL
#define GPIOB_PIN09 GPIO_PORT_PIN(gpiob, 9) // PB_9, D14, SPI2_SSEL, PWM4/4, I2C1_SDA
// AVDD
// GND
#define GPIOA_PIN05 GPIO_PORT_PIN(gpioa, 5) // PA_5, D13, SPI1_SCLK, LED1, PWM2/1, ADC1/5
#define GPIOA_PIN06 GPIO_PORT_PIN(gpioa, 6) // PA_6, D12, SPI1_MISO, PWM3/1, ADC1/6
#define GPIOA_PIN07 GPIO_PORT_PIN(gpioa, 7) // PA_7, D11, SPI1_MOSI, PWM1/1N, ADC1/7
#define GPIOB_PIN06 GPIO_PORT_PIN(gpiob, 6) // PB_6, D10, I2C1_SCL, PWM4/1, UART1_TX
#define GPIOC_PIN07 GPIO_PORT_PIN(gpioc, 7) // PC_7, D9, PWM3/2, UART6_RX
#define GPIOA_PIN09 GPIO_PORT_PIN(gpioa, 9) // PA_9, D8, USB_VBUS, PWM1/2, UART1_TX
#define GPIOA_PIN08 GPIO_PORT_PIN(gpioa, 8) // PA_8, D7, USB_SOF, PWM1/1, I2C3_SCL
#define GPIOB_PIN10 GPIO_PORT_PIN(gpiob, 10) // PB_10, D6, SPI2_SCLK, PWM2/3, I2C2_SCL
#define GPIOB_PIN04 GPIO_PORT_PIN(gpiob, 4) // PB_4, D5, SPI1_MISO, PWM3/1, I2C3_SDA
#define GPIOB_PIN05 GPIO_PORT_PIN(gpiob, 5) // PB_5, D4, SPI1_MOSI, PWM3/2,
#define GPIOB_PIN03 GPIO_PORT_PIN(gpiob, 3) // PB_3, D3, SPI1_SCLK, PWM2/2, I2C2_SDA
#define GPIOA_PIN10 GPIO_PORT_PIN(gpioa, 10) // PA_10, D2, USB_ID, PWM1/3, UART1_RX
#define GPIOA_PIN02 GPIO_PORT_PIN(gpioa, 2) // PA_2, D1, UART2_TX
#define GPIOA_PIN03 GPIO_PORT_PIN(gpioa, 3) // PA_3, D0, UART2_RX
// CN10 even
#define GPIOC_PIN08 GPIO_PORT_PIN(gpioc, 8) // PC_8, PWM3/3
#define GPIOC_PIN06 GPIO_PORT_PIN(gpioc, 6) // PC_6, UART6_TX, PWM3/1
#define GPIOC_PIN05 GPIO_PORT_PIN(gpioc, 5) // PC_5, ADC1/15
// U5V
// -
#define GPIOA_PIN12 GPIO_PORT_PIN(gpioa, 12) // PA_12, UART6_RX, UART1_RTS, USB_DP
#define GPIOA_PIN11 GPIO_PORT_PIN(gpioa, 11) // PA_11, UART6_TX, PWM1/4, USB_DM, UART1_CTS
#define GPIOB_PIN12 GPIO_PORT_PIN(gpiob, 12) // PB_12, SPI2_SSEL
// -
// GND
#define GPIOB_PIN02 GPIO_PORT_PIN(gpiob, 2) // PB_2
#define GPIOB_PIN01 GPIO_PORT_PIN(gpiob, 1) // PB_1, ADC1/9, PWM1/3N
#define GPIOB_PIN15 GPIO_PORT_PIN(gpiob, 15) // PB_15, PWM1/3N, SPI2_MOSI
#define GPIOB_PIN14 GPIO_PORT_PIN(gpiob, 14) // PB_14, PWM1/2N, SPI2_MISO
#define GPIOB_PIN13 GPIO_PORT_PIN(gpiob, 13) // PB_13, PWM1/1N, SPI2_SCLK
// AGND
#define GPIOC_PIN04 GPIO_PORT_PIN(gpioc, 4) // PC_4, ADC1/14
// -
// -
// 各モデル毎に作成する場合は、ここでエイリアスを定義しておくと各モデルとの移植性も維持できる
#define GREEN_LED GPIOA_PIN05
#define USER_BUTTON GPIOC_PIN13
playbook/boards/st/nucleo_f401re.c
そして、ボード依存の具体的な実装を記述。
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/device.h>
#include "boards/unique.h"
// struct gpio_dt_spec と同じデータ構造
struct gpio_spec {
struct gpio_port_pin gpio;
uint32_t flags;
};
// gpio_pin_configure を一括で行うための設定リスト
static const struct gpio_spec s_gpio_list[] = {
{ GREEN_LED, GPIO_OUTPUT_ACTIVE }
};
// 上記リストをまとめて初期化
void gpio_init_pin(void)
{
for (size_t i = 0; i < ARRAY_SIZE(s_gpio_list); ++i) {
if (device_is_ready(s_gpio_list[i].gpio.port) == false)
continue;
gpio_pin_configure(s_gpio_list[i].gpio.port, s_gpio_list[i].gpio.pin, s_gpio_list[i].flags);
}
}
// ボード固有の初期化の冒頭
void uni_board_init(void)
{
gpio_init_pin(); // gpio_pin_configure() をまとめて行う
drv_init_button(); // ドライバ群の初期化もここでまとめて行う
}
きれいな作りを目指すなら、ドライバの有効/無効等は Kconfig を設けた上で CMakeLists.txt 側での分岐や、#ifdef などで処理の分岐をさせましょう。
あまり分岐が多くないのであれば、各固有の header に記述しても良いかもしれません。
7. 最後に
今回は GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios)
のように dts に定義されたものを使うのではなく、別途マクロを定義して利用する方法を例示してみました。
「マクロが増えて余計大変なのでは?」と思うかもしれませんが、blinky のように使う場合、 dts に書き起こした上で DT_ALIAS(led0)
のようにアクセスする必要が出てきます。
しかし、基板とソースコードを公開するような企業や個人でなければ、そこまで流儀に従う必要もなく、
直接マクロとして定義してしまう方が、ソースコード内から定義元にジャンプして確認できるなど、長期的に見るとマクロのほうが効率が良いと感じています。
また、「回路屋さんが作成したエクセルベースの pin-assign 表をマクロに一括変換」みたいな仕組みを持っている会社もあるでしょうし、その辺の流用性も鑑みて検討してみてはいかがでしょうか。
(もちろん、エクセルからdtsに変換という道もあります😇)