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

  • Frequency Modulated DDS: SPI Mock 1

    The general idea is to frequency modulate the sine wave coming from a DDS, thereby generating a signal suitable for upconverting in amateur repeaters now tied to unobtainable crystals. The crystals run from 4-ish to 20-ish MHz, with frequency multiplication from 3 to 36 producing RF outputs from 30-ish MHz through 900-ish MHz; more details as I work through the choices.

    The demo code runs on a bare Teensy 3.6 as a dipstick test for the overall timing and functionality:

    FM DDS - Teensy 3.6 SPI demo
    FM DDS – Teensy 3.6 SPI demo

    The fugliest thing you’ve seen in a while, eh?

    An overview of the results:

    Analog 4 kHz @ 40 kHz - SPI demo overview
    Analog 4 kHz @ 40 kHz – SPI demo overview

    The pulses in D1 (orange digital) mark timer ticks at a 40 kHz pace, grossly oversampling the 4 kHz audio bandwidth in the hope of trivializing the antialiasing filters. The timer tick raises the DDS latch pin (D6, top trace) to change the DDS frequency, fires off another ADC conversion, and (for now) copies the previous ADC value to the DAC output:

    void timer_callback(void) {
      digitalWriteFast(TIMER_PIN,HIGH);
      digitalWriteFast(DDS_FQUD_PIN,HIGH);                // latch previously shifted bits
      adc->startSingleRead(AUDIO_PIN, ADC_0);             // start ADC conversion
      analogWriteDAC0(AnalogSample);                      // show previous audio sample
      digitalWriteFast(TIMER_PIN,LOW);
    }
    

    The purple analog trace is the input sine wave at 4 kHz. The yellow analog stairstep comes from the DAC, with no hint of a reconstruction filter knocking off the sharp edges.

    The X1 cursor (bold vertical dots) marks the start of the ADC read. I hope triggering it from the timer tick eliminates most of the jitter.

    The Y1 cursor (upper dotted line, intersecting X1 just left of the purple curve) shows the ADC sample apparently happens just slightly after the conversion. The analog scales may be slightly off, so I wouldn’t leap to any conclusions.

    The pulses in D2 mark the ADC end-of-conversion interrupts:

    void adc0_isr(void) {
      digitalWriteFast(ANALOG_PIN,HIGH);
      AnalogSample = adc->readSingle();                     // fetch just-finished sample
      SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
      digitalWriteFast(DDS_FQUD_PIN, LOW);
      SPI.transfer(DDSBuffer.Phase);                // interleave with FM calculations
      SPI.transfer(DDSBuffer.Bits31_24);
      SPI.transfer(DDSBuffer.Bits23_16);
      SPI.transfer(DDSBuffer.Bits15_8);
      SPI.transfer(DDSBuffer.Bits7_0);
      SPI.endTransaction();                         // do not raise FQ_UD until next timer tick!
      digitalWriteFast(ANALOG_PIN,LOW);
    }
    

    The real FM code will multiply the ADC reading by the amplitude-to-frequency-deviation factor, add it to the nominal “crystal” frequency, convert the sum to the DDS delta-phase register value, then send it to the DDS through the SPI port. For now, I just send five constant bytes to get an idea of the minimum timing with the SPI clock ticking along at 8 MHz.

    The tidy blurs in D4 show the SPI clock, with the corresponding data in D5.

    D6 (top trace) shows the DDS FQ_UD (pronounced “frequency update”) signal dropping just before the SPI data transfer begins. Basically, FQ_UD is the DDS Latch Clock: low during the delta-phase value transfer, with the low-to-high transition latching all 40 control + data bits into the DDS to trigger the new frequency.

    A closer look at the sample and transfer:

    Analog 4 kHz @ 40 kHz - SPI demo detail
    Analog 4 kHz @ 40 kHz – SPI demo detail

    For reference, the digital players from bottom to top:

    • D0 – unused here, shows pulses marking main loop
    • D1 – 40 kHz timer ticks = ADC start conversion
    • D2 – ADC end of conversion,”FM calculation”, send DDS data
    • D3 – unused here, shows error conditions
    • D4 – SPI clock = rising edge active
    • D5 – SPI MOSI data to DDS = MSB first
    • D6 – SPI CS = FQ_UD = DDS latch

    Remember, the yellow analog stairstepped trace is just a comfort signal showing the ADC actually samples the intended input.

    The ARM CPU has floating-point hardware, but I suspect fixed-point arithmetic will once again win out over double-precision multiplies & divides.

    Dropping the sampling to 20 kHz would likely work just as well and double the time available for calculations. At least now I can measure what’s going on.

    All in all, it looks feasible.

    And, yes, the scope is a shiny new Siglent SDS2304X with the MSO logic-analyzer option. It has some grievous UX warts & omissions suggesting an architectural botch job, but it’s mostly Good Enough for what I need. More later.

    The TeensyDuino source code as a GitHub Gist:

    // FM DDS
    // Ed Nisley – KE4ZNU
    // 2017-04-19 Demo 1
    #include <IntervalTimer.h>
    #include <ADC.h>
    #include <SPI.h>
    #define HEART_PIN 14
    #define TIMER_PIN 15
    #define ANALOG_PIN 16
    #define GLITCH_PIN 17
    #define AUDIO_PIN A9
    #define DDS_FQUD_PIN 10
    // data to DDS MOSI0 11
    // no data from DDS MISO0 12
    // DDS clock on SCK0 13 — also LED
    #define BUILTIN_LED 13
    //———————
    // Useful constants
    int SamplePeriod = 25; // microseconds per analog sample
    //———————
    // Globals
    ADC *adc = new ADC();
    IntervalTimer timer;
    volatile unsigned int AnalogSample;
    typedef struct {
    uint8_t Phase;
    uint8_t Bits31_24;
    uint8_t Bits23_16;
    uint8_t Bits15_8;
    uint8_t Bits7_0;
    } DDS;
    DDS DDSBuffer = {0x01,0x02,0x04,0x08,0x10};
    double DDSFreq, EpsilonFreq, DDSStepFreq;
    double CenterFreq, TestFreq;
    //———————
    // Handy routines
    void FlipPin(int pin) {
    digitalWriteFast(pin,!digitalRead(pin));
    }
    void PulsePin(int p) {
    FlipPin(p);
    FlipPin(p);
    }
    //———————
    // Timer handler
    void timer_callback(void) {
    digitalWriteFast(TIMER_PIN,HIGH);
    digitalWriteFast(DDS_FQUD_PIN,HIGH); // latch previously shifted bits
    adc->startSingleRead(AUDIO_PIN, ADC_0); // start ADC conversion
    analogWriteDAC0(AnalogSample); // show previous audio sample
    digitalWriteFast(TIMER_PIN,LOW);
    }
    //———————
    // Analog read handler
    void adc0_isr(void) {
    digitalWriteFast(ANALOG_PIN,HIGH);
    AnalogSample = adc->readSingle(); // fetch just-finished sample
    SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
    digitalWriteFast(DDS_FQUD_PIN, LOW);
    SPI.transfer(DDSBuffer.Phase); // interleave with FM calculations
    SPI.transfer(DDSBuffer.Bits31_24);
    SPI.transfer(DDSBuffer.Bits23_16);
    SPI.transfer(DDSBuffer.Bits15_8);
    SPI.transfer(DDSBuffer.Bits7_0);
    SPI.endTransaction(); // do not raise FQ_UD until next timer tick!
    digitalWriteFast(ANALOG_PIN,LOW);
    }
    //———————
    // Hardware setup
    void setup(void) {
    pinMode(BUILTIN_LED,OUTPUT); // will eventually become SCK0
    pinMode(HEART_PIN, OUTPUT); // show we arrived
    digitalWrite(HEART_PIN,LOW);
    PulsePin(HEART_PIN);
    PulsePin(HEART_PIN);
    pinMode(TIMER_PIN,OUTPUT);
    digitalWrite(TIMER_PIN,LOW);
    pinMode(GLITCH_PIN,OUTPUT);
    digitalWrite(GLITCH_PIN,LOW);
    pinMode(ANALOG_PIN,OUTPUT);
    digitalWrite(ANALOG_PIN,LOW);
    pinMode(AUDIO_PIN,INPUT);
    pinMode(DDS_FQUD_PIN,OUTPUT);
    digitalWriteFast(DDS_FQUD_PIN,HIGH);
    Serial.begin(115200);
    int waited = 0;
    while (!Serial && waited < 3000) { // fall out after a few seconds
    delay(1);
    waited++;
    if (! (waited % 50))
    FlipPin(BUILTIN_LED);
    }
    Serial.printf("FM Modulated DDS\nEd Nisley KE4ZNU\n");
    Serial.printf(" serial wait: %d ms\n\n",waited);
    SPI.begin();
    SPI.usingInterrupt(255); // attached through analog IRQs
    adc->setAveraging(0);
    adc->setResolution(12);
    adc->setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED);
    adc->setSamplingSpeed(ADC_SAMPLING_SPEED::MED_SPEED);
    adc->enableInterrupts(ADC_0);
    if (!timer.begin(timer_callback, SamplePeriod)) {
    Serial.printf("Timer start failed\n");
    while (true) {
    FlipPin(BUILTIN_LED);
    delay(50);
    }
    }
    DDSFreq = 180.0e6;
    EpsilonFreq = 1.0e-5;
    DDSStepFreq = DDSFreq / (1LL << 32);
    Serial.printf("DDS frequency: %18.7f Hz\n",DDSFreq);
    Serial.printf(" epsilon: %18.7f Hz\n",EpsilonFreq);
    Serial.printf(" step: %18.7f Hz\n\n",DDSStepFreq);
    CenterFreq = 146520000.0;
    TestFreq = CenterFreq;
    Serial.printf("Center frequency: %18.7f Hz\n",CenterFreq);
    Serial.printf("Setup done\n");
    }
    //———————
    // Do things forever
    void loop(void) {
    digitalWrite(HEART_PIN,HIGH);
    if (TestFreq < (CenterFreq + 100*EpsilonFreq))
    TestFreq += EpsilonFreq;
    else
    TestFreq += DDSStepFreq;
    Serial.printf(" %18.7f Hz\n",TestFreq);
    digitalWrite(HEART_PIN,LOW);
    delay(500);
    }
    view raw FMDDS.ino hosted with ❤ by GitHub
  • Teensy 3.6 Double Precision Floats

    Having spent a bit of effort wringing enough precision from an Arduino to make the 60 kHz quartz resonator tester, this came as a relief:

    DDS frequency:  180000000.0000000 Hz
          epsilon:          0.0000001 Hz
             step:          0.0419095 Hz
    
    Center frequency:  146520000.0000000 Hz
      146520000.0000001 Hz
      146520000.0000002 Hz
      146520000.0000003 Hz
      146520000.0000004 Hz
      146520000.0000004 Hz
      146520000.0000005 Hz
      146520000.0000006 Hz
      146520000.0000007 Hz
      146520000.0000008 Hz
      146520000.0000009 Hz
      146520000.0000010 Hz
    
    ... snippage ...
    
      146520000.0000099 Hz
      146520000.0000100 Hz
      146520000.0419195 Hz
      146520000.0838290 Hz
      146520000.1257386 Hz
      146520000.1676481 Hz
      146520000.2095576 Hz
      146520000.2514671 Hz
      146520000.2933766 Hz
      146520000.3352861 Hz
      146520000.3771957 Hz
      146520000.4191052 Hz
      146520000.4610147 Hz
      146520000.5029242 Hz
      146520000.5448337 Hz
      146520000.5867432 Hz
      146520000.6286528 Hz
      146520000.6705623 Hz
      146520000.7124718 Hz
      146520000.7543813 Hz
      146520000.7962908 Hz
      146520000.8382003 Hz
      146520000.8801098 Hz
      146520000.9220194 Hz
      146520000.9639289 Hz
      146520001.0058384 Hz
      146520001.0477479 Hz
      146520001.0896574 Hz
      146520001.1315669 Hz
      146520001.1734765 Hz
    

    Which comes from a PJRC Teensy 3.6 running this code:

    double DDSFreq, EpsilonFreq, DDSStepFreq;
    double CenterFreq, TestFreq;
    
    ... in setup() ...
    
      DDSFreq = 180.0e6;
      EpsilonFreq = 1.0e-7;
      DDSStepFreq = DDSFreq / (1LL << 32);
      Serial.printf("DDS frequency: %18.7f Hz\n",DDSFreq);
      Serial.printf("      epsilon: %18.7f Hz\n",EpsilonFreq);
      Serial.printf("         step: %18.7f Hz\n\n",DDSStepFreq);
    
      CenterFreq = 146520000.0;
      TestFreq = CenterFreq;
      Serial.printf("Center frequency: %18.7f Hz\n",CenterFreq);
    
    ... in loop() ...
    
      if (TestFreq < (CenterFreq + 100*EpsilonFreq))
        TestFreq += EpsilonFreq;
      else
        TestFreq += DDSStepFreq;
    
      Serial.printf(" %18.7f Hz\n",TestFreq);
    

    The IEEE-754 spec says a double floating-point variable carries about 15.9 decimal digits, which agrees with the 9 integer + 7 fraction digits. The highlight lowlight (gray bar) in the first figure shows the slight stumble where adding 1e-7 changes the sum, but not quite enough to affect the displayed fraction.

    In round numbers, an increment of 1e-5 would work just fine:

      
      146520000.0000100 Hz
      146520000.0000200 Hz
      146520000.0000300 Hz
      146520000.0000401 Hz
      146520000.0000501 Hz
      146520000.0000601 Hz
      146520000.0000701 Hz
      146520000.0000801 Hz
      146520000.0000901 Hz
      146520000.0001001 Hz
      146520000.0001101 Hz
      146520000.0001202 Hz
      146520000.0001302 Hz
      146520000.0001402 Hz
      146520000.0001502 Hz
      146520000.0001602 Hz
      146520000.0001702 Hz
      146520000.0001802 Hz
      146520000.0001903 Hz
      146520000.0002003 Hz
      146520000.0002103 Hz
      146520000.0002203 Hz
      146520000.0002303 Hz
    

    You’d use the “smallest of all” epsilon in a multiplied increment, perhaps to tick a value based on a knob or some such. Fine-tuning a VHF frequency with millihertz steps probably doesn’t make much practical sense.

    The DDS frequency increment works out to 41.9095 mHz, slightly larger than with the Arduino, because it’s fot a cheap DDS eBay module with an AD9851 running a 180 MHz (6 × 30 MHz ) clock.

  • Streaming Radio Player: CE Timing Tweak

    Adding delays around the SPI control signal changes reduced the OLED glitch rate from maybe a few a week  to once a week, but didn’t completely solve the problem.

    However, (nearly) all the remaining glitches seem to occur while writing a single row of pixels, which trashes the rest of the display and resolves on the next track update. That suggests slowing the timing during the initial hardware setup did change the results.

    Another look at the Luma code showed I missed the Chip Enable (a.k.a. Chip Select in the SH1106 doc) change in serial.py:

    def _write_bytes(self, data):
        gpio = self._gpio
        if self._CE:
            time.sleep(1.0e-3)
            gpio.output(self._CE, gpio.LOW)  # Active low
            time.sleep(1.0e-3)
    
        for byte in data:
            for _ in range(8):
                gpio.output(self._SDA, byte & 0x80)
                gpio.output(self._SCLK, gpio.HIGH)
                byte <<= 1
                gpio.output(self._SCLK, gpio.LOW)
    
        if self._CE:
            time.sleep(1.0e-3)
            gpio.output(self._CE, gpio.HIGH)
    

    What remains unclear (to me, anyway) is how the code in Luma's bitbang class interacts with the hardware-based SPI code in Python’s underlying spidev library. I think what I just changed shouldn’t make any difference, because the code should be using the hardware driver, but the failure rate is now low enough I can’t be sure for another few weeks (and maybe not even then).

    All this boils down to the Pi’s SPI hardware interface, which changes the CS output with setup / hold times measured in a few “core clock cycles”, which is way too fast for the SH1106. It seems there’s no control over CS timing, other than by changing the kernel’s bcm2708 driver code, which ain’t happening.

    The Python library includes a no_cs option, with the caveat it will “disable use of the chip select (although the driver may still own the CS pin)”.

    Running vcgencmd measure_clock core (usage and some commands) returns frequency(1)=250000000, which says a “core clock cycle” amounts to a whopping 4 ns.

    Forcibly insisting on using Luma’s bitbang routine may be the only way to make this work, but I don’t yet know how to do that.

    Obviously, I should code up a testcase to hammer the OLED and peer at the results on the oscilloscope: one careful observation outweighs a thousand opinions.

  • Brother BAS-311 Control Head Salvage

    A control head from an ancient Brother BAS-311 sewing machine emerged from a recent Squidwrench clearing-out session:

    Brother BAS-311 Control Head
    Brother BAS-311 Control Head

    The sturdy metal enclosure ought to be good for something, I thought, so I rescued it from the trash.

    One of the ten button-head screws galled in place and resisted a few days of penetrating oil, so I drilled it out:

    Drilled-out button screw head
    Drilled-out button screw head

    The PCB has no ICs! It simply routes all the LED and button pins through the pillar into the sewing machine controller:

    Brother BAS-311 Control Head - interior
    Brother BAS-311 Control Head – interior

    The ribbon cable alternates the usual flat strip with sections of split conductors:

    Segmented ribbon cable
    Segmented ribbon cable

    The split segments let it roll up into the pillar, with enough flexibility to allow rotating the head. I’ve seen segmented twisted-pair ribbon cable, but never just flat conductors.

    Maybe the control head can become Art in its next life?

  • Fluorescent Shop Light Ballasts, Redux

    As usual, several shoplights didn’t survive the winter, so I gutted and rebuilt them with LED tubes. Even the fancy shoplights with genuine electronic ballasts survive less than nine years, as two of those eight “new” lamps have failed so far.

    The dead ballast looks the same as it did before:

    Electronic ballast - label
    Electronic ballast – label

    Some deft work with a cold chisel and my Designated Prydriver popped the top to reveal a plastic-wrapped circuit board:

    Electronic ballast - interior wrapped
    Electronic ballast – interior wrapped

    Perhaps the flexy gunk reduces the sound level:

    Electronic ballast - interior A
    Electronic ballast – interior A

    While also preventing casual failure analysis and organ harvesting:

    Electronic ballast - interior B
    Electronic ballast – interior B

    The black gunk smells more like plastic and less like old-school tar. It’s definitely not a peel-able conformal coating.

    One the other paw, the two magnetic ballasts in another lamp sported actual metal-film capacitors, which I harvested and tossed into the Big Box o’ Film Caps:

    Shoplight choke ballast - film cap
    Shoplight choke ballast – film cap

    If a dying ballast didn’t also kill its fluorescent tube(s), I’d be less annoyed. I’m running the remaining tubes through the surviving fixtures, but the end is nigh for both.

    The new LED tubes produce more light than the old fluorescents, although I still don’t like their 6500 K “daylight glow” color.

  • Sena PS410 Serial Server: Shelf with Calculations

    A crude shelf bandsawed from a plank moves the Sena PS410 serial server and an old Ethernet switch off the bench:

    Serial server shelf - front
    Serial server shelf – front

    The brackets holding it to the studs came from a 2×4 inch scrap:

    Serial server shelf - rear
    Serial server shelf – rear

    Obviously, the Basement Laboratory lacks stylin’ home decor.

    None of which would be worth mentioning, except for some Shop Calculations scrawled on the 2×4:

    Wood shop calculations
    Wood shop calculations

    It’s in my handwriting, although whatever it related to is long gone.

    Trigonometry FTW!

  • Fake Flash

    This 2 GB flash drive arrived with datasheets & sample files for a (computerized) sewing machine Mary eventually decided she wasn’t going to get (because computerized):

    Fake Flash drive
    Fake Flash drive

    Being of sound mind, we reformatted it and dropped it in the bag o’ random drives. She eventually used it for one of her gardening presentations, whereupon the library’s (Windows) laptop said it needed formatting; she pulled out a backup drive and continued the mission.

    Lather, rinse, verify a good format, verify presentation files on the Token Windows Box, and repeat, right down to having another library’s laptop kvetch about the drive.

    Soooo, I did what I should have done in the first place:

    sudo f3probe -t /dev/sdc
    F3 probe 6.0
    Copyright (C) 2010 Digirati Internet LTDA.
    This is free software; see the source for copying conditions.
    
    WARNING: Probing normally takes from a few seconds to 15 minutes, but
             it can take longer. Please be patient.
    
    Probe finished, recovering blocks... Done
    
    Bad news: The device `/dev/sdc' is a counterfeit of type limbo
    
    You can "fix" this device using the following command:
    f3fix --last-sec=25154 /dev/sdc
    
    Device geometry:
    	         *Usable* size: 12.28 MB (25155 blocks)
    	        Announced size: 1.86 GB (3893248 blocks)
    	                Module: 2.00 GB (2^31 Bytes)
    	Approximate cache size: 511.00 MB (1046528 blocks), need-reset=no
    	   Physical block size: 512.00 Byte (2^9 Bytes)
    
    Probe time: 55'18"
     Operation: total time / count = avg time
          Read: 8'35" / 3145715 = 163us
         Write: 46'37" / 18838872 = 148us
         Reset: 350.7ms / 2 = 175.3ms
    

    Huh.

    As long as you don’t write more than a few megabytes, it’s all good, which was apparently enough for its original use.

    The front of the PCB looks normal:

    Fake Flash - controller
    Fake Flash – controller

    But it seems they really didn’t want you to see the flash chip:

    Fake Flash - covered chip
    Fake Flash – covered chip

    Given the two rows of unused pads, it must be a really small chip!

    Memo to Self: Always examine the dentition of any Equus ferus received as a gift.