Read ADC without reinitializing channel

The official pyboard running MicroPython.
This is the reference design and main target board for MicroPython.
You can buy one at the store.
Target audience: Users with a pyboard.
felix.letkemann
Posts: 10
Joined: Sat Mar 21, 2015 1:29 pm

Read ADC without reinitializing channel

Post by felix.letkemann » Sun Feb 19, 2017 1:34 pm

Hey there! :)
I've got some issues trying to optimize the performance of my ADC with Micropython on my PyBoard Lite. I need two ADC channels to get the voltage of two hall-effect-sensors, which is then used to calculate the angle of a motor.
Since knowledge about the angle is required to define which of the motors coils are to be switched on, it is important to calculate the angle very fast to be able to spin up the motor to high frequencies.
First I simply used .read() to get the value, but I found that to be relatively slow. So I checked out the source code of the micropython firmware and looked for the adc_read funtion. This is when I found out, that the function adc_config_channel is executed every time I do a adc.read() in my python code.
Now I am looking for a way to execute adc_read_channel without reinitializing the channel everytime I call adc.read() to save some time. To do this, I guess I would have to initialize the channel somehow and save the channel to a variable I can pass to the adc_read_channel function.
Unfortunately I could not find anything about this in the documentation. Does anyone know, if this is possible or I would have to hack the original firmware to save some time?
Or do you know if this would not save time at all? Thank you very much in advance!

Yours Sincerely,
Felix

User avatar
deshipu
Posts: 1388
Joined: Thu May 28, 2015 5:54 pm

Re: Read ADC without reinitializing channel

Post by deshipu » Sun Feb 19, 2017 6:32 pm

Now I am looking for a way to execute adc_read_channel without reinitializing the channel everytime I call adc.read() to save some time. To do this, I guess I would have to initialize the channel somehow and save the channel to a variable I can pass to the adc_read_channel function.
Before you do that in MicroPython, I would recommend to test it in C and make sure that initializing the channel is really slower than all the book-keeping with the last state and checking if it changed or not. Chances are that it won't give you such a huge advantage.

felix.letkemann
Posts: 10
Joined: Sat Mar 21, 2015 1:29 pm

Re: Read ADC without reinitializing channel

Post by felix.letkemann » Tue Feb 21, 2017 7:25 pm

Why would you try that out in C before doing it in Python? The whole point of using a pyboard was to be able to use micropython instead of C, since that is a whole lot easier for me.
What do you mean by "book-keeping with the last state and checking if it changed"? The value does alwas change a little bit, doesn't it?

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Read ADC without reinitializing channel

Post by Roberthh » Tue Feb 21, 2017 7:41 pm

Did you determine the time needed for each of these steps:

- init channel
- read the adc value
- the net python function call
- the processing of the data got from the ADC

According to the data sheet, the sampling time goes up to about 16 µs, with faster times for small changes. An empty Micropython function call takes between 3 and 4 µs. So many short functions are worse than a longer single string of statements. Before I would change the firmware, i would first determine where most time is spent.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Read ADC without reinitializing channel

Post by pythoncoder » Wed Feb 22, 2017 9:28 am

An ADC read takes 94us here. When you apply a voltage to a motor winding, current increases relatively slowly (usually and to a first order proportional to the time integral of V/L). It's current which provides the motive force. Are you really sure that in this context 94us is slow?

I'd measure the current waveform before hacking code ;)
Peter Hinch
Index to my micropython libraries.

User avatar
deshipu
Posts: 1388
Joined: Thu May 28, 2015 5:54 pm

Re: Read ADC without reinitializing channel

Post by deshipu » Wed Feb 22, 2017 12:25 pm

felix.letkemann wrote:Why would you try that out in C before doing it in Python? The whole point of using a pyboard was to be able to use micropython instead of C, since that is a whole lot easier for me.
But you are setting out for hacking a C program now. And it's a huge and complex C program -- the micropython interpreter. It will certainly be much less work to write a short C program that validates your theory before learning all about micropython's source code just to make that change, and then learn that it didn't improve your situation.
felix.letkemann wrote: What do you mean by "book-keeping with the last state and checking if it changed"? The value does alwas change a little bit, doesn't it?
I meant the state of the ADC peripheral, in particular its mux, not the value being read.

