鉄火巻キのメモ書き

ただのメモ代わり

STM32Lシリーズで遊ぶ -DMAを使い倒す①UART

STM32マイコンの情報は今や簡単に見つかると思いますが,HALを使わないレジスタ直たたきコーディングについてのサイトをあまり見かけないと知人から言われたのでチョット何か書いてみようかなと思い至った次第.

私はロボコンでSTM32マイコンを使うときはHALメインで使ってましたし,実際ロボコン程度であればそっちの方が移植性的にも開発コスト的にもメリットが多いと思います. なので今回は省電力というスコープで話をしていきます.

よってSTM32Lシリーズ固定です.

(最近STM32U5シリーズというさらに省電力のファミリが追加されたのですが、まだ触ってないです...)

対象マイコン

ターゲットのマイコンはとりあえず秋月でもnucleoボードが入手可能(多分)のSTM32L053を例に使います.

www.st.com

https://akizukidenshi.com/catalog/g/gM-08022/

L0x3シリーズのリファレンスは日本語化されているので多少開発しやすいですが,rev2ベースで最新のリファレンスとはかけ離れているので注意が必要です(3敗).

仕様構成

今回は直接省電力とは関わってこないですが,前準備としてDMAをいじっていきます.

でもDMAだけでは何とも面白くないので,ついでにADCとTIMとUARTでも使って周期的にADCで読みだした値をUARTでぶん投げることとしますが,ちょとボリュームが多いので何回かに記事を分けます.

UARTの設定

まずDMAで使う前提なのでDMAのチャネル割り当てを確認します.

f:id:tekkamaki200:20220211104839p:plain
L0x3のリファレンス(RM0367 Rev8)より DMAのチャネル表1
f:id:tekkamaki200:20220211104955p:plain
L0x3のリファレンス(RM0361 Rev8)より DMAのチャネル表2

対象がnucleo64なのでUSBから直接通信できるUSART2を使用します.よって送信に使えるDMAチャネルはChannel4かChannel7です.

今回は後のチャネル割り当ての都合でChannel7を使用します.

チャネルの割り当てはDMA_CSELRレジスタで出来るので以下のようにします.

void DMA_init(){
    RCC->AHBENR |= RCC_AHBENR_DMAEN;
    DMA1_CSELR->CSELR |= (0x4 << DMA_CSELR_C7S_Pos);
}

これでDMAの初期設定は終わりなので次にUARTの設定です.

UARTの通信設定自体はとりあえずよくある以下の設定に合わせます.

  • 9600bps
  • 8bit
  • parity none
  • stop bit 1

そんでとりあえずオーバーサンプリングは16にしときます.

void UART_init(){
    /**
    * UART2 ->  9600bps
    * TX -> DMA1_CH7
    */

    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    USART2->BRR = APB_FREQENCY / 9600;

    /**
    * 8bit
    * parity none
    * stop1
    * over16
    * interrupt all disable
    * DMA Tx enable
    */
    USART2->CR1 |=
            USART_CR1_TE |
            USART_CR1_UE;
    //USART2->CR2;  //default
    USART2->CR3 |=
            USART_CR3_DMAT;
}

レジスタのリセット値で問題ないフラグについては何も記述していないので,別の設定にしたい場合はCRレジスタ当たりをいい感じにしてください.送信設定しかしていないのでこのままでは受信もできません.

さて,これで初期設定が終わったので実際にUARTでDMAを使用していきます.

DMAでUSRT送信

ここからが本題です.

STM32のDMA転送にはいくつかの種類と機能があります.

  • 転送サイズ
  • ポインタインクリメント
  • 転送方向
  • サーキュラモード
  • メモリ間モード

転送サイズはByte(8bit) / Harf Word(16bit) / Word(32bit)の3種あり,転送元と転送先で別々のサイズにすることも可能です.ただし,転送を行うペリフェラルレジスタによっては特定サイズしないと動かないことがあります.

