Proper Larson Scanner with Software PWM (Repost)

Add some persistence and use more Arduino pins

proper larson scanner

Last week I had a detailed Arduino tutorial on software pulse width modulation using millis() and micros(). Why? Because I wanted to create a Proper Larson Scanner, with persistence and at least 8 LEDs.

KITT larson scanner example

From Amazon

Even though it is a popular project for the Arduino Uno, most Larson scanner tutorials, like my first one, have a few flaws. First, there is no persistence, or tail, to the LED as it moves back and forth. Persistence could be solved by using pulse-width-modulation. The Uno and other 328p-based micros only have 6 Pulse Width Modulation (PWM) pins. And let’s be honest, every project is made better by adding more LEDs. 🙂

If you look at this cover shot of KITT from Knight Rider you will see there isn’t just a single light source. It appears multiple lights are turned on, as well as fading effect. This fading effect creates a tail. Of course, the reason is probably that standard light bulbs were being used back in the 80s. Traditional light bulbs don’t turn on or off nearly as fast as LEDs.

Presenting the Proper Larson Scanner

Knowing that a popular Halloween hack is to add Cylon (or KITT) lights to your pumpkins, I thought it was time for a Proper Larson Scanner. This code example does a couple of important things.

  1. It implements my “software pulse width modulation.”
  2. Can be used on all 20 I/O pins of an Uno (or other 328p Arduino)
  3. Does not use any delay()s!

So if you want to make your pumpkin even more Cylon-like this Halloween, check out this full tutorial on a proper Larson scanner.

Proper Larson Scanner Video

Very short demo video of what this code does. (Sound effects not part of the code, obviously.)  Pay careful attention to the persistence of the LEDs. Red LEDs are tough to work with since they aren’t very bright. If your project has a short lifespan, you might consider selecting a current limiting resistor that drives the LED harder than 20mA. (Just make sure you use a current limiting resistor!)

Full Larson Scanner Example Code

Here is the full code (collapsed) or you can also grab it out of my pastebin.

//proper-larson.ino

// macros for LED state
#define ON true
#define OFF false

#define LEFT true
#define RIGHT false

// variables for larson pattern timing
unsigned long currentMillis = millis();
unsigned long previousMillis = 0;
unsigned long millisInterval = 5;

// LED fading (recommended value)
int fadeIncrement = 5;

// tells which LED is the brightest
// start at the far "right" (or left?)
int larsonStatus = 0x80;

// bit mask for maximum scan value
// picture larsonMax in binary
// 00001000 00000000 
// if you had 30 LEDs, you'd need an a long...
int larsonMax = 0x80;
// arbitrary starting direction
bool direction = RIGHT;

// variables for software PWM
// really have to play with pwmMax
// and microInterval to get a smooth 
// fade effect
unsigned long currentMicros = micros();
unsigned long previousMicros = 0;
unsigned long microInterval = 10;
// instead of HW PWM's "255" levels, we get 100
const byte pwmMax = 100;

// typedef for properties of each sw pwm pin
typedef struct pwmPins {
  int pin;
  int pwmValue;
  bool pinState;
  int pwmTickCount;
} pwmPin;

// create the sw pwm pins
// these can be any I/O pin
// that can be set to output!
const int pinCount = 8;
//const byte pins[pinCount] = {2,3,5,6,9,10,11,12};
const byte pins[pinCount] = {2,3,4,5,6,7,8,9};

// an array of Software PWM Pins
pwmPin myPWMpins[pinCount];

void setup() {
	// sets each element of myPWMpins to something
	setupPWMpins();
}

