STM32のタイマとDMAを組み合わせてLチカする


「STM32のタイマだけを使ってLチカする」に引き続き、STM32CubeMXを使ってプログラムを作成する。今回は、DMAを使ってCPUを介さずLチカの点滅パターンを制御してみる。出力は2系統、それぞれ別のパターンで点滅させる。

STM32CubeMXによるペリフェラル設定

Pinoutタブ

 タイマTIM3を選択する。Internal Clockにチェックを入れ、Channel3とChannel4をPWM出力のモードに設定する。TIM3のチャネル2は、DMAチャネルにつながっていないので、これ以外を選択する。


 STM32F04x MCUの場合、以下の表の様にTIM3のDMAチャネルが割り当てられている。

ペリフェラル DMAチャネル1 DMAチャネル2 DMAチャネル3 DMAチャネル4 DMAチャネル5
TIM3 - TIM_CH3 TIM_CH4 TIM3_UP TIM_CH1 TIM_TRIG -

Clock Configurationタブ

クロック設定は、初期設定のままにしておく。タイマのクロック(APB1 Timer clocks)は、8MHz(④)に設定されている。

Configurationタブ

TIM3ボタン→Parameter/Settingsタブを選択する。

 チャネル設定は前回と同様に、PWM mode 1(②③⑨⑫)でCH PolarityをHighに設定する(⑪⑭)。「Pulse」欄はDMAで書き換えるが、起動時のパルス幅に関係するので、0を設定しておく(⑩⑬)。

 DMA Settingsタブを開く。

 Addボタンを押し、設定行を追加する。
 「DMA Request」列でチャネル3、4(⑮⑯)を選択し、「Direction」をMemory To Pheripheral(メモリ上のuint16_t型のリストからタイマのチャネルのレジスタ宛)(⑰)に設定する。DMA Request SettingエリアのModeリストで「Circular」を選択する(⑱)と、メモリ上のリストを繰り返し参照する。Increment Addressの項目のPeripheralのチェックを外す(⑲)と、アクセスするチャネルのレジスタのアドレスを固定し、Memoryにチェックを入れる(⑳)と、メモリ上のリストを順にアクセスする。Data Widthは、レジスタのサイズにあわせてHalf Word(uint16_t)(㉑)を指定する。
 アクセスするアドレスとデータの個数は、プログラム中に挿入する関数呼び出しで指定する。

プログラミング

main.c
uint16_t  pwmlist1[] = {
  100, 200, 300, 400, 500,
};
uint16_t  pwmlist2[] = {
  900, 800, 700,
};

