概要
Nordic社の BLE通信モジュール nRF54l15 を用いて、nRF connect SDK で シリアル通信を設定する方法について解説します。
今回の記事の内容は、Nordic Developer Academy の以下のレクチャーを参照しました。
EasyDMA による非同期UART通信
UART通信には幾つか方式がありますが、今回は EasyDMA を用いた非同期UART通信を使用します。
EasyDMA とはCPUを介さずに AHBバスという経路でRAMにアクセスしてUART通信を行う手法のことです。
CPUの使用を節約できるので低消費電力に優れています。
非同期UART通信は、クロックとは同期せずに通信する方式のことです。
非同期をつけずにUART通信と言われた場合は、この非同期UART通信であることが多いです。
一方、同期UART通信はクロックと同期して通信する方式で、USART通信とも呼ばれます。
デバイスツリーによる設定
非同期UART通信を使用するには、KConfigファイルの設定が必要になります。prj.conf ファイルに以下を追加してください。
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y
また、デバイスツリーに以下のように通信設定と端子設定を行います。
&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端子の受信を有効化します
uart_rx_enable(uart_dev ,rx_buf,sizeof(rx_buf),RECEIVE_TIMEOUT);
第1引数はUARTデバイスのインスタンス、第2引数は受信バッファ、第3引数は受信バッファのサイズ、第4引数は受信の制限時間です。
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割込みも有効化されています。
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y
UART割込みのコールバック関数を作成し、それを登録すればUART割込みが使用できるようになります。
// 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で値を確認してください。
また、起動後に割込みの優先度を変更したい場合は NVIC を使用します。
NVIC_SetPriority(UARTE0_UART0_IRQn, 新しい優先度);
また、起動後に割込みの一時停止と再開をしたい場合も NVIC を使用します。
// 割込み禁止
NVIC_DisableIRQ(UARTE0_UART0_IRQn);
// 割込み許可
NVIC_EnableIRQ(UARTE0_UART0_IRQn);
割込みイベント状態の取得
現在、UARTデバイスの割込みイベントの状態がどうなっているかは、以下の関数からでも取得できます。
// 送信割込みが準備完了かどうかを確認
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 ファイルに以下を追加してください。
CONFIG_PM=y
必須ではないかもしれませんが、以下も設定しておきます。
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y
なお、nRF54HではCONFIG_CAF_POWER_MANAGER
を有効にするとCONFIG_PM_DEVICE
とCONFIG_PM_DEVICE_RUNTIME
が自動で有効化されます。
CONFIG_CAF_POWER_MANAGER=y
またインクルードファイルに以下を指定します。
#include <zephyr/pm/device.h>
以上の設定をした上で pm_device_action_run
関数を以下のように使用します。
// UARTデバイスを一時停止する
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_SUSPEND);
// UARTデバイスを再開する
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_RESUME);
第1引数はUARTの初期化時に作成したUARTデバイスのインスタンス、第2引数はパラメータです。
なお、以下のDevZoneの議論によれば、確実に消費電力させるためには、繰り返しサスペンドを行うことが推奨されています。
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端子の受信機能も無効化すると更に消費電力を低減できます。
uart_rx_disable(uart_dev);
pm_device_action_run(uart_dev, PM_DEVICE_ACTION_SUSPEND);
uart_rx_disable
の第1引数は UARTデバイスのインスタンスです。
Sleep時における端子の低消費電力モード
また、デバイスツリーもしくはoverlayファイルにおいて、sleep時の端子設定にlow-power-enable
を追加しておくことで、UART端子がSleep時には低消費電力モードになります。
例えば、nRF54l14 DK(nRF54l15開発ボード)では最初から以下のように low-power-enable
が設定されています。
&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 ファイルに以下を追加してください。
CONFIG_PINCTRL_DYNAMIC=y
また、あらかじめ代替ピン設定を 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;
};
};
};
以上の設定をした上で、コード内において代替ピン設定の状態を定義します。
// 代替ピン設定の状態を定義
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
関数を使用します
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;
}
}
上記の設定方法は以下を参照しました。
起動後の通信設定の変更
動的に通信設定を変更するためには、KConfigファイルの設定が必要になります。
prj.conf ファイルに以下を追加してください。(初期状態で有効になっていることが多いので、設定しなくても変更できるかもしれないのですが、ここでは明示的に設定しておきます。)
CONFIG_UART_USE_RUNTIME_CONFIGURE=y
以上の設定をした上で、構造体uart_config
とuart_configure
関数を用いて設定変更します。
// 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デバイスを起動する前か、一時停止してから変更した方がいいでしょう。