oPossum 1,083 Posted January 25, 2012 Share Posted January 25, 2012 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 msresolution = 256 / 16,000,000 = 16 usfrequency = 16,000,000 / 65,536 = 244.24 HzAn 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. timotet, fj604, dkedr and 10 others 13 Quote Link to post Share on other sites
zeke 693 Posted January 25, 2012 Share Posted January 25, 2012 Nice! Quote Link to post Share on other sites
timotet 44 Posted January 25, 2012 Share Posted January 25, 2012 great work! Is that dark side of the moon playing in the back ground? Quote Link to post Share on other sites
bluehash 1,581 Posted January 26, 2012 Share Posted January 26, 2012 great work!Is that dark side of the moon playing in the back ground? Yes. Eclipse. Nice work oPossum. Quote Link to post Share on other sites
fatihinanc 14 Posted January 26, 2012 Share Posted January 26, 2012 Great Work. Just a question : Which compiler and IDE are you using ? Best Quote Link to post Share on other sites
oPossum 1,083 Posted January 26, 2012 Author Share Posted January 26, 2012 I am using CCS 4.2.3 I think GCC and IAR would just require changing the ISR declaration. Quote Link to post Share on other sites
fatihinanc 14 Posted January 26, 2012 Share Posted January 26, 2012 I am using CCS 4.2.3 I think GCC and IAR would just require changing the ISR declaration. Thanks. I think the majority of forum members are using the CCS. I am using the IAR IDE. I wonder : CCS or IAR most useful ? Best. Quote Link to post Share on other sites
RobG 1,892 Posted January 26, 2012 Share Posted January 26, 2012 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 } Quote Link to post Share on other sites
oPossum 1,083 Posted January 26, 2012 Author Share Posted January 26, 2012 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. Quote Link to post Share on other sites
RobG 1,892 Posted January 26, 2012 Share Posted January 26, 2012 Got it, you are basically calculating when the next change happens, so you have to interrupt maximum 16 times per cycle. Quote Link to post Share on other sites
Mac 67 Posted January 27, 2012 Share Posted January 27, 2012 Please, do you suspend PWM output when executing pwm_set()? PWM period is 244 Hz? PWM pulse width resolution is ~16 usecs? Quote Link to post Share on other sites
oPossum 1,083 Posted January 28, 2012 Author Share Posted January 28, 2012 Good questions. I have updated the first post with answers and a video of all 16 PWM outputs active at once. Quote Link to post Share on other sites
sn00p 2 Posted August 22, 2013 Share Posted August 22, 2013 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 Quote Link to post Share on other sites
sn00p 2 Posted August 22, 2013 Share Posted August 22, 2013 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 Quote Link to post Share on other sites
sn00p 2 Posted August 22, 2013 Share Posted August 22, 2013 Well I never, I was offered a solution by somebody else.... instead of: volatile TPWM *pa; // Active list entry the actual declaration should be: volatile TPWM * volatile pa; // Active list entry This fixes the issue. bluehash and oPossum 2 Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.