ATMega 328 to control an ESC (PWM or DShot)

Hello all,

I've asked on the topic before and got a big help from Flashko with the below code. I've been testing and running into issues. The below seems to be working properly when the motorpin is viewed on my tiny oscope. It seems to go from 1000microsec to 2000 as it should. However, the ESC is only responding to IN1. IN2-4 all give the same speed as IN1 despite the pulse width changing. I suspect is due to not doing ESC calibration but I'm not sure how to do that with the Arduino. I should note that when I use a drone flight controller the ESC responds as it should. This is using the DShot protocol however.

Within the ESC I have the bounds set at 900 and 2100. I don't need really fast response but had thought about using DShot. It's a digital protocol that uses an SPI pin. There are a few projects out there that implement the created DShot library but it's way over my head to strip out what I need. Oneshot and multishot are just faster pwm. Noise and jitter are not really "fixed". DShot does fix the jitter and noise. If I cannot implement DShot I just need the pwm at 50hz to work.

The idea here is using the original HVAC switch in my car that goes to ground. 1-4 positions for the inputs of the fan speed. My original fan was quite anemic.

The arduino tester that supports PWM, Oneshot, Multishot and DShot.
[Reefwing Robotics: Arduino ESC Tester - Adding DShot]

The board I'm using is the Metro Mini by Adafruit.

#include <Servo.h>

Servo ESC;

const byte
IN1 = 4,
IN2 = 5,
IN3 = 6,
IN4 = 7,
MotorPin = 9
           ;

void setup ()
{
  pinMode ( IN1, INPUT_PULLUP );
  pinMode ( IN2, INPUT_PULLUP );
  pinMode ( IN3, INPUT_PULLUP );
  pinMode ( IN4, INPUT_PULLUP );
  ESC.attach ( MotorPin, 950, 2050 );
}

void loop ()
{
  if ( digitalRead ( IN1 ) == LOW )
  {
    ESC.write ( 45 ); // 1/4 speed
  }
  else if ( digitalRead ( IN2 ) == LOW )
  {
    ESC.write (90 ); // 1/2 speed
  }
  else if ( digitalRead ( IN3 ) == LOW )
  {
    ESC.write ( 135 ); // 3/4 speed
  }
  else if ( digitalRead ( IN4 ) == LOW )
  {
    ESC.write ( 180 ); // full speed
  }
  else
  {
    ESC.write ( 0 ); // stand still
  }
}

Thanks for the code posted.

Please post schematics. Poetry often introduces uncertainty or misunderstandings.

Hi @exidous

PWM for drone motors work at 490Hz, which can be provided by the Arduino analogWrite() function on pins: D9 (OC1A), D10 (OC1B), D11 (OC2A) and D3 (OC2B).

If you happen to be using a traditional RC receiver with separate channels, it's possible to calibrate the ESCs independently from the Arduino, by connecting the receiver's throttle channel to the ESC input directly and following ESC calibration procedure. As you would for any RC aircraft model without a flight controller.

DShot isn't possible with the Atmega328P, since it doesn't have a DMAC (Direct Memory Access Controller). For that you'll require an ARM based microcontroller or equivalent.

DShot works by taking the 11-bit motor output value, plus 1 telemetry bit, calculating a 4-bit CRC code then coding this 16-bit pattern as pulse widths in memory. Upon a command from the CPU, the DMAC lifts each pulse width in the pattern one by one and feeds them to a timer that fires each one down the line to the ESC.

The DMAC operates without CPU intervention, so all the CPU has to do is load the pulse widths into memory and tell the DMAC to start. This process is repeated for each motor on separate DMAC channels.

The whole DShot process is quite elaborate compared with PWM, where analogWrite() simply loads the timer's corresponding OCRxx register with the motor value.

Please forgive the crude drawing. Calling anything that comes out of my mind "poetry" is being far too kind.

I do have a couple receivers laying around but calibrating to them rather than what the Metro is providing probably won't work. I'd be willing to bet you're spot on about 490Hz. The ESC knows something is there and turns on but it cannot register the duty. I'm not sure how to get the output to run at 490Hz when servo/h default is 50Hz.

There are a couple folks using bit banging at a method to run DShot without DMA. I understand, in theory, what they did but the code is far beyond me.

Youtube of one way.

ESC tester using the 328.

Hi @exidous

Regarding calibration, it usual to set the bounds to whatever the device controlling them is using.

Just use the analogWrite() and map() functions rather than the servo library.

I guess it depends on what else the microcontroller's required to do in the meantime?

