Pi Zero W USB Proxy

I'm not sure exactly what to call it, but here's a little something I set up this weekend.
On my Linux desktop, I have occasional problems where being stopped at the debugger prompt for a plugged-in USB device hoses the whole computer. The problem waxes and wanes but on a particularly frustrating day I decided that maybe a Pi was the answer to the problem.
Using screen I can access the USB-serial devices on the pi, and using sshfs I can access the files. If the whole pi freezes, I can just reboot it with essentially no harm done.
I selected a Pi Zero W with a Zero4U hub and Adafruit MiniPiTFT 1.14" attached. To a base raspbian lite system I added some software, including tio, udiskie, screen, and Adafruit Blinka; enabled ssh access and disk mounting by the pi user, and set up GNU screen and my custom script for the LCD which is (confusingly) also called screen.
The screen shows information about each of the 4 USB connectors. In brackets "S" is shown if there is a serial device; "D" is shown if there's a partitioned disk, "d" if there's an unpartitioned disk; and "M" is shown if it is mounted. After that, the device name is shown.
Automount can be toggled with the B button (silk screen 23) and any non-mounted disks can be mounted with the A button (silk screen 24)
So far I've only used it lightly, but if it prevents a single crash of my desktop, it will be worth it.
This isn't a detailed guide so a lot of the setup is omitted. However, here are the scripts that are the essential parts:
source ~/.screenrc | |
zombie xa | |
screen tio /dev/serial/by-path/platform-20980000.usb-usb-0:1.1:1.0 | |
screen tio /dev/serial/by-path/platform-20980000.usb-usb-0:1.2:1.0 | |
screen tio /dev/serial/by-path/platform-20980000.usb-usb-0:1.3:1.0 | |
screen tio /dev/serial/by-path/platform-20980000.usb-usb-0:1.4:1.0 |
#!/usr/bin/python3 | |
# -*- coding: utf-8 -*- | |
import os.path | |
import time | |
import subprocess | |
import digitalio | |
import board | |
from PIL import Image, ImageDraw, ImageFont | |
import adafruit_rgb_display.st7789 as st7789 | |
from math import cos, pi | |
# Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4): | |
cs_pin = digitalio.DigitalInOut(board.CE0) | |
dc_pin = digitalio.DigitalInOut(board.D25) | |
reset_pin = None | |
# Config for display baudrate (default max is 24mhz): | |
BAUDRATE = 64000000 | |
# Setup SPI bus using hardware SPI: | |
spi = board.SPI() | |
# Create the ST7789 display: | |
disp = st7789.ST7789( | |
spi, | |
cs=cs_pin, | |
dc=dc_pin, | |
rst=reset_pin, | |
baudrate=BAUDRATE, | |
width=135, | |
height=240, | |
x_offset=53, | |
y_offset=40, | |
) | |
# Create blank image for drawing. | |
# Make sure to create image with mode 'RGB' for full color. | |
height = disp.width # we swap height/width to rotate it to landscape! | |
width = disp.height | |
image = Image.new("RGB", (width, height)) | |
rotation = 270 | |
# Get drawing object to draw on image. | |
draw = ImageDraw.Draw(image) | |
# Draw a black filled box to clear the image. | |
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0)) | |
disp.image(image, rotation) | |
# Draw some shapes. | |
# First define some constants to allow easy resizing of shapes. | |
padding = -2 | |
top = padding | |
bottom = height - padding | |
# Move left to right keeping track of the current x position for drawing shapes. | |
x = 0 | |
# Load a TTF font. | |
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 22) | |
# The max height of any glyph in the font is the sum of the ascent and descent | |
font_height = font.getmetrics()[0] + font.getmetrics()[1] | |
# Turn on the backlight | |
backlight = digitalio.DigitalInOut(board.D22) | |
backlight.switch_to_output() | |
backlight.value = True | |
buttonA = digitalio.DigitalInOut(board.D23) | |
buttonB = digitalio.DigitalInOut(board.D24) | |
buttonA.switch_to_input() | |
buttonB.switch_to_input() | |
class Invert: | |
def __init__(self, pin): | |
self.pin = pin | |
@property | |
def value(self): | |
return not self.pin.value | |
class EdgeDetect: | |
def __init__(self, pin): | |
self.pin = pin | |
self.old = self.pin.value | |
@property | |
def value(self): | |
new = self.pin.value | |
try: | |
return new and not self.old | |
finally: | |
self.old = new | |
def resolve_link(f): | |
try: | |
return os.path.realpath(f) | |
except OSError as e: | |
return f | |
def content(path): | |
try: | |
with open(path) as f: | |
return f.read().strip() | |
except: | |
return "" | |
def mounted(path): | |
path = resolve_link(path) | |
with open("/proc/mounts") as f: | |
for row in f: | |
if row.split()[0] == path: return True | |
return False | |
def spinner(): | |
while True: | |
yield from '.oOo' | |
spinner = spinner() | |
toggle_mount = EdgeDetect(Invert(buttonA)) | |
mount_once = EdgeDetect(Invert(buttonB)) | |
automount = False | |
serial_pattern = "/dev/serial/by-path/platform-20980000.usb-usb-0:1.{}:1.0" | |
disk_pattern = "/dev/disk/by-path/platform-20980000.usb-usb-0:1.{}:1.2-scsi-0:0:0:0-part1" | |
disk_pattern_alt = "/dev/disk/by-path/platform-20980000.usb-usb-0:1.{}:1.2-scsi-0:0:0:0" | |
sys_pattern = "/sys/devices/platform/soc/20980000.usb/usb1/1-1/1-1.{}/product" | |
shade = "#ffffff" | |
while True: | |
# Draw a black filled box to clear the image. | |
draw.rectangle((0, 0, width, height), outline=0, fill=0) | |
y = top | |
to_mount = [] | |
for i in [1, 2, 3, 4]: | |
serial_exists = os.path.exists(serial_pattern.format(i)) | |
serial_mark = "S" if serial_exists else " " | |
disk_path = disk_pattern.format(i) | |
disk_path = resolve_link(disk_path) | |
disk_exists = os.path.exists(disk_path) | |
if not disk_exists: | |
disk_path = disk_pattern_alt.format(i) | |
disk_path = resolve_link(disk_path) | |
disk_exists = os.path.exists(disk_path) | |
disk_mark = "d" if disk_exists else " " | |
else: | |
disk_mark = "D" | |
disk_mounted = mounted(disk_path) | |
disk_mark += "M" if disk_mounted else " " | |
descr = content(sys_pattern.format(i)) | |
ROW=f"{i}: [{serial_mark}{disk_mark}] {descr}" | |
draw.text((x, y), ROW, font=font, fill=shade) | |
y += font_height + 1 | |
if disk_exists and not disk_mounted: | |
to_mount.append(disk_path) | |
if toggle_mount.value: | |
automount = not automount | |
if automount or mount_once.value: | |
for a in to_mount: | |
ROW = f"mount {a}" | |
draw.text((x, y), ROW, font=font, fill=shade) | |
disp.image(image, rotation) | |
# Display image. | |
disp.image(image, rotation) | |
subprocess.run(['sudo', '-u', 'pi', 'udiskie-mount', a]) | |
draw.rectangle((x, y, width, y+font_height), outline=0, fill=0) | |
ROW = "automount B: off" if automount else "A: mount B: auto" | |
ROW = ROW + " " + next(spinner) | |
draw.text((x, y), ROW, font=font, fill=shade) | |
# Display image. | |
disp.image(image, rotation) | |
time.sleep(0.1) |
Some notes on the Si5351a
Si5351 "XA" can be driven by a clock signal (25/27 MHz, 1Vpp) source: [2] p23, figure 14 Internet commenters report success driving it with a much wider frequency range as well as successfully exceeding the specified PLL range. Inputs from 3MHz to 146MHz and PLL frequencies from 168 to 1168MHz were tested. source: [1] intermediate PLL frequency: 600 to 900 MHz [3] p2 divider range 15 + 0/1048575 .. 90 "integer divide" FBA_INT flag is for _EVEN_ integers [3] p4 multisynth: valid dividiers are 4, 6, 8, 8 + 1/1048575 ... 2048 "integer divide" FBA_INT flag is for _EVEN_ integers [3] p6
[1]: http://www.simonsdialogs.com/2018/11/si5351a-any-frequency-cmos-clock-generator-and-vco-specifications-myths-and-truth/
[2]: https://www.silabs.com/documents/public/data-sheets/Si5351-B.pdf (rev 1.3)
[3]: https://www.silabs.com/documents/public/application-notes/AN619.pdf Manually Generating an Si5351 Register Map rev 0.8
Si5351 Frequency Planner in Python
The Si5351 and related clock generators are very flexible, but they are a bit cumbersome to "program".
The basic design of the Si5351 is that an incoming frequency is first multiplied to create a high internal frequency (nominally in the range from 600MHz to 900MHz), then divided to create an output frequency. The multipliers and dividers have restrictions on their ranges, there are just 2 PLLs but 3 output clocks, and certain types of configurations (e.g., "divisor is an integer multiple of 2") are said to give lower jitter outputs than others.
The datasheet advising users on how to create their own register values is very complicated and some of the parts resist comprehension even after multiple readings. Thie chip maker Silicon Labs provides a graphical closed source Windows program for generating register maps, though some internet commenters regard it as buggy.
This program represents my own effort to create a frequency planner. It neglects some datasheet rules, mostly related to output frequencies above 50MHz or so. It tries to
- Favor lower-jitter configurations where possible
- Favor making the first-listed frequency as accurate as possible
- Try each combination of ways to allocate output clocks to the PLLs
- Obey the datasheet restrictions as I've understood them
- Do all arithmetic as infinite precision arithmetic, not floating point
- Implement the divisor rules for extremely fast clocks
- Implement the divisor rules for higher numbered outputs on 20QFN packages
For exact input clocks such as 25MHz, it is surprisingly hard to find a "plausible" frequency that is inexact, and even harder to find a "plausible" frequency that is less exact than your reference clock. I didn't actually find a case where the error is not much smaller than the frequency stability of any reference I'd plausibly have access to.
(Of course, if you measure your reference clock as 25MHz + 13.37Hz and plug that in as the --input-freq then almost everything is related to that clock inexactly, so the fact that the clocks will still be really, really close to the requested value is very nice.)
It does not directly create a register map, but the values shown are easy enough to convert to the form used in the CircuitPython adafruit_si5151 library. Sadly, as CircuitPython does not support the fractions module, it's not currently feasible to run this code on a CircuitPython board directly controlling the si5351 chip.
Example:
Generate 315M/88 (NTSC colorburst), 4.43361875M (PAL colour carrier), 25.8048M (UART clock) all exactly from a 25MHz reference clock or crystal. However, the PAL colour carrier will have more jitter since it uses a fractional divisor:
$ ./party.py 315M/88 4.43361875M 25.8048M Input frequency: 25000000 (25000000.0) Frequency plan score: 10.38 PLL A: Frequency plan type: Fractional multiplier, double integer divisor (1) Multiplier = 126/5 (25.2) Intermediate frequency = 630000000 (630000000.0) Desired output frequency: 39375000/11 (3579545.4545454546) Divider = 176 (176.0) Exact r_divider = 0 (/ 1) PLL B: Frequency plan type: Fractional multiplier, fractional divisor (5) Multiplier = 12059443/500000 (24.118886) Intermediate frequency = 602972150 (602972150.0) Desired output frequency: 17734475/4 (4433618.75) Divider = 136 (136.0) Exact r_divider = 0 (/ 1) Desired output frequency: 25804800 (25804800.0) Divider = 12059443/516096 (23.366666279141864) Exact r_divider = 0 (/ 1)
Generate pi MHz from a 25MHz reference clock or crystal (error: 27.8 nano Hz)
$ ./party.py 3.1415926535898M Input frequency: 25000000 (25000000.0) Frequency plan score: 1.00 PLL A: Frequency plan type: Fractional multiplier, fractional divisor (5) Multiplier = 24 (24.0) Intermediate frequency = 600000000 (600000000.0) Desired output frequency: 15707963267949/5000000 (3141592.6535898) Divider = 40306053/211042 (190.98593171027568) Actual output frequency: 42208400000000/13435351 (3141592.653589772) Relative Error: -8.83757e-15 Absolute Error: 2.78e-08Hz r_divider = 0 (/ 1)
The source is available in a github gist:
#!/usr/bin/python3 | |
# The MIT License (MIT) | |
# | |
# Copyright (c) 2020 Jeff Epler | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in | |
# all copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
# THE SOFTWARE. | |
import argparse | |
import fractions | |
import functools | |
import math | |
# Find all the ways to partition 'seq' into two subsequences a and b. | |
# Arbitrarily, the first item is always in the 'a' sequence, so e.g., | |
# the partitions of 2 items [p, q] are just 2: [[p], [q]] and [[p, q], []]. | |
# the partitions [[q], [p]] and [[], [p, q]] will not be returned. | |
# | |
# The empty sequence results in the single partition [[], []] | |
def partitions(seq): | |
seq = list(seq) | |
# Special case: empty sequence | |
if len(seq) == 0: | |
yield [], [] | |
else: | |
for j in range(1, 2**len(seq), 2): | |
yield ( | |
[si for i, si in enumerate(seq) if j & (1<<i)], | |
[si for i, si in enumerate(seq) if (~j) & (1<<i)] | |
) | |
# Convert various representations to fractions | |
# besides what fractions.Fraction will parse, you can write | |
# - underscores within numbers, as place separators | |
# 1_000_000 | |
# - Mixed fractions | |
# 1+3/4 or 1 3/4 (both equal to 7/4) | |
# or 1-3/4 (equal to 1/4) | |
# - Exponentials in any part | |
# 1e3 5_000e3/1e9 | |
# - Common suffixes: M, G, K, m, u | |
def F(n): | |
if isinstance(n, str): | |
n = n.replace("_", "") | |
n = n.replace("+", " ") | |
n = n.replace("-", " -") | |
n = n.replace("m", "e-3") | |
n = n.replace("u", "e-6") | |
n = n.replace("k", "e3") | |
n = n.replace("M", "e6") | |
n = n.replace("G", "e9") | |
if ' ' in n: # Accomodate 1 1/3, 1+1/3 | |
w, n = n.rsplit(None, 1) | |
else: | |
w = 0 | |
if '/' in n: | |
n, d = n.split('/') | |
else: | |
d = 1 | |
return F(w) + fractions.Fraction(n) / fractions.Fraction(d) | |
return fractions.Fraction(n) | |
def flcm(a, b): | |
a = F(a) | |
b = F(b) | |
p = a.numerator * b.denominator | |
q = b.numerator * a.denominator | |
r = math.gcd(p, q) | |
d = a.denominator * b.denominator | |
return fractions.Fraction(p * q, r * d) | |
def err_str(x): | |
if x == 0: return "0" | |
if x < 1e-12: return "%g" % x | |
if x < 1e-9: return "%.3fppt" % (x * 1e12) | |
if x < 1e-6: return "%.3fppb" % (x * 1e9) | |
return "%.3fppm" % (x * 1e6) | |
def place_in_range(freq, f_low): | |
while freq < f_low / 2048: | |
freq *= 2 | |
return freq | |
def ilog2(x): | |
j = 0 | |
while x > 1: | |
j += 1 | |
x /= 2 | |
return j | |
def calculate_freq(clocks, f_low, f_high): | |
freq = functools.reduce(flcm, clocks, 1) | |
n = (f_low + freq - 1) // freq | |
r = freq * n | |
return r | |
( | |
INTEGER_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, | |
FRACTIONAL_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, | |
INTEGER_MULTIPLIER_INTEGER_DIVISOR, | |
FRACTIONAL_MULTIPLIER_INTEGER_DIVISOR, | |
INTEGER_MULTIPLIER_FRACTIONAL_DIVISOR, | |
FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR, | |
) = range(6) | |
plan_names = { | |
INTEGER_MULTIPLIER_DOUBLE_INTEGER_DIVISOR: "Integer multiplier, double integer divisor", | |
FRACTIONAL_MULTIPLIER_DOUBLE_INTEGER_DIVISOR: "Fractional multiplier, double integer divisor", | |
INTEGER_MULTIPLIER_INTEGER_DIVISOR: "Integer multiplier, integer divisior", | |
FRACTIONAL_MULTIPLIER_INTEGER_DIVISOR: "Fractional multiplier, integer divisor", | |
INTEGER_MULTIPLIER_FRACTIONAL_DIVISOR: "Integer multiplier, fractional divisor", | |
FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR: "Fractional multiplier, fractional divisor", | |
} | |
MAX_DENOM = 1048575 | |
class Pll: | |
def __init__(self, f_in, clocks, *, f_low=600_000_000, f_high=900_000_000): | |
self.f_in = F(f_in) | |
self.f_low = F(f_low) | |
self.f_high = F(f_high) | |
self.clocks = [F(c) for c in clocks] | |
self.setting_type, self.multiplier = self._calculate() | |
def _calculate(self): | |
f_in = self.f_in | |
clocks = [place_in_range(c, self.f_low) for c in self.clocks] | |
clocks2 = [2*c for c in clocks] | |
f = calculate_freq(clocks2 + [f_in], self.f_low, self.f_high) | |
if f < self.f_high: | |
return INTEGER_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, f / f_in | |
clocks = [2*c for c in clocks] | |
f = calculate_freq(clocks2, self.f_low, self.f_high) | |
if f < self.f_high and (f / f_in).denominator <= MAX_DENOM: | |
return FRACTIONAL_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, f / f_in | |
f = calculate_freq(clocks + [f_in], self.f_low, self.f_high) | |
if f < self.f_high: | |
return INTEGER_MULTIPLIER_INTEGER_DIVISOR, f / f_in | |
while True: | |
clocks.pop() | |
if not clocks: | |
break | |
f = calculate_freq(clocks + [f_in], self.f_low, self.f_high) | |
if f < self.f_high: | |
return INTEGER_MULTIPLIER_INTEGER_DIVISOR, f / f_in | |
f = calculate_freq(clocks, self.f_low, self.f_high) | |
if f < self.f_high and (f / f_in).denominator <= MAX_DENOM: | |
return FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR, f / f_in | |
ratio = (self.f_low / f_in).limit_denominator(MAX_DENOM) | |
return FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR, ratio | |
def exact_divider(self, clock): | |
clock2 = place_in_range(clock, self.f_low) | |
return ((self.f_out / clock2), ilog2(clock2 / clock)) | |
def divider(self, clock): | |
de, r = self.exact_divider(clock) | |
d = de.limit_denominator(MAX_DENOM) | |
return d, r | |
def error(self, clock): | |
de, r = self.exact_divider(clock) | |
d = de.limit_denominator(MAX_DENOM) | |
return (de - d) / de | |
@property | |
def f_out(self): | |
return self.f_in * self.multiplier | |
@property | |
def dividers(self): | |
return [self.divider(c) for c in self.clocks] | |
@property | |
def errors(self): | |
return [self.error(c) for c in self.clocks] | |
@property | |
def score(self): | |
if not self.clocks: return 0 | |
e = len([e for e in self.errors if e == 0]) | |
did = len([d for d, r in self.dividers if d.denominator == 1 and d.numerator % 2 == 0]) | |
sid = len([d for d, r in self.dividers if d.denominator == 1]) | |
lc = len(self.clocks) | |
return 6-self.setting_type + fractions.Fraction(e, lc) + fractions.Fraction(did, lc*lc) + fractions.Fraction(did, lc**3) | |
def print(self): | |
print(f"Frequency plan type: {plan_names[self.setting_type]} ({self.setting_type})") | |
print(f"Multiplier = {self.multiplier} ({float(self.multiplier)})") | |
print(f"Intermediate frequency = {self.f_out} ({float(self.f_out)})") | |
print() | |
for c in self.clocks: | |
d, r = self.divider(c) | |
e = self.error(c) | |
print(f"Desired output frequency: {c} ({float(c)})") | |
print(f"Divider = {d} ({float(d)})") | |
if e == 0: | |
print("Exact") | |
else: | |
c_actual = self.f_out / d / (2**r) | |
print(f"Actual output frequency: {c_actual} ({float(c_actual)})") | |
print(f"Relative Error: {err_str(e)}") | |
print(f"Absolute Error: {float(c - c_actual):.3g}Hz") | |
print(f"r_divider = {r} (/ {2**r})") | |
print() | |
def plan(f_in, c1, c2): | |
p = Pll(f_in, c1) | |
q = Pll(f_in, c2) | |
return (p.score + q.score, p, q) | |
parser = argparse.ArgumentParser( | |
description='Create a frequency plan for Si5351') | |
parser.add_argument('frequencies', metavar='freq', nargs='+', | |
type=F, help='Integer, ratio, or decimal frequencies') | |
parser.add_argument('--input-frequency', '-i', metavar='freq', | |
default=25000000, type=F, | |
help='Input frequency') | |
args = parser.parse_args() | |
f_in = args.input_frequency | |
score, p, q = max((plan(f_in, p, q) for p, q in partitions(args.frequencies)), key=lambda s: s[0]) | |
print(f"Input frequency: {f_in} ({float(f_in)})") | |
print(f"Frequency plan score: {float(score):.2f}") | |
print() | |
print("PLL A:") | |
p.print() | |
if q.clocks: | |
print() | |
print("PLL B:") | |
q.print() |
Quad CharliePlex FeatherWing hack

