Using Analog Input to control motors

I am trying to use multiple ODrives to control a cable robot. The four boards will be fed by a Quanser QPIDe Data Acquisition Device. The Quanser can only give out simple analog outputs. So, my question is multi-faceted.

  1. is it even possible to control both motors of an Odrive with only analog inputs? if so, in what control mode?
  2. how would you test such a system? would it be okay if I just attached a basic circuit (with variable resistance) to the analog input pins and then tested the system using differing resistances? or do I need something that can input a variable voltage?

It seemed like there was another with a similar issue, but his question hasn’t been answered: Analog Velocity Control not working for me

I don’t know enough about the ODrive to know where to begin, so I am asking here before I start trying to edit firmware (which I assume is necessary to get a system like this working).

@Riewert, I remember your project was using analog inputs from load cells. I was wondering if you can give some advice to neelpuri.

Thanks for summoning me. From my experience:

  1. Yes that is perfectly possible. The best way is to make some changes to the firmware in Communication, add a make_protocol_function() or make_protocol_property(). From what I understand it is also possible to use the odrv0.config.gpio#_pwm_mapping() functions, but I don’t have experience using them.

  2. You can read the GPIO values from your PC, using stock firmware, and write a control loop in python to test your control solution without modifying the firmware. Should work just fine. Just call odrv0.get_adc_voltage(#).

Lastly; be careful you don’t accidentaly fry any of your GPIO ports. Remember they are 0 - 3.2V (4096 bits). I think they are 5V tolerant, not sure, but the readout will not increase above 3.2V.

Thanks so much for your help. I, however, have no idea what you are talking about with your response to the first part of the question. I don’t know what it means to “add a make_protocol_function() or make_protocol_property()”.

After reading your comment @Riewert, this is what I was able to do:

odrv0.config.gpio3_analog_mapping.max = 5000

odrv0.config.gpio3_analog_mapping.min = -5000

odrv0.config.gpio3_analog_mapping.endpoint = odrv0.axis0.controller._remote_attributes['pos_setpoint']



But I guess I shouldn’t have done that before I knew how the functions worked. Nothing seems to have broken, but I can’t control the motor with the voltage input through GPIO3. Right now I just have the ability to vary the voltage input to GPIO3 between 3.3 and 0 volts. But changing the input does literally nothing other than change what I get when I run odrv0.get_adc_voltage(3).

I was able to write a python script that reads the value input to GPIO3 and control the motors through that. This, however, requires that I have a computer connected to each ODrive and have the script running in the background. This makes my processing very slow. The robot I am building needs to input information from the payload to the Quanser, have the computer compute the changes needed and then have the Quanser send commands to the ODrives. This is made significantly slower and more difficult if the output has to go from the computer to the Quanser and then back to the computer to be decoded.

I have tried to read through the firmware documentation but I don’t have nearly enough experience to understand what is going on in there. Any help as to what changes I have to do and where would be amazing. Thank you so much for any assistance you can provide.

No problem, I understand what you are going through.

  1. I have no idea how odrv0.config.gpio3_analog_mapping.endpoint works, but from what your saying, maybe try making a mapping for gpio2 and gpio4. I noticed that the numbering of the GPIOs is not always consistent. I don’t know of anybody who has actually used the mapping functions… @Wetmelon Also, googleing led me to:

  2. Get the firmware to compile on your computer. When that works, go to and change the function on line 112. The function is called by line 175, and is visible from your python code under odrv0.test_function().

I am trying to achieve something similar, so maybe the function I am writing helps as a reference (though I haven’t tested it yet). Odrive and Java

I guess the ultimate objective to get a control loop running on the ODrive, but I’m not sure what the best place to do that is.


It seems like the analog_mapping function is not working for me and doesn’t seem super well documented. So I doubt I will ever get it properly working. So, editing the firmware it is.

A slight issue, however, is that I don’t know how to compile the firmware (I actually don’t even know what that really means). I tried to go through the setup guide for windows on the documentation page here. But I don’t know if I downloaded the right materials in the right places because I don’t know how to use them in the first place.

I was able to download the firmware master and I can edit the code. But I am having issues with flashing the experimental firmware to the board. Whenever I’ve tried to flash the new code I bricked the board and had to completely reflash the master from here. Essentially I am asking if I have to compile the whole master (with my updates) into a single .hex file and then flash that? or if it is possible to flash a single .cpp file as an update to the old firmware? I can’t seem to find an answer online because I don’t know really anything about embedded systems.

Once again, thank you so much for all the help you have already given. You probably don’t know how much of godsend this is.

Yes, you need to compile the whole .hex file (even for the smallest changes). The easiest way to do that for me was through Visual Studio Code. How far are you getting with the compilation? Be sure to create a copy of tup.config for you board based on the tup.config.default file.

Also, how are you tranfering your code to the Odrive? Have you tried using the DFU mode over USB, by running odrivetool dfu custom_firmware.hex from the folder where the .hex file is? That works quite reliably for me.

I am currently in the process of working out how to compile the firmware in VSCode. But I ran into an issue that I can’t seem to surpass. I made a post about it here.

I think I correctly copied over the tup.config.default file and made it into a tup.config file correctly.

For flashing the firmware I was trying to use odrivetool dfu (file path) but that resulted in me bricking the board. The tool erased the old firmware and then crashed immediately afterward, resulting in a board with nothing on it. That meant that I couldn’t even run odrivetool dfu to put the firmware back on the board. So, I used the Upgrading firmware with a different DFU tool instructions to restore the firmware. I am more confident in my ability to flash the firmware than to edit it.

The main holdup right now is simply getting the firmware to compile into a flashable hex file. Which I am, slowly, making progress on.

1 Like

Hi all.
Has there been any update to the analogue input use case since this thread in 2019?
I normally wouldn’t use an analogue input, but for my eBike conversion, it makes sense.
I have a Hall effect thumb throttle connected to GPIO5 (PC4 on the STM32). Is there any easy way to read that and map it to axis1.controller.input_current, or will I need to hack the firmware?


To answer my own question:

Using analogue input on GPIO3 or GPIO4 is easy:

odrv0.config.gpio3.max = 10

This sets the analog pin up to command between 0 and 10 amps. In my case I had to increase ‘min’ a bit to account for some offset in the thumb throttle.

However, I am using GPIO5, which doesn’t exist. To add it, I simply had to edit Firmware/odrive_interface.yaml (this seems to be a rather nice new feature in the Devel branch, but it has a couple of extra Python dependencies on build)

I added GPIO5 here and recompiled:

          gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[2]'}
          gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]'}
          gpio5_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[4]'}

