Close

Crossfade attempt #1 - learning from failure

A project log for Just Another Nixie Clock

An arduino based multiplexed, cross-fading IN-14 Nixie tube clock.

kevin-mosseyKevin Mossey 05/21/2021 at 04:030 Comments

Once I got the digits displaying, I wanted to crossfade them.  This was something I had seen people talk about doing, and I saw videos of people successfully doing it, but I couldn't find examples of it being done.  I figured the Arduino had pulse width modulation on some pins, so I thought I could use one of those with the anode and increase the brightness on the new digit and decrease the brightness on the old digit.

I wanted the bulb to transition over about a quarter second.  The following is just representative of what I originally wrote, which has been long since lost because I didn't have any version control set up yet as I was just trying different things, and not really trying to write a formal clock yet.  

#define anode 3
#define A 8
#define B 9
#define C 10
#define D 11

void setup() {
  pinMode(anode, OUTPUT);
  pinMode(A, OUTPUT);
  pinMode(B, OUTPUT);
  pinMode(C, OUTPUT);
  pinMode(D, OUTPUT);
}

void loop() {
static int previous=9;
  for (int i=0; i<10; i++) {
    transition_tube(i, previous);
    delay(750);
  }
}

void transition_tube(int new_digit, int old_digit) {
  unsigned long step_start_time = micros();
  unsigned long current_micros = micros();
  int delay_array[] = {100,200,300,400,500,600,700,800,900};  
  int pw_array[] = {30,60,90,120,150,180,210,240,255};
  for (int x=0; x<9; x++) {
    analogWrite(anode, 0);      // turn off anode before changing digit
    update_tube(old_digit);
    analogWrite(anode, 255 - pw_array[x]);
    step_start_time = micros();
    while (current_micros - step_start_time < 1000 - delay_arr[x])
    {
      current_micros = micros();
    }
    analogWrite(anode, 0);      // turn off anode before changing digit
    update_tube(new_digit);
    analogWrite(anode, pw_array[x]);
    step_start_time = micros();
    while (current_micros - step_start_time < delay_arr[x])
    {
      current_micros = micros();
    }
}

void updateTube(int val)
{
  digitalWrite(A, val & 1 ? HIGH : LOW);
  digitalWrite(B, val & 2 ? HIGH : LOW);
  digitalWrite(C, val & 4 ? HIGH : LOW);
  digitalWrite(D, val & 8 ? HIGH : LOW);
}

There are a few problems with what I originally did here.  What I was trying to do was turn the tube on with the new digit for 0.1ms at a pulse width of about 12%, then turn the tube on with the old digit for 0.9ms at roughly 88%.  But all I got was a hot mess as you can see with the video below, taken in slow motion on my phone.

I learned a lot in troubleshooting the issue. I had assumed (yes, Mom, I know what happens when I assume something!) that it was working at some magical percentage of the 16MHz clock that was really fast, like in the microsecond range. When I started I didn't know that the Arduino's pulse width modulation through analogWrite() only works between about 490Hz to 980Hz. At it's fastest, though, it can only work to a resolution of 1.0ms, which means that my attempts to drive it at 0.1ms intervals was useless.  It just can't work for what I'm trying to do.  It also explains why the bulb appears to have no fade in or out - it just flickers between the old and new until it "pops" to the new digit. 

While troubleshooting the awful flickering, I discovered that the digitalWrite() function is slow.  Like really slow.  About 20x times slower than direct port writes.  https://roboticsbackend.com/ has a fantastic explanation and benchmark comparison of doing direct port writes vs using the Arduino functions.  I love the fact that Arduino is easy to get started with, but I really love the fact that when you're ready to get more advanced, you can.

I think another big problem with my approach was I was trying to replace delayMicroseconds() with my own loop so that I might be able to execute other code instead of freezing my program, but the loop once through took longer to execute than I intended the loop to be, so I couldn't get accurate timings.  

I benchmarked the mostly-empty while loop (which only updates the current_micros), looping 100,000 times inside a for loop, and found the overhead of the loop ran between 104.6 and 104.7 microseconds for what should have been a 100us loop.  A 4.5% error is definitely no good for a clock, so I knew I'd have to switch to something more accurate, like timer interrupts.

This isn't to say that pulse width modulation has no use, as I did get a nice fade-out, fade-in for each digit using it, but not while multiplexing.  I'll talk about that next time.

Discussions