Uno/Nano R3 Servo-7seg digit

I saw a project for a mechanical, 7-segment digit using a motor driven cam to lift the segments. As the motor rotated the cam, brass "lifters" would put segments in place to form the digit.

I liked it, but decided not to try a cam, and use SG90 servos, or at least to write a sketch for a single, 7-segment digit, driven by SG90 (seven per digit) to lift the segments.

Rather than using a typical bit-pattern to lower every segment, then lift every needed segment for every digit, I wanted to use a least-movement approach, using "kept" segments shared between adjacent digits, "removed" segments not used in the following digit and "added" segments not used in the previous digit. This is the highlight, and why I filed this under "programming."

I included a WS2812-digit to color-code the segments that that have been "kept", "removed" and "added".

An "ASCII-drawing" of the physical setup of the servo lifters is in the comments.

At the bottom of the sketch are notes for the next step; a four-digit clock.

// Mechanical, single-digit, seven-segment display.
// Each segment is attached to a servo by an arm

// See bottom of code for MUXED servos

#include <FastLED.h>   // https://github.com/FastLED/FastLED
#define NUMPIX 21      // 3pix x 7seg
#define DATAPIN 10     //
#define MAXBRIGHT 255  // adjust
CRGB led[NUMPIX];      // create WS2812B object

#include <Servo.h>      // https://github.com/arduino-libraries/Servo
#define SEGMENTS 7      // one servo per segment
Servo servo[SEGMENTS];  // seven servos

int armup = 0, armdown = 90;  // servo horn positions
byte count;                   // case counter
unsigned long timer;          // time digit change

enum segs { sega, segb, segc, segd, sege, segf, segg
          }; // enumerated segments for calling their servo-pin number

byte srvpin[] = { 2, 8, 6, 5, 4, 3, 7 };  // pins for seg a, b, c, d, e, f, g

/*
  SEGMENTS attached to SERVOS by non-interfering ARMS.
  - START SEGMENTS (segments in current digit)
  - KEEP SEGMENTS (will be used in next digit)
  - REMOVE SEGMENTS (will not be used in next digit)
  - ADD SEGMENTS (will be added to next digit)
  .         _____
  +--------/--a  \
  |      /\\_____//\
  |     |  |     |  |
  | +---|-f|     |b-|-----+
  | |   |  |_____|  |     |
  | |    \//  g--\\/----+ |
  | |    /\\_____//\    | |
  | |   |  |     |  |   | |
  | | +-|-e|     |c-|-+ | |
  | | | |  |_____|  | | | |
  | | |  \//  d  \\/  | | |
  | | |    \__ __/    | | |
  | | |       |       | | |
  + + +       +       + + +
  a f e       d       c g b <- SEGMENT SERVOS

  WS2812B GRN - Kept segment (armup)
  WS2812B RED - Removed segment (armdown)
  WS2812B BLU - Added segment (armup)
  .   _____
     /  a  \    leda  0,  1,  2
   /\\_____//\
  |  |     |  | ledb  3,  4,  5
  | f|     |b | ledf  6,  7,  8
  |  |_____|  |
   \//  g  \\/  ledg  9, 10, 11
   /\\_____//\
  |  |     |  | ledc 12, 13, 14
  | e|     |c | lede 15, 16, 17
  |  |_____|  |
   \//  d  \\/  ledd 18, 19, 20
     \_____/
*/

void setup() {
  Serial.begin(115200);

  FastLED.addLeds<WS2812B, DATAPIN, GRB>(led, NUMPIX);
  FastLED.setBrightness(MAXBRIGHT);
  FastLED.clear();
  FastLED.show();

  for (byte i = 0; i < SEGMENTS; i++) {
    servo[i].write(armdown);     // configure arms "down"
    servo[i].attach(srvpin[i]);  // servos 0 - 6 attached to servo pins
  }

  delay(1000); // settle servos
  // test(); // cycle through segment servos and WS2812B
  zero();  // starting number
  delay(820); // 180ms to move servos 90 degrees
}