int main(void)
{
    :
  HAL_TIM_PWM_Start_DMA (&htim3, TIM_CHANNEL_3, (uint32_t*)pwmlist1, sizeof (pwmlist1) / sizeof (uint16_t));
  htim3.State = HAL_TIM_STATE_READY;
  HAL_TIM_PWM_Start_DMA (&htim3, TIM_CHANNEL_4, (uint32_t*)pwmlist2, sizeof (pwmlist2) / sizeof (uint16_t));
    :

 DMAでタイマのPulseの値(Capture/Compare 2,3 registerに書き込む値)のリストを用意し、HAL_TIM_PWM_Start_DMA () 関数のパラメータに引き渡し、タイマのチャネル3, 4に関連付けられたDMAのチャネル2, 3に、転送元アドレスとデータ数を設定する。転送先のCapture/Compare registerは、ライブラリ内にハードコードされているので、現時点では(STM32CubeMX バージョン4.24.0)HALライブラリで設定できる転送先は限られる。HAL_TIM_PWM_Start_DMA () 関数は、DMAのアドレス設定とタイマの起動の両方を行うので、1回の関数呼び出しで出力ピンへのパルスの出力が始まる。しかし、下記のように、関数の冒頭でhtim->StateがHAL_TIM_STATE_READYであることをチェックし、HAL_TIM_STATE_BUSYを書き込むため、そのままでは2系統のDMAを設定することができない。ここでは、htim->StateにHAL_TIM_STATE_READYを書き戻して、2番目のDMAを設定した。CMSISのAPIを利用するプログラムを次の章で示す。

stm32f0xx_hal_tim.c
HAL_StatusTypeDef HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length)
{
  /* Check the parameters */
  assert_param(IS_TIM_CCX_INSTANCE(htim->Instance, Channel));

  if((htim->State == HAL_TIM_STATE_BUSY))
  {
     return HAL_BUSY;
  }
  else if((htim->State == HAL_TIM_STATE_READY))
  {
    if(((uint32_t)pData == 0U ) && (Length > 0U))
    {
      return HAL_ERROR;
    }
    else
    {
      htim->State = HAL_TIM_STATE_BUSY;
    }
  }

 HAL_TIM_PWM_Start_DMA () 関数を実行すると、直ちにタイマが起動され、下図のタイミングチャートのように、TIM3_CH3, TIM3_CH4ピンにパルスが出力される。

CMSISのAPIによるプログラミング

 前述のように、HAL_TIM_PWM_Start_DMA () 関数などHALライブラリで用意されているAPIでは、利用できる状況が限られる。この関数の内部で実行されているレジスタ設定などを取り出してプログラムを記述する。

main.c
  HAL_DMA_Start (htim3.hdma[TIM_DMA_ID_CC3], (uint32_t)pwmlist1, (uint32_t)&htim3.Instance->CCR3, sizeof (pwmlist1) / sizeof (uint16_t));
  HAL_DMA_Start (htim3.hdma[TIM_DMA_ID_CC4], (uint32_t)pwmlist2, (uint32_t)&htim3.Instance->CCR4, sizeof (pwmlist2) / sizeof (uint16_t));
  htim3.Instance->DIER |= TIM_DIER_CC3DE | TIM_DIER_CC4DE;
  htim3.Instance->CCER &= ~(TIM_CCER_CC3E | TIM_CCER_CC4E);
  htim3.Instance->CCER |= TIM_CCER_CC3E | TIM_CCER_CC4E;
  __HAL_TIM_MOE_ENABLE(&htim3);
  __HAL_TIM_ENABLE(&htim3);

 以下ペリフェラルのレジスタに設定されるパラメータを順に列挙する。
 HAL_DMA_Start () の内部で設定されるレジスタ。

  • DMA_CNDTRレジスタ -- データ数
  • DMA_CPARレジスタ -- ペリフェラルのアドレス
  • DMA_CMARレジスタ -- メモリのアドレス
  • DMA_CCRレジスタ
    • ENビット -- DMAイネーブル

 DMA_CCRレジスタのDIRビット(Direction⑪)は、初期化関数内で設定されている。
 続いて設定されるレジスタ。

  • TIM3_DIERレジスタ
    • CC3DEビット -- Capture/Compare 3 registerのDMAイネーブル
    • CC4DEビット -- Capture/Compare 4 registerのDMAイネーブル
  • TIM3_CCERレジスタ
    • CC3Eビット -- TIM_CH3出力イネーブル
    • CC4Eビット -- TIM_CH4出力イネーブル

 __HAL_TIM_MOE_ENABLE()マクロは、BDTRレジスタのMOEビットをセットするが、TIM3タイマにはBDTRレジスタは存在しない。TIM1タイマやTIM15タイマなどではセットする必要がある。

  • TIMx_BDTRレジスタ
    • MOEビット -- メイン出力イネーブル

 引き続き、__HAL_TIM_ENABLE()マクロでタイマのカウンタをイネーブルする。

  • TIM3_CR1レジスタ
    • CENビット -- カウンタイネーブル

次回

 タイマを使い、一定間隔でADCでアナログデータをサンプリングしてDMAでメモリに書き込む。
 ADCの制御がよくわからないので、代わりに、タイマでNeoPixelの制御信号を生成する。→STM32のタイマとDMAを組み合わせてNeoPixelでLチカする

おまけ

 HAL_TIM_PWM_Start_DMA () を実行した後、HAL_TIM_PWM_Stop_DMA () で一旦停止して、再度、別のアドレスを指定して HAL_TIM_PWM_Start_DMA () を実行したところ、アドレスが前のまま変更されないというトラブルが発生した。調べてみると、HAL_TIM_PWM_Start_DMA () の内部で、HAL_DMA_Start () が呼ばれて DMA_HandleTypeDefデータのStateメンバに HAL_DMA_STATE_BUSY がセットされ、その後、HAL_TIM_PWM_Stop_DMA () を実行しても、HAL_DMA_STATE_READY に戻されることがないため、アドレス設定がスキップされていた。だめじゃん。