Generating Phase-Shifted GPIO Pulses Using STM32 Timer Interrupts
Cross-File Variable Sharing via UART
To dynamically control the pulse width of GPIOA, pulse width of GPIOB, and the interval between them via serial commands, declare the configuration variables in the UART source file:
int pulseWidthA = 20;
int pulseWidthB = 20;
int pulseGap = 20;Access these variables in other modules, such as the GPIO driver, by using the extern keyword:
extern int pulseWidthA;
extern int pulseWidthB;
extern int pulseGap;Within the UART receive callback, parse the incoming buffer to update these values. Combining stdlib.h and atoi() allows extracting integers, including negative values, from the buffer payload. By offsetting the buffer pointer, specific command payloads can be isolated.
if (rxBuf[0] == 'P' && rxBuf[1] == 'I' && rxBuf[2] == '=') {
int val = atoi(rxBuf + 3);
pulseGap = val;
printf("OK_GAP");
}Interrupt Priority Configuration
If HAL_Delay() is invoked inside an interrupt service routine (ISR), the SysTick interrupt must be assigned the highest priority (0). However, HAL_Delay() is blocking and lacks high precision, often resulting in slightly longer delays than requested. For strict timing requirements, hardware timers are superior.
When an external interrupt triggers a timer to manage GPIO states, the timer's priority must be higher (lower numeric value) than the external interrupt. Otherwise, the timer ISR cannot preempt the external ISR, causing the system to stall. Additionally, timers used purely for polling delays do not require their interrupt lines to be enabled.
External Interrupt Setup
Configure the GPIO pin for the triggering button by selecting the appropriate trigger edge (rising or falling) and internal pull-up/pull-down resistors based on the hardware design. Assigning a user label to the pin in the configuration tool generates a macro in the header file, improving code readability.
Hardware Timer Interrupts
When initializing the timer, the period parameter can often be left at its default if it will be dynamically modified during runtime. Upon timer expiration, the HAL executes the HAL_TIM_PeriodElapsedCallback.
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim == &htimPulseA) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_TIM_Base_Stop_IT(&htimPulseA);
} else if (htim == &htimPulseB) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
HAL_TIM_Base_Stop_IT(&htimPulseB);
}
}Prior to starting the timer with HAL_TIM_Base_Start_IT, clear any pending update flags using __HAL_TIM_CLEAR_IT. Failing to do so may cause the callback to execute immediately upon starting the timer, skipping the desired delay.
Prescaler and Period Calculations
The prescaler register divides the incoming APB timer clock. With a 72 MHz clock and a prescaler of 7199, the timer increments at 10 KHz (0.1 ms per tick). A 10 ms delay requires 100 ticks. This relationship allows for precise polling-based delays without using interrupts:
void custom_delay_ms(uint16_t ms) {
__HAL_TIM_SetCounter(&htimDelay, 0);
__HAL_TIM_ENABLE(&htimDelay);
while (__HAL_TIM_GetCounter(&htimDelay) < (10 * ms));
__HAL_TIM_DISABLE(&htimDelay);
}Given the 16-bit counter limit, the maximum achievable delay with this 10 KHz configuration is roughly 6553 ms.
Non-Blocking Phase-Shifted Pulse Generation
Initially, generating phase-shifted pulses using blocking delays inside an ISR prevents achieving overlapping or reverse-phase relationships. Pulse A will always complete before Pulse B starts.
To resolve this, delegate the pulse termination to timer interrupts. The external interrupt only handles setting the GPIO pins high and starting the respective one-shot timers. For instance, generating Pulse B involves setting the pin and starting the timer:
void triggerPulseB(uint32_t ms) {
MX_TIM_PulseB_Init(ms * 10); // Reinitialize period
__HAL_TIM_CLEAR_IT(&htimPulseB, TIM_IT_UPDATE);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
HAL_TIM_Base_Start_IT(&htimPulseB);
}The timer ISR then automatically pulls the pin low after the specified duration. This non-blocking architecture allows the external interrupt callback to sequence the rising edges in any order based on the interval parameter, while the hardware timers independently manage the falling edges.