以前、「CubeMXとHALを使ったフルカラーLEDの制御」という記事を書いた。
https://matsubara-ui-design.net/2021/01/ostw3535_1/

今回は実際にLEDを点灯させるコードについてまとめる。

動作の概要

  • 目的 24ビットxLEDの個数分のPWM信号を隙間なく連続で出力する
  • TIMとDMAを使って必要な信号を生成する
  • DMAはサーキュラーモードで動作させて、半周ごとの割込みで次のバッファをセットする
  • 24ビット分の信号生成の間に次の24ビット分のDMAバッファを準備しておくことで、途切れずにPWM信号を生成できる
  • LEDの個数分が終了したらインターバル期間で、この期間中もDMAが動作し続けてPWMに無信号を出力する
  • メインループからは、フレームバッファを使ってLEDの点灯状態をDMAから読み出せるようにする

PWMについて

  • 1周期で1ビット分のパルスを生成する
  • 1ビット分の期間は1.25usで、ビット0でもビット1でも同じ
  • 16MHzのクロックで20カウントとすると、1.25usの期間となる
  • パルス幅の違いを、TIMにセットする値で設定する
  • 5カウント分でビット0の約0.3usのパルス
  • 15カウント分でビット1の約0.9usのパルス

DMA割込みでの処理の流れ

  • 全体で48ビット分のサイズとし、半分の24ビットごとに割込みが入るようにする
  • 割込み処理の間、DMAは次の24ビット分のデータを読み出してPWM信号を生成している
  • 割込み処理内では、そのさらに次の24ビット分のバッファをセットする
  • 同様に、24ビット分の出力が終わるごとに、その2つ先の24ビット分のバッファをセットしていく

フレームバッファについて

  • LEDの点灯状態は、【LEDの個数x3バイト(RGB)】というサイズのフレームバッファで管理する
  • DMA割込みの処理では、フレームバッファから順に読み出し、ビットごとのカウント値に変換してDMAバッファにセットする
  • メインループでは、次のフレームでのLEDの点灯状態をフレームバッファにセットする

詳細な設定

PWMの設定

  • TIM3のCH1を使用
  • TIMは16MHzのクロックで動作 システムクロックの4分の1
  • 周期は20カウントに設定(設定値は-1で19)
  • プリロードを有効にして、DMAで1パルスずつ確実に生成されるようにする

DMAの設定

  • メモリからTIM3のCH1へ、8ビット値での転送
  • Circularモード

DMAバッファ

  • 1ビット分のパルスについて1バイト必要
  • 24バイトでLED1個分の信号に相当する
  • 全体では48バイトのサイズ

フレームバッファ

  • わかりやすいようにR、G、Bでそれぞれ配列を作る
  • R、G、Bそれぞれについて、LEDの個数x1バイト
  • 0が点灯しない、255がもっとも明るい

リセット期間とフレーム更新レート

  • 最低でも280usの無信号の期間が必要
  • 24ビット分の長さでカウントしていくので、30us単位でのカウントになる
  • 最低でも10カウント分は必要
  • カウント値を大きくすると更新レートが遅くなる
  • たとえば2000カウントに設定した場合、2000x30usで60msの更新レートになる(厳密にはLEDの個数分の時間も加わる)

補足

なぜ割込み処理を24ビット分ずつにするのか
  • LED1個分とするのが処理としてはわかりやすい
  • 30usの周期 バッファの書換え時間として十分余裕がある
リセット期間にDMAを止めないのか
  • 停止や開始の処理で、余分なパルスが出力されないように気を付ける必要がある
  • 0をセットすることでパルスを出力しないようにできるので問題ない
  • そのまま割込みの周期をカウントすることで、リセット期間を正確に設定できる

実際のコード

DMA割込み

  • バッファ前半の読み出しが終わった割込みで前半部分の書き換え、バッファ後半の読み出しが終わった割込みで後半部分の書き換えを処理する
  • リセット期間の場合と、フレームデータの読み出し期間の場合で分岐
  • 設定したリセット期間はPWMに無信号を出力し、それが終わった後も次のフレームデータがセットされるまでは無信号を出力し続ける
  • フレーム読み出し期間の場合は、次のLED1個分をフレームバッファから読み出して、24バイトのデータを生成してDMAバッファに書き込む
  • LEDの個数分が終了したらリセット期間へ移行
