In the second part of PCA9685 PWM module guide, we shall create the library that allow us to set the frequency and control the duty cycle of each individual channel.
In this guide, we shall cover the following:
- Creating the header and source file.
- Developing the header file.
- Developing the source file.
- Testing the library.
6. Creating the Header and Source Files:
After the project has been generated by STM32CubeMX within STM32CubeIDE, we shall add new source file and header file.
Right click on Src folder and add new source file as following:
Give a name for the source file, we shall name it as PCA9685.c as following:
In similar manner, right click on Inc folder and add new header file with name of PCA9685.h.
7. Developing the Header File:
Start off by including the stdint header file as following:
#include "stdint.h"
Then, declare the following enumeration which holds the channels of the module as following:
typedef enum { CH0=0, CH1, CH2, CH3, CH4, CH5, CH6, CH7, CH8, CH9, CH10, CH11, CH12, CH13, CH14, CH15 }PCA9685_ChannelTypedef;
Next, we shall define all the registers of the module as following:
// Mode Registers #define PCA9685_MODE1 0x00 // Mode register 1 #define PCA9685_MODE2 0x01 // Mode register 2 // Subaddress Registers #define PCA9685_SUBADR1 0x02 // I2C-bus subaddress 1 #define PCA9685_SUBADR2 0x03 // I2C-bus subaddress 2 #define PCA9685_SUBADR3 0x04 // I2C-bus subaddress 3 // All Call Address Register #define PCA9685_ALLCALLADR 0x05 // LED All Call I2C address // LED Output and PWM Control Registers #define PCA9685_LED0_ON_L 0x06 // LED0 output and brightness control - ON low byte #define PCA9685_LED0_ON_H 0x07 // LED0 output and brightness control - ON high byte #define PCA9685_LED0_OFF_L 0x08 // LED0 output and brightness control - OFF low byte #define PCA9685_LED0_OFF_H 0x09 // LED0 output and brightness control - OFF high byte #define PCA9685_LED1_ON_L 0x0A // LED1 output and brightness control - ON low byte #define PCA9685_LED1_ON_H 0x0B // LED1 output and brightness control - ON high byte #define PCA9685_LED1_OFF_L 0x0C // LED1 output and brightness control - OFF low byte #define PCA9685_LED1_OFF_H 0x0D // LED1 output and brightness control - OFF high byte // Repeat for all 16 channels #define PCA9685_LED2_ON_L 0x0E #define PCA9685_LED2_ON_H 0x0F #define PCA9685_LED2_OFF_L 0x10 #define PCA9685_LED2_OFF_H 0x11 // ... #define PCA9685_LED15_ON_L 0x42 #define PCA9685_LED15_ON_H 0x43 #define PCA9685_LED15_OFF_L 0x44 #define PCA9685_LED15_OFF_H 0x45 // All LEDs Control #define PCA9685_ALL_LED_ON_L 0xFA // Load all LEDn_ON registers, low byte #define PCA9685_ALL_LED_ON_H 0xFB // Load all LEDn_ON registers, high byte #define PCA9685_ALL_LED_OFF_L 0xFC // Load all LEDn_OFF registers, low byte #define PCA9685_ALL_LED_OFF_H 0xFD // Load all LEDn_OFF registers, high byte // Prescaler Register #define PCA9685_PRESCALE 0xFE // Prescaler for PWM output frequency // Test Mode Register (Not recommended for normal use) #define PCA9685_TESTMODE 0xFF // Test mode register #define FREQUENCY_OSCILLATOR 25000000 /**< Int. osc. frequency in datasheet */ #define PCA9685_PRESCALE_MIN 3 /**< minimum prescale value */ #define PCA9685_PRESCALE_MAX 255 /**< maximum prescale value */
Next, we shall declare a function that allows the use to change the default address as following:
void PCA9685SetAddr(uint8_t add);
The function takes the new address as argument and returns no thing.
Next, a function that will initialize the module with the desired frequency:
void PCA9685_Init(uint16_t freq);
The function will intialize the module and take the desired frequency as argument and return nothing.
Declare a function that will set the duty cycle of the desired channel as following:
void PCA9685_SetPWMDutyCycle(PCA9685_ChannelTypedef ch, uint8_t dutyCycle);
The function takes the following parameters:
- Channel number (from 0 to 15).
- Duty cycle of the channel.
Hence, the entire header file as following:
#ifndef INC_PCA9685_H_ #define INC_PCA9685_H_ #include "stdint.h" typedef enum { CH0=0, CH1, CH2, CH3, CH4, CH5, CH6, CH7, CH8, CH9, CH10, CH11, CH12, CH13, CH14, CH15 }PCA9685_ChannelTypedef; // Mode Registers #define PCA9685_MODE1 0x00 // Mode register 1 #define PCA9685_MODE2 0x01 // Mode register 2 // Subaddress Registers #define PCA9685_SUBADR1 0x02 // I2C-bus subaddress 1 #define PCA9685_SUBADR2 0x03 // I2C-bus subaddress 2 #define PCA9685_SUBADR3 0x04 // I2C-bus subaddress 3 // All Call Address Register #define PCA9685_ALLCALLADR 0x05 // LED All Call I2C address // LED Output and PWM Control Registers #define PCA9685_LED0_ON_L 0x06 // LED0 output and brightness control - ON low byte #define PCA9685_LED0_ON_H 0x07 // LED0 output and brightness control - ON high byte #define PCA9685_LED0_OFF_L 0x08 // LED0 output and brightness control - OFF low byte #define PCA9685_LED0_OFF_H 0x09 // LED0 output and brightness control - OFF high byte #define PCA9685_LED1_ON_L 0x0A // LED1 output and brightness control - ON low byte #define PCA9685_LED1_ON_H 0x0B // LED1 output and brightness control - ON high byte #define PCA9685_LED1_OFF_L 0x0C // LED1 output and brightness control - OFF low byte #define PCA9685_LED1_OFF_H 0x0D // LED1 output and brightness control - OFF high byte // Repeat for all 16 channels #define PCA9685_LED2_ON_L 0x0E #define PCA9685_LED2_ON_H 0x0F #define PCA9685_LED2_OFF_L 0x10 #define PCA9685_LED2_OFF_H 0x11 // ... #define PCA9685_LED15_ON_L 0x42 #define PCA9685_LED15_ON_H 0x43 #define PCA9685_LED15_OFF_L 0x44 #define PCA9685_LED15_OFF_H 0x45 // All LEDs Control #define PCA9685_ALL_LED_ON_L 0xFA // Load all LEDn_ON registers, low byte #define PCA9685_ALL_LED_ON_H 0xFB // Load all LEDn_ON registers, high byte #define PCA9685_ALL_LED_OFF_L 0xFC // Load all LEDn_OFF registers, low byte #define PCA9685_ALL_LED_OFF_H 0xFD // Load all LEDn_OFF registers, high byte // Prescaler Register #define PCA9685_PRESCALE 0xFE // Prescaler for PWM output frequency // Test Mode Register (Not recommended for normal use) #define PCA9685_TESTMODE 0xFF // Test mode register #define FREQUENCY_OSCILLATOR 25000000 /**< Int. osc. frequency in datasheet */ #define PCA9685_PRESCALE_MIN 3 /**< minimum prescale value */ #define PCA9685_PRESCALE_MAX 255 /**< maximum prescale value */ void PCA9685SetAddr(uint8_t add); void PCA9685_Init(uint16_t freq); void PCA9685_SetPWMDutyCycle(PCA9685_ChannelTypedef ch, uint8_t dutyCycle); #endif /* INC_PCA9685_H_ */
8. Developing the Source File:
Open PCA9685.c source file.
We start off by including the PCA9685 header file as following:
#include "PCA9685.h"
Include both main.h and i2c.h as following:
#include "main.h" #include "i2c.h"
Next, declare the address of the module as following:
static uint8_t PCA9685_ADDR = 0x40<<1;
The reason it is static, it won’t be used outside the current source file.
Next, the function that will change the address as following:
void PCA9685SetAddr(uint8_t add) { PCA9685_ADDR=add<<1; }
Next, the function that will initialize the module with the desired frequency:
void PCA9685_Init(uint16_t freq) { uint8_t data[2]; data[0] = PCA9685_MODE1; // MODE1 register data[1] = 0x30; // Auto-increment and normal mode sleep on oscillator off HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100); float prescaleval = ((FREQUENCY_OSCILLATOR / (freq * 4096.0))); if (prescaleval < PCA9685_PRESCALE_MIN) prescaleval = PCA9685_PRESCALE_MIN; if (prescaleval > PCA9685_PRESCALE_MAX) prescaleval = PCA9685_PRESCALE_MAX; uint8_t prescale = (uint8_t)prescaleval-1; data[0] = PCA9685_PRESCALE; // PRE_SCALE register data[1] = prescale; HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100); data[0] = PCA9685_MODE1; // MODE1 register data[1] = 0xA0; HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100); }
First, declare a buffer of two bytes to hold the data to be transmitted.
Next, set the module into sleep mode and enable auto address increment as following:
Then write the data to MODE1 register as following:
uint8_t data[2]; data[0] = PCA9685_MODE1; // MODE1 register data[1] = 0x30; // Auto-increment and normal mode sleep on oscillator off HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100);
Next, we shall calculate the prescaler to to generate the desired frequency as following:
Write the new prescaler values to the prescaler register as following:
float prescaleval = ((FREQUENCY_OSCILLATOR / (freq * 4096.0))); if (prescaleval < PCA9685_PRESCALE_MIN) prescaleval = PCA9685_PRESCALE_MIN; if (prescaleval > PCA9685_PRESCALE_MAX) prescaleval = PCA9685_PRESCALE_MAX; uint8_t prescale = (uint8_t)prescaleval-1; data[0] = PCA9685_PRESCALE; // PRE_SCALE register data[1] = prescale; HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100);
Next, restart the module the enable the auto address increment as following:
data[0] = PCA9685_MODE1; // MODE1 register data[1] = 0xA0; HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100);
Next the function to set the duty cycle of the c hannel:
void PCA9685_SetPWMDutyCycle(PCA9685_ChannelTypedef ch, uint8_t dutyCycle) { if(dutyCycle>100)dutyCycle=100; if (ch>CH15) return; uint16_t ON_count = 0; // Start at 0 uint16_t OFF_count = (uint16_t)((dutyCycle * 4096) / 100); // Calculate register addresses uint8_t reg_base = PCA9685_LED0_ON_L + 4 * ch; // Prepare data for the ON and OFF registers uint8_t data[4]; data[0] = ON_count & 0xFF; // LEDn_ON_L data[1] = (ON_count >> 8) & 0x0F; // LEDn_ON_H data[2] = OFF_count & 0xFF; // LEDn_OFF_L data[3] = (OFF_count >> 8) & 0x0F; // LEDn_OFF_H // Write data to the PCA9685 HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDR, reg_base, I2C_MEMADD_SIZE_8BIT, data, 4, HAL_MAX_DELAY); }
First, check if the duty cycle is beyond the 100, if it is, set it to 100 as following:
if(dutyCycle>100)dutyCycle=100;
If the channel number is beyond the 15, exit the channel:
if (ch>CH15) return;
We start by setting the On count to 0 and off count to be the desired duty cycle as following:
uint16_t ON_count = 0; // Start at 0 uint16_t OFF_count = (uint16_t)((dutyCycle * 4096) / 100);
Next, we shall calculate the address as following:
// Calculate register addresses uint8_t reg_base = PCA9685_LED0_ON_L + 4 * ch;
Start by the LED0_ON_L address and multiply the channel by 4 then add it to the address and you will get the desired address.
Convert the duty cycle 4 bytes as following:
uint8_t data[4]; data[0] = ON_count & 0xFF; // LEDn_ON_L data[1] = (ON_count >> 8) & 0x0F; // LEDn_ON_H data[2] = OFF_count & 0xFF; // LEDn_OFF_L data[3] = (OFF_count >> 8) & 0x0F; // LEDn_OFF_H // Write data to the PCA9685 HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDR, reg_base, I2C_MEMADD_SIZE_8BIT, data, 4, HAL_MAX_DELAY);
Hence, the entire source file as following:
#include "PCA9685.h" #include "main.h" #include "i2c.h" static uint8_t PCA9685_ADDR = 0x40<<1; void PCA9685SetAddr(uint8_t add) { PCA9685_ADDR=add<<1; } void PCA9685_Init(uint16_t freq) { uint8_t data[2]; data[0] = PCA9685_MODE1; // MODE1 register data[1] = 0x30; // Auto-increment and normal mode sleep on oscillator off HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100); float prescaleval = ((FREQUENCY_OSCILLATOR / (freq * 4096.0))); if (prescaleval < PCA9685_PRESCALE_MIN) prescaleval = PCA9685_PRESCALE_MIN; if (prescaleval > PCA9685_PRESCALE_MAX) prescaleval = PCA9685_PRESCALE_MAX; uint8_t prescale = (uint8_t)prescaleval-1; data[0] = PCA9685_PRESCALE; // PRE_SCALE register data[1] = prescale; HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100); data[0] = PCA9685_MODE1; // MODE1 register data[1] = 0xA0; HAL_I2C_Master_Transmit(&hi2c1, (PCA9685_ADDR), data, 2, 100); } void PCA9685_SetPWMDutyCycle(PCA9685_ChannelTypedef ch, uint8_t dutyCycle) { if(dutyCycle>100)dutyCycle=100; if (ch>CH15) return; uint16_t ON_count = 0; // Start at 0 uint16_t OFF_count = (uint16_t)((dutyCycle * 4096) / 100); // Calculate register addresses uint8_t reg_base = PCA9685_LED0_ON_L + 4 * ch; // Prepare data for the ON and OFF registers uint8_t data[4]; data[0] = ON_count & 0xFF; // LEDn_ON_L data[1] = (ON_count >> 8) & 0x0F; // LEDn_ON_H data[2] = OFF_count & 0xFF; // LEDn_OFF_L data[3] = (OFF_count >> 8) & 0x0F; // LEDn_OFF_H // Write data to the PCA9685 HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDR, reg_base, I2C_MEMADD_SIZE_8BIT, data, 4, HAL_MAX_DELAY); }
9. Testing the Library:
Open main.c file.
In user begin includes, include the header file as following:
#include "PCA9685.h"
In user code begin 2, we shall initialize the module to 1KHz and set the first 4 channels as following:
PCA9685_Init(1000); PCA9685_SetPWMDutyCycle(CH0, 25); PCA9685_SetPWMDutyCycle(CH1, 50); PCA9685_SetPWMDutyCycle(CH2, 75); PCA9685_SetPWMDutyCycle(CH3, 40);
Save the project, build it and run it on your board as following:
The screen shot below shows the results.
We are getting near the desired frequency and the duty cycles are identical to what we wrote in the code.
Happy coding 😉
Add Comment