Optimized, MSQ6441C Four Digit Stick Hex Display, Arduino Driver

A cathode voltage pulse after optimizing the code with direct register writes. Ideal (delay only) pulse would be 1 ms in length according to the loop I set up, which almost matches what we see here (referring to the downward pulse).

The assumed way of setting digital pins in Arduino is to use the digitalWrite procedure to set each pin. That is slow, however, as you make a procedure call (presumably — I’m not sure exactly how much inlining happens in the compiler) and also you have to set one pin at a time.

However, since the pins map to bits that are stuffed into eight-bit registers in the microcontroller, you can just write a byte to each register and change multiple pins at once. And it is just two clock cycles for that operation.

The tradeoff is you have to look up the processor register mappings, and have decent bit toggling skills.

E.g., the code to set eight pins low and four pins high, i.e., to “reset” the LED anodes and cathodes, becomes…

PORTD &= 0b10000011;
PORTD |= 0b10000000;
PORTB &= 0b11100110;
PORTB |= 0b00100110;

Just four opcodes, and no procedure calls. To figure that out, you would need to reference the Arduino Uno pinout, and also the 328P register guide:

http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf#G3.1187134

https://upload.wikimedia.org/wikipedia/commons/c/c9/Pinout_of_ARDUINO_Board_and_ATMega328PU.svg

And also understand how to use the & and | operators to turn bits high or low. This guide is a shortcut:

https://www.arduino.cc/en/Reference/PortManipulation

The current iteration of the code:

/*
  Copyright 2020 Christopher Howard

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

/* Pins 1-12 on MSQ6X41C should be mapped to pins 2-13 on Uno */

#define LP1 2
#define LP2 3
#define LP3 4
#define LP4 5
#define LP5 6
#define LP6 7
#define LP7 8
#define LP8 9
#define LP9 10
#define LP10 11
#define LP11 12
#define LP12 13

#define SA LP11
#define SB LP7
#define SC LP4
#define SD LP2
#define SE LP1
#define SF LP10
#define SG LP5
#define SDOT LP3

enum characters
  {
   ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
   NINE, ALPHA, BRAVO, CHARLIE, DELTA, ECHO, FOXTROT, BLANK
  };

enum digit {
	D_ONE, D_TWO, D_THREE, D_FOUR
};

void setup() {
	pinMode(LP1, OUTPUT);
	pinMode(LP2, OUTPUT);
	pinMode(LP3, OUTPUT);
	pinMode(LP4, OUTPUT);
	pinMode(LP5, OUTPUT);
	pinMode(LP6, OUTPUT);
	pinMode(LP7, OUTPUT);
	pinMode(LP8, OUTPUT);
	pinMode(LP9, OUTPUT);
	pinMode(LP10, OUTPUT);
	pinMode(LP11, OUTPUT);
	pinMode(LP12, OUTPUT);
}

void set_led_pins(byte ch, byte dig, bool point) {
  /* blank all digits */
  PORTD &= 0b10000011;
  PORTD |= 0b10000000;
  PORTB &= 0b11100110;
  PORTB |= 0b00100110;
	switch(dig) {
	case D_ONE:
    PORTB &= 0b11011111;
		break;
	case D_TWO:
    PORTB &= 0b11111011;
		break;
	case D_THREE:
    PORTB &= 0b11111101;
		break;
	case D_FOUR:
    PORTD &= 0b01111111;
		break;
	}
	switch(ch)
    {
    case BLANK :
      break;
    case ZERO :
      PORTD |= 0b00101100;
      PORTB |= 0b00011001;
      break;
    case ONE :
      PORTD |= 0b00100000;
      PORTB |= 0b00000001;
      break;
    case TWO :
      PORTD |= 0b01001100;
      PORTB |= 0b00010001;
      break;
    case THREE :
      PORTD |= 0b01101000;
      PORTB |= 0b00010001;
      break;
    case FOUR :
      PORTD |= 0b01100000;
      PORTB |= 0b00001001;
      break;
    case FIVE :
      PORTD |= 0b01101000;
      PORTB |= 0b00011000;
      break;
    case SIX :
      PORTD |= 0b01101100;
      PORTB |= 0b00011000;
      break;
    case SEVEN :
      PORTD |= 0b00100000;
      PORTB |= 0b00010001;
      break;
    case EIGHT :
      PORTD |= 0b01101100;
      PORTB |= 0b00011001;
      break;
    case NINE :
      PORTD |= 0b01101000;
      PORTB |= 0b00011001;
      break;
    case ALPHA :
      PORTD |= 0b01100100;
      PORTB |= 0b00011001;
      break;
    case BRAVO :
      PORTD |= 0b01101100;
      PORTB |= 0b00001000;
      break;
    case CHARLIE :
      PORTD |= 0b00001100;
      PORTB |= 0b00011000;
      break;
    case DELTA :
      PORTD |= 0b01101100;
      PORTB |= 0b00000001;
      break;
    case ECHO :
      PORTD |= 0b01001100;
      PORTB |= 0b00011000;
      break;
    case FOXTROT :
      PORTD |= 0b01000100;
      PORTB |= 0b00011000;
      break;
    }
}

