Capturing Dual Encoder Waveforms with STM32F407 Timer 5 Input Capture

This document details a method for capturing and processing waveforms from two digital quadrature encoders using the STM32F407ZGT6 microcontroller. The approach leverages the four input capture channels of Timer 5 (TIM5) to track the rotational position and direction of each encoder.

System Overview

The system utilizes the STM32F407's Cortex-M4 core running at 168MHz. TIM5's channels are configured for input capture to intefrace with two separate encoders, each providing A and B phase quadrature signals. Key functionalities include:

  • Quad Channel Input Capture: TIM5\_CH1, TIM5\_CH2, TIM5\_CH3, and TIM5\_CH4 are assigned to the A and B phases of two encoders (Encoder 1: CH1=A1, CH2=B1; Encoder 2: CH3=A2, CH4=B2).
  • Edge Detection: Capturing on both rising and falling edges of the input signals doubles the effective resolution.
  • Direction and Counting: The phase relationship between the A and B signals determines the direction of rotation, and a pulse counter is maintained accordingly.
  • Real-time Data Access: Encoder counts and direction can be queried from the main application loop or handled via interrupts.

Hardware Design

Core Components

Component Model/Specification Function
Microcontroller STM32F407ZGT6 (TIM5, 4-channel input capture) Encoder signal acquisition, pulse counting, direction determination
Encoders 2 x EC11 Rotary Encoders (A/B phase, 5V) Generate orthogonal pulse trains
Interface Circuit Optocoupler Isolation (TLP521) + Pull-up Resistors (4.7kΩ) Isolates encoders from the MCU, preventing noise interference

Hardware Connections

Signal STM32F407 Pin (TIM5_CHx) Encoder Interface Description
Encoder 1_A PA0 (TIM5_CH1) Encoder 1 Phase A Orthogonal pulse input (dual-edge capture)
Encoder 1_B PA1 (TIM5_CH2) Encoder 1 Phase B Orthogonal pulse input (dual-edge capture)
Encoder 2_A PA2 (TIM5_CH3) Encoder 2 Phase A Orthogonal pulse input (dual-edge capture)
Encoder 2_B PA3 (TIM5_CH4) Encoder 2 Phase B Orthogonal pulse input (dual-edge capture)
Power 3.3V/GND Encoder VCC/GND Encoder power supply (common ground with MCU required)

Software Design (STM32 HAL Library)

Development Environment

  • IDE: STM32CubeIDE 1.13.0+
  • Library: STM32Cube_FW_F4_V1.28.0 (HAL Library)
  • Toolchain: GCC ARM Embedded

Core Principle

Quadrature encoders output A/B phase pulses with a 90° phase difference. By capturing edges on both phases and analyzing their temporal relationship, the system can:

  • Determine Rotation Direction: When Phase A transitions high, if Phase B is high, it indicates forward rotation; if Phase B is low, it indicates reverse rotation. The logic is mirrored for Phase A's falling edge.
  • Count Pulses: Increment or decrement a counter based on the detected edge and rotation direction.
  • Enhance Resolution: Using dual-edge capture effectively quadruples the resolution compared to single-edge capture.

Core Code Implementation

Header File encoder_capture.h (Global Definitions and Variables)


#ifndef __ENCODER_CAPTURE_H
#define __ENCODER_CAPTURE_H

#include "stm32f4xx_hal.h"

// Timer 5 Channel Definitions (Mapping to Encoder Pins)
#define ENCODER1_A_CHANNEL   TIM_CHANNEL_1  // PA0 (TIM5_CH1)
#define ENCODER1_B_CHANNEL   TIM_CHANNEL_2  // PA1 (TIM5_CH2)
#define ENCODER2_A_CHANNEL   TIM_CHANNEL_3  // PA2 (TIM5_CH3)
#define ENCODER2_B_CHANNEL   TIM_CHANNEL_4  // PA3 (TIM5_CH4)

// Encoder State Structure
typedef struct {
    volatile int32_t pulse_count;     // Accumulated pulse count (positive for forward, negative for reverse)
    uint8_t rotation_direction;  // Direction: 0=Stopped, 1=Forward, 2=Reverse
} EncoderState_TypeDef;

// Global Encoder Variables
extern TIM_HandleTypeDef htim5;
extern EncoderState_TypeDef encoder_1_state;  // State for Encoder 1
extern EncoderState_TypeDef encoder_2_state;  // State for Encoder 2

// Function Declarations
void initialize_tim5_capture(void);                                           // Initialize TIM5 for input capture
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);                     // Input capture interrupt handler
void process_encoder_data(EncoderState_TypeDef *encoder_state, uint32_t timer_channel,
                          GPIO_TypeDef *gpio_port, uint16_t gpio_pin_a, uint16_t gpio_pin_b); // Process encoder data

