No description
  • Python 42.1%
  • Assembly 33.9%
  • JavaScript 16.4%
  • CSS 4.4%
  • HTML 2.2%
  • Other 1%
Find a file
Hans Hübner c445a25221 config: switch WiFi SSID to roberta2.4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:36:16 +02:00
docs README: add web panel screenshot, document toolchain and test programs 2026-06-21 22:44:21 +02:00
pcb connect HALT button, schematic/pcb partity fixes 2026-07-04 07:25:22 +02:00
pdp11 pdp11: add scan, the bus-scan diagnostic 2026-06-26 09:39:57 +02:00
src config: switch WiFi SSID to roberta2.4 2026-07-08 17:36:16 +02:00
tools Move PDP-11 programs and bin2mop into pdp11/ 2026-06-21 19:45:02 +02:00
web web: double-width/height lines, wheel scrollback, 10k history 2026-06-23 09:09:32 +02:00
.gitignore pdp11: Makefile build, LTC test, drop committed binaries 2026-06-21 22:27:13 +02:00
CLAUDE.md pdp11: Makefile build, LTC test, drop committed binaries 2026-06-21 22:27:13 +02:00
deploy.sh deploy.sh: don't open a terminal on deploy 2026-06-20 20:46:19 +02:00
README.md front panel: halt button + run/pok/halt LEDs 2026-07-08 17:36:16 +02:00
requirements.txt README: add web panel screenshot, document toolchain and test programs 2026-06-21 22:44:21 +02:00

QBUS PDP-11 front-panel controller (Pico W / MicroPython)

A MicroPython re-implementation of the Avitech Arduino QBUS front panel for the Raspberry Pi Pico W. It synthesises the front-panel / power-supply signals a bare QBUS PDP-11 needs to run when it has no original DEC power supply or console:

  • Power sequencing — drives BDCOK H then BPOK H with correct timing once it sees the QBUS +5V rail come up.
  • Line-time clock — generates a 50/60 Hz square wave on BEVNT L.
  • Halt control — drives BHALT L.
  • Restart — pulses BDCOK L for ~50 µs to reinitialise a running CPU.
  • Run sensing — reads SRUN L to report whether the processor is executing.
  • NTP-disciplined LTC — uses the Pico W's WiFi to measure and correct the crystal's frequency error, pulling the line-time clock toward ~1 ppm long-term.

Everything is driven from a command line over the USB CDC serial port, and over WiFi from a web control panel the Pico serves itself (see Web control panel). Both work at the same time.

⚠️ Safety — read before wiring

  • On this H9278-A / BA23-family backplane, console connector J1 pin 1 is a +12V output. Connecting it to the Pico will destroy the Pico.
  • Connect GND (J1 pin 5) between the Pico and the backplane.
  • Other backplanes use different pinouts — meter every pin before connecting.

Wiring

The Pico-side GPIO assignment is fixed in src/qbus_panel.py. The controller is USB-powered (USB also carries the CLI) and senses +5V on GP8 through a ~110 kΩ series resistor; once +5V is present it waits 500 ms for the +12V rail before running the BDCOK → BPOK sequence.

This build is wired for a standalone H9278-A backplane driven by an AT PC PSU. On it the 10-pin console connector (J1) uses odd/even pin numbering with pin 1 at +12V, and BPOK H and +5V sense go to the high-current power-supply connector rather than J1.

Naming note: the BA23 manual also calls the backplane's power-supply connector "J1". Throughout this README J1 is the 10-pin console connector; the power connector is named explicitly.

Console connector J1:

 top row:  1:+12V     3:SRUN L   5:GND    7:spare   9:BHALT L
 bottom:   2:BEVNT L  4:key      6:GND    8:spare  10:BDCOK H
J1 pin Signal Pico Notes
1 +12 V leave unconnected
2 BEVNT L GP4 line-time clock
3 SRUN L GP5 run sense
4 key
5 GND GND Pico on-board GND
6 GND second ground (unused)
7 spare
8 spare
9 BHALT L GP6
10 BDCOK H GP7

+5 V sense and BPOK H go to the power-supply connector:

Signal Pico Connect to
+5 V sense GP8 any +5 V pin on the power-supply connector
BPOK H GP9 power-supply connector pin 15

The CPU needs BPOK H to run; the backplane pull-up plus GP9 drive it.

Front-panel indicators and the halt button are local to the PCB (not on the QBUS). Each LED is wired GPIO→resistor→LED→GND, so a high output lights it; the button is a momentary switch to ground read through the pin's internal pull-up:

