Lab 5: Interrupts
Hours spent on lab: 5 Hours
Overview
In this lab, we learn about interrupts, a mechanism to do some action when you detect something. This method compared to polling saves on MCU’s CPU usage, letting the MCU rest or do other tasks and only do the desired action only when needed. We used motor 25GA370 to read out the encoded and waves.
Hardware Setup
I followed the wiring diagram on the Motor 25GA370 Datasheet. Then checked Table 13 and 14 on the STM32L432KCUx MCU Datasheet to choose PB1 and PB7 as 5 Volt tolerant pins:
Schematic
The schematic for the circuit includes the MCU and the Motor.
Calculations
Angular Velocity Calculation (Interrupts)
Looking through the datasheets, we see our motor has some constant 408 \(\frac{\text{Ticks}}{\text{Revolution}}\). Doing some rearranging we discover that we’re going to need to compute the amount of ticks occuring in some time interval to find \(\frac{\text{Ticks}}{\text{Time}}\). Taking the quotient of these values … \(\frac{\frac{\text{Ticks}}{\text{Time}}}{\frac{\text{Ticks}}{\text{Revolution}}} = \frac{\text{Revolutions}}{\text{Time}}\) which is angular velocity. So using a 200 ms sample period and triggering on the rising and falling edge of both signals, let’s increment or decrement some counter (for direction) and then divide by the 4 \(\times\) the sample period to get our desired unknown: \(\frac{\text{Ticks}}{\text{Time}}\).
Polling Comparison
First calibrating my MCU by toggling a pin on and off in a while loop, we can see the fastest frequency our main function can operate is roughly \(15 kHz\). Taking the inverse, we see our sample period is \(T_{MCU} = 66.6 \micro s\).
Additionally, looking at the encoder outputs of our motor, the occur at roughly \(1 kHz\). Which is updating every 1 ms.
Using the Nyquist-Shannon Sampling Theorem (thank you Prof. Bassman & Prof. Yong): \(F_{\text{Sampling}} = 2 \times F_{\text{Max}}\). Since our main loop samples at 15 kHz, and the encoder outputs pulses at \(~1 kHz\), we are oversampling by nearly two orders of magnitude. According to the Nyquist–Shannon Sampling Theorem, we would need at least a \(2 kHz\) sampling rate to accurately capture this signal, and our polling method catches more than enough encoder transitions.
Given the encoder produces 408 ticks per revolution and operates near 1 kHz, the motor speed corresponds to approximately \(\frac{1000 \text{ticks per second}}{408 \text{ticks per revolution}}\) = \(2.45 \frac{\text{Revolutions}}{\text{Second}}\). Because my MCU loop runs at 15 kHz, it reads the encoder every \(66.6 \micro s\) during which less than a tick should occur (0.067 ticks). This confirms that the polling rate is high enough to capture each encoder transition and accurately determine velocity.
AI Prototype
This week I prompted Claude AI with “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?”. For additional context, I was able to upload the Datasheet and Reference Manual to the AI.
/* Quadrature Encoder Interface for STM32L432KC
* Uses TIM2 in Encoder Mode with overflow/underflow interrupts
*
* Connections:
* - Encoder Channel A -> PA0 (TIM2_CH1)
* - Encoder Channel B -> PA1 (TIM2_CH2)
*/
#include "stm32l4xx_hal.h"
// Global variables
TIM_HandleTypeDef htim2;
volatile int32_t encoder_count = 0;
volatile uint32_t overflow_count = 0;
// Initialize quadrature encoder on TIM2
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
TIM_Encoder_InitTypeDef sConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFFFFFF;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
sConfig.EncoderMode = TIM_ENCODERMODE_TI12;
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 0x0F;
sConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sConfig.IC2Filter = 0x0F;
if (HAL_TIM_Encoder_Init(&htim2, &sConfig) != HAL_OK)
Error_Handler();
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
Error_Handler();
__HAL_TIM_SET_COUNTER(&htim2, 0x7FFFFFFF);
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
}
void TIM2_IRQHandler(void)
{
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
{
if (__HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2))
overflow_count--;
else
overflow_count++;
}
}
}
int32_t Encoder_GetCount(void)
{
uint32_t tim_count = __HAL_TIM_GET_COUNTER(&htim2);
int32_t adjusted_count = (int32_t)(tim_count - 0x7FFFFFFF);
return adjusted_count + (overflow_count * 0x100000000LL);
}
void Encoder_Reset(void)
{
__HAL_TIM_SET_COUNTER(&htim2, 0x7FFFFFFF);
overflow_count = 0;
encoder_count = 0;
}
int32_t Encoder_GetVelocity(void)
{
static int32_t last_count = 0;
int32_t current_count = Encoder_GetCount();
int32_t velocity = current_count - last_count;
last_count = current_count;
return velocity;
}
int8_t Encoder_GetDirection(void)
{
return __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2) ? -1 : 1;
}
__weak void Error_Handler(void)
{
__disable_irq();
while (1) {}
}
I think the AI did okay this week. It wanted to use STM32L4xx-hal-driver library and I didn’t put in a lot of effort getting that import to work. Also my check-off is in an hour and I would like to do work in other more important places. Briefly searching through the library, It looks like everything exists and it would likely compile however, I am skeptical of the velocity calculations.
Conclusion
The design worked as intended. The interrupts trigger on the rising and falling edge of each signal and computes the correct angular velocity and direction of spin.