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: Science

If you measure something often enough, it becomes science

  • AD9850 DDS Module: 125 MHz Oscillator vs. Temperature, Linear Edition

    A day of jockeying the AD9850 DDS oscillator shows an interesting relation between the frequency offset and the oscillator temperature:

    DDS Oscillator Frequency Offset vs. Temperature - complete
    DDS Oscillator Frequency Offset vs. Temperature – complete

    Now, as it turns out, the one lonely little dot off the line happened just after I lit the board up after a tweak, so the oscillator temperature hadn’t stabilized. Tossing it out produces a much nicer fit:

    DDS Oscillator Frequency Offset vs. Temperature
    DDS Oscillator Frequency Offset vs. Temperature

    Looks like I made it up, doesn’t it?

    The first-order coefficient shows the frequency varies by -36 Hz/°C. The actual oscillator frequency decreases with increasing temperature, which means the compensating offset must become more negative to make the oscillator frequency variable match reality. In previous iterations, I’ve gotten this wrong.

    For example, at 42.5 °C the oscillator runs at:

    125.000000 MHz - 412 Hz = 124.999588 MHz

    Dividing that into 232 = 34.35985169 count/Hz, which is the coefficient converting a desired frequency into the DDS delta phase register value. Then, to get 10.000000 MHz at the DDS output, you multiply:
    10×106 × 34.35985169 = 343.598517×106

    Stuff that into the DDS and away it goes.

    Warmed half a degree to 43.0 °C, the oscillator runs at:

    125.000000 MHz - 430 Hz = 124.999570 MHz

    That’s 18 Hz lower, so the coefficient becomes 34.35985667, and the corresponding delta phase for a 10 MHz output is 343.598567×106.

    Obviously, you need Pretty Good Precision in your arithmetic to get those answers.

    After insulating the DDS module to reduce the effect of passing breezes, I thought the oscillator temperature would track the ambient temperature fairly closely, because of the more-or-less constant power dissipation inside the foam blanket. Which turned out to be the case:

    DDS Oscillator Temperature vs. Ambient
    DDS Oscillator Temperature vs. Ambient

    The little dingle-dangle shows startup conditions, where the oscillator warms up at a constant room temperature. The outlier dot sits 0.125 °C to the right of the lowest pair of points, being really conspicuous, which was another hint it didn’t belong with the rest of the contestants.

    So, given the ambient temperature, the oscillator temperature will stabilize at 0.97 × ambient + 20.24, which is close enough to a nice, even 20 °C hotter.

    The insulation blanket reduces short-term variations due to breezes, which, given the -36 Hz/°C = 0.29 ppm temperature coefficient, makes good sense; you can watch the DDS output frequency blow in the breeze. It does, however, increase the oscillator temperature enough to drop the frequency by 720 Hz, so you probably shouldn’t use the DDS oscillator without compensating for at least its zero-th order offset at whatever temperature you expect.

    Of course, that’s over a teeny-tiny temperature range, where nearly anything would be linear.

    The original data:

    DDS Oscillator offset vs temperature - 2017-06-24
    DDS Oscillator offset vs temperature – 2017-06-24
  • Monthly Science: Cheap WS2812 LED Failures

    The two knockoff Neopixel test fixtures went dark while their USB charger accompanied me on a trip, so they spent a few days at ambient basement conditions. When I plugged them back into the charger, pretty much the entire array lit up in pinball panic mode:

    WS2812 LED - test fixture multiple failures
    WS2812 LED – test fixture multiple failures

    Turns out three more WS2812 chips failed in quick succession. I’ve hotwired around the deaders (output disconnected, next chip input in parallel) and, as with the other zombies, they sometimes work and sometimes flicker. That’s five failures in 28 LEDs over four months, a bit under 3000 operating hours.

    For lack of a better explanation: the cool chips pulled relatively moist air through their failed silicone encapsulation, quietly rotted out in the dark, then failed when reheated. After they spend enough time flailing around, the more-or-less normal operating temperatures drives out the moisture and they (sometimes) resume working.

    Remember, all of them passed the Josh Sharpie Test, so you can’t identify weak ones ahead of time.

  • LF Crystal Tester: Pretty Plots

    A slight modification spits out the (actual) frequency and dBV response (without subtracting the 108 dB intercept to avoid negative numbers for now) to the serial port in CSV format, wherein a quick copypasta into a LibreOffice Calc spreadsheet produces this:

    Spectrum-32
    Spectrum-32

    Changing the center frequency and swapping in a 60 kHz resonator:

    Spectrum-60
    Spectrum-60

    Much prettier than the raw scope shot with the same data, there can be no denyin’:

    Log V vs F - 32766 4 Hz - CX overlay
    Log V vs F – 32766 4 Hz – CX overlay

    I think the wobbulations around the parallel resonant dip come from the excessively hugely too large 10 µF caps in the signal path, particularly right before the log amp input, although the video bandwidth hack on the AD8310 module may contribute to the problem. In any event, I can see the log amp output wobbling for about a second, which is way too long.

    Anyhow, the series-resonant peaks look about 1 Hz wide at the -3 dBV points, more or less agreeing with what I found with the HP 8591 spectrum analyzer. The series cap is a bit smaller, producing a slightly larger frequency change in the series resonant frequency: a bit under 2 Hz, rather than the 1 Hz estimated with the function generator and spectrum analyzer.

    I still don’t understand why the parallel resonant dip changes, although I haven’t actually done the pencil pushing required for true understanding.

    Ain’t they lovely, though?

  • Golden Tortoise Beetle

    An iridescent ball appeared on the kitchen wall:

    Golden Tortoise Beetle - left top - light
    Golden Tortoise Beetle – left top – light

    Despite the silvery shine under LED lighting, it was a Golden Tortoise Beetle:

    Golden Tortoise Beetle - right top
    Golden Tortoise Beetle – right top

    The iridescence shows up better with a bit of underexposure:

    Golden Tortoise Beetle - left top - dark
    Golden Tortoise Beetle – left top – dark

    Transparent armor: who’d’a thunk it?

    Golden Tortoise Beetle - left front
    Golden Tortoise Beetle – left front

    Mary spotted one in the garden some years ago; I’ve never seen such a thing.

  • Monthly Science: Significant Figures vs. Accuracy vs. Precision, Marathon Edition

    The rail trail recently sprouted white mile markers:

    Rail Trail - Marathon 13 mile marker
    Rail Trail – Marathon 13 mile marker

    This one stood out:

    Rail Trail - Marathon 13.10938 mile marker
    Rail Trail – Marathon 13.10938 mile marker

    Not being a marathoner, I had the vague notion a marathon should be an even number of kilometers, because it’s not an even number of miles, but nooooo it’s just an arbitrary distance everybody agreed would be about right for a good long run.

    During the rest of the ride, I worked out that 1 micro mile = 5+ milli foot = 60+ milli inch, so the rightmost significant figure in that marker represents increments of, oh, a smidge under ¾ inch. Middle of the hash line marks the spot, perhaps?

    I’ve seen similar markers along other courses, with varying numbers of ahem significant figures, and will not say how long it took me to recognize what it represented.

  • AD9850 DDS Module: Temperature Sensitivity

    While tinkering with the SPI code for the AD9850 DDS module, I wrote down the ambient temperature and the frequency tweak required to zero-beat the 10 MHz output with the GPS-locked oscillator. A quick-n-dirty plot summarizing two days of randomly timed observations ensued:

    AD9850 DDS Module - Frequency vs Temperature
    AD9850 DDS Module – Frequency vs Temperature

    The frequency offset comes from the tweak required to zero-beat the output by adjusting the initial oscillator error: a positive tweak produces a smaller count-per-hertz coefficient and reduces the output frequency. As a result, the thermal coefficient sign is backwards, because increasing temperature raises the oscillator frequency and reduces the necessary tweak. I think so, anyway; you know how these things can go wrong. More automation and reliable data would be a nice touch.

    Foam sheets formed a block around the DDS module, isolating it from stray air currents and reducing the clock oscillator’s sensitivity:

    AD9850 DDS module - foam insulation
    AD9850 DDS module – foam insulation

    I used the ambient temperature, because the thermocouple inside the foam (not shown in the picture) really wasn’t making good contact with the board, the readings didn’t make consistent sense, and, given a (nearly) constant power dissipation, the (average) oscillator temperature inside the foam should track ambient temperature with a constant offset. I think so, anyway.

    The coefficient works out to 0.02 ppm/°C. Of course, the initial frequency offset is something like -400 Hz = 3 ppm, so we’re not dealing with lab-grade instrumentation here.

  • Arduino vs. Significant Figures: Preliminary 64-bit Fixed Point Exercise

    Although it’s not advertised, the Arduino / AVR compiler mostly does the right thing with long long = uint64_t variables: add & subtract work fine, but multiplication & division discard anything that doesn’t fit into 64 bits. Fitting a 32 bit integer and a 32 bit fraction into such a thing should eliminate (most) problems with significant figures.

    The general idea is to set up a struct giving access to the two 32 bit halves for direct manipulation, then overlay / union them with a single 64 bit integer for arithmetic purposes:

    struct ll_s {
      uint32_t low;
      uint32_t high;
    };
    
    union ll_u {
      uint64_t ll_64;
      struct ll_s ll_32;
    };
    

    Of course, the integer part still falls one bit shy of holding 2³². At the cost of one bit’s worth of resolution, you can still compute 2³² / 125×10⁶ by pre-dividing each quantity by 2:

    2^63 = [80000000 00000000]
    2^63 / 125/2 M = [00000022 5c17d04d]
    

    The low-order digit should be 0xe, not 0xd, but I think that’s survivable.

    Unfortunately, printf doesn’t handle 64 bit quantities, necessitating some awkward conversion routines. “Printing” to a string seems the least awful method, as I’ll eventually squirt the strings to a display, not send them to the serial port:

    void PrintFractionLL(char *pBuffer,uint64_t *pLL) {
      uint64_t Fraction;
    
      Fraction = (uint32_t)*pLL;                      // copy 32 fraction bits, high order = 0
      Fraction *= ONEGIG;                             // times 10^9 for conversion
      Fraction >>= 32;                                // align integer part in low long
      sprintf(pBuffer,"%09lu",(uint32_t)Fraction);    // convert low long to decimal
    }
    
    void PrintIntegerLL(char *pBuffer,uint64_t *pLL) {
      sprintf(pBuffer,"%lu",*((uint32_t *)pLL+1));
    }
    
    void PrintDecimalLL(char *pBuffer,uint64_t *pLL) {
      PrintIntegerLL(pBuffer,pLL);
      pBuffer += strlen(pBuffer);       // pointer to end of integer part
      *pBuffer++ = '.';                 // drop in the decimal point, tick pointer
      PrintFractionLL(pBuffer,pLL);
    }
    

    The result seems nearly indistinguishable from the Right Answer:

    Integer:  34
    Fraction: 359738367
    Decimal: 34.359738367
    

    This whole mess has a bunch of rough edges, but it looks promising. The code coalesced while fiddling around, so the union notation didn’t get much love at first.

    The Arduino source code as a GitHub Gist:

    Long long integer exercise
    Ed Nisley – KE4ZNU – May 2017
    Long long size = 8 bytes
    .. value = [12345678 9abcdef0]
    divided result = [01234567 89abcdef]
    2^32 = [00000001 00000000]
    125M = [00000000 07735940]
    2^32 / 125M = [00000000 00000022]
    Scaled fixed point tests
    2^63 = [80000000 00000000]
    2^63 / 125/2 M = [00000022 5c17d04d]
    Integer: 34
    Fraction: 359738367
    Decimal: 34.359738367
    Hz Per Ct: 0.029103830
    60000: 60000.000000000
    60 kHz as count: 2061584.302070550
    0.1 Hz as count: 3.435973836
    60000.1 Hz as count: 2061587.738044387
    60000.1 Hz from count: 60000.078519806
    60000.2 Hz as count: 2061591.174018223
    60000.2 Hz from count: 60000.194935128
    60000.2 Hz rnd count: 60000.224038958
    Union size: 8
    TestFreq: [0000ea60 395a9e00]
    Union ll: [0000ea60 395a9e00]
    From union: 60000.224038958
    Buffer length: 15
    Trunc dec: 60000.224
    view raw LongLong.txt hosted with ❤ by GitHub
    // Long long integer exercise for 60 kHz crystal tester
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    char Buffer[10+1+10]; // string buffer for long long conversions
    #define ONEGIG 1000000000LL
    uint64_t CtPerHz; // will be 2^32 / 125 MHz
    uint64_t HzPerCt; // will be 125 MHz / 2^32
    uint64_t TenthHzCt; // 0.1 Hz in counts
    struct ll_s {
    uint32_t low;
    uint32_t high;
    };
    union ll_u {
    uint64_t ll_64;
    struct ll_s ll_32;
    };
    //———–
    // Long long print-to-buffer helpers
    // Assumes little-Endian layout
    void PrintHexLL(char *pBuffer,uint64_t *pLL) {
    sprintf(pBuffer,"%08lx %08lx",*((uint32_t *)pLL+1),(uint32_t)*pLL);
    }
    // converts 9 decimal digits
    void PrintFractionLL(char *pBuffer,uint64_t *pLL) {
    uint64_t Fraction;
    Fraction = (uint32_t)*pLL; // copy 32 fraction bits, high order = 0
    Fraction *= ONEGIG; // times 10^9 for conversion
    Fraction >>= 32; // align integer part in low long
    sprintf(pBuffer,"%09lu",(uint32_t)Fraction); // convert low long to decimal
    }
    void PrintIntegerLL(char *pBuffer,uint64_t *pLL) {
    sprintf(pBuffer,"%lu",*((uint32_t *)pLL+1));
    }
    void PrintDecimalLL(char *pBuffer,uint64_t *pLL) {
    PrintIntegerLL(pBuffer,pLL);
    pBuffer += strlen(pBuffer); // pointer to end of integer part
    *pBuffer++ = '.'; // drop in the decimal point, tick pointer
    PrintFractionLL(pBuffer,pLL);
    }
    //———–
    void setup ()
    {
    Serial.begin (115200);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println ("Long long integer exercise");
    Serial.println ("Ed Nisley – KE4ZNU – May 2017");
    unsigned long long LongLong;
    uint64_t LongLong2;
    LongLong = 0x123456789abcdef0LL;
    printf("Long long size = %d bytes\n",sizeof(LongLong));
    printf(" .. value = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)(LongLong & 0x00000000ffffffffLL));
    LongLong /= 16;
    printf(" divided result = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong);
    LongLong = 1LL << 32;
    printf(" 2^32 = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong);
    LongLong2 = 125000000LL;
    printf(" 125M = [%08lx %08lx]\n",(long)(LongLong2 >> 32),(long)LongLong2);
    LongLong /= LongLong2;
    printf("2^32 / 125M = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong);
    Serial.println("Scaled fixed point tests");
    uint64_t TestFreq,TestCount;
    CtPerHz = 1LL << 63; // start with 2^31 to avoid overflow
    PrintHexLL(Buffer,&CtPerHz);
    printf("2^63 = [%s]\n",Buffer);
    CtPerHz /= 125000000LL / 2; // divided by 2 to to match 2^31
    PrintHexLL(Buffer,&CtPerHz);
    printf("2^63 / 125/2 M = [%s]\n",Buffer);
    PrintIntegerLL(Buffer,&CtPerHz);
    printf("Integer: %s\n",Buffer);
    PrintFractionLL(Buffer,&CtPerHz);
    printf("Fraction: %s\n",Buffer);
    PrintDecimalLL(Buffer,&CtPerHz);
    printf("Decimal: %s\n",Buffer);
    HzPerCt = 125000000LL; // 125 MHz / 2^32, directly to fraction part
    PrintDecimalLL(Buffer,&HzPerCt);
    printf("Hz Per Ct: %s\n",Buffer);
    TestFreq = 60000LL << 32;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000: %s\n",Buffer);
    TestCount = 60000LL * CtPerHz;
    PrintDecimalLL(Buffer,&TestCount);
    printf("60 kHz as count: %s\n",Buffer);
    TenthHzCt = CtPerHz / 10; // 0.1 Hz as counts
    PrintDecimalLL(Buffer,&TenthHzCt);
    printf("0.1 Hz as count: %s\n",Buffer);
    TestCount += TenthHzCt;
    PrintDecimalLL(Buffer,&TestCount);
    printf("60000.1 Hz as count: %s\n",Buffer);
    TestFreq = (TestCount >> 32) * HzPerCt;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000.1 Hz from count: %s\n",Buffer);
    TestCount = (60000LL * CtPerHz) + (2 * TenthHzCt);
    PrintDecimalLL(Buffer,&TestCount);
    printf("60000.2 Hz as count: %s\n",Buffer);
    TestFreq = (TestCount >> 32) * HzPerCt;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000.2 Hz from count: %s\n",Buffer);
    TestFreq = ((TestCount + (TenthHzCt / 2)) >> 32) * HzPerCt;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000.2 Hz rnd count: %s\n",Buffer);
    union ll_u LongLongUnion, TempLLU;
    printf("Union size: %d\n",sizeof(LongLongUnion));
    LongLongUnion.ll_64 = TestFreq;
    PrintHexLL(Buffer,&TestFreq);
    printf("TestFreq: [%s]\n",Buffer);
    PrintHexLL(Buffer,&LongLongUnion.ll_64);
    printf("Union ll: [%s]\n",Buffer);
    TempLLU.ll_64 = LongLongUnion.ll_32.low;
    TempLLU.ll_64 *= ONEGIG; // times 10^9 for conversion
    TempLLU.ll_64 >>= 32; // align integer part in low long
    sprintf(Buffer,"%lu.%09lu",LongLongUnion.ll_32.high,TempLLU.ll_32.low);
    printf("From union: %s\n",Buffer);
    sprintf(Buffer,"%lu.%09lu",LongLongUnion.ll_32.high,TempLLU.ll_32.low);
    printf("Buffer length: %d\n",strlen(Buffer));
    Buffer[strlen(Buffer) – 9 + 3] = 0;
    printf(" Trunc dec: %s\n",Buffer);
    }
    //———–
    void loop () {
    }