IoT何をいまさら(99) ATSAMD51、DMAで1バイト転送成功までの長い道

Joseph Halfmoon

Microchip社のArm Cortex-M4コア搭載「近代的」ATSAMD51 マイコンの周辺機能をレジスタレベルでプログラムしてその操作を噛みしめておりますシリーズ、今回はDMACであります。昔からあるDMAですが、近代化したマイコンのそれは機能も増えていろいろできます。その分設定はメンドいです。たった1バイトの転送設定に大わらわ。

(実験に使用したC++ソースコード全文は末尾に)

DMAC(Direct Memory Access Controller)は、CPUを単純作業から解き放ち(オフロード)、もっとインテリジェンスの高い作業に集中していただくために「古代より」つかわれてきております。ぶっちゃけ、遅い周辺装置どもからのデータの入出力をCPUに代わって行ってくれたりするもの。典型的な使い方を想像するにこんな感じかと。

  1. 周辺装置が、ぽろぽろと入力してくるデータをある容量に達するまでDMACがメモリに転送して貯めていく。所定の容量溜まったら割り込みなどでCPUにお知らせする。そうしたらCPUは後続の処理を行う。
  2. CPUが周辺装置に送りたいデータブロックをメモリ上に準備してDMACに「後はよろしく」と頼む。DMACは周辺装置が一度に処理できる分量に合わせてデータブロックを小分けにしてちまちま転送をする。周辺装置への全転送が終了したら、割り込みなどでCPUに完了報告する。
  3. 周辺装置が入力したデータを別な周辺装置にCPUの手を煩わさずダイレクトに転送したいときに、DMACに直送をお願いする。

上記のような仕組みなので、転送元の場所(アドレス)と転送先の場所、1回の転送の分量、転送の回数など、最低知らせないとならない情報があります。しかし、組み込み用のマイクロコントローラ(MCU)内蔵のDMACとは言え、近代的なものはすべからく高機能、逆に言えば前述の情報程度では足りませぬ。

ATSAMD51 マイコンのDMAC機能

CPUに「代わって」転送を行うDMACは、他の多くの周辺回路とは一線を画した「立ち位置」におります。なんといってもCPU同様

バスマスタ

になれる存在です。他の周辺回路がバスのスレーブ側にいらっしゃるのとは多いに違うのであります。そしてバスへの接続も特別です。多くの周辺回路はペリフェラル用のバスであるAPB(ATSAMD51マイコンの場合4つもあります)のどれかにぶら下がっているのに対して、DMACは高速なバス・マトリックス(AHB)側にいらっしゃいます。そしてその接続口も一つではないです。そしてさらに、メモリ(主記憶SRAM)上に自身のDescriptorとよぶ制御構造を持つ関係からか、メモリとのダイレクトなインタフェースまで持っているようです。なんともゴージャス。そして

  • 周辺装置間
  • 周辺装置とメモリ間
  • メモリ間

の全てで転送可能です。

転送のキッカケは「誰か」のDMACへの「お願い」ですが、以下の種類があります。

  • CPUからのお願い(ソフトウエア・トリガ)
  • イベントシステムからのイベントお知らせ
  • 一部の周辺は各周辺専用のDMAリクエストを持つ

以前にやりましたが、ATSAMD51のイベントシステムには、周辺装置のほとんどがなんらかの形で接続しています。これを使えばほとんど誰でもDMACにお願いできる、っと。

幅広く多くの周辺回路に門を開いているDMACですが、同時に処理できるチャネルが少なければ争奪戦となってしまいます。ATSAMD51の場合、

充実の32チャンネル

まで使えます。まあこんだけあれば、足らないことは無いんでないの。

チャネルが多ければ、チャネル間でブツかるケースもあるでしょう。そんなこともお見通し、チャネル間の優先順位も4レベルあり、その制御も充実してます。