#endif // __ENCODER_CAPTURE_H

TIM5 Initialization (Input Capture Mode)


#include "encoder_capture.h"
#include "gpio.h" // Assuming GPIO initialization is in gpio.c

TIM_HandleTypeDef htim5;
EncoderState_TypeDef encoder_1_state = {0};  // Initialize Encoder 1 count to 0
EncoderState_TypeDef encoder_2_state = {0};  // Initialize Encoder 2 count to 0

// Initialize TIM5 for 4-channel input capture, dual-edge trigger
void initialize_tim5_capture(void) {
    TIM_IC_InitTypeDef tim_ic_config = {0};

    // Basic TIM5 configuration
    htim5.Instance = TIM5;
    htim5.Init.Prescaler = 0;                  // No prescaling (168MHz clock, 168MHz counter frequency)
    htim5.Init.CounterMode = TIM_COUNTERMODE_UP; // Up-counting mode
    htim5.Init.Period = 0xFFFFFFFF;             // Maximum period for 32-bit counter
    htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_IC_Init(&htim5);

    // Configure Channel 1 (Encoder 1_A): Dual-edge trigger, no filtering
    tim_ic_config.ICPolarity = TIM_ICPOLARITY_BOTHEDGE; // Capture on both rising and falling edges
    tim_ic_config.ICSelection = TIM_ICSELECTION_DIRECTTI; // Map TI1 to channel 1
    tim_ic_config.ICPrescaler = TIM_ICPSC_DIV1;           // No prescaling for capture events
    tim_ic_config.ICFilter = 0x00;                        // No filtering (set to 0x0F for noise filtering if needed)
    HAL_TIM_IC_ConfigChannel(&htim5, &tim_ic_config, ENCODER1_A_CHANNEL);

    // Configure Channel 2 (Encoder 1_B): Same configuration
    HAL_TIM_IC_ConfigChannel(&htim5, &tim_ic_config, ENCODER1_B_CHANNEL);

    // Configure Channel 3 (Encoder 2_A): Same configuration
    HAL_TIM_IC_ConfigChannel(&htim5, &tim_ic_config, ENCODER2_A_CHANNEL);

    // Configure Channel 4 (Encoder 2_B): Same configuration
    HAL_TIM_IC_ConfigChannel(&htim5, &tim_ic_config, ENCODER2_B_CHANNEL);

    // Enable TIM5 interrupts for input capture
    HAL_TIM_IC_Start_IT(&htim5, ENCODER1_A_CHANNEL);
    HAL_TIM_IC_Start_IT(&htim5, ENCODER1_B_CHANNEL);
    HAL_TIM_IC_Start_IT(&htim5, ENCODER2_A_CHANNEL);
    HAL_TIM_IC_Start_IT(&htim5, ENCODER2_B_CHANNEL);
}

GPIO Initialization (TIM5 Channel Pins)


void initialize_gpio(void) {
    GPIO_InitTypeDef gpio_config = {0};

    // Enable GPIO Port A clock
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // Configure PA0-PA3 for TIM5 Alternate Function
    gpio_config.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3;
    gpio_config.Mode = GPIO_MODE_AF_PP;       // Alternate Function Push-Pull
    gpio_config.Pull = GPIO_PULLUP;           // Enable pull-up resistors (important for some encoder outputs)
    gpio_config.Speed = GPIO_SPEED_FREQ_HIGH;
    gpio_config.Alternate = GPIO_AF2_TIM5;    // Select TIM5 Alternate Function
    HAL_GPIO_Init(GPIOA, &gpio_config);
}

Input Capture Interrupt Callback (Core Processing Logic)


// Input Capture Interrupt Callback Function (called by HAL library)
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance != TIM5) {
        return; // Only process TIM5 interrupts
    }

    // Event on Encoder 1 Phase A (Channel 1)
    if (htim->Channel == ENCODER1_A_CHANNEL) {
        process_encoder_data(&encoder_1_state, ENCODER1_A_CHANNEL, GPIOA, GPIO_PIN_0, GPIO_PIN_1);
    }
    // Event on Encoder 1 Phase B (Channel 2) - Optional: can be used for redundancy checks
    else if (htim->Channel == ENCODER1_B_CHANNEL) {
        // Additional processing for Phase B if needed
    }
    // Event on Encoder 2 Phase A (Channel 3)
    else if (htim->Channel == ENCODER2_A_CHANNEL) {
        process_encoder_data(&encoder_2_state, ENCODER2_A_CHANNEL, GPIOA, GPIO_PIN_2, GPIO_PIN_3);
    }
    // Event on Encoder 2 Phase B (Channel 4) - Optional
    else if (htim->Channel == ENCODER2_B_CHANNEL) {
        // Additional processing for Phase B if needed
    }
}

