こんにちは、ENGかぴです。
VSCodeの拡張機能であるPico SDKを使用するとRaspberry Pi PicoにDMAを実装することができます。DMAはCPUとは独立して動作するためCPU負荷を減らしながらリアルタイム制御ができます。
本記事では、Pico SDK を使用した C/C++ プロジェクトを作成し、UART(シリアル通信)を DMA で制御する方法を説明しています。Debug Probe をシリアルモニターとして利用し、DMA による受信/送信の動作を確認します。
Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。
VSCodeのダウンロードとインストールの方法やVSCodeにPicoの開発環境を追加する方法は下記記事を参考にしてください。
Raspberry Pi Picoの開発をVSCodeで行う方法
Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。
DMAを実装する
DMAはCPUがデータの転送情報を設定する(初期化)とメモリとUART・SPI・ADCなど数バイトのデータを自動転送するハードウェアです。
DMAがCPUとは独立して動作するため、CPUの処理の負荷を減らすことができます。下記記事で紹介しているPIO(Programmable I/O)と同様のイメージで動作します。
Raspberry Pi PicoのPIOでGPIOを操作する
PIOはGPIOのように1ビットの操作をするのに対してDMAは1バイト以上のデータを自動で転送する使い分けになります。
DMAはPIOのようにアセンブリ言語でソースコードを作成する必要はありませんが、転送情報をCPUから指定する必要があります。
DMAを使用するとUARTのAPIでよく使用されている受信処理のuart_is_readable()関数、uart_getc()関数等が不要になり、送信処理ではuart_putc()関数等が不要です。
以下ではDMAを使ったUARTの受信と送信の指定方法について説明します。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
プロジェクトの生成

プロジェクトの生成方法はRaspberry Pi Picoの開発をVSCodeで行う方法で説明していますが、DMA supportにチェックを入れます。プロジェクトを生成するとデフォルトでDMA関連の初期化と定義が自動で生成されます。
受信の設定

