PCA9685 Code Overview (Hardware PWM) - Carleton-SRCL/SPOT GitHub Wiki

Note to the reader - right now, the hardware PWM board is not being used for the thrusters. See this guide for an overview of the software PWM. However, the hardware PWM was left installed in the event that others wish to use it.

This guide will cover the code used for the hardware PWM on all three platforms. This code is located in the Custom_Library folder, under NVIDIA_Jetson. The functions specifically covered here are in the src folder, in the file pca9685_controller.cpp.

Initialization of the PCA9685

image

This section includes several standard libraries and headers that will be used in the program:

  • <iostream.h>: For standard input-output operations in C++.
  • <fcntl.h>: Provides file control options, mainly for opening files using flags.
  • <unistd.h>: Provides access to the POSIX operating system API functions.
  • <sys/ioctl.h>: Provides control methods for device-specific operations.
  • <linux/i2c-dev.h>: Gives access to functions and macros for I2C (Inter-Integrated Circuit) communication in the Linux environment.
  • <cmath.h>: Provides mathematical functions. Specifically, it's included here for the floor function, though it offers many other math functions as well.
image

This section defines several constants related to the PCA9685 PWM controller:

  • PCA9685_MODE1 and PCA9685_MODE2: Addresses of the mode registers of the PCA9685. These registers control various modes of the device.
  • PCA9685_PRESCALE: Address of the prescale register of the PCA9685. This register is used to set the PWM frequency.
  • PCA9685_LED0_ON_L: Address of the low byte of the LED0 ON time register. This is used to control the ON period of channel 0's PWM signal.
  • OSC_CLOCK: The frequency of the oscillator clock used by the PCA9685. The given value of 25 MHz is the typical oscillator frequency for this device.
  • FREQUENCY: The desired PWM frequency. In this code, the desired frequency is set to 24 Hz. The minimum this value can be is 24 Hz, and the maximum is about 1.6 kHz.

PWM Functions

This subsection will cover the different functions:

percentageToPWM

image

The percentageToPWM function takes a floating-point percentage value as its argument and returns its equivalent 16-bit PWM value. The PCA9685 PWM controller operates on a 12-bit resolution, meaning it provides PWM values ranging from 0 to 4095.

The input is a floating-point value representing the desired duty cycle in percentage format (e.g., 50.0 for 50% duty cycle). The function divides the input percentage by 100 to convert it into a decimal representation (e.g., 50.0 becomes 0.5). The decimal value is then multiplied by 4095, the maximum 12-bit value, to determine the equivalent PWM value. The resulting floating-point number is type-casted to a 16-bit unsigned integer using static_cast<uint16_t> to ensure an appropriate type for return. The function then returns the computed PWM value in 16-bit format.

readRegister

image

The readRegister function is designed to fetch the value of a specified register from an I2C device, in this case, the PCA9685. It communicates with the device by first writing the register address it wants to access, and then reading the data from that register.

The input is an 8-bit value indicating the address of the register you wish to read from the PCA9685. The function divides the input percentage by 100 to convert it into a decimal representation (e.g., 50.0 becomes 0.5). Using the write function, the code attempts to send the desired register address (reg) to the PCA9685 through the I2C bus. If the write operation fails (i.e., if the return value isn't 1), it prints an error message indicating a failure in writing to the I2C device. Next, the function attempts to read the value stored in the specified register. This is achieved using the read function, which reads one byte from the I2C device and stores it in the value variable. If the read operation fails (i.e., if the return value isn't 1), an error message is printed, indicating a failure in reading from the I2C device. The function returns the 8-bit value stored in the specified register.

writeRegister

image

The writeRegister function is intended to set a value in a specific register of the PCA9685 over the I2C interface. It writes the given value to the specified register on the device.

