nRF connect SDK によるシリアル通信(UART)設定方法

# 概要

Nordic社の BLE通信モジュール nRF54l15 を用いて、nRF connect SDK で シリアル通信を設定する方法について解説します。

今回の記事の内容は、Nordic Developer Academy の以下のレクチャーを参照しました。

https://academy.nordicsemi.com/courses/nrf-connect-sdk-fundamentals/lessons/lesson-4-serial-communication-uart/

#  EasyDMA による非同期UART通信

UART通信には幾つか方式がありますが、今回は EasyDMA を用いた非同期UART通信を使用します。


[EasyDMA](https://docs.nordicsemi.com/bundle/ps_nrf9160/page/easydma.html) とはCPUを介さずに AHBバスという経路でRAMにアクセスしてUART通信を行う手法のことです。
CPUの使用を節約できるので低消費電力に優れています。

非同期UART通信は、クロックとは同期せずに通信する方式のことです。
非同期をつけずにUART通信と言われた場合は、この非同期UART通信であることが多いです。
一方、同期UART通信はクロックと同期して通信する方式で、USART通信とも呼ばれます。

https://edn.itmedia.co.jp/edn/articles/2305/25/news032.html

# デバイスツリーによる設定

非同期UART通信を使用するには、KConfigファイルの設定が必要になります。prj.conf ファイルに以下を追加してください。

```:prj.conf 
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y
```

また、デバイスツリーに以下のように通信設定と端子設定を行います。

```dtsi
&uart20 {
    status = "okay";
    compatible = "nordic,nrf-uarte";
    current-speed = <115200>;
    parity = <UART_CFG_PARITY_EVEN>;  // 偶数パリティを設定
    stop-bits = <UART_CFG_STOP_BITS_1>;  // 1ストップビット
    data-bits = <UART_CFG_DATA_BITS_8>;  // 8 ビットデータ長
    rx-pin = <8>;
    tx-pin = <6>;
    rts-pin = <5>;
    cts-pin = <7>;
    hw-flow-control;  // ハードウェアフロー制御を有効化
};
```

compatible について、EasyDMAによるUART通信を使用する場合、UARTE に設定します。
もし EasyDMAを使用しない場合、UART に設定します。
nRF connect SDK では 低消費電力に優れた UARTE の使用が推奨されています


parity は以下の定義から選択します。parity の記載を省略した場合は、自動で UART_CFG_PARITY_NONE が設定されます。

```
/** @brief Parity modes */
enum uart_config_parity {
	UART_CFG_PARITY_NONE,   /**< No parity */
	UART_CFG_PARITY_ODD,    /**< Odd parity */
	UART_CFG_PARITY_EVEN,   /**< Even parity */
	UART_CFG_PARITY_MARK,   /**< Mark parity */
	UART_CFG_PARITY_SPACE,  /**< Space parity */
};
```

stop-bits は以下の定義から選択します。stop-bits の記載を省略した場合は、自動で UART_CFG_STOP_BITS_1 が設定されます。

```
/** @brief Number of stop bits. */
enum uart_config_stop_bits {
	UART_CFG_STOP_BITS_0_5,  /**< 0.5 stop bit */
	UART_CFG_STOP_BITS_1,    /**< 1 stop bit */
	UART_CFG_STOP_BITS_1_5,  /**< 1.5 stop bits */
	UART_CFG_STOP_BITS_2,    /**< 2 stop bits */
};
```

data-bits は以下の定義から選択します。data-bits の記載を省略した場合は、自動で UART_CFG_DATA_BITS_8 が設定されます。

```
/** @brief Number of data bits. */
enum uart_config_data_bits {
	UART_CFG_DATA_BITS_5,    /**< 5 data bits */
	UART_CFG_DATA_BITS_6,    /**< 6 data bits */
	UART_CFG_DATA_BITS_7,    /**< 7 data bits */
	UART_CFG_DATA_BITS_8,    /**< 8 data bits */
	UART_CFG_DATA_BITS_9,    /**< 9 data bits */
};
```

hw-flow-control はハードウェアフロー制御を使用する場合にのみ記載します。
ハードウェアフロー制御を使用しない場合、RTS端子とCTS端子の設定は無効になるので、設定上は割り当てらている端子を（上記の例では端子5番と端子7番を）GPIOやI2C端子など他の目的に使用することができます。

# UART起動

`uart_ctrl_ready`を実行することでUARTデバイスが起動します。
起動に成功すると1を返し、失敗すると0を返します。

```
if (!uart_ctrl_ready()){
    printk("UART device not ready\r\n");
    return 1 ;
}
```

また、受信機能を使用するならば、`uart_ctrl_rx_enable`でRX端子の受信を有効化します

```C:
uart_rx_enable(uart_dev ,rx_buf,sizeof(rx_buf),RECEIVE_TIMEOUT);
```

第１引数はUARTデバイスのインスタンス、第２引数は受信バッファ、第３引数は受信バッファのサイズ、第４引数は受信の制限時間です。

# UART割込み

UART割込みとは、UARTに関するイベントが発生した時に呼び出される割込み処理です。
イベントには以下の種類があります。

- UART_TX_DONE
    - 送信バッファのデータを全て送信完了した
- UART_TX_ABORTED
    - 制限時間切れ（ハードウェアフロー制御の場合のみ有効）や何らかの理由で送信が中止した
- UART_RX_RDY
    - 受信準備を完了した。以下の場合にこのイベントは発生する
        - データが受信バッファに格納された場合
        - 受信バッファが埋まった場合
        - uart_rx_disable() の後
        - 外部イベント (#UART_RX_STOPPED) により停止した後
- UART_RX_BUF_REQUEST
    - 連続受信の為の受信バッファを要求
- UART_RX_BUF_RELEASED
    - データの受信が終わった
- UART_RX_DISABLED
    - データ受信が無効になった
- UART_RX_STOPPED
    - データ受信が停止した


UART初期化の時にも説明した以下のKConfigファイルを設定していれば、UART割込みも有効化されています。

```:prj.conf 
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y
```

UART割込みのコールバック関数を作成し、それを登録すればUART割込みが使用できるようになります。

```C:
// UART割込みのコールバック関数を作成
void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
	switch (evt->type) {

	case UART_RX_RDY:
        // イベントに対応した処理を挿入する
		break;
	case UART_RX_DISABLED:
        // イベントに対応した処理を挿入する
		break;
    case UART_TX_DONE:
        // イベントに対応した処理を挿入する
		break;
    case UART_TX_ABORTED:
        // イベントに対応した処理を挿入する
        break;
    case UART_RX_BUF_REQUEST:
        // イベントに対応した処理を挿入する
        break;
    case UART_RX_BUF_RELEASED:
        // イベントに対応した処理を挿入する
        break;
    case UART_RX_STOPPED:
        // イベントに対応した処理を挿入する
        break;
	default:
		break;
	}
}

int main(void)
{
....

    // UART割込みのコールバック関数を登録
    ret = uart_callback_set(uart_dev, uart_cb, NULL);
    if (ret) {
        return 1;
    }
```

UART割込みの優先度はデバイスツリーで設定できます。

```
&uart20 {
    interrupts = <割込み番号 割込み優先度>;
};
```

割込み番号は各割込み機能にあらかじめ割り当てられている数値です。[ハードウェアマニュアルのinstantiation](https://docs.nordicsemi.com/bundle/ps_nrf54L15/page/_tmp/nrf54l15/autodita/global.instantiation.html)で値を確認してください。


また、起動後に割込みの優先度を変更したい場合は NVIC を使用します。

```
NVIC_SetPriority(UARTE0_UART0_IRQn, 新しい優先度);
```

また、起動後に割込みの一時停止と再開をしたい場合も NVIC を使用します。


```C:
// 割込み禁止
NVIC_DisableIRQ(UARTE0_UART0_IRQn);


// 割込み許可
NVIC_EnableIRQ(UARTE0_UART0_IRQn);
```
### 割込みイベント状態の取得

現在、UARTデバイスの割込みイベントの状態がどうなっているかは、以下の関数からでも取得できます。

```C:
// 送信割込みが準備完了かどうかを確認
if (uart_irq_tx_ready(uart_dev)) {
    // 送信処理
}

// 受信割込みが準備完了かどうかを確認
if (uart_irq_rx_ready(uart_dev)) {
    // 受信処理
}

// 送信が完了したかどうかを確認
if (uart_irq_tx_complete(uart_dev)) {
    // 送信完了処理
}

// 割込みが保留中かどうかを確認
if (uart_irq_is_pending(uart_dev)) {
    // 割込み処理
}
```

引数はUARTデバイスのインスタンスです。

# UARTの一時停止と再開

低消費電力の為には、UART通信機能を使用していないときには UARTを一時停止しておいたほうがいいでしょう。
Power Management用の `pm_device_action_run`関数を利用してモジュールの一時停止させたり、再開させたりできます。

この関数を使用するためには、KConfigファイルの設定が必要になります。
prj.conf ファイルに以下を追加してください。

```:prj.conf 
CONFIG_PM=y
```

必須ではないかもしれませんが、以下も設定しておきます。

```:prj.conf 
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y
```

なお、nRF54Hでは`CONFIG_CAF_POWER_MANAGER`を有効にすると`CONFIG_PM_DEVICE`と`CONFIG_PM_DEVICE_RUNTIME`が自動で有効化されます。

```:prj.conf 
CONFIG_CAF_POWER_MANAGER=y
```

またインクルードファイルに以下を指定します。

```
#include <zephyr/pm/device.h>
```

以上の設定をした上で `pm_device_action_run`関数を以下のように使用します。

```C:
// UARTデバイスを一時停止する
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_SUSPEND);

// UARTデバイスを再開する
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_RESUME);
```

第１引数はUARTの初期化時に作成したUARTデバイスのインスタンス、第２引数はパラメータです。

なお、以下のDevZoneの議論によれば、確実に消費電力させるためには、繰り返しサスペンドを行うことが推奨されています。

https://devzone.nordicsemi.com/f/nordic-q-a/114512/power-consumption-on-uart

```C:
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_SUSPEND);
k_msleep(100);
pm_device_action_run(modem_uart, PM_DEVICE_ACTION_RESUME);
k_msleep(100);
pm_device_action_run(modem_uart, PM_DEVICE_ACTION_SUSPEND);
```

更には `pm_device_action_run` を実行する前に、RX端子の受信機能も無効化すると更に消費電力を低減できます。

```C:
uart_rx_disable(uart_dev);
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_SUSPEND);
```

`uart_rx_disable`の第１引数は UARTデバイスのインスタンスです。

### Sleep時における端子の低消費電力モード

また、デバイスツリーもしくはoverlayファイルにおいて、sleep時の端子設定に`low-power-enable`を追加しておくことで、UART端子がSleep時には低消費電力モードになります。

例えば、nRF54l14 DK（nRF54l15開発ボード）では最初から以下のように `low-power-enable`が設定されています。

```:\ncs\v2.9.0\zephyr\boards\nordic\nrf54l15dk\nrf54l15dk_nrf54l_05_10_15-pinctrl.dtsi
&pinctrl {
	/omit-if-no-ref/ uart20_default: uart20_default {
		group1 {
			psels = <NRF_PSEL(UART_TX, 1, 4)>,
				<NRF_PSEL(UART_RTS, 1, 6)>;
		};
		group2 {
			psels = <NRF_PSEL(UART_RX, 1, 5)>,
				<NRF_PSEL(UART_CTS, 1, 7)>;
			bias-pull-up;
		};
	};

	/omit-if-no-ref/ uart20_sleep: uart20_sleep {
		group1 {
			psels = <NRF_PSEL(UART_TX, 1, 4)>,
				<NRF_PSEL(UART_RX, 1, 5)>,
				<NRF_PSEL(UART_RTS, 1, 6)>,
				<NRF_PSEL(UART_CTS, 1, 7)>;
			low-power-enable;
		};
	};
```

# 起動後の端子設定の変更

`pinctrl_update_states`関数を用いれば、起動後にでも端子設定を変更することができます。
トリッキーな使用方法として、端子が不足した時に、使用していないモジュールの端子を解放して、別の端子に割り当てるという使い方もありえます。
ただし、基本的には端子設定はデバイス初期化前に行うことが推奨されるので、動的な端子設定の変更をしても安定した動作をするかどうかは十分な確認が必要です。高い信頼性が要求される処理には使用しない方がいいでしょう。

もしくは不揮発化メモリにどちらのモジュールを使用するか記憶させてからリセットをかけ、起動時に不揮発化メモリからその情報を読み出して、選択したモジュールに合わせた端子設定を行うというような工夫が必要です。


端子設定を動的に変更するためには、KConfigファイルの設定が必要になります。
prj.conf ファイルに以下を追加してください。

```:prj.conf 
CONFIG_PINCTRL_DYNAMIC=y
```


また、あらかじめ代替ピン設定を overlayファイルに追加しておきます

```:overlay
&pinctrl {
    uart20_default_alt: uart0_default_alt {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 24)>,
                   <NRF_PSEL(UART_RTS, 0, 5)>;
        };
        group2 {
            psels = <NRF_PSEL(UART_RX, 0, 22)>,
                   <NRF_PSEL(UART_CTS, 0, 7)>;
            bias-pull-up;
        };
    };

    uart20_sleep_alt: uart0_sleep_alt {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 24)>,
                   <NRF_PSEL(UART_RX, 0, 22)>,
                   <NRF_PSEL(UART_RTS, 0, 5)>,
                   <NRF_PSEL(UART_CTS, 0, 7)>;
            low-power-enable;
        };
    };
};
```

以上の設定をした上で、コード内において代替ピン設定の状態を定義します。


```C:
// 代替ピン設定の状態を定義
static const struct pinctrl_state uart1_alt[] = {
    PINCTRL_DT_STATE_INIT(DT_NODELABEL(uart1_default_alt), PINCTRL_STATE_DEFAULT),
    PINCTRL_DT_STATE_INIT(DT_NODELABEL(uart1_sleep_alt), PINCTRL_STATE_SLEEP)
};
```

端子設定の変更前にはUARTデバイスを一時停止します。その後に`pinctrl_update_states`関数を使用します


```C:
void uart_pin_config_update(void)
{
    int err = 0;
    
    // デバイスを一時停止する
    err = pm_device_action_run(uart_dev, PM_DEVICE_ACTION_SUSPEND);
    if (err) {
        LOG_ERR("pm_device_action_run(SUSPEND) error %d.", err);
        return;
    }
    
    // ピン設定を更新する
    err = pinctrl_update_states(uart_config, uart1_alt, ARRAY_SIZE(uart1_alt));
    if (err) {
        LOG_ERR("pinctrl_update_states error %d.", err);
        return;
    }
    
    // デバイスを再開する
    err = pm_device_action_run(uart_dev, PM_DEVICE_ACTION_RESUME);
    if (err) {
        LOG_ERR("pm_device_action_run(RESUME) error %d.", err);
        return;
    }
}
```

上記の設定方法は以下を参照しました。

https://devzone.nordicsemi.com/f/nordic-q-a/117310/nrf52840-dynamic-pin-configuration-for-same-uart-instance-uart1-but-not-work?ReplyFilter=Answers&ReplySortBy=Answers&ReplySortOrder=Descending



# 起動後の通信設定の変更


動的に通信設定を変更するためには、KConfigファイルの設定が必要になります。
prj.conf ファイルに以下を追加してください。（初期状態で有効になっていることが多いので、設定しなくても変更できるかもしれないのですが、ここでは明示的に設定しておきます。）

```:prj.conf 
CONFIG_UART_USE_RUNTIME_CONFIGURE=y
```

以上の設定をした上で、構造体`uart_config`と`uart_configure`関数を用いて設定変更します。

```C:
// UARTデバイスを取得
const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart20));

// UART設定構造体
struct uart_config cfg;

// 現在の設定を取得
uart_config_get(uart_dev, &cfg);

// 設定を変更
cfg.baudrate = 115200;
cfg.parity = UART_CFG_PARITY_EVEN;
cfg.stop_bits = UART_CFG_STOP_BITS_2;
cfg.data_bits = UART_CFG_DATA_BITS_8;
cfg.flow_ctrl = UART_CFG_FLOW_CTRL_RTS_CTS;

// 新しい設定を適用
uart_configure(uart_dev, &cfg);
```

UART通信の設定変更中はUART通信を行わないようにしてください。
UARTデバイスを起動する前か、一時停止してから変更した方がいいでしょう。

# 高精度HFCLKソースの使用

内部発振器は最悪の場合±8%の誤差があるので、外部の高精度HFCLKソース（HFXO）を使用することでより高い精度を得られます。
ボーレートが1Mbpsを超える場合は、HFXOの使用が推奨されます。
ただし消費電力が上がるので、低消費電力を優先する場合やたまに通信が失敗しても問題なければ、使用しないという選択肢もあり得ます。

```C:
#include <zephyr/kernel.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/nrf_clock_control.h>
```

HFXOを要求する場合は以下を実行します

```C:
hfxo_mgr = z_nrf_clock_control_get_onoff(CLOCK_CONTROL_NRF_SUBSYS_HF);
int err = onoff_request(hfxo_mgr, &hfxo_cli);
```

UART通信を一時停止するときは、以下を実行してHFXOも停止しておきます

```C:
int err = onoff_release(hfxo_mgr);
```


上記は以下のQ&Aとサンプルを参照しました。


https://devzone.nordicsemi.com/f/nordic-q-a/111640/ble-event-trigger---keep-hfclock-always-on

https://github.com/nrfconnect/sdk-nrf/blob/e895510c7cad107d815070c02923e82564ed0337/samples/peripheral/radio_test/src/main.c#L15


別の方法としては、prj.confで`CONFIG_MPSL=y`を設定し、MPSLの以下のAPIを使用してもいいそうです。

```C:
// HFXOを要求
mpsl_clock_hfclk_request(NULL);  // コールバックが不要な場合はNULL

// HFXOが実行中かチェック
uint32_t is_running;
mpsl_clock_hfclk_is_running(&is_running);

// 使用後に解放
mpsl_clock_hfclk_release();
```

今回は MPSLではなく、他のモジュールから解放されないので完全に制御できる Zephyr RTOSの標準APIを使用しました。