The GNDs don't look the best. Use the point near the power supply and wire all GNDs from that point.

Your text tells about IN1... IN4 but they can't be seen in Your post.
Early work tomorrow and good night from here.

That would be rather difficult. Changing the grounds. This is in a car and the battery is not close by. Power is provided by a Power Distribution Module. No relays or fuses.

The IN1-4 are referenced in the code. IN1=pin 4/IN4=pin 7.

Something like this?

I wasn't certain if ESC.attach was a servo.h command but I still would need to set the min/max for the pulse width right? By the way, the ESC is programable and I can set the min/max within the software. I can also disable the need for calibration.

It looks like a manual delay would need to be added?

const byte
IN1 = 4,
IN2 = 5,
IN3 = 6,
IN4 = 7,
MotorPin = 9;

void setup ()
{
  pinMode ( IN1, INPUT_PULLUP );
  pinMode ( IN2, INPUT_PULLUP );
  pinMode ( IN3, INPUT_PULLUP );
  pinMode ( IN4, INPUT_PULLUP );
}

void loop ()
{
  if ( digitalRead ( IN1 ) == LOW )
  {
    analogWrite(9, 63); // 1/4 speed
  }
  else if ( digitalRead ( IN2 ) == LOW )
  {
    analogWrite(9, 127); // 1/2 speed
  }
  else if ( digitalRead ( IN3 ) == LOW )
  {
    analogWrite(9, 191); // 3/4 speed
  }
  else if ( digitalRead ( IN4 ) == LOW )
  {
    analogWrite(9, 255); // full speed
  }
  else
  {
   analogWrite(9, 0); // stand still
  }
}

Hi @exidous

ESCs takes PWM pulse widths roughly between 1000us to 2000us from the microcontroller's timer. To store values up to 2000 takes 11-bit of binary data. (Note that DShot packet data field is also 11-bits as well).

The analogWrite() function however sets the PWM duty-cycle with values between 0 and 255, this only takes 8-bits.

Therefore it's necessary to use the Arduino map() function to convert from 0 to 2000, to a value between 0 and 255. There is some data loss in the conversion, but it allows the Atmega328P to control 4 ESCs with a resolution of 127 steps.

Note that if you're doing other processing on the motor's throttle value, it's also possible to use the constrain() function to limit the value to a given range.

Here's an example:

int throttle = 1300;                                    // Set the throttle value
int mappedThrottle = map(throttle, 0, 2000, 0, 255);    // Map the throttle value from 11-bits to 8-bits
mappedThrottle = constrain(mappedThrottle, 0, 255)      // Constrain the output between 0 an 255
analogWrite(9, mappedThrottle);                         // Output the result as 490Hz PWM on digital pin 9

Isn't that reversed though? The control is 8bit but I need to output 11 bit?

int mappedThrottle = map(throttle, 0, 255, 0, 2000) ?

Also, does the mapping need to be in the loop or just initial? I'm only controlling one ESC and no extra variables for the output. The 4 digital inputs is all I'm using.

IN1 = 4,
IN2 = 5,
IN3 = 6,
IN4 = 7,
MotorPin = 9;

void setup ()
{
  pinMode ( IN1, INPUT_PULLUP );
  pinMode ( IN2, INPUT_PULLUP );
  pinMode ( IN3, INPUT_PULLUP );
  pinMode ( IN4, INPUT_PULLUP );
  int throttle = 1000;                                    // Set the throttle value
  int mappedThrottle = map(throttle, 0, 255, 0, 2000);    // Map the throttle value from 11-bits to 8-bits
  mappedThrottle = constrain(mappedThrottle, 0, 255);      // Constrain the output between 0 an 255
}

void loop ()
{
  if ( digitalRead ( IN1 ) == LOW )
  {
    analogWrite(MotorPin, 63); // 1/4 speed
  }
  else if ( digitalRead ( IN2 ) == LOW )
  {
    analogWrite(MotorPin, 127); // 1/2 speed
  }
  else if ( digitalRead ( IN3 ) == LOW )
  {
    analogWrite(MotorPin, 191); // 3/4 speed
  }
  else if ( digitalRead ( IN4 ) == LOW )
  {
    analogWrite(MotorPin, 255); // full speed
  }
  else
  {
   analogWrite(MotorPin, 0); // stand still
  }
}

Hi @exidous

No, it's not reversed. The limitation is the Atmega328P. The microcontroller has three timers 0, 1 and 2.