また、処理によっては、1か所から1か所への決まった長さの転送では不足する場合もあるでしょう。ココからアソコへ動かしたあと、別なトコロからまた別なトコロにサイズも変えて転送したいとか、要求は複雑なのであります。しかしATSAMD51マイコンのDMACはできます。メモリ上にDescriptorとよぶ構造を設け、それをリンク(チェーン)していけるので、DMACのレジスタの数などに依存せず複雑な転送パターンを記述可能であります。

ここまで書いただけでも疲れました。詳しくはMicrochip社のデータシートをご覧ください。しかし嫌な予感もします。

機能が多いということは、設定しなければならないことも多いんじゃね

はい、その通り。

CPUトリガのメモリ間転送、1バイトだけの設定例

実験に使用したソース全文は末尾に掲げました。以下には、そのソースから設定部分を抜き出し、それぞれ説明してまいりたいと思います。

デスクリプタ領域の確保

以下にデスクリプタ領域の確保例を掲げました。この程度ではハッキリ言って本格利用にはどこも足らない、最低線です。みてみましょう。

DmacDescriptor descSection[MAX_DMA_CH] __attribute__ (( aligned (16) ));
volatile DmacDescriptor wbSection[MAX_DMA_CH] __attribute__ (( aligned (16) ));

まず後ろの方にある アライメントの指示が普通の構造体とは違います。データシートによるとアライメントは128bit境界に置かねばならない筈なので aligned (16) としてみました。ただ、DmacDescriptorの定義をしらべると aligned(8)が付加されていました。データシートと違うじゃん。ここは安全めに16と。

そして定義されている DmacDescriptor タイプを使って、デスクリプタ領域が2個確保してあります。最初の1個は初期設定を記述するためのデスクリプタ領域で、もう1個は、途中経過や結果をDMACがストアするためのデスクリプタ領域です。そちらには volatile 属性つけました。DMA転送は長期間にわたったりもするので、途中で中断したりすることもありえます。その際、転送途中の状態がもう一つの方のデスクリプタに書き込まれます。後でそれを使って中断したところからまた再開ということもできるわけです。1個の領域に重ねてしまうこともアリですが、その場合、初期設定値が上書きされてしまうので同じことをもう一回やりたい、というときには再設定しないとなりません。

上記では、必要なチャネル数だけの単純な配列としてメモリ領域を確保しています。この部分は最低必要です。しかし、各チャネルとも、次々にデスクリプタをチェーンしていけるので、複雑な転送パターンを行う場合には、そのチェーン(リンク)するデスクリプタを並べておくメモリ領域も必要となります。今回は使っておらないです。

デスクリプタの設定

デスクリプタの領域を確保しただけでは動きませぬ。実際にデスクリプタ内に転送元、転送先、転送回数、転送単位、リンクする次のデスクリプタなどを書き込んでおかねばなりませぬ。実験でのその部分はこんな感じ。

descSection[0].DESCADDR.reg = (uint32_t)descSection;
descSection[0].DSTADDR.reg = (uint32_t)dstMemory;
descSection[0].SRCADDR.reg = (uint32_t)srcMemory;
descSection[0].BTCNT.reg = 1; //SINGLE Half word
descSection[0].BTCTRL.bit.STEPSIZE = 0x0; // 1 byte step.
descSection[0].BTCTRL.bit.STEPSEL = 0;
descSection[0].BTCTRL.bit.DSTINC = 0;  // Disable
descSection[0].BTCTRL.bit.SRCINC = 0;  // Disable
descSection[0].BTCTRL.bit.BEATSIZE = DMAC_BTCTRL_BEATSIZE_BYTE_Val;
descSection[0].BTCTRL.bit.BLOCKACT = DMAC_BTCTRL_BLOCKACT_NOACT_Val;
descSection[0].BTCTRL.bit.EVOSEL = DMAC_BTCTRL_EVOSEL_DISABLE_Val;
descSection[0].BTCTRL.bit.VALID = 1;

DMAC本体レジスタの設定

デスクリプタ書いているだけでいい加減疲れてきましたが、デスクリプタは単なるメモリ上の構造体なので、それを読み取るDMAC本体レジスタの設定なしには動きませぬ。その設定例がこちら。

