Zephyrのdriverを作ってみよう③~単純なシリアル受信~

はじめに

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

|> zephyr/drivers/serial/uart_stellaris.c

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

今回は、割込みなしのUART (polling) で、データを受信する部分を写経していきます。

実装するAPI

前回、poll_outを実装しました。今回は、poll_inを実装します。

|> ${ZEPHYR_BASE}/include/uart.h

/** @brief Driver API structure. */
struct uart_driver_api {
    /** Console I/O function */
    int (*poll_in)(struct device *dev, unsigned char *p_char);
    void (*poll_out)(struct device *dev, unsigned char out_char);
...
}

シリアル受信

uart_driver_api構造体のpoll_in APIを実装します。

static const struct uart_driver_api uart_stellaris_driver_api = {
    .poll_in = uart_stellaris_poll_in,  // これから実装
    .poll_out = uart_stellaris_poll_out,  // 実装済み
...
}

UARTのpoll_in APIでは、UARTデバイスの受信バッファにデータが入っているかどうかを確認します。 データがない場合は-1を返し、データがある場合はパラメータのcに読み込んだ文字を詰めて0を返します。

static int uart_my_uart_poll_in(struct device *dev, unsigned char *c)
{
    volatile struct _uart *uart = UART_STRUCT(dev);

    if (uart->fr & UARTFR_RXFE)
        return (-1);

    /* got a character */
    *c = (unsigned char)uart->dr;

    return 0;
}

動作確認

受信処理を実装する前に、Zephyrのshellを有効にすると、poll_in API呼び出しで例外が発生してしまいます。

$ 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:~$ 
***** USAGE FAULT *****
  Illegal use of the EPSR
***** Hardware exception *****
Current thread ID = 0x20000400
Faulting instruction address = 0x0
Fatal fault in essential thread! Spinning...
QEMU 3.0.50 monitor - type 'help' for more information
(qemu) QEMU: Terminated

受信処理を実装した後、同じ設定でZephyrを起動すると、無事shellが動きます。

$ 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:~$ help
Please press the <Tab> button to see all available commands.
You can also use the <Tab> button to prompt or auto-complete all commands or its subcommands.
You can try to call commands with <-h> or <--help> parameter for more information.

おまけ

UART poll_in APIの実装と、shell subsystemから利用する方法を見てみましょう。 poll_inはsystem callとして提供され、shell subsystemのどこかからタイマ割り込みで、処理が開始されているはずです。

system call

まずは、UARTのAPIを深堀りしていきます。

|> zephyr/include/uart.h

/**
 * @brief Poll the device for input.
 *
 * @param dev UART device structure.
 * @param p_char Pointer to character.
 *
 * @retval 0 If a character arrived.
 * @retval -1 If no character was available to read (i.e., the UART
 *            input buffer was empty).
 * @retval -ENOTSUP If the operation is not supported.
 */
__syscall int uart_poll_in(struct device *dev, unsigned char *p_char);

static inline int _impl_uart_poll_in(struct device *dev, unsigned char *p_char)
{
    const struct uart_driver_api *api =
        (const struct uart_driver_api *)dev->driver_api;

    return api->poll_in(dev, p_char);
}

uart_poll_in()システムコールとして宣言されています。 ここでは宣言だけですが、uart_poll_in()の定義で、_impl_uart_poll_in()が呼び出されると推測できます。

_impl_uart_poll_in()では、driverのpoll_in() (さきほど実装したAPI) が呼ばれています。

uart_poll_in()の定義は、下記です (多分) 。

|> zephyr/drivers/serial/uart_handlers.c

Z_SYSCALL_HANDLER(uart_poll_in, dev, p_char)
{
        Z_OOPS(Z_SYSCALL_DRIVER_UART(dev, poll_in));
        Z_OOPS(Z_SYSCALL_MEMORY_WRITE(p_char, sizeof(unsigned char)));
        return _impl_uart_poll_in((struct device *)dev,
                                  (unsigned char *)p_char);
}

Z_SYSCALL_HANDLERは、可変長引数のハンドラ登録マクロです。引数に応じたハンドラ登録マクロが呼ばれる仕組みになっています。

|> zephyr/kernel/include/syscall_handler.h

#define Z_SYSCALL_HANDLER(...) \
        _SYSCALL_CONCAT(__SYSCALL_HANDLER, \
                        _SYSCALL_NARG(__VA_ARGS__))(__VA_ARGS__)

subsys/shell

次にsubsystemのshellを見ていきます。 今回は割込み駆動の設定を行っていないため、定期的にpoll_inを呼び出すような実装になっているはずです。

|> zephyr/subsys/shell/shell_uart.c

早速、それっぽいタイマハンドラが見つかりました。

static void timer_handler(struct k_timer *timer)
{
    u8_t c;
    const struct shell_uart *sh_uart = k_timer_user_data_get(timer);

    while (uart_poll_in(sh_uart->ctrl_blk->dev, &c) == 0) {  // ★
        if (ring_buf_put(sh_uart->rx_ringbuf, &c, 1) == 0) {
            /* ring buffer full. */
            LOG_WRN("RX ring buffer full.");
        }
        sh_uart->ctrl_blk->handler(SHELL_TRANSPORT_EVT_RX_RDY,
                       sh_uart->ctrl_blk->context);
    }
}

上記ソースコードの★の部分で、uart_poll_in()システムコールを行っています。 uart_poll_in()では、受信バッファにデータがあった場合に、0を返す実装になっていました。 UARTに受信データがあった場合、shellのリングバッファに受信したデータを詰めています。

タイマの初期化は、同ファイル内のinit()で行われています。

static int init(const struct shell_transport *transport,
        const void *config,
        shell_transport_handler_t evt_handler,
        void *context)
{
    const struct shell_uart *sh_uart = (struct shell_uart *)transport->ctx;
...
    if (IS_ENABLED(CONFIG_SHELL_BACKEND_SERIAL_INTERRUPT_DRIVEN)) {
        uart_irq_init(sh_uart);
    } else {
        // 今回はこちら
        k_timer_init(sh_uart->timer, timer_handler, NULL);
        k_timer_user_data_set(sh_uart->timer, (void *)sh_uart);
        k_timer_start(sh_uart->timer, RX_POLL_PERIOD, RX_POLL_PERIOD);
    }

    return 0;
}

参考

docs.zephyrproject.org