void loop() {
	// constantly ping handlePWM()
	handlePWM();

	// typical millis timer code
	// see what time it is.
	currentMillis = millis();
	if (currentMillis - previousMillis >= millisInterval) {
		if (direction == LEFT) {
			// going "left"
			// need a reference for which LED is the "bright" one
			int index = 0;
			for (index=pinCount; index >= 0; index--) {
				if (bitRead(larsonStatus, index))
					break;
			}
			// index is the bit set to "1"

			// see if we're the last pin
			if (index < (pinCount)) {
				// fade up the LED next to the "on" LED
				myPWMpins[index+1].pwmValue += fadeIncrement;

				// check if the fading LED is fully on yet
				if (myPWMpins[index+1].pwmValue >= pwmMax) {
					// okay, time to move to the next one!
					// "barrel shift" the "1", one bit over
					larsonStatus = larsonStatus << 1;
					// start fading the previous on
					myPWMpins[index].pwmValue   = myPWMpins[index].pwmValue - fadeIncrement;
					// did we hit the end yet?
					if (larsonStatus == (larsonMax << 1)) {
						// okay, time to change direction
						larsonStatus = larsonMax;
						direction = RIGHT;
					}
				}
			} 
			// create the tails.
			createFadeTail((index-1),4,15);
			createFadeTail((index-2),2,5);
			createFadeTail((index-3),1,1);
		} else {
			// going right
			int index = 0;
			// need a reference for which LED is the "bright" one
			for (index=0; index < pinCount; index++) {
				if (bitRead(larsonStatus, index))
					break;
			}
			// index is the bit set to "1"

			if (index > 0) {
				// fade up the LED next to the "on" LED
				myPWMpins[index-1].pwmValue += fadeIncrement;

				if (myPWMpins[index-1].pwmValue >= pwmMax) {
					larsonStatus = larsonStatus >> 1;
					// start fading the previous on
					myPWMpins[index].pwmValue   = myPWMpins[index].pwmValue - fadeIncrement;
					// didn't use a const here, because, it is probably 0
					if ((larsonStatus == 0x01)) {
						direction = LEFT;
					}
				}	
				// very similar to opposite direction
				// except we are modifying pins "after" bright one
				createFadeTail((index+1),4,15);
				createFadeTail((index+2),2,5);
				createFadeTail((index+3),1,1);
			}
		}
		// setup clock for next tick
		previousMillis = currentMillis;
	}
}

void createFadeTail(int pinIndex, int fadeMultiplier, int pwmLimit) {
	// make sure we aren't modifying past the end of myPWMpins or pins array
	if ((pinIndex) < pinCount) {
		// change pwmValue to make our fade effect
		myPWMpins[pinIndex].pwmValue = myPWMpins[pinIndex].pwmValue - fadeIncrement*fadeMultiplier;
		// make sure the number doesn't go negative
		if (myPWMpins[pinIndex].pwmValue <= fadeIncrement)
			// limit the lowest brightness
			// found this looks better with "1-5" instead of 0
			myPWMpins[pinIndex].pwmValue = pwmLimit;
	}
	handlePWM();
}
	

void handlePWM() {
  currentMicros = micros();
  // check to see if we need to increment our PWM counters yet
    if (currentMicros - previousMicros >= microInterval) {
    // Increment each pin's counter
    for (int index=0; index < pinCount; index++) {
    // each pin has its own tickCounter
      myPWMpins[index].pwmTickCount++;

    // determine if we're counting on or off time
      if (myPWMpins[index].pinState == ON) {
        // see if we hit the desired on percentage
        // not as precise as 255 or 1024, but easier to do math
        if (myPWMpins[index].pwmTickCount >= myPWMpins[index].pwmValue) {
          myPWMpins[index].pinState = OFF;
        }
      } else {
        // if it isn't on, it is off
        if (myPWMpins[index].pwmTickCount >= pwmMax) {
          myPWMpins[index].pinState = ON;
          myPWMpins[index].pwmTickCount = 0;
        }
      }

      // for the LEDs on or off, for min and max values
      // similar to how Arduino library does PWM
      if (myPWMpins[index].pwmValue == pwmMax)
      	myPWMpins[index].pinState = ON;
      if (myPWMpins[index].pwmValue == 0)
      	myPWMpins[index].pinState = OFF;

	  // could probably use some bitwise optimization here, digitalWrite()
	  // really slows things down after 10 pins.
      digitalWrite(myPWMpins[index].pin, myPWMpins[index].pinState);
    }
    // reset the micros() tick counter.
    previousMicros = currentMicros;
  }
}

