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.

AD9850 DDS Module: Hardware Assisted SPI and Fixed-point Frequency Stepping

Having conjured fixed-point arithmetic into working, the next step is to squirt data to the AD9850 DDS chip. Given that using the Arduino’s hardware-assisted SPI doesn’t require much in the way of software, the wiring looks like this:

Nano to DDS schematic
Nano to DDS schematic

Not much to it, is there? For reference, it looks a lot like you’d expect:

AD9850 DDS Module - swapped GND D7 pins
AD9850 DDS Module – swapped GND D7 pins

There’s no point in building an asynchronous interface with SPI interrupts and callbacks and all that rot, because squirting one byte at 1 Mb/s (a reasonable speed for hand wiring; the AD9850 can accept bits at 140+ MHz) doesn’t take all that long and it’s easier to have the low-level code stall until the hardware finishes:

#define PIN_HEARTBEAT    9          // added LED

#define PIN_RESET_DDS    7          // Reset DDS module
#define PIN_LATCH_DDS    8          // Latch serial data into DDS

#define PIN_SCK        13          // SPI clock (also Arduino LED!)
#define PIN_MISO      12          // SPI data input
#define PIN_MOSI      11          // SPI data output
#define PIN_SS        10          // SPI slave select - MUST BE OUTPUT = HIGH

void EnableSPI(void) {
  digitalWrite(PIN_SS,HIGH);        // set SPI into Master mode
  SPCR |= 1 << SPE;
}

void DisableSPI(void) {
  SPCR &= ~(1 << SPE);
}

void WaitSPIF(void) {
  while (! (SPSR & (1 << SPIF))) {
    TogglePin(PIN_HEARTBEAT);
    TogglePin(PIN_HEARTBEAT);
    continue;
  }
}

byte SendRecSPI(byte Dbyte) {           // send one byte, get another in exchange
  SPDR = Dbyte;
  WaitSPIF();
  return SPDR;                          // SPIF will be cleared
}

With that in hand, turning on the SPI hardware and waking up the AD9850 looks like this:

void EnableDDS(void) {

  digitalWrite(PIN_LATCH_DDS,LOW);          // ensure proper startup

  digitalWrite(PIN_RESET_DDS,HIGH);         // minimum reset pulse 40 ns, not a problem
  digitalWrite(PIN_RESET_DDS,LOW);
  delayMicroseconds(1);                     // max latency 100 ns, not a problem

  DisableSPI();                             // allow manual control of outputs
  digitalWrite(PIN_SCK,LOW);                // ensure clean SCK pulse
  PulsePin(PIN_SCK);                        //  ... to latch hardwired config bits
  PulsePin(PIN_LATCH_DDS);                  // load hardwired config bits = begin serial mode

  EnableSPI();                              // turn on hardware SPI controls
  SendRecSPI(0x00);                         // shift in serial config bits
  PulsePin(PIN_LATCH_DDS);                  // load serial config bits
}

Given 32 bits of delta phase data and knowing the DDS output phase angle is always zero, you just drop five bytes into a hole in the floor labeled “SPI” and away they go:

void WriteDDS(uint32_t DeltaPhase) {

  SendRecSPI((byte)DeltaPhase);             // low-order byte first
  SendRecSPI((byte)(DeltaPhase >> 8));
  SendRecSPI((byte)(DeltaPhase >> 16));
  SendRecSPI((byte)(DeltaPhase >> 24));

  SendRecSPI(0x00);                         // 5 MSBs = phase = 0, 3 LSBs must be zero

  PulsePin(PIN_LATCH_DDS);                  // write data to DDS
}

In order to have something to watch, the loop() increments the output frequency in steps of 0.1 Hz between 10.0 MHz ± 3 Hz, as set by the obvious global variables:

      PrintFixedPtRounded(Buffer,ScanFreq,1);

      TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
      printf("%12s -> %9ld\n",Buffer,TestCount.fx_32.high);

      WriteDDS(TestCount.fx_32.high);

      ScanFreq.fx_64 += ScanStep.fx_64;

      if (ScanFreq.fx_64 > (ScanTo.fx_64 + ScanStep.fx_64 / 2)) {
        ScanFreq = ScanFrom;
        Serial.println("Scan restart");
      }

Which produces output like this:

DDS SPI exercise
Ed Nisley - KE4ZNU - May 2017

Inputs: 124999656 = 125000000-344
Osc freq: 124999656.000000000
Hz/Ct: 0.029103750
Ct/Hz: 34.359832926
0.1 Hz Ct: 3.435983287
Test frequency:  10000000.0000
Delta phase: 343598329

Scan limits
 from:   9999997.0
   at:  10000000.0
   to:  10000003.0

Sleeping for a while ...

Startup done!

Begin scanning

  10000000.0 -> 343598329
  10000000.1 -> 343598332
  10000000.2 -> 343598336
  10000000.3 -> 343598339
  10000000.4 -> 343598343
  10000000.5 -> 343598346
  10000000.6 -> 343598349
  10000000.7 -> 343598353
  10000000.8 -> 343598356
  10000000.9 -> 343598360
  10000001.0 -> 343598363
  10000001.1 -> 343598367
  10000001.2 -> 343598370
  10000001.3 -> 343598373
<<< snippage >>>

The real excitement happens while watching the DDS output crawl across the scope screen in relation to the 10 MHz signal from the Z8301 GPS-locked reference:

DDS GPS - 10 MHz -48 Hz offset - zero beat
DDS GPS – 10 MHz -48 Hz offset – zero beat

The DDS sine in the upper trace is zero-beat against the GPS reference in the lower trace. There’s no hardware interlock, but they’re dead stationary during whatever DDS output step produces exactly 10.0000000 MHz. The temperature coefficient seems to be around 2.4 Hz/°C, so the merest whiff of air changes the frequency by more than 0.1 Hz.

It’s kinda like watching paint dry or a 3D printer at work, but it’s my paint: I like it a lot!

The Arduino source code as a GitHub Gist:

// SPI exercise for 60 kHz crystal tester
#include <avr/pgmspace.h>
//---------------------
// Pin locations
// SPI uses hardware support: those pins are predetermined
#define PIN_HEARTBEAT 9 // added LED
#define PIN_RESET_DDS 7 // Reset DDS module
#define PIN_LATCH_DDS 8 // Latch serial data into DDS
#define PIN_SCK 13 // SPI clock (also Arduino LED!)
#define PIN_MISO 12 // SPI data input
#define PIN_MOSI 11 // SPI data output
#define PIN_SS 10 // SPI slave select - MUST BE OUTPUT = HIGH
char Buffer[10+1+10+1]; // string buffer for long long conversions
#define GIGA 1000000000LL
#define MEGA 1000000LL
#define KILO 1000LL
struct ll_fx {
uint32_t low; // fractional part
uint32_t high; // integer part
};
union ll_u {
uint64_t fx_64;
struct ll_fx fx_32;
};
union ll_u CtPerHz; // will be 2^32 / 125 MHz
union ll_u HzPerCt; // will be 125 MHz / 2^32
union ll_u One; // 1.0 as fixed point
union ll_u Tenth; // 0.1 as fixed point
union ll_u TenthHzCt; // 0.1 Hz in counts
// All nominal values are integers for simplicity
#define OSC_NOMINAL (125 * MEGA)
#define OSC_OFFSET_NOMINAL (-344LL)
union ll_u OscillatorNominal; // nominal oscillator frequency
union ll_u OscOffset; // ... and offset, which will be signed 64-bit value
union ll_u Oscillator; // true oscillator frequency with offset
#define SCAN_WIDTH 6
#define SCAN_SETTLE 2000
union ll_u ScanFrom, ScanTo, ScanFreq, ScanStep; // frequency scan settings
union ll_u TestFreq,TestCount; // useful variables
#define HEARTBEAT_MS 3000
unsigned long MillisNow,MillisThen;
//-----------
// Useful functions
// Pin twiddling
void TogglePin(char bitpin) {
digitalWrite(bitpin,!digitalRead(bitpin)); // toggle the bit based on previous output
}
void PulsePin(char bitpin) {
TogglePin(bitpin);
TogglePin(bitpin);
}
// SPI I/O
void EnableSPI(void) {
digitalWrite(PIN_SS,HIGH); // set SPI into Master mode
SPCR |= 1 << SPE;
}
void DisableSPI(void) {
SPCR &= ~(1 << SPE);
}
void WaitSPIF(void) {
while (! (SPSR & (1 << SPIF))) {
TogglePin(PIN_HEARTBEAT);
TogglePin(PIN_HEARTBEAT);
continue;
}
}
byte SendRecSPI(byte Dbyte) { // send one byte, get another in exchange
SPDR = Dbyte;
WaitSPIF();
return SPDR; // SPIF will be cleared
}
// DDS module
void EnableDDS(void) {
digitalWrite(PIN_LATCH_DDS,LOW); // ensure proper startup
digitalWrite(PIN_RESET_DDS,HIGH); // minimum reset pulse 40 ns, not a problem
digitalWrite(PIN_RESET_DDS,LOW);
delayMicroseconds(1); // max latency 100 ns, not a problem
DisableSPI(); // allow manual control of outputs
digitalWrite(PIN_SCK,LOW); // ensure clean SCK pulse
PulsePin(PIN_SCK); // ... to latch hardwired config bits
PulsePin(PIN_LATCH_DDS); // load hardwired config bits = begin serial mode
EnableSPI(); // turn on hardware SPI controls
SendRecSPI(0x00); // shift in serial config bits
PulsePin(PIN_LATCH_DDS); // load serial config bits
}
// Write delta phase count to DDS
// This comes from the integer part of a 64-bit scaled value
void WriteDDS(uint32_t DeltaPhase) {
SendRecSPI((byte)DeltaPhase); // low-order byte first
SendRecSPI((byte)(DeltaPhase >> 8));
SendRecSPI((byte)(DeltaPhase >> 16));
SendRecSPI((byte)(DeltaPhase >> 24));
SendRecSPI(0x00); // 5 MSBs = phase = 0, 3 LSBs must be zero
PulsePin(PIN_LATCH_DDS); // write data to DDS
}
//-----------
// Round scaled fixed point to specific number of decimal places: 0 through 8
// You should display the value with only Decimals characters beyond the point
// Must calculate rounding value as separate variable to avoid mystery error
uint64_t RoundFixedPt(union ll_u TheNumber,unsigned Decimals) {
union ll_u Rnd;
// printf(" round before: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
Rnd.fx_64 = (One.fx_64 / 2) / (pow(10LL,Decimals));
// printf(" incr: %08lx %08lx\n",Rnd.fx_32.high,Rnd.fx_32.low);
TheNumber.fx_64 = TheNumber.fx_64 + Rnd.fx_64;
// printf(" after: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
return TheNumber.fx_64;
}
//-----------
// Multiply two unsigned scaled fixed point numbers without overflowing a 64 bit value
// The product of the two integer parts mut be < 2^32
uint64_t MultiplyFixedPt(union ll_u Mcand, union ll_u Mplier) {
union ll_u Result;
Result.fx_64 = ((uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.high) << 32; // integer parts (clear fract)
Result.fx_64 += ((uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.low) >> 32; // fraction parts (always < 1)
Result.fx_64 += (uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.low; // cross products
Result.fx_64 += (uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.high;
return Result.fx_64;
}
//-----------
// Long long print-to-buffer helpers
// Assumes little-Endian layout
void PrintHexLL(char *pBuffer,union ll_u FixedPt) {
sprintf(pBuffer,"%08lx %08lx",FixedPt.fx_32.high,FixedPt.fx_32.low);
}
// converts all 9 decimal digits of fraction, which should suffice
void PrintFractionLL(char *pBuffer,union ll_u FixedPt) {
union ll_u Fraction;
Fraction.fx_64 = FixedPt.fx_32.low; // copy 32 fraction bits, high order = 0
Fraction.fx_64 *= GIGA; // times 10^9 for conversion
Fraction.fx_64 >>= 32; // align integer part in low long
sprintf(pBuffer,"%09lu",Fraction.fx_32.low); // convert low long to decimal
}
void PrintIntegerLL(char *pBuffer,union ll_u FixedPt) {
sprintf(pBuffer,"%lu",FixedPt.fx_32.high);
}
void PrintFixedPt(char *pBuffer,union ll_u FixedPt) {
PrintIntegerLL(pBuffer,FixedPt); // do the integer part
pBuffer += strlen(pBuffer); // aim pointer beyond integer
*pBuffer++ = '.'; // drop in the decimal point, tick pointer
PrintFractionLL(pBuffer,FixedPt);
}
void PrintFixedPtRounded(char *pBuffer,union ll_u FixedPt,unsigned Decimals) {
char *pDecPt;
//char *pBase;
// pBase = pBuffer;
FixedPt.fx_64 = RoundFixedPt(FixedPt,Decimals);
PrintIntegerLL(pBuffer,FixedPt); // do the integer part
// printf(" Buffer int: [%s]\n",pBase);
pBuffer += strlen(pBuffer); // aim pointer beyond integer
pDecPt = pBuffer; // save the point location
*pBuffer++ = '.'; // drop in the decimal point, tick pointer
PrintFractionLL(pBuffer,FixedPt);
// printf(" Buffer all: [%s]\n",pBase);
if (Decimals == 0)
*pDecPt = 0; // 0 places means discard the decimal point
else
*(pDecPt + Decimals + 1) = 0; // truncate string to leave . and Decimals chars
// printf(" Buffer end: [%s]\n",pBase);
}
//-----------
// Calculate useful "constants" from oscillator info
// Args are integer constants in Hz
void CalcOscillator(uint32_t Base,uint32_t Offset) {
union ll_u Temp;
Oscillator.fx_32.high = Base + Offset; // get true osc frequency from integers
Oscillator.fx_32.low = 0;
HzPerCt.fx_32.low = Oscillator.fx_32.high; // divide oscillator by 2^32 with simple shifting
HzPerCt.fx_32.high = 0;
CtPerHz.fx_64 = -1; // Compute (2^32 - 1) / oscillator
CtPerHz.fx_64 /= (uint64_t)Oscillator.fx_32.high; // remove 2^32 scale factor from divisor
TenthHzCt.fx_64 = MultiplyFixedPt(Tenth,CtPerHz); // 0.1 Hz as delta-phase count
if (true) {
printf("Inputs: %ld = %ld%+ld\n",Base+Offset,Base,Offset);
PrintFixedPt(Buffer,Oscillator);
printf("Osc freq: %s\n",Buffer);
PrintFixedPt(Buffer,HzPerCt);
printf("Hz/Ct: %s\n",Buffer);
PrintFixedPt(Buffer,CtPerHz);
printf("Ct/Hz: %s\n",Buffer);
PrintFixedPt(Buffer,TenthHzCt);
printf("0.1 Hz Ct: %s",Buffer);
}
}
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//-----------
void setup ()
{
pinMode(PIN_HEARTBEAT,OUTPUT);
digitalWrite(PIN_HEARTBEAT,HIGH); // show we got here
Serial.begin (115200);
fdevopen(&s_putc,0); // set up serial output for printf()
Serial.println (F("DDS SPI exercise"));
Serial.println (F("Ed Nisley - KE4ZNU - May 2017\n"));
// DDS module controls
pinMode(PIN_LATCH_DDS,OUTPUT);
digitalWrite(PIN_LATCH_DDS,LOW);
pinMode(PIN_RESET_DDS,OUTPUT);
digitalWrite(PIN_RESET_DDS,HIGH);
// configure SPI hardware
SPCR = B01110001; // Auto SPI: no int, enable, LSB first, master, + edge, leading, f/16
SPSR = B00000000; // not double data rate
pinMode(PIN_SS,OUTPUT);
digitalWrite(PIN_SCK,HIGH);
pinMode(PIN_SCK,OUTPUT);
digitalWrite(PIN_SCK,LOW);
pinMode(PIN_MOSI,OUTPUT);
digitalWrite(PIN_MOSI,LOW);
pinMode(PIN_MISO,INPUT_PULLUP);
TogglePin(PIN_HEARTBEAT); // show we got here
// Calculate useful constants
One.fx_64 = 1LL << 32; // Set up 1.0, a very useful constant
Tenth.fx_64 = One.fx_64 / 10; // Likewise, 0.1
// Calculate oscillator "constants"
CalcOscillator(OSC_NOMINAL,OSC_OFFSET_NOMINAL);
TogglePin(PIN_HEARTBEAT); // show we got here
// Set up 10 MHz calibration output
TestFreq.fx_64 = One.fx_64 * (10 * MEGA);
PrintFixedPtRounded(Buffer,TestFreq,4);
printf("\nTest frequency: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert delta phase counts
TestCount.fx_64 = RoundFixedPt(TestCount,0); // ... to nearest integer
PrintFixedPt(Buffer,TestCount);
printf("Delta phase: %lu\n",TestCount.fx_32.high);
// Set up scan limits
ScanFreq = TestFreq;
ScanStep.fx_64 = One.fx_64 / 10; // 0.1 Hz = 3 or 4 tuning register steps
ScanFrom.fx_64 = ScanFreq.fx_64 - SCAN_WIDTH * (One.fx_64 >> 1); // centered on test freq
ScanTo.fx_64 = ScanFreq.fx_64 + SCAN_WIDTH * (One.fx_64 >> 1);
Serial.println("\nScan limits");
PrintFixedPtRounded(Buffer,ScanFrom,1);
printf(" from: %11s\n",Buffer);
PrintFixedPtRounded(Buffer,ScanFreq,1);
printf(" at: %11s\n",Buffer);
PrintFixedPtRounded(Buffer,ScanTo,1);
printf(" to: %11s\n",Buffer);
// Wake up and load the DDS
EnableDDS();
WriteDDS(TestCount.fx_32.high);
Serial.println("\nSleeping for a while ...");
delay(15 * 1000);
Serial.println("\nStartup done!");
Serial.println("\nBegin scanning\n");
MillisThen = millis();
}
//-----------
void loop () {
MillisNow = millis();
if ((MillisNow - MillisThen) >= SCAN_SETTLE) {
TogglePin(PIN_HEARTBEAT);
MillisThen = MillisNow;
if (true) {
PrintFixedPtRounded(Buffer,ScanFreq,1);
TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
printf("%12s -> %9ld\n",Buffer,TestCount.fx_32.high);
WriteDDS(TestCount.fx_32.high);
ScanFreq.fx_64 += ScanStep.fx_64;
if (ScanFreq.fx_64 > (ScanTo.fx_64 + ScanStep.fx_64 / 2)) {
ScanFreq = ScanFrom;
Serial.println("Scan restart");
}
}
}
}
view raw DDSSPITest.ino hosted with ❤ by GitHub
DDS SPI exercise
Ed Nisley - KE4ZNU - May 2017
Inputs: 124999656 = 125000000-344
Osc freq: 124999656.000000000
Hz/Ct: 0.029103750
Ct/Hz: 34.359832926
0.1 Hz Ct: 3.435983287
Test frequency: 10000000.0000
Delta phase: 343598329
Scan limits
from: 9999997.0
at: 10000000.0
to: 10000003.0
Sleeping for a while ...
Startup done!
Begin scanning
10000000.0 -> 343598329
10000000.1 -> 343598332
10000000.2 -> 343598336
10000000.3 -> 343598339
10000000.4 -> 343598343
10000000.5 -> 343598346
10000000.6 -> 343598349
10000000.7 -> 343598353
10000000.8 -> 343598356
10000000.9 -> 343598360
10000001.0 -> 343598363
10000001.1 -> 343598367
10000001.2 -> 343598370
10000001.3 -> 343598373
view raw DDSSPITest.txt hosted with ❤ by GitHub

Bathroom Door Retainer

The weather got warm enough to open the windows before pollen season started, which led to the front bathroom door slamming closed in the middle of the night when a gusty rainstorm blew through town. After far too many years, I decided this was an annoyance up with which I need no longer put.

A few minutes with OpenSCAD and Slic3r produces the shape:

Bathroom Door Retainer - Slic3r
Bathroom Door Retainer – Slic3r

It’s basically an extrusion of a 2D shape with a rectangular recess for the door chewed out.

An hour later, it’s in full effect:

Bathroom Door Retainer - installed
Bathroom Door Retainer – installed

The model now sports a little ball to secure the retainer against the towel bar:

Bathroom Door Retainer - bump
Bathroom Door Retainer – bump

Maybe someday I’ll reprint it.

That was easy …

The cast-iron pig sometimes standing guard as a doorstop in the relatively narrow doorway poses a bit of a foot hazard, so he moves into a closet during the off season. He can now remain there, snug and comfy, until a need for ballast arises.

The OpenSCAD source code as a GitHub Gist:

// Bathroom Door Retainer
// Ed Nisley KE4ZNU - May 2017
Layout = "Show"; // Show Build
//-------
//- Extrusion parameters must match reality!
ThreadThick = 0.20;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//-------
// Dimensions
TowelBarSide = 20.5; // towel bar across flat side
TowelBarAngle = 45; // rotation of top flat from horizontal
DoorOffset = 16.0; // from towel bar to door
DoorThick = 36.5;
WallThick = 4.0; // minimum wall thickness
RetainerDepth = 10.0; // thickness of retaining notch
NumSides = 6*4;
CornerRad = WallThick;
BarClipOD = TowelBarSide*sqrt(2) + 2*WallThick;
BarClipRad = BarClipOD/2;
OAH = RetainerDepth + WallThick;
module LatchPlan() {
union() {
linear_extrude(height=OAH,convexity=4)
difference() {
union() {
circle(d=BarClipOD,$fn=NumSides);
hull()
for (i=[0,1], j=[0,1])
translate([i*(BarClipRad + DoorOffset + DoorThick + WallThick - CornerRad),j*(BarClipRad - CornerRad)])
circle(r=CornerRad,$fn=4*4);
}
rotate(TowelBarAngle) // towel bar shape
square(size=TowelBarSide,center=true);
translate([0,-TowelBarSide/sqrt(2)]) // make access slot
rotate(-TowelBarAngle)
square(size=[2*TowelBarSide,TowelBarSide],center=false);
}
translate([0,-TowelBarSide/sqrt(2),OAH/2])
rotate([90,0,45])
sphere(r=TowelBarSide/25,$fn=4*3);
}
}
module Latch() {
difference() {
LatchPlan();
translate([BarClipRad + DoorOffset,-BarClipRad/2,-Protrusion])
cube([DoorThick,BarClipOD,RetainerDepth + Protrusion],center=false);
}
}
//-------
// Build it!
if (Layout == "Show") {
Latch();
}
if (Layout == "Build") {
translate([0,0,OAH])
rotate([180,0,0])
Latch();
}

Road Conditions: Rt 376 Brush North of Maloney

NYS DOT ground the asphalt surface and repaved Rt 376, dramatically improving the southern route to the rail trail along Maloney Drive.

Alas, the Japanese Knotweed continues to flourish:

This slideshow requires JavaScript.

I sent a note to their email contact and got the usual autoresponder message, but may have a side channel through the Dutchess County Planning Department to their Bicycle Coordinator. We shall see.

Beware the Hissing Goose!

Rolling into Vassar Farms, we encountered a Canadian Canada Goose (*) family:

Geese at Vassar Farm Pond 2017-05-21
Geese at Vassar Farm Pond 2017-05-21

The gander pulled straight up and hissed as we rolled by at what we thought was a respectful distance:

Geese at Vassar Farm Pond 2017-05-21 - detail
Geese at Vassar Farm Pond 2017-05-21 – detail

Their little fuzzballs retreated in good order under the fence toward the pond; they don’t need much survival training.

Word has it a goose family (perhaps this one) built their nest near a path around the ponds and defend their turf with sufficient resolve to deter even singletrack bikers.

I occasionally see snakes along the way, but none that hiss:

Black Snake on Rail Trail - 2017-04-28
Black Snake on Rail Trail – 2017-04-28

We approach rail-trail curves with a bit more caution than some folks; I’m at about the spot where that rider began losing control and didn’t quite wipe us out.

Update: They’re “Canada Geese“, with (AFAICT) a legal distinction between Canadian tourists and resident Yanks during the hunting season. Thanks to David for the reminder!

Arduino vs. Significant Figures: Useful 64-bit Fixed Point

Devoting eight bytes to every fixed point number may be excessive, but having nine significant figures apiece for the integer and fraction parts pushes the frequency calculations well beyond the limits of the DDS hardware, without involving any floating point library routines. This chunk of code performs a few more calculations using the format laid out earlier and explores a few idioms that may come in handy later.

Rounding the numbers to a specific number of decimal places gets rid of the repeating-digit problem that turns 0.10 into 0.099999:

uint64_t RoundFixedPt(union ll_u TheNumber,unsigned Decimals) {
union ll_u Rnd;

  Rnd.fx_64 = (One.fx_64 / 2) / (pow(10LL,Decimals));
  TheNumber.fx_64 = TheNumber.fx_64 + Rnd.fx_64;
  return TheNumber.fx_64;
}

That pretty well trashes the digits beyond the rounded place, so you shouldn’t display any more of them:

void PrintFixedPtRounded(char *pBuffer,union ll_u FixedPt,unsigned Decimals) {
char *pDecPt;

  FixedPt.fx_64 = RoundFixedPt(FixedPt,Decimals);

  PrintIntegerLL(pBuffer,FixedPt);  // do the integer part

  pBuffer += strlen(pBuffer);       // aim pointer beyond integer

  pDecPt = pBuffer;                 // save the point location
  *pBuffer++ = '.';                 // drop in the decimal point, tick pointer

  PrintFractionLL(pBuffer,FixedPt);

  if (Decimals == 0)
    *pDecPt = 0;                    // 0 places means discard the decimal point
  else
    *(pDecPt + Decimals + 1) = 0;   // truncate string to leave . and Decimals chars
}

Which definitely makes the numbers look prettier:

  Tenth.fx_64 = One.fx_64 / 10;             // Likewise, 0.1
  PrintFixedPt(Buffer,Tenth);
  printf("\n0.1: %s\n",Buffer);
  PrintFixedPtRounded(Buffer,Tenth,9);                    // show rounded value
  printf("0.1 to 9 dec: %s\n",Buffer);

  TestFreq.fx_64 = RoundFixedPt(Tenth,3);                 // show full string after rounding
  PrintFixedPt(Buffer,TestFreq);
  printf("0.1 to 3 dec: %s (full string)\n",Buffer);

  PrintFixedPtRounded(Buffer,Tenth,3);                    // show truncated string with rounded value
  printf("0.1 to 3 dec: %s (truncated string)\n",Buffer);

0.1: 0.099999999
0.1 to 9 dec: 0.100000000
0.1 to 3 dec: 0.100499999 (full string)
0.1 to 3 dec: 0.100 (truncated string)

  CtPerHz.fx_64 = -1;                       // Set up 2^32 - 1, which is close enough
  CtPerHz.fx_64 /= 125 * MEGA;              // divide by nominal oscillator
  PrintFixedPt(Buffer,CtPerHz);
  printf("\nCt/Hz = %s\n",Buffer);

  printf("Rounding: \n");
  for (int d = 9; d >= 0; d--) {
    PrintFixedPtRounded(Buffer,CtPerHz,d);
    printf("     %d: %s\n",d,Buffer);
  }

Ct/Hz = 34.359738367
Rounding:
     9: 34.359738368
     8: 34.35973837
     7: 34.3597384
     6: 34.359738
     5: 34.35974
     4: 34.3597
     3: 34.360
     2: 34.36
     1: 34.4
     0: 34

Multiplying two scaled 64-bit fixed-point numbers should produce a 128-bit result. For all the values we (well, I) care about, the product will fit into a 64-bit result, because the integer parts will always multiply out to less than 232 and we don’t care about more than 32 bits of fraction. This function multiplies two fixed point numbers of the form a.b × c.d by adding up the partial products thusly: ac + bd + ad + bc. The product of the integers ac won’t overflow 32 bits, the cross products ad and bc will always be slightly less than their integer factors, and the fractional product bd will always be less than 1.0.

Soooo, just multiply ’em out as 64-bit integers, shift the products around to align the appropriate parts, and add up the pieces:


uint64_t MultiplyFixedPt(union ll_u Mcand, union ll_u Mplier) {
union ll_u Result;

  Result.fx_64  = ((uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.high) << 32; // integer parts (clear fract) 
  Result.fx_64 += ((uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.low) >> 32;   // fraction parts (always < 1)
  Result.fx_64 += (uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.low;          // cross products
  Result.fx_64 += (uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.high;

  return Result.fx_64;
}

This may be a useful way to set magic numbers with a few decimal places, although it does require keeping the decimal point in mind:

  TestFreq.fx_64 = (599999LL * One.fx_64) / 10;           // set 59999.9 kHz differently
  PrintFixedPt(Buffer,TestFreq);
  printf("\nTest frequency: %s\n",Buffer);
  PrintFixedPtRounded(Buffer,TestFreq,1);
  printf("         round: %s\n",Buffer);

Test frequency: 59999.899999999
         round: 59999.9

Contrary to what I thought, computing the CtPerHz coefficient doesn’t require pre-dividing both 232 and the oscillator by 2, thus preventing the former from overflowing a 32 bit integer. All you do is knock the numerator down by one little itty bitty count you’ll never notice:

  CtPerHz.fx_64 = -1;                       // Set up 2^32 - 1, which is close enough
  CtPerHz.fx_64 /= 125 * MEGA;              // divide by nominal oscillator
  PrintFixedPt(Buffer,CtPerHz);
  printf("\nCt/Hz = %s\n",Buffer);

Ct/Hz = 34.359738367

That’s also the largest possible fixed-point number, because unsigned:

  TempFX.fx_64 = -1;
  PrintFixedPt(Buffer,TempFX);
  printf("Max fixed point: %s\n",Buffer);

Max fixed point: 4294967295.999999999

With nine.nine significant figures in the mix, tweaking the 125 MHz oscillator to within 2 Hz will work:

Oscillator tune: CtPerHz
 Oscillator: 125000000.00
 -10 -> 34.359741116
  -9 -> 34.359741116
  -8 -> 34.359740566
  -7 -> 34.359740566
  -6 -> 34.359740017
  -5 -> 34.359740017
  -4 -> 34.359739467
  -3 -> 34.359739467
  -2 -> 34.359738917
  -1 -> 34.359738917
  +0 -> 34.359738367
  +1 -> 34.359738367
  +2 -> 34.359737818
  +3 -> 34.359737818
  +4 -> 34.359737268
  +5 -> 34.359737268
  +6 -> 34.359736718
  +7 -> 34.359736718
  +8 -> 34.359736168
  +9 -> 34.359736168
 +10 -> 34.359735619

So, all in all, this looks good. The vast number of strings in the test program bulk it up beyond reason, but in actual practice I think the code will be smaller than the equivalent floating point version, with more significant figures. Speed isn’t an issue either way, because the delays waiting for the crystal tester to settle down at each frequency step should be larger than any possible computation.

The results were all verified with my trusty HP 50g and HP-15C calculators, both of which wipe the floor with any other way of handling mixed binary / hex / decimal arithmetic. If you do bit-wise calculations, even on an irregular basis, get yourself a SwissMicro DM16L; you can thank me later.

The Arduino source code as a GitHub Gist:

// Fixed point exercise for 60 kHz crystal tester
#include <avr/pgmspace.h>
char Buffer[10+1+10+1]; // string buffer for long long conversions
#define GIGA 1000000000LL
#define MEGA 1000000LL
#define KILO 1000LL
struct ll_fx {
uint32_t low;
uint32_t high;
};
union ll_u {
uint64_t fx_64;
struct ll_fx fx_32;
};
union ll_u CtPerHz; // will be 2^32 / 125 MHz
union ll_u HzPerCt; // will be 125 MHz / 2^32
union ll_u One; // 1.0 as fixed point
union ll_u Tenth; // 0.1 as fixed point
union ll_u TenthHzCt; // 0.1 Hz in counts
union ll_u Oscillator; // nominal oscillator frequency
union ll_u OscOffset; // oscillator calibration offset
union ll_u TestFreq,TestCount; // useful variables
union ll_u TempFX;
//-----------
// Round scaled fixed point to specific number of decimal places: 0 through 8
// You should display the value with only Decimals characters beyond the point
// Must calculate rounding value as separate variable to avoid mystery error
uint64_t RoundFixedPt(union ll_u TheNumber,unsigned Decimals) {
union ll_u Rnd;
// printf(" round before: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
Rnd.fx_64 = (One.fx_64 / 2) / (pow(10LL,Decimals));
// printf(" incr: %08lx %08lx\n",Rnd.fx_32.high,Rnd.fx_32.low);
TheNumber.fx_64 = TheNumber.fx_64 + Rnd.fx_64;
// printf(" after: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
return TheNumber.fx_64;
}
//-----------
// Multiply two unsigned scaled fixed point numbers without overflowing a 64 bit value
// The product of the two integer parts mut be < 2^32
uint64_t MultiplyFixedPt(union ll_u Mcand, union ll_u Mplier) {
union ll_u Result;
Result.fx_64 = ((uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.high) << 32; // integer parts (clear fract)
Result.fx_64 += ((uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.low) >> 32; // fraction parts (always < 1)
Result.fx_64 += (uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.low; // cross products
Result.fx_64 += (uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.high;
return Result.fx_64;
}
//-----------
// Long long print-to-buffer helpers
// Assumes little-Endian layout
void PrintHexLL(char *pBuffer,union ll_u FixedPt) {
sprintf(pBuffer,"%08lx %08lx",FixedPt.fx_32.high,FixedPt.fx_32.low);
}
// converts all 9 decimal digits of fraction, which should suffice
void PrintFractionLL(char *pBuffer,union ll_u FixedPt) {
union ll_u Fraction;
Fraction.fx_64 = FixedPt.fx_32.low; // copy 32 fraction bits, high order = 0
Fraction.fx_64 *= GIGA; // times 10^9 for conversion
Fraction.fx_64 >>= 32; // align integer part in low long
sprintf(pBuffer,"%09lu",Fraction.fx_32.low); // convert low long to decimal
}
void PrintIntegerLL(char *pBuffer,union ll_u FixedPt) {
sprintf(pBuffer,"%lu",FixedPt.fx_32.high);
}
void PrintFixedPt(char *pBuffer,union ll_u FixedPt) {
PrintIntegerLL(pBuffer,FixedPt); // do the integer part
pBuffer += strlen(pBuffer); // aim pointer beyond integer
*pBuffer++ = '.'; // drop in the decimal point, tick pointer
PrintFractionLL(pBuffer,FixedPt);
}
void PrintFixedPtRounded(char *pBuffer,union ll_u FixedPt,unsigned Decimals) {
char *pDecPt;
//char *pBase;
// pBase = pBuffer;
FixedPt.fx_64 = RoundFixedPt(FixedPt,Decimals);
PrintIntegerLL(pBuffer,FixedPt); // do the integer part
// printf(" Buffer int: [%s]\n",pBase);
pBuffer += strlen(pBuffer); // aim pointer beyond integer
pDecPt = pBuffer; // save the point location
*pBuffer++ = '.'; // drop in the decimal point, tick pointer
PrintFractionLL(pBuffer,FixedPt);
// printf(" Buffer all: [%s]\n",pBase);
if (Decimals == 0)
*pDecPt = 0; // 0 places means discard the decimal point
else
*(pDecPt + Decimals + 1) = 0; // truncate string to leave . and Decimals chars
// printf(" Buffer end: [%s]\n",pBase);
}
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//-----------
void setup ()
{
Serial.begin (115200);
fdevopen(&s_putc,0); // set up serial output for printf()
Serial.println (F("DDS calculation exercise"));
Serial.println (F("Ed Nisley - KE4ZNU - May 2017\n"));
// set up useful constants
TempFX.fx_64 = -1;
PrintFixedPt(Buffer,TempFX);
printf("Max fixed point: %s\n",Buffer);
One.fx_32.high = 1; // Set up 1.0, a very useful constant
PrintFixedPt(Buffer,One);
printf("\n1.0: %s\n",Buffer);
Tenth.fx_64 = One.fx_64 / 10; // Likewise, 0.1
PrintFixedPt(Buffer,Tenth);
printf("\n0.1: %s\n",Buffer);
PrintFixedPtRounded(Buffer,Tenth,9); // show rounded value
printf("0.1 to 9 dec: %s\n",Buffer);
TestFreq.fx_64 = RoundFixedPt(Tenth,3); // show full string after rounding
PrintFixedPt(Buffer,TestFreq);
printf("0.1 to 3 dec: %s (full string)\n",Buffer);
PrintFixedPtRounded(Buffer,Tenth,3); // show truncated string with rounded value
printf("0.1 to 3 dec: %s (truncated string)\n",Buffer);
CtPerHz.fx_64 = -1; // Set up 2^32 - 1, which is close enough
CtPerHz.fx_64 /= 125 * MEGA; // divide by nominal oscillator
PrintFixedPt(Buffer,CtPerHz);
printf("\nCt/Hz = %s\n",Buffer);
printf("Rounding: \n");
for (int d = 9; d >= 0; d--) {
PrintFixedPtRounded(Buffer,CtPerHz,d);
printf(" %d: %s\n",d,Buffer);
}
HzPerCt.fx_64 = 125 * MEGA; // 125 MHz / 2^32, without actually shifting!
PrintFixedPt(Buffer,HzPerCt);
printf("\nHz/Ct: %s\n",Buffer);
TenthHzCt.fx_64 = MultiplyFixedPt(Tenth,CtPerHz); // 0.1 Hz as delta-phase count
PrintFixedPt(Buffer,TenthHzCt);
printf("\n0.1 Hz as ct: %s\n",Buffer);
printf("Rounding: \n");
for (int d = 9; d >= 0; d--) {
PrintFixedPtRounded(Buffer,TenthHzCt,d);
printf(" %d: %s\n",d,Buffer);
}
// Try out various DDS computations
TestFreq.fx_64 = One.fx_64 * (60 * KILO); // set 60 kHz
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TestFreq.fx_64 += Tenth.fx_64; // set 60000.1 kHz
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TestFreq.fx_64 -= Tenth.fx_64 * 2; // set 59999.9 kHz
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TestFreq.fx_64 = (599999LL * One.fx_64) / 10; // set 59999.9 kHz differently
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
PrintFixedPt(Buffer,TestFreq);
printf("Int ct -> freq: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestFreq.fx_64 = One.fx_64 * (10 * MEGA); // set 10 MHz
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
PrintFixedPt(Buffer,TestFreq);
printf("Int ct -> freq: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestFreq.fx_64 = One.fx_64 * (10 * MEGA); // set 10 MHz + 0.1 Hz
TestFreq.fx_64 += Tenth.fx_64;
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
PrintFixedPt(Buffer,TestFreq);
printf("Int ct -> freq: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestFreq.fx_64 = One.fx_64 * (10 * MEGA); // set 10 MHz - 0.1 Hz
TestFreq.fx_64 -= Tenth.fx_64;
PrintFixedPt(Buffer,TestFreq);
printf("\nTest frequency: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
PrintFixedPt(Buffer,TestCount);
printf("Delta phase ct: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestCount,0);
printf(" round to int: %s\n",Buffer);
TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
PrintFixedPt(Buffer,TestFreq);
printf("Int ct -> freq: %s\n",Buffer);
PrintFixedPtRounded(Buffer,TestFreq,1);
printf(" round: %s\n",Buffer);
Oscillator.fx_64 = One.fx_64 * (125 * MEGA);
Serial.println("Oscillator tune: CtPerHz");
PrintFixedPtRounded(Buffer,Oscillator,2);
printf(" Oscillator: %s\n",Buffer);
for (int i=-10; i<=10; i++) {
OscOffset.fx_64 = i * One.fx_64;
CtPerHz.fx_64 = 1LL << 63;
CtPerHz.fx_64 /= (Oscillator.fx_64 + OscOffset.fx_64) >> 33;
PrintFixedPt(Buffer,CtPerHz);
printf(" %+3d -> %s\n",i,Buffer);
}
}
//-----------
void loop () {
}
view raw DDSCalcTest.ino hosted with ❤ by GitHub
DDS calculation exercise
Ed Nisley - KE4ZNU - May 2017
Max fixed point: 4294967295.999999999
1.0: 1.000000000
0.1: 0.099999999
0.1 to 9 dec: 0.100000000
0.1 to 3 dec: 0.100499999 (full string)
0.1 to 3 dec: 0.100 (truncated string)
Ct/Hz = 34.359738367
Rounding:
9: 34.359738368
8: 34.35973837
7: 34.3597384
6: 34.359738
5: 34.35974
4: 34.3597
3: 34.360
2: 34.36
1: 34.4
0: 34
Hz/Ct: 0.029103830
0.1 Hz as ct: 3.435973831
Rounding:
9: 3.435973832
8: 3.43597383
7: 3.4359738
6: 3.435974
5: 3.43597
4: 3.4360
3: 3.436
2: 3.44
1: 3.4
0: 3
Test frequency: 60000.000000000
round: 60000.0
Delta phase ct: 2061584.302070550
round to int: 2061584
Test frequency: 60000.099999999
round: 60000.1
Delta phase ct: 2061587.738044382
round to int: 2061588
Test frequency: 59999.900000000
round: 59999.9
Delta phase ct: 2061580.866096718
round to int: 2061581
Test frequency: 59999.899999999
round: 59999.9
Delta phase ct: 2061580.866096710
round to int: 2061581
Int ct -> freq: 59999.914551639
round: 59999.9
Test frequency: 10000000.000000000
round: 10000000.0
Delta phase ct: 343597383.678425103
round to int: 343597384
Int ct -> freq: 10000000.014506079
round: 10000000.0
Test frequency: 10000000.099999999
round: 10000000.1
Delta phase ct: 343597387.114398935
round to int: 343597387
Int ct -> freq: 10000000.114506079
round: 10000000.1
Test frequency: 9999999.900000000
round: 9999999.9
Delta phase ct: 343597380.242451271
round to int: 343597380
Int ct -> freq: 9999999.914506079
round: 9999999.9
Oscillator tune: CtPerHz
Oscillator: 125000000.00
-10 -> 34.359741116
-9 -> 34.359741116
-8 -> 34.359740566
-7 -> 34.359740566
-6 -> 34.359740017
-5 -> 34.359740017
-4 -> 34.359739467
-3 -> 34.359739467
-2 -> 34.359738917
-1 -> 34.359738917
+0 -> 34.359738367
+1 -> 34.359738367
+2 -> 34.359737818
+3 -> 34.359737818
+4 -> 34.359737268
+5 -> 34.359737268
+6 -> 34.359736718
+7 -> 34.359736718
+8 -> 34.359736168
+9 -> 34.359736168
+10 -> 34.359735619
view raw DDSCalcTest.txt hosted with ❤ by GitHub

Sharing the Lane on Burnett Blvd. at Rt 55

When we get to the end of Overocker Road, we occupy the entire left-and-straight lane, because we’re turning left onto Burnett Blvd and there’s no room for another vehicle beside us:

Burnett at Rt 55 - Right pass - 2017-05-23 - 1
Burnett at Rt 55 – Right pass – 2017-05-23 – 1

I’m towing a trailer of groceries.

On Burnett Blvd, we take the left side of the right lane (marked for left-and-right turns), because we’re turning left onto Rt 55, don’t want to get right-hooked by right-on-red traffic, and will be on the right side of the right lane of Rt 55 when we’re through the turn.

Without turn signals, it’s not clear whether the car following us from Overocker will turn left or right, but the driver is snuggling up next to Mary:

Burnett at Rt 55 - Right pass - 2017-05-23 - 2
Burnett at Rt 55 – Right pass – 2017-05-23 – 2

The driver’s window is sliding downward. Fortunately, we started moving before any comments were made. Perhaps he was going tell us we’re riding cool bikes?

Ah-ha! The driver is turning left and intending to pass me on the right while we’re in the intersection:

Burnett at Rt 55 - Right pass - 2017-05-23 - 3
Burnett at Rt 55 – Right pass – 2017-05-23 – 3

Helmet mirror FTW!

I’m moving rightward across the turning lane to end up on the right side of the Rt 55 lane, while not riding across the steel manhole cover at the car’s front wheel:

Burnett at Rt 55 - Right pass - 2017-05-23 - 4
Burnett at Rt 55 – Right pass – 2017-05-23 – 4

Mary doesn’t accelerate nearly as hard as I do; those pictures are one second apart.

I’m un-leaning from the turn into Rt 55, with the trailer still on my left and the driver accelerating toward me:

Burnett at Rt 55 - Right pass - 2017-05-23 - 5
Burnett at Rt 55 – Right pass – 2017-05-23 – 5

A close pass, but not too bad:

Burnett at Rt 55 - Right pass - 2017-05-23 - 6
Burnett at Rt 55 – Right pass – 2017-05-23 – 6

Most of the time, our rides aren’t this interesting, but I have plenty of examples showing how NYS DOT’s road designs ignore cyclists. The Burnett intersection signals still give us four seconds to clear the intersection.