Signal Pico Notes
RUN LED GP10 lit while the processor is running (SRUN toggling)
POK LED GP11 lit while BPOK H is asserted
HALT LED GP12 lit while BHALT L is asserted (processor halted)
HALT btn GP13 momentary to ground; each press toggles halt

Power-up / power-down sequence

+5V rail rises ──▶ wait 500 ms (settle, +12V comes up)
               ──▶ assert BDCOK H
               ──▶ wait 70 ms
               ──▶ assert BPOK H  ──▶ system live

+5V rail falls ──▶ de-assert BPOK H ──▶ de-assert BDCOK H ──▶ off

This runs automatically.

Autorun (default on). When +5 V is detected the controller releases BHALT and switches the line-time clock on. On power loss it switches the LTC off. Disable with autorun off (or AUTORUN_ON_POWERUP = False in config.py).

PIO usage

Two state machines use PIO:

  • SRUN L detection — toggles at ~166 kHz when the CPU runs; a PIO state machine (_srun_edges) pushes a word on every edge, and the CLI drains the FIFO to check whether new edges arrive.
  • BEVNT L line-time clock — generated by a PIO state machine (_ltc_od) running at the full system clock, toggling the pin direction. Frequency is set by an integer loop count, giving ~0.4 ppm tuning resolution for the NTP discipliner.

The 50 µs restart pulse is a one-shot with interrupts disabled.

On the Pico W the cyw43 WiFi driver permanently claims PIO1 SM0, so both state machines live on PIO0 (SM0 = SRUN, SM1 = LTC).

WiFi clock disciplining

This runs only on the Pico W and uses its WiFi. The LTC's accuracy is limited by the RP2040 crystal (~±30 ppm, plus temperature drift) — a few seconds per day in any PDP-11 software clock counting LTC ticks. The controller corrects it:

  1. It queries an NTP server.
  2. It maintains a wrap-free monotonic local clock and compares its rate against NTP over a baseline that grows from the first sample.
  3. The resulting ratio (local time ÷ true time) retunes the PIO LTC live.

Accuracy improves with baseline: roughly ±10 ppm after a few minutes and ~1 ppm after an hour. Watch it with the clock command. Without a usable time source the LTC runs at its nominal frequency.

WiFi setup

Set your network and NTP server in src/config.py (the WiFi password is stored in clear text):

WIFI_SSID = "roberta"
WIFI_PASSWORD = "..."
NTP_HOST = "pool.ntp.org"     # a LAN NTP server works too

Setup & deploy

The Pico must be running MicroPython. Download the Raspberry Pi Pico W build from micropython.org/download/RPI_PICO_W and copy the .uf2 onto the board while it is in BOOTSEL mode.

Host tooling lives in a Python virtualenv — mpremote (firmware deploy), pyserial (the tools/ host scripts) and pdpy11 (assembling the pdp11/ test programs):

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

The web UI additionally needs Node.js / npm (see Web control panel).

Edit src/config.py for your WiFi/NTP settings (see WiFi setup), then flash the firmware and open the CLI:

./deploy.sh                       # auto-detect port, copy, reset, open CLI
./deploy.sh /dev/tty.usbmodemXXXX # target a specific port
./deploy.sh copy                  # copy only
./deploy.sh repl                  # just open the CLI

deploy.sh copies all of src/ to the device root. main.py runs automatically on boot and starts the CLI.

Using the CLI

Connect with any serial terminal at the Pico's USB CDC port:

.venv/bin/mpremote connect /dev/tty.usbmodemXXXX repl

Commands:

Command Action
status power state, bus signal states, LTC, processor run state
run de-assert BHALT (allow the processor to run)
halt assert BHALT (halt the processor)
restart pulse BDCOK low ~50 µs (only when powered)
ltc on / ltc off enable / disable the line-time clock
ltc 50 / ltc 60 set LTC frequency in Hz
power show +5V sense and power-state-machine state
wifi show WiFi connection / IP
clock show NTP disciplining state (ppm correction, baseline)
sync force an NTP measurement now
sync reset restart the disciplining baseline
log on / log off enable/disable async state-change messages (default on)
autorun on / autorun off auto-run the CPU on power-up (BHALT released + LTC on; default on)
web show the web control panel URL / server state
web on / web off enable / disable the WiFi web server (default on)
help, ? command help

As the controller changes state it prints asynchronous * ... lines — the power-up sequence (+5V detected, BDCOK asserted, BPOK asserted), power loss, WiFi connect, NTP lock (with ppm), and processor run/halt. On by default; silence them with log off.