void setupDMA() {
  int dmaMCLK = MCLK->AHBMASK.bit.DMAC_;
  if (dmaMCLK != 1) {
    Serial.printf("DMA MCLK: Disable!\r\n");
    return;
  }
  DMAC->CTRL.bit.DMAENABLE = 0; // DMA Disable
  DMAC->CTRL.bit.SWRST = 1; // Software RESET
  while(DMAC->CTRL.bit.SWRST == 1) {} // Wait for the RESET process finished.
  DMAC->BASEADDR.reg = (uint32_t)descSection;
  DMAC->WRBADDR.reg = (uint32_t)wbSection;
  Serial.printf("BASE_ADDR=0x%08x\r\n", DMAC->BASEADDR.reg);
  DMAC->CTRL.reg = 0xF00; // All LVL enabled.
  DMAC->Channel[0].CHCTRLA.bit.ENABLE = 0; // CH0 Disable
  DMAC->Channel[0].CHCTRLA.bit.SWRST = 1; // CH0 Software Reset
  while(DMAC->Channel[0].CHCTRLA.bit.SWRST == 1) {} // Wait for the CH0 RESET process finished.
  DMAC->CTRL.bit.DMAENABLE = 1; // DMA Enable
}

他の泡沫?な周辺回路と違い、DMACは中心部に鎮座しているので、デフォルトでクロックは供給されておるようです。そして普通の周辺とは違いCPUからのアクセス用のクロックもAHB経由用です。上記では、クロックが生きていることを確認するだけで、設定はしていません。

設定は、DMAC全体と、それぞれのチャネルの2段階で行う必要があります。ここではチャネル0のみを使い、ほぼほぼRESET時初期値の設定のままできる「1バイト転送1回」を前提に設定しています。先ほどのデスクリプタのありかを指定して、全レベルOKとしている以外には、明示的にはあまり具体的なことは設定していません。一応レジスタが初期値になっていることを担保するためにDMACおよびチャネルに対するSoftware RESETをかけています。最低限の初期化です。

ATSAMD51マイコンの通例により、イネーブル状態では書き換えできない部分が多いのでディセーブルにしてから設定していきます。また、Software RESETかけた場合は処理完了を待つための待ちも必要です。

ソフトウエアトリガの設定

本格的に周辺回路のトリガで転送するためには、該当の周辺回路と場合によってはイベントシステムもプログラムしないとならないです。今回は、CPUによるソフトウエア・トリガなので簡単、以下のようにしてみました。

void triggerDMAC0() {
  DMAC->Channel[0].CHCTRLA.bit.ENABLE = 1; // CH0 Enable (Software Trigger Only)
  DMAC->SWTRIGCTRL.bit.SWTRIG0 = 1;
}

これでようやく「1バイト転送」の準備完了。

実機動作確認

末尾に掲げたテスト用のプログラムの「転送」動作は以下のようです。

  1. 転送先メモリをクリア、転送元のメモリにテスト用の値をセット
  2. デスクリプタをセット、書き込み用のデスクリプタは書き込み確認できるようにクリア
  3. DMA転送をトリガ
  4. DMA内のフラグなどを確認
  5. デスクリプタをダンプ(書き込み用デスクリプタが更新されているか)
  6. 転送元、転送先メモリをダンプ(転送先領域にテストパターンが転送されているか)

動作させてみたところがこちら。

