Lab 5: Interrupts

Introduction

This report outlines the design of interrupt based firmware to calculate the angular velocity of a DC motor using quadrature encoders

Design methodology

To ensure maximum accuracy accross all speeds, we use rising and falling edge interrupts for both quad encoders, and then use the values values of the two quad encoder signals to determine the direction of rotation

Timing design and calculations

To find the rotation speed of the motor, I have a variable that stores the number of IRQs that have occured which is incremented in the interrupt handlers for the A and B quad encoder signals. To turn this count into a speed, I have a while loop in my main function that resets the counter and an internal timer, waits for the timer to surpass 1 sample period (200 ms), then does the following calculation to find the number of revolutions per second

\[ M (Pulses) = \frac{N (Interrupts)}{4 (Interrupts/Pulse)} \]

\[ RPS = \frac{\frac{M (Pulses)}{408 (Pulses/Rev)}}{\frac{P (ms)}{1000 (ms/s)}} \]

Where N is the number of interrupts counted, M is the number of pulses, and P is the sample period in milliseconds.

This methodology was implemented into a program with the following process flow diagram. Figure 1: Process flow for main program and interrupt handlers

Timing Verification

To verify the timing calculations above, I used an oscilliscope to measure the period of a quad encoder pulse, and the used the known value of 408 pulses/rev to find the true RPS.

Figure 2: Oscilliscope trace of quad encoder with 5V motor input voltage Figure 3: Debug terminal output with 5V motor input voltage

\[ RPS = \frac{F (pulse/s)}{408 (pulse/rev)} = \frac{462 (pulse/s)}{408 (pulse/rev)} = 1.1326 rev/s \]

While this trace was taken, the output terminal reported an RPS of ~1.15, which is approximately equal to the true value calculated above.

Technical Documentation

The SystemVerilog source code for this lab can by found on my github page

Schematic

Figure 4: Schematic of motor and quad encoder circuit

The schematic in figure 4 demonstrates the simple wiring for the quad encoder to the motherboard.

Results and discussion

The design accurately displayed the angular speed of the motor and direction of rotation across a wide range of speeds.

This lab took roughly 10 hours.

Comparison with polling

To investigate the advantage of interrupt based timing calculations to polling, I repeated this assignment to use polling instead of interrupts, and in the main while loop toggled a pin to probe the frequency of this sampling.

Figure 5: Oscilliscope trace of main polling loop

Since this pin toggles every time the main while loop cycles, one half period is the sample period for this polling based design. To ensure that polling does not miss any pin transition events, this sample frequency must be at least twice that of the quad encoders (per Shannons Theorem).

This means that the maximum pulses per second is 2 times the polling frequency, or 11.62 kHz. Finally, since there are 408 pulses per revolution, the maximum revolutions per second is

\[ RPS \le \frac{11620 (pulses/s)}{408 (pulses/rev)} = 28.48 (rev/s) \]

So polling should be adequate for the operating range of the motor.

AI Prototype

I gave MS Bing the following prompt:

“Write me interrupt handlers to interface with a quadrature encoder. I’m using the STM32L432KC, what pins should I connect the encoder to in order to allow it to easily trigger the interrupts?”

And it produced the following code:


// --- Initialization ---
void Encoder_Init(void) {
    // Enable GPIOA and TIM2 clocks
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN;

    // Configure PA0, PA1 as AF1 (TIM2_CH1, TIM2_CH2)
    GPIOA->MODER &= ~((3 << (0*2)) | (3 << (1*2))); // clear mode
    GPIOA->MODER |=  (2 << (0*2)) | (2 << (1*2));   // AF mode
    GPIOA->AFR[0] |= (1 << (0*4)) | (1 << (1*4));   // AF1

    // Configure TIM2 in encoder mode
    TIM2->SMCR = TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1;   // Encoder mode 3 (count on both TI1 and TI2)
    TIM2->CCMR1 = (1 << 0) | (1 << 8);              // CC1S=01, CC2S=01 (inputs)
    TIM2->CCER = 0;                                 // Rising edge polarity
    TIM2->ARR = 0xFFFFFFFF;                         // Max count
    TIM2->CNT = 0;                                  // Reset counter

    // Enable update interrupt
    TIM2->DIER |= TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM2_IRQn);

    // Start timer
    TIM2->CR1 |= TIM_CR1_CEN;
}

// --- Interrupt Handler ---
void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {   // Update interrupt flag
        TIM2->SR &= ~TIM_SR_UIF;   // Clear flag
        // Handle overflow/underflow here
    }
}

#define ENCODER_CPR   (PPR * 4)    // Example: 1024 PPR encoder, x4 decoding

volatile int32_t last_count = 0;
volatile float rps = 0.0f;

void SysTick_Handler(void) {
    int32_t current = TIM2->CNT;
    int32_t delta = current - last_count;
    last_count = current;

    // Handle wraparound (TIM2 is 32-bit, so usually not needed unless very long intervals)
    // delta is signed, so negative means reverse rotation

    float revolutions = (float)delta / ENCODER_CPR;
    rps = revolutions / (SAMPLE_PERIOD/1000);
}

Which is different from any approach I have seen my peers pursue in the lab. Bing claims that this makes use on built in hardware encoder handling that is more efficient than counting interrupts.

All I had to do was make minor edits and write a simple main function to begin testing this code. While this approach may have been viable, I could not get it to work and Bing was not helpful in debugging.

Overalll, my impression is that this tool is great for ideating general approaches, but the implementation of those approaches are better left to the engineer, since it is incredibly difficult to debug another “person’s” code that you had no part in writing.

I see myself using AI in the future only to give me ideas on how to go about solving a problem, not to try and solve the problem for me.