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: Amateur Radio

Using and building radio gadgetry

  • FM DDS: SPI Mock 3

    Running some serial I/O in the background adds jitter to the timer interrupt pacing the ADC samples and as-yet-unwired DDS updates. For reference, an overview of the process showing the procession from the IRQ on the left to the SPI outputs near the middle and another IRQ on the far right:

    DDS Mock - 0 VAC - SPI
    DDS Mock – 0 VAC – SPI

    Now, speed up the sweep and delay the trace by 25 μs to put the triggering pulse off-screen to the left and the second pulse at the center division:

    ADC Sample IRQ jitter
    ADC Sample IRQ jitter

    The orange smear in the middle should be a tidy pulse, but it isn’t.

    The  25 μs timer interrupt now has the highest priority on the front burner:

    IntervalTimer AudioSampler;
    
    ... snippage ...
    
      AudioSampler.priority(0);
      if (!AudioSampler.begin(AudioSamplerIRQ, SamplePeriod)) {
        Serial.printf("Timer start failed\n");
        while (true) {
          FlipPin(BUILTIN_LED);
          delay(75);
        }
      }
    

    Although nothing can interrupt it, other code / handlers may disable interrupts around their own critical sections and delay the tick. If the triggering tick (the off-screen one starting the trace) is delayed, then the on-screen pulse will appear “too soon”, to the left of center. If the triggering tick is on time, but the on-screen pulse is delayed, it’ll appear “too late” on the right.

    The blur is (roughly) symmetric around the center graticule line, so the handwaving seems about right.

    In round numbers, the jitter moves the interrupt ±325 ns on either side of its nominal position, with most of the pulses within ±100 ns. I doubt the jitter distribution is Gaussian, but vigorous handwaving says the RMS jitter might amount to 75 ns.

    At the 4 kHz audio band limit, a 75 ns sampling error a phase error of 0.1°, so the maximum amplitude jitter would be sin(0.1°) = 0.002 = -55 dB, which might suffice for amateur-radio audio.

    I think, anyhow.

  • Monthly Science: As Seen On Radio

    This showed up when I looked at our APRS tracks after a recent ride:

    Balloon chase - KJ5HY-9
    Balloon chase – KJ5HY-9

    Poking around a bit showed the target:

    Balloon chase - W2KGY-12
    Balloon chase – W2KGY-12

    Contrary to what I thought, it didn’t come up the Hudson River from West Point:

    Balloon chase - W2KGY-12 track - 2018-04-21 to 2018-04-24
    Balloon chase – W2KGY-12 track – 2018-04-21 to 2018-04-24

    Knowledge of the Universal Law of the Conservation of Perversity informs you a balloon will never land in the middle of a putting green:

    Balloon chase - W2KGY-12 landing site
    Balloon chase – W2KGY-12 landing site

    Apparently the launch is part of a regular class project at West Point. Good clean fun!

  • FM DDS: SPI Mock 2

    Doing the DDS calculations in full-frontal double floating point turns out to be maybe fast enough:

    DDS Mock - 0 VAC - SPI
    DDS Mock – 0 VAC – SPI

    I set the ADC to HIGH_SPEED conversion and sampling, reducing the time between the start of conversion (first pulse in D1) and the ADC end-of-conversion interrupt (rising edge in D2) from 4.7 μs to 2.6 μs, more-or-less, kinda-sorta.

    The ADC hardware can return the average of several sample taken in quick succession, so I set it to average four samples. The vertical cursors show the combination of fast conversion and averaging requires 7 μs (-ish) from start to finish: long enough to justify separating the two by an interrupt and short enough to allow calculations after fetching the result.

    The purple trace shows the analog input voltage hovering close to a constant VCC/2 (about 1.6+ V), rather than the sine-wave I used earlier, again courtesy of the scope’s arbitrary function generator. The loop() dumps the min and max ADC values (minus half the ADC range (4096/2= 2048):

        -4 to     2
        -3 to     2
        -3 to     2
    

    A span of half a dozen counts = 3 bits means the 12 bit ADC really delivers 9 bits = 0.2% resolution = 54 dB dynamic range = probably not good enough. However, the “circuit” is an open-air hairball on the bench, driven from the scope’s arbitrary waveform generator in high-Z mode, so things can only get better with more any attention to detail.

    The 1.9 μs gap between the first and second burst of SPI clocks contains all the floating-point calculations required to convert an ADC sample to DDS delta-phase bits:

    void adc0_isr(void) {
    
      int Audio;
    
      digitalWriteFast(PIN_ANALOG,HIGH);
    
      AnalogSample = adc->readSingle();             	  // fetch just-finished sample
      Audio = AnalogSample - 2048;                      // convert to AC signal
    
      DDSBuffer.Phase = 0;
    
      SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
      digitalWriteFast(PIN_DDS_FQUD, LOW);
    
      SPI.transfer(DDSBuffer.Phase);
    
      DDSBuffer.DeltaPhase = (uint32_t)((((double)Audio / 2048.0) * Deviation + Crystal) * CountPerHertz);
    
      SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 24));      // MSB first!
    
      if (Audio > AudioMax)                                     // ignore race conditions
        AudioMax = Audio;
      if (Audio < AudioMin) AudioMin = Audio; SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 16));
    
      SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >>  8));
      SPI.transfer((uint8_t)DDSBuffer.DeltaPhase);
    
      SPI.endTransaction();                         // do not raise FQ_UD until next timer tick!
    
      digitalWriteFast(PIN_ANALOG,LOW);
    }
    

    A closer look lets the scope decode and present the SPI data:

    DDS Mock - 0 VAC - SPI detail
    DDS Mock – 0 VAC – SPI detail

    The program calculates and displays various “constants” I set for convenience:

    FM Modulated DDS
    Ed Nisley KE4ZNU
     serial wait: 890 ms
    
    DDS clock:     180000000.000 Hz
    CountPerHertz:        23.861 ct
    HertzPerCount:         0.042 Hz
    
    Crystal:    20000000.000 Hz
    Deviation:      5000.000 Hz
    

    You can confirm the SPI data by working backwards with a calculator:

    • DDS delta-phase register bytes: 1C 71 C6 E2 = 477218530 decimal
    • Multiply by 180 MHz / 2^32 to get frequency: 1999997.5506 Hz
    • Subtract nominal 20.0 MHz crystal to get modulation: -2.4494 Hz
    • Divide by nominal 5.0 kHz deviation to get fractional modulation: -4.89.9e-6
    • Multiply by half the ADC range (4096/2) to get ADC counts: -1.003
    • Add 2048 to get the actual ADC sample: 2047

    Nicely inside the range of values reported by the main loop, whew.

    Which means I can avoid screwing around with fixed-point arithmetic until such time as clawing back a few microseconds makes a meaningful difference.

    Now, to begin paying attention to those pesky hardware details …

    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 PIN_HEART 14
    #define PIN_TIMER 15
    #define PIN_ANALOG 16
    #define PIN_GLITCH 17
    #define PIN_AUDIO A9
    #define PIN_DDS_FQUD 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 int AnalogSample;
    volatile int AudioMax = -4096;
    volatile int AudioMin = 4096;
    typedef struct {
    uint8_t Phase;
    uint32_t DeltaPhase; // DDS expects MSB first!
    } DDS;
    DDS DDSBuffer;
    double DDSClock = 180.0e6; // nominal DDS oscillator
    double CountPerHertz, HertzPerCount; // DDS delta-phase increments
    double Crystal = 20.0e6; // nominal DDS frequency
    double Deviation = 5.0e3; // nominal FM signal deviation (one-sided)
    double 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(PIN_TIMER,HIGH);
    digitalWriteFast(PIN_DDS_FQUD,HIGH); // latch previously shifted bits
    adc->startSingleRead(PIN_AUDIO, ADC_0); // start ADC conversion
    analogWriteDAC0(AnalogSample); // show previous audio sample
    digitalWriteFast(PIN_TIMER,LOW);
    }
    //———————
    // Analog read handler
    void adc0_isr(void) {
    int Audio;
    digitalWriteFast(PIN_ANALOG,HIGH);
    AnalogSample = adc->readSingle(); // fetch just-finished sample
    Audio = AnalogSample – 2048; // convert to AC signal
    DDSBuffer.Phase = 0;
    SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
    digitalWriteFast(PIN_DDS_FQUD, LOW);
    SPI.transfer(DDSBuffer.Phase);
    DDSBuffer.DeltaPhase = (uint32_t)((((double)Audio / 2048.0) * Deviation + Crystal) * CountPerHertz);
    SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 24)); // MSB first!
    if (Audio > AudioMax) // ignore race conditions
    AudioMax = Audio;
    if (Audio < AudioMin)
    AudioMin = Audio;
    SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 16));
    SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 8));
    SPI.transfer((uint8_t)DDSBuffer.DeltaPhase);
    SPI.endTransaction(); // do not raise FQ_UD until next timer tick!
    digitalWriteFast(PIN_ANALOG,LOW);
    }
    //———————
    // Hardware setup
    void setup(void) {
    pinMode(BUILTIN_LED,OUTPUT); // will eventually become SCK0
    pinMode(PIN_HEART, OUTPUT); // show we arrived
    digitalWrite(PIN_HEART,LOW);
    PulsePin(PIN_HEART);
    PulsePin(PIN_HEART);
    pinMode(PIN_TIMER,OUTPUT);
    digitalWrite(PIN_TIMER,LOW);
    pinMode(PIN_GLITCH,OUTPUT);
    digitalWrite(PIN_GLITCH,LOW);
    pinMode(PIN_ANALOG,OUTPUT);
    digitalWrite(PIN_ANALOG,LOW);
    pinMode(PIN_AUDIO,INPUT);
    pinMode(PIN_DDS_FQUD,OUTPUT);
    digitalWriteFast(PIN_DDS_FQUD,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(4); // choices: 0, 4, 8, 16, 32
    adc->setResolution(12); // choices: 8, 10, 12, 16
    adc->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED);
    adc->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
    adc->enableInterrupts(ADC_0);
    if (!timer.begin(timer_callback, SamplePeriod)) {
    Serial.printf("Timer start failed\n");
    while (true) {
    FlipPin(BUILTIN_LED);
    delay(75);
    }
    }
    CountPerHertz = (1LL << 32) / DDSClock;
    HertzPerCount = 1.0 / CountPerHertz;
    Serial.printf("DDS clock: %13.3f Hz\n",DDSClock);
    Serial.printf("CountPerHertz: %13.3f ct\n",CountPerHertz);
    Serial.printf("HertzPerCount: %13.3f Hz\n\n",HertzPerCount);
    TestFreq = Crystal;
    Serial.printf("Crystal: %13.3f Hz\n",Crystal);
    Serial.printf("Deviation: %13.3f Hz\n",Deviation);
    Serial.printf("\nSetup done\n");
    }
    //———————
    // Do things forever
    void loop(void) {
    digitalWrite(PIN_HEART,HIGH);
    Serial.printf(" %5d to %5d\n",AudioMin,AudioMax);
    AudioMax = 99*AudioMax/100; // ignore race conditions
    AudioMin = 99*AudioMin/100;
    digitalWrite(PIN_HEART,LOW);
    delay(500);
    }
    view raw FMDDS.ino hosted with ❤ by GitHub
  • Motorola K1003A Channel Element: Oscillation!

    A handful of Motorola K1003A Receive Channel Elements arrived from eBay:

    Motorola Channel Elements - overview
    Motorola Channel Elements – overview

    Having three 13466.666 kHz candidates, two with gold labels (2 ppm tempco) I disemboweled a silver-label (5 ppm) victim:

    Motorola Channel Element - silver label
    Motorola Channel Element – silver label

    They’re well-studied, with readily available schematics:

    k1003-schematic
    k1003-schematic

    For lack of anything smarter, I put a 1 kΩ resistor from RF Out to Ground to get some DC current going, then used a 470 nF cap and 47 Ω resistor as an AC load:

    K1003 Channel Element - bias lashup
    K1003 Channel Element – bias lashup

    Which oscillated around a mid-scale DC bias, but looked ugly:

    K1003 Channel Element - 13.4 MHz output - 1k bias
    K1003 Channel Element – 13.4 MHz output – 1k bias

    Perusing some receiver schematics suggested a heavier DC load, so I swapped in a 470 Ω resistor:

    K1003 Channel Element - 470 ohm bias - 13.4 MHz output
    K1003 Channel Element – 470 ohm bias – 13.4 MHz output

    It’s now running around 3 V bias with fewer harmonics; the scope’s frequency display in the upper right corner seems happier, too.

    The receiver will run that through a filter to wipe off the harmonics, then multiply the frequency by three to get the mixer LO.

    There are many, many different Channel Elements out there, in receive and transmit flavors, but at least I have some idea what’s going on inside.

  • 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
  • Sena PS410 Serial Server: Capturing HP 8591 Screen Images

    The objective is to capture screen shots from the HP 8591 spectrum analyzer, now connected to Serial Port 2 of the Sena PS410 serial server.

    My analyzer is an old one with a 3322A serial number, so its Opt 023 came with a genuine DB-25 female connector, not the DE-9 male connector described in the HP doc for the later Op 043 hardware. With that in mind, the HP doc says the spectrum analyzer supports only hardware handshaking:

    • Baud rate 300 to 57,000 baud.
    • 8 bits per character.
    • 1 stop bit.
    • No parity.
    • Software handshake – none.
    • Xon/Xoff and ENQ/ACK not supported by the spectrum analyzer.

    The manual enumerates the handshaking lines:

    • Request to send (RTS) – Output signal indicates that the spectrum analyzer is ready to communicate. This line is true at power-up and stays true while power is on.
    • Clear to send (CTS) – Input signal indicates that the external controller is ready to receive data.
    • Data terminal ready (DTR) – Output signal from the spectrum analyzer. When the input buffer is full, this line goes false.
    • Data set ready (DSR) – Is not available.
    • Data carrier detect (DCD) – Input to the spectrum analyzer. If DCD is true, the spectrum analyzer will receive data from the controller. If false, no data will be input. The data will be ignored.

    Furthermore, it is written:

    The spectrum analyzer checks its CTS input before transmitting data to the computer. If the CTS line is false, the spectrum analyzer will not transmit data. The spectrum analyzer transmits data when the CTS line is true.

    The spectrum analyzer sets the DTR line (PC CTS) false when its input buffer is full.

    They offer several wiring diagrams, none of which correspond to the hardware on my bench, but swapping the “Personal Computer” and “Analyzer” headings on this diagram seems close to reality:

    HP 8591 - RS232 DB25 to DE9 wiring diagram
    HP 8591 – RS232 DB25 to DE9 wiring diagram

     

    On the other end of the cable, the PS410 does “hardware flow control using RTS/CTS”. They also offer a diagram:

    Sena PS410 - RS232 wiring diagram
    Sena PS410 – RS232 wiring diagram

    So I rewired the cable thusly:

    HP 8591 vs Sena PS410 - RS232 cable diagram
    HP 8591 vs Sena PS410 – RS232 cable diagram

    Pin 1 on the 8591 interface connects to both frame ground and signal ground and, back when I first made this cable, many years ago, I had wired it to the shield of the cable and thence to the DE9 shell. Alas, the PS410 took offense; for reasons I don’t understand, a shell-to-ground connection ignites a ferrite bead on the PS410’s PCB.

    With the rewired cable in hand, the PS410 serial port setup looks like this:

    Port 2 - 8591 serial config
    Port 2 – 8591 serial config

    The PS410 apparently wiggles its RTS output after every byte it receives, because the CTS input at the 8591 turns into a blur during screen captures. This seems unaffected by the Inter character time-out setting and doesn’t (seem to) produce any problems, so it’s like that and that’s the way it is.

    Using 9600 b/s isn’t as slow as you might think. The HP manual  notes:

    Some of the programs in this manual use 1200 baud for proper operation. If your system uses the RS-232 handshake lines, you can use 9600 baud for all of the programs.

    I tried 19200 b/s and got mysterious errors that resemble overruns, which suggests the 8591 ignores the PS410’s flickering RTS output. The screen dumps require only a few seconds, so it’s not a big deal, although timing issues have a way of resurfacing at the most inopportune, uh, times.

    Kermit knows how to handle network sockets and suchlike, so aiming it at the spectrum analyzer is a one-liner:

    set host 192.168.1.40 7002 /raw-socket
    set modem none
    

    The /raw-socket disables Kermit’s default Telnet interface, preventing it from squirting IAC + BRK characters when closing the session; I think that’s what happens, but I don’t use Telnet enough to know better. As you might expect, the 8591 deals poorly with characters outside its lexicon.

    It’s not obvious set modem none does anything in this context, but it seems reasonable.

    Then the rest of the script Just Works:

    FM 104.7 MHz peak hold
    FM 104.7 MHz peak hold

    Which is the peak-hold spectrum of a local FM station, as received through an amateur radio HT rubber duck antenna.

    The Kermit source code as a GitHub Gist:

    #!/usr/bin/kermit +
    # Fetches screen shot from HP8591E spectrum analyzer
    # Presumes it's set up for plotter output…
    # Converts HPGL to PNG image
    # use raw-socket to disable telnet termination chars
    set host 192.168.1.40 7002 /raw-socket
    set modem none
    # Make sure we have a param
    if not defined \%1 ask \%1 {File name? }
    set input echo off
    set input buffer-length 200000
    # Tell it what size to plot
    echo Triggering plot output…
    output plot 0,0,60000,40000;
    log session "\%1.hgl"
    # Wait for end of data stream
    input 400 SP;
    echo … HPGL data captured
    close session
    close
    echo Converting HPGL in
    echo –\%1.hgl
    echo to PNG in
    echo –\%1.png
    run hp2xx -q -m png -c 1436 "\%1.hgl"
    echo Cropping and resizing
    #run mogrify -crop "515×395+0+0!" "\%1.png"
    run mogrify -density 300 -resize 200% "\%1.png"
    echo Finished!
    exit 0
    view raw gethp8591 hosted with ❤ by GitHub
  • Wouxun KG-UV3D: Failure 3 PCB

    For completeness, here’s the inside of the most recent Wouxun KG-UV3D radio failure:

    Wouxun KG-UV3D - failure 3 - PCB
    Wouxun KG-UV3D – failure 3 – PCB

    The build quality has definitely improved since the previous radios in 2011:

    Wouxun KG-UV3D - PCB overview
    Wouxun KG-UV3D – PCB overview

    There’s still plenty of flux to go around, but it’s not nearly as crusty as before.