- Python 42.1%
- Assembly 33.9%
- JavaScript 16.4%
- CSS 4.4%
- HTML 2.2%
- Other 1%
|
|
||
|---|---|---|
| docs | ||
| pcb | ||
| pdp11 | ||
| src | ||
| tools | ||
| web | ||
| .gitignore | ||
| CLAUDE.md | ||
| deploy.sh | ||
| README.md | ||
| requirements.txt | ||
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 HthenBPOK Hwith 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 Lfor ~50 µs to reinitialise a running CPU. - Run sensing — reads
SRUN Lto 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 ~1–10 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 Ldetection — 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 Lline-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:
- It queries an NTP server.
- It maintains a wrap-free monotonic local clock and compares its rate against NTP over a baseline that grows from the first sample.
- 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.
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 with100g, 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.serialonly exists over https, http://localhost or a local file:// page — whichnpm run dev(localhost) anddist/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)