// Function to process encoder data (determine direction and update count)
void process_encoder_data(EncoderState_TypeDef *encoder_state, uint32_t timer_channel,
                          GPIO_TypeDef *gpio_port, uint16_t gpio_pin_a, uint16_t gpio_pin_b) {

    // Read current states of Phase A and Phase B
    uint8_t phase_a_level = HAL_GPIO_ReadPin(gpio_port, gpio_pin_a);
    uint8_t phase_b_level = HAL_GPIO_ReadPin(gpio_port, gpio_pin_b);

    // Process only when Phase A edge is captured (primary trigger for direction)
    if (timer_channel == ENCODER1_A_CHANNEL || timer_channel == ENCODER2_A_CHANNEL) {
        // Determine direction based on Phase B level at the time of Phase A transition
        if (phase_a_level) { // Assuming transition captured is Phase A going HIGH
            encoder_state->rotation_direction = (phase_b_level) ? 1 : 2; // B High -> Forward (1), B Low -> Reverse (2)
        } else { // Phase A going LOW
            encoder_state->rotation_direction = (phase_b_level) ? 2 : 1; // B High -> Reverse (2), B Low -> Forward (1)
        }

        // Update the pulse count based on direction
        if (encoder_state->rotation_direction == 1) {
            encoder_state->pulse_count++;
        } else {
            encoder_state->pulse_count--;
        }
    }
}

Main Function (Reading Encoder Data]


#include "main.h" // Assuming main.h contains system clock config and UART handles
#include "encoder_capture.h"
#include <stdio.h> // For printf

// Declare UART handle if not in main.h
extern UART_HandleTypeDef huart1; // Example UART handle for printf

int main(void) {
    // HAL initialization and system clock configuration
    HAL_Init();
    SystemClock_Config(); // Configures system clock to 168MHz

    // Peripheral initialization
    initialize_gpio();        // Initialize GPIO pins for TIM5 channels
    initialize_tim5_capture(); // Initialize TIM5 for input capture

    // Main application loop
    while (1) {
        // Example: Print encoder data to a serial console via UART
        printf("Encoder 1: Count=%ld, Direction=%d\r\n", encoder_1_state.pulse_count, encoder_1_state.rotation_direction);
        printf("Encoder 2: Count=%ld, Direction=%d\r\n", encoder_2_state.pulse_count, encoder_2_state.rotation_direction);

        HAL_Delay(100); // Update interval of 100 milliseconds
    }
}

// Redirect printf to UART for debugging output
int fputc(int ch, FILE *f) {
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

Key Issues and Solutions

1. Encoder Signal Interference

  • Problem: Noisy signals from the encoder due to motor operation can lead to erroneous counts.
  • Solutions:
    • Hardware: Add a series resistor (e.g., 100Ω) and a parallel capacitor (e.g., 0.1μF) on the encoder output lines.
    • Software: Implement software debouncing in process_encoder_data, requiring multiple consistent edge detections before updating the count.

2. Interrupt Priority Conflicts

  • Problem: Simultaneous triggers on multiple capture channels can cause lost events or incorrect sequencing.
  • Solutions:
    • Configure a higher priority for the TIM5 interrupt in the NVIC (e.g., Preemption Priority 2).
    • Minimize code within the HAL_TIM_IC_CaptureCallback function. Mark events and perform complex calculations in the main loop or a dedicated task to reduce interrupt latency.

3. Incorrect Direction Determination

  • Problem: The logic for direction detection may not match the encoder's physical output or wiring (e.g., swapped A/B phases).
  • Solutions:
    • Verify the physical wiring of Phase A and Phase B signals.
    • Adjust the direction logic within process_encoder_data. For instance, invert the condition on phase_b_level if the encoder is wired backward or if the definition of "forward" needs to be reversed.

Testing and Verification

  1. Hardware Connection Check: Ensure encoders are connected correctly as per section 2.2, paying attention to the optocoupler circuit.
  2. Functional Test: Manually rotate Encoder 1 and observe if encoder_1_state.pulse_count increments for one direction and decrements for the other. Verify encoder_1_state.rotation_direction.
  3. Accuracy Test: Rotate an encoder for one full revolution (e.g., 20 pulses). With dual-edge capture, the expected count should be 20 * 4 = 80. Verify the count is within an acceptable tolerance (e.g., ±1 count).

Conclusion

This implementation effectively uses the STM32F407's TIM5 peripheral in input capture mode to monitor two quadrature encoders. The dual-edge capture technique enhances resolution, while phase analysis accurately determines rotation direction. The modular design allows for scalability to systems with more encoders or integration into closed-loop control systems like PID controllers.

Tags: STM32F407 TIM5 Input Capture Quadrature Encoder Embedded C

Posted on Wed, 03 Jun 2026 18:13:14 +0000 by mattd123