The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Category: Electronics Workbench

Electrical & Electronic gadgets

  • Arduino: Using Ancient Litronix DL-1414 LED Displays

    Tek 492 Memory Board Reader
    Tek 492 Memory Board Reader

    While I was putting together the Tek 492 memory board reader, I though it’d be a nice idea to add a small display. While the Arduino has USB serial I/O, the update rate is fairly pokey and an on-board display can provide more-or-less real time status.

    Given that the reader was built for an early-80s memory board, I just had to use a pair of Litronix DL-1414 4-character LED displays from my parts heap. The DL-1414 datasheet [Update: link rot? try that] proudly proclaims:

    The 0.112″ high characters of the DL1414T gives readability up to eight feet. The user can build a display that enhances readability over this distance by proper filter selection.

    I think that distance is exceedingly optimistic.

    DL-1414 LED display schematic
    DL-1414 LED display schematic

    However, I needed to see only a few feet to the benchtop. Even better, adding the displays required no additional hardware: the SPI-driven shift registers on the board already had address and data lines, plus a pair of unused bits for the write strobes. What’s not to like?

    This schematic connects to the one you just clicked on; the two big blue bus lines are the same bus as in that schematic.

    If you don’t have anything else riding the data bus, adding a pullup on the D7 bit that isn’t used by these displays will make all the bits float high; the DL-1414s seem to pull their inputs upward. That came in handy when I was debugging the EPROM-burning code, because reading data without an EPROM in the socket produced 0xff, just like an erased EPROM byte.

    The two displays are the dark-red rectangle in the lower-right of the first picture, covered with a snippet of the Primary Red filter described there.

    These closeups, without and with the filter, demonstrate why you really, really need a filter of some sort.

    DL1414 Unfiltered
    DL1414 Unfiltered
    DL1414 Filtered
    DL1414 Filtered

    Using the displays is straightforward, given the hardware-assisted SPI code from there. You could actually do it with just the I/O pins on an Arduino board, but you wouldn’t be able to do anything else. If you don’t have any other SPI registers, you could get away with a pair of HC595 outputs:  7 data + 2 address + 2 strobes + 5 outputs left over for something else.

    A few constants set the display size and a global buffer holds the characters:

    #define LED_SIZE            4            // chars per LED
    #define LED_DISPLAYS        2            // number of displays
    #define LED_CHARS           (LED_DISPLAYS * LED_SIZE)
    
    char LEDCharBuffer[LED_CHARS + 1];       // raw char buffer, can be used as a string
    

    A routine to exercise the LEDs by scrolling all 64 characters they can display goes a little something like this:

    Serial.println("Exercising LED display ...");
    
     Outbound.Controls |= CB_N_WRLED1_MASK | CB_N_WRLED0_MASK;        // set write strobes high
     digitalWrite(PIN_DISABLE_DO,LOW);                                // enable data outputs
    
     while (digitalRead(PIN_PB)) {
    
      digitalWrite(PIN_HEARTBEAT,HIGH);
    
      byte Character, Index;
    
      for (Character = 0x20; Character < 0x60; ++Character) {
       for (Index = 0; Index < LED_CHARS; ++Index) {
        LEDCharBuffer[Index] = Character + Index;
       }
       UpdateLEDs(LEDCharBuffer);
       delay(500);
    
       if (!digitalRead(PIN_PB)) {
        break;
       }
      }
    
      digitalWrite(PIN_HEARTBEAT,LOW);
     }
    
     WaitButtonUp();
    

    A routine to plop a string (up to 8 characters!) on the LEDs looks like this:

    void UpdateLEDs(char *pString) {
    byte Index = 0;
    
     while (*pString && (Index < LED_CHARS)) {
    
      Outbound.DataOut = *pString;           // low 6 bits used by displays
      Outbound.Address = ~Index;             // low 2 bits used by displays, invert direction
      Outbound.Controls &= ~(Index < LED_SIZE ? CB_N_WRLED1_MASK : CB_N_WRLED0_MASK);
    
      RunShiftRegister();
    
      digitalWrite(PIN_DISABLE_DO,LOW);      // show the data!
    
      Outbound.Controls |= CB_N_WRLED1_MASK | CB_N_WRLED0_MASK;
      RunShiftRegister();
    
      digitalWrite(PIN_DISABLE_DO,HIGH);     // release the buffers
    
      ++pString;
      ++Index;
     }
    
    

    You can use sprintf() to put whatever you like in that string:

    void ShowStatus(word Address,byte Data) {
    
     sprintf(LEDCharBuffer,"%04X  %02X",Address,Data);
     UpdateLEDs(LEDCharBuffer);
    
    }
    

    Not, of course, that anybody would actually use DL-1414 displays in this day & age, but the general idea might come in handy for something more, mmmm, elegant…

  • Tektronix 492 Spectrum Analyzer ROM and EPROM HEX Files

    Tek 492 Memory Board
    Tek 492 Memory Board

    Having gotten my buddy Eks back on the air with new EPROMs for his Tek 492 spectrum analyzer, here are the Tek 492 ROM Images (← that’s the link to the file!) you’ll need to fix yours.

    [Update: the comments for that post have pointers to other images and a clever hack to use a standard EPROM. If you’re not a stickler for perfection, that’s the way to go.]

    They’re taken from a “known good” Tek 492, so they should work fine: the firmware verifies the checksum in each chip as part of the startup tests; if it’s happy, we’re happy.

    Because WordPress doesn’t allow ZIP files, I tucked the HEX files into an OpenDocument file that also contains the pinouts and some interposer wiring hints & tips.

    If you’re using the OpenOffice.org word processor, you’re good to go. Open the document and get all the instructions you need to extract the files and put them to good use.

    If you’re not using OOo, then choose one of:

    • Install OpenOffice.org (it’s free software, so kwitcher bitchin’)
    • Futz with whatever Microsoft claims will import ODT files (if it doesn’t work, don’t blame me)
    • Just extract the HEX files and do whatever you want (if you know what you want)

    The trick, explained in the document itself, is that ODT files are just ZIP files with a different file extension, so any unzip program will unpack them. You won’t see the HEX files in the document, you must apply unzip to the ODT file itself.

    After unzipping, you’ll find three HEX files in the directory that originally held the ODT file, along with the collection of files that make up the OpenDocument document.

    The only files you care about:

    U1012 – 160-0886-04.hex
    U2023 – 160-0838-00.hex
    U2028 – 160-0839-00.hex

    Use ’em in good health…

    Oh, if you haven’t already figured it out, the DIP switch on your board is also bad. Saw the damn thing apart with a Dremel tool, pry off the debris, unsolder the pins, and install a new one. Just Do It.

  • Tektronix 492 Spectrum Analyzer Backplane Pin Spacing

    Tek 492 Memory Board
    Tek 492 Memory Board

    My buddy Eks asked me to help fix his new-to-him and guaranteed broken Tek 492 spectrum analyzer, which turned into a tour-de-force effort. One sub-project involved sucking the bits out of an existing “known-good” Tek memory card, which meant building a backplane connector and a circuit that behaved like a 6800 microcontroller… fortunately, it could be a lot slower.

    [Update: It seems searches involving “Tektronix 492” produce this page. You may also be interested in these posts…

    If those aren’t what you’re looking for, note that the correct spelling is “Tektronix“.

    Good luck fixing that gadget: it’s a great instrument when it works!]

    You can tell just by looking that this board was designed back in the day when PCB layout involved flexible adhesive tape traces and little sticky donut pads. Ground plane? We don’t need no stinkin’ ground plane!

    Actually, it’s a four-layer board done with the usual Tek attention to detail. They didn’t need a ground plane because they knew what they were doing. Remember, this is in a spectrum analyzer with an 18-GHz bandwidth and 80 dB dynamic range; a little digital hum and buzz just wouldn’t go unnoticed.

    Tek 492 Backplane Geometry
    Tek 492 Backplane Geometry

    Anyhow, the backplane pins are on a 0.150-inch grid within each block. The center block (pins 13-36) is 0.200 inches from the left block (pins 1-12) and 0.250 from the right block (pins 37-60).

    That means the left and right blocks are neatly aligned on the same 0.150-inch grid, with the middle block offset by 50 mils. You can’t plug the board in backwards unless you really work at it.

    Of course, Eks had some genuine gold-plated Tek pins in his stash: 24 mils square and 32 mils across the diagonal. They have 1/4″ clear above the crimped area that anchors them to the black plastic spacer and are 1/2″ tall overall. They’re not standard header pins, but I suspect you could use some newfangled pins in a pinch.

    Here’s what the reader board finally looked like, hacked traces and all, with the board connector to the rear. The memory board didn’t use all the backplane pins, so I only populated the ones that did something useful. The power-and-ground pins (left side of right pin block) stand separately from the other because I had to solder them to both the top and the bottom of the board: no plated-through holes!

    Tek 492 Memory Board Reader
    Tek 492 Memory Board Reader

    I cannot imagine this being useful to anybody else, but I defined an Eagle part for the connector so I could CNC-drill the board. Drop me a note and I’ll send it to you.

    [Update: this turned into a Circuit Cellar column, so you can fetch a ZIP file from their FTP site that has all manner of useful stuff.]

    Memo to Self: The drill size follows the pin’s diagonal measurement… not the side! Duh.

  • Useful Hex and Binary File Utilities

    I’m doing some work with a one-off ROM reader & EPROM programmer, so it’s once again time to mess around with Intel HEX files, raw binary images, and the like.

    The key routine (which runs on an Arduino Decimila) to dump a ROM in HEX format goes like this, with all the constants & variables & functions doing the obvious things:

    void DumpHC641(void) {
    
    word Address,Offset;
    byte DataRd,Checksum;
    
     for (Address = 0; Address < ROM_SIZE; Address += IHEX_BYTES) {
      sprintf(PrintBuffer,":%02X%04X00",IHEX_BYTES,(word)Address);           // emit line header
      Serial.print(PrintBuffer);
      Checksum = IHEX_BYTES + lowByte(Address) + highByte(Address) + 0x00;   // record type 0x00
      for (Offset = 0; Offset < IHEX_BYTES; ++Offset) {
       digitalWrite(PIN_HEARTBEAT,HIGH);
       DataRd = ReadHC641(Address + Offset);
       digitalWrite(PIN_HEARTBEAT,LOW);
       Checksum += DataRd;
       sprintf(PrintBuffer,"%02X",DataRd);                                   // data byte
       Serial.print(PrintBuffer);
      }
      Checksum = -Checksum;                                                  // two's complement
      sprintf(PrintBuffer,"%02X",Checksum);
      Serial.println(PrintBuffer);
     }
     Serial.println(":00000001FF");                                          // emit end-of-file line
    }
    

    So getting an Intel HEX file is just a matter of capturing the serial output, whacking off any debris on either side of the main event, and saving it.

    The srec_cat program handles conversions among a myriad formats, most of which I can’t even pronounce. The few I use go a little something like this:

    srec_cat mumble.hex -intel -o mumble.bin -binary
    srec_cat mumble.bin -binary -o mumble.hex -intel
    srec_cat mumble.bin -binary -o mumble.txt -hex_dump
    srec_cat -generate 0x0000 0x2000 -constant 0x00 -o mumble.txt -intel
    

    It’s sometimes handy to apply srec_cat to a group of similarly suffixed files, in which case some Bash string chopping comes in handy. For example, to convert some hex files into binary:

    for f in 27HC641*hex ; do echo ${f%%hex} ; srec_cat "$f"  -intel -o "${f%%hex}"bin -binary ; done
    

    Good old diff works fine on text files, but in this case it’s better to see which bytes have changed, rather than which lines (which don’t apply in the context of a binary file). The vbindiff program looks great on a portrait-mode display.

    I don’t do much binary editing, but tweak serves my simple needs. Confusingly, members of this class of program are called “hex editors”, but they really work on binary files.

    There’s also diff3, for those rare cases where you must mutually compare three text files. Makes my head spin every time…

    All those programs are likely packages in your favorite Linux distro.

  • Trimming Voltage Regulators by Stacking SMD Chips

    LM317 Regulator (Partial) Schematic
    LM317 Regulator (Partial) Schematic

    Quite often, the values you need for voltage regulators, like the venerable LM317 and its ilk, don’t work out to anything you have in your parts bin. What to do?

    One of the really nice things about SMD resistors is that you can stack them up without much effort. That parallels their value, so you can only make the final value smaller than any of the stacked resistors, but we can work with that.

    The schematic shows part of a multi-voltage power supply for the EPROM programmer I mentioned there. Normally you use a 240-Ω resistor between the Output and Adjust terminals, but anything in that range will work fine. Alas, when I went to the parts bin, that’s the value I didn’t have any of.

    But, having recently acquired an assortment of 60-some-odd 1% chip resistors, 100 to the bag, I had enough raw material to make it work. In fact, the values in the schematic reflect the parts on hand, which is how it sometimes happens.

    A pair of 499-Ω resistors in parallel gives you 249.5 Ω, close enough to 240 (and shown as 250 because that’s only 0.2% off). Plug that value and the desired voltages into the LM317 equation to find the other resistors:

    12.5 V = 1.25 * (1 + R / 250)

    R = 250 * ((12.5 / 1.25) – 1) = 2250 Ω

    If you happen to have something close to that in your parts heap, great. I didn’t, and a stock 2200 Ω 5% resistor would produce 12.25 V; a bit lower than I wanted.

    Three sets of stacked chip resistors
    Three sets of stacked chip resistors

    This pic shows the solution: stack some SMD resistors to get the right value.

    To make this work easily, you need a calculator that has a reciprocal (1/x or x-1) key. My ancient HP-48 does that, natch, but both of the Official School Calculators our daughter uses has 1/x as a shifted function. Your mileage will certainly vary.

    Anyhow, the reciprocal of the resistance of two parallel resistors, RA and RB, is the sum of their reciprocals. Got that?

    1/R = 1/RA + 1/RB

    If you know the total resistance R that you want and one of the resistors RA, then you find the other resistor RB thusly:

    1/RB = 1/R – 1/RA

    In order to get 2250 Ω, I started with the next higher value in the assortment, 2740 Ω, and turned the crank:

    1/RB = 1/2250 – 1/2740 = 79.4809E-6

    RB = 1/79.4809E-6 = 12.58 kΩ

    As it happens, the assortment didn’t have that value, either, but it did have 15 kΩ. The parallel resistance of 2740 and 15 k is:

    2317 = 1 / (1/2740 + 1/15000)

    So turn the crank one more time to find the third resistor RC:

    1/RC = 1/2250 – 1/2317 = 12.814E-6

    RC = 78.04 kΩ

    Well, that isn’t one of the values I have either, but I do have 82.5 kΩ. The parallel value of those three resistors is:

    1/R = 1/2740 + 1/15000 + 1/82500 = 443.75E-6

    R = 2254 Ω

    Which is 0.1% off the desired value. Close enough.

    Actually, it won’t be nearly that close, because the 2740 Ω resistor can be off by 27 Ω either way. If you really care, measure the actual values and feed those into the equations. If, of course, you can measure resistors better than 1% and you don’t care about temperature effects and suchlike.

    This is appropriate for one-off projects and prototypes, not production runs, but it’s a handy trick to keep in mind. If you want to be fancy, you can lay the circuit board out with parallel resistor tracks and make it look like you knew what you were doing all along…

  • Batteries.com Alkaline AA Cells: Early Failures Thereof

    Swollen vs normal alkaline AA cells
    Swollen vs normal alkaline AA cells

    I’ve bought plenty of batteries from batteries.com over the years, but the alkaline AA cells I picked up last year have been a real disappointment: some had very short service lives. It took quite a while to figure this out, as I mentioned there, and when I finally got around to checking the rest of the package, most of them were dead… in Spring 2009 with a 12-2012 date code.

    One characteristic of the weak / dead cells is that the negative terminal is swollen, even on the deaders direct from the package. This picture shows four cells removed from service: the front two are used with some remaining charge, the rear two are dead.

    When I checked the package, most of the dead-on-delivery cells had swollen bottoms, so I suspect they had a manufacturing problem with at least one batch of cells.

    A query to batteries.com asking about this got no reply. Perhaps they were busy dealing with the aftermath of their security breach?

    A 48-pack of alkaline cells from the late Circuit City, bought about the same time, seems just fine.

    Memo to self: check the bottom!

  • Arduino Hardware-assisted SPI: Synchronous Serial Data I/O

    Many interesting projects require more digital output bits than the Arduino hardware can support. You then use 74HC595 serial-in/parallel-out chips and that tutorial pretty well explains how it works. The shiftOut() library function squirts a byte out through an arbitrary pin, leading with either the high or low bit.

    Software SPI: Clock and Data
    Software SPI: Clock and Data

    Just drop one byte into shiftOut() for each ‘595 lined up on your board. Remember to latch the bits (LOW-to-HIGH on RCK @ pin 12 of the ‘595) and enable the output drivers (LOW on -G @ pin 13, similarly) when you’re done sending everything. You can have separate latches-and-enables for each ‘595 if that suits your needs, although then you once again run out of Arduino bits pretty quickly. It’s entirely possible to devote a ‘595 to latches-and-enables for the rest of the chain, but that gets weird in short order.

    The scope shot shows that shiftOut() ticks along at 15 µs per bit (clock in the upper trace, data in the lower trace). For back-of-the-envelope purposes, call it 8 kB/s, which is probably less than you expected. If you have a string of 5 external bytes, as I did on a recent project, that’s only 1600 updates / second. It was part of a memory board reader & EPROM programmer: reading an 8 kB ROM chip requires two shift-register runs (one to set the address & data, one to read in the chip output), so the overall rate was on the order of 10 seconds per pass and much worse for programming. You can optimize the number of bits by not shifting out all the bytes, but that’s the general idea.

    Because ‘595 chips are output-only, in order to get 8 bits of data into the Arduino board, add a 74HC166 parallel-in/serial-out chip to the string. Alas, shiftOut() doesn’t know about input bits, so you’re on your own.

    Hardware SPI: Clock and Data
    Hardware SPI: Clock and Data

    If you’re going to have to write some code to get input bits anyway, you may as well use the ATmega168 (and its ilk) hardware SPI as it was intended to be used: for high-speed synchronous serial I/O. This scope shot shows the SPI clock (in the top trace again) ticking along at 1 µs per bit, which is 1/16 the Diecimila’s oscillator frequency. You can pick any power of two between 1/2 and 1/128; I used 1/16 because it’s fast enough to make the rest of the software the limiting factor, while slow enough to not require much attention to layout & so forth.

    Start by Reading The Fine Manual section about the ATmega168’s SPI hardware, starting at page 162.

    The pin definitions, being lashed to internal hardware, are not optional. Note that SCK is also the standard Arduino LED, which won’t be a problem unless you need a tremendous amount of drive for a zillion ‘595s. I stuck an additional LED on Arduino digital pin 2.

    #define PIN_HEARTBEAT     2             // added LED
    #define PIN_SCK          13             // SPI clock (also Arduino LED!)
    #define PIN_MISO         12             // SPI data input
    #define PIN_MOSI         11             // SPI data output
    

    Initial hardware setup goes in the usual setup() function:

    pinMode(PIN_SCK,OUTPUT);       // set up for "manual" SPI directions
    digitalWrite(PIN_SCK,LOW);
    pinMode(PIN_MOSI,OUTPUT);
    digitalWrite(PIN_MOSI,LOW);
    
    pinMode(PIN_MISO,INPUT);       // configure inputs
    digitalWrite(PIN_MISO,HIGH);
    
    SPCR = B01110001;              // Auto SPI: no int, enable, LSB first, master, + edge, leading, f/16
    SPSR = B00000000;              // not double data rate
    

    Basically, the “manual” setup allows you to wiggle the bits by hand with the hardware SPI control disabled.

    Arduino Hardware SPI Schematic
    Arduino Hardware SPI Schematic

    Here’s a chunk of the schematic so you can see how the bits rattle around. You’ll surely want to click it to get the details…

    I put the data in a structure that matches the shift register layout, with the first byte (Controls) connected to the ATmega’s MOSI pin and the last byte (DataIn) connected to MISO. The SCK pin drives all of the serial clock pins on the ‘595 and ‘166 chips in parallel. Your structure will certainly be different; this was intended to suck data from a Tek 492 Spectrum Analyzer memory board.

    typedef struct {      // external hardware shift register layout
     byte Controls;       // assorted control bits
     word Address;        // address value
     byte DataOut;        // output to external devices
     byte DataIn;         // input from external devices
    } SHIFTREG;
    
    SHIFTREG Outbound;    // bits to be shifted out
    SHIFTREG Inbound;     // bits as shifted back in
    

    The functions that make it happen are straightforward:

    void TogglePin(char bitpin) {
     digitalWrite(bitpin,!digitalRead(bitpin));
    }
    void PulsePin(char bitpin) {
     TogglePin(bitpin);
     TogglePin(bitpin);
    }
    
    void EnableSPI(void) {
     SPCR |= 1 << SPE;
    }
    
    void DisableSPI(void) {
     SPCR &= ~(1 << SPE);
    }
    
    void WaitSPIF(void) {
     while (! (SPSR & (1 << SPIF))) {
    //        TogglePin(PIN_HEARTBEAT);       // use these for debugging!
    //        TogglePin(PIN_HEARTBEAT);
     continue;
     }
    }
    
    byte SendRecSPI(byte Dbyte) {             // send one byte, get another in exchange
     SPDR = Dbyte;
     WaitSPIF();
     return SPDR;                             // SPIF will be cleared
    }
    
    void CaptureDataIn(void) {                // does not run the shift register!
     digitalWrite(PIN_ENABLE_SHIFT_DI,LOW);   // allow DI bit capture
     PulsePin(PIN_SCK);                       // latch parallel DI inputs
     digitalWrite(PIN_ENABLE_SHIFT_DI,HIGH);  // allow DI bit shifting
    }
    
    void RunShiftRegister(void) {
     EnableSPI();                             // turn on the SPI hardware
    
     Inbound.DataIn  = SendRecSPI(Outbound.DataIn);
     Inbound.DataOut = SendRecSPI(Outbound.DataOut);
    
     Inbound.Address  =         SendRecSPI(lowByte(Outbound.Address));
     Inbound.Address |= ((word) SendRecSPI(highByte(Outbound.Address))) << 8;
    
     Inbound.Controls = SendRecSPI(Outbound.Controls);
    
     PulsePin(PIN_LATCH_DO);                   // make new shift reg contents visible
     PulsePin(PIN_LATCH_ADDRESS);
     PulsePin(PIN_LATCH_CONTROLS);
    
     DisableSPI();                             // return to manual control
    }
    

    Actually using the thing is also straightforward. Basically, you put the data-to-be-sent in the Outbound variables and call RunShiftRegister(), which drops output bytes into SPDR and yanks incoming bytes out, then stuffing them in the Inbound variables. I have separate latch controls for the Controls, Address, and Data chips, although I don’t use them separately here.

    You must wiggle the parallel latch enable line on the 74HC166 chip before shifting to capture the data, as shown in CaptureDataIn(). That chip also requires a separate pulse on its serial clock line to latch the data, which you do manually with the hardware SPI disabled. If you’re paying attention, you’ll wonder if that clock pulse also screws up the data in the rest of the chips: yes, it does. If this is a problem, you must add some external clock-gating circuitry, disable the ‘595s, or pick a different input shift register chip; it wasn’t a problem for what I was doing.

    Here’s a function that reads data from a RAM chip on the Tek memory board, so it must write the address and read the RAM chip’s output. The PIN_DISABLE_DO bit controls the output buffers on the ‘595 that drives the RAM’s data pins; they must be disabled to read data back from the RAM. Don’t worry about the other undefined bits & suchlike; just assume everything does what the comments would have you believe.

    byte ReadRAM(word Address) {
     digitalWrite(PIN_DISABLE_DO,HIGH);            // turn off data latch output
     digitalWrite(PIN_BUS_READ,HIGH);              // allow RAM read access
    
     Outbound.Controls |=  CB_BUS_CLKPH2_MASK;     // set up RAM -CS gate
     Outbound.Address = Address;
     Outbound.DataOut = 0x55;                      // should not be visible
     RunShiftRegister();
    
     digitalWrite(PIN_BUS_N_SYSRAM,LOW);           // activate RAM -CS
     CaptureDataIn();                              // latch RAM data
     digitalWrite(PIN_BUS_N_SYSRAM,HIGH);          //  ... and turn -CS off
    
     Outbound.Controls &= ~CB_BUS_CLKPH2_MASK;     // disable -CS gate
     RunShiftRegister();                           // tell the board and get data
    
     return Inbound.DataIn;
    }
    
    Hardware SPI - Detail of clock and data timing
    Hardware SPI – Detail of clock and data timing

    Here’s a detailed shot of the outbound bit timing. Notice that the upward clock transitions shift bits into the ‘595 and ‘166 chips, while the SPI output data changes on the downward transitions. You can tweak that to match your hardware if you’re using different shift register chips, by messing with the SPCR settings.

    Bottom line: using the ATmega168 hardware SPI provided a factor-of-15 speedup and serial digital input, too.