It seemed like I was going to have to dig in and become an expert on gpio operation. Instead I decided to throw the problem over to
and see if it could sort it out. It produced a credible analysis of root cause and an alternate implementation that I have tested.
Originally, Pasco explicitly put the inversion in the report_ss instead of the debounce code, saying that it seemed best to change it as late as possible. However it seems that the change needs to be as early as possible, otherwise there is a conflict between the internal and external state of the switches.
Others: I'd be grateful for additional review.
PiDP-8/I DEP Switch Inversion Bug - Root Cause Analysis
========================================================
Bill Cattey and Claude.ai, February 2026
PROBLEM SUMMARY
===============
When the --invert-dep configuration option is enabled, the system hangs during
OS/8 restart (Load Address 7600 / Start). The system becomes completely
unresponsive, requiring a power cycle to recover.
Initial testing showed the DEP switch working correctly in isolation, but the
hang occurs specifically during the OS/8 boot sequence.
ROOT CAUSE IDENTIFIED
=====================
The bug is NOT a race condition or timing issue. It's a **permanent state
mismatch** between internal and external switch state variables caused by
inverting the switch value at the wrong point in the code flow.
The State Mismatch
------------------
The original patch placed the inversion in report_ss():
static void report_ss(int row, int col, int ss, struct switch_state* pss)
{
pss->stable_state = ss; // Line 1: Store BEFORE inversion
pss->last_change = na_ms;
int mask = 1 << col;
#ifdef INVERT_DEP
if (row == 2 && mask == SS2_DEP)
ss = !ss; // Line 2: INVERT here
#endif
if (ss) switchstatus[row] |= mask; // Line 3: Store AFTER inversion
else switchstatus[row] &= ~mask;
}
This creates a critical problem:
1. **Line 1**: gss[].stable_state is set to the NON-inverted value
2. **Line 2**: ss is inverted
3. **Line 3**: switchstatus[] is set to the INVERTED value
Result: The two state variables DISAGREE and never get corrected.
Demonstrating the Bug
---------------------
Testing with debug output on an UNMODIFIED board (DEP in standard orientation)
with --invert-dep enabled shows the mismatch:
BEFORE THE FIX:
DEP: count=1000, gpio_ss=1, gss_stable=1, switchstatus=0
gpio_ss=1 <- GPIO reads DEP as open (physically open)
gss_stable=1 <- Internal state thinks it's open
switchstatus=0 <- External state reports it as CLOSED
MISMATCH! Internal and external state disagree.
Why the Mismatch Never Corrects
--------------------------------
The debounce_switch() function is called on every GPIO scan for every switch:
static void debounce_switch(int row, int col, int ss, ms_time_t now_ms)
{
struct switch_state* pss = &gss[row][col];
if (!gss_initted) {
// First time: calls report_ss(), creates the mismatch
report_ss(row, col, ss, pss);
}
else if (ss == pss->stable_state) {
// Subsequent scans: ss=1, stable_state=1, they MATCH!
// Just resets timer and returns, never calls report_ss() again
pss->last_change = na_ms;
return;
}
// ... other cases for actual switch changes
}
After initialization:
1. GPIO reads DEP = 1 (physically open)
2. gss[].stable_state = 1 (stored before inversion)
3. Condition "ss == pss->stable_state" is TRUE (1 == 1)
4. Code takes the "line 372" path: just resets timer and returns
5. report_ss() is NEVER called again
6. switchstatus[] stays frozen at 0 (inverted value from initialization)
**The mismatch is permanent for the entire session.**
Why This Causes the System Hang
--------------------------------
With switchstatus[] showing DEP as permanently closed (0):
1. SIMH thinks the DEP switch is constantly pressed
2. During OS/8 boot, this can:
- Trigger spurious deposit operations at critical moments
- Block boot sequences waiting for DEP to be released
- Corrupt initialization of critical data structures
3. The "hang" is actually SIMH responding to what it thinks is a held-down
DEP switch during the sensitive boot sequence
THE SOLUTION
============
Move the inversion to debounce_switch() BEFORE any state management:
static void debounce_switch(int row, int col, int ss, ms_time_t now_ms)
{
struct switch_state* pss = &gss[row][col];
#ifdef INVERT_DEP
// Invert the raw GPIO value BEFORE any processing
// This ensures BOTH gss[].stable_state AND switchstatus[]
// see the same inverted value
if (row == 2 && (1 << col) == SS2_DEP) {
ss = !ss;
}
#endif
if (!gss_initted) {
report_ss(row, col, ss, pss); // Now stores inverted value in BOTH places
}
else if (ss == pss->stable_state) {
pss->last_change = na_ms;
}
// ... rest of function
}
Why This Works
--------------
Now the inversion happens at the RAW GPIO read level, before any state
management:
1. GPIO reads DEP = 1 (physically open)
2. **Inversion happens immediately**: ss = !1 = 0
3. debounce_switch() processes ss=0
4. report_ss() is called with ss=0
5. Sets gss[].stable_state = 0
6. Sets switchstatus[] = 0
7. **BOTH state variables agree: DEP is closed (inverted from physically open)**
This consistency is maintained for:
- Initial GPIO scan during initialization
- All subsequent scans during normal operation
- Actual switch state changes (debouncing logic sees consistent values)
VERIFICATION
============
Testing with debug output after the fix shows consistency:
AFTER THE FIX:
DEP: count=1000, gpio_ss=1, gss_stable=0, switchstatus=0
DEP(after): inverted_ss=0
gpio_ss=1 <- GPIO reads DEP as open (physically open)
inverted_ss=0 <- Inversion happens immediately
gss_stable=0 <- Internal state: inverted to closed
switchstatus=0 <- External state: inverted to closed
ALL THREE AGREE! State is consistent.
Functional Testing
------------------
With the fix in place:
1. ✅ OS/8 boots successfully to monitor dot prompt
2. ✅ No hang during initial boot
3. ✅ No hang during restart (boot rk0 from sim> prompt)
4. ✅ System remains responsive to Ctrl-E
5. ✅ Can cleanly quit simulator
6. ✅ State consistency maintained throughout all operations
THE CODE CHANGES
=================
File: src/pidp8i/
gpio-common.c.inChange 1: Remove from report_ss()
----------------------------------
REMOVE the entire #ifdef INVERT_DEP block, leaving:
static void report_ss(int row, int col, int ss,
struct switch_state* pss)
{
pss->stable_state = ss;
pss->last_change = na_ms;
int mask = 1 << col;
if (ss) switchstatus[row] |= mask;
else switchstatus[row] &= ~mask;
#ifdef DEBUG
printf("%cSS[%d][%02d] = %d ", gss_initted ? 'N' : 'I', row, col, ss);
#endif
}
Change 2: Add to debounce_switch()
-----------------------------------
ADD the inversion block at the beginning, right after the pss declaration:
static void debounce_switch(int row, int col, int ss, ms_time_t now_ms)
{
struct switch_state* pss = &gss[row][col];
#ifdef INVERT_DEP
// If the DEP(osit) switch is mounted in reverse orientation, i.e.
// active "up" as on a real PDP-8/I, invert the raw GPIO value here
// BEFORE any debouncing or state management. This ensures both
// gss[].stable_state and switchstatus[] see the same inverted value.
if (row == 2 && (1 << col) == SS2_DEP) {
ss = !ss;
}
#endif
if (!gss_initted) {
// First time thru, so set this switch's module-global and
// exported state to its defaults now that we know the switch's
// initial state.
report_ss(row, col, ss, pss);
}
// ... rest of function unchanged
SUMMARY
=======
The fix is minimal: move 6 lines of code from one function to another.
The impact is significant: it eliminates a permanent state mismatch that
caused the system to think DEP was constantly pressed, leading to hangs
during OS/8 boot.
The key insight: State transformations must happen at the raw input level,
BEFORE any state management, to ensure consistency across all code paths.
ATTRIBUTION
===========
Original --invert-dep patch by: Pesco
Root cause analysis and fix: Co-authored by Bill Cattey and Claude.ai
Testing: Bill Cattey
TESTING NOTES
=============
The testing was performed on an UNMODIFIED PiDP-8 board (DEP switch in
standard orientation, active-down) with --invert-dep enabled. This "reverse
polarity" test proves:
1. The state consistency bug exists with the original patch placement
2. The fix resolves the state consistency issue
3. The inversion logic works correctly (just backwards on unmodified hardware)
For users with modified boards (DEP installed upside-down, active-up), the
fix should work correctly with --invert-dep enabled, providing the authentic
PDP-8/I switch orientation.
Note on Misconfiguration Behavior
----------------------------------
When --invert-dep is enabled on unmodified hardware (reverse polarity test),
the debouncing logic operates on an inverted signal. This can cause switch
bounce artifacts. For example, during a front panel test sequence:
Load Address 200; Examine; Load Address 200; Set SR to 7000; Deposit
The deposit didn't occur until the DEP switch was released (physically opened,
seen as closed after inversion). During release, the switch bounced and
deposited the value to location 201 instead of 200.
This is expected behavior when debouncing an inverted signal - the debounce
logic sees transitions at opposite edges from the physical switch operation.
While this demonstrates why --invert-dep should only be used with modified
hardware, it's a benign outcome (slight timing glitch) rather than a
catastrophic failure (system hang).
The key point: The original bug caused PERMANENT state mismatch leading to
system hangs. The fix eliminates this mismatch. Any bounce artifacts from
misconfiguration are transient and non-destructive.
RECOMMENDATION
==============
This fix should be merged to the invert-deposit branch and tested by Pesco
on actual modified hardware before merging to trunk.
The fix resolves a definite bug in state management that prevented the
--invert-dep feature from working correctly.