Zephyrのdriverを作ってみよう④~device tree~

はじめに

Zephyrのdriverを作る方法を学びます。 まずは、qemu_cortex_m3をターゲットとした場合の、UART device driverを写経します。

|> zephyr/drivers/serial/uart_stellaris.c

全体で700行くらいなので、全てを単純に写経する、というよりポイントを押さえて行きたいと思います。

前回までで、ポーリングベースの送受信ができるUART Driverを実装しました。

tomo-wait-for-it-yuki.hatenablog.com

今回は、ハードウェアを定義するためのdevice treeを作成していきます。 device treeは他のモジュールとのインタフェースにも使われます。UARTの場合はshell subsystemがどのUARTデバイスを入出力に利用するか、の指定はdevice treeの記述を元に行われます。

device tree

Linuxでハードウェア構成を記述するための形式です。主にARMなど組込みで用いられるアーキテクチャで利用されます。

組込みLinuxでplatform deviceを記述する3つの方法(board file/device tree/ACPI DSDT)

Linux Foundation傘下のプロジェクトのためか、Zephyrもハードウェア構成の記述にdevice treeを用います。 文法は同じですが、扱われ方は、Linuxとはかなり違います。

Linuxでは、device treeは単独でblobを形成し、Linux kernelの外に置かれます。そして、Linux kernelはランタイムでこのblobにある情報を参照します。

一方、Zephyrでは、device treeを、ビルド時にheaderファイルを生成するために利用します。 ソフトウェアが扱うハードウェア構成は、ビルド時に固定されます。

UART device tree nodeの使われ方

まず、写経対象であるstellaris-uartのdevice tree nodeがどのように記述されるか、を見ます。

|> build/zephyr/qemu_cortex_m3.dts_compiled

                uart0: uart@4000c000 {
                        compatible = "ti,stellaris-uart";
                        reg = <0x4000c000 0x4c>;
                        interrupts = <0x05 0x03>;
                        status = "ok";
                        label = "UART_0";
                        current-speed = <0x1c200>;
                };

このdevice treeノードから生成されるheaderファイルは次のようになります。

|> build/zephyr/include/generated/generated_dts_board.h

/* uart@4000c000 */
#define CONFIG_UART_CONSOLE_ON_DEV_NAME                 "UART_0"
#define DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS      0x4000c000
#define DT_TI_STELLARIS_UART_4000C000_CURRENT_SPEED     115200
#define DT_TI_STELLARIS_UART_4000C000_IRQ_0             5
#define DT_TI_STELLARIS_UART_4000C000_IRQ_0_PRIORITY    3
#define DT_TI_STELLARIS_UART_4000C000_LABEL             "UART_0"
#define DT_TI_STELLARIS_UART_4000C000_SIZE              76
#define DT_TI_STELLARIS_UART_UART_0_BASE_ADDRESS        DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS
#define DT_TI_STELLARIS_UART_UART_0_CURRENT_SPEED       DT_TI_STELLARIS_UART_4000C000_CURRENT_SPEED
#define DT_TI_STELLARIS_UART_UART_0_IRQ                 DT_TI_STELLARIS_UART_4000C000_IRQ_0
#define DT_TI_STELLARIS_UART_UART_0_IRQ_PRIORITY        DT_TI_STELLARIS_UART_4000C000_IRQ_0_PRIORITY
#define DT_TI_STELLARIS_UART_UART_0_LABEL               DT_TI_STELLARIS_UART_4000C000_LABEL
#define DT_TI_STELLARIS_UART_UART_0_SIZE                DT_TI_STELLARIS_UART_4000C000_SIZE
#define UART_0_BASE_ADDRESS                             DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS
#define UART_0_CURRENT_SPEED                            DT_TI_STELLARIS_UART_4000C000_CURRENT_SPEED
#define UART_0_IRQ                                      DT_TI_STELLARIS_UART_4000C000_IRQ_0
#define UART_0_IRQ_PRIORITY                             DT_TI_STELLARIS_UART_4000C000_IRQ_0_PRIORITY
#define UART_0_LABEL                                    DT_TI_STELLARIS_UART_4000C000_LABEL
#define UART_0_SIZE                                     DT_TI_STELLARIS_UART_4000C000_SIZE

Driverでは、このheaderファイルで定義されたマクロを使って、デバイスパラメータの設定や、デバイス名の参照を行います。

static const struct uart_device_config uart_my_uart_dev_cfg_0 = {
        .base = (u8_t *)DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS,
        .sys_clk_freq = DT_UART_MY_UART_CLK_FREQ,
};

UART device tree binding

対応するdevice treeのbindingを見てみます。

|> zephyr/dts/bindings/serial