felix.letkemann
Posts: 10
Joined: Sat Mar 21, 2015 1:29 pm

Re: Read ADC without reinitializing channel

Post by felix.letkemann » Fri Feb 24, 2017 8:08 pm

Hey guys, thank you very much for writing such a lot of answers :)

First, I'd like to clarify what I am planning to do to avoid some confusion
An ADC read takes 94us here. When you apply a voltage to a motor winding, current increases relatively slowly (usually and to a first order proportional to the time integral of V/L). It's current which provides the motive force. Are you really sure that in this context 94us is slow?
I don't want to regulate current using the micropython, instead I am just switching the current on and of based on the position of the motor. The motor is running synchronous and does have four (not 3) coils. This is why I am building a controller myself, since all commercially available controllers are for 3 coils.
The motor does have a position encoder using two hall-effect sensors I can read out using the micropython adc converter. Using the two values, I can calculate the position using these functions:

Code: Select all

@micropython.native
def anglemapping (value: uint, in_min: uint, in_max: uint, angle_min: uint, angle_max: uint) -> uint:
    returnval = ((value - in_min) * (angle_max - angle_min) // (in_max - in_min)) + angle_min
    if returnval > 0:
      return returnval
    return 0

Code: Select all

    channel_a = int(adcA.read())
    channel_c = int(adcC.read())

    channel_b = 600-(channel_a-600)
    channel_d = 600-(channel_c-600)

    angle_last = angle
    if (channel_a > channel_d) and (channel_a > channel_c) and (channel_a > channel_d):
        angle = 360 - int(anglemapping(channel_c, channel_b, channel_a, 0, 90))
    if (channel_b > channel_a) and (channel_b > channel_c) and (channel_b > channel_d):
        angle = 360 - int(anglemapping(channel_d, channel_a, channel_b, 180, 270))
    if (channel_c > channel_d) and (channel_c > channel_a) and (channel_c > channel_b):
        angle = 360 - int(anglemapping(channel_b, channel_d, channel_c, 90, 180))
    if (channel_d > channel_a) and (channel_d > channel_c) and (channel_d > channel_b):
        angle = 360 - int(anglemapping(channel_a, channel_c, channel_d, 270, 360))
The calculation of the angle works quite nice and relatively precise. But there comes the problem: When I want the motor to speed up until it reaches 600 Hertz, that means that I have to switch the coil at least 2400 times per second (4 coils * 600 Hz). That means, that I have to measure the angle at least that often, more would be better. The faster I can calculate the angle, the better I can switch the coil in the right moment. Lets say I need to measure the angle 7200 times per second, that means I do have less that 138 µs per calculation. Since reading adc can take about 100µs this seems to be a hard job.
Please tell me if I don't get that right :)
But you are setting out for hacking a C program now. And it's a huge and complex C program -- the micropython interpreter.
Yes, you're right. I thought it might be easier to add one function without changing big parts of the programm. Maybe my perception about this was a little bit too optimistic.
The best way would be to be able to set the ad converter to a faster mode, which must be possible in theory since read_timed does read the values much faster but just doesn't allow me to do something in between.

Yours Sincerely,
Felix

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Read ADC without reinitializing channel

Post by pythoncoder » Sat Feb 25, 2017 11:09 am

That's a fast motor, so I see your problem. The MicroPython ADC read functions are rather limited, notably the fact that read_time blocks. Also it can't read more than one ADC concurrently which makes phase measurements of fast signals difficult.

One possible solution might be to investigate the stm module which enables direct access to the MCU registers. If you need even more speed there is support for inline assembler. I would look into those approaches before resorting to modifying MicroPython. Note that I haven't investigated stm in the context of ADC's but I have used it to good effect in other roles.

On a more general topic do you actually need the position feedback? In many applications a stepper motor can be driven open loop. The main reason for closed loop control is in cases where the mechanical load (inertial and frictional) is unknown at runtime. If it is known and constant you can usually ramp up the speed at a predetermined rate. But you probably already know this ;)
Peter Hinch
Index to my micropython libraries.