ポインタインクリメント機能は,複数の連続するアドレスのデータを転送できる機能です.転送元と転送先でそれぞれインクリメントの有無を独立して設定可能です.

転送方向設定はその名の通り転送する向きの設定です.メモリからペリフェラルまたはペリフェラルからメモリに加えてペリフェラルからペリフェラルへの転送をサポートしています.

サーキュラモードは転送を完了したのち,すぐに再度転送を繰り返し行うモードです.これは受信バッファ等によく使われます.対して,サーキュラモードでない場合はワンショットモードと呼ばれたり呼ばれなかったりします.

メモリ間モードはメモリとメモリの間で転送を行うモード...ではないです.転送方向に記述したようにメモリからメモリへの転送をDMAはサポートしていません.(リファレンスに書いてないだけでもしかしたらできるかもしれないけど)
このモードはDMAのトリガをソフトウェアで行うというモードです.本来,DMAのリクエストは各チャネルごとに割り当てられたペリフェラルからのトリガリエストによって発生します.しかし,メモリ間モードを使用することでこのトリガなしに転送を行えます.

とDMAの概要を説明したところでとりあえず設定を関数化しておきます.

typedef enum{
    SizeByte,
    SizeHWord,
    SizeWord
}DMA_size;
typedef enum {
    ToMem,
    ToPeripheral
}DMA_Direction;

void DMA_set(DMA_Channel_TypeDef* DMA_ch, void* address_p, void* address_m){
    DMA_ch->CPAR = (uint32_t)(address_p);
    DMA_ch->CMAR = (uint32_t)(address_m);
}
void DMA_start(DMA_Channel_TypeDef* DMA_ch, DMA_size p_size, DMA_size m_size, DMA_Direction dir, uint32_t size, bool circular, bool inc_p){
    DMA_ch->CCR &= ~DMA_CCR_EN;
    DMA_ch->CNDTR = size;
    DMA_ch->CCR =
                (m_size << DMA_CCR_MSIZE_Pos) |
                (p_size << DMA_CCR_PSIZE_Pos) |
                (DMA_CCR_MINC) |
                (inc_p << DMA_CCR_PINC_Pos) |
                (circular << DMA_CCR_CIRC_Pos) |
                (dir << DMA_CCR_DIR_Pos) |
                (DMA_CCR_EN);
}

アドレスのセットとその他設定及びDMA有効化を分けました.この分け方は大変分かりにくいかと思いますが,レジスタへのアクセス回数をただ減らしたかっただけです. DMA_start()ではメモリのインクリメントだけは使わないシチュエーションがあまり思い浮かばなかったので強制的に設定しています. またDMA_CCR_ENフラグを下ろしているのは,ワンショットモードで転送を終えた際にCNDTRレジスタが0になるのに対し,DMA_CCR_ENが立ち上がりっぱなしになりCNDTRレジスタへの書き込みがロックされるためです.

では実際にUARTを動かします. 今回は8bitデータの送信なので,転送サイズは転送元/先ともにByteを選択.そして転送方向はメモリからペリフェラルになります.サーキュラモードを使うとひっきりなしに連続送信してしまうので今回は使いません.

int main(void)
{
    uint8_t send_data[] = {'H', 'e', 'l', 'l', 'o', '\n', '\r'};

    PWR_init();
    GPIO_init();
    UART_init();
    DMA_init();

    /**
    * DMA set
    */
    DMA_set(DMA_CH_USART2_TX, (void*)&USART2->TDR, (void*)send_data);


    while(true){
        DMA_start(DMA_CH_USART2_TX, SizeByte, SizeByte, ToPeripheral, sizeof(send_data), false, false);
        GPIOA->ODR ^= (0b1 << 5);

        for(uint32_t i = 0 ;i < 1e5; ++i);
    }
}

クロック等の設定は特にしていないのでforで無理やり待ちを作って適当にループを回しています.

これで無事DMAでのUART送信ができました. 次回はTIMとADCを組み合わせてDMA転送を行います.

今回コードはプロジェクトごとgitに上げておきます.

github.com