The theory
PWM (Pulse Width Modulation) is a modulation technique where the duration of a signal pulse is used to encode information. In the case of a quadcopter it's usually used to modulate the rotational speed of the motors/propellers. The signal is periodic and the duty cycle represent the proportion of the period where the signal is "on" (logical 1).
To generate such a signal, I'll be using the NXP PCA9685 chip on a pre-assembled Adafruit PCB module. The output channels will eventually be connected to ESC circuits (electronic speed controllers) that will drive the motors with a higher voltage/amperage than the Pi or the PCA9685 could deliver.
The signals could be generated on the GPIO pins of the Pi, but there is only 1 PWM dedicated hardware pin. Since we need 4 PWM signals (one for each motor), three of the signals would need to be generated in software - which would gobble up a lot of CPU power at high frequencies. It's therefore better to use a dedicated IC like the PCA9685.
A longer pulse width will increase propeller rotational speed. A shorter pulse width will decrease the propeller rotational speed. Some baselining and calibration will be required when I get there. For the moment I want to make sure that I can properly interface with the circuit and generate a PWM signal on any output pins.
What we need
You can find the NXP PCA9685 specification
here. There are 3 things I need to code:
- Initialization routine of the PCA to make sure the IC is running in the proper mode and is ready to go.
- Function to modify the PWM frequency
- The IC has an internal oscillator (clock) of 25 MHz, but the output frequency of the PWM signal can only be set between 24Hz and 1526Hz
- To adjust the PWM frequency, a special register is used (prescale)
- The output frequency will need to be set according to what can be taken as input in my future ESC motor drivers (usually maximum of 500Hz according to what I gathered so far)
- Function to modify the duty cycle of the PWM signal
- The duty cycle will be changed according to the input throttle value - i.e. the speed at which we want the motors to spin
By the way, the duty cycle doesn't have to start exactly at the beginning of a period. You can have a delay before the duty cycle starts. That's why the PCA9685 allows you to define both an ON and an OFF value. The circuit uses 12 bits registers for those. That means that each period is divided in 4096 sub intervals.
The ON value will specify the interval when the signal goes to "1", the OFF value will specify the interval when the signal goes back to "0". For example: ON = 0 and OFF = 2048 means that the signal will be active for the first 50% of the period.
You'll see that I'm using the I2C interface I coded in the previous blog post to send the commands.
this->devAddress was initialized to 0x40 (address of the PCA9685 on the I2C bus).
All the registers and bit masks are defined in an enum (e.g. ALL_LED_ON, MODE2, etc.) and the names are fully aligned with the NXP spec.
Initialization routine
The init routine does three things:
- Turn off all output channels (effectively setting a duty cycle of 0%)
- Writes 0 to the ALL_LED_ON and ALL_LED_OFF registers
- Set the right modes of operations
- Restart the module
As per the spec, we need to wait 500us after some operations to make sure the oscillator stabilizes.
int PCA9685::init()
{
debug_print("[PCA9685] Init...\n");
debug_print("[PCA9685] Turning off all PWM channels\n");
this->pBus->WriteRegisterByte(this->devAddress, ALL_LED_ON, 0);
this->pBus->WriteRegisterByte(this->devAddress, ALL_LED_ON+1, 0);
this->pBus->WriteRegisterByte(this->devAddress, ALL_LED_OFF, 0);
this->pBus->WriteRegisterByte(this->devAddress, ALL_LED_OFF+1, 0);
debug_print("[PCA9685] Set mode registers\n");
// configure totem pole structured output
this->pBus->WriteRegisterByte(this->devAddress, MODE2, OUTDRV);
// respond to ALL_LED register changes
this->pBus->WriteRegisterByte(this->devAddress, MODE1, ALLCALL);
usleep(5000); // wait for oscillator
debug_print("[PCA9685] Reset\n");
int mode = this->pBus->ReadRegisterByte(this->devAddress, MODE1);
this->pBus->WriteRegisterByte(this->devAddress, MODE1, mode & ~SLEEP);
usleep(5000); // wait for oscillator
return 0;
}
Set the PWM frequency
We need a routine to modify the periodicity of the PWM signal. This is done by tweaking the prescale register value according to this formula (from the NXP spec).
T
he frequency of the internal clock is 25MHz. The IC also accepts an external clock up to 50MHz.
The refresh rate will be set according to what we need to drive the motors ESC - this will be the function's input parameter (e.g. 250 Hz)
int PCA9685::setPWMFequency (int frequency)
{
debug_print("[PCA9685] Setting PWM frequency @ %dHz...\n", frequency);
int prescale = floor((PCA9685_CLOCK_HZ / (float)(PCA9685_RESOLUTION * frequency)) + 0.5) - 1;
debug_print("[PCA9685] Prescale @ %d...\n", prescale);
// preserve original mode
int mode = this->pBus->ReadRegisterByte(this->devAddress, MODE1);
// disable restart, sleep, set prescale and reset
this->pBus->WriteRegisterByte(this->devAddress, MODE1, (mode & ~RESTART) | SLEEP);
this->pBus->WriteRegisterByte(this->devAddress, PRESCALE, prescale);
this->pBus->WriteRegisterByte(this->devAddress, MODE1, mode);
// wait for oscillator
usleep(5000);
// enable restart
this->pBus->WriteRegisterByte(this->devAddress, MODE1, mode | RESTART);
return 0;
}
Set the PWM duty cycle
This function takes 3 parameters: the channel id (between 0 and 15 as the PCA9685 has 16 output pins), and the interval values for when to turn the PWM signal on and off during a period (between 0 and 4095).
The channel id is used to determine what registers to write to as an offset from the base LED0_ON register at the bottom of the addressable register space.
int PCA9685::setPWM (int channel, int on, int off)
{
int reg = LED0_ON + 4*channel;
this->pBus->WriteRegisterByte(this->devAddress, reg, on & 0xFF);
this->pBus->WriteRegisterByte(this->devAddress, reg+1, (on & 0xFF00) >> 8);
this->pBus->WriteRegisterByte(this->devAddress, reg+2, off & 0xFF);
this->pBus->WriteRegisterByte(this->devAddress, reg+3, (off & 0xFF00) >> 8);
return 0;
}
All of the code is on bitbucket.
Performance
For the sake of making sure that running this on the Pi is performant enough, I've measured the elapsed time to set a new PWM duty cycle 5000 times in a loop.
I obtained 770 times per second. Assuming that the routine has its own thread running on one of the four cores, that means that in the best case scenario I can update the speed of all 4 motors 193 times/s, half the maximum input frequency of some ESC circuits I've looked into. That frequency will be lower once we add the PID control loops....
Hopefully that will be enough. I've read on a forum that we need at least 50Hz for marginal performance and flight stability. I'll look into optimizing the I2C interface code just to make sure.
UPDATE
I've optimized the code to achieve 2500 Hz in PWM duty cycle setting instead of 770Hz.
I've done two things to achieve this:
- Used the PCA9685 register auto increment feature. It allows me to write an array of bytes to a base register, and the address will auto increment after each byte.
- Removed writing the "On" values. Since I don't need a delay before the start of the duty cycle, it can always be 0 and left untouched. I only write the "Off" values to modify the duty cycle length.
UPDATE 8/24
I've been trying to increase the performance of those routines and figured out that I'm bumping into a bottleneck: the I2C bus clock frequency.
The default frequency on the Pi is 100kHz - but it's possible to tweak it !
I've cranked it to 400kHz and now I've achieved 8.3kHz in updating the PWM.
sudo nano /boot/config.txt
Add this line:
dtparam=i2c1_baudrate=400000