felix.letkemann
Posts: 10
Joined: Sat Mar 21, 2015 1:29 pm

Re: Read ADC without reinitializing channel

Post by felix.letkemann » Fri Mar 24, 2017 2:48 pm

Thank you guys for your fast answers! :)

I am currently trying to hack the micropython firmware in order to implement a function in C that allows me to directly get the angle if the rotor. The base for doing this are all the other examples I find in the file stmhal/adc.c, such as the read battery function.
What is find confusing: I have not been able to figure out how the pins are mapped to the analog digital converters. In my python programm I am using the pins X19, X20, X21 and X22 (shielded adc) but there are only 18 ADC channels defined in stmhal/hal/l4/inc/stm32l4xx_hal_adc_ex.h. So it was wrong to assume that the number of each pin correlates to the number of the adc channel. Unfortunately I could not find out which channels I am currently using in my software. Do you have any idea how to find out anything about the mapping?

Yours Sincerely,
Felix

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: Read ADC without reinitializing channel

Post by dhylands » Fri Mar 24, 2017 4:13 pm

Hi Felix,

The mapping of the ADC to the pins is documented in the stm32 datasheet. This is then reflected in the stm32f405_af.csv file. If you look at this: https://github.com/micropython/micropyt ... 405_af.csv and scroll over to the very right (you'll probably need to scroll to the bottom first) you'll see an ADC column. The ADC column will have something like: ADC123_IN14 in it. That means that pin is adc channel 14 on ADC units 1, 2, and 3.

This then gets intersected with pins.csv and when the build happens it generates a pins_PYBV11.c (The PYBV11 portion of the filename will match the board name, and the file will be found in the build directory, for example stmhal/build-PYBV11). If you scroll to the end of that file then you'll see a table which looks something like this:

Code: Select all

const pin_obj_t * const pin_adc1[] = {
  &pin_A0, // 0
  &pin_A1, // 1
  &pin_A2, // 2
  &pin_A3, // 3
  &pin_A4, // 4
  &pin_A5, // 5
  &pin_A6, // 6
  &pin_A7, // 7
  &pin_B0, // 8
  &pin_B1, // 9
  &pin_C0, // 10
  &pin_C1, // 11
  &pin_C2, // 12
  &pin_C3, // 13
  &pin_C4, // 14
  &pin_C5, // 15
#if defined(MCU_SERIES_L4)
  NULL,    // 16
#endif
};
That's the mapping of ADC channel to pin numbers that are exposed on your board. Just before that you'll find a table for pin_board_pins_locals_dict_table:

Code: Select all

STATIC const mp_map_elem_t pin_board_pins_locals_dict_table[] = {
  { MP_OBJ_NEW_QSTR(MP_QSTR_X1), (mp_obj_t)&pin_A0 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X2), (mp_obj_t)&pin_A1 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X3), (mp_obj_t)&pin_A2 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X4), (mp_obj_t)&pin_A3 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X5), (mp_obj_t)&pin_A4 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X6), (mp_obj_t)&pin_A5 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X7), (mp_obj_t)&pin_A6 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X8), (mp_obj_t)&pin_A7 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X9), (mp_obj_t)&pin_B6 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X10), (mp_obj_t)&pin_B7 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X11), (mp_obj_t)&pin_C4 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X12), (mp_obj_t)&pin_C5 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X17), (mp_obj_t)&pin_B3 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X18), (mp_obj_t)&pin_C13 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X19), (mp_obj_t)&pin_C0 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X20), (mp_obj_t)&pin_C1 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X21), (mp_obj_t)&pin_C2 },
  { MP_OBJ_NEW_QSTR(MP_QSTR_X22), (mp_obj_t)&pin_C3 },
  ...
This is the mapping of the board pin names (X1, X2, etc) to the cpu pins.

Post Reply