Jump to content
43oh

16 channel software PWM using a single timer


Recommended Posts

This is a method of generating multichannel PWM using a single timer.

A linked list of events is used to allow a very efficient ISR and very low CPU usage.

This is the linked list entry structure...

typedef struct { // PWM ISR info struct
unsigned time; // Time for on/off action to occur
unsigned port_off; // Port pins to turn off
unsigned port_on; // Port pins to turn on
void *next; // Next entry in linked list
} TPWM; //

...and the ISR...
(written in C for clarity, using assembly would reduce the size and execution time by half)

__interrupt void Timer0_A1 (void)
{
volatile unsigned x = TAIV; // Clear interrupt flag
P1OUT &= ~pa->port_off; // Port pins off
P2OUT &= ~pa->port_off >> 8; //
P1OUT |= pa->port_on; // Port pins on
P2OUT |= pa->port_on >> 8; //
pa = pa->next; // Next entry in list
TACCR1 = pa->time; // Update timer compare time
}

When an interrupt occurs, the ports are updated, the timer is updated, and the next linked list entry is made the active entry. An interrupt will only occur when port pin(s) have to be changed. Very simple and efficient.

The tradeoff is a rather complex chunk of code to maintain the linked list. The code shown here uses a simple, but rather crude, method of reducing glitches that can occur when the linked list is manipulated. The code will wait for the current pulse to complete before removing the list entry for it. There are better ways to do this, but the code is much more complex due to the many corner cases that must be handled. There is no disruption in the PWM output when PWM values are changed. All PWM outputs are active as long as the timer and global interrupt are enabled.

The PWM period is 2^16 / SMCLK and the resolution is 2^8 / SMCLK (8 bit resolution).
So for a 16 MHz clock:
period = 65,536 / 16,000,000 = 4.096 ms
resolution = 256 / 16,000,000 = 16 us
frequency = 16,000,000 / 65,536 = 244.24 Hz

An interrupt will occur no more than 17 times the base frequency when all 16 channels are active and set to different non-zero/non-max values. So at 16 MHz there will be no more than 4151 interrupts per second. Assuming 100 cycles per interrupt (the C compiler should be able to do better than that) would be 415,100 cycles per second of 16,000,000 or 2.6% CPU usage (worst case).

The resolution can be increased as long as there are not other interrupts that use too many cycles. Consider the worst case scenario of two back-to-back PWM interrupts with another interrupt in between. The maximum time allowed for other interrupts is the PWM resolution minus the PWM ISR time - it has to squeeze in between back-to-back PWM interrupts.

Complete code with test case...

#include "msp430g2553.h"
#include "string.h"

typedef struct { // PWM ISR info struct
unsigned time; // Time for on/off action to occur
unsigned port_off; // Port pins to turn off
unsigned port_on; // Port pins to turn on
void *next; // Next entry in linked list
} TPWM; //

TPWM pw[17], *pi; // Array and inactive list head
volatile TPWM *pa; // Active list entry

void pwm_set(const unsigned pin, const unsigned time)
{
const unsigned mask = 1 << pin; //
const unsigned tm = time & 0xFF00; // Limit closeness of entries
TPWM *b, *p, *n; //
//
// -- Try to find existing active entry for this pin
for(b = &pw[0], p = b->next; p != &pw[0]; b = p, p = b->next) {
if(p->port_off & mask) { // Found it
if(p->time == tm) return; // - Time is the same, nothing to do, return...
while(pa != p); // Wait for this entry to be active
while(pa == p); // Wait for this entry to become inactive
// Safe to remove now
if(p->port_off == mask) { // - Entry is only used for this pin, remove it
b->next = p->next; // Remove link
p->next = pi; // Add to inactive list
pi = p; //
} else { // - Entry is used for multiple pins
p->port_off &= ~mask; // Remove this pin from the entry
} //
break; // Found the pin, so stop searching
} //
} //
// - Update first entry in list
if(tm == 0) { // If time is 0, turn off and return
pw[0].port_on &= ~mask; //
pw[0].port_off |= mask; //
return; //
} else { // If time is non-zero, turn on
pw[0].port_on |= mask; //
pw[0].port_off &= ~mask; //
if(time == 0xFFFF) return; // If max, no need to turn off, so return
} //
//
// Find where the new turn off entry will go
for(b = &pw[0], p = b->next; p != &pw[0]; b = p, p = b->next)
if(p->time >= tm) break; // Stop when an entry of >= time is found
//
if(p->time == tm) { // If same time, use existing entry
p->port_off |= mask; // Add this pin
return; // All done...
} //
//
n = pi; // Get an entry from the inactive list
pi = n->next; //
//
n->port_off = mask; // Setup new entry
n->port_on = 0; //
n->time = tm; //
//
n->next = p; // Insert in to active list
b->next = n; //
}

