Fix the keypad: row_out must be volatile

The previous commit removed three TRACE fprintfs from py32f071.c as
cleanup. That silently broke the keypad completely -- no press reached the
UI, and nothing warned about it.

Root cause is dead-code elimination, not the printing.
qdev_init_gpio_out_named() is inlinable and only records the row_out array;
the lines are filled in later by qdev_connect_gpio_out_named() from board
code, which GCC cannot see. At -O2 GCC therefore proves every element is
still NULL, sees that qemu_set_irq() returns immediately on a NULL irq, and
deletes the body of keypad_update_rows() along with all five calls to it. No
row line is ever driven and the firmware's scan reads all-high.

From the object code:

  callers reaching keypad_update_rows
    plain     none -- the calls are gone
    volatile  keypad_key_changed, keypad_col_changed, keypad_set_press,
              keypad_reset, uvk5_machine_init

keypad_col_changed compiles to a store and a ret with no call at all; with
volatile it ends in jmp keypad_update_rows. Declaring row_out volatile fixes
it at the cause. 10/10 on the press test, 3/3 on keypad_test.py, no build
warnings.

Scoped rather than assumed: PY32GpioState::out is not affected. Marking it
volatile too gives a byte-identical object file, because py32_gpio_write()
is only reachable through a MemoryRegionOps function-pointer table so GCC
cannot enumerate its callers. It stays plain.

Adds tools/keypad_test.py: boots its own instance on private ports and
checks that a short MENU press opens the menu, DOWN moves the cursor, and a
held key is visible to the scan. This is what should have caught the
breakage before it was pushed.

Docs corrected. The breakage had been written up as "power save stops the
keypad scan" and called a gap in the model; it was neither. AGENTS.md now
records the mechanism, the measurements, the objdump check, and the two
measurement traps that made this hard: reading gKeyReading0 after releasing
the key (always KEY_INVALID), and trusting a gdb breakpoint on
KEYBOARD_Poll (with the guest stopped the scan's delays cost no guest time,
so Poll returns KEY_MENU on a build where it fails when running free).

README screenshots regenerated from the current build.
This commit is contained in:
mckero committed 2026-08-27 18:34:41 +01:00
1 parent 1c9a2afe55
commit 2154f80414
6 files changed
+357 -49

No files matched your search

+23 -2
View File
@@ -414,7 +414,21 @@ struct UVK5KeypadState {
bool pressed[KEYPAD_COLS][KEYPAD_ROWS];
bool col_high[KEYPAD_COLS];
qemu_irq row_out[KEYPAD_ROWS];
/*
* volatile is required, not decorative. qdev_init_gpio_out_named() is
* inlinable and only records this array; the lines are filled in later by
* qdev_connect_gpio_out_named() from the board, which GCC cannot see. Left
* plain, GCC at -O2 proves every element is still NULL, notices that
* qemu_set_irq() returns immediately on a NULL irq, and deletes the whole
* body of keypad_update_rows() along with all five calls to it -- so no row
* line is ever driven and the firmware's keypad scan reads nothing. That
* failure is silent and looks exactly like a broken keypad model.
*
* Verified from the object code: without volatile, keypad_col_changed
* compiles to a store and a ret with no call at all; with it, the call is
* emitted. See AGENTS.md.
*/
qemu_irq volatile row_out[KEYPAD_ROWS];
};
/*
@@ -491,7 +505,14 @@ static void keypad_init(Object *obj)
qdev_init_gpio_in_named(dev, keypad_col_changed, "col", KEYPAD_COLS);
qdev_init_gpio_in_named(dev, keypad_key_changed, "key",
KEYPAD_COLS * KEYPAD_ROWS);
qdev_init_gpio_out_named(dev, s->row_out, "row", KEYPAD_ROWS);
/*
* Cast away volatile for the registration call only. row_out is declared
* volatile so GCC cannot conclude the lines stay NULL and delete
* keypad_update_rows() -- see the comment on the field. qdev only stores the
* pointer here, so dropping the qualifier for this one call is safe and
* keeps -Wdiscarded-qualifiers quiet.
*/
qdev_init_gpio_out_named(dev, (qemu_irq *)s->row_out, "row", KEYPAD_ROWS);
}
/*