DMA(受信)を使用する場合と使用しない場合のイメージで示しました。
図の右側はDMAを使用しない場合ですが、CPUの処理で受信データがUART RX FIFOに存在していることを確認して読み取る必要があります。そのため1バイトずつUART RX FIFOからデータを読み取る処理が発生します。
図の左側はDMAを使用する場合ですが、CPUからDMAに対して転送情報を初期設定として送るとDMAがCPUを関与せず転送情報に従って自動でUART RX FIFOからデータを読み出して指定の受信バッファに格納します。そのためCPU処理の負荷が少なくなります。
受信の初期化の例は以下の通りです。
#include "hardware/dma.h"
//UART FIFOから読み込む設定
chan_rx = dma_claim_unused_channel(true);
c_rx = dma_channel_get_default_config(chan_rx);
channel_config_set_transfer_data_size(&c_rx, DMA_SIZE_8); //転送サイズ:8ビット(1バイト)
channel_config_set_read_increment(&c_rx, false); //読み込み元(UART RX FIFO)は固定
channel_config_set_write_increment(&c_rx, true); //書き込み先(受信バッファ)のアドレスをインクリメントする
channel_config_set_dreq(&c_rx, uart_get_dreq(uart0, false)); //RX用DREQ
dma.hをインクルードしてDMA関係のAPIが使用できるようにします。DMA supportにチェックしているとデフォルトで生成されているので特に気にする必要はありません。
dma_claim_unused_channel()関数はDMAの12チャンネルから使用されていないチャンネルの番号を戻り値として返します。これにより空いているチャンネルを自動で取得することができるため管理が楽にできます。引数に空いているチャンネルがなかった場合エラー停止するかを指定します。trueの場合はエラー停止します。
dma_channel_get_default_config()関数は指定したチャンネルのデフォルト設定を取得します。引数にチャンネル番号を指定すると戻り値にチャンネルの情報が格納されるので、この情報を使って任意の設定を行います。
channel_config_set_transfer_data_size()関数はDMAが1回の転送で扱うデータ幅を指定します。第1引数にチャンネル情報を指定し、第2引数にデータ幅(8ビット・16ビット・32ビット)を指定します。UARTは1バイトでデータを受信/送信するためDMA_SIZE_8を指定しています。
channel_config_set_read_increment()関数はDMAが読み取り元のアドレスを転送ごとにインクリメントするかを設定します。第1引数にチャンネル情報を指定し、第2引数にインクリメントするかを指定します。受信の場合はUART FIFOのアドレスを固定して読み込むためインクリメントしないようにfalseを指定します。
channel_config_set_write_increment()関数はDMAが書き込み先のアドレスを転送ごとにインクリメントするかを設定します。第1引数にチャンネル情報を指定し、第2引数にインクリメントするかを指定します。受信の場合はUART FIFOから読み出したデータを受信バッファにアドレスをインクリメントしながら格納するためtrueを指定します。
channel_config_set_dreq()関数はDMAの動作トリガーを設定します。第1引数にチャンネル情報を指定し、第2引数にトリガー条件(DREQ番号)を指定します。
第2引数のuart_get_dreq()関数は対象のUARTの受信/送信のトリガーを指定します。第1引数にUARTのチャンネル番号、第2引数に受信/送信どちらかを指定します。受信(RX)の場合はfalse、送信(TX)の場合はtrueを指定します。
9行目はUART0を対象にUART RX FIFOに受信データが格納されると受信のトリガー(DREQ)を出してDMAが応答するようにする設定です。
次にDMAをスタートするために転送情報を指定します。
dma_channel_abort(chan_rx); //DMA停止(転送情報をリセット)
dma_channel_configure(
chan_rx, //RX用のDMAチャンネル番号
&c_rx, //チャンネル情報
&rcvdata.buf[0], //転送先のアドレス(write address)
&uart_get_hw(uart0)->dr, //読み込み元のアドレス(read address)
RX_BUF_SIZE, //転送回数
true //DMAスタート
);
dma_channel_abort()関数はDMAを停止し転送情報をリセットします。初期化時は不要ですが、一度スタートしたDMAの転送情報をリセットする場合に使用します。引数に停止するDMAのチャンネルを指定します。
dma_channel_configure()関数はDMAの転送情報を指定します。第1引数にDMAのチャンネルを指定します。第2引数にチャンネル情報のアドレスを指定します。第3引数に転送先のアドレスを指定します。UART RX FIFOから読み込んだデータを格納する受信バッファのアドレスを指定します。
第4引数は読み込み元のアドレスを指定します。ハードウェアの構造体でポインタ(アドレス)を返すuart_get_hw()関数にuart0を指定することでUART0のレジスタの構造体を参照し、その中のデータレジスタ(DR)を指定しています。このDRを読み込むことでUART RX FIFOのデータを取得します。
第5引数はDMAの転送回数を指定します。例では128回を指定していますが、途中で転送回数をリセットする場合は1行目のdma_channel_abort()関数でリセットして3行目のdma_channel_configure()関数で転送情報を指定し直す必要があります。
第6引数はDMA転送を直ちに開始するかを指定します。trueを指定するとDMAが転送開始します。falseを指定すると転送待ちで待機します。
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
送信の設定

DMA(送信)を使用する場合と使用しない場合のイメージで示しました。
図の右側はDMAを使用しない場合ですが、CPUの処理で送信データをUART TX FIFOに1バイトずつ書き込む必要があります。
図の左側はDMAを使用する場合ですが、CPUからDMAに対して転送情報を初期設定として送るとDMAがCPUを関与せず転送情報に従って自動でUART TX FIFOにデータを転送するためCPU処理の負荷が少なくなります。
送信の初期化の例は以下の通りです。送信は受信と同じ関数を使用しますが引数の指定が受信と反対になるイメージです。
chan_tx = dma_claim_unused_channel(true);
c_tx = dma_channel_get_default_config(chan_tx);
channel_config_set_transfer_data_size(&c_tx, DMA_SIZE_8); //転送サイズ:8ビット(1バイト)
channel_config_set_read_increment(&c_tx, true); //読み取り元(送信バッファ)のアドレスをインクリメントする
channel_config_set_write_increment(&c_tx, false); //書き込み先(UART TC FIFO)は固定
channel_config_set_dreq(&c_tx, uart_get_dreq(uart0, true)); // TX用DREQ
1行目から3行目は送信用のDMAチャンネルを確保して転送サイズを指定しています。
channel_config_set_read_increment()関数の第1引数にチャンネル情報を指定し、第2引数にインクリメントするかを指定します。送信の場合は読み取り元(送信バッファ)のアドレスをインクリメントしながら送信させるためtrueを指定します。
channel_config_set_write_increment()関数の第1引数にチャンネル情報を指定し、第2引数にインクリメントするかを指定します。送信の場合はUART TX FIFOに送信データを書き込むためアドレスは固定になります。そのためインクリメントしないようにfalseを指定します。
6行目のchannel_config_set_dreq()関数の第1引数に送信用のDMAチャンネルを指定する以外は受信と同様です。
次にDMAをスタートするために転送情報を指定します。
void dmaTxSet(uint8_t *dat, uint8_t sz){
dma_channel_configure(
chan_tx, //TX用のDMAチャンネル番号
&c_tx, //チャンネル情報
&uart_get_hw(uart0)->dr, //転送先のアドレス(write address)
dat, //読み込み元のアドレス(read address)
sz, //転送回数
true //DMAスタート
);
}
送信のDMAの転送情報の設定もdma_channel_configure()関数を使用します。
第1引数、第2引数は送信用に準備したチャンネル番号とチャンネル情報を指定します。第3引数は転送先のアドレスを指定します。UART TX FIFOに送信データを書き込むため、UART0のデータレジスタ(DR)のアドレスを指定しています。
第4引数は読み込み元のアドレスを指定します。送信バッファのアドレスを指定します。例ではdmaTxSet()関数(自作)の第1引数から取得したアドレスを指定しています。
第5引数は転送回数を指定します。例ではdmaTxSet()関数(自作)の第2引数から取得した転送回数を指定しています。
第6引数でDMAの転送を直ちに行うためtrueを指定しています。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
動作確認

