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.
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. On every interrupt an accumulator variable is incremented from 0 to 255 and each time is compared against a PWM channel. At the beginning of the cycle the pins are set high and when the accumulator equals to a channel's set value then the specific pin is set low. On 16MHz CPU the ISR takes between 1.75 to 3.8 us with 3 channels to execute the code. The size of the accumulator variable dictates the PWM resolution and it is set to 8 bits for optimum code execution time.
Calculate the time between each interrupt
f_isr = F_CPU / (PRESCALER * (OCR + 1))
Calculate PWM base frequency
f_PWM = F_CPU / (256 * 256)
or
f_PWM = f_isr / (OCR + 1)
For example on 8MHz CPU the frequency will be 8000000 / (256 * 256) = 122 Hz. 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.
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
Characteristics
- Language: C.
- PWM generation: using timer interrupts.
- Supported devices: tested on ATmega328PB and ATtiny402. Class 1 AVR devices (UDPI devices) supported.
Using the software PWM library
Library configuration
Selecting the timer
#define SOFTPWM_TIMER SOFTPWM_TIMER1
Available constants:
#define SOFTPWM_TIMER0 0 // Classic Timer0 #define SOFTPWM_TIMER1 1 // Classic Timer1 #define SOFTPWM_TIMER2 2 // Classic Timer2 #define SOFTPWM_TIMER_TCA0 10 // UPDI TCA0 #define SOFTPWM_TIMER_TCB0 11 // UPDI TCB0
Logarithmic values
#define USE_LOGARITHMIC_ARRAY 0
Used mainly for led control. Setting this to 1 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.
Channel to pin mapping
#define SOFTPWM_CH0_PORT PORT_C #define SOFTPWM_CH0_PIN 1
There are 10 channels defined. Each channel maps to a port and pin. Ports can be: PORT_A, PORT_B, PORT_C, PORT_D, PORT_E. These are defined by the library as an integer and used internally to map to a real port based on AVR architecture.
API
Initialize the library
void softwarePWM_Init(void)
Sets pins to output, configures the specified timer and enables global interrupts.
Set duty cycle
void softwarePWM_Set(uint8_t channel, uint8_t value)
Sets the duty cycle for a specific PWM channel. Configures the target channel compare buffer. The value will take effect at the start of the next PWM period.
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:
Duty cycle as a raw 8-bit compare match value (0 = 0%, 255 = 100%). To convert a percentage from 0 to 100 to a raw value use softwarePWM_PercentToCompare().
Convert duty cycle
uint8_t softwarePWM_PercentToCompare(uint8_t percent)
Converts a duty cycle percentage (0-100%) to a raw PWM compare value (0-255). Return value can be used as an argument to softwarePWM_Set().
percent:
Duty cycle percentage (0 to 100).
return:
Equivalent 8-bit compare register value.
Stopping all PWM channels
void softwarePWM_Pause(void)
Disables the timer and the interrupt.
Re-enabling the PWM channels
void softwarePWM_Resume(void)
Enables the timer and the interrupt.
Convert linear to logarithmic PWM values
uint8_t softwarePWM_LineartoLog(uint8_t dutyCycle)
dutyCycle:
Value between 0 and 255.
return:
A logarithmic value using an array with precalculated values using CIE 1931 formula. 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.
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
int main(void){ // Start RED at max (255) so fade_down works cleanly uint8_t rgb[NUM_COLORS] = {255, 0, 0}; uint8_t fade_up = GREEN; uint8_t fade_down = RED; softwarePWM_Init(); while(1){ softwarePWM_Set(0, rgb[RED]); softwarePWM_Set(1, rgb[GREEN]); softwarePWM_Set(2, rgb[BLUE]); // OR: with linear to log conversion //softwarePWM_Set(0, softwarePWM_LineartoLog(rgb[RED])); //softwarePWM_Set(1, softwarePWM_LineartoLog(rgb[GREEN])); //softwarePWM_Set(2, softwarePWM_LineartoLog(rgb[BLUE])); // Adjust color values rgb[fade_up]++; rgb[fade_down]--; // Reached top of fading up color, hand off channels to the next color if (rgb[fade_up] == 255) { fade_down = fade_up; fade_up++; if (fade_up > BLUE) { fade_up = RED; } } _delay_ms(10); } return 0; }
Links
| v2.0 | |
| softwarePWM.h | |
| softwarePWM.c | |
| Changelog | |
| v2.0 |
12-08-2026: - Simplified API. - Changed logarithmic array to use CIE 1931 formula. - Added support for UPDI devices. |


No comments:
Post a Comment