皆さん こんにちは
ラズピコ研究員の moon です。
この記事は「初心者必見! Raspberry Pi Picoの C/C++デバッグ環境を容易に構築する」によって環境構築していることを前提にしています。
皆さんはDMAを使ったことがありますか?
私は以下の状況で良く使います。
・UARTやSPIの送信処理で送るデータの量が多い場合
・繰り返し送信する場合
今回は UART で DMA を使わない場合/使った場合 について送信処理してみます。
DMAとは
DMA は Direct Memory Access の略で、CPUを介さずにメモリとペリフェラル(周辺機器)間でデータを直接転送する機能です。
ラズピコのSDKではペリフェラルを使って送受信するための API が用意されています。
これらは一般的に同期関数で実行されるため、通信処理している間は関数から戻ってきません。
ですからその間、CPUが遊んでしまうことになり、時間がもったいないですね。
なおトラブルになることがあるため私は受信ではDMAを使いません。
DMAを使うデメリットはDMAを使うための知識を得る必要があることや初期設定がやや複雑であること、少しRAMを使うこと位でしょうか。
一度勉強しておけば、後は比較的簡単に扱えるようになると思います。
通信時間が比較的長いと感じる場合、ぜひDMAを使ってみてください。
接続
UART を使うにはPicoProbeとラズピコを以下のように接続する必要があります。
これはPicoProbeのUSBシリアル機能とラズピコ(マイコンRP2040)のUARTをクロス接続するものです。

新規プロジェクトを作成する
VSCodeを起動し、フォルダを開いている場合には ファイル – フォルダーを閉じる を選択します。
次にアクティビティーバーから Raspberry Pi Pico Project を選択し、サイドバーの New C/C++ Project を選択します。

Name は uart_dma_send とし、Board type で Pico を選択します。
後は Features の UART と DMA support 及び HW timer にチェックを入れます。
code generation options の Use project name as entry point file name にチェックを入れます。
Debugger は DebugProbe(CMSIS-DAP)[Default] を選択し、CMake Tools の Enable CMake-Tools extension integration にチェックを入れます。
そして右下の Create ボタンを押します。

キットの選択
キットはビルドツールと考えれば良いでしょう。
uart_dma_send のキットを選択してください と言われるので、その下に出て来る候補の一番下の 「Pico コンパイラの使用: C = C:\Users…」を選択します。
実行とデバッグ
アクティビティーバーの実行とデバッグを選択します。
メニューの 実行 – デバッグの開始を選択します。
uart_dma_send の起動対象を選択します と言われるので、その下の候補から uart_dma_send を選択します。
main()関数の中の stdio_init_all() のところでプログラムが停止するので、F5キーを押してプログラムが動作することを確認しておきます。
確認後、Shift + F5キーを押してプログラムを停止しておきます。
シリアルモニター
シリアルモニタ ーは Tera Term の簡易版と考えれば良いと思います。
Raspberry Pi Picoの拡張を入れたら自動的にインストールされるようです。
エディター下の、下部パネルの右の方にあるシリアルモニタ ーを選択します。

ポートで正しいCOMポートを選択し、ボーレートを1200に設定します。
(歯車アイコンを選択すると、データビットやパリティを選択することができますが今回はデフォルト値のまま動くようなので設定不要です)
歯車アイコンを選択して設定内容だけ確認しておきましょう。
今回の設定値は 1200bps, データ:8ビット, パリティ:なし, ストップ:1ビット です。
確認後「監視の開始」を選択しておきます。
CMakeLists.txtを編集する
CMakeLists.txtを編集します。
Ctrl + Shift + E でサイドバーにエクスプローラーを表示し、ツリーから CMakeLists.txt を選択します。
いつものおまじないでコンパイラの設定を「最適化なし」にします。
target_compile_options(uart_dma_send PRIVATE -O0)
launch.jsonを編集する
いつものおまじないで launch.json を編集します。
アクティビティーバーからエクスプローラーを選択します。(ショートカットは Ctrl + Shift + E)
.vscode下の launch.json の “name”: “Pico Debug (Cortex-Debug)” の最後の項目 ”oepnOCDLaunchCommands” の ] の後に , (カンマ)を追加し、
その次の行に “preLaunchTask”: “Compile Project” を追加します。

