Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

 「STM32のタイマとDMAを組み合わせてLチカする」に引き続き、DMA転送するデータをCPUで作成しながらNeoPixelの制御信号を生成させてみる。NeoPixelは、チップ内に3色のLEDと制御マイコンが組み込まれており、電飾用によく使われるデバイスである。

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

Pinoutタブ

 タイマとしてTIM3を選択し、Channel1にPWM出力を設定する。

Pinout設定 ピン割り当て

Clock Configurationタブ

 NeoPixelには、パルス幅変調した800KHzの信号を供給するため、タイマの原発クロックには、48MHzを選択することとする。System Clock MuxでHSI48を選択する。

スクリーンショット 2018-03-15 15.38.49.png

Configurationタブ

 NeoPixelの制御信号は、1.25μsec周期で、Hレベルのパルス幅を0.4μsecまたは0.8μsecとして0と1をエンコードしたものである。
 原発クロックの48MHzを5分周して9.6MHzのクロックを得る。これを、12カウントして800KHz周期のPWM出力を行う。この時、4カウントで約0.42μsec、8カウントで約0.83μsecのパルス幅となる。NeoPixelは、公称±150nsecの誤差を許容するので、これで十分であろう。
 TIM3ボタン→Parameter Settingsタブを選択し、Counter Settings、PWM Generation Channel 1の欄を設定する。

Parameter Settingsタブ

 DMA Settingsタブを選択する。TIM3_CH1のDMAを追加し、DirectionをMemory To Peripheral、ModeをCircular、MemoryのIncrement Addressにチェックを入れ、PeripheralはCapture/CompareレジスタのサイズのHalf Word、Memoryは、値が4か8のどちらかなのでByteにする。

DMA Settingsタブ

 NVIC Settingsタブを選択する。DMAチャネルのインタラプトはデフォルトでEnabledが選択されているので、そのままでよい。

NVIC Settingsタブ

 GPIO Settingsタブを選択する。STM32F042K6T6の場合、データシートによると、Maximum output speedが Low の場合、Output rise/fall timeの最大値が125nsecとなっており、これではNeoPixelの最大許容値に近いので、Middle以上に設定する。Middleの場合、最大25nsecとなっている。

GPIO Settingsタブ

プログラミング

 NeoPixelの制御信号としては、RGB値の各8ビット、計24ビットをチップの数だけ連続して出力すればよい。
 基本として、ビットの0/1にあわせて、1バイトの4または8を24個並べたリストを用意し、DMAでサイクル毎にタイマのCompare/Captureレジスタに書き込む。ただし、DMA転送が終わった後にリストを書き換えはじめると、次のパルスの出力にデータ作成が間に合わないので、ダブルバッファ構成とし、1番目のリストのDMA転送中に2番目のリストをCPUで作成する。
 STM32のDMAコントローラでは、DMAの1/2に到達した時と、最後まで到達した時にインタラプトを発生させることができるので、リスト2個分の48バイトのメモリを用意し、DMAを巡回させながら半分転送する毎に次の転送データを作成する。
 タイマのHALライブラリには、タイマでDMA転送の設定を行う関数HAL_TIM_PWM_Start_DMA () があるが、この関数では1/2転送インタラプトを設定することができないので、DMAチャネルに直接アクセスして設定を行う。
 STM32CubeMXが自動生成したコードでは、TIM3のハンドルは、htim3、TIM3のCapture/CompareレジスタのDMAのハンドルは、htim3.hdma[TIM_DMA_ID_CC1]に格納されている。
 DMAチャネルのインタラプトのハンドラは、HAL_DMA_RegisterCallback () 関数を使用して設定する。パラメータに1/2転送インタラプトと完了インタラプトのラベルとハンドラの関数を指定する。
 同時に、タイマのTIMx_DIERレジスタのCCxDEビットをセットし、Capture/Compare DMAリクエストをイネーブルし、TIMx_CCERレジスタのCCxEビットをセットし、タイマチャネル1の出力TIM3_CH1をアクティブに設定する。