Ctrl-C drops out of the CLI to the MicroPython REPL; re-run with import main; main.main() or reset the board.

Defaults at boot: BHALT de-asserted (run permitted), LTC 50 Hz, power sequencing armed, autorun on — so on +5 V the LTC switches on and the CPU is released to run.

Web control panel

The UI runs on the host — from a dev server or a built local file — and reaches the Pico through its JSON API over WiFi.

Web control panel

Running the UI

cd web
npm install
npm run dev      # serve on http://localhost:8000
# or
npm run build    # build into web/dist/

See web/README.md for build details. Put the controller's IP (shown by the wifi, status or web CLI command) in the controller field at the top right. The page has two columns: the control panel on the left, and a serial console / memory / file-upload workspace on the right.

The control panel shows power state, +5 V sense, BDCOK/BPOK, BHALT, processor run, Wi-Fi, and the LTC frequency with NTP ppm correction, and provides Run/Halt, LTC on/off, 50/60 Hz, and a guarded Restart (enabled only when powered). It polls the Pico once a second.

HTTP API

The Pico exposes a JSON API over WiFi. Every response includes Access-Control-Allow-Origin: *.

Route Action
GET / a one-line text hint (the UI is served separately)
GET /api/status merged panel + clock status as JSON
GET /api/events?since=N state-change log entries with seq > N
GET /api/run · /api/halt · /api/restart as the CLI commands
GET /api/ltc?on=0|1 · /api/ltc?hz=50|60 line-time clock control
GET /api/autorun?on=0|1 · /api/sync autorun toggle / force NTP sync

Each action returns the post-action status JSON.

Serial console, memory and file upload (Web Serial)

The right column talks to a serial port from the browser via the Web Serial API — point it at the PDP-11's console line (a USB-serial adapter on its console SLU, where ODT lives) for the memory/upload features, or at the Pico's USB CDC for its CLI.

  • Serial console — a character-mode terminal (keys are sent to the port as you type; the remote echoes them) with a blinking cursor. The port and baud (default 9600) are remembered and reconnected on reload.
  • Memory — examine (addr/), deposit (addr/ value⏎) and go (addr g) against the processor's console ODT; the raw exchange shows in the terminal.
  • File upload — a browser port of ../odt-uploader/odt-uploader.py: it pokes a small loader to address 100, switches the machine to run (releases BHALT via the API), runs the loader with 100g, streams the .bin (the loader copies it to your octal load address and HALTs), and — with “run after” ticked — switches to run and starts the uploaded program at its load address.

ODT pacing. ODT is half-duplex with a single-character SLU buffer, so the page talks to it in lock-step: one character at a time, waiting for ODT's echo (or its formatted reply after /, <LF>, <CR>) before sending the next. The raw binary phase (loader running, no echo) is paced to the serial line rate. ODT echo/format varies between PDP-11 models; the terminal shows the exchange.

Web Serial needs a secure context. navigator.serial only exists over https, http://localhost or a local file:// page — which npm run dev (localhost) and dist/index.html (file) provide.

PDP-11 test programs

pdp11/ holds small standalone PDP-11 diagnostics that exercise the rig (romdump, rommap, romfind, ramtest, dl11test, ltctest — a line-time clock pass/fail test — and two CPU benchmarks). make (in pdp11/) assembles each .asm with pdpy11 to a raw image and wraps it into a MOP .3.SYS. Load a program either over the console with tools/pdp11.py upload <img> <addr> (or the web file-upload box), or over Ethernet via MOP. See pdp11/README.md and tools/README.md.

Layout

src/qbus_panel.py   QbusPanel: pins, power state machine, LTC (PIO), SRUN PIO, restart
src/clock_sync.py   ClockSync: WiFi + NTP-disciplined oscillator
src/web_server.py   WebServer: non-blocking HTTP JSON API (status/command)
src/config.py       site config (WiFi credentials, DNS, NTP server, web server)
src/main.py         USB CDC command-line interface + main loop
deploy.sh           copy firmware to device / open CLI via mpremote
requirements.txt    host venv: mpremote, pyserial, pdpy11
web/                front-end project (the control panel UI)
  src/              index.html, styles.css, JS modules, fonts
  build.mjs         build / dev-server script
tools/              host scripts driving the controller + PDP-11 console (ODT)
pdp11/              standalone PDP-11 test programs + MOP packaging
docs/               reference material (DESQA manual, mopd pacing patch)