ATSAMD51 DMA test.
BASE_ADDR=0x20000120
Trial: 1
-----Setup descriptors and memory----
DESC.BTCNT = 1
DESC.BTCTRL = 0x0001
DESC.DST = 0x20000160
DESC.SRC = 0x20000168
DESC.DESC = 0x20000120
WRB.BTCNT = 0
WRB.BTCTRL = 0
WRB.DST = 0x00000000
WRB.SRC = 0x00000000
WRB.DESC = 0x00000000
Source memory: 100,101,102,103,104,105,106,107,
Destination memory: 0,0,0,0,0,0,0,0,
-----Start DMA-----------------------
CH0STAT: 0x01
CH0STAT: 0x00
INTPEND: 0x00000000
DESC.BTCNT = 1
DESC.BTCTRL = 0x0001
DESC.DST = 0x20000160
DESC.SRC = 0x20000168
DESC.DESC = 0x20000120
WRB.BTCNT = 1
WRB.BTCTRL = 1
WRB.DST = 0x20000160
WRB.SRC = 0x20000168
WRB.DESC = 0x20000120
Source memory: 100,101,102,103,104,105,106,107,
Destination memory: 100,0,0,0,0,0,0,0,
=====END OF TRIAL ==========================================

Destination memoryとあるところ、1回目の出現では0, と始まっているのが、最後の2回目では、100, となっているのがDMA転送が起こったお印。

これ1バイトにたどり着くまで、長い道のりだったな。。。

IoT何をいまさら(98) ATSAMD51、ADからDA直接、Wio Terminal へ戻る

IoT何をいまさら(100) ATSAMD51、アナログコンパレータACを使ってみる へ進む

実験に使用したソース全文を以下に掲げます。開発環境 VSCode+PlatformIOにて、ターゲットボードを Wio Terminal、プラットフォームを Arduino にてビルドし、デバイスにアップロードして動作確認したもの。

#include <Arduino.h>

#define MAX_DMA_CH  (4)
#define MEM_SIZE  (8)

int counter = 0;

DmacDescriptor descSection[MAX_DMA_CH] __attribute__ (( aligned (16) ));
volatile DmacDescriptor wbSection[MAX_DMA_CH] __attribute__ (( aligned (16) ));

volatile uint8_t dstMemory[MEM_SIZE] __attribute__ (( aligned (4) ));
uint8_t srcMemory[MEM_SIZE] __attribute__ (( aligned (4) ));

void setupTestMemory() {
  for (int idx=0; idx < MEM_SIZE; idx++) {
    dstMemory[idx] = 0;
    srcMemory[idx] = idx + 100;
  }
}

void setupDMAdescriptor() {
  descSection[0].DESCADDR.reg = (uint32_t)descSection;
  descSection[0].DSTADDR.reg = (uint32_t)dstMemory;
  descSection[0].SRCADDR.reg = (uint32_t)srcMemory;
  descSection[0].BTCNT.reg = 1; //SINGLE Half word
  descSection[0].BTCTRL.bit.STEPSIZE = 0x0; // 1 byte step.
  descSection[0].BTCTRL.bit.STEPSEL = 0;
  descSection[0].BTCTRL.bit.DSTINC = 0;  // Disable
  descSection[0].BTCTRL.bit.SRCINC = 0;  // Disable
  descSection[0].BTCTRL.bit.BEATSIZE = DMAC_BTCTRL_BEATSIZE_BYTE_Val;
  descSection[0].BTCTRL.bit.BLOCKACT = DMAC_BTCTRL_BLOCKACT_NOACT_Val;
  descSection[0].BTCTRL.bit.EVOSEL = DMAC_BTCTRL_EVOSEL_DISABLE_Val;
  descSection[0].BTCTRL.bit.VALID = 1;
  wbSection[0].BTCNT.reg=0;
  wbSection[0].BTCTRL.reg=0;
  wbSection[0].DESCADDR.reg=0;
  wbSection[0].DSTADDR.reg=0;
  wbSection[0].SRCADDR.reg=0;
}