# at dts/bindings/serial
$ ls
altera,jtag-uart.yaml    nxp,kinetis-uart.yaml
arm,cmsdk-uart.yaml      nxp,lpc-usart.yaml
atmel,sam0-uart.yaml     sifive,uart0.yaml
atmel,sam-uart.yaml      silabs,gecko-leuart.yaml
atmel,sam-usart.yaml     silabs,gecko-uart.yaml
cypress,psoc6-uart.yaml  silabs,gecko-usart.yaml
intel,qmsi-uart.yaml     st,stm32-lpuart.yaml
microsemi,coreuart.yaml  st,stm32-uart.yaml
nordic,nrf-uarte.yaml    st,stm32-usart.yaml
nordic,nrf-uart.yaml     ti,cc32xx-uart.yaml
ns16550.yaml             ti,msp432p4xx-uart.yaml
nxp,imx-uart.yaml        ti,stellaris-uart.yaml
nxp,kinetis-lpsci.yaml   uart-device.yaml
nxp,kinetis-lpuart.yaml  uart.yaml

Zephyrでは、device treeのbindingはyaml形式で記述します。 pythonで解析しやすいですし、人間にとっても見やすいので、これはLinuxから進化しているところだと思います。

早速、これまで写経してきたstellaris-uartのbindingを見てみます。

|> ti,stellaris-uart.yaml

---
title: TI Stellaris UART
version: 0.1

description: >
    This binding gives a base representation of the TI Stellaris UART

inherits:
    !include uart.yaml

properties:
    compatible:
      constraint: "ti,stellaris-uart"

    reg:
      type: array
      description: mmio register space
      generation: define
      category: required

    interrupts:
      type: array
      category: required
      description: required interrupts
      generation: define
...

ファイル名と、compatibleが対応していそうです。後程、自分のDriverのbindingを作るときに試してみます。

UARTデバイスで共通して必要となるプロパティは、uart.yamlで定義されており、それを継承しています。

|> uart.yaml

---
title: Uart Base Structure
version: 0.1

description: >
    This binding gives the base structures for all UART devices

child:
    bus: uart

properties:
    compatible:
      type: string
      category: required
      description: compatible strings
      generation: define
    clock-frequency:
      type: int
      category: optional
      description: Clock frequency information for UART operation
      generation: define
    current-speed:
      type: int
      category: required
      description: Initial baud rate setting for UART
      generation: define
...

自作Driverのbinding作成

stellarisのbindingをコピーして、compatibleだけ変更します。

# at zephyr/dts/bindings/serial
cp ti,stellaris-uart.yaml tn,my-uart.yaml
properties:
    compatible:
-      constraint: "ti,stellaris-uart"
+      constraint: "tn,my-uart"

device tree nodeの修正

compatibleを上書きします。

|> qemu_cortex_m3.dts

&uart0 {
+   compatible = "tn,my-uart";
    status = "ok";
    current-speed = <115200>;
};

UART driverの修正

device treeから生成されるマクロを使用している部分を修正します。 例えば、次のような箇所です。

static const struct uart_device_config uart_my_uart_dev_cfg_0 = {
-   .base = (u8_t *)DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS,
+   .base = (u8_t *)DT_TN_MY_UART_4000C000_BASE_ADDRESS,
    .sys_clk_freq = DT_UART_MY_UART_CLK_FREQ,
};

ビルド

ビルドしてみます。

$ mkdir build && cd $_
$ cmake -GNinja -DBOARD=qemu_cortex_m3 ..
$ ninja
[3/112] Preparing syscall dependency handling

[107/112] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:       23548 B       256 KB      8.98%
            SRAM:        8196 B        64 KB     12.51%
        IDT_LIST:           8 B         2 KB      0.39%
[112/112] Linking C executable zephyr/zephyr.elf

通りましたね…。

生成されたheaderを確認してみます。

|> build/zephyr/include/generated/generated_dts_board.h

/* uart@4000c000 */
#define CONFIG_UART_CONSOLE_ON_DEV_NAME         "UART_0"
#define DT_TN_MY_UART_4000C000_BASE_ADDRESS     0x4000c000
#define DT_TN_MY_UART_4000C000_CURRENT_SPEED    115200
#define DT_TN_MY_UART_4000C000_IRQ_0            5
#define DT_TN_MY_UART_4000C000_IRQ_0_PRIORITY   3
#define DT_TN_MY_UART_4000C000_LABEL            "UART_0"
#define DT_TN_MY_UART_4000C000_SIZE             76
...

ちゃんとマクロが生成されています。

動作確認

$ ninja run
[1/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
***** Booting Zephyr OS 1.13.99 *****
Hello World! qemu_cortex_m3


uart:~$ 
  clear    help     history  resize   shell

問題ありません。

実験

compatibleから対応するbindingを見つけるのを、どうやっているか調べます。 まず、単純にyamlのファイル名とcompatibleをマッチングしている可能性があります。 実験してみます。

$ mv tn,my-uart.yaml ti,my-uart.yaml

これは、普通にビルドが通ります。

ということで、ちゃんとyamlcompatibleまで見に行っているみたいですね。

ざっと、pythonを見てみると、device treeからすべてのcompatibleを取得した後、対象のyamlディレクトリ内からcompatibleで引っかけているみたいです。

$ find . -name *.yaml | wc -l
232

全ファイル舐めても232しかないので、計算時間はあまり気にならなさそうです。