// function to "setup" the sw pwm pin states
// modify to suit your needs
void setupPWMpins() {
  for (int index=0; index < pinCount; index++) {
  	// connect a myPWMpin to an actual pin
    myPWMpins[index].pin = pins[index];
    // start with all LEDs off
    myPWMpins[index].pwmValue = 0;
    // doesn't matter ON or OFF
    myPWMpins[index].pinState = ON;
    // set the counter to 0
    myPWMpins[index].pwmTickCount = 0;
    // make sure pin is set correctly
    pinMode(pins[index], OUTPUT);
  }
  // start with the "go-right" pattern, so
  // turn on "last" LED full
  myPWMpins[pinCount-1].pwmValue = 100;
}

// Code from [email protected]
// email | twitter | www
// See more at: https://www.baldengineer.com/larson

Definitions

#define ON true
#define OFF false

#define LEFT true
#define RIGHT false

These are simple text macros to make some of the code easier to read. LEFT and RIGHT are somewhat arbitrary. Depending on how you orient your LEDs, these may be “backwards (to you).”

// variables for larson pattern timing
unsigned long currentMillis = millis();
unsigned long previousMillis = 0;
unsigned long millisInterval = 5;

The fadeIncrement variable is how much to change the PWM of the LEDs. Depending on how pwmMax is set (on line 38), you can play with this value. You’ll get more “precision” with a lower number, but the fades will be slightly slower. I found 5 was a pretty good trade-off.

// tells which LED is the brightest
// start at the far "right" (or left?)
int larsonStatus = 0x80;

// bit mask for maximum scan value
// picture larsonMax in binary
// 00001000 00000000 
// if you had 30 LEDs, you'd need an a long...
int larsonMax = 0x80;
// arbitrary starting direction
bool direction = RIGHT;

The variable larsonStatus defines which “bit” or “LED” is fully “on.” Remember, to make a proper Larson scanner, you need to emulate incandescent light bulbs. This type of bulb has a slight “fade up” when it turns on. The variable defines the currently “on” LED. Later, in the code, we will fade the LED “next” to it. With an int variable type, you can have up to 16 LEDs since an int is 16 bits wide. For more LEDs, you’ll need a larger data type.

The value “0x80” is the same as 0000 0000 1000 0000 in binary. Which means we are starting at the “left” position.

The variable type of larsonStatus and larsonMax need to be the same. Since the int has 16 bits, but my code is only using 8 bits, we need to tell the scanning code when to stop going “up” (or “left”). If you added more LEDs, you’ll need to set larsonMax to a larger data type.

For direction, bool is used since the direction can only be LEFT or RIGHT.

// variables for software PWM
// really have to play with pwmMax
// and microInterval to get a smooth 
// fade effect
unsigned long currentMicros = micros();
unsigned long previousMicros = 0;
unsigned long microInterval = 10;
// instead of HW PWM's "255" levels, we get 100
const byte pwmMax = 100;

// typedef for properties of each sw pwm pin
typedef struct pwmPins {
  int pin;
  int pwmValue;
  bool pinState;
  int pwmTickCount;
} pwmPin;

These variables control the software PWM. Since we don’t have enough hardware PWM pins, we’ll use software PWM to do all of the dimmings of the LEDs. Tracking the status of each of the software PWM pins is made easier with this struct.

For more information on this section, check out my tutorial on software PWM.

// create the sw pwm pins
// these can be any I/O pin
// that can be set to output!
const int pinCount = 8;
//const byte pins[pinCount] = {2,3,5,6,9,10,11,12};
const byte pins[pinCount] = {2,3,4,5,6,7,8,9};

// an array of Software PWM Pins
pwmPin myPWMpins[pinCount];

If you have more than eight pins in your scanner, add them to this array and set the value of pinCount appropriately. In my case, I’m using 8 I/O pins. Notice that not all of the pins are the Uno’s hardware PWM!

setup ()

