(Raspberry Pi Pico)PIOへのDMA転送の切れ目

Raspberry Pi PicoのPIOやDMAは1クロックごとに処理が可能ですが(125MHzなら秒間1億2500万回)、データをDMAでPIOに送り続ける場合にautopull・PIOからDMAへのDREQ(データリクエスト)・DMAチャンネル間のchain trigger等の使用時でも待ち時間なしにデータ転送され続けるのだろうか、という疑問が湧いたので確認してみました。

環境はWindows 11とVS Code、PlatformIOでearlephilhower版Arduinoコア(Raspberry Pi Pico/RP2040)を使用してます。Raspberry Pi Pico C/C++ SDKでもだいたい同じだと思います、earlephilhower版がC/C++ SDKをベースに作られているので。

 

 

 

PIOのpullをautopullに任せた場合「TX FIFO→autopullでosrへ→osrからout」で転送待ちは発生するのか


DMAでTX FIFOに毎クロックデータを入れつつ、PIOのステートマシンではpullはautopullに任せて毎クロックoutする、とした場合、autopull待ちが1クロック発生したりするのだろうか、という疑問。

DMAで「0,1,2,3,0・・・」と0から3でループする32bitデータを毎クロックTX FIFOに送り続け、autopullからout pins, 32して毎クロック32bitをGPIOへ出力して結果を確認してみます。

出力結果はこう(タイミング図で表記してます、右方向が時間経過、1マスが1クロック)。

1クロックに1つずつデータが出力されています、autopull待ちなども発生していないようです。

 

では、DMA転送にchannel_config_set_dreqでDREQ_PIOn_TXnを設定して、TX FIFOが空になった時にPIOからDREQが来てDMA転送するようにした場合に「TX FIFOの空き確認→DREQ→DMA転送」でどこかで待ち時間は発生しないだろうか。

先程のテストにchannel_config_set_dreqを追加して検証。

こちらも待ち時間は発生していないようです。これならPIOのpullするペースが可変でもDMAに転送を任せられそうですね。待ち時間が発生してデータ転送ペースが落ちるという事態にならない。

 

以上は「32bit outしたらautopull」「毎クロック32bitをout pins」=「毎クロックautopull」した結果ですが、これを「8bit outしたらautopull」「毎クロック8bitをout pins」=「毎クロックautopull」という設定に変更して試してみます(最後に載せているソースが当初こっちの設定になっていたため今は32bitに修正してあります)。
DREQ待ち無しの場合は全部1クロックで待ちは発生しませんでしたが、DREQを使った場合はこう。

3の時だけ3クロックになってます、ループする時に遅れるということですかね?ということで0~3のループではなく0~16のループにして確認。

ループ箇所関係なく4回に1回3クロックに増えてました。4回に1回autopullではなく、毎クロックautopullしてるのにこの4回に1回ってなんでしょうね?しかも2クロック増えてる、1クロックではなく。
16bitにしたら32bitと同じ結果、2bitにしたら8bitと同じ結果になりました。32bitの1/4の8bitだから4回に1回というわけでもないらしい。

 

DMAチャンネル間のchain trigger使用時はそれぞれのDMA転送が切れ目なく続くのか

picoでは1つのDMAチャンネルの転送終了時に別のDMAチャンネルの転送を開始させるという、DMAチャンネルの連鎖(chain trigger)設定が可能です(channel_config_set_chain_to関数)。

この時に転送終了と転送開始の間に転送してない時間が発生してないか、を確認してみます。

2つのDMAチャンネルに「0,1,2,3」の4つのデータを転送するという設定をして(最初のテストと同じく毎クロック32bit autopullする設定)、転送終了時にもう一方のチャンネルの転送を開始する設定をしておきます、ここから片方のDMAチャンネルを転送開始することでDMA転送が終わらず続くようになります。

これで切れ目なく転送できてるかどうか。

あれ、「3」が1クロックで終わらず4クロック続いてますね。PIOが「3」をout pinsした後データが来ないので待機してる時間が3クロックある?

転送しっぱなしではなくDMAがPIOからのDREQを受け付ける場合はどうでしょうか。channel_config_set_dreqでDREQ_PIOn_TXnを設定してみます。

更に「3」の時間が伸びました、6クロック続いています。

てっきり切れ目なく転送が続くのかと思ったんですがそうでもないっぽいです、ステートマシンから毎クロックpullする場合はこのことを考慮した方がいい場合があるかもしれません。

 

その他の転送開始方法も試してみる

転送終了時再び転送開始したい場合によくある方法として、転送終了時に割り込みが発生するようIRQを設定し割り込みで呼ばれた関数内で転送開始処理をする、というのがあります、そちらも試してみます。

転送するデータはこれまでと同じ設定で、転送終了時の割り込み関数内でDMAのINTSnレジスタ(割り込みステータスレジスタ)のビットをクリアし、dma_channel_transfer_from_buffer_now関数で読み込みアドレスと転送回数を設定して転送を開始する、という条件で試してみます。

すみませんこれ図にする方がむしろわからんっすね。
言葉で説明すると、「3」の状態が44~48クロック続きました。続く長さは固定ではなく変動しっぱなしです。これはデュアルコアの2コア目の場合の結果で、1コア目だと更にバラつきます。

dma_channel_transfer_from_buffer_nowは読み込みアドレスと転送回数をレジスタに代入する関数ですが、代入を転送回数だけにすると4クロック縮んで40~44クロックになりました。

他の手段と比べると40クロック以上ってのは長いですね…まぁCPUはいろいろ仕事がありますし、割り込み終了後の原状復帰のための準備しながら他の処理止めて割り込むので仕方ないのですが…。

 