void loop() {
  unsigned long servotimeout = 820;  // 1000ms/second - 180ms/servomove = 820ms
  if (millis() - timer > servotimeout) {
    timer = millis();  // set new timer

    FastLED.clear();
    FastLED.show();

    if (count > 9)  // upper bounds reached...
      count = 0;    // reset value

    switch (count) {  // transition digits
      case 0: zro_one(); break;
      case 1: one_two(); break;
      case 2: two_tre(); break;
      case 3: tre_for(); break;
      case 4: for_fiv(); break;
      case 5: fiv_six(); break;
      case 6: six_svn(); break;
      case 7: svn_eit(); break;
      case 8: eit_nin(); break;
      case 9: nin_zro(); break;
      default: break;
    }
    count++;  // increase digit count
  }
}

void keepseg(byte seg) {
  // servo[seg.write();  // No servo in keepseg();
  for (int j = 0; j < 3; j++) {              // only show in WS2812B
    led[seg * 3 + j] = CRGB(191, 255, 255);  // blu segment of three WS2812B
  }
  FastLED.show();  // show buffer
}

void addseg(byte seg) {
  servo[seg].write(armup);
  for (int j = 0; j < 3; j++) {
    led[seg * 3 + j] = CRGB(191, 255, 191);  // grn
  }
  FastLED.show();
}

void remseg(byte seg) {
  servo[seg].write(armdown);
  for (int j = 0; j < 3; j++) {
    led[seg * 3 + j] = CRGB(128, 64, 64);  // red
  }
  FastLED.show();
}

void zero() {
  for (byte i = 0; i < 6; i++) {  // SEGMENTS minus SEG G
    addseg(i);                    // call "add segment"
  }
}

void zro_one() {  // changing from ZERO to ONE...
  keepseg(segb); keepseg(segc);  // keep SEG B and C
  remseg(sega); remseg(segd); remseg(sege); remseg(segf);   // remove SEG A, D, E, and F
  // addseg(); // no addseg()
}

void one_two() {  // change from ONE to TWO
  keepseg(segb);
  remseg(segc);
  addseg(sega); addseg(segd); addseg(sege); addseg(segg);  // add SEG A, D, E, and G
}

void two_tre() {
  keepseg(sega); keepseg(segb); keepseg(segd); keepseg(segg);
  remseg(sege);
  addseg(segc);
}

void tre_for() {
  keepseg(segb); keepseg(segc); keepseg(segg);
  remseg(sega); remseg(segd);
  addseg(segf);
}

void for_fiv() {
  keepseg(segf); keepseg(segc); keepseg(segg);
  remseg(segb);
  addseg(sega); addseg(segd);
}

void fiv_six() {
  keepseg(segc); keepseg(segd); keepseg(segf); keepseg(segg);
  remseg(sega);
  addseg(sege);
}

void six_svn() {
  keepseg(segc);
  remseg(segd); remseg(sege); remseg(segf); remseg(segg);
  addseg(sega); addseg(segb);
}

void svn_eit() {
  keepseg(sega); keepseg(segb); keepseg(segc);
  // remseg(); // none
  addseg(segd); addseg(sege); addseg(segf); addseg(segg);
}

void eit_nin() {
  keepseg(sega); keepseg(segb); keepseg(segc); keepseg(segf); keepseg(segg);
  remseg(segd); remseg(sege);
  // addseg(); // none
}

void nin_zro() {
  keepseg(sega); keepseg(segb); keepseg(segc); keepseg(segf);
  remseg(segg);
  addseg(segd); addseg(sege);
}

void test() {                            // cycle through servo/WS2812 segments
  int servo90degrees = 180;              // milliseconds for 90 degree servo move
  for (int i = 0; i < SEGMENTS; i++) {   // number of segments
    for (int j = 0; j < 3; j++) {        // three WS2812 per segment
      led[i * 3 + j] = CRGB(0, 0, 255);  // blu loaded to buffer
    }
    FastLED.show();           // display buffer
    FastLED.clear();          // clear buffer
    servo[i].write(armup);    // move servo UP
    delay(servo90degrees);    // servo takes 2ms per degree to move. 90deg * 2ms/deg = 180ms
    servo[i].write(armdown);  // move servo DOWN
    delay(servo90degrees);
  }
  FastLED.show();
}

/*
  - NEXT to do:
  - Four digits
  - RTC (clock with "blinking" colon)
  - two PCAS9685 (16 channel PWM mux, daisy-chained, two "digits" per mux)
  - https://github.com/NachtRaveVL/PCA9685-Arduino
  - keep track of which of ten digit (0..9) is in each of four positions (HH:MM)
  - 12vdc power supply for Arduino VIN
  - 5V/5A BEC/buck converter for SERVOES and first PCA9685A
  - Not as genius as this cam-driven model: https://hackaday.io/project/187684-eptaora
*/