Now I can configure GPIO5 as an analogue pin like the rest.

1 Like

Well, I can confirm that ODrive works in an e-bike setting.

There’s a bit of noise induced onto the analogue input when the motor is running faster than a certain speed, and that can cause it to run indefinitely until I stop it with the manual brake.
I could probably fix that with a resistor to GND, since this is a Hall effect throttle, it can probably source enough current for a 10k pulldown without affecting its output value.
Oddly, putting it in INPUT_MODE_TORQUE_RAMP seems to make the problem worse.

Also, I’ve somehow managed to blow one of the 20A automotive fuses that I put in line with the motor phases, even though I have set the analogue mapping max to 10 (which produces a demand of 8A when at full throttle due to the offset/scaling of the throttle). Not sure how that could happen, unless the current controller is marginally stable, which wouldn’t surprise me. meas_l is 65400 (uH?) and I’m currently running from 19V.
I didn’t trip the 10A breaker which I fitted to the input to be on the safe side.

However, I’d now like to have a digital input control whether the bike applies forward or reverse thrust. This will be connected to the rear brake lever.
I’ll need to hack the firmware anyway, because I want that reverse thrust only to apply when the bike is moving above a certain positive speed. I don’t want a reverse gear! Also, I’d like it only to go into reverse “mode” when the throttle is zero, so I don’t get any sudden changes. (same applies to going forwards again)
But, not sure how best to define a digital input. Not sure that it’s possible in the odrive_interface.yaml file. I’d like it to map to any boolean or real-valued property in the same way the analogue input does.

1 Like

I’m surprised you’re doing this directly. I’d be inclined to wire everything to a Teensy or similar and then send CAN torque commands to the ODrive.