HAL_DMA_RegisterCallback (htim3.hdma[TIM_DMA_ID_CC1], HAL_DMA_XFER_CPLT_CB_ID, dmaCallback);
HAL_DMA_RegisterCallback (htim3.hdma[TIM_DMA_ID_CC1], HAL_DMA_XFER_HALFCPLT_CB_ID, dmaCallback);
htim3.Instance->DIER |= TIM_DIER_CC1DE;
htim3.Instance->CCER |= TIM_CCER_CC1E;

 DMAチャネル割込みのハンドラでは、NeoPixelのRGB値の3バイトをビット毎にCapture/Compareレジスタに設定する値、4または8をバッファのリストに書き込む。NeoPixelに送る信号は、BRGの順で、それぞれMSBから順に供給する。DMAコントローラがバッファメモリの半分読む毎に割込みが発生するので、バッファの半分づつリストを作る。全てのチップのデータを供給した後、Lレベルの信号を50μsec以上送るために、割込み2回分の0で埋めたリストを作成する。
 PWM信号の出力を開始するには、HAL_DMA_Start_IT () 関数を実行してDMAと割込みをイネーブルし、__HAL_TIM_ENABLE () マクロを実行してタイマを起動する。DMAコントローラの内部カウンタをリセットするために、一連のデータ送信毎にHAL_DMA_Start_IT () を実行する。
 信号送信を終えた後は、HAL_DMA_Abort () 関数を実行して、DMAを停止し、HALライブラリの内部の状態をクリアする。
 実際に作成したプログラムは、以下の通り。

neopixel.h
#ifndef NEOPIXEL_H
#define NEOPIXEL_H

#include <stdio.h>
#include "tim.h"

void  NeoPixInit (TIM_HandleTypeDef* _htim, uint32_t channel);
void  NeoPixStart (uint8_t* data, int len, bool wait = true);

#endif /* NEOPIXEL_H */
neopixel.cpp
#include <stdio.h>
#include "neopixel.h"
#include "tim.h"

static TIM_HandleTypeDef*  htim;
static DMA_HandleTypeDef*  hdma;
volatile static uint32_t*  pccr;
static uint32_t  dier;
static uint32_t  ccer;
static uint8_t*  srcRGB;
static int  srcRGBLen;
static uint8_t  pwmcount[48];
static int  pos;
volatile static bool  busy;

static void  dmaCallback (DMA_HandleTypeDef* _hdma) {
    busy = false;
}

static void  update () {
    uint8_t*  p;
    uint8_t  c;
    ++ srcRGB;      // G
    c = *srcRGB;
    p = pwmcount + pos;
    *(p++) = (c & 0x80) ? 8 : 4;
    *(p++) = (c & 0x40) ? 8 : 4;
    *(p++) = (c & 0x20) ? 8 : 4;
    *(p++) = (c & 0x10) ? 8 : 4;
    *(p++) = (c & 0x08) ? 8 : 4;
    *(p++) = (c & 0x04) ? 8 : 4;
    *(p++) = (c & 0x02) ? 8 : 4;
    *(p++) = (c & 0x01) ? 8 : 4;
    -- srcRGB;      // R
    c = *srcRGB;
    *(p++) = (c & 0x80) ? 8 : 4;
    *(p++) = (c & 0x40) ? 8 : 4;
    *(p++) = (c & 0x20) ? 8 : 4;
    *(p++) = (c & 0x10) ? 8 : 4;
    *(p++) = (c & 0x08) ? 8 : 4;
    *(p++) = (c & 0x04) ? 8 : 4;
    *(p++) = (c & 0x02) ? 8 : 4;
    *(p++) = (c & 0x01) ? 8 : 4;
    srcRGB += 2;        // B
    c = *srcRGB;
    *(p++) = (c & 0x80) ? 8 : 4;
    *(p++) = (c & 0x40) ? 8 : 4;
    *(p++) = (c & 0x20) ? 8 : 4;
    *(p++) = (c & 0x10) ? 8 : 4;
    *(p++) = (c & 0x08) ? 8 : 4;
    *(p++) = (c & 0x04) ? 8 : 4;
    *(p++) = (c & 0x02) ? 8 : 4;
    *(p++) = (c & 0x01) ? 8 : 4;
    ++ srcRGB;
    srcRGBLen -= 3;
    pos += 24;
    if (pos >= 48)
        pos = 0;
}