void pwm_detach(unsigned pin)
{
const unsigned mask = 1 << pin;
TPWM *b, *p;

// Try to find existing active entry for this pin
for(b = &pw[0], p = b->next; p != &pw[0]; b = p, p = b->next) {
if(p->port_off & mask) { // Found it
if(p->port_off == mask) { // Entry is only used for this pin, remove it
_DINT();
if(pa == p) pa = p->next;
b->next = p->next;
_EINT();
p->next = pi;
pi = p;
} else {
p->port_off &= ~mask; // Remove this pin from the entry
}
break;
}
}
pw[0].port_on &= ~mask; // Don't turn on or off
pw[0].port_off &= ~mask;
}

void pwm_init(void)
{
unsigned n;
memset(pw, 0, sizeof(pw)); // Clear entire array
pa = &pw[0]; // Active list always begins with first array element
pa->next = &pw[0]; // Begin with only 1 entry in list
pi = &pw[1]; // First inactive entry is second array element
for(n = 1; n < sizeof(pw)/sizeof(TPWM) - 1; ++n) // Link the inactive entries
pw[n].next = &pw[n + 1]; //
//
TACTL = TASSEL_2 + MC_2 + ID_0; // Setup timer, continuous mode, SMCLK/1
TACCTL1 = CCIE; // Enable timer interrupt
_EINT(); // Enable interrupts
}

#pragma vector = TIMER0_A1_VECTOR
__interrupt void Timer0_A1 (void)
{
volatile unsigned x = TAIV; // Clear interrupt flag
P1OUT &= ~pa->port_off; // Port pins off
P2OUT &= ~pa->port_off >> 8; //
P1OUT |= pa->port_on; // Port pins on
P2OUT |= pa->port_on >> 8; //
pa = pa->next; // Next entry in list
TACCR1 = pa->time; // Update timer compare time
}

// Test case for 2 channels - Launchpad LEDs
void main(void)
{
unsigned n, o;

WDTCTL = WDTPW | WDTHOLD; // Disable watchdog
DCOCTL = 0; // Run at 16 MHz
BCSCTL1 = CALBC1_16MHZ; //
DCOCTL = CALDCO_16MHZ; //

P1DIR = BIT0 | BIT6; // Enable outputs for LEDs on Launchpad

pwm_init(); // Initialize software PWM

for(;;-) {
pwm_set(0, n);
pwm_set(6, o);
n += 100;
o += 123;
__delay_cycles(100000);
}
}

// Test case for all 16 channels
void main(void)
{
unsigned n;
unsigned w[16];
unsigned r[16] = { 100, 103, 106, 109, 112, 115, 118, 121, 124, 127, 130, 133, 136, 139, 142, 145 };

WDTCTL = WDTPW | WDTHOLD; // Disable watchdog
DCOCTL = 0; // Run at 16 MHz
BCSCTL1 = CALBC1_16MHZ; //
DCOCTL = CALDCO_16MHZ; //
P2SEL = 0; // Allow P2.6 and P2.7 to be used as GPIO

P1DIR = P2DIR = 0xFF; // Enable all P1 and P2 pins as output

pwm_init(); // Initialize software PWM

for(;;-) {
for(n = 0; n < 16; ++n) {
pwm_set(n, w[n]);
w[n] += r[n];
}
__delay_cycles(100000);
}
}

