[Arduino]UNO QでAB相ハードウエアカウンタ(TIM2)が動いた話

やっとArduino UNO Qで、ハードウエアエンコーダ(AB相)の計測ができるようになった。TIM2を用いることで、32bitカウンタが利用できる。これはでかい。ESP32にはハードウエアABカウンタがない(らしい)ので、STM32U585の得点になりますね。
一応、記念に残しておきます。どなたかのために。

#Arduino #UNO #Q #encoder #TIM2

[stm32_enc_tim2.ino]
/*
  TIM2でAB相エンコーダのカウント成功(2026/01/28)
    TIM2_CH1 : D15(PA5)
    TIM2_CH2 : D02(PB3)
*/

#include "stm32_enc.h"
#include <stm32u5xx_ll_tim.h>

#define XSERIAL Serial1 // PDF資料に基づきUART1を使用
#define ENC_A 15        // D15
#define ENC_B 2         // D2

unsigned long debug_interval = 0;
STM32Encoder enc(1.0);  // 係数 1.0 で初期化

void setup() {
    XSERIAL.begin(230400);
    enc.begin();
    enc.reset();
    XSERIAL.println("TIM2 Encoder (D15/D2) Ready.");
}

void loop() {
    if (millis() - debug_interval >= 500) {
        debug_interval = millis();
        
        // TIM2の値を直接、およびクラス経由で表示
        uint32_t raw_reg = LL_TIM_GetCounter(TIM2);
        
        XSERIAL.print("Reg:");
        XSERIAL.print(raw_reg);
        XSERIAL.print(" | Count:");
        XSERIAL.print(enc.getCount());
        XSERIAL.print(" | Phys:");
        XSERIAL.println(enc.getPhysicalValue());
    }

    if (XSERIAL.available() > 0) {
        String input = XSERIAL.readStringUntil('\n');
        input.trim();
        if (input == "R") {
            enc.reset();
            XSERIAL.println("Reset Done.");
        }
    }
}
[stm32_enc.h]
#ifndef STM32_ENC_H
#define STM32_ENC_H

#pragma once
#include <stdint.h>

class STM32Encoder {
public:
    // コンストラクタ: 物理変換係数を設定 (デフォルト 1.0)
    STM32Encoder(double factor = 1.0);

    void begin();
    void reset();
    
    // 生のカウント値を返す (TIM2は32bit)
    int32_t getCount();
    
    // (getCount / 4.0) * factor を返す
    double getPhysicalValue();

    // 係数を後から変更する
    void setFactor(double factor);

private:
    double _factor;
};

#endif
[stm32_enc.cpp]
#include "stm32_enc.h"
#include <Arduino.h>
#include <stm32u5xx_ll_tim.h>
#include <stm32u5xx_ll_bus.h>
#include <stm32u5xx_ll_gpio.h>

STM32Encoder::STM32Encoder(double factor) : _factor(factor) {}

void STM32Encoder::begin() {
    // 1. クロック有効化 (TIM2, Port A, Port B)
    LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOA);
    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOB);

    // 2. D15 (PA5) -> TIM2_CH1 (AF1)
    LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_ALTERNATE);
    LL_GPIO_SetAFPin_0_7(GPIOA, LL_GPIO_PIN_5, LL_GPIO_AF_1);
    LL_GPIO_SetPinPull(GPIOA, LL_GPIO_PIN_5, LL_GPIO_PULL_UP);
    LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_5, LL_GPIO_SPEED_FREQ_VERY_HIGH);

    // 3. D2 (PB3) -> TIM2_CH2 (AF1)
    LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_3, LL_GPIO_MODE_ALTERNATE);
    LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_3, LL_GPIO_AF_1);
    LL_GPIO_SetPinPull(GPIOB, LL_GPIO_PIN_3, LL_GPIO_PULL_UP);
    LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_3, LL_GPIO_SPEED_FREQ_VERY_HIGH);

    // 4. TIM2 設定
    LL_TIM_DisableCounter(TIM2);
    LL_TIM_SetPrescaler(TIM2, 0);
    LL_TIM_SetEncoderMode(TIM2, LL_TIM_ENCODERMODE_X4_TI12);

    // 入力設定 (正転/逆転は以前の知見に基づき、物理配線に合わせて調整してください)
    LL_TIM_IC_SetActiveInput(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_ACTIVEINPUT_DIRECTTI);
    LL_TIM_IC_SetPolarity(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_IC_POLARITY_RISING); 
    LL_TIM_IC_SetFilter(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_IC_FILTER_FDIV1);

    LL_TIM_IC_SetActiveInput(TIM2, LL_TIM_CHANNEL_CH2, LL_TIM_ACTIVEINPUT_DIRECTTI);
    LL_TIM_IC_SetPolarity(TIM2, LL_TIM_CHANNEL_CH2, LL_TIM_IC_POLARITY_RISING);
    LL_TIM_IC_SetFilter(TIM2, LL_TIM_CHANNEL_CH2, LL_TIM_IC_FILTER_FDIV1);

    // 32bit対応 (最大値 0xFFFFFFFF)
    LL_TIM_SetAutoReload(TIM2, 0xFFFFFFFF);
    LL_TIM_SetCounter(TIM2, 0);
    LL_TIM_EnableCounter(TIM2);
}

int32_t STM32Encoder::getCount() {
    // 32bitレジスタをそのまま符号付き32bitとして取得
    return (int32_t)LL_TIM_GetCounter(TIM2);
}

double STM32Encoder::getPhysicalValue() {
    return ((double)getCount() / 4.0) * _factor;
}

void STM32Encoder::setFactor(double factor) {
    _factor = factor;
}

void STM32Encoder::reset() {
    LL_TIM_SetCounter(TIM2, 0);
}