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_CaptureCallbackfunction. 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 onphase_b_levelif the encoder is wired backward or if the definition of "forward" needs to be reversed.
Testing and Verification
- Hardware Connection Check: Ensure encoders are connected correctly as per section 2.2, paying attention to the optocoupler circuit.
- Functional Test: Manually rotate Encoder 1 and observe if
encoder_1_state.pulse_countincrements for one direction and decrements for the other. Verifyencoder_1_state.rotation_direction. - 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.