byte tc = 0;
unsigned int dc = 0;

void loop() {
	set_led_pins((dc >> 12) & 0xF, D_ONE, false);
	delay(1);
	set_led_pins((dc >> 8) & 0xF, D_TWO, false);
	delay(1);
	set_led_pins((dc >> 4) & 0xF, D_THREE, false);
	delay(1);
	set_led_pins(dc & 0xF, D_FOUR, false);
	delay(1);
  tc++;
  if(tc % 64 == 0) dc++;
}

MSQ6441C Four Digit Stick Hex Display, Arduino Driver

Photo: Arduino driving MSQ6441C
Video: Hex Display with Arduino and MSQ6441C

For learning purposes I am programming my Uno to drive a hex display on a MSQ6441C Four Digit Stick Display. The display has no built in driver, so you must drive it by controlling voltages to anodes and cathodes. At first, I assumed this would be quite simple, but to be honest it was more challenging then I expected. Due to the common cathode design (reducing the number of pins required) you are not simply setting all the pins you want at once, but instead you must cycle through each of the cathodes, setting one digit at a time while blanking the others. So, you must take advantage of “flicker fusion” in human vision, similar to what is done in movies and animation.

At this point, I could either tie this functionality into a processor interrupt, so the Arduino could do other tasks as well, or set up an SPI communication line so that another microcontroller could send simple commands to the driver microcontroller. The Atmega328P of the Uno seems a bit overkill for just running an LED display, however.

Here is some code for those interested. Pins 1-12 on the display should be mapped into pins 2-13 on the Uno. The code could be improved by replacing those expensive digitalWrite calls with assignments directly into the digital pin registers. Also I have not gotten around to coding the dot segment yet.

/*
  Copyright 2020 Christopher Howard

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

#define LP1 2
#define LP2 3
#define LP3 4
#define LP4 5
#define LP5 6
#define LP6 7
#define LP7 8
#define LP8 9
#define LP9 10
#define LP10 11
#define LP11 12
#define LP12 13

#define SA LP11
#define SB LP7
#define SC LP4
#define SD LP2
#define SE LP1
#define SF LP10
#define SG LP5
#define SDOT LP3

enum characters
  {
   ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
   NINE, ALPHA, BRAVO, CHARLIE, DELTA, ECHO, FOXTROT, BLANK
  };

enum digit {
	D_ONE, D_TWO, D_THREE, D_FOUR
};

void setup() {
	pinMode(2, OUTPUT);
	pinMode(3, OUTPUT);
	pinMode(4, OUTPUT);
	pinMode(5, OUTPUT);
	pinMode(6, OUTPUT);
	pinMode(7, OUTPUT);
	pinMode(8, OUTPUT);
	pinMode(9, OUTPUT);
	pinMode(10, OUTPUT);
	pinMode(11, OUTPUT);
	pinMode(12, OUTPUT);
	pinMode(13, OUTPUT);
}

void set_led_pins(byte ch, byte dig, bool point) {
	digitalWrite(LP1, LOW);
	digitalWrite(LP2, LOW);
	digitalWrite(LP3, LOW);
	digitalWrite(LP4, LOW);
	digitalWrite(LP5, LOW);
	digitalWrite(LP7, LOW);
	digitalWrite(LP10, LOW);
	digitalWrite(LP11, LOW);
	digitalWrite(LP6, HIGH);
	digitalWrite(LP8, HIGH);
	digitalWrite(LP9, HIGH);
	digitalWrite(LP12, HIGH);
	switch(dig) {
	case D_ONE:
		digitalWrite(LP12, LOW);
		break;
	case D_TWO:
		digitalWrite(LP9, LOW);
		break;
	case D_THREE:
		digitalWrite(LP8, LOW);
		break;
	case D_FOUR:
		digitalWrite(LP6, LOW);
		break;
	}
	switch(ch)
    {
    case BLANK :
      break;
    case ZERO :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case ONE :
      digitalWrite(SB, HIGH);
      digitalWrite(SC, HIGH);
      break;
    case TWO :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SE, HIGH);
      digitalWrite(SD, HIGH);
      break;
    case THREE :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      break;
    case FOUR :
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SC, HIGH);
      break;
    case FIVE :
      digitalWrite(SA, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      break;
    case SIX :
      digitalWrite(SA, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case SEVEN :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SC, HIGH);
      break;
    case EIGHT :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case NINE :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      break;
    case ALPHA :
      digitalWrite(SA, HIGH);
      digitalWrite(SB, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case BRAVO :
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case CHARLIE :
      digitalWrite(SF, HIGH);
      digitalWrite(SA, HIGH);
      digitalWrite(SD, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case DELTA :
      digitalWrite(SB, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SC, HIGH);
      digitalWrite(SD, HIGH);
      digitalWrite(SE, HIGH);
      break;
    case ECHO :
      digitalWrite(SA, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SE, HIGH);
      digitalWrite(SD, HIGH);
      break;
    case FOXTROT :
      digitalWrite(SA, HIGH);
      digitalWrite(SF, HIGH);
      digitalWrite(SG, HIGH);
      digitalWrite(SE, HIGH);
      break;
    }
}

byte tc = 0;
unsigned int dc = 0;

void loop() {
	set_led_pins((dc >> 12) & 0xF, D_ONE, false);
	delay(1);
	set_led_pins((dc >> 8) & 0xF, D_TWO, false);
	delay(1);
	set_led_pins((dc >> 4) & 0xF, D_THREE, false);
	delay(1);
	set_led_pins(dc & 0xF, D_FOUR, false);
	delay(1);
  tc++;
  if(tc % 64 == 0) dc++;
  /* PORTB &= ~(1<<PB0); */
	/* PORTB |= (1<<PB0); */
}