Two PWM outputs with the pulse with changing at different rates (lower trace is faster). The two outputs are the Launchpad LEDs.



All 16 channels.

Link to post
Share on other sites

Why can't you just use a simple array?

 

For clarity, number of channels was reduced to 8, but any number of channels is possible.

#include "msp430g2553.h"

char pwms[8];						// array of pwm values
char counter = 0;					// PWM counter
char index = 0;
int pwmChannel = 0;

void main(void) {

   WDTCTL = WDTPW + WDTHOLD;			// disable WDT

   P2SEL &= ~(BIT6|BIT7);				// port 2 all out
   P2OUT = 0;
   P2DIR = 0xFF;

   CCTL0 = CCIE;						// setup timer
   CCR0 = 1000;						// PWM resolution
   TACTL = TASSEL_2 + MC_1;			// SMCLK, upmode

   index = 0;
   while(index < 8) {					// clear PWM
       pwms[index] = 0;
       index++;
   }

   __bis_SR_register(GIE);				// enable global

   while(1) {							// main loop
       index = 0;
       pwmChannel = 1;					// first pwm on port 2 (P2.0-P2.7)
       while(index < 8) {				// loop through pwms 0-7
           if((pwms[index] >= counter)) {// when value greater or equals PWM counter...
               P2OUT |= pwmChannel ;	// turn bit on
           } else {
               P2OUT &= ~pwmChannel ;	// else, turn it off
           }
           index++;					// next pwm
           pwmChannel <<= 1;			// next channel on port 2
       }
       __bis_SR_register(LPM0_bits);	// go to sleep and wait for timer
   }
}

#pragma vector = TIMER0_A0_VECTOR
__interrupt void Timer_A0 (void) {
   counter++;						// increment PWM counter
   if(counter == 100) {
       counter = 0;				// reset PWM counter
   }
   __bic_SR_register_on_exit(LPM0_bits);	// wake up and update
}

Link to post
Share on other sites

That has *much* higher CPU usage. You are checking every PWM value on every timer tick.

 

My code only interrupts when one or more outputs need to change. The ISR is very short and to the point because all of the work to be done has been predetermined by mainline code. CPU usage is very low. Worst case is aprox 4000 interrupts per second with all 16 channels active and set to different PWM values. That is under 2% CPU utilization for 16 channels of 244 Hz 8 bit PWM.

Link to post
Share on other sites
  • 1 year later...

Firstly, many thanks for this code, I've never though about implementing multiple soft PWM's like this!  Great idea.

 

I've been playing with the code, but I've now come up against something which I can't figure out how to fix.

 

Junk compiler code generation.

 

Despite my best efforts (and the variable being defined as volatile!!!) , when using code composer studio and compiling the code in "release mode", the following lines:

while(pa != p);                 // Wait for this entry to be active
while(pa == p);

get compiled to:

$C$L2:
    CMP.W   &p,&pa
    JNE     ($C$L2)
$C$L3:
    JMP     ($C$L3)

As you can see, the first loop is correct, but the second one is horribly horribly wrong! (or optimised, depending on your point of view!)

 

Any ideas why the compiler is doing this, even though pa is defined as volatile it seems to have still done an optimisation which is based on the != contradicting the == and therefore the == always being true, which is not the case as pa is volatile!

 

Any ideas on how to stop the compiler being "so smart" here or how to get it to generate the correct code?

 

Cheers.

 

Adrian

Link to post
Share on other sites

My brain is frazzled, I realised after typing that lot out that the solution was easy.

 

inline assembler

__asm("neloop:\n"
      " cmp.w &p,&pa\n"
      " jne neloop");

__asm("eloop:\n"
      " cmp.w &p,&pa\n"
      " jeq eloop");

That's the solution for the CCS compiler, replace the two "C" lines with those assembler lines because we know better than the compiler!

 

Hopefully that's of use to somebody!

 

Adrian

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...