Creating a Library for PCA9685 PWM module Part 2: Creating the library

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

Your email address will not be published. Required fields are marked *