Fast pin switching in Arduino

I’ve been playing around with my Arduino Uno and Arduino Mega for the last few weeks, but I didn’t think I had discovered anything interesting enough to post about. In the last day or so, though, I was interested in finding out just how fast one could switch an Arduino digital pin on and off.

binary comment

The Uno’s ATmega328P processor is driven by a 16Mhz processor, so one might hope to get a 16Mhz signal out of a pin. But you need several processor instructions to create the signal: one to bring the pin HIGH, one to bring the pin LOW, and then one to jump back to the beginning of the loop. Per the data sheet, those each require two clock cycles. So, 6 instructions, meaning ~2.7 Mhz is a more reasonable expectation.

Implementing this code with the digitalWrite C function, what I got in the end was only a measly 150 Khz. Turns out that the digitalWrite function call is incredible slow. But fortunately, with the ATmega328P, you can simple write the HIGH or LOW directly to the IO register, which gets you back down to your 6 instructions. You’ve get to spend some time looking at the Atmel data sheet and the Uno pin diagram (or ask the helpful folks at #arduino IRC channel) to figure out how to do that. This is for digital pin 2:

void setup() { 
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);
  delay(2);
  noInterrupts();
}

void loop() {
  start:
  PORTD |= (1<<PD2);
  PORTD &= ~(1<<PD2);
  goto start;
}

Note that I called the noInterrupts procedure. noInterrupts suspends processor interrupts, which normally do various things in the background like keeping your microsecond counter running. They introduce some randomness and delay that I didn’t want here.

The above code produced about 2.7 Mhz on the oscilloscope, as expected.

binary comment

Notice there is some lack of symmetry in the signal, since the pin stays low while doing the loop jump. It occurred to me afterwards that maybe a better way to do this, while saving a few more clock cycles, would be to just bit-toggling that register bit instead, but I haven’t had a chance to explore that idea yet.

Harmongrams on Arduino Via Serial Commands

Previously I was experimenting with trying to write advanced programs in uLisp, embedded on an Arduino with a TFT display/touchscreen. Ultimately this proves not the best approach due to limitations in the memory and also the uLisp interpreter. A different approach is to keep the uLisp interpreter on the Arduino, but limit the on-board programming to a set of core interface commands, e.g., (drawline) and (fillscr). Then have the desktop computer or server sending uLisp commands over the serial connection. Of course, the uLisp commands can be constructed in Racket Scheme, allowing the power of the Racket syntax and libraries, and the great CPU and memory access, in controlling the arduino. I.e., remote control by the Mother Brain.

The demo I’ve programmed here is drawing the harmonograms slow enough to watch, so as to simulate the harmonograph experience. I do not have a camera setup suitable to show this in live video, but here are some snapshots:

The Racket code for the demo is available here:

ftp://lavender.qlfiles.net/Racket/arduino-harmon-demo.7z (md5sum 7e2bc9bc2ea25d048577b28c95c4fca5)

The project to adapt uLisp with TFT control primitives is here:

http://savannah.nongnu.org/projects/ulisp-tft

Though control over serial cables is doable for some applications, the longer-term goal would be to have them transmitted over Wi-Fi.