void setupDMA() {
  int dmaMCLK = MCLK->AHBMASK.bit.DMAC_;
  if (dmaMCLK != 1) {
    Serial.printf("DMA MCLK: Disable!\r\n");
    return;
  }
  DMAC->CTRL.bit.DMAENABLE = 0; // DMA Disable
  DMAC->CTRL.bit.SWRST = 1; // Software RESET
  while(DMAC->CTRL.bit.SWRST == 1) {} // Wait for the RESET process finished.
  DMAC->BASEADDR.reg = (uint32_t)descSection;
  DMAC->WRBADDR.reg = (uint32_t)wbSection;
  Serial.printf("BASE_ADDR=0x%08x\r\n", DMAC->BASEADDR.reg);
  DMAC->CTRL.reg = 0xF00; // All LVL enabled.
  DMAC->Channel[0].CHCTRLA.bit.ENABLE = 0; // CH0 Disable
  DMAC->Channel[0].CHCTRLA.bit.SWRST = 1; // CH0 Software Reset
  while(DMAC->Channel[0].CHCTRLA.bit.SWRST == 1) {} // Wait for the CH0 RESET process finished.
  DMAC->CTRL.bit.DMAENABLE = 1; // DMA Enable
}

void triggerDMAC0() {
  DMAC->Channel[0].CHCTRLA.bit.ENABLE = 1; // CH0 Enable (Software Trigger Only)
  DMAC->SWTRIGCTRL.bit.SWTRIG0 = 1;
}

uint8_t readDMAC0pend() {
  return DMAC->Channel[0].CHSTATUS.bit.PEND;
}

uint8_t readDMAC0busy() {
  return DMAC->Channel[0].CHSTATUS.bit.BUSY;
}

void dumpDescriptors() {
  Serial.printf("DESC.BTCNT  = %d\r\n",descSection[0].BTCNT.reg);
  Serial.printf("DESC.BTCTRL = 0x%04x\r\n",descSection[0].BTCTRL.reg);
  Serial.printf("DESC.DST    = 0x%08x\r\n",descSection[0].DSTADDR.reg);
  Serial.printf("DESC.SRC    = 0x%08x\r\n",descSection[0].SRCADDR.reg);
  Serial.printf("DESC.DESC   = 0x%08x\r\n",descSection[0].DESCADDR.reg);
  Serial.printf("WRB.BTCNT   = %d\r\n",wbSection[0].BTCNT.reg);
  Serial.printf("WRB.BTCTRL  = %d\r\n",wbSection[0].BTCTRL.reg);
  Serial.printf("WRB.DST     = 0x%08x\r\n",wbSection[0].DSTADDR.reg);
  Serial.printf("WRB.SRC     = 0x%08x\r\n",wbSection[0].SRCADDR.reg);
  Serial.printf("WRB.DESC    = 0x%08x\r\n",wbSection[0].DESCADDR.reg);
}

void dumpMEMORY() {
  Serial.printf("Source memory: ");
  for (int idx=0; idx < MEM_SIZE; idx++) {
    Serial.printf("%d,",srcMemory[idx]);
  }
  Serial.printf("\r\n");
  Serial.printf("Destination memory: ");
  for (int idx=0; idx < MEM_SIZE; idx++) {
    Serial.printf("%d,",dstMemory[idx]);
  }
  Serial.printf("\r\n");
}

void setup() {
  Serial.begin(9600);
  while(!Serial);
  Serial.printf("ATSAMD51 DMA test.\r\n");
  setupDMA();
}  

void loop() {
  Serial.printf("Trial: %d\r\n", ++counter);
  Serial.printf("-----Setup descriptors and memory----\r\n");
  setupTestMemory();
  setupDMAdescriptor();
  dumpDescriptors();
  dumpMEMORY();
  Serial.printf("-----Start DMA-----------------------\r\n");
  triggerDMAC0();
  Serial.printf("CH0STAT: 0x%02x\r\n", DMAC->Channel[0].CHSTATUS.reg);
  while(readDMAC0pend() == 1) {}
  Serial.printf("CH0STAT: 0x%02x\r\n", DMAC->Channel[0].CHSTATUS.reg);
  while(readDMAC0busy() == 1) {}
  Serial.printf("INTPEND: 0x%08x\r\n", DMAC->INTPEND.reg);
  dumpDescriptors();
  dumpMEMORY();
  Serial.printf("=====END OF TRIAL ==========================================\r\n\r\n");
  delay(10000);
}