Skip to main content
Graduate II
September 9, 2025
Question

STM32L475V: Using ADC without HAL fails miserably

  • September 9, 2025
  • 9 replies
  • 1221 views

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?

    This topic has been closed for replies.

    9 replies

    Super User
    September 9, 2025

    @EThom.3 wrote:

    With HAL, it works fine


    So why not just use HAL ?

    EThom.3Author
    Graduate II
    September 9, 2025

    Fair point.

    I need to understand how this stuff works. Especially when I'm going to do some more advanced stuff.

    In my opinion, the settings that HAL creates do not correspond with the device manual.

    Graduate II
    September 11, 2025

    HAL adc driver code works perfectly well, it's hardly rocket science. It just writes to the relevant registers to configure and activate ADC. If you're going to do more advanced stuff and want to know how every line of code works you might spend a lot of time faffing around re-inventing the wheel. I'd suggest choosing your battles and this one isn't worth the effort.

    Graduate II
    September 9, 2025

    The HAL init functions do a lot of "hidden" stuff, like activating the peripheral clock and setting up the GPIOs.

    Have you taken care of that?

    PS: I don't like using HAL and try to avoid it, but for the H7 ADC with the "ranks" & mux I gave up and used HAL. Mostly because in that app the ADC is used almost only for system-irrelevant info.
    For another project with a L031 with only 32kB flash and the ADC being an important feature (and much simpler) I used direct register access.

    EThom.3Author
    Graduate II
    September 9, 2025

    Thanks. Although I didn't write it in my post, I did read the RCC_AHB2ENR register to check that the ADC clock is enabled. And it is.

    The GPIOs should be set up in main.c, but I'll check the registers.

    At the moment, my main concern is that HAL sets the SMPR and SQR registers completely differently from what I would have expected – and to something that, according to the manual, doesn't make any sense.

    P.S. I also have a dertain dislike of HAL, as I think it is often too...opaque. It is a bit like Arduino, I think. "You don't need to know how technical stuff works. Just use this, and with minimal knowledge and effort, you are programming Yaaay!"

    Super User
    September 9, 2025

    HAL source is freely available and can be easily inspected in the project you use it in. It's only opaque if you don't look at it.

    Can you post the IOC you're using? The SMPR and SQR registers should be configured differently than what you say, as you note, so something is amiss. If they are working then perhaps you're looking at them before they're configured.

    EThom.3Author
    Graduate II
    September 9, 2025

    Actually, I find the HAL code pretty hard to follow.

    I have attached the IOC file, manually modified to remove customer information.

    No, I most definitely look at them after configuration. I have used different methods, such as copying the register contents to a debug array, which I could then show in a display. Or halting the processor and reading the registers with STM32CubeProgrammer. The results from these two methods match.

    An example of register content reading:

     HAL_ADC_DeInit(ADC2Handle);
     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[1] = ADC2->CR;
     slDebug[2] = ADC2->CFGR;
     slDebug[3] = ADC2->CFGR2;
     slDebug[4] = ADC2->SMPR1;
     slDebug[5] = ADC2->SMPR2;
     slDebug[6] = ADC2->SQR1;
     //slDebug[5] = ADC2DMAHandle->Instance->CCR;
     //slDebug[6] = ADC2DMAHandle->Instance->CNDTR;
     slDebug[7] = ADC2->SQR2;

     

    Super User
    September 9, 2025

    @EThom.3 wrote:

    I find the HAL code pretty hard to follow.


    Might be easiest to follow in the debugger?

    EThom.3Author
    Graduate II
    September 9, 2025

    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.

    Super User
    September 10, 2025

    I don't understand this whole thread.

    You complain about your own non-Cube/HAL code which is failing, but where is it?

    JW

    Explorer
    September 10, 2025

    I would agree. The presentation of the non-HAL code and related issues is quite confusing.

    Perhaps the OP can extract the relevant parts in snippets, and explains what fails.

    Understanding the distaste for Cube/HAL code, I'd like to add that the roll-your-own way definitely includes a careful study of the related reference manual sections.

    And I would start with a Cube example project and debug it, less as a code base for my own projects but rather to observe and check the proper sequence of required initialisations.

    On a related note, I did the same for some F3 / F4 derivates back then, taking apart "old" SPL code. But this library had a clean design, and was much easier to strip down.

    EThom.3Author
    Graduate II
    September 11, 2025

    @Ozone wrote:

    I would agree. The presentation of the non-HAL code and related issues is quite confusing.

     


    Yeah, sorry about that. I'll blame the confusion on my brain being fried by struggling too long without a healthy break.

    Super User
    September 10, 2025

    The best I can guess, "non-HAL code" here actually means "non-CubeMX generated code". As the issue was the ADC2 is being re-initialized after it had already been initialized by CubeMX.

    Took quite a few posts to actually convey that information.

    EThom.3Author
    Graduate II
    September 11, 2025

    Not quite.

    What I meant by non-HAL code was writing directly to the registers, such as

    ADC2->SQR1 = (7 << ADC_SQR1_SQ2_Pos) | (6 << ADC_SQR1_SQ1_Pos) | 1;

    Strangely, I prefer to do this, rather than using the HAL functions.

    But my point of the whole thread was actually why the HAL function were filling the registers with junk I didn't understand. That has been sorted out now.

    Super User
    September 10, 2025

    Ah, I see, thanks for the explanation. 

    JW

    Graduate II
    September 11, 2025

    Strangely, I prefer to do this, rather than using the HAL functions.

    :D Absolutely not strange!

    We should not forget that HAL is written for at least whole controller families, and for all the features which many users don't need, plus the assert stuff, and many (IMO) unnecessary macros for register access.

    All that makes some parts of HAL hard to understand / get through.

    As a colleague once said: it's ugly.

     

    EThom.3Author
    Graduate II
    September 11, 2025

    Thank you! Your comment makes me feel less weird. :smiling_face_with_smiling_eyes:

    By the way, next time I run into a similarly weird issue, I'll try to remember to do something else to clear my mind before flooding the ST community with incoherent rants.

    Explorer
    September 11, 2025

    And one should not forget the often incomprehensible concepts and restrictions the Cube framework imposes on the user. Like sequestering the SysTick interrupt for delay loops.
    Leaving aside the average quality and robustness of example code, I just don't see the point in installing another 1+GB vendor-specific development environment, especially in a professional environment working with several vendors and architectures.

    I understand that ST wants to attract unexperienced students and hobbyist as future customers with a simple "click and drag" GUI. However, I might remind of one of Murphy's laws here, which states :
    Build a system that even a f.o.o.l  can use and only a f.o.o.l will want to use it.

    Graduate II
    September 11, 2025

    I think that too many people use HAL without understanding what's actually going on in the STM32.

    So for using registers you must understand what's going on, RTFM is a must! ;)

    HAL-using people fall on their faces as soon as something's not working as it should, or if something cannot be solved by using HAL.

    I'm responsible for the (industrial testing) products I develop, I must understand 100% how it's working, to make sure it's reliable, and in case of problems being able to debug and improve it.

    So when using HAL, I make sure that a) I couldn't do it better / faster / more reliable (e.g. some clock & DMA setup for me), or b) the HAL part is not "important", or c) it's a quick way to bring a peripheral up for 1st tests.

    And I make sure that the HAL sources I use are not updated automatically (I don't get how one can be so *** careless).

    EThom.3Author
    Graduate II
    September 11, 2025

    I "grew up" with the Atmel 8-bit AVR microcontrollers and command-line GCC. No helpful HAL-like stuff whatsoever. The way forward was the reading the datasheet/manual and writing hex-codes directly to registers.

    I guess it is kinda in my blood, so when I in desperation succumb to using HAL functions, I feel that I am becoming an Arduino amateur.

    That said, in many STM32s, the clock setup is massive, and getting through it correctly without using the graphic clock configurator is pretty hardcore.

    Explorer
    September 11, 2025

    I see, we are going off on a tangent here ... ;)
    It was the Z80 for me, when it was still state of the art.

    But as a matter of fact that all community members here with a decent experience agree that the ability to read and understand technical manuals (like datasheets & user manuals) is crucial for engineering. Encouraging someone to skip this step is detrimental, I think.

    > That said, in many STM32s, the clock setup is massive, and getting through it correctly without using the graphic clock configurator is pretty hardcore.

    Which is a good use case for dissecting a provided example, even a Cube one.
    The old SPL I continue to use is much less polluted, easier to understand and to strip down. The CMSIS-based core setup was much less polluted by vendor- or OS specific frameworks like Cube.

    I only managed to setup the clock for a C031 manually, which is relatively simple.
    For other STM board I either use old SPL code as template, or NuttX for the newer ones.