Wednesday, February 3, 2021

Multi-channel software PWM library for AVR microcontrollers | ATmega328P

What you do when you run out of PWM pins on hardware? You make software PWM of course. This library is based on "AVR136: Low-Jitter Multi-Channel Software PWM" application note. It supports up to 10 PWM channels (more can be added) and it's suitable for led dimming, DC motor control and RGB led controller.

Multi-channel software PWM library for AVR microcontrollers | ATmega328P

Since this method is already explained in the AVR136 app note I won't go in to too many details. So the basic principles behind software PWM are this. A timer interrupt is set to trigger every 256 system clocks. For 8 bit timers the interrupt is on overflow and for 16 bit timers is on compare match. A 16 bit timer has the advantage that the base frequency can be modified. On every interrupt a variable "softcount" is incremented from 0 to 255 and each time is compared against each PWM channel. At the beginning of the cycle the pins are set high and when "softcount" equals to a channel's set value then the specific pin is set low. On 8MHz CPU the ISR takes between 4 and 10us to execute the code depending on how many channels and on how many ports there are. The size of the "softcount" variable dictates the PWM resolution and it is set to 8 bits. 

Calculate the time between each interrupt

(1 / F_CPU) * 256

256 is the overflow value for an 8 bit timer or the OCRA value for a 16 bit timer.

Calculate PWM base frequency

F_CPU / (256 * 256)

For example on 8MHz CPU the frequency will be 8000000 / (256 * 256) = 122 Hz. With a 16 bit timer one of the 256 can be replaced with a random value such as 356 in which case the base frequency will drop to 87.7 Hz. I have tested this frequency with some RGB leds and didn't notice any flickering. Having a lower frequency means that the interrupt will trigger at a slower rate which is good if you have other interrupts or many things for the CPU to do.

Software PWM in PulseView

Another type of PWM called Binary Code Modulation (BCM) can be found here https://www.programming-electronics-diy.xyz/2021/01/binary-code-modulation-bcm-aka-bit.html

The main advantage of the BCM is that it requires very low processing power compared to soft PWM however the drawback of the BCM is when it is used for fading leds such as RGB. During the transition between 127 and 128 there will be a short blink. Software PWM doesn't have this issue.

Using the software PWM library

Selecting the timer

#define SOFTPWM_TIMER                   SOFTPWM_TIMER1

Can be SOFTPWM_TIMER0, SOFTPWM_TIMER1 or SOFTPWM_TIMER2.

Changing PWM base frequency

#define SOFTPWM_TIMER1_OCR		255

OCRA value for timer 1. If timer 1 is used this can be used to change the frequency. A higher value means a lower frequency.

Number of PWM channels

#define SOFTPWM_CHMAX			3

Maximum 10. How many PWM channels.

Logarithmic PWM

#define USE_LOGARITHMIC_ARRAY		FALSE

Setting this to TRUE will include an 8 bit logarithmic array which can be used with the function described below to convert between a linear 8 bit value (0 - 255) to a logarithmic value. More details can be found on the link about Binary Code Modulation. Using a logarithmic brightness with an RGB led can produce better colors in my opinion.

Mapping channels to pin and ports

#define SOFTPWM_CHANNEL0_DDR			DDRC
#define SOFTPWM_CHANNEL0_PORT			PORTC
#define SOFTPWM_CHANNEL0_PIN			4
#define SOFTPWM_CHANNEL0_PIN_STATES		pinlevelC

For each used channel from 0 to n must be set the DDR, PORT and pin number where the PWM will be output. The default values are just as an example. "pinlevel" in an important variable and must be terminated with the port letter. For example pinlevelC for port 'C'. If a channel is not used all values can be set to 0.

Which ports are used

#define SOFTPWM_ON_PORT_B			TRUE
#define SOFTPWM_ON_PORT_C			TRUE
#define SOFTPWM_ON_PORT_D			FALSE
#define SOFTPWM_ON_PORT_E			TRUE

If a PWM pin is on that port set it to TRUE. Set FALSE if the port is not used.

What pins are on this ports

#define SOFTPWM_PINS_PORTB			_BV(5)
#define SOFTPWM_PINS_PORTC			_BV(4)
#define SOFTPWM_PINS_PORTD			FALSE
#define SOFTPWM_PINS_PORTE			_BV(3) | _BV(2)

Set the pin numbers used on each port using the _BV() macro. The order of the pins is not important. Multiple pins must be or'ed  using | between _BV().

Initialize the library

softwarePWM_Init()

This function will set pins to output, set up the specified timer and enable global interrupts.

Using a software PWM channel

softwarePWM_Set(channel, value)

channel - the PWM channel for which to set the duty cycle. The channel starts from 0 to the maximum number of channels minus 1. For 3 channels this parameter would be from 0 to 2.

value - the duty cycle value from 0 to 255. This is not a percentage.

Stopping all PWM channels

softwarePWM_Pause()

Disables the timer and the interrupt.

Re-enabling the PWM channels

softwarePWM_Resume()

Enables the timer and the interrupt.

Convert between linear to logarithmic PWM values 

softwarePWM_LineartoLog(dutyCycle)

dutyCycle -  is a PWM value between 0 and 255.

The function returns a logarithmic value using an array with precalculated values. It can be used in fading or dimming leds because it gives a more linear brightness versus duty cycle for the eyes even though the brightness curve will be logarithmic. More details can be found at the link about BCM.

Use this code to see the difference in color between linear and logarithmic values:

softwarePWM_Set(0, softwarePWM_LineartoLog(255));
softwarePWM_Set(1, softwarePWM_LineartoLog(128));
softwarePWM_Set(2, softwarePWM_LineartoLog(0));
		
_delay_ms(5000);
		
softwarePWM_Set(0, 255);
softwarePWM_Set(1, 128);
softwarePWM_Set(2, 0);
		
_delay_ms(5000);
 

Example: controlling an RGB led using software PWM

#include <avr/io.h>
#include "softwarePWM.h"

#define RGB_MAX_COLOR_VALUE		255

enum RGB{
	RED,
	GREEN,
	BLUE,
	NUM_COLORS
};

int main(void){
	
	// RGB related
	int16_t RGB_values[] = {255, 0, 0}; // Red, Green, Blue (must be int16_t or int)
	uint8_t RGB_fading_up_color = GREEN;
	uint8_t RGB_fading_down_color = RED;
	const uint8_t RGB_fade_step = 1;
	
	softwarePWM_Init();
	
	while(1){

		softwarePWM_Set(0, RGB_values[RED]);
softwarePWM_Set(1, RGB_values[GREEN]);
softwarePWM_Set(2, RGB_values[BLUE]); RGB_values[RGB_fading_up_color] += RGB_fade_step; RGB_values[RGB_fading_down_color] -= RGB_fade_step; // Reached top of fading up color, change to the next one if(RGB_values[RGB_fading_up_color] > RGB_MAX_COLOR_VALUE){ RGB_values[RGB_fading_up_color] = RGB_MAX_COLOR_VALUE; RGB_fading_up_color++; if(RGB_fading_up_color > BLUE) RGB_fading_up_color = RED; } // Reached bottom of fading down color, change to the next one if(RGB_values[RGB_fading_down_color] < 0){ RGB_values[RGB_fading_down_color] = 0; RGB_fading_down_color++; if(RGB_fading_down_color > BLUE) RGB_fading_down_color = RED; } _delay_ms(10); } return 0; }

 

If you have any questions or suggestions leave them in the comments.

Download

v1.0

softwarePWM.h