PWM again…

What to do if you favourite microcontroller doesn’t have enough hardware PWM pins? Software driven PWM of course.

There many absolutely dumb ways to do it, and I’ve probably tested all of these. They can be classified as “brute force”. Very cpu intensive, starving the rest of your code.

A better way of doing it is to only do any work if there is anything to do. But you already know all of this of course. Calculate when to toggle the lines and let a timer do all of the work. Code can be found on my blog, which would be right here.

I started a discussion on avrfreaks.net about this. Initially I had trouble with _delay_ms(), which I only inserted for debugging purposes. And all of this was also affected by different optimization levels (-O3 vs. -Os). The execution time of the ISR changes by a factor of 3. As it turns out most (if not all) of my problems were caused by the ISR starving the rest of the code.

So here’s how NOT to do PWM if you have to be conscious of cpu time:

ISR(TIMER1_COMPA_vect)
{ /* Framebuffer interrupt routine */
   uint8_t pwm_cycle;

   __DISPLAY_ON; // only enable the drivers when we actually have time to talk to them

   for (pwm_cycle = 0; pwm_cycle <= __pwm_loop_max; pwm_cycle++) {

      uint8_t led;
      uint8_t red = 0x00; // off

      for (led = 0; led <= 7; led++) {
         if ( pwm_cycle < brightness[led] ) {
            red |= _BV(led);
         }
         else {
            red &= ~_BV(led);
         }
      }

      __LATCH_LOW;
      spi_transfer(red);
      __LATCH_HIGH;

   }

   __DISPLAY_OFF; // we're done with this line, turn the driver's off until next time

} 

This piece of code is looping again and again, constantly checking if the data lines should be high or low. Suffice to say that this takes quite a lot of time. And if you don’t keep the ISR running all the time the duty cycle goes down. In my case this caused the LEDs to go dim.

Now to the “all new” and improved version. Use at your own risk ;-)

ISR(TIMER1_COMPA_vect)
{				/* Framebuffer interrupt routine */
	__LED0_ON;
	__DISPLAY_OFF;
	static uint8_t data = 0;	// init as off
	static uint8_t index = 0;

	/* starts with index = 0 */
	/* now calculate when to run the next time and turn on LED0 */
	if (index == 0) {
		OCR1A =
		    (uint16_t) ((uint32_t) (brightness[index].dutycycle) *
				(uint32_t) (__OCR1A_max) / (uint32_t) (100));
		index++;
	} else if (index == 8) {	// the last led in the row
		data |= _BV(brightness[(index - 1)].number);
		/* calculate when to turn everything off */
		OCR1A =
		    (uint16_t) (((uint32_t) (100) -
				 (uint32_t) (brightness[(index - 1)].
					     dutycycle)) *
				(uint32_t) (__OCR1A_max) / (uint32_t) (100));
		index++;
	} else if (index == 9) {
		/* cycle completed, reset everything */
		data = 0;
		index = 0;
		/* immediately restart */
		OCR1A = 0;
		/* DON'T increase the index counter ! */
	} else {
		/* turn on the LED we deciced to turn on in the last invocation */
		data |= _BV(brightness[(index - 1)].number);
		/* calculate when to run the next time and turn on the next LED */
		OCR1A =
		    (uint16_t) (((uint32_t) (brightness[index].dutycycle) -
				 (uint32_t) (brightness[(index - 1)].
					     dutycycle)) *
				(uint32_t) (__OCR1A_max) / (uint32_t) (100));
		index++;
	}

	__LATCH_LOW;
	spi_transfer(data);
	__LATCH_HIGH;

	__DISPLAY_ON;
	__LED0_OFF;
}

This piece of code uses a hardware timer to trigger an interrupt, which calculates when it should be invoked again and changes the state of the data lines. Basically it only does work when necessary. Total execution time for this one is just 100µs. The crude and dumb ISR used up more than 60% of cpu time, the improved version is happy with just 8%.

The full code (including a timer0 driven system ticker + time/delay function) is here. It’s tailored for an ATtiny 2313.

I’ll have to test if this approach is easily applied to a row multiplexed application, probably not.

This entry was posted in Electronics. and tagged , , , . Bookmark the permalink.