static void  clear () {
    uint32_t*  p = (uint32_t*)(pwmcount + pos);
    int  i;
    for (i = 0; i < 6; ++ i)
        *(p ++) = 0;
    pos += 24;
    if (pos >= 48)
        pos = 0;
}

static void  initChannel (uint32_t channel) {
    switch (channel) {
    case TIM_CHANNEL_1:
        hdma = htim->hdma[TIM_DMA_ID_CC1];
        pccr = &htim->Instance->CCR1;
        dier = TIM_DIER_CC1DE;
        ccer = TIM_CCER_CC1E;
        break;
    case TIM_CHANNEL_2:
        hdma = htim->hdma[TIM_DMA_ID_CC2];
        pccr = &htim->Instance->CCR2;
        dier = TIM_DIER_CC2DE;
        ccer = TIM_CCER_CC2E;
        break;
    case TIM_CHANNEL_3:
        hdma = htim->hdma[TIM_DMA_ID_CC3];
        pccr = &htim->Instance->CCR3;
        dier = TIM_DIER_CC3DE;
        ccer = TIM_CCER_CC3E;
        break;
    case TIM_CHANNEL_4:
        hdma = htim->hdma[TIM_DMA_ID_CC4];
        pccr = &htim->Instance->CCR4;
        dier = TIM_DIER_CC4DE;
        ccer = TIM_CCER_CC4E;
        break;
    default:;
    }
}

static void  initDMA () {
    *pccr = 0;
    HAL_DMA_RegisterCallback (hdma, HAL_DMA_XFER_CPLT_CB_ID, dmaCallback);
    HAL_DMA_RegisterCallback (hdma, HAL_DMA_XFER_HALFCPLT_CB_ID, dmaCallback);
    htim->Instance->DIER |= dier;
    htim->Instance->CCER |= ccer;
    __HAL_TIM_MOE_ENABLE (htim);
}

void  NeoPixInit (TIM_HandleTypeDef* _htim, uint32_t channel) {
    htim = _htim;
    hdma = NULL;
    pccr = NULL;
    srcRGB = NULL;
    srcRGBLen = 0;
    pos = 0;
    busy = true;
    clear ();
    clear ();
    initChannel (channel);
    initDMA ();
};

void  NeoPixStart (uint8_t* data, int len, bool wait) {
    srcRGB = data;
    srcRGBLen = len;
    pos = 0;
    if (srcRGBLen > 0) {
        update ();
        busy = true;
        __HAL_TIM_SET_COUNTER (htim, 0);
        HAL_DMA_Start_IT (hdma, (uint32_t)pwmcount, (uint32_t)pccr, 48);
        __HAL_TIM_ENABLE (htim);
        while (srcRGBLen > 0) {
            update ();
            busy = true;    // 最適化による影響を避けるためにvolatile
            while (busy) {};
        }
        clear ();
        busy = true;
        while (busy) {};
        if (wait) {
            clear ();
            busy = true;
            while (busy) {};
            busy = true;
            while (busy) {};
        }
        __HAL_TIM_DISABLE (htim);
        HAL_DMA_Abort (hdma);
    }
}
main.cpp
uint8_t  rgb[] = {
    0x44, 0x88, 0x88,
};

          :
    NeoPixInit (&htim3, TIM_CHANNEL_1);
    NeoPixStart (rgb, sizeof (rgb));
          :

 実行した結果、以下のような信号を得た。

タイミングチャート

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした