You’re missing the interrupt handler for the ADC that actually triggers this callback.
extern "C" void ADC1_2_IRQHandler(void) {
HAL_ADC_IRQHandler(&hadc1);
}
I’ve verified that this example works fine:
#include <Arduino.h>
ADC_HandleTypeDef hadc1;
TIM_HandleTypeDef htim3;
void init_adc_and_tim() {
__HAL_RCC_ADC1_CLK_ENABLE();
ADC_ChannelConfTypeDef sConfig = {0};
/* Common config */
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
Error_Handler();
}
/* Configure Regular Channel */
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK){
Error_Handler();
}
/* TIM3 Init */
__HAL_RCC_TIM3_CLK_ENABLE();
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM3_Init 1 */
/* USER CODE END TIM3_Init 1 */
htim3.Instance = TIM3;
htim3.Init.Prescaler = 7200 - 1;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 100 - 1;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim3) != HAL_OK) {
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK) {
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK) {
Error_Handler();
}
// Enable interrupt for ADC1_2
HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(ADC1_2_IRQn);
}
/* interrupt handler for ADC */
extern "C" void ADC1_2_IRQHandler(void) {
HAL_ADC_IRQHandler(&hadc1);
}
static volatile uint16_t adcValue = 0;
static volatile bool adcChanged = false;
/* called by the HAL ADC in interrupt context when conversion is complete */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
adcValue = (uint16_t) HAL_ADC_GetValue(hadc);
adcChanged = true;
}
}
void setup() {
Serial.begin(921600);
// use one analogRead to setup pin correctly in analog mode
(void) analogRead(PA0);
init_adc_and_tim();
// kick off timer to generate TRGO events
HAL_TIM_Base_Start(&htim3);
// kick off process once
HAL_ADC_Start_IT(&hadc1);
}
void loop() {
if (adcChanged) {
__disable_irq(); // so that the value doesn't change as we read it out
String line = "[" + String(millis()) + "] New ADC value: " + String(adcValue);
adcChanged = false;
__enable_irq();
Serial.println(line);
}
}
It consistently produces a new ADC value every 10 milliseconds, which is 100 Hz.

This example still doesn’t use DMA to transfer the value from the ADC peripheral into a e.g. (circular) RAM buffer, but it’s a start. Now, the interrupt processing time for the ADC should be much, much lower than before, because the ADC result is already available.