Timer 0 is an 8-bit timer used to calculate the delay and timing functions such as millis(), micros(), delay(), etc... It's has two analogWrite() PWM outputs on D5 and D6, but these are at a frequency of 980Hz.

Timer 1 is a 16-bit timer with two PWM outputs on D8 and D9 at 490Hz. It's the only timer capable of generating a PWM with 11-bit resolution, however for analogWrite(), it's functionality has been reduced to 8-bits to bring it into line with the other timers.

Timer 2 is another 8-bit timer with two PWM outputs on D3 and D11 at 490Hz.

This means that the Atmega328P has only two pins on timer 1 capable of supporting PWM with a resolution of 11-bits and this isn't supported by any of the existing Arduino functions.

The Atmega32U4 microcontroller found on the Arduino Micro and other similar boards, does have two 16-bit timers with 4 output channels. It's therefore capable generating PWM outputs with 11-bit resolution, but this is only be achieved with register manipulation.

If you're recalulating the throttle value in the loop() then it requires the mapping to be performed in the loop as well.

Better?

const byte
IN1 = 4,
IN2 = 5,
IN3 = 6,
IN4 = 7,
MotorPin = 9;

void setup ()
{
  pinMode ( IN1, INPUT_PULLUP );
  pinMode ( IN2, INPUT_PULLUP );
  pinMode ( IN3, INPUT_PULLUP );
  pinMode ( IN4, INPUT_PULLUP );
  int throttle = 1000;                                    // Set the throttle value 
}

void loop ()
{
  if ( digitalRead ( IN1 ) == LOW )
  {
    int mappedMotorPin = map(MotorPin, 0, 2000, 0, 255);    // Map the throttle value from 11-bits to 8-bits
    mappedMotorPin = constrain(mappedMotorPin, 0, 255);      // Constrain the output between 0 an 255
    analogWrite(MotorPin, 63); // 1/4 speed
  }
  else if ( digitalRead ( IN2 ) == LOW )
  {
    int mappedMotorPin = map(MotorPin, 0, 2000, 0, 255);    // Map the throttle value from 11-bits to 8-bits
    mappedMotorPin = constrain(mappedMotorPin, 0, 255);      // Constrain the output between 0 an 255
    analogWrite(MotorPin, 127); // 1/2 speed
  }
  else if ( digitalRead ( IN3 ) == LOW )
  {
    int mappedMotorPin = map(MotorPin, 0, 2000, 0, 255);    // Map the throttle value from 11-bits to 8-bits
    mappedMotorPin = constrain(mappedMotorPin, 0, 255);      // Constrain the output between 0 an 255
    analogWrite(MotorPin, 191); // 3/4 speed
  }
  else if ( digitalRead ( IN4 ) == LOW )
  {
    int mappedMotorPin = map(MotorPin, 0, 2000, 0, 255);    // Map the throttle value from 11-bits to 8-bits
    mappedMotorPin = constrain(mappedMotorPin, 0, 255);      // Constrain the output between 0 an 255
    analogWrite(MotorPin, 255); // full speed
  }
  else
  {
   analogWrite(MotorPin, 0); // stand still
  }
}

Hi @exidous

If you're testing the PWM output at set values you don't need to map anything, just set analogWrite() as in your code.

If you're mapping the throttle value then the "throttle" variable needs to replace the "MotorPIn" constant in the map() function and the "mappedMotorPin" should be passed to analogWrite().

Before testing and for safety, if you haven't done so already, I'd remove the fan and run the motor without anything attached to it.

I think I want to scratch all this and just change to Oneshot. Thoughts?

Hi @exidous

My apologies, I think I got the wrong end of the stick.

So you just need to drive a single fan with 4 input switches to adjust the speed?

You'd like to achieve this, driving the fan with a brushless motor driven by a single ESC?

Spot on.

Hi @exidous

Might I ask how you're powering the ESC?

Also, what make/model of ESC and motor are you using?

Hey,

I have this one. Within the settings I've disables throttle calibration, battery protection and tones. I've also adjusted the acceleration to reduce noise on startup.

Power is coming from a Power Distribution module on the car which pulls from the battery/alternator. I expect voltages between 11-14.5v.




This mounts where the stock DC brushed fan speed resistor used to go. The ESC sits directly in the airflow covered in conformal coating.

Hi @exidous

Thanks for the images and link to the ESC information. Looking at the spec it doesn't mention standard 490Hz PWM, I guess it's too old.

It should be possible to get Oneshot125 working on timer 1 output on ditgital pin D9 though. I'm looking into it.