#define PWM_BIT_0   5
#define PWM_BIT_1   15
#define PWM_RESET_PERIOD   2000

void pwmUpdateFn(uint8_t *p_buf)
{
    uint8_t i, r, g, b;
    uint32_t grbColor;
    uint32_t mask = 0x800000;

    HAL_GPIO_WritePin(DEBUG1_GPIO_Port, DEBUG1_Pin, GPIO_PIN_SET);
    if (pwmState==PWM_STATE_RESET){
        for (i=0; i<BIT_PER_LED; i++){
            p_buf[i] = 0;
        }
        if (pwmResetCount<=0){
            if (bufferFlag!=0){
                ledIndex = 0;
                pwmState = PWM_STATE_SET;
            }
        }else{
            pwmResetCount--;
        }
    }else{
        r = *(frameBufferR+ledIndex);
        g = *(frameBufferG+ledIndex);
        b = *(frameBufferB+ledIndex);
        grbColor = ((uint32_t)g<<16) | ((uint32_t)r<<8) | b;
        for (i=0; i<BIT_PER_LED; i++){
            if (grbColor & mask){
                p_buf[i] = PWM_BIT_1;
            }else{
                p_buf[i] = PWM_BIT_0;
            }
            mask = mask>>1;
        }
        ledIndex++;
        if (ledIndex>=LED_NUM){
            bufferFlag = 0;
            pwmState = PWM_STATE_RESET;
            pwmResetCount = PWM_RESET_PERIOD;
        }
    }
    HAL_GPIO_WritePin(DEBUG1_GPIO_Port, DEBUG1_Pin, GPIO_PIN_RESET);
}
void HAL_TIM_PWM_PulseFinishedHalfCpltCallback(TIM_HandleTypeDef *htim)
{
    pwmUpdateFn(&dmaBuffer[0]);
}
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
    pwmUpdateFn(&dmaBuffer[BIT_PER_LED]);
}

メインループ

  • 前回のフレームの読み出しが終わっているかを確認して、フレームバッファを書き換える
  • ここではLEDを1個ずつ移動させながら赤色で点灯させている
  • 書き終えたら新しいフレームがセットされたことをフラグで示す
#define LED_NUM   16
#define BIT_PER_BYTE   8
#define BIT_PER_LED   24
#define FRAME_SIZE   (LED_NUM)

uint8_t frameBufferR[FRAME_SIZE];
uint8_t frameBufferG[FRAME_SIZE];
uint8_t frameBufferB[FRAME_SIZE];
volatile uint8_t bufferFlag = 0;
uint8_t dmaBuffer[BIT_PER_LED*2];

#define PWM_STATE_RESET   0
#define PWM_STATE_SET   1
volatile uint8_t pwmState = PWM_STATE_RESET;
uint16_t ledIndex;
uint16_t pwmResetCount;

  /* USER CODE BEGIN WHILE */
    for (i=0; i<BIT_PER_LED*2; i++){
        dmaBuffer[i] = 0;
    }
    for (i=0; i<FRAME_SIZE; i++){
        frameBufferR[i] = 0;
        frameBufferG[i] = 0;
        frameBufferB[i] = 0;
    }
    ledIndex = 0;
    bufferFlag = 0;
    pwmState = PWM_STATE_RESET;
    pwmResetCount = 0;
    HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_1, (uint32_t*)dmaBuffer, BIT_PER_LED*2);
    while (1) {
        if (bufferFlag==0){
            HAL_GPIO_WritePin(DEBUG0_GPIO_Port, DEBUG0_Pin, GPIO_PIN_SET);
            for (i=0; i<LED_NUM; i++){
                if (i==ledPos){
                    frameBufferR[i] = 100;
                }else{
                    frameBufferR[i] = 0;
                }
            }
            bufferFlag = 1;
            ledPos = (ledPos+1)%LED_NUM;
            HAL_GPIO_WritePin(DEBUG0_GPIO_Port, DEBUG0_Pin, GPIO_PIN_RESET);
        }
    /* USER CODE END WHILE */