19 Responses to PWM again…

  1. Pingback: Weekly Shared Items – 25. February, 2011 | TOXIN LABS - weblog of a german design student from wuerzburg

  2. Pingback: Optimizing code for PWM efficiency | You've been blogged!

  3. Rando says:

    Cool post! Hey what’s that tiny 20 pin AVR dev board you’re using???
    R.

  4. robert says:

    Thanks.

    It’s from ‘tinkerlog':

    http://store.tinkerlog.com/store/index.php?main_page=product_info&cPath=3&products_id=7

    I replaced the male headers with female ones and upgraded it with an ATtiny4313 (which I only could find on ebay back then).

  5. Pingback: Optimizing code for PWM efficiency « Black Hat Security

  6. Lauri says:

    How do you measure CPU utilization on an AVR chip or Arduino?

    • robert says:

      In this case I use my oscilloscope. At the beginning of a function I set a pin to high and at the end back to low again (using direct port manipulation, not “digitalWrite()”). The resulting waveform is all that is needed. It is not 100% accurate as it only covers the time spent inside a function and not pro/epilogues as well. Running a simulator works as well and is more accurate, but I like using my scope ;-)

  7. Lauri says:

    Good idea :)
    Thanks!

  8. Pingback: hackaholicballa - Optimizing code for PWM efficiency

  9. Greeeg says:

    Is that a 1.9mm led matrix you are using? Where did you buy it from?

    I got 10 of them awhile back but they weren’t diffused, IMO diffused matrices look alot better.

    cool project.

  10. robert says:

    I think I got them from SeeedStudio quite a while ago. If it’s still available, I don’t know.

  11. Bouba says:

    Hi!

    Nice job! I’ve been trying myself to do this kind of job. I have a working implementation that drives 9 rgb led strips (so 27 leds). It’s based on BAM (bit angle modulation http://www.picbasic.co.uk/forum/showthread.php?t=7393&page=1) which is I think the same thing that you’ve done. Do you experiment any blinking issue when changing the brightness value? Because I do :-( I havent find how to solve the issue for the moment, but I will certainly try with your implementation!

    Regards

    • robert says:

      Interesting read, seems different though. The effect may be the same.

      If everything runs fine, there’s no blinking as long as I use double buffering and only update the ‘live’ data once the PWM cycle has fully completed. Flipping the buffers is done in the ISR at the start of a new cycle and only if the main code has set a flag that things have changed.

      The code excerpt posted above is _not_ the latest version I use. You can find it in the git repo (2313 folder). For the real hardware I used an ATtiny24 and threw away most of the stuff anyway. Just one channel and no bubble-sort anymore…

  12. Daniel says:

    You would probably be interested in my pwm code. It sorts a list of PWM pins in the main loop (not in an ISR) and iterates through the list in the compare interrupt. The overflow interrupt resets the loop. The code could be improved as it seems to be more sensitive to missed interrupts (e.g. handling the UART in an interrupt) than necessary.


    ISR(TIMER0_COMPA_vect)
    {
    while (pp->width pin;
    pp++;
    OCR0A = pp->width;
    }
    }

  13. robert says:

    You also merge identical states… sneaky. Too complicated for my ever shrinking little brain.

  14. Pingback: Optimizing code for PWM efficiency | House of Mods

  15. rf says:

    the idea looks nice, but maybe is it possible to do the the math with the 32bit (!!) ints in the workloop and only shift out the data via spi and reload the OCR1A in the ISR?

    @Daniel:
    the code is nice! i will spend some time on it.. thanks for sharing!

  16. robert says:

    Yes, that would be much better. The ISR is running for almost too long right now. It must finish between two ticks of its associated timer. I’d need a prescaler of 2048, which doesn’t exist.

    Anyway, the real hardware doesn’t use this PWM code anymore. It dims all of the leds globally using the enable line of the driver chip. Problem is that for real individual control all of the pwm values must be sorted. Running something like bubble-sort works, but takes too much time for smooth transitions. At least when I write the code ;-)

    And I only started this PWM madness, because some time ago I bought a very large number of a certain type of led driver which doesn’t do PWM on its own. But at least they were cheap. Monetarily…

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA Image

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>