← index #12115Issue #12048
Related · high · value 1.458
QUERY · ISSUE

STM32 machine.ADC channel constants overflowing MP_ROM_INT() 31 bit signed type

openby renestraubopened 2023-07-29updated 2023-10-23
bugport-stm32

When trying to add the STM32H5 core voltage input, I found that the constants for internal channels don't work as expected.

TLDR

  • MP_ROM_INT() handles only signed 31 bit integers
  • STM32 channel constants are using full unsigned 32 bit range making them unusable for MP_ROM_INT().
  • Channel numbers taken from machine_adc_locals_dict_table work by accident if they have bits 31 and 30 set.
  • Channel numbers get corrupted when bit 31 is 0, bit 30 is 1

Detailed description

machine_adc.c defines constants for internal channels by means of STM32 driver #defines. See lib/stm32lib/STM32H5xx_HAL_Driver/Inc/stm32h5xx_hal_adc.h. Those can be used when creating a machine.ADC() object, i.e. adc=ADC(ADC.CORE_TEMP)

STATIC const mp_rom_map_elem_t machine_adc_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_read_u16), MP_ROM_PTR(&machine_adc_read_u16_obj) },

    { MP_ROM_QSTR(MP_QSTR_VREF), MP_ROM_INT(ADC_CHANNEL_VREF) },
    { MP_ROM_QSTR(MP_QSTR_CORE_VREF), MP_ROM_INT(ADC_CHANNEL_VREFINT) },
    #if defined(STM32G4)
    { MP_ROM_QSTR(MP_QSTR_CORE_TEMP), MP_ROM_INT(ADC_CHANNEL_TEMPSENSOR_ADC1) },
    #else
    { MP_ROM_QSTR(MP_QSTR_CORE_TEMP), MP_ROM_INT(ADC_CHANNEL_TEMPSENSOR) },
    #endif
    #if defined(ADC_CHANNEL_VBAT)
    { MP_ROM_QSTR(MP_QSTR_CORE_VBAT), MP_ROM_INT(ADC_CHANNEL_VBAT) },
    #endif
    #if defined(ADC_CHANNEL_VDDCORE)
    { MP_ROM_QSTR(MP_QSTR_CORE_VDD), MP_ROM_INT(ADC_CHANNEL_VDDCORE) },
    #endif
};
STATIC MP_DEFINE_CONST_DICT(machine_adc_locals_dict, machine_adc_locals_dict_table);

Unfortunately the channel constants are uint32_t types that don't map to MP_ROM_INT() 31 bit signed integers. The following shows the literal values of the STM32 driver.

ADC_CHANNEL_VREFINT 0xc7520000
ADC_CHANNEL_TEMPSENSOR 0xc3210000
ADC_CHANNEL_VBAT 0x43290000
ADC_CHANNEL_VDDCORE 0x475a0000

When instantiating a machine.ADC object with one of the constants from machine_adc_locals_dict_table, the value gets changed due to shifting and sign extension in the SMALL_INT macros.

#define MP_OBJ_SMALL_INT_VALUE(o) (((mp_int_t)(o)) >> 1)
#define MP_OBJ_NEW_SMALL_INT(small_int) ((mp_obj_t)((((mp_uint_t)(small_int)) << 1) | 1))

The relevant code sequence where the incorrect constants are loaded is mp_obj_get_int() in the constructor.

// ADC(id)
STATIC mp_obj_t machine_adc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
    // Check number of arguments
    mp_arg_check_num(n_args, n_kw, 1, 1, false);

    mp_obj_t source = all_args[0];

    uint32_t channel;
    uint32_t sample_time = ADC_SAMPLETIME_DEFAULT;
    ADC_TypeDef *adc;
    if (mp_obj_is_int(source)) {
        #if defined(STM32WL)
        adc = ADC;
        #else
        adc = ADC1;
        #endif
        channel = mp_obj_get_int(source);
        ...

To make the problem more clear, I modified machine_adc_print() to display the channel number in hex notation.

STATIC void machine_adc_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
    machine_adc_obj_t *self = MP_OBJ_TO_PTR(self_in);
    unsigned adc_id = 1;
    #if defined(ADC2)
    if (self->adc == ADC2) {
        adc_id = 2;
    }
    #endif
    #if defined(ADC3)
    if (self->adc == ADC3) {
        adc_id = 3;
    }
    #endif

    mp_printf(print, "<ADC%u channel=%u (%x)>", adc_id, self->channel, self->channel);
}

Test script

# Test Code
from machine import ADC


# Expect 0xc7520000 --> Ok, by accident
a=ADC(ADC.CORE_VREF)
a
<ADC1 channel=17 (c7520000)>

# Expect 0xc3210000 --> Ok, by accident
a=ADC(ADC.CORE_TEMP)
a
<ADC1 channel=16 (c3210000)>

# Expect 0x43290000 --> Fail, constant gets signed extended when getting with MP_OBJ_SMALL_INT_VALUE()
# Results in getting MSB set. 
a=ADC(ADC.CORE_VBAT)
a
<ADC1 channel=16 (**c3290000**)>

# Expect 0x475a0000 --> Fail, constant gets signed extended when getting with MP_OBJ_SMALL_INT_VALUE()
a=ADC(ADC.CORE_VDD)
a
<ADC2 channel=17 (**c75a0000**)>

In my opinion the code that uses the uint32_t constants works only by accident. Values starting with 0xC....... have their MSB truncated in MP_ROM_INT -> MP_OBJ_NEW_SMALL_INT(). Since bit 30 is 1, during arithmetic right shift in MP_OBJ_SMALL_INT_VALUE() bit 31 gets accidentally restored.