I’m surprised, too! :joy:
But then it really is just another thing to go wrong. I have access to the source code of this wonderful open-source motor controller, so why not? :slight_smile:

fyi, here’s the code I had to change to make it work.
I added input offset & scaling support to the analogue (and PWM) inputs also.

Not tested the changes to PWM inputs though.

index 0c423346..2d1c5c06 100644
--- a/Firmware/MotorControl/low_level.cpp
+++ b/Firmware/MotorControl/low_level.cpp
@@ -737,13 +737,17 @@ void handle_pulse(int gpio_num, uint32_t high_time) {
     if (high_time < PWM_MIN_LEGAL_HIGH_TIME || high_time > PWM_MAX_LEGAL_HIGH_TIME)
-    if (high_time < PWM_MIN_HIGH_TIME)
-        high_time = PWM_MIN_HIGH_TIME;
-    if (high_time > PWM_MAX_HIGH_TIME)
-        high_time = PWM_MAX_HIGH_TIME;
-    float fraction = (float)(high_time - PWM_MIN_HIGH_TIME) / (float)(PWM_MAX_HIGH_TIME - PWM_MIN_HIGH_TIME);
+    float max_high_time = float(PWM_MAX_HIGH_TIME) * odrv.config_.pwm_mappings[gpio_num - 1].in_max;
+    float min_high_time = float(PWM_MIN_HIGH_TIME) / (1-odrv.config_.pwm_mappings[gpio_num - 1].in_min);
+    float fraction = (float)(high_time - min_high_time) / (float)(max_high_time - min_high_time);
     float value = odrv.config_.pwm_mappings[gpio_num - 1].min +
                   (fraction * (odrv.config_.pwm_mappings[gpio_num - 1].max - odrv.config_.pwm_mappings[gpio_num - 1].min));
+   if (value < odrv.config_.pwm_mappings[gpio_num - 1].min)
+        value = odrv.config_.pwm_mappings[gpio_num - 1].min;
+    if (value > odrv.config_.pwm_mappings[gpio_num - 1].max)
+        value = odrv.config_.pwm_mappings[gpio_num - 1].max;
     fibre::set_endpoint_from_float(odrv.config_.pwm_mappings[gpio_num - 1].endpoint, value);
@@ -774,8 +778,14 @@ void pwm_in_cb(int channel, uint32_t timestamp) {
 static void update_analog_endpoint(const struct PWMMapping_t *map, int gpio)
-    float fraction = get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)) / 3.3f;
+    float fraction = (get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)) - map->in_min) / (map->in_max - map->in_min);
     float value = map->min + (fraction * (map->max - map->min));
+    //Clamp
+    if(value < map->min)
+       value = map->min;
+    if(value > map->max)
+       value = map->max;
     fibre::set_endpoint_from_float(map->endpoint, value);
diff --git a/Firmware/MotorControl/odrive_main.h b/Firmware/MotorControl/odrive_main.h
index b112a19d..4756eb2c 100644
--- a/Firmware/MotorControl/odrive_main.h
+++ b/Firmware/MotorControl/odrive_main.h
@@ -79,7 +79,10 @@ typedef struct {
 struct PWMMapping_t {
     endpoint_ref_t endpoint;
     float min = 0;
-    float max = 0;
+    float max = 1;
+    float in_min = 0;
+    float in_max = 1;
 // @brief general user configurable board configuration
diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml
index 79f0c1e8..9b2d6d4d 100644
--- a/Firmware/odrive-interface.yaml
+++ b/Firmware/odrive-interface.yaml
@@ -181,6 +181,7 @@ interfaces:
           gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]'}
           gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[2]'}
           gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]'}
+          gpio5_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[4]'}
       user_config_loaded: readonly bool
       axis0: {type: Axis, c_name: get_axis(0)}
@@ -215,8 +216,11 @@ interfaces:
     c_is_class: False
       endpoint: endpoint_ref
+      in_min: float32
+      in_max: float32
       min: float32
       max: float32

Not got general purpose digital inputs & outputs working yet though

I’m working with the current devel version, and just wanted to let ppl know that the current syntax to set the endpoint is:

odrv0.config.gpio3_analog_mapping.endpoint = odrv0.axis0.controller._input_pos_property

Works great btw.