Lighting Dim LEDs – Making Bit Banging PWM Precise to a Single Cycle

The Ognite does not use any current limiting resistors, instead it turns on the LEDs for *very* brief moments of time.  The longer the LED is left on each time it is flicked, the brighter it looks. This is called PWM.

Normally this is done by dedicated hardware, but because of the way the Ognite LEDs must be multiplexed, the chip has to do PWM the hard way- by actually turning the power on, waiting, then turning it off. 

It is important to do this very accurately and very quickly. A dim LED on an Ognite only gets turned on for 0.000000125 seconds at a time! At this time scale, you have to take into account how much time it takes to think when doing anything. Just deciding if it is time to turn off the LED can take longer than it should be on for.

If the LED needs to be on for 1 or 2 cycles, there literally is not time to decide or check anything while the LED is on, so instead the program gets everything ready and then turns the LED on and then off in one swift and single motion.

The code that does this needs to take into account how long each and every instruction takes and balance them out to make sure that the LED is always on for exactly the right amount of time.

Below is the code that will turn on an LED for exactly the specified number of clock cycles (there are 8,000,000 clock cycles in one second on the Ognite).

It is this level of fine brightness control that gives the flame its lifelike motion. Your eye translates the smooth fades in and out into a continuous moving image, rather than a bunch of blinking lights.


// Generating high resolution delays for PWM via Bit Banging on AVR

typedef unsigned char byte;

// Set (ledonbits) to (LED_DUTY_CYCLE_PORT) for (cycles) CPU cycles, then send a zero to the port

#define LED_DUTY_CYCLE_PORT (PORTA)

static inline void ledDutyCycle(unsigned char cycles , byte ledonbits )
{

	switch (cycles) {

		// First, special case out the 0,1, and 2 cycle cases becuase these are so short that we can't do it any other way then hard code

		case 0:

			break;

		case 1:	{

				__asm__ __volatile__ (
				"OUT %0,%1 \n\t"				// DO OUTPORT first because in current config it will never actually have both pins so LED can't turn on (not turn of DDRB)
				"OUT %0,__zero_reg__ \n\t"

				: :  "I" (_SFR_IO_ADDR(LED_DUTY_CYCLE_PORT)) , "r" (ledonbits)

				);
			}
			break;

		case 2: {

				__asm__ __volatile__ (
				"OUT %0,%1 \n\t"				// DO OUTPORT first because in current config it will never actually have both pins so LED can't turn on (not turn of DDRB)
				"NOP \n\t"		// waste one cycle that will (hopefully) not get optimized out
				"OUT %0,__zero_reg__ \n\t"

				: :  "I" (_SFR_IO_ADDR(LED_DUTY_CYCLE_PORT)) , "r" (ledonbits)

				);
			}
			break;

		default: {

				// for any delay greater than 2, we have enough cycles to go though a general loop construct

				// Loop delay for loop counter c:

				// --When c==1---
				//	loop:   DEC c		// 1 cycle  (c=0)
				//	BRNE loop			// 1 cycle
				//                      // =============
				//                      // 2 cycles total

				// --When c==2---
				//	loop:   DEC c		// 1 cycle	(c=1)
				//	BRNE loop			// 2 cycles (branch taken)
				//	loop:   DEC c		// 1 cycle	(c=0)
				//	BRNE loop			// 1 cycles (branch not taken)
				//                      // =============
				//                      // 5 cycles total

				// if c==1 then delay=2 cycles (branch not taken at all)
				// if c==2 then delay=5 (2+3) cycles
				// if c==3 then delay=8 (2+3+3)

				// ...so loop overhead is always 2+(c-1)*3 cycles

				// Include the single cycle overhead for the trailing OUT after the loop and we get...

				// delay = 3+(c-1)*3
				// delay = 3+(3*c)-3
				// delay = (3*c)
				// delay/3 = c

				byte loopcounter = cycles/ (byte) 3;		// TODO: either do faster bit compute here, or store dividends and remainder in lookup

				byte remainder = cycles - (loopcounter*3);			// THis is how many cycles we need to pick up the slack to make up for the granularity of the loop

				switch (remainder) {

					case 0:		{// No remainder, so just loop and we will get right delay

						__asm__ __volatile__ (
						"OUT %[port],%[bits] \n\t"			// DO OUTPORT first because in current config it will never actually have both pins so LED can't turn on (not turn of DDRB)
						"L_%=:dec %[loop]\n\t"			// 1 cycle
						"BRNE L_%= \n\t"			// 1 on false, 2 on true cycles
						"OUT %[port],__zero_reg__ \n\t"

						: [loop] "=r" (loopcounter) : [port] "I" (_SFR_IO_ADDR(LED_DUTY_CYCLE_PORT)) , [bits] "r" (ledonbits) , "0" (loopcounter)

						);
					}
					break;

					case 1:  {// We need 1 extra cycle to come out with the right delay

						__asm__ __volatile__ (
						"OUT %[port],%[bits] \n\t"			// DO OUTPORT first because in current config it will never actually have both pins so LED can't turn on (not turn of DDRB)
						"NOP \n\t"		// waste one cycle that will (hopefully) not get optimized out
						"L_%=:dec %[loop] \n\t"			// 1 cycle
						"BRNE L_%= \n\t"			// 1 on false, 2 on true cycles
						"OUT %[port],__zero_reg__ \n\t"

						: [loop] "=r" (loopcounter) : [port] "I" (_SFR_IO_ADDR(LED_DUTY_CYCLE_PORT)) , [bits] "r" (ledonbits) , "0" (loopcounter)

						);
					}
					break;

					case 2:  { // We need 2 extra cycles to come out with the right delay

						__asm__ __volatile__ (

						"OUT %[port],%[bits] \n\t"			// DO OUTPORT first because in current config it will never actually have both pins so LED can't turn on (not turn of DDRB)
						"RJMP L_%=\n\t"					// Waste 2 cycles using half as much space as 2 NOPs
						"L_%=:dec %[loop] \n\t"			// 1 cycle
						"BRNE L_%= \n\t"			// 1 on false, 2 on true cycles
						"OUT %[port],__zero_reg__ \n\t"

						: [loop] "=r" (loopcounter) : [port] "I" (_SFR_IO_ADDR(LED_DUTY_CYCLE_PORT)) , [bits] "r" (ledonbits) , "0" (loopcounter)

						);
					}

					break;

				} // switch (remainder)

		}	// default case where b>2

		break;

	}	// switch (b)

}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s