diagram.json for the nerds
{
  "version": 1,
  "author": "",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-arduino-nano", "id": "nano", "top": -33.6, "left": -86.9, "attrs": {} },
    {
      "type": "wokwi-led-strip",
      "id": "strip1",
      "top": -333.9,
      "left": 316.8,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    { "type": "wokwi-servo", "id": "servo2", "top": -405.2, "left": 76.8, "attrs": {} },
    { "type": "wokwi-servo", "id": "servo3", "top": -357.2, "left": 76.8, "attrs": {} },
    { "type": "wokwi-servo", "id": "servo4", "top": -309.2, "left": 76.8, "attrs": {} },
    { "type": "wokwi-servo", "id": "servo5", "top": -261.2, "left": 76.8, "attrs": {} },
    { "type": "wokwi-servo", "id": "servo6", "top": -213.2, "left": 76.8, "attrs": {} },
    { "type": "wokwi-servo", "id": "servo7", "top": -165.2, "left": 76.8, "attrs": {} },
    { "type": "wokwi-servo", "id": "servo8", "top": -117.2, "left": 76.8, "attrs": {} },
    {
      "type": "wokwi-led-strip",
      "id": "strip2",
      "top": -218.7,
      "left": 316.8,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    {
      "type": "wokwi-led-strip",
      "id": "strip4",
      "top": -272.6,
      "left": 379.5,
      "rotate": 90,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    {
      "type": "wokwi-led-strip",
      "id": "strip5",
      "top": -167,
      "left": 379.5,
      "rotate": 90,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    {
      "type": "wokwi-led-strip",
      "id": "strip6",
      "top": -165.6,
      "left": 255.5,
      "rotate": 270,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    {
      "type": "wokwi-led-strip",
      "id": "strip7",
      "top": -271.2,
      "left": 255.5,
      "rotate": 270,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    {
      "type": "wokwi-led-strip",
      "id": "strip3",
      "top": -104.3,
      "left": 318.2,
      "rotate": 180,
      "attrs": { "pixels": "3", "pixelShape": "circle" }
    },
    {
      "type": "wokwi-text",
      "id": "text1",
      "top": -57.6,
      "left": 297.6,
      "attrs": { "text": "GRN - KEPT SEG\nRED - REMOVED SEG\nBLU - ADDED SEG\nBLK - UNCHANGED" }
    },
    { "type": "wokwi-text", "id": "text2", "top": -67.2, "left": 249.6, "attrs": { "text": "a" } },
    {
      "type": "wokwi-text",
      "id": "text3",
      "top": -115.2,
      "left": 249.6,
      "attrs": { "text": "f" }
    },
    {
      "type": "wokwi-text",
      "id": "text4",
      "top": -163.2,
      "left": 249.6,
      "attrs": { "text": "e" }
    },
    {
      "type": "wokwi-text",
      "id": "text5",
      "top": -211.2,
      "left": 249.6,
      "attrs": { "text": "d" }
    },
    {
      "type": "wokwi-text",
      "id": "text6",
      "top": -259.2,
      "left": 249.6,
      "attrs": { "text": "c" }
    },
    {
      "type": "wokwi-text",
      "id": "text7",
      "top": -307.2,
      "left": 249.6,
      "attrs": { "text": "g" }
    },
    {
      "type": "wokwi-text",
      "id": "text8",
      "top": -355.2,
      "left": 249.6,
      "attrs": { "text": "b" }
    },
    { "type": "wokwi-vcc", "id": "vcc1", "top": -373.64, "left": 297.6, "attrs": {} },
    { "type": "wokwi-gnd", "id": "gnd1", "top": -48, "left": 258.6, "attrs": {} },
    { "type": "wokwi-vcc", "id": "vcc2", "top": -402.44, "left": 28.8, "attrs": {} },
    {
      "type": "wokwi-text",
      "id": "text9",
      "top": -28.8,
      "left": 96,
      "attrs": {
        "text": "EACH SERVO HORN\nIS ATTACHED TO A\nROD (Ø1mm) THEN TO A\n\"SEGMENT\" (see code)"
      }
    }
  ],
  "connections": [
    [ "nano:2", "servo8:PWM", "green", [ "v0" ] ],
    [ "nano:GND.1", "servo8:GND", "black", [ "v0" ] ],
    [ "nano:GND.1", "servo7:GND", "black", [ "v0" ] ],
    [ "nano:GND.1", "servo6:GND", "black", [ "v0" ] ],
    [ "nano:GND.1", "servo5:GND", "black", [ "v0" ] ],
    [ "nano:GND.1", "servo4:GND", "black", [ "v0" ] ],
    [ "nano:GND.1", "servo3:GND", "black", [ "v0" ] ],
    [ "nano:GND.1", "servo2:GND", "black", [ "v0" ] ],
    [ "strip4:DIN", "strip1:DOUT", "green", [ "v0" ] ],
    [ "strip4:VDD", "strip1:VDD.2", "red", [ "v0" ] ],
    [ "strip4:VSS", "strip1:VSS.2", "white", [ "v0" ] ],
    [ "strip4:VSS.2", "strip5:VSS", "white", [ "v0" ] ],
    [ "strip6:VSS.2", "strip7:VSS", "white", [ "v0" ] ],
    [ "strip7:VSS.2", "strip2:VSS", "white", [ "v-1.4", "h18.4" ] ],
    [ "strip4:DOUT", "strip5:DIN", "green", [ "v0" ] ],
    [ "strip6:DOUT", "strip7:DIN", "green", [ "v0" ] ],
    [ "strip7:DOUT", "strip2:DIN", "green", [ "v-1.4", "h28.4" ] ],
    [ "strip7:VDD.2", "strip2:VDD", "red", [ "v-1.4", "h38.4" ] ],
    [ "strip7:VDD", "strip6:VDD.2", "red", [ "v0" ] ],
    [ "strip3:VSS.2", "strip6:VSS", "white", [ "h0" ] ],
    [ "strip3:DOUT", "strip6:DIN", "green", [ "h0" ] ],
    [ "strip6:VDD", "strip3:VDD.2", "red", [ "v0" ] ],
    [ "strip3:VSS", "strip5:VSS.2", "white", [ "h0" ] ],
    [ "strip3:DIN", "strip5:DOUT", "green", [ "h0" ] ],
    [ "strip3:VDD", "strip5:VDD.2", "red", [ "h0" ] ],
    [ "strip5:VDD", "strip4:VDD.2", "red", [ "v0" ] ],
    [ "vcc1:VCC", "strip1:VDD", "red", [ "v0" ] ],
    [ "gnd1:GND", "strip1:VSS", "black", [ "v0" ] ],
    [ "vcc2:VCC", "servo2:V+", "red", [ "v0" ] ],
    [ "vcc2:VCC", "servo3:V+", "red", [ "v0" ] ],
    [ "vcc2:VCC", "servo4:V+", "red", [ "v0" ] ],
    [ "vcc2:VCC", "servo5:V+", "red", [ "v0" ] ],
    [ "vcc2:VCC", "servo6:V+", "red", [ "v0" ] ],
    [ "vcc2:VCC", "servo7:V+", "red", [ "v0" ] ],
    [ "vcc2:VCC", "servo8:V+", "red", [ "v0" ] ],
    [ "nano:3", "servo7:PWM", "green", [ "v0" ] ],
    [ "nano:4", "servo6:PWM", "green", [ "v0" ] ],
    [ "nano:5", "servo5:PWM", "green", [ "v0" ] ],
    [ "nano:6", "servo4:PWM", "green", [ "v0" ] ],
    [ "nano:7", "servo3:PWM", "green", [ "v0" ] ],
    [ "nano:8", "servo2:PWM", "green", [ "v0" ] ],
    [ "nano:10", "strip1:DIN", "green", [ "v-384", "h316.8", "v96" ] ]
  ],
  "dependencies": {}
}

A link, until Wokwi fades away:

Thanks for sharing!

This reminds me of the display we once used at the stock exchange. I think it was in use up to about late 70's early 80's. Your 'least movement' approach is exactly what we used.

I would probably try to get rid of these helper functions: zro_one() ... nin_zro()

You can use a look up table for the segment pattern and some bit logic

A = 0b0110
b = 0b0011

  A &  B -> 0b0010 (keep on segments)
 ~A &  B -> 0b0001 (added segments)
  A & ~B -> 0b0100 (removed segments)
 ~A & ~B -> 0b1000 (keep off segments)

Very clean. I like it, a lot.

My helper functions got cluttered as they change while the sketch developed... the names let me "see" the progression of the segments.

haven't gone thru the code, but wouldn't it make sense to always drive each servo to the desired position without worrying about whether it changes or not? If it's up and needs to be up it remains up and would only change if it moves from up to down and visa versa

I do not understand, but that's definitely because I am imagining the operation in the way I programmed it. Would you explain this timing?

i'm a it confused a bit by your code. Looks like it sequences thru a set of digits and knowing which was the previous digit, you eiher add, keep or remove a segment.

If I understand this correctly, this seems impractical since to display randome digits, you'd need a unique routine for each transition; from each digit to any other digit.

as you must know, when driving a 7-segment LED display, for any digit you have a 7-bit pattern and set he LEDs per the pattern. There's no benefit in checking if an LED is already on/off and skipping setting/reseting the bit affecting the LED

isn't the same true for driving servos, if an segment arm is already up, is there any benefit in not performing an servo.write(armup)? It It won't cause any unnecessary movements.

is this really necessary?

The idea for this is: do not move all segments (like a "clear segments"), but to (1) preserve segments that do not change state (if it is on, leave it on... if it is off, leave it off), (2) remove segments that are not used in the following digit, then (3) add segments that were not in the previous digit.

That was saying, I do not want to "clear screen, show digit"... I wanted to only move the servos/segments that were not in adjacent digits.

Yes, that is a good point. This sketch is only for sequential digits, as in a clock (discussed at the bottom of the sketch). Your "random digits" would need bit-centered code, like the advice in Post #4. The sketch would lose a lot of "human readability" to let bits determine segments (kept, removed, added, ignored), rather than pre-programmed "helper functions" (mentioned in Post #4.

If you are only working with LEDs, I agree, BUT, if moving servos, with a need for 200ms for 90 degrees of movement, eliminating unnecessary servo movement is beneficial, maybe imperative.

Your "random digits" will be a nice test with Post #4 suggestion of dropping functions and just operate on bits.

It think the question is

servo.write(armup); // last digit
servo.write(armup); // new digit

The second write is basically a no operation. Why prevent it?

When you aim for a clock you need some additional transitions

  • 12 -> 1
  • 23 -> 0
  • 59 -> 00

yes but a loop updates each servo position and within 200 ms they should all reach a "changed" position or remain in their current position.

it sounds like you also want to save time by avoiding waiting 200 msec for each segment to move if it needs to change

disp:    a  b  c  d  e  f  g
disp: 4 Dn Up Up Dn Dn Up Up
disp: 7 Up Up Up Dn Dn Dn Dn
disp: 5 Up Dn Up Up Dn Up Up
disp: 2 Up Up Dn Up Up Dn Up
disp: 4 Dn Up Up Dn Dn Up Up
disp: 8 Up Up Up Up Up Up Up
disp: 6 Up Dn Up Up Up Up Up
disp: 8 Up Up Up Up Up Up Up
disp: 8 Up Up Up Up Up Up Up
disp: 1 Dn Up Up Dn Dn Dn Dn
disp: 0 Up Up Up Up Up Up Dn
disp: 8 Up Up Up Up Up Up Up
disp: 6 Up Dn Up Up Up Up Up
disp: 8 Up Up Up Up Up Up Up
disp: 0 Up Up Up Up Up Up Dn
disp: 2 Up Up Dn Up Up Dn Up
disp: 7 Up Up Up Dn Dn Dn Dn
disp: 7 Up Up Up Dn Dn Dn Dn
// 7-seg display using servos

#include <Servo.h>

// pins for seg            a, b, c, d, e, f, g
const byte PinServo [] = { 2, 8, 6, 5, 4, 3, 7 };
const int  Nseg        = sizeof(PinServo);;

Servo servos [Nseg];
enum  { ArmUp = 0, ArmDown = 90 };  // servo positions

char s [90];

// -------------------------------------

//    a
//   f b
//    g
//   e c
//    d h
//
//   h g f e d c b a

const byte SEGMENT_MAP_DIGIT[] = {
//     0     1     2     3     4     5     6     7     8     9
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90
};

// -------------------------------------
int hdr;
void disp (
    uint16_t  digit)
{
    if (!hdr++)  {
        sprintf (s, "disp:    a  b  c  d  e  f  g");
        Serial.println (s);
    }

    sprintf (s, "disp: %d", digit);
    Serial.print (s);

    byte bits = SEGMENT_MAP_DIGIT [digit];
    for (unsigned n = 0; n < Nseg; n++, bits >>= 1)  {
        if (0x1 & bits)  {                  // HIGH bit
            servos [n].write  (ArmDown);
            sprintf (s, " Dn");
        }
        else  {
            servos [n].write  (ArmUp);
            sprintf (s, " Up");
        }
        Serial.print (s);
    }
    Serial.println ();
}

// -----------------------------------------------------------------------------
void loop ()
{
    disp(random(9));
    delay (2000);
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    for (int n = 0; n < Nseg; n++)  {
        servos [n].attach (PinServo [n]);

        // exercise each
        servos [n].write  (ArmUp);
        delay (250);
        servos [n].write  (ArmDown);
    }
}

You'd get the full range 0 to 9 with: disp(random(10))
Are you taking advantage of an arm that may already be in the correct position from displaying the previous digit ?

what do you need to do that other than just setting its position?

Avoid setting a servo arm that is already in the correct position. At least that is what I interpret by the "least movement approach". The OP said this:

It makes sense because it speeds the whole thing up. For example a transition from a 0 to an 8 requires only the setting of segment G.

but even if you lower all segmens, don't you just need to write to the servos that need to be raised?

how does it speed things up? is servo.write() that time consuming? what about the data & logic to keep track of a servo position and determine if it needs to change? how fast does it need to be?

this approach is confusing

OK. I've now understood there has been some debate about whether there is any real gain in minimising the writes to the servo. I don't know but, anyway, it should be easy to implement a "minimal write" approach.
I've just made a few additions to your code to achieve this. It is UNTESTED but I'm sure you'll see what I mean.

static uint16_t digitOld = 8 ;  // should really initialised be in setup() 
                                // together with initialising the servos
byte bits = SEGMENT_MAP_DIGIT [digit];
byte bitsOld = SEGMENT_MAP_DIGIT [digitOld];
    for (unsigned n = 0; n < Nseg; n++, bits >>= 1, bitsOld >>= 1)  {
        if ((0x1 & bits) && !(0x1 & bitsOld))  {                  // HIGH bit
            servos [n].write  (ArmDown);
            sprintf (s, " Dn");
        }
        else (!(0x1 & bits) && (0x1 & bitsOld))  {
            servos [n].write  (ArmUp);
            sprintf (s, " Up");
        }
        else {
           sprintf (s, " NC");  //no change
        }
        Serial.print (s);
    }
digitOld = digit ;

fair enough. however, there are some problems and i can see potential improvements.

But the code is more complicated (certainly much less than the original), how much faster is it, does it need to be faster, is it worth the effort?

I am not following... is this referring to using "add segment" for each segment rather than a group "add segment?" If so, the reason was for me to be able to red all the segment names. I tend to overlook code (dys-sketch-ia?).

Yes, I have another "clock" project that started with Serial Monitor output, now it is a bicycle rim with WS2812B.

I think that is what I am doing... maybe we are saying the same thing differently?

Yes, this was the goal.

I read "2ms per degree" - I noted that time in the comments.

Cool. I will need to play with it for a while until I understand it.

Yes, I wanted to show "human readable" code for clarity to show how the code "works" rather than burying it under bit arrays. However, "burying it under bit arrays" is my next step. This is one segment of four segments.

Only for if "seconds" are handled or day+hour+minute transitions. I probably will not do "seconds" because that will destroy these servos in 100 hours of use (I lost this reference by ?idahofarmer?).

The only reason you would need to turn all segments off between digits is when multiplexing a display, to prevent ghosting.

You are way over-complicating this, there is no harm at all in telling a servo to move to the position it is currently at, and no motion takes place. If you insist in only moving a servo that is not currently at the desired position, then just use servo.read() to check what the last commanded position was and only use servo.write() if that differs:

// Mechanical, single-digit, seven-segment display.
// Each segment is attached to a servo by an arm

// See bottom of code for MUXED servos

#include <FastLED.h>   // https://github.com/FastLED/FastLED
#define NUMPIX 21      // 3pix x 7seg
#define DATAPIN 10     //
#define MAXBRIGHT 255  // adjust
CRGB led[NUMPIX];      // create WS2812B object

#include <Servo.h>      // https://github.com/arduino-libraries/Servo
#define SEGMENTS 7      // one servo per segment
Servo servo[SEGMENTS];  // seven servos

int armup = 0, armdown = 90;  // servo horn positions
byte count;                   // case counter
unsigned long timer;          // time digit change

enum segs { sega, segb, segc, segd, sege, segf, segg
          }; // enumerated segments for calling their servo-pin number

byte srvpin[] = { 2, 8, 6, 5, 4, 3, 7 };  // pins for seg a, b, c, d, e, f, g

/*
  SEGMENTS attached to SERVOS by non-interfering ARMS.
  - START SEGMENTS (segments in current digit)
  - KEEP SEGMENTS (will be used in next digit)
  - REMOVE SEGMENTS (will not be used in next digit)
  - ADD SEGMENTS (will be added to next digit)
  .         _____
  +--------/--a  \
  |      /\\_____//\
  |     |  |     |  |
  | +---|-f|     |b-|-----+
  | |   |  |_____|  |     |
  | |    \//  g--\\/----+ |
  | |    /\\_____//\    | |
  | |   |  |     |  |   | |
  | | +-|-e|     |c-|-+ | |
  | | | |  |_____|  | | | |
  | | |  \//  d  \\/  | | |
  | | |    \__ __/    | | |
  | | |       |       | | |
  + + +       +       + + +
  a f e       d       c g b <- SEGMENT SERVOS

  WS2812B GRN - Kept segment (armup)
  WS2812B RED - Removed segment (armdown)
  WS2812B BLU - Added segment (armup)
  .   _____
     /  a  \    leda  0,  1,  2
   /\\_____//\
  |  |     |  | ledb  3,  4,  5
  | f|     |b | ledf  6,  7,  8
  |  |_____|  |
   \//  g  \\/  ledg  9, 10, 11
   /\\_____//\
  |  |     |  | ledc 12, 13, 14
  | e|     |c | lede 15, 16, 17
  |  |_____|  |
   \//  d  \\/  ledd 18, 19, 20
     \_____/
*/

const byte digits[11] = {
  //xGFEDCBA LED segments
  0b00111111, //0
  0b00000110, //1
  0b01011011, //2
  0b01001111, //3
  0b01100110, //4
  0b01101101, //5
  0b01111100, //6
  0b00000111, //7
  0b01111111, //8
  0b01100111, //9
  0b00000000, //all off
}; 

void setup() {
  Serial.begin(115200);

  FastLED.addLeds<WS2812B, DATAPIN, GRB>(led, NUMPIX);
  FastLED.setBrightness(MAXBRIGHT);
  FastLED.clear();
  FastLED.show();

  for (byte i = 0; i < SEGMENTS; i++) {
    servo[i].write(armdown);     // configure arms "down"
    servo[i].attach(srvpin[i]);  // servos 0 - 6 attached to servo pins
  }

  delay(1000); // settle servos
  // test(); // cycle through segment servos and WS2812B
  displayLED(0);
  FastLED.show();
  displayServo(0);
  delay(820); // 180ms to move servos 90 degrees
}

void loop() {
  unsigned long servotimeout = 820;  // 1000ms/second - 180ms/servomove = 820ms
  if (millis() - timer > servotimeout) {
    timer = millis();  // set new timer

    if (count > 9)  // upper bounds reached...
      count = 0;    // reset value

    count++;  // increase digit count

    displayLED(count);
    FastLED.show();
    displayServo(count);
  }
}

void displayServo(const byte number) {
  byte pattern = digits[number];
  int armPosition;
  Serial.println(number);
  for (size_t seg = 0; seg < 7; seg++) {
    if (((pattern >> seg) & 0x01) == 0) {
      armPosition = armdown;
    } else {
      armPosition = armup;
    }
    Serial.print(servo[seg].read());
    Serial.print("  ");
    Serial.print(armPosition);
    if (armPosition != servo[seg].read()){
      servo[seg].write(armPosition);
      Serial.print(" << writing");
    }
    Serial.println();
  }
}


void displayLED(const byte number) {
  byte pattern = digits[number];
  FastLED.clear();
  for (size_t seg = 0; seg < 7; seg++) {
    if (((pattern >> seg) & 0x01) == 0x01) {
      for (size_t l = 0; l < 3; l++) {
        led[seg * 3 + l] = CRGB(191, 255, 255);
      }
    }
  }
}

The call to 'write()' just sets a pulse width and return quickly. It doesn't wait for the servo to reach the position, there's no feedback from the servo