← index #12115PR #12152
Likely Duplicate · high · value 4.742
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 · PULL REQUEST

stm32/machine_adc: Add STM32H5 support.

mergedby renestraubopened 2023-08-03updated 2023-09-27
port-stm32

Description

Add STM32H5 ADC support to machine.ADC.

Changes are:

  • Run ADC on PCLK/16
  • Verify and optimize timings (ADC_STAB_DELAY_US, ADC_SAMPLETIME_DEFAULT)
  • Add support for STM32H5 VBAT and COREVDD channels on ADC2.
  • Replace ADC constants in machine_adc_locals_dict_table . Reasoning: The STM32 driver literals are uint32_t that don't work with MP_ROM_INT() which handles signed 31 bit integers only. Introduce enumerator machine_adc_internal_ch_t to define external channels (0..19), internal channels (256..) and the special channel VREF (0xffff). Values are converted to STM32 literals with adc_ll_channel() when required in adc_config_and_read_u16().
  • Convert STM32 literal to channel numbers in adc_config_channel with corresponding STM32 LL library functions (__LL_ADC_IS_CHANNEL_INTERNAL(), __LL_ADC_CHANNEL_TO_DECIMAL_NB())

Thanks to @yn386 for discussions.

Environment

  • NUCLEO-H563ZI
  • Using STM32H573I-DK board with minor modifications (input clock, on-chip flash)
  • First ADC Input (PF12) connected externally to GPIO PF14
MicroPython v1.20.0-283-g7d66ae603-dirty on 2023-08-01; STM32H573I-DK with STM32H573IIK3Q

Tests

from machine import ADC, Pin
from machine import mem16

io = Pin('F14', Pin.OUT_PP)     # Connect to PF12
io.low()

ts_cal_30 = mem16[0x08FFF814]
ts_cal_130 = mem16[0x08FFF818]
scale = (ts_cal_130 - ts_cal_30)/(130-30)
print(f"Calibration values: 30°C = {ts_cal_30}, 130°C = {ts_cal_130}")

adc_pf12 = ADC(Pin('F12'))
print(adc_pf12)
<ADC1 channel=6 sampletime=1>

adc_vref = ADC(ADC.VREF)
print(adc_vref)
<ADC1 channel=65535 sampletime=1>

adc_vrefint = ADC(ADC.CORE_VREF)
print(adc_vrefint)
<ADC1 channel=256 sampletime=6>

adc_temp = ADC(ADC.CORE_TEMP)
print(adc_temp)
<ADC1 channel=257 sampletime=6>

adc_vbat = ADC(ADC.CORE_VBAT)
print(adc_vbat)
<ADC2 channel=258 sampletime=6>

adc_corevdd = ADC(ADC.CORE_VDD)
print(adc_corevdd)
<ADC2 channel=259 sampletime=6>


# Expect: 3.30 V (fixed value)
adc_vref.read_u16() * 3.3 / 65535
3.3

# Expect: 1.21 V
adc_vrefint.read_u16() * 3.3 / 65535
1.210378

# Expect: 25 to 35 °C
val = adc_temp.read_u16() >> 4
((val - ts_cal_30) / scale)+30
31.5748

# Expect: 3.30V
adc_vbat.read_u16() * 3.3 / 65535 * 4
3.326638

# Expect: 1.30 to 1.40 V
adc_corevdd.read_u16() * 3.3 / 65535
1.360284

# Expect 0.0
io.low()
adc_pf12.read_u16() * 3.3 / 65535
0.002417029

# Expect 3.3
io.high()
adc_pf12.read_u16() * 3.3 / 65535
3.296777

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