Picoの電源を入れるとDMAの初期化を行い受信用のDMAが動作を開始します。
Pico Debugのシリアルモニターからの文字列を送るとDMAが受信したデータを受信バッファに転送します。受信バッファの内容を確認し、「LED-01-01」であればLED1を点灯します。また「CHK-OK-01」の文字列をDMAを使って返信し、シリアルモニターに返信データが表示されます。

次にシリアルモニターから「LED-01-00」を送信するとPicoはLED1を消灯し、「CHK-OK-00」を返信するのでシリアルモニターに返信データが表示されます。
「LED-01-23」のように01及び00以外の文字を送信するとシリアルモニターに返信データとして「CHK-NG-23」が表示されます。
「123456789」のように想定していない文字列を送信するとシリアルモニターに返信データとして「CHK-NG」が表示されます。
シリアルモニターからデータを送信すると受信用のDMAが動作し、受信バッファにデータが格納されることが確認できました。また、送信用のDMAが動作してシリアルモニターに返信データが表示されていることが確認できました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include <stdlib.h>
#define RX_BUF_SIZE 128
#define PIN_DO1 25
#define PIN_DO2 16
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define LED_ONOFF 50
#define TIM_RX_WAIT 20 //200ms
struct RING_MNG{
uint16_t wp;
uint16_t rp;
uint8_t buf[RX_BUF_SIZE];
};
dma_channel_config c_rx;
int chan_rx;
dma_channel_config c_tx;
int chan_tx;
RING_MNG rcvdata;
int16_t timled = LED_ONOFF;
int16_t timrcv = TIME_OFF;
uint32_t beforetimCnt;
uint8_t txbuf[] ="CHK-OK-00xx";
uint8_t txbuf2[] ="CHK-NGxx";
const uint8_t denchk[] = "LED-01-00";
uint8_t rxbuf[RX_BUF_SIZE];
void mainTimer(void);
void mainApp(void);
void chkden(void);
void dmaRxInit(void);
void dmaTxSet(uint8_t *dat, uint8_t sz);
int main()
{
stdio_init_all();
gpio_init(PIN_DO1); // ピン初期化
gpio_set_dir(PIN_DO1, GPIO_OUT); //DOピン
gpio_init(PIN_DO2); // ピン初期化
gpio_set_dir(PIN_DO2, GPIO_OUT); //DOピン
//UART FIFOから読み込む設定(受信)
chan_rx = dma_claim_unused_channel(true);
c_rx = dma_channel_get_default_config(chan_rx);
channel_config_set_transfer_data_size(&c_rx, DMA_SIZE_8); //転送サイズ:8ビット(1バイト)
channel_config_set_read_increment(&c_rx, false); //読み込み元(UART RX FIFO)は固定
channel_config_set_write_increment(&c_rx, true); //書き込み先(受信バッファ)のアドレスをインクリメントする
channel_config_set_dreq(&c_rx, uart_get_dreq(uart0, false)); //RX用DREQ
dmaRxInit(); //DMAを初期化してスタート(自作の関数)
// UART FIFOに書き込む(送信)
chan_tx = dma_claim_unused_channel(true);
c_tx = dma_channel_get_default_config(chan_tx);
channel_config_set_transfer_data_size(&c_tx, DMA_SIZE_8); //転送サイズ:8ビット(1バイト)
channel_config_set_read_increment(&c_tx, true); //読み取り元(送信バッファ)のアドレスをインクリメントする
channel_config_set_write_increment(&c_tx, false); //書き込み先(UART TC FIFO)は固定
channel_config_set_dreq(&c_tx, uart_get_dreq(uart0, true)); // TX用DREQ
while (true) {
mainTimer();
mainApp();
}
}
/* 受信用DMAの初期化 */
void dmaRxInit(void){
dma_channel_abort(chan_rx); //DMA停止(リセット)
//dma_hw->ints0 = 1u << chan_rx; // 割り込みクリア
dma_channel_configure(
chan_rx, //RX用のDMAチャンネル番号
&c_rx, //チャンネル情報
&rcvdata.buf[0], //転送先のアドレス(write address)
&uart_get_hw(uart0)->dr, //読み込み元のアドレス(read address)
RX_BUF_SIZE, //転送回数
true //DMAスタート
);
rcvdata.wp = 0;
rcvdata.rp = 0;
}
/* 送信用DMAの初期化(データの送信) */
void dmaTxSet(uint8_t *dat, uint8_t sz){
dma_channel_configure(
chan_tx, //TX用のDMAチャンネル番号
&c_tx, //チャンネル情報
&uart_get_hw(uart0)->dr, //転送先のアドレス(write address)
dat, //読み込み元のアドレス(read address)
sz, //転送回数
true //DMAスタート
);
}
/* タイマ管理 */
void mainTimer(void){
if( to_ms_since_boot(get_absolute_time()) - beforetimCnt > BASE_CNT ){
beforetimCnt = to_ms_since_boot(get_absolute_time());
//10msごとにここに遷移する
if( timled > TIME_UP ){
timled--;
}
if( timrcv > TIME_UP ){
timrcv--;
}
}
}
/* メイン処理 */
void mainApp(void){
int16_t sz;
int16_t i;
if ( dma_channel_is_busy(chan_rx) ){
rcvdata.wp = RX_BUF_SIZE - dma_channel_hw_addr(chan_rx)->transfer_count;
sz = rcvdata.wp - rcvdata.rp;
if( sz < 0) sz += RX_BUF_SIZE;
if( sz == 0 ){
timrcv = TIME_OFF;
}
else{
//データあり
if( timrcv == TIME_OFF ){
timrcv = TIM_RX_WAIT;
}
if( sz >= sizeof(denchk)-1 ){
for( i=0; i<sz; i++ ){
rxbuf[i] = rcvdata.buf[rcvdata.rp];
if( ++rcvdata.rp >= RX_BUF_SIZE ){
rcvdata.rp = 0;
}
}
chkden();
}
}
if( timrcv == TIME_UP ){
timrcv = TIME_OFF;
dmaRxInit();
}
}
else{
dmaRxInit();
}
if( timled == TIME_UP ){
timled = LED_ONOFF;
gpio_put(PIN_DO1, !gpio_get(PIN_DO1));
}
}
/* 電文のチェック */
void chkden(void){
uint8_t i;
bool ret = true;
uint8_t no;
uint8_t sz;
for( i=0; i<sizeof(denchk)-3; i++ ){
if( rxbuf[i] != denchk[i]){
ret = false;
break;
}
}
if(ret){
no = strtol((const char*)&rxbuf[7], NULL, 10);
txbuf[4]='O';
txbuf[5]='K';
switch (no)
{
case 0:
gpio_put(PIN_DO2, 0);
txbuf[7]='0';
txbuf[8]='0';
txbuf[9]= 0x0d;
txbuf[10]= 0x0a;
sz = 11;
break;
case 1:
gpio_put(PIN_DO2, 1);
txbuf[7]='0';
txbuf[8]='1';
txbuf[9]= 0x0d;
txbuf[10]= 0x0a;
sz = 11;
break;
default:
txbuf[4]='N';
txbuf[5]='G';
txbuf[7]= rxbuf[7];
txbuf[8]= rxbuf[8];
txbuf[9]= 0x0d;
txbuf[10]= 0x0a;
sz = 11;
break;
}
dmaTxSet(txbuf, sz);
}
else
{
txbuf2[6]= 0x0d;
txbuf2[7]= 0x0a;
sz = 8;
dmaTxSet(txbuf2, sz);
}
}
メインのファイルの内容をコピーして置き換えることで使用できます。
プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
最後まで、読んでいただきありがとうございました。

データ量が大きくなる場合や頻繁に通信する場合はCPUの処理が関与しないDMAを使用するほうがCPUの処理の負荷が少なくなるメリットがあります。