そして Ctrl + S でファイルを保存します。
uart_dma_send.cを編集する
プロジェクトジェネレーターで DMA support を選択したおかけで結構な初期設定を行ってくれています。
ただ、このサンプルではメモリーとメモリー間の転送ですから実感が湧きません。
こちらで用意した以下に示すプログラムをコピペしてください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/uart.h"
#include "hardware/dma.h"
#define UART_ID uart0
#define BAUD_RATE 1200
#define UART_TX_PIN 0
#define UART_RX_PIN 1
#define LED_PIN 25
#define PULSE_PIN 2
#define DMA_PIN 3
void uart_dma_init(uart_inst_t *uart);
void next_work();
const char send_msg[] = "0123456789abcdefghij0123456789ABCDEFGHIJ0123456789\n";
const int len = sizeof(send_msg) / sizeof(char) - 1;
int dma_ch;
bool dma_enabled = false;
bool timer_arrived = false;
bool repeating_timer_callback(struct repeating_timer *t) {
static int sw = 0;
if (sw) {
gpio_put(LED_PIN, 1);
gpio_put(DMA_PIN, 1);
dma_enabled = true;
} else {
gpio_put(LED_PIN, 0);
gpio_put(DMA_PIN, 0);
dma_enabled = false;
}
sw ^= 1;
timer_arrived = true;
return true;
}
int main()
{
stdio_init_all();
uart_init(UART_ID, BAUD_RATE);
uart_set_format(UART_ID, 8, 1, UART_PARITY_NONE);
uart_set_fifo_enabled (UART_ID, false); // FiFoを無効にする
gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);
uart_dma_init(UART_ID);
gpio_init(LED_PIN);
gpio_init(PULSE_PIN);
gpio_init(DMA_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
gpio_set_dir(PULSE_PIN, GPIO_OUT);
gpio_set_dir(DMA_PIN, GPIO_OUT);
uart_puts(UART_ID, send_msg);
struct repeating_timer timer;
add_repeating_timer_ms(-1000, repeating_timer_callback, NULL, &timer);
while (true) {
if (timer_arrived) {
timer_arrived = false;
if (dma_enabled) {
dma_channel_set_read_addr(dma_ch, send_msg, true);
next_work();
} else {
uart_puts(UART_ID, send_msg);
next_work();
}
}
}
}
void uart_dma_init(uart_inst_t *uart) {
// 空いているDMAチャンネルを使う
dma_ch = dma_claim_unused_channel(true);
// 指定したDMAチャンネルのデフォルト設定を取得する
dma_channel_config c = dma_channel_get_default_config(dma_ch);
// データ長の設定(8ビット)
channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
// DMA転送のトリガーになるDREQ信号を設定する
// UARTの送信が可能になるたびにDMA転送が自動的にトリガーされる
channel_config_set_dreq(&c, uart_get_dreq_num(uart, true));
// 読む側のアドレスを転送する毎にインクリメントする
channel_config_set_read_increment(&c, true);
// 書く側は(UARTのデータレジスタ固定だから)インクリメントしない
channel_config_set_write_increment(&c, false);
dma_channel_configure(
dma_ch, // DMAチャンネル
&c, // 設定用構造体
&uart_get_hw(uart)->dr, // write address
send_msg, // read address
len, // データ長
false // 転送を開始しない
);
}
// 次の仕事 : 100msec幅のハイのパルスを3つ生成する
void next_work() {
gpio_put(PULSE_PIN, 1);
sleep_ms(100);
gpio_put(PULSE_PIN, 0);
sleep_ms(100);
gpio_put(PULSE_PIN, 1);
sleep_ms(100);
gpio_put(PULSE_PIN, 0);
sleep_ms(100);
gpio_put(PULSE_PIN, 1);
sleep_ms(100);
gpio_put(PULSE_PIN, 0);
sleep_ms(100);
}
プログラムを動作させてみる
シリアルモニタ ーは監視の開始ボタンを押して受信を受け付けるようにしておきます。
ビルドしてプログラムを動作させます。
LEDは1秒周期で点滅します。
LEDが点灯している期間にDMAを使った通信を行い、消灯している間はDMAを使わずに通信します。
シリアルモニタ ーで51バイトのデータを1秒毎に受信できれば成功です。
モニタ ーできている場面を貼っておきます。

プログラムの解説
タイマ ー割り込みを使って1秒毎に DMAを使わない送信 と 使った送信 を交互に繰り返します。
送信開始の API を呼んだ後に GPIO でパルスを3つ出力してみます。
1200bpsで(改行コードを含む)51バイトのデータを送るので、通信時間は0.4秒強になります。
DMAを使った場合と使わない場合で、どんな違いが出るのか確認してみました。
GPIO3 がHの期間がDMA通信している期間(1秒毎)です。
GPIO2 は送信を開始する API から戻った後の処理に使うパルス信号(100msec幅で3つ)の出力ポートです。
送信を開始するAPIとは
DMA使用時: dma_channel_set_read_addr()
DMA不使用時: uart_puts()
です。
繰り返しDMA転送するために dma_channel_set_read_addr() が必要になります。
転送しながら読む側のアドレスがインクリメントされるため、次の転送開始時に読み出し元を再設定する必要があるからです。
このAPIは第3引数を true にするとただちに送信を開始してくれるため転送のスタートも兼ねています。
DMAを使わない場合は、通信の信号をほぼ出力し終えた後にパルスが出力されていますが、
DMAを使った場合は、ほぼ通信開始と同時にパルスが出力されているのがわかります。
オシロで波形を観測したのでプログラムと照らし合わせて見てください。
上から
TXD (UARTの送信) : GPIO0 (UART0 TX) でラズピコの1番ピンです。
Next work (パルス出力): GPIO2 でラズピコの4番ピンです。
DMA_H (DMAを使う間 H の幅の広いパルス出力): GPIO3 でラズピコの5番ピンです。
横軸のひと目盛りが1秒です。

補足:
FiFoを無効に設定する処理を追加しました。
FiFoを有効にすると、FiFo無しの場合に比べて早く次の仕事にかかることができ、DMAの効果が薄れるために あえてFiFoを無効に設定してみました。
いかがでしたか、皆さんの環境ではうまくDMA通信できましたか?
GitHubの こちら にプロジェクトを置きましたので参考になさってください。
お疲れさまでした。