RC ESC based Brushed DC motor closed loop control

Hey everyone, thanks for this great library, and I look forward to contributing.
I am working with some brushless and brushed DC motors and would like to leverage the SimpleFOC ecosystem to control both and integrate with ROS. Right now, my issue relates to the closed loop brushed DC motor control using the Simple FOC DC motor library (GitHub - simplefoc/Arduino-FOC-dcmotor: Control DC motors using SimpleFOC's infrastructure). I want to use it to control a brushed motor through an RC ESC’s PWM from my microcontroller. I have a rotary quadrature encoder and an AS5047P magnetic encoder to use, but I would first like to test in open-loop mode. The library references this kind of setup here (Arduino-FOC-dcmotor/src/drivers at main · simplefoc/Arduino-FOC-dcmotor · GitHub), with the DCDriver1PWM driver to control both speed and direction, which is common for Servos and RC ESCs. However, I am having difficulties setting this up. My ESC operates at 50Hz PWM frequency with 1000 microseconds being full reverse and 2000 microseconds signaling full forward throttle with 1500 microseconds being the neutral point (called threshold) in the library. The input voltage to the ESC/motor is about 6 volts. I should also note that I am able to control the motors using the standard Arduino IDE Servo library (e.g. using Servo.write(0-180 degrees) or Servo.writeMicroseconds(1000-2000)). I have a Raspberry Pi Pico, Teensy 4.1, and Arduino Mega 2560 for testing. My questions are as follows:

  1. Is it possible to use the library/Simple FOC with my setup?
  2. If so, how can I set this up as the documentation does not specify units, e.g. for threshold, setPwm function, etc?
  3. How does the library write to the DCDriver1PWM driver? Using <Servo.h> or analogWrite?
  4. What are the expected values for the setPwm function?

If all else works, then I will try the other control methods. Thanks for your help.

I’ll copy here what I wrote in discord :slight_smile:

Hi @privvyledge , welcome to SimpleFOC!

Firstly, what you want to do with the brushed DC motor should be quite possible. I made a test setup using only very cheap components (DC motor and magnetic sensor), and was able to regulate the position of the motor as if it were a servo. Velocity control via 1-PWM should be possible also.

However, I should give you fair warning: our SimpleDC library is very new, so in a way you would be helping us to test it… :grimacing: so if you’re expecting a fully ready and tested piece of software here, I have to disappoint. That’s for the DC-motors part. For the brushless motors the SimpleFOC library has been around a while and is well tested.

So this also means the documentation of the DCDriver library is not yet very detailed.

The DCDriver1PWM is intended to work a bit like a RC servo controller, but I have not tested it yet, and therefore I may not have considered all the needs… lets see if it can work for you.

The setPwm() works like this: the idea is that the value represents a voltage, based on the driver.voltage_power_supply

So if your driver.voltage_power_supply is 10V, and you call setPwm(5.0); then the resulting PWM duty cycle should be 50%.

When controlling this type of servo with 50Hz, you have 20000us period time. If it works with 1000 - 1500 - 2000us timings, that corresponds to 5%, 7.5% and 10% of 20000us, so translating that into voltages for setPwm() it means you would operate the DCDriver1PWM using voltages between 9V and 9.5V, with the middle point at 9.25V.

That’s assuming a 10V power supply. More generally, you would choose the setpoints as:

float full_rev = driver.voltage_power_supply * 0.95; // 1000us/20000us = 5%
float full_fwd = driver.voltage_power_supply * 0.9; // 2000us/20000us = 10%
float zero_point = driver.voltage_power_supply * 0.925; // 20000us/1500us = 7.5%

(this is assuming the servo has active-low polarity) (edited)

How does the library write to the DCDriver1PWM driver? Using <Servo.h> or analogWrite?

No, neither, you just use driver.setPwm(); So you would configure and init the driver in your setup() routine, and use it via driver.setPwm(some_voltage); in the rest of the code.

You can also look at using our DCMotor class, but I am not sure that it can deal with the PWM range you need at the moment. I think it will use 0-10V on the driver1PWM which is not what you need.

#define PIN_PWM 1
#define PIN_EN 5
#define THESHOLD 0.925f

DCDriver driver = DCDriver1PWM(PIN_PWM, THESHOLD, PIN_EN);

void setup() {
  driver.voltage_power_supply = 10.0f;
  driver.init();
}

void loop() {
   ...
    driver.setPwm(9.43);
   ...
}

I’d be very happy to help you get this working, as it would help us a lot to get feedback and test results for the DC motor library :slight_smile:

Thank you very much for the quick response. I understand that the DC motor class is new and I would need to test it so it can be improved. In fact, that’s the reason I am trying to use this library as opposed to other methods to perform closed-loop brushed motor control. I also have a few other types of brushed motor drivers that I will use to test the other drivers in the simple DC motor library. While I am waiting for a 2S battery to use with the ESC, I am using an RC servo motor to test since it uses the same type of command interface I need. For some more context, it’s a Traxxas 4-Tec 2.0 chassis with the XL-5 waterproof ESC connected to the Titan 12-turn 550 modified motor and the 2075 Traxxas servo.

Here’s the code:

#include <Arduino.h>
#include <SimpleFOC.h>
// #include "SimpleFOCDrivers.h"
#include "SimpleDCMotor.h"

#define PIN_PWM 9
#define THRESHOLD 0.925f  // range: [0, 1].
#define MAX_DUTY 0.9f  // range: [0, 1]. Maybe 0.05f
#define MIN_DUTY 0.95f  // range: [0, 1]. Maybe 0.1f
#define NEUTRAL_DUTY 0.925f  // range: [0, 1]. Maybe 0.075
#define SUPPLY_VOLTAGE 6.0f  // volts

// DCMotor motor = DCMotor();

DCDriver1PWM driver = DCDriver1PWM(PIN_PWM, THRESHOLD);

const float minSteering = SUPPLY_VOLTAGE * MAX_DUTY;  // microseconds: 1000
const float maxSteering = SUPPLY_VOLTAGE * MIN_DUTY;  // microseconds: 2000
const float neutralSteering = SUPPLY_VOLTAGE * NEUTRAL_DUTY;  // microseconds: 1500

// Setup analog joystick
const int joystick_switch_pin = 15;  // digital pin connected to switch output
const int joystick_Rx_pin = A1; // analog pin connected to X output
const int joystick_Ry_pin = A0; // analog pin connected to Y output

int throttle_value = neutralSteering; // throttle input
int steering_value = neutralSteering; // steering input
int switch_value;  // josytick pin

int rx_min = 7;
int rx_max = 1023;
int ry_min = 6;
int ry_max = 1023;

float fmap(float toMap, float in_min, float in_max, float out_min, float out_max) {
  return (toMap - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void setup() {
  // basic driver setup - set power supply voltage
  driver.voltage_power_supply = SUPPLY_VOLTAGE;
  // driver.pwm_frequency = 50;
  // initialize driver
  driver.init();
}

void loop() {
  steering_value = analogRead(joystick_Ry_pin);  // steering input
  steering_value = constrain(steering_value, ry_min, ry_max);
  steering_value = fmap(steering_value, ry_min, ry_max, minSteering, maxSteering);
  // driver.setPwm(steering_value); // 3.0f. 3 Volts
  driver.setPwm(neutralSteering); // 3.0f. 3 Volts
  delay(50);
}

  1. The servo power supply in this case is 6.0 volts.
  2. When uploaded to my Raspberry Pi Pico, the steering just turns full left and maintains it, i.e minSteering (which would be full reverse for the main DC motor) but does not move no matter what the value of driver.setPwm() is.

What could be wrong with this code?
P.S. I have already tested and controlled both at some point using servo.write() and servo.writeMicroseconds(), but from what I understand, they might not work with regular PWM duty cycle signals, which is why the Servo library works, but analogWrite will not work in this case. That’s why I want to know how the driver.setPwm() is implemented.

Hey,

this is definately going in the right direction. Regarding your code:

steering_value is an int - but the return value of your fmap() function is a float. I think you need to use a float variable to transport fmap return value to the setPwm() function. Otherwise you can only set voltages like 3.0V, 4.0V, 2.0V and this will not be precise enough.

In terms of the timings, a 50Hz 1000 - 1500 - 2000 servo will have a period time of 20000us, and 1000us = 5% of that, etc…
Hence you would use duty cycles between 95% (full reverse, 1000us pulse), 92.5% (zero point) and 90% (full forward, 2000us pulse).
Note that the control is reverse direction, with the higher duty cycle representing the lower velocity.
Why? Because I assume the servo has active-low polarity, i.e. it wants the control pulse to go from high to low. So a 90% duty cycle has a 10% low pulse…
If your servo has active-high polarity, you would just use the 5% (reverse) 7.5% (zero) and 10% (full forward) values.

Now in terms of setPwm(), with a setting of driver.voltage_power_supply = 6.0; this would mean you use control “voltages” between 6.0 * 0.9 and 6.0 * 0.95, i.e. 5.4V to 5.7V with the centre at 5.55V.
(so certainly you can’t control this using just integer voltages!)

Now you may be wondering, why deal with these voltages? Doesn’t this just make everything more complicated?

In this special case it may seem so - normally our drivers aren’t controlling RC-ESCs with receiver inputs, or servos, but rather the PWM is controlling the half-bridge switches of the ESC’s power stage directly. So the PWM normally represents a voltage quite directly.

But there is another advantage: by using a voltage as the parameter to the driver API, the other code in SimpleFOC and SimpleDC libraries can provide motor control algorithms that work on the basis of physical principals, dealing in voltages, resistances, currents and inductances.
If the output of these algorithms had to be in terms of RC-servo units we could not provide very general solutions.

Concretely, what this means for you is that if we can make this work well based on voltages, you can use SimpleDC and SimpleFOC’s other code to do closed-loop control of the motor in terms of angle radians, or velocity radians per second.

For your input control (a potentiometer?) you would read it using analogRead() and then map the integer analog value to a float voltage for the driver. Later, when you use closed loop control you would change this to map it to an angle or a velocity in rad/s for passing to the motor.move() function.

In the case of the 1PWMDriver, and the special sub-case of controlling RC-servos, we need to do the following:

  1. make the driver accept a 0-centred voltage as input to setPwm() - 0V means no motion, positive voltages mean turning in one direction and negative voltages turning in the other. Speed (or torque) are proportional to the voltage applied.
    If we make it work like this the setPwm() will be compatible with the rest of SimpleFOC.

  2. In the setPwm() we want to map the input voltage (which is 0-centred) to an output PWM duty cycle, (which is not zero-centred). In addition to the offset given by the servo zero point, we also want to scale the output by a factor. The scaling factor is given by the desired output PWM duty-cycle range, and the desired full scale voltage for the maximum duty cycle (which we could define to be the driver.voltage_power_supply). The input voltage is limited by the driver.voltage_limit, the output should be limited to the min and max duty-cycles.

  3. A complication is the fact that output duty cycle range in the positive and negative directions doesn’t have to be symmetrical - e.g. the zero point doesn’t have to be exactly between the min and the max. It could be closer to the min, for example - not uncommon for RC-cars where you generally need more range in the forward direction than the backward direction.
    This begs the question whether the scaling factor is kept the same for the forward and reverse directions, or adjusted to account for the difference in range. The remaining SimpleFOC code would generally expect the control to be symmetrical and independent of the direction.
    But you can ignore this question because in your case it is the same.

  4. Finally we should account for polarity, and allow the user to choose either polarity for the output.

Hey, I pushed some changes to the 1-PWM driver in the dev branch of the library:

I don’t have a test-setup ready to try it on, but if you can manage to use the dev-branch version of the library from GitHub you could test it out. I did not have time to add any documentation yet (I will in the future) but you should now be able to use it like this:

#define PIN_PWM 1
#define PIN_EN 5
#define THRESHOLD 0.5f

DCDriver1PWM driver = DCDriver1PWM(PIN_PWM, THRESHOLD, PIN_EN);

void setup() {
  driver.voltage_power_supply = 6.0f;
  driver.configureMicroseconds(50, 1000, 1500, 2000);
  driver.init();
}

void loop(){
  driver.setPwmMicroseconds(1900); // forwards
}

I’m guessing this will need still more work before it actually works though. The reason is that the PWM frequency on RP2040 (e.g. Pico) currently has a minimum value of 4000Hz - so you can’t set 50Hz. :frowning:
I don’t know if the servo can manage 4kHz, but I somehow doubt it, if it actually wanted 50Hz.

I will see what I can do about updating the RP2040 PWM driver to support such low frequencies.

I’ve updated the RP2040 PWM driver code to support arbitrary PWM frequencies, down to 50Hz and even below. Of course such low frequencies won’t be good for BLDC motor driving, but for this application of controlling a servo it will be useful.

I will be back at my workstation by tomorrow night and I can give it a test, I can’t test it right now. I’ll try to test it out tomorrow or Wednesday night and then I’ll commit it.

I actually tried this out, fixed some bugs, and result is merged to the dev branches of SimpleFOC and SimpleDC libraries.

If you want to test it out you need the dev branch version of the libraries. Otherwise it will be part of the next releases.

I’ll try to put some parts together also with a sensor and see if I can get velocity control or position control working well on a continuous rotation servo.

At the moment it works for RP2040 (e.g. Pico). I’ll also look into extending the other PWM drivers for other MCU types to support lower frequencies, but this could be a long-running effort :slight_smile:

Thanks for inspiring these improvements :slight_smile:

1 Like

steering_value is an int - but the return value of your fmap() function is a float. I think you need to use a float variable to transport fmap return value to the setPwm() function. Otherwise you can only set voltages like 3.0V, 4.0V, 2.0V and this will not be precise enough.

You are right, I rushed while porting the code and corrected it.

But there is another advantage: by using a voltage as the parameter to the driver API, the other code in SimpleFOC and SimpleDC libraries can provide motor control algorithms that work on the basis of physical principals, dealing in voltages, resistances, currents and inductances.
If the output of these algorithms had to be in terms of RC-servo units we could not provide very general solutions.

I agree. It makes sense for the code to work since the Simple FOC family of products was made to be general. I ultimately plan to also use it for reading encoders, commander, monitoring and also using a BLDC motor so it makes sense to keep everything consistent.

I actually tried this out, fixed some bugs, and result is merged to the dev branches of SimpleFOC and SimpleDC libraries.

I have setup the dev branch libraries in my Arduino IDE. Were you able to get it working with an RC servo receiver? My batteries came and I tested the code you posted earlier (referenced below) with the dev libraries as well as the servo motor and DC brushed motor but the steering servo behaved the same and the brushed motor did not move no matter what voltage was chosen. What did you do to get it working?

#define PIN_PWM 9
#define THRESHOLD 0.5f

DCDriver1PWM driver = DCDriver1PWM(PIN_PWM, THRESHOLD);

void setup() {
  driver.voltage_power_supply = 6.0f;
  driver.configureMicroseconds(50, 1000, 1500, 2000);
  driver.init();
}

void loop(){
  driver.setPwmMicroseconds(1900); // forwards
  delay(50);
}

No, I tested only the waveform on my oscilloscope.

You’re using RP2040, right? I have only implemented the slow PWM for this MCU type so far.

You code looks ok - is it certain the dev-branch versions are being used, and not another version you have somewhere?

You’re using RP2040, right? I have only implemented the slow PWM for this MCU type so far.

Yes, but I can also test with a Teensy 4.1 (also Arduino Mega or Portenta H7).

Your code looks ok - is it certain the dev-branch versions are being used, and not another version you have somewhere?

I am pretty sure, but I can uninstall and reinstall later tomorrow just to make sure. I will also test with a Teensy 4.1.

Thanks runger and privvyledge. I’ll follow this and experiment with similar methods to sync brushed and brushless motors together.

Consider this product as a well known example of combining brushed and brushless in a single product and code library SPARK MAX Motor Controller - REV Robotics it will be one of the things which the simpleFOC implementation will be compared against.

Brushed ESCs taking servo input are fairly common. And while it is technically PWM, it is used as a signaling protocol. Treating it as equivalent voltage up front will make many applications harder later on if the supply voltage is not known in some situation. The proportional signal is still valid without a known supply. Implementing deadband to eliminate hum (position hunting) is another difficulty with the approach above.

Proportional voltage is just one of the properties which the 1000-2000ms pulse durations may represent. Current, position, temperature are others. And even when it does represent voltage, it is common to scale the output in some way.
Servo expo (exponential) control is one example of intentional nonlinearity where output is less sensitive mid scale and exagerated at the extreme ends. Servo signals are well defined in their own right making the generic PWM duty cycle approach confusing.

It seems that simpleFOC may want a class of functions that take as parameters a simpleFOC motor object and an Arduino Servo object. Translate both ways.

ServoVelocityToSimpleFOC Read servo setpoint and interpret as velocity to Write to simpleFOC velocity(voltage) setpoint.
ServoPositionToSimpleFOC Read servo setpoint and interpret as position to Write to simpleFOC position setpoint.
ServoTorqueToSimpleFOC Read servo setpoint and interpret as torque to Write to simpleFOC torque(current) setpoint.
SimpleFOCPositionToServo Read SimpleFOC Position and Write to Servo object setpoint.

Method driver.PositionToServo or the like is simpler but the corresponding servo.ToSimpleFOCposition would depend on updates to the Servo library.

I recently got a couple of CUI AMT31-V brushless commutation encoders which I intend to put on the shaft of a brushed motor. The intent is that as the brushed motors move, the encoder will produce a 3 phase signal for the brushless motor to follow. The brushless driver would then add or subtract duty cycle depending on if its own hall sensors are lagging or leading the reference signal.
No idea if that is practical or not. I cannot test till I locate the special cable needed for the encoders.

In examples/dc-torque-voltage/dc-torque-voltage.ino what are the applications the comments refer to? Is it IR compensation? Or is IR compensation the unimplemented current mode control?

You’re talking about a digital servo or ESC’s programming protocols etc here, right? The (admittedly basic) RC gear and servos I have here don’t support any of this. They just like a PWM input signal for “speed control”.

We’re really mainly interested in FOC control in the SimpleFOC library. As such the library’s abstractions assume you’re controlling a BLDC or Stepper via a driver providing direct control of its half-bridges. So the voltage abstraction is appropriate, it’s the quantity we’re controlling with the PWM. The control of current is built on top of this.

The SimpleDC library (very new!) attempts to extend the features of SimpleFOC to DC motor control. The main goal is to make our “infrastructure” like Sensor classes, the Commander interface and more available in a more general setting.
Controlling a RC ESC or RC-Servo via PWM is a bit of a stretch even for this scenario, the intention again was more to interface with the many DC motor drivers that take various forms of GPIO and PWM input to control their half-bridges directly.

I don’t think we have any intention of competing with a fully featured servo driving solution. Instead, the user should just use this solution to set advanced parameters, and (if it makes sense) SimpleFOC for the motor control.

Why not just read the CUI encoder and use the value to change the set-point of the BLDC in velocity or position mode?

I don’t understand this question… which applications are you referring to?

dc-torque-voltage is basically just like setting a DC voltage to the motor - like open loop control.
You’d not normally need SimpleFOC for that. I think velocity or position control are more interesting for users since this is closed loop control, and an improvement on what you can do with just a basic driver…

I must admit I’m really confused by this thread.

The OP wants to control a standard servo. There are many excellent servo libraries for Arduino that work very well, and can control a standard RC servo in all possible settings, including deadbands, etc.

The SimpleFOC library adds no value to this scenario. No disrespect meant: it’s really not designed for this nor it makes sense to use a much more complex, processor and memory intensive library for something as simple as an RC servo control.

DC motor control is a valuable add to the SimpleFOC family of direct motor control. But, once again, a convoluted and suboptimal way to handle an RC servo.

If the OP wants to control an RC servo, SimpleFOC is not the best way…

I would generally agree with what you’re saying, @robca .

But @privvyledge did mention that he was working with both BLDCs and DC motors in the same system, and in this case maybe using SimpleDC library for the DC motors makes sense. But maybe it makes more sense to use the best servo library for the servos, and SimpleFOC for the BLDCs.

But I did not really intend it to control servos - in fact when I wrote the first version of the DCDriver1PWM I had a kind of DC driver in mind that uses a single PWM to control the velocity, with positive velocities above a certain duty cycle threshold and negative ones below. It’s true that RC ESCs work a bit like this, although as we saw I had to recode the driver quite a bit to support the narrow PWM signal and slow frequency.
Still, now that it is done you could use SimpleFOC for example to control a continuous rotation servo with closed loop velocity control.

This is the application I had in mind. Use SimpleFOC to tune, monitor, and command both the brushed and brushless motors in the system. But in the special case of servo style input ESCs(brushed or brushless really) allow disabling the half bridge control and instead pass an appropriate numeric value to a Servo object. SimpleFOC still reads the encoders and does closed loop control, it just does so through an additional abstraction layer necessitated by the end user choice of ESC. Is that sort of control within the scope of SimpleFOC at some point?

Re: CUI encoder tangent. No particular reason other than I got a deal on those encoders and wanted to experiment synchronizing a master brushed motor and several brushless motors with just the encoder bus. Each commutation tic triggers an interrupt to wake up the controller and move a bit. Not relevant other than it mixes brushed and brushless.

Re: IR compensation. It is a requirement in brushed wheelchair controls. Encoder based position control is too “harsh” according to users. Sudden starts and stops throw users around causing them to push the joystick unknowingly and user induced oscillations occur.

Open loop control has the opposite problem. Getting over thresholds and other small obstacles, users need to increase joystick throw to get over bump, then they are going much too fast and need large control input to slow down again. Too much effort for someone with poor hand control.

IR compensation is a nice middle ground without encoders that provides the controller knowledge of the connected DC motor impedance so it can infer RPM and load based on current. It allows slowing down a little on obstacles while automatically ramping current, then reducing current automatically when RPM increases and load decreases.

I don’t have a solid grounding in it yet, but have heard several complaints when IR compensation was not implemented in wheelchair controls. It seems intuitive to users. I was wondering if dc-torque-voltage.ino was the simpleFOC incarnation of IR compensation by a different name.

From https://github.com/simplefoc/Arduino-FOC-dcmotor/blob/main/examples/dc-torque-voltage/dc-torque-voltage.ino

  • So why include it? There are some advantages which may be of interest
  • to some users. For example, by using the sensor we can compute the velocity
  • and thereby estimate the back-electromotive force (BEMF) of the motor, and
  • take this into account when setting voltage.

I was just curious what advantages and users you had in mind for this example.

We address this kind of use case in our new SimpleDC library. This kind of control is no longer field-oriented control, so it’s not our primary interest. But we think the surrounding code like closed loop velocity control, or our sensor drivers can have value for people interested in these use cases, so the SimpleDC library attempts to address them by providing drivers that work with other kinds of motors or ESCs.
So its not really in scope, but if anyone is interested and writes some code for some specific system, we would certainly consider including it in the SimpleDC library.

This is very interesting. I think I understand quite well now from your description.

I think the IR control you describe can be achieved in different ways using brushless motors:

  • using just torque voltage control, you could already limit torque and limit the rate of change of torque relative to the users input
  • using velocity control you could limit velocity, as well as acceleration (max_ramp) using the PID controller
  • using angle control, you can implement a position based control scheme based on the user input, which limits acceleration and velocity to fit within pre-defined envelopes (note: we don’t offer this functionality, but it could be implemented)
  • using current control, you can implement limiting of the torque, and following torque profiles, etc

I think a FOC controller with current sensing and position sensor probably has more information available to act on than the DC solution, as well as better control possibilities. Of course this comes at extra cost and complexity.

Since there is no FOC, the position sensor isn’t used for DC torque-voltage mode. But I think users could be interested in driving a DC motor while reading the sensor position for some higher level control loop. With this code example you get simple open loop control of the motor, but get back encoder readings as well.

But it’s difficult to predict the use cases of our users. It’s really surprising, again and again, what people are doing with SimpleFOC. So our code examples should be understood more as examples of how to use the library code, rather than examples of how to achieve some real-world goal… it’s up to our users to turn the examples into something real :smiley:

1 Like