void setup() {
	// sets each element of myPWMpins to something
	setupPWMpins();
}

Nothing exciting here for setup(). Just calls a function that sets up the initial state of the SW PWM pins.

loop()

	// constantly ping handlePWM()
	handlePWM();

Each iteration of the loop(), we call the function handlePWM(). This function is where all of the SW PWM magic occurs. Since SW PWM relies on micros(), call this function whenever you have big chunks of code, like for-loops or while-loops. If, for some reason, you wanted to use delay(), you need to be careful. delay() will cause SW PWM to fail.

To use delay() with SW PWM, you’d need to break up your delay() inside of a for-loop(), calling handlePWM() on each iteration. My millis() multitasking tutorial covers this delay()-based alternative to using millis().
	currentMillis = millis();
	if (currentMillis - previousMillis >= millisInterval) {

Typical millis() code. Checking to see if it is time to increase the fade value of the LEDs. For this code, I found 5ms to work pretty well.

		if (direction == LEFT) {

Depending on our current direction, we’ll handle the code slightly differently. A clever programmer could probably cut this stuff down. I’m not going to repeat my explanations for “RIGHT.” It’s the opposite of “LEFT.”

			int index = 0;
			for (index=pinCount; index >= 0; index--) {
				if (bitRead(larsonStatus, index))
					break;
			}

We need to know which bit is the “on” bit in the larsonStatus variable. The value of index corresponds to an LED on our board. There might be a better way to do this, but I found a for-loop works fine. Once we find the “1” inside of larsonStatus, the loop exits, leaving the value of the index variable to be the current “on” LED! The next code fades the LED “next to” the on LED.

			if (index < (pinCount)) {
				// fade up the LED next to the "on" LED
				myPWMpins[index+1].pwmValue += fadeIncrement;

The index value should never be higher than the number of pins. I don’t have any “exception handling code” here. So this is just to make sure we don’t go off in the weeds. You could add something around line 101 that sets the index variable to something sane, but I haven’t tried that yet.

The increase to the pwmValue on line 85 fades the LED “next to” the on LED. If you look at the RIGHT code, you’ll see we do “index-1”. (Like I said, you could probably be clever and use a single set of code. I didn’t.)

				if (myPWMpins[index+1].pwmValue >= pwmMax) {
					// okay, time to move to the next one!
					// "barrel shift" the "1", one bit over
					larsonStatus = larsonStatus << 1;
					// start fading the previous on
					myPWMpins[index].pwmValue = myPWMpins[index].pwmValue - fadeIncrement;

When the LED “next to” the “on” LED is fully turned on, it’s time to move to the next LED. The code does a “barrel shift” of the larsonStatus variable. In other words, the 1 moves to the “next” bit of larsonStatus.

Barrel Shift Note

Sometimes people confuse “<< 1” with moving 1 bit and making the value of the least significant bit (LSB) “1”. The number in this expression is: how many times to shift the bit in that direction. The shift is loaded with zeros. In the case of the scanner, that works fine.

					if (larsonStatus == (larsonMax << 1)) {
						// okay, time to change direction
						larsonStatus = larsonMax;
						direction = RIGHT;
					}

This code is a little different from left and right, but the logic is the same. When the LarsonStatus variable gets to the “end”, we need to change scanning directions.

			createFadeTail((index-1),4,15);
			createFadeTail((index-2),2,5);
			createFadeTail((index-3),1,1);

To give a proper trailing effect, we need the LEDs “after” the “on” LED to fade out. I had a lot of trouble with these values. So you’ll need to tweak them yourself. In the section below, I’ll explain what each argument does in more detail. For now you need to pass createFadeTail() three things: which LED, the fadeIncrement multiplier, and the floor or smallest value to set the LEDs.

Why do you need a floor or minimum value?

Watch the Knight Rider or Cylon scanning closely. One thing you might notice is that it appears like there is light coming from ALL of the scanners. So to make this Larson scanner more correct, the LEDs glow just slightly instead of turning off completely.

Scanning RIGHT

Again, I’m not going to explain the RIGHT scanning in detail. It’s effectively the opposite of the LEFT, with a few tweaks in the logic. If you have a question, leave a comment.

createFadeTail()

This function gives the LEDs a tail-like glow. This effect is the reason I wanted to make a proper Larson scanner in the first place! There is some persistence in the scanning, which is what this function emulates.

void createFadeTail(int pinIndex, int fadeMultiplier, int pwmLimit) {
	// make sure we aren't modifying past the end of myPWMpins or pins array
	if ((pinIndex) < pinCount) {
		// change pwmValue to make our fade effect
		myPWMpins[pinIndex].pwmValue = myPWMpins[pinIndex].pwmValue - fadeIncrement*fadeMultiplier;
		// make sure the number doesn't go negative
		if (myPWMpins[pinIndex].pwmValue <= fadeIncrement)
			// limit the lowest brightness
			// found this looks better with "1-5" instead of 0
			myPWMpins[pinIndex].pwmValue = pwmLimit;
	}
	handlePWM();
}

Line 143 makes sure we aren’t accessing an element outside of the myPWMpins array like we did inside of loop().

The increment on line 145 should look familiar. The only difference is that we’re multiplying the fadeIncrement by a number. I found 4, 2, 1 to be the right sequence of the tail. Your mileage may vary (YMMV).

Since we’re using a multiplier, it is possible the pwmValue will drop below zero. (This is also why it is a “signed integer”.) The third argument of this function, pwmLimit, is the “floor” value. This limit forces the LED to a known state.

The handlePWM() call may or may not be necessary. However, I have a habit of calling these “periodic” style functions from other functions. That way, if the original function calls end up in a loop, I know the software PWM won’t be affected (much).

handlePWM()

Lines 156 through 194 are the heart of Software PWM. Read my tutorial on software PWM to understand how it works.

setupPWMpins()

void setupPWMpins() {
  for (int index=0; index < pinCount; index++) {
  	// connect a myPWMpin to an actual pin
    myPWMpins[index].pin = pins[index];
    // start with all LEDs off
    myPWMpins[index].pwmValue = 0;
    // doesn't matter ON or OFF
    myPWMpins[index].pinState = ON;
    // set the counter to 0
    myPWMpins[index].pwmTickCount = 0;
    // make sure pin is set correctly
    pinMode(pins[index], OUTPUT);
  }

Nothing too unique happening here. To initialize the myPWMpins array, I use a for-loop() to go through and set each value to a known state. That state is off, by the way. Don’t be confused by line 205’s “pinState = ON”. That’s for the PWM code, not for the actual state of the LED.

Line 209 sets the actual pins to OUTPUT. (Arduino’s analogWrite() does this for you.)

  myPWMpins[pinCount-1].pwmValue = 100;

Again, nothing too unique. We’re just forcing the “left most” bit and pin fully on. The initial state of this scanner is to start going “RIGHT.”

Conclusion

If you want to customize your lights, here are some ideas. These are exercises for the reader that I might do as future tutorials. If you’re interested in one, leave a comment.

  • Add buttons (or serial commands) to speed up, slow down, or pause the animation.
  • Create a small amount of “cell leakage” to the LED immediately next to the one “turning-on.” (You can see that in the KITT picture.)
  • Use the same code on a NeoPixel Digital RGB Strip from Adafruit

Long comments, URLs, and code tend to get flagged for spam moderation. No need to resubmit.

Leave a comment

3 thoughts on “Proper Larson Scanner with Software PWM (Repost)

  1. Excellent tutorial.
    Super helpful.
    I am trying to make a larson scanner that shifts with a distance measurement from an ultrasonic sensor. Any tips. Really struggling.

  2. Back in 1986 I built and installed a 24 light KITT system using bidirectional shift registers and a timer. Worked fairly well and the bulbs did give a trailing effect. Just got back into this stuff after 30 years and prototyped three shiftregisters driven by the arduino doing a similar thing. Would appreciate some feed back. Thanks for sharing all your knowledge, this is a great site!