RP2040 Datasheetの"2.5.6. Example Use Cases"内の"2.5.6.2. DMA Control Blocks"では、レジスタの更新にCPUを使わずDMAでレジスタ更新&転送開始するという手法が紹介されています。当ブログのPicoによるNTSCのコンポジット映像信号モノクロ出力でも使用してて、そっちの記事で解説してます。
この方法についてもタイミングを確認してみます。

「3」の状態が8クロック続いています。chain triggerだけの時は4クロックでしたが、それにchannel trigger registerへの書き込み&転送開始までの時間が加わり更に4クロック増えています。それでもCPUでやるよりは断然早いですね、繰り返しても8クロックからほとんど変化しませんし。

 

ソースコード

最後に今回のテストのソースコードを。test.pioはPIOASMでtest.pio.hを生成する必要があります。

Arduino開発環境向けに書いたソースでありpicoのC/C++ SDKでは動作確認していません、main.cppをC/C++ SDKで動かす場合は#include <arduino.h>とloop()関数の記述を消してvoid setup()をint main()にする必要があります。

ArduinoでのPIOASMの使用やC/C++ SDKとの違いはこちらのページを参考にしました。

vabenecosi.blog.fc2.com

 

 

main.cpp

#include <Arduino.h>

#include "hardware/pio.h"
#include "hardware/dma.h"
#include "piotest.pio.h"

#define TEST_PIN_BASE 0
#define TEST_PIN_NUM 8

PIO pio = pio0;
uint offset;
int sm;
int dma_ch = 0;
int dma_ch2 = 1;

#define buf_size_shift (2)

const int ring_buf_size = ( 1 << buf_size_shift ); // buf_size_shiftが2なら2bit(0~3) 4なら4bit(0~15)
uint32_t ring_buf[ring_buf_size] __attribute__((aligned(ring_buf_size*sizeof(uint32_t))));    // channel_config_set_ringでループさせるためにアラインメント

uint32_t* controll_block_readaddr;

void dma_handler() {    // 割り込み処理
    dma_hw->ints0 = 1u << dma_ch;
    dma_channel_transfer_from_buffer_now(dma_ch, ring_buf, ring_buf_size);
}

static inline void pio_test_program_init(PIO pio, uint sm, uint offset, uint pin)
{
    pio_sm_config c = piotest_program_get_default_config(offset);
    pio_sm_set_consecutive_pindirs(pio, sm, TEST_PIN_BASE, TEST_PIN_NUM, true);
    for (int idx = 0; idx < TEST_PIN_NUM; idx++) {
        pio_gpio_init(pio, TEST_PIN_BASE + idx);
    }
    sm_config_set_out_pins(&c, TEST_PIN_BASE, TEST_PIN_NUM);
    sm_config_set_out_shift(&c, true, true, 32);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
}

void setup()
{
    for (int idx = 0; idx < ring_buf_size; idx++) {
        ring_buf[idx] = idx;
    }

    sm = pio_claim_unused_sm(pio, true);
    offset = pio_add_program(pio, &piotest_program);

    pio_test_program_init(pio, sm, offset, TEST_PIN_BASE);

    dma_channel_config dma_conf = dma_channel_get_default_config(dma_ch);
    channel_config_set_read_increment(&dma_conf, true);
    channel_config_set_write_increment(&dma_conf, false);
    channel_config_set_transfer_data_size(&dma_conf, DMA_SIZE_32);   // size (uint32_t)
    //channel_config_set_dreq(&dma_conf, pio_get_dreq(pio, sm, true)); // pioのTX FIFOが空の時dreq
    channel_config_set_ring(&dma_conf, false, buf_size_shift+2);  // read_addrの下[buf_size_shift+2]bitをループさせる +2はsizeof(uint32_t)が4(下2bit)なため

    switch(0) {
        case 0:
            dma_channel_configure(dma_ch, &dma_conf, &pio->txf[sm], ring_buf, 0xffffffff, false);
            break;
        case 1:
            // DMA chain test
            channel_config_set_chain_to(&dma_conf, dma_ch2); // 転送終了時のdma_ch2へのトリガー
            dma_channel_configure(dma_ch, &dma_conf, &pio->txf[sm], ring_buf, ring_buf_size, false);
            channel_config_set_chain_to(&dma_conf, dma_ch);  // 転送終了時のdma_chへのトリガー
            dma_channel_configure(dma_ch2, &dma_conf, &pio->txf[sm], ring_buf, ring_buf_size, false);
            break;
        case 2:
            // IRQ test
            dma_channel_configure(dma_ch, &dma_conf, &pio->txf[sm], ring_buf, ring_buf_size, false);
            dma_channel_set_irq0_enabled(dma_ch, true);
            irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
            irq_set_enabled(DMA_IRQ_0, true);
            break;
        case 3:
            // control block test
            channel_config_set_chain_to(&dma_conf, dma_ch2); // 転送終了時のdma_ch2へのトリガー
            dma_channel_configure(dma_ch, &dma_conf, &pio->txf[sm], ring_buf, ring_buf_size, false);
            channel_config_set_chain_to(&dma_conf, dma_ch2); // トリガー設定キャンセル(自分にトリガー)
            channel_config_set_read_increment(&dma_conf, false);
            controll_block_readaddr = (uint32_t*)ring_buf;
            dma_channel_configure(dma_ch2, &dma_conf, &dma_hw->ch[dma_ch].al3_read_addr_trig, &controll_block_readaddr, 1, false);
            break;
    }

    dma_channel_start(dma_ch);
}

void loop()
{
}


test.pio

.program test
    out pins,32