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

  • Toner Transfer PCBs: Alignment Accuracy

    Here’s an example of the dimensional accuracy you can get from toner-transfer PCBs in real life.

    I drill the holes with a CNC-ed Sherline mill, so they’re pretty much spot on. Drilling the holes by hand simply isn’t practical: there’s no way to get both global alignment and local accuracy.

    The toner transfer sheet, printed on a laser printer, gets aligned to the existing holes atop a light table. The paper stretches & shrinks and moves around while printing, but I can generally average out the errors so that the 24-mil holes (the smallest I generally use) across the board have no more than a few mils of error: the pads don’t show more than that inside the drilled holes. In the picture below, you can see a dark rim around the corner alignment hole that looks worse than it really is due to the perspective.

    I put the toner transfer sheet on the light table, toner-side up, lay the PCB atop the paper, and adjust for best overall alignment. I then tape them together along one edge with strips of laser-printer address labels: guaranteed to hold up to high temperatures, which is more than you can say for most tapes.

    PCB alignment and taping
    PCB alignment and taping

    Here’s the board after etching both sides, with the black toner and green sealant film still in place. The toner & film are slightly smeared from the solvent I used to clean off the other side before etching it. The brownish dabs on the green areas come from a brown Sharpie that works fine as a touch-up etching resist.

    WWVB Simulator - Top surface toner mask
    WWVB Simulator – Top surface toner mask

    The narrowest traces are 16 mils, most of the others are 32 mils, and the fat ones down the middle of the chip are 40 mils. Click on the images for bigger versions; you’ll get some JPG compression artifacts, but the resolution is good enough to see what’s going on.

    Here’s the same area with the toner removed and a touch of silver plating applied to make it pretty and more easily solderable. The colors aren’t particularly reliable; in real life, it’s a lot more silvery.

    Top surface copper
    Top surface copper

    Fairly obviously, the alignment isn’t nearly as good as you’d expect from the initial taping. In round numbers, the pads to the left side seem offset by about the diameter of the holes; call it 25 mils. The holes in the DIP pads are off by perhaps 10 mils.

    The bottom surface looks pretty much the same, with similar alignment issues.

    Bottom surface copper
    Bottom surface copper

    The misalignments are not uniform, as you’d expect if the toner transfer sheet moved across the board during fusing. The sheet deforms during the fusing process in a completely unpredictable way, despite my trying all of the usual tricks:

    • Pre-shrinking the transfer paper by running it through the printer with a pure-white image (so no toner gets applied)
    • Fusing quickly after printing to prevent moisture absorption (there’s a limit to how fast I can work)
    • Taping more than one edge to lock the paper in place

    It’s fair to say you (well, I) can get within 25 mils of a board hole for sure, less than that most of the time, and be spot on over much of the board. I use large pads and vias for anything I have control over, as witness the pads surrounding the DIP, and avoid very fine features near holes.

    Anyhow, it’s good enough for what I do, but you shouldn’t get your hopes up that toner-transfer circuit boards come anywhere close to commercial quality. If you’re doing a lot of pure surface-mount work, it’ll probably be good enough because there’s no need for global alignment to holes in the underlying board. Obviously, the smaller the board, the better off you’ll be.

    I etched this board by rubbing ferric chloride on it with a sponge (wearing forearm-length rubber gloves and a shop apron!), renewing the solution as it turned black and gooey. Works like a charm, gives good control of the process, doesn’t erode the Sharpie masking, doesn’t over-etch the traces (much, anyway), and uses less etchant than soaking the board in a bath.

    I have other posts describing the process in more detail. Search for PCB, toner-transfer, and other keywords to unearth those entries.

  • Converting Day-of-Year to Month and Day-of-Month, Then Back Again

    I needed the conversions for a WWVB simulator, so it knows when to twiddle the leap-year and leap-second bits.

    The general notion is a table with the Day-of-Year values for the last day of each month. The most expedient way of doing this is with two columns: one for normal years and the other for leap years, thusly…

    Month EOM EOM-LY DOY DOY-LY
    0 0 0 0 0
    1 31 31 31 31
    2 28 29 59 60
    3 31 31 90 91
    4 30 30 120 121
    5 31 31 151 152
    6 30 30 181 182
    7 31 31 212 213
    8 31 31 243 244
    9 30 30 273 274
    10 31 31 304 305
    11 30 30 334 335
    12 31 31 365 366

    Hint: even if you can recite the “Thirty days hath November …” jingle, it’s much better to build a spreadsheet so the additions all work out. It’s even better if you don’t attempt any of this with a late-summer head cold. You might want to check all my work, because I’m still stuffy.

    With table in hand, the code is straightforward.

    Define a structure with all the various bits & pieces of the current time, much of which isn’t used here. It’s all needed in the WWVB simulator:

    enum EVENTSTATE {EVENT_INACTIVE,EVENT_PENDING,EVENT_ACTIVE};
    
    struct timecode_ {			// The current moment in time...
      byte Year;				// 0 - 99
      word DayOfYear;			// 1 - 366
      byte Hour;				// 0 - 23
      byte Minute;				// 0 - 59
      byte Second;				// 0 - 60 (yes!)
      byte Tenth;				// 0 - 9
      enum EVENTSTATE LeapYear;		// 0 = no, 1 = pending, 2 = active
      enum EVENTSTATE LeapSecond;		// 0 = no, 1 = pending, 2 = active in this minute
      enum EVENTSTATE DaylightSavingTime;	// 0 = no, 1 = pending, 2 = active
      char UT1Correction;			// 100 ms units, -10 to +10 range (+/- 1 second)
      byte MinuteLength;			// 60 or 61
      byte Month;				// 1 - 12 (not sent in frame)
      byte DayOfMonth;			// 1 - 28..31	(not sent in frame)
    };
    

    That’s obviously overspecified, because DayOfYear with LeapYear uniquely determines Month and DayOfMonth. It’s handy to have both forms around, sooo there they are.

     

    Then set up the table, glossing over the quick matrix transposition that turns the entries for each year into rows rather than columns:

    prog_uint16_t PROGMEM MonthEnds[2][13] = {
      0,31,59,90,120,151,181,212,243,273,304,334,365,
      0,31,60,91,121,152,182,213,244,274,305,335,366
    };
    

    Conversion from DayOfYear to Month and DayOfMonth requires searching backwards through the appropriate table row until you find the entry that’s smaller than the DayOfYear value, at which point you’ve found the right month.

    void ConvertDOYtoDOM(struct timecode_ *pTime) {
    byte Index,LY;
    word EndOfMonth;
      LY = (EVENT_INACTIVE != pTime->LeapYear) ? 1 : 0;
      Index = 12;
      while ((EndOfMonth = pgm_read_word(&MonthEnds[LY][Index])) >= pTime->DayOfYear) {
    	--Index;
      };
      pTime->Month = Index + 1;									// months start with 1, not 0
      pTime->DayOfMonth = (byte)(pTime->DayOfYear - EndOfMonth);
    }
    

    Converting from Month and DayOfMonth to DayOfYear is much easier, as it’s pretty much just a table lookup:

    word ConvertDOMtoDOY(struct timecode_ *pTime) {
    word EndOfMonth;
      EndOfMonth = pgm_read_word(&MonthEnds[(EVENT_INACTIVE != pTime->LeapYear) ? 1 : 0][pTime->Month - 1]);
      return EndOfMonth + pTime->DayOfMonth;
    }
    

    This code might actually work, but if I were you, I’d test it pretty thoroughly before lashing it into your project…

     

    The PROGMEM and pgm_read_word() stuff is the Arduino mechanism that puts the lookup table into Flash program memory rather than the ATMega168’s exceedingly limited RAM space. The definitive word about that process resides there.

    Memo to Self: Using const simply makes the variable kinda-sorta read-only, but still initializes RAM from Flash. The PROGMEM routines delete the RAM copy entirely.

  • Arduino: Dividing an External Frequency

    A friend asked for a Totally Featureless Clock (it’s a long story) and in order to build that, I must concoct a WWVB simulator. Not needing really precise atomic time, I can use ordinary bench-grade crystals and suchlike; microsecond-level jitter isn’t a big problem.

    Anyhow, I must divide down an external 60 kHz signal to produce a 10-Hz interrupt to drive the modulator. The 60 kHz comes from a 12 MHz crystal, through a divide-by-200 counter, and feeds the ATmega T1 input (aka Arduino pin 5, normally the PWM5 output).

    The key is setting up Timer1 to divide T1 inputs by 6000 (count 0 through 5999), then have it produce an interrupt when the maximum count occurs. You’ll want to read Chapter 15 of The Fine Manual to learn how the hardware works.

    I used CTC Mode 12, so that the counts occur on the falling edge of the signal on T1 with the maximum value stored in ICR1. That causes the Input Capture Interrupt to occur when ICR1 == TCNT1.

    Review the avr-lib interrupt doc to get the proper interrupt vector names. You want the ICF interrupt, enabled with ICIE1.

    Note that Table 15-4 is misleading. The TOV1 Flag may be set when TCNT == MAX, but unless ICR1 == MAX it’ll never get there. You (well, I) can spend a distressing amount of time figuring out why TOV1 doesn’t happen.

    With that in mind, Timer1 setup is straightforward:

    TCCR1B = 0;                // stop Timer 1 by shutting off the clock
    TCNT1 = 0;                 // force count to start from scratch
    TCCR1A = 0;                // no compare outputs to OC1A OC1B, WGM1 1:0 = 00
    TCCR1C = 0;                // no forced compares
    ICR1 = 5999;               // count 0 through 5999 = divide by 6000
    TIMSK1 = 1 << ICIE1; // allow interrupt on capture event (TCNT == ICF)
    TCCR1B = B00011110;        // start Timer 1: CTC mode = 12, TOP=ICR1, ext clock on T1, falling edge
    

    The interrupt handler can do whatever you want. This one just flips an output bit (ultimately connected to the modulator) to show it’s arrived:

    ISR(TIMER1_CAPT_vect) {
       PINB |= _BV(1);
    }
    

    That weird-looking line takes advantage of an Arduino hardware feature: if you write a 1 to a bit in the PIN register, the corresponding PORT value toggles. It’s documented in The Fine Manual on page 74, section 13.2.2 and mentioned there.

    Then the rest of the simulator is just a simple matter of software…

  • Arduino vs. ATMega168 Chip Pinouts

    The Arduino pin names are silkscreened right on the board, but sometimes you must know the corresponding ATMega168 pin name. I printed out The Fine Manual and penciled in the Arduino names, but that’s getting smudgy.

    Herewith, the ATmega168 pinout with neatly printed Arduino pin names.

    Arduino vs ATMega168 chip pinouts
    Arduino vs ATMega168 chip pinouts

    [Update:Turns out there’s an Official Version.]

    Sometimes, you also must know the relation between hardware Timers and PWM output pins:

    OC0A PWM6 PWM3 OC2B
    OC0B PWM5 PWM5 OC0B
    OC1A PWM9 PWM6 OC0A
    OC1B PWM10 PWM9 OC1A
    OC2A PWM11 PWM10 OC1B
    OC2B PWM3 PWM11 OC2A
  • Seiko Epson RTC-65271 Real Time Clock Datasheet

    RTC-65271 Module
    RTC-65271 Module

    I have a stash of RTC65271 real-time clock modules and might use one in an upcoming project. They’re obsolete by nigh onto two decades, but it’s a one-off project and I know I’ve been saving these things for some good reason.

    Alas, the datasheet doesn’t seem to appear anywhere else on the web; you can find an overview & general description, but not how the thing actually works.

    However, if you happen to have a chip and need the datasheet, this is indeed your lucky day: a scanned RTC65271 Datasheet.

    The datasheet alleges it’s “functionally compatible with MC146818A and DS1287“, and those datasheets may be more readable, if not exactly applicable. It seems to be (similar to) the clock chip used in the original PC/AT, if you recall those relics, and might actually use standard hardware & software protocols.

    Dealing with this thing may be more trouble than it’s worth in this day of bus-less microcontrollers with Serial Peripheral Interface widgetry. A back-of-the-envelope count says it’d require three ‘595 output chips and a ‘166 input chip to fit on an SPI bus. Yuch…

    Hey, if you want one, drop me a note. I have far more than a lifetime supply at my current rate of consumption.

  • Adding a Device to LTSpiceIV

    Searching around for an LM386 SPICE model turned up this useful thread.

    The model has some limitations, discussed there, but seems practical. So far, the main gotcha is that the output voltage doesn’t center neatly at Vcc/2, but that’s in the nature of fine tuning.

    The trick is getting the model & symbol into Linear Technology’s LTSpiceIV

    Running under WINE in Xubuntu, the emulated C drive is in your home directory at

    .wine/drive_c/

    with the Linear Tech LTSpiceIV files tucked inside that at

    .wine/drive_c/Program\ Files/LTC/LTspiceIV/

    Incidentally, WINE puts the program icon in

    .local/share/icons/05f1_scad3.0.xpm

    It’s not clear what the prefix means, but the actual executable is scad3.exe (I think that’s historical cruft, as the new overall name is LTSpiceIV).

    Copy the LM386.sub file to lib/sub and the LM386.asy file to lib/sym, then restart LTSpiceIV.

    After putting the symbol in the schematic, I had to edit its attributes (other-click the symbol), make both InstName & Value visible to see them on the schematic, then move them to somewhere other than dead-center in the symbol. I can’t figure out how to make that happen automagically, as it does with other symbols. Comparing the two files to ordinary components doesn’t show anything obviously missing.

    Link rot being what it is, here’s the LM386.sub file:

    * lm386 subcircuit model follows:
    
    ************************************original* IC pins:     2   3   7   1   8   5   6   4
    * IC pins:     1   2   3   4   5   6   7   8
    *              |   |   |   |   |   |   |   |
    .subckt lm386 g1  inn inp gnd out  vs byp g8
    ************************************original*.subckt lm386 inn inp byp  g1  g8 out  vs gnd
    
    * input emitter-follower buffers:
    
    q1 gnd inn 10011 ddpnp
    r1 inn gnd 50k
    q2 gnd inp 10012 ddpnp
    r2 inp gnd 50k
    
    * differential input stage, gain-setting
    * resistors, and internal feedback resistor:
    
    q3 10013 10011 10008 ddpnp
    q4 10014 10012 g1 ddpnp
    r3 vs byp 15k
    r4 byp 10008 15k
    r5 10008 g8 150
    r6 g8 g1 1.35k
    r7 g1 out 15k
    
    * input stage current mirror:
    
    q5 10013 10013 gnd ddnpn
    q6 10014 10013 gnd ddnpn
    
    * voltage gain stage & rolloff cap:
    
    q7 10017 10014 gnd ddnpn
    c1 10014 10017 15pf
    
    * current mirror source for gain stage:
    
    i1 10002 vs dc 5m
    q8 10004 10002 vs ddpnp
    q9 10002 10002 vs ddpnp
    
    * Sziklai-connected push-pull output stage:
    
    q10 10018 10017 out ddpnp
    q11 10004 10004 10009 ddnpn 100
    q12 10009 10009 10017 ddnpn 100
    q13 vs 10004 out ddnpn 100
    q14 out 10018 gnd ddnpn 100
    
    * generic transistor models generated
    * with MicroSim's PARTs utility, using
    * default parameters except Bf:
    
    .model ddnpn NPN(Is=10f Xti=3 Eg=1.11 Vaf=100
    + Bf=400 Ise=0 Ne=1.5 Ikf=0 Nk=.5 Xtb=1.5 Var=100
    + Br=1 Isc=0 Nc=2 Ikr=0 Rc=0 Cjc=2p Mjc=.3333
    + Vjc=.75 Fc=.5 Cje=5p Mje=.3333 Vje=.75 Tr=10n
    + Tf=1n Itf=1 Xtf=0 Vtf=10)
    
    .model ddpnp PNP(Is=10f Xti=3 Eg=1.11 Vaf=100
    + Bf=200 Ise=0 Ne=1.5 Ikf=0 Nk=.5 Xtb=1.5 Var=100
    + Br=1 Isc=0 Nc=2 Ikr=0 Rc=0 Cjc=2p Mjc=.3333
    + Vjc=.75 Fc=.5 Cje=5p Mje=.3333 Vje=.75 Tr=10n
    + Tf=1n Itf=1 Xtf=0 Vtf=10)
    
    .ends
    *----------end of subcircuit model-----------

    And the corresponding LM386.asy file:

    Version 4
    SymbolType CELL
    LINE Normal -64 -63 64 0
    LINE Normal -64 65 64 0
    LINE Normal -64 -63 -64 65
    LINE Normal -60 -48 -52 -48
    LINE Normal -60 48 -52 48
    LINE Normal -56 52 -56 44
    LINE Normal -48 -80 -48 -55
    LINE Normal -48 80 -48 57
    LINE Normal -44 -68 -36 -68
    LINE Normal -40 -72 -40 -64
    LINE Normal -44 68 -36 68
    LINE Normal -16 -39 -16 -64
    LINE Normal 0 32 0 48
    LINE Normal 48 -8 48 -32
    SYMATTR Value LM386
    SYMATTR Prefix X
    SYMATTR ModelFile LM386.sub
    SYMATTR Value2 LM386
    SYMATTR Description Low power audio amplifier
    PIN -16 -64 LEFT 8
    PINATTR PinName g1
    PINATTR SpiceOrder 1
    PIN -64 -48 NONE 0
    PINATTR PinName In-
    PINATTR SpiceOrder 2
    PIN -64 48 NONE 0
    PINATTR PinName In+
    PINATTR SpiceOrder 3
    PIN -48 80 NONE 0
    PINATTR PinName V-
    PINATTR SpiceOrder 4
    PIN 64 0 NONE 0
    PINATTR PinName OUT
    PINATTR SpiceOrder 5
    PIN -48 -80 NONE 0
    PINATTR PinName V+
    PINATTR SpiceOrder 6
    PIN 0 48 LEFT 8
    PINATTR PinName bp
    PINATTR SpiceOrder 7
    PIN 48 -32 LEFT 8
    PINATTR PinName g8
    PINATTR SpiceOrder 8

    Props to Roff, who actually created those files…

  • Anderson Powerpoles: Stress Relief

    This is quick & easy. When you’re making a Powerpole connector, shrink a length of small heatshrink tubing over the end of the terminal after crimping.

    Heatshrink tubing stress relief for Anderson Powerpole terminals
    Heatshrink tubing stress relief for Anderson Powerpole terminals

    You can’t cover the entire crimped region, lest the terminal not snap into the housing, but halfway seems to work fine.

    The goal is to keep the wires from flexing right at the end of the terminal, which is exactly where they’ll break.

    I’ve also wrapped a length of self-vulcanizing rubber tape around the entire connector housing and the wire, which is appropriate for high-stress applications. Looks hideous, though, not that that matters much.