There are two inputs: An 8-bit value indicating the address of the register you wish to write to on the PCA9685 (reg), and value, the 8-bit value that you want to write/store in the specified register. An array named data of size 2 bytes is then initialized. The first byte (data[0]) is set to the register address (reg), and the second byte (data[1]) is set to the value you want to write. The write function is then called, aiming to send both the register address and the desired data value to the PCA9685 through the I2C bus. If the write operation fails (specifically, if two bytes aren't successfully written), an error message is displayed, indicating a failure in writing to the I2C device.

setPWMFrequency

image

The setPWMFrequency function is designed to adjust the PWM frequency of the PCA9685 chip. The function sets a new PWM frequency by calculating and updating the prescale register value on the PCA9685.

The function calculates the appropriate prescale value based on the desired PWM frequency (FREQUENCY) and the PCA9685's oscillator clock (OSC_CLOCK). The calculated prescale value is rounded to the nearest integer to ensure it's a valid value for the PCA9685_PRESCALE register. The PCA9685 is put into sleep mode by writing 0x10 to the PCA9685_MODE1 register. This is necessary as the prescale register can only be updated when the chip is in sleep mode. The prescale value is then written to the PCA9685_PRESCALE register. The PCA9685 is woken up from sleep mode by writing 0x00 to the PCA9685_MODE1 register. A short delay (usleep(5000)) is implemented to give the internal oscillator time to stabilize. Finally, the PCA9685 is restarted by writing 0x80 to the PCA9685_MODE1 register.

initPCA9685

image

The initPCA9685 function initializes and configures the PCA9685 PWM chip for use via the I2C protocol.

The I2C device file path (/dev/i2c-8) and PCA9685's I2C address (0x40) are defined. An attempt is made to open the I2C device using the open system call with read and write permissions. If successful, the file descriptor is stored in the global variable FD_PCA. If the I2C device can't be opened, an error message is displayed. The ioctl function is then called with the I2C_SLAVE flag to set the I2C slave device address for the opened file descriptor. If this operation is unsuccessful, an error message is displayed, and the I2C device is closed. The PCA9685 chip is reset by writing the 0x80 (Restart) command to the PCA9685_MODE1 register. A confirmation message is printed to indicate that the I2C device was successfully opened. The setPWMFrequency function is called to configure the PWM frequency for the PCA9685 chip. The file descriptor FD_PCA is returned, which can be used for further I2C communications.

setPWM

image

The setPWM function configures a specific PWM channel of the PCA9685 chip to a desired duty cycle by setting the ON and OFF counts.

The function has three inputs:

  • channel: The channel (0-15 for PCA9685) for which the PWM values are to be set.
  • on: The ON count for the PWM signal. A 16-bit value.
  • off: The OFF count for the PWM signal. A 16-bit value.

The function calculates the appropriate address for the ON count (low byte) by adding the base address PCA9685_LED0_ON_L with the offset (4 * channel). The ON count (low byte) is written to this calculated address. The ON count (high byte) is calculated by bit-shifting the on value right by 8 bits and written to the address incremented by 1. Similarly, the OFF count (low byte) is calculated and written to the address incremented by 2. Similarly, the OFF count (low byte) is calculated and written to the address incremented by 2.

commandPWM

image

The commandPWM function takes in 8 floating point values, each representing the desired PWM duty cycle as a percentage (from 0% to 100%) for the channels 1 through 8 of the PCA9685 chip. It then sets the appropriate ON and OFF counts for each channel to achieve the desired duty cycle.

The function has 8 inputs:

  • PWM1 to PWM8: Floating point values (in percentage) indicating the desired duty cycle for channels 1 to 8 respectively.

The input duty cycle percentages for all 8 channels are stored in the duties array. For each channel (from 0 to 7, corresponding to channels 1 to 8):

  1. The desired duty cycle percentage is converted to an equivalent ON count using the percentageToPWM function. This count represents the number of counts (out of a possible 4096) the PWM signal should remain high.
  2. The setPWM function is called with the channel number, an ON count of 0 (meaning the PWM signal starts from low), and the calculated ON count. This effectively sets the PWM signal to start from a low level, go high after 0 counts, and go low again after the calculated count, completing one PWM cycle.

closePCA9685

image

The closePCA9685 function is responsible for closing the I2C connection to the PCA9685 device. It is a cleanup function that ensures the I2C device file descriptor is closed properly when it's no longer needed.

MATLAB Device Driver

Here is the complete MATLAB Device Driver used to control the hardware:

classdef PWM_Write < matlab.System & coder.ExternalDependency
	%
	% System object template for a GPIO_Write block.
	% 
	% This template includes most, but not all, possible properties,
	% attributes, and methods that you can implement for a System object in
	% Simulink.
	%
	
	% Copyright 2021 The MathWorks, Inc.
	%#codegen
	%#ok<*EMCA>
	
	properties
		% Specify custom variable names

	end 
	
	properties (Nontunable)
		% Public, non-tunable properties.
	end
	
	methods
		% Constructor
		function obj = PWM_Write(varargin)
			% Support name-value pair arguments when constructing the object.
			setProperties(obj,nargin,varargin{:});
		end
	end

	methods (Access=protected)
		function setupImpl(obj) %#ok<MANU>
			if isempty(coder.target)
				% Simulation setup code
			else
				coder.cinclude('pca9685_controller.h');
				coder.ceval('initPCA9685');
			end
		end
		
		function stepImpl(obj, u1,u2,u3,u4,u5,u6,u7,u8)
			if isempty(coder.target)
				% Simulation output code
			else
				coder.cinclude('pca9685_controller.h');
				coder.ceval('commandPWM', u1, u2, u3, u4, u5, u6, u7, u8);
			end
		end
		
		function releaseImpl(obj) %#ok<MANU>
			if isempty(coder.target)
				% Simulation termination code
			else
				% Termination code for PCA9685
				coder.cinclude('pca9685_controller.h');
				coder.ceval('commandPWM', 0, 0, 0, 0, 0, 0, 0, 0);
				coder.ceval('closePCA9685');
			end
		end
	end
	
	methods (Access=protected)
		%% Define input properties
		function num = getNumInputsImpl(~)
			num = 8;
		end
		
		function num = getNumOutputsImpl(~)
			num = 0;
		end
		
		function flag = isInputSizeMutableImpl(~,~)
			flag = false;
		end
		
		function flag = isInputComplexityMutableImpl(~,~)
			flag = false;
		end
		
		function validateInputsImpl(~, u)
			if isempty(coder.target)
				% Run input validation only in Simulation
				validateattributes(u,{'double'},{'scalar'},'','u');
			end
		end
		
		function icon = getIconImpl(~)
			% Define a string as the icon for the System block in Simulink.
			icon = 'PWM_Write';
		end
	end
	
	methods (Static, Access=protected)
		function simMode = getSimulateUsingImpl(~)
			simMode = 'Interpreted execution';
		end
		
		function isVisible = showSimulateUsingImpl
			isVisible = false;
		end
		
		function header = getHeaderImpl
			header = matlab.system.display.Header('GPIO_Write','Title',...
				'Debugging Block','Text',...
				['This block allows you to control the PWM pins on the PCA9685 board via I2C.' ...
				' The inputs are the duty cycle commands for thrusters 1-8 in order (input #1 is thruster #1).']);
		end
		
	end
	
	methods (Static)
		function name = getDescriptiveName()
			name = 'PWM_Write';
		end
		
		function b = isSupportedContext(context)
			b = context.isCodeGenTarget('rtw');
		end
		
		function updateBuildInfo(buildInfo, context)
			if context.isCodeGenTarget('rtw')
				srcDir = fullfile(fileparts(mfilename('fullpath')),'src');
				includeDir = fullfile(fileparts(mfilename('fullpath')),'include');
				addIncludePaths(buildInfo,includeDir);
				% Add the source and header files to the build
				addIncludeFiles(buildInfo,'pca9685_controller.h',includeDir);
				addSourceFiles(buildInfo,'pca9685_controller.cpp',srcDir); % Assuming .cpp extension
			end
		end
	end
end

The points to highlight are the setupImpl, the stepImpl, and the releaseImpl. These are highlighted below:

function setupImpl(obj) %#ok<MANU>
	if isempty(coder.target)
		% Simulation setup code
	else
		coder.cinclude('pca9685_controller.h');
		coder.ceval('initPCA9685');
	end
end

function stepImpl(obj, u1,u2,u3,u4,u5,u6,u7,u8)
	if isempty(coder.target)
		% Simulation output code
	else
		coder.cinclude('pca9685_controller.h');
		coder.ceval('commandPWM', u1, u2, u3, u4, u5, u6, u7, u8);
	end
end

function releaseImpl(obj) %#ok<MANU>
	if isempty(coder.target)
		% Simulation termination code
	else
		% Termination code for PCA9685
		coder.cinclude('pca9685_controller.h');
		coder.ceval('commandPWM', 0, 0, 0, 0, 0, 0, 0, 0);
		coder.ceval('closePCA9685');
	end
end 

In brief, the setup function calls the initPCA9685 C++ code to initialize the PCA9685. This is only run ONCE at the start of the code execution. The stepImpl is called every time step, and sends the latest duty cycle commands from the Simulink diagram. Lastly, the releaseImpl function closes the PCA9685 connection and sets the duty cycles to zero to ensure they thrusters are off.

Click here to go HOME

⚠️ **GitHub.com Fallback** ⚠️