The two values starting with 0x4....... get converted by MP_OBJ_NEW_SMALL_INT() to 0x8...... | 1, which makes them negative numbers. When reading with MP_OBJ_SMALL_INT_VALUE the result has the MSB incorrectly set (arithmetic shift right).

All in all the use of 32 bit unsigned integers makes lot of the code work by pure accident.
MP_ROM_INT() doesn't work with STM32 (at least H5) ADC channel literals.

Version Info

Custom Build

MicroPython v1.20.0-283-g7d66ae603-dirty on 2023-07-29; STM32H573I-DK with STM32H573IIK3Q
Type "help()" for more information.

* 7d66ae603 - (2 weeks ago) esp32/machine_timer: Switch from legacy driver to timer HAL. - Damien George (HEAD -> master, origin/master, origin/HEAD)
* 671b35cea - (4 weeks ago) py/builtinimport: Fix built-in imports when external import is disabled. - Jim Mussared
CANDIDATE · ISSUE

STM32H5 machine.ADC core temperature sensor not working

openby renestraubopened 2023-07-20updated 2023-10-23
bugport-stm32

Description

While testing with an STM32H5 Nucleo board I noticed the internal ADC channels are not working as expected. External (I/O) pins return expected values. Internal ADC input are not reporting reasonable values.

I understand the STM32H5 port is under work and some features not working. I hope my input helps to fix a problem. Let me know if I can help with more testing.

Problem description:

  • ADC(ADC.CORE_TEMP) returns incorrect values.
>>> from machine import ADC
>>> coretemp=ADC(ADC.CORE_TEMP)

Expected result:

  • Values should lie around 30°C calibration point, or slightly higher.
>>> from machine import mem16
>>> mem16[0x08FFF814]
760
>>> mem16[0x08FFF818]
1014

Observed result:

  • ADC value incorrect, indicates wrong core temperature
  • Value is far below 30°C calibration point
>>> coretemp.read_u16() >> 4
173

See detailed log below.

The issue could be in machine_adc.c, adc_config_channel(). ADC object shows channel is an encoded number containing additional information besides the ADC channel number. For ADC.CORE_TEMP channel is 0xc3210000. Using this channel number value leads to incorrect loading of the SMPR register.

Note: The same kind of problem will be present for ADC.CORE_VREF, ADC.CORE_VBAT.

>>> coretemp=ADC(ADC.CORE_TEMP)
>>> coretemp
<ADC1 channel=3273719808>
>>> hex(3273719808)
'0xc3210000'

For G4 cores the channel number is extracted via __LL_ADC_CHANNEL_TO_DECIMAL_NB. Maybe this is required for H5 cores as well.

STATIC void adc_config_channel(ADC_TypeDef *adc, uint32_t channel, uint32_t sample_time) {
    [removed]

    #if defined(STM32G4)
    channel = __LL_ADC_CHANNEL_TO_DECIMAL_NB(channel);
    adc->DIFSEL &= ~(1 << channel); // Set channel to Single-ended.
    #endif
    adc->SQR1 = (channel & 0x1f) << ADC_SQR1_SQ1_Pos | (1 - 1) << ADC_SQR1_L_Pos;
    __IO uint32_t *smpr;
    if (channel <= 9) {
        smpr = &adc->SMPR1;
    } else {
        smpr = &adc->SMPR2;
        channel -= 10;
    }
    *smpr = (*smpr & ~(7 << (channel * 3))) | sample_time << (channel * 3); // select sample time

    #endif
}

Version Information

% git lg1
* 7d66ae603 - (9 days ago) esp32/machine_timer: Switch from legacy driver to timer HAL. - Damien George (HEAD -> master, origin/master, origin/HEAD)
* 671b35cea - (2 weeks ago) py/builtinimport: Fix built-in imports when external import is disabled. - Jim Mussared
* 606ec9bfb - (9 weeks ago) py/compile: Fix async for's stack handling of iterator expression. - Damien George

MicroPython v1.20.0-282-g671b35cea-dirty on 2023-07-14; STM32H573I-DK with STM32H573IIK3Q
Type "help()" for more information.

Log

>>> from machine import ADC

### Working external inputs, PF12 taken as example
>>> adcext=ADC(machine.Pin('F12'))
>>> adcext
<ADC1 channel=6>

# Tied to 3.3 V
>>> adcext.read_u16()
65471
>>> adcext.read_u16()
65519
>>> adcext.read_u16()
65503
>>> adcext.read_u16()
65519

# Tied to GND
>>> adcext.read_u16()
48
>>> adcext.read_u16()
0
>>> adcext.read_u16()
16

### Test with core temperature

>>> coretemp=ADC(ADC.CORE_TEMP)
>>> coretemp
<ADC1 channel=3273719808>
>>> hex(3273719808)
'0xc3210000'

# Check low (30°C) and high (130°C) calibration points
>>> from machine import mem16
>>> mem16[0x08FFF814]
760
>>> mem16[0x08FFF818]
1014

# Read sensor, should be between two calibration points, actually close to 30° value
# Right align 12 bit ADC value

>>> coretemp.read_u16() >> 4
173
>>> coretemp.read_u16() >> 4
176
>>> coretemp.read_u16() >> 4
178
>>> coretemp.read_u16() >> 4

# Computes to a core temperature of -199 °C...

>>> (178-760)/((1014-760)/(130-30))+30
-199.1339

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied