2025-09-09 7:45 AM
I am officially confused.
I'm trying to use an ADC without the HAL layer, and for some reason I just don't get it to work. With HAL, it works fine.
I have tried comparing register contents, and I've now run into a wall. There must be something the HAL function is doing that I am missing or misunderstanding.
In this test, I am trying to sample ADC2, channel 16, just once, without DMA or anything. With my own code, ADCSTART never goes low after I've started the ADC.
When I check what the HAL code does differently, I simply don't get it.
The HAL setup code (which works):
ADC2Handle->Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
ADC2Handle->Init.ContinuousConvMode = DISABLE;
ADC2Handle->Init.DMAContinuousRequests = DISABLE;
ADC2Handle->Init.DataAlign = ADC_DATAALIGN_RIGHT;
ADC2Handle->Init.DiscontinuousConvMode = DISABLE;
ADC2Handle->Init.EOCSelection = ADC_EOC_SINGLE_CONV;
ADC2Handle->Init.ExternalTrigConv = ADC_SOFTWARE_START;
ADC2Handle->Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
ADC2Handle->Init.LowPowerAutoWait = DISABLE;
ADC2Handle->Init.NbrOfConversion = 1;
ADC2Handle->Init.NbrOfDiscConversion = 0;
ADC2Handle->Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
ADC2Handle->Init.OversamplingMode = DISABLE;
ADC2Handle->Init.Resolution = ADC_RESOLUTION_12B;
ADC2Handle->Init.ScanConvMode = ADC_SCAN_ENABLE;
HAL_ADC_Init(ADC2Handle);
ChannelCfg.Channel = 16;
ChannelCfg.Offset = 0;
ChannelCfg.OffsetNumber = ADC_OFFSET_NONE;
ChannelCfg.Rank = ADC_REGULAR_RANK_1;
ChannelCfg.SamplingTime = ADC_SAMPLETIME_640CYCLES_5;
ChannelCfg.SingleDiff = ADC_SINGLE_ENDED;
HAL_ADC_ConfigChannel(ADC2Handle, &ChannelCfg);
HAL_ADC_Start(ADC2Handle);
In short, I just want to sample channel 16 once, with 12 bit resolution and 640.5 cycles.
The main registers I have checked are these:
CR = 0x10000005 (ADC regulator on; regular conversion started; ADC enabled)
CFGR = 0x80001000 (Injected queue disabled; overrun mode)
CFGR2 = 0 (Makes sense, as I don't use oversampling)
SMPR1 = 0x00000007 (This confuses me a lot. Doesn't this just set channel 0 sampling time to 640.5 cycles?
SMPR2 = 0 (... and why is this 0? Shouldn't it have been used for setting the channel 16 sampling time?)
SQR1 = 0 (Even more confusing. Surely, this should be pointing at channel 16 as the one and only channel in the sequence. Instead, it points at channel 0.)
I have also checked that the DMA channel if off, as I don't use DMA for this single sample:
DMA2->CCR4 = 0x00000580 (The main thing here is that the DMA channel is off. Which it should be.)
I would have expected SMPR2 to be (at least) 0x001C0000, to set the channel 16 sampling time to 640.5 cycles. Also, I expected SQR1 to be 0x00000400, to indicate channel 16 for the first and only conversion. Instead, both registers read 0, which I simply don't understand.
Have I completely misunderstood how the ADC works? When I read the manual, the SMPRx registers contain the sample times for each channel (regardless of sequence), and the SQRx registers are used for specifying the sample sequence and the number of conversions.
Can anytone shed some light on this?
2025-09-09 10:53 AM - edited 2025-09-09 10:56 AM
This IOC configuration doesn't match the code you posted. Here is the ADC2 configuration. Note that 3 channels are being converted, not 1.
Nor does it produce the code you posted.
The handle it generates is called hadc2, not ADC2Handle.
I don't think you're looking at the code it's generating.
> I find the HAL code pretty hard to follow.
In my experience any non-trivial code that you didn't write is hard to follow, especially at first. Takes a bit of time and effort. Single step through, it can be understood.
2025-09-09 11:18 AM
I know it doesn't match. Also, you don't have my entire code - nor would you want it.
Rather than dumping a ton of code in main.c, I spread it out in separate files. One crucial part of my file adc.c is this:
void adcInitHandle(ADC_HandleTypeDef* adc2Handle, DMA_HandleTypeDef* DMAHandle)
{
ADC2Handle = adc2Handle;
ADC2DMAHandle = DMAHandle;
}
which is used for passing on the pointer to the HAL handle like this (in main.c)
adcInitHandle(&hadc2, &hdma_adc2);
I do this all the time, and it works nicely.
I assure you, that the registers I read are the actual ones.
Mostly, "foreign" code can be read. But in my opinion. the HAL code is unnecessarily cumbersome, such as
__STATIC_INLINE void LL_ADC_REG_SetSequencerRanks(ADC_TypeDef *ADCx, uint32_t Rank, uint32_t Channel)
{
/* Set bits with content of parameter "Channel" with bits position */
/* in register and register position depending on parameter "Rank". */
/* Parameters "Rank" and "Channel" are used with masks because containing */
/* other bits reserved for other purpose. */
__IO uint32_t *preg = __ADC_PTR_REG_OFFSET(ADCx->SQR1,
((Rank & ADC_REG_SQRX_REGOFFSET_MASK) >> ADC_SQRX_REGOFFSET_POS));
MODIFY_REG(*preg,
ADC_CHANNEL_ID_NUMBER_MASK_POSBIT0 << (Rank & ADC_REG_RANK_ID_SQRX_MASK),
((Channel & ADC_CHANNEL_ID_NUMBER_MASK) >> ADC_CHANNEL_ID_NUMBER_BITOFFSET_POS)
<< (Rank & ADC_REG_RANK_ID_SQRX_MASK));
}
Some may find this easy-peasy. I do not. Especially since STM32CubeIDE doesn't immediately show me what all these constants are. I need to search for each and everyone. So going through an entire HAL function can take a very long time.
2025-09-09 1:06 PM
Seems to be helpful.
2025-09-09 2:28 PM
Okay, I've done some more digging.
First, I read the contents of a bunch of ADC2 registers right after initialisation in main.c (the auto-generated code):
static void MX_ADC2_Init(void)
{
/* USER CODE BEGIN ADC2_Init 0 */
/* USER CODE END ADC2_Init 0 */
ADC_ChannelConfTypeDef sConfig = {0};
/* USER CODE BEGIN ADC2_Init 1 */
/* USER CODE END ADC2_Init 1 */
/** Common config
*/
hadc2.Instance = ADC2;
hadc2.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
hadc2.Init.Resolution = ADC_RESOLUTION_12B;
hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc2.Init.ScanConvMode = ADC_SCAN_ENABLE;
hadc2.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
hadc2.Init.LowPowerAutoWait = DISABLE;
hadc2.Init.ContinuousConvMode = DISABLE;
hadc2.Init.NbrOfConversion = 3;
hadc2.Init.DiscontinuousConvMode = DISABLE;
hadc2.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc2.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc2.Init.DMAContinuousRequests = DISABLE;
hadc2.Init.Overrun = ADC_OVR_DATA_PRESERVED;
hadc2.Init.OversamplingMode = DISABLE;
if (HAL_ADC_Init(&hadc2) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_5;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_640CYCLES_5;
sConfig.SingleDiff = ADC_SINGLE_ENDED;
sConfig.OffsetNumber = ADC_OFFSET_NONE;
sConfig.Offset = 0;
if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_6;
sConfig.Rank = ADC_REGULAR_RANK_2;
if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_7;
sConfig.Rank = ADC_REGULAR_RANK_3;
if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN ADC2_Init 2 */
slDebug[0] = ADC2->CR;
slDebug[1] = ADC2->CFGR;
slDebug[2] = ADC2->CFGR2;
slDebug[3] = ADC2->SMPR1;
slDebug[4] = ADC2->SMPR2;
slDebug[5] = ADC2->SQR1;
slDebug[6] = ADC2->SQR2;
slDebug[7] = ADC2->SQR3;
slDebug[8] = ADC2->SQR4;
slDebug[14] = (uintptr_t)&hadc2;
/* USER CODE END ADC2_Init 2 */
}
In addition, I also read the address of hadc2, just to make sure that it matches my pointer in adc.c.
All the values make sense. In particular, I would like to share the contents of SMPR1 and SQR1:
ADC2_SMPR1 = 0x00FF8000 (ADC5, 6 and 7 are all set to sample time 640.5)
ADC2_SQR1 = 0x001C6142 (ADC5, 6 and 7 are set as the first three channels in a sequence of three conversions)
Perfect. Then I copied the exact same register readouts to my actual adc code:
HAL_ADC_DeInit(ADC2Handle);
ADC2Handle->Instance = ADC2;
ADC2Handle->Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
ADC2Handle->Init.ContinuousConvMode = DISABLE;
ADC2Handle->Init.DMAContinuousRequests = DISABLE;
ADC2Handle->Init.DataAlign = ADC_DATAALIGN_RIGHT;
ADC2Handle->Init.DiscontinuousConvMode = DISABLE;
ADC2Handle->Init.EOCSelection = ADC_EOC_SINGLE_CONV;
ADC2Handle->Init.ExternalTrigConv = ADC_SOFTWARE_START;
ADC2Handle->Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
ADC2Handle->Init.LowPowerAutoWait = DISABLE;
ADC2Handle->Init.NbrOfConversion = 1;
ADC2Handle->Init.NbrOfDiscConversion = 0;
ADC2Handle->Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
ADC2Handle->Init.OversamplingMode = DISABLE;
ADC2Handle->Init.Resolution = ADC_RESOLUTION_12B;
ADC2Handle->Init.ScanConvMode = ADC_SCAN_ENABLE;
HAL_ADC_Init(ADC2Handle);
ChannelCfg.Channel = 16;
ChannelCfg.Offset = 0;
ChannelCfg.OffsetNumber = ADC_OFFSET_NONE;
ChannelCfg.Rank = ADC_REGULAR_RANK_1;
ChannelCfg.SamplingTime = ADC_SAMPLETIME_640CYCLES_5;
ChannelCfg.SingleDiff = ADC_SINGLE_ENDED;
HAL_ADC_ConfigChannel(ADC2Handle, &ChannelCfg);
HAL_ADC_Start(ADC2Handle);
slDebug[0] = ADC2->CR;
slDebug[1] = ADC2->CFGR;
slDebug[2] = ADC2->CFGR2;
slDebug[3] = ADC2->SMPR1;
slDebug[4] = ADC2->SMPR2;
slDebug[5] = ADC2->SQR1;
slDebug[6] = ADC2->SQR2;
slDebug[7] = ADC2->SQR3;
slDebug[8] = ADC2->SQR4;
slDebug[15] = (uintptr_t)ADC2Handle;
Now SMPR1 and SQR1 no longer made sense. That is, until I removed the line
HAL_ADC_DeInit(ADC2Handle);
I had added it in order to "begin from scratch". But clearly that's not how it works. What is missing, I can't quite figure out. And I'm not sure that I even care. Without the DeInit function and the unnecessary line below it, I now get meaningful values in the registers.
I'm closing this for now. Have a good night.
P.S. ADC2Handle perfectly points at hadc2.