Adafruit makes these neat "CharlieWing" displays that allow you to control a 15x7 LED matrix using the I2C bus. I2C uses two signal wires (called SDA and SCL, for Serial DAta and Serial CLock), and can connect multiple devices as long as they have different addresses.
I noticed that the bigger brother of this device, with a whopping 144 LEDs, could be configured for 4 different I2C addresses, while this one could only be configured for 2.
Or could it?
Calibrating the DS3231 and PCF8523 RTCs

The DS3231 and PCF8523 real time clocks (RTCs) can both be calibrated by writing various register values. To follow the calibration procedures you'll need a frequency counter you trust, with at least 6 digits to calibrate the PCF8523 and 7 digits to calibrate the DS3231. (It also has to operate at the comparatively low frequency of 32.768kHz; a common inexpensive 8-digit frequency counter such as the "SANJIAN STUDIO" has a minimum of 100kHz so it's not usable for this purpose) I use an old HP 5315B universal counter that has been calibrated against GPS time.
Helpful Scripts for CircuitPython & Real Time Clocks (RTCs)

I have used two different RTCs in the Feather form factor. One has the PCF8523, and the other has the DS3231. The former has an SD card slot while the latter has higher precision including a temperature-compensated crystal oscillator.
Precision vs Accuracy: A Clock
I was inspired by this watch face design (I think that's the original version) and by the arrival of a "OCXO", a very reliable time keeping circuit, to finally make an electronic clock.
Accuracy: Between hardware and software tuning, the OCXO keeps time with an accuracy of possibly better than 100 microseconds per day (loses or gains well less than a half second per year) (Yes, I'm deliberately ignoring a lot about crystal aging here!)
Precision: The time displayed is to the nearest minute, and the touchscreen setting mechanism is (deliberately?) poor, making it hard to set the time closer than +- 2 minutes or so. Oh, and it takes a good fraction of a second to update the screen anytime it changes. (The best way to set it seems to be to wait until a few seconds before 6AM/6PM and plug it in, since it boots with that time showing)
The clock consists of:- A PyBoard running micropython
- A touchscreen LCD and controller
- A 10MHz OCXO frequency reference

The dial as an animation (1 revolution = 12 hours)
Time to finish that languishing clock project!
A leap second has been announced at the end of June 2012.
Side track: wwvb links
Soldering "helping hands" made with Loc-Line coolant hose
Arduino Random Number Generator
Two-element capacitative touch sensor
Bah, it's garbage
1MHz+ Quadrature Divider for attiny13
400kHz Triple quadrature divider for atmega8 and quadrature state table generator
Creating a Quadrature Divider: What Won't Work
All older entries
Website Copyright © 2004-2024 Jeff Epler