mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
copilot/fix-github-actions-unit-job
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
57106c0a66 |
Fix the two pixel bugs behind "the other firmware looks shifted"
uvk5_stream.py used STATUS_BYTES without importing it, so the panel branch raised NameError on every frame and a bare except swallowed it: every screen the page drew came from guest RAM at one build's addresses. The pump now reports which source it used and why, webui exposes it (/api/panel and frame_source), and test_uvk5_stream asserts the panel wins when reachable and that a fallback is announced. The ST7565 column counter wrapped at 128 instead of the controller's 132, so addresses 128..131 came back as 0..3, fell outside the col>=4 store, and were dropped: every row lost its last four pixels, which is where the battery icon lives. Pre-fix, filling a page with 0xFF left columns 124..127 blank; now they carry content and the page's frame matches the panel memory 8192/8192. |
||
|
|
f4c9d343fc |
Probe what a BK4819 read presents, and say plainly that it is not the guard yet
UVK5_BK4819_PROBE reassembles the sixteen bits a read clocks out and logs them against the register's own value: 1566 of 1566 agree on the working model. That agreement is not proof -- removing the skip_falling fix, which is exactly the historical left-shift regression, leaves the reassembled word unchanged, so this observation point is not the one the guest samples at. tools/test_bk4819_readback.sh therefore stays the guard. tools/test_bk4819_readback.py is the working draft of a portable replacement (no source patch, no rebuild, no ARM gdb) and is deliberately NOT registered in run_tests.sh, so a proven guard is not swapped for an unproven one. Both the file and the two READMEs say so. |
||
|
|
4bddccfe7b |
Measure how a build renders, and keep the tool that does it
tools/panel_dump.py prints the display controller's own memory as ASCII or PNG for any firmware, with the four mappings, so two builds can be compared instead of glanced at. The panel model gained a bounded UVK5_PANEL_PROBE diagnostic along the way. Measured: the 5.9.0.CN panel agrees with its own framebuffer 8188 of 8192 pixels with the data untouched, and the fetched 6.0.0 build renders identically. Both program the same geometry registers, which is why the mapping is a driver convention and cannot be derived from the controller: it has to be measured. Noted, not yet fixed: the display start line (0x40|n) is ignored, and the model stores pixels at col-4 with the column counter wrapping at 128 instead of the controller's 132 columns. |
||
|
|
1308c98769 |
Document where Moto/DFU entry is, and let a boot key hold PTT plus a key
The bootloader's DFU handler is reachable only when SRAM[0x20000020] is 3, which only a program that then resets can write. The application's 0x05DD takes that path only with ENABLE_OVERLAY; this build resets straight back into the application instead. Ruled out by measurement: PTT alone, PTT+SIDE1/SIDE2, MENU, a host byte in the boot window including 0x0530, and 0x05DD. boot-key now reads a + separated list, because the firmware's own BOOT_GetMode() needs PTT and a matrix key together for every special boot mode. Tested against all four documented combinations. |
||
|
|
2667e046e8 |
Emulator: multiboot slots from the page, flash controller, portable tests
flash controller: store ACR/OPTKEYR instead of swallowing them, which is what stopped the factory bootloader from starting slots over the firmware's own serial protocol (0x0720 family); uvk5_socket/uvk5_testenv so a fresh checkout skips instead of failing; web UI slot table and Multiboot button; quick start, CONTRIBUTING, and stop tracking firmware images and radio dumps |
||
|
|
8b995aa610 |
Make RSSI depend on tuning instead of being a constant
The S-meter had a number to draw, but a fixed RSSI above squelch meant the band was
uniformly and permanently occupied. Scanning, squelch, and every "is this channel busy"
decision therefore faced a situation that never varied, so none of that logic was
really being tested -- the tests passed without testing much.
RSSI is now derived from where the firmware tuned. BK4819_SetFrequency splits the
frequency across REG_38 and REG_39 (driver/bk4819.c:743), which the model already
records; verified against a live guest that 0x0262/0x5A00 reads back as 400.00000 MHz,
matching the screen. A small table of virtual stations plus a noise floor and a fade
either side of centre gives a band with signals in some places and not others.
Measured through the firmware's own tuning path -- typing 410.000 on the keypad rather
than poking the registers, so the test does not check the model against itself:
400.000 MHz (station) RSSI 0x01E5
410.000 MHz (empty) RSSI 0x0091 a gap of 85 dB
What is honest and what is not, recorded in the code: the shape is real physics, power
falls off away from a carrier with a noise floor underneath. The station list is
invented. So this reproduces "the firmware copes with a band that is busy in places",
which is genuine coverage, and it reproduces no actual radio environment -- a dBm figure
from here is not a claim about the world.
Also records why backlight PWM is deliberately left stubbed. Intermediate brightness
runs TIM7 -> DMA rewriting GPIOA BSRR at 128 kHz, so modelling it costs 128,000 GPIO
writes per emulated second and changes nothing observable: backlight is LED brightness
and never touches the framebuffer. The two endpoints that are observable, off and full,
bypass the timer and already work.
Full run: 16 passed, 0 failed.
|
||
|
|
fdcbe80056 |
Model TIM2, so millis() advances and timeouts can expire
Second finding from the audit. TIM2 was covered by the catch-all stub, which returns
the last value written, so
uint32_t millis(void) { return LL_TIM_GetCounter(TIM2); }
returned 0 forever. All 17 call sites that measure elapsed milliseconds could never
see time pass -- a silent wrong answer rather than a hang, which is harder to notice
and was not noticed.
The counter is derived on read from QEMU_CLOCK_VIRTUAL rather than stored, with
CR1.CEN starting and freezing it and a CNT write rebasing it. Guest time here is not
proportional to wall time anyway, and code measuring elapsed milliseconds wants
something advancing at roughly the rate a human sees; this is explicitly not for
anything needing cycle accuracy.
Measured: 24358 ms, then 29527 ms five seconds later -- 5169 ms elapsed, so the rate
is right rather than merely non-zero. The test checks the rate for that reason: a
counter ticking at the wrong speed would satisfy "non-zero" and "increasing" and still
break every timeout.
AGENTS.md now carries the audit itself: a table of what the firmware actually drives
against what is modelled versus stubbed, and the point that answering reads is not the
same as being reproduced. The honest summary is that the digital side the firmware
depends on is reproduced, and the analogue side is not and cannot be.
Full run: 15 passed, 0 failed.
|
||
|
|
e46cae2e48 |
Make the ADC settable, which reaches the battery behaviour
Prompted by a fair criticism: the reports said what runs, not what is actually reproduced. An audit found the ADC was modelled but returned a hardcoded 2200 forever, so gBatteryDisplayLevel, gLowBattery and the warning popup were all unreachable. A peripheral that answers reads is not the same as a peripheral that is reproduced. adc-result is now settable over QOM and clamped to 12 bits. Measured: 2200 gives level 4 and no warning, 1200 gives level 0 and raises gLowBattery, and the level recovers to 4 afterwards. tools/test_battery.py covers it, and deliberately does NOT assert that gLowBattery clears on recovery. helper/battery.c:190-204 only clears it when the level lands exactly on 2; above that it clears gLowBatteryConfirmed and leaves gLowBattery set. So 4 -> 0 -> 4 really does leave the flag raised. The first version of this test called that a failure -- the test was wrong, not the model. The emulator reproduces the firmware, including behaviour that looks like a bug. |
||
|
|
7d299dbad2 |
Model the audio path, which is a single enable line and no samples
Asked for a speaker and a microphone. The honest answer is that neither exists to
model: on the real radio neither passes through the MCU. Receive audio is demodulated
inside the BK4819 and leaves it as analogue on its AF pin; transmit audio goes from
the microphone into the chip's own ADC. The firmware's entire involvement is
- PA8, the amplifier enable (GPIO_EnableAudioPath, driver/gpio.h:34)
- REG_47, which AF source the chip routes
- REG_64, a level it displays
No audio samples exist anywhere in the MCU's address space, so a device model has
nothing to capture or play, and a browser has nothing to be granted permission for.
Synthesising sound would be inventing data the firmware never produced.
What is real is the firmware's intent, and PA8 states it exactly. TYPE_UVK5_AUDIO
watches that pin and exposes read-only speaker-on; the web UI shows it as a speaker
glyph beside the power state, and /api/status reports it. Read-only deliberately:
letting a test write it would only let the test lie to itself. The page asks for no
audio permission, and a test asserts it never will -- no getUserMedia, no
AudioContext, no <audio>.
Measured: amplifier off while idle in power save, on after SIDE1 engages monitor, and
still on afterwards rather than blipping.
One bug found the hard way, and the stub was the cause. QmpClient.command returns the
unwrapped value and raises on error, but the test stub returned {"return": ...}. So
webui.py was written to unwrap a second time, every test passed, and the live UI
returned 500 with "argument of type 'bool' is not iterable". The stub is now pinned to
the real contract by a test. A stub more forgiving than the real thing is worse than
no stub.
Also stopped swallowing the failure: a bare `except: return None` made the error
indistinguishable from a radio that simply was not making sound, and cost a detour
into looking for a stale process.
|
||
|
|
88994b1bd0 |
Add PTT, which brings the transmit level bar with it
PTT was the one input the model never had, so the radio could not be keyed and the
mic level bar was unreachable. It is not a matrix key -- GPIO_IsPttPressed reads
PB10 directly (driver/gpio.h:31, active low) -- so it gets its own GPIO line rather
than a column/row intersection.
With it the transmit screen is complete: TX annunciator, a running timer, and a
level bar at roughly 80% of scale, fed from REG_64 via BK4819_GetVoiceAmplitudeOut.
app/app.c:1700 draws that only while gCurrentFunction == FUNCTION_TRANSMIT with
gSetting_mic_bar set; the setting is Data[7] bit 4 at flash 0xA0A8, and blank flash
reads 0xFF, so it is already on.
Exposed as a boolean on the keypad device and as POST /api/ptt with an explicit
held flag, plus a button in the browser UI. Held rather than tapped, because
transmitting is a state the operator stays in and a fixed duration would be wrong
for it.
Releasing is treated as the important half:
- pointerleave and pointercancel release, so dragging off the button cannot leave
the radio keyed
- pagehide releases, so closing the tab cannot either
- /api/release-all clears PTT too, since it is outside the matrix and an empty
press does not touch it
- non-boolean bodies are rejected, so {"held": "false"} cannot key the transmitter
by truthiness
Two things the test suite caught, both real:
The browser wired '.key' handlers over every styled button, and the PTT button
carries no data-key, so it would have sent the key "undefined". Narrowed to
'.key[data-key]'.
StubClient.presses() collected every qom-set regardless of property, so PTT's
booleans landed among the key names and presses()[-1] reported False after a
release-all. Now filtered by property, with a matching ptts() accessor.
tools/test_ptt.py covers the path end to end and asserts the release as well as the
press: a PTT that stuck would leave every later test running against a transmitting
radio.
|
||
|
|
e6cebed84e |
Report a receiver with a signal, so the S-meter reads
The firmware now draws a working meter: -53 dBm, +40 over S9, nine of thirteen segments, next to a MONI label and a running receive timer. The two numbers agree with each other -- S9 is -93 dBm on UHF, so -53 really is S9+40. Three pieces had to line up, and the order they were found in was the hard part. RSSI and audio amplitude are refreshed when the firmware polls REG_0C, not when it configures the chip. Raising a flag at configuration time is a trap: REG_3F is written 0 then 0x0C0C repeatedly during setup, so anything announced there is disabled again before it can be collected. The squelch flag is SQUELCH_LOST, bit 2 -- not SQUELCH_FOUND. Per app/app.c:1027 "squelch lost" is what sets g_SquelchLost = true, i.e. a signal is present. SQUELCH_FOUND reads like "found a signal" and means the opposite. Announcing is rate-limited to every 64th poll. Announcing once means the firmware collects it during startup, before the flag leads anywhere. Announcing on every poll re-arms the request bit inside the firmware's own collection loop, which uses REG_0C as its condition and has no timeout, so it never exits. Periodic satisfies both. What finally made the meter appear was not the interrupt at all. The radio idles in power save and does not act on squelch there. ACTION_Monitor skips squelch entirely -- app/app.c:482 picks FUNCTION_MONITOR over FUNCTION_RECEIVE when gMonitor is set -- and settings.c:263 defaults an out-of-range stored action to ACTION_OPT_MONITOR, which blank flash (0xFF) is. So SIDE1 short-press is the way in. Measured: fn=5 idle=1 monitor=0 before, fn=2 idle=0 monitor=1 after. Gating on RX_DSP (REG_30 bit 0, from App/driver/bk4819-regs.h:240) rather than the whole register being zero: TX and tone paths leave other bits set with RX_DSP clear, and would otherwise look like a live receiver. tools/test_smeter.py covers the whole path -- boots pristine, confirms power save, presses SIDE1, and checks the screen gained content. It compares lit-pixel counts rather than matching pixels, so an unrelated UI change does not produce a mysterious failure. None of this is radio simulation. The levels are plausible numbers that move; they are not the result of modelling a signal. What they buy is firmware control flow running on live values instead of on zero. |
||
|
|
ad88ee1519 |
Fix BK4819 register reads arriving shifted one bit left
Every read came back doubled: seed REG_0C with 0x1248 and the firmware received
0x2490. The command byte's own trailing falling edge was being treated as a data
clock, so bit 15 was shifted away before the guest sampled it and the whole word
landed one place too high.
Each firmware bit is read/raise/lower (BK4819_ReadU16), which means the eighth
command bit is followed by a falling edge before the data phase begins. Skip that
one edge.
Why it went unnoticed: writes were always fine -- 52 registers held exactly what
the firmware wrote -- and the register the firmware polls hardest, REG_0C, was
legitimately 0 in this model. Reading zero and getting zero looks like success.
The skew only surfaced when something tried to report a value through it.
It also explains four failed attempts at the squelch interrupt. The model raised
REG_0C bit 0; the firmware received bit 1. So
while (BK4819_ReadRegister(BK4819_REG_0C) & 1u)
was never true, the acknowledging write inside it never ran, and 1719 polls saw a
flag the guest could not act on. Every one of those attempts was diagnosed as a
timing or gating problem and was not.
tools/test_bk4819_readback.sh locks it down. It seeds REG_0C -- read ~1700 times
per 30s, so a sample is guaranteed -- with a value carrying bits in both halves,
so a shift either way is unmistakable, and names the direction on failure. Bit 0
is left clear on purpose: with it set the firmware enters an acknowledge loop that
has no timeout, and this test is about alignment only.
Confirmed by A/B: committed code 0x2490, patched 0x1248.
|
||
|
|
4e29d11810 |
Update the docs now that the BK4819 is modelled
The "what this cannot do" section said the transceiver was not modelled and treated that as permanent. Half of it is now wrong: the register interface works. The other half is still true and worth keeping sharp -- analogue behaviour is out of reach because the chip has no public datasheet, so the driver is the only specification and it can only say which registers were written. Rewritten to separate the two: what is modelled and what it fixed (RSSI was hard zero at 18 call sites), the two constraints the untimed spin loops impose, and then the line that does not move. Explicitly warns against reading the new test as evidence about RF. Also corrects the GPIO comment about idling PB9 low. It described the pin as a workaround pending a device model; that model now exists, and the idle level only covers the window between reset and the bus being wired. |
||
|
|
76fc72d5ed |
Model the BK4819 register interface
The transceiver was not modelled at all. Its bit-banged three-wire bus went nowhere, so every register read returned whatever the floating GPIO happened to be, and PB9 had to be idled low as a workaround: with the line high, reads came back 0xFFFF and RADIO_SetupRegisters spun forever on bit 0 of REG_0C. Now a real device, wired to the pins the driver uses -- CS on PF9, SCL PB8, SDA PB9 with both directions connected -- decoding the protocol from App/driver/bk4819.c: CS low, eight bits of register number MSB first with bit 7 set for a read, then sixteen bits of data. Registers read back what the firmware wrote; the few it reads without having written return plausible values. Scope, deliberately narrow: this is the register interface, not the radio. The chip has no public datasheet, so the driver is the only specification available and it can only say which registers were written, never what left the antenna. Keying envelopes, spurious emissions and sensitivity still need a real radio and a spectrum analyser. The comments say so at the top of the device, so a passing test here is not mistaken for evidence about RF. What it buys is control flow that evaluates real values. RSSI was hard zero at 18 call sites -- -160 dBm -- so the S-meter read empty and squelch and scan decisions saw a dead band. It now reports about -40 dBm. Measurably, the main screen comes up on 400 MHz instead of the 18 MHz floor, because band setup no longer reads zeros. Two things the untimed spins force: - REG_0C bit 0 must stay clear. App/app/app.c:910 and :1417 loop on it with no timeout whatsoever, so a stuck bit hangs the guest rather than degrading. - A soft reset (REG_00 bit 15, which BK4819_Init issues first) has to re-seed the measurement registers. Real hardware keeps measuring afterwards; this model would be left holding zeros. That was not theoretical -- the first test run decoded 48 registers correctly and still reported RSSI as 0 for exactly this reason. The register file is exposed over QOM as regNN so tests can inspect it without gdb. That matters beyond convenience: attaching a debugger pauses the guest and changes timing-sensitive behaviour, which has repeatedly produced conclusions that were artefacts of the measurement rather than facts about the firmware. tools/test_bk4819.py checks the guest still boots (i.e. the spin terminates), that dozens of registers hold written values (52 currently, so the transfer really is being decoded), that RSSI is not zero, and that REG_0C bit 0 is clear. keypad_test.py, test_flash_persist.py, test_freq_entry.py, test_serial_rx.py and the 143 unit tests all still pass. |
||
|
|
b84d229326 |
Implement serial receive, unlocking the UV-K5 programming protocol
Nothing could be sent to the firmware. Three separate pieces were missing.
USART1 was a register stub with no chardev, so there was no source of incoming
bytes. It now takes a chardev property, defaulting to serial0, so -serial works.
The DMA model never serviced USART. App/driver/uart.c receives over a circular
peripheral-to-memory channel and never reads DR; it finds new data with
write_ptr = sizeof(UART_DMA_Buffer) - LL_DMA_GetDataLength(DMA1, CHANNEL_2)
so leaving CNDTR at its programmed value made the buffer look permanently empty
however many bytes arrived. DMA now drains USART1's queue byte by byte, decrementing
CNDTR and reloading it in circular mode. Channels also remember the length they were
given, since CNDTR counts down and the offset into the buffer has to be derived from
the difference.
Servicing happens on a CNDTR read rather than from a timer: that read is precisely
how the driver looks for data, so no polling is needed and no byte can be delivered
before the guest asks.
And transmit was invisible to the far end. DR writes went to stderr only, so a host
tool would send a command, the firmware would answer, and the answer went nowhere the
tool could see -- indistinguishable from being ignored. This cost a debugging round:
the first test run reported "no reply at all" with 0 bytes of boot output, which
looked like receive failing when the boot banner was in fact being written to stderr
as always. DR now also writes the raw byte to the chardev when one is connected.
SR reports RXNE when bytes are queued and DR consumes one, so a polling firmware
would work too, even though this one uses DMA.
tools/test_serial_rx.py speaks the real wire protocol -- AB CD framing, the fixed XOR
obfuscation, CRC-16/XMODEM -- and checks two exchanges end to end:
0x0514 hello -> 0x0515 ack
0x051B EEPROM read -> 0x051C with the requested 8 bytes at 0x0E70
Both pass. CPS/CHIRP-style tools can now talk to the emulator. keypad_test.py,
test_flash_persist.py, test_freq_entry.py and the 143 unit tests still pass.
|
||
|
|
798905f154 |
Give DMA the CPU's address space, so a typed frequency sticks
DMA moved bytes through address_space_memory, which cannot decode this SoC's memory at all: the container region holding flash, SRAM and the peripherals is handed only to the ARMv7M core and never registered with global system memory. Reads came back MEMTX_DECODE_ERROR with all-zero data, and writes went nowhere. Proved directly -- an address_space_read of SRAM through it returns result=2 and 00000000, while the same address read through the container returns the real contents. This is what "the frequency will not change" and "flash behaves like RAM" had in common. PY25Q16_WriteBuffer reads a 4 KB sector into SectorCache, patches the part it wants, and programs the whole sector back. The read looked healthy from the flash side -- the model handed over real 0xFF bytes -- but DMA dropped them, so the write-back sourced 4096 zeros and cleared the sector, VFO frequencies at 0x9000 included. RADIO_ConfigureChannel only substitutes a band's lower limit for 0xFFFFFFFF, so a stored zero was used as-is and clamped to BX4819_band1_lower. That is where the 18.000 MHz came from, every time. DMA now runs over an AddressSpace built on the SoC container, and refuses to transfer at all if none is configured rather than silently moving zeros. tools/test_freq_entry.py covers the whole user-visible path: type 435000, confirm 435.00000 MHz lands in band 5, confirm no other band was zeroed, and confirm it is still there after a power cycle. It drives QMP with no debugger attached, because the input box times out in ~2.5 s and a gdb attach takes longer -- that alone invalidated several earlier investigations. Verified: 435 MHz now appears at flash 0x90A0 where before the entire sector read zero. keypad_test.py, test_flash_persist.py and the 141 unit tests all pass. |
||
|
|
f114666b42 |
Start DMA on the peripheral's request, and clock both directions together
Two related faults in the DMA model, both of which corrupted flash reads.
Transfers started when a channel was enabled. On hardware, enabling only arms a
channel; the transfer begins when the peripheral raises its DMA request. The flash
driver's SPI_ReadBuf arms RX, arms TX, then enables SPI and sets TXDMAEN -- so
firing at arm time clocked the bus before the read command had been sent. SPI now
kicks the armed channels from CR1/CR2 when SPE and a DMA request enable are both
set, which covers the read path (SPE last) and the write path alike.
Each channel also ran to completion independently. SPI is duplex: one clocked byte
is simultaneously sent and received, and the driver relies on that, pairing a
memory-to-peripheral channel feeding dummy bytes with a peripheral-to-memory
channel collecting the reply. Running them in sequence meant TX clocked the whole
transfer out before RX looked at the bus, so RX collected nothing. They are now
stepped together, one byte at a time.
Either fault alone made a 4 KB sector read return zeros. PY25Q16_WriteBuffer reads
a sector into SectorCache, patches it, and writes the whole thing back, so a zeroed
read turned into a zeroed sector -- including the per-band VFO frequencies at
0x9000. That is a second, independent cause of typed frequencies reverting to
18 MHz, on top of the missing page wrap fixed in
|
||
|
|
da1ad7e1e6 |
Wrap page-program writes within their 256-byte page
Real SPI NOR latches only the low address bits into its page buffer, so a program burst that runs past the page boundary continues at the start of the same page. The model incremented the address straight through instead. Consequence: the firmware issues a 512-byte burst at 0x008F00 inside a single CS assertion (measured -- the CS never drops mid-burst), which spilled into 0x009000. That is the per-band VFO frequency area in eeprom_compat.c's map, so stored frequencies were zeroed. RADIO_ConfigureChannel only substitutes the band's lower limit when it reads 0xFFFFFFFF, so a stored 0 was taken literally and clamped to BX4819_band1_lower -- which is why every typed frequency reverted to 18.000 MHz. Verified: writes now align to sectors (0x8000-0x9000 and 0xA000-0xB000) and an instrumented build records zero stores into 0x9000-0x90D6, where before it was overwritten on every boot. test_flash_persist.py had encoded the bug in its expectations: it watched 0x008100 and 0x00A100, which were only ever written *because* of the missing wrap. Those move to 0x008000/0x00A000, and a MUST_NOT_CHANGE guard on the frequency area now fails if a write spills there again. keypad_test.py still passes. |
||
|
|
23385d2eba |
Persist flash writes to the backing file
The SPI NOR model read its image at realize and never wrote back, so the "flash" was a g_malloc buffer: everything the firmware saved -- settings, edited frequencies, channel data -- vanished when the QEMU process exited. That is the "it behaves like RAM" the user reported, and the image on disk still had its original mtime and was byte-identical to what make_flash.py produces. Page-program and sector-erase now mark the image dirty, and it is written out when chip select is released. Flushing there rather than per byte means one file write per settings save instead of thousands, because the firmware's driver holds CS for a whole erase-and-program sequence. Written via a temporary file and rename: an interrupted flush must not leave a truncated image, since that file is the only copy of the radio's state. A short write keeps the previous image rather than replacing it with a partial one. Also flushes from an exit notifier. Deselect covers normal operation, but QMP quit -- which is what the web UI's power off sends -- can arrive with the chip still selected, and the last write would be dropped. tools/test_flash_persist.py covers it end to end on a copy of the image, so it cannot disturb a running session. It asserts specific regions rather than just a changed hash: flash 0x008100 (MR/VFO attributes) and 0x00A100 (settings), which are the two sectors a boot demonstrably writes. Both were 0xFF before and non-0xFF after, and sha256 moved from 933d6974 to 7cdff6ce. Three approaches were tried and abandoned first, all for the same reason: driving the guest from gdb. `call EEPROM_WriteBuffer` and `call SETTINGS_SaveSettings` both hang, because the main loop is running and the called function waits on hardware the debugger has frozen. Observing the file is simpler and closer to what the user actually sees. An earlier version of the test also watched EEPROM offsets instead of flash offsets and reported "same" for every region while persistence was in fact working -- the two address spaces are related by the table in App/driver/eeprom_compat.c, not equal. keypad_test.py still passes, which matters because this file is where deleting three fprintfs once silently removed the keypad. |
||
|
|
c6d58f5601 |
Surface firmware serial output instead of dropping it
The firmware has been printing all along and nothing was listening. USART1 has no
real model here -- it is one of the logging catch-all stubs -- so every byte went
into qemu_log_mask(LOG_UNIMP) and vanished.
Two things were needed, and the second was not in the plan:
1. Print USART1 DR writes (+0x04, per the vendor CMSIS header) as SERIAL lines.
2. Report TXE|TC in USART1 SR. This is the part I had missed. UART_Send() in
App/driver/uart.c spins on LL_USART_IsActiveFlag_TXE() with a bounded timeout
and *skips the byte* when the flag never sets. A stub returning 0 for SR meant
the firmware discarded its own output before it ever reached DR -- the only
write arriving was UART_Init()'s priming zero. So step 1 alone produced
nothing, which is why the first attempt looked like "the build has no logging".
Then a bug of my own: the priming byte is 0x00, I buffered it, and fprintf("%s")
stopped at that NUL and printed an empty line while all 46 bytes sat behind it.
NULs are now dropped, and a line flushes on CR as well as LF.
Verified: SERIAL UV-K5 Firmware, EGZUMER-F4HWN+NR7Y c91cec95
keypad_test.py still passes, which matters because this file is where removing
three fprintfs once silently deleted the keypad.
|
||
|
|
2154f80414 |
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.
|
||
|
|
1c9a2afe55 |
Fix key.py hold times; the keypad was never broken
key.py held every key for 2500 ms, on the assumption that guest time runs fast during delays so a press needs a long wall-clock hold. That is wrong for this path, and it is why the keypad looked dead. The two SysTick mechanisms are separate. poll-boost accelerates counter *reads* so SYSTICK_DelayUs converges; it does not speed up interrupt delivery. Interrupts drive SysTick_Handler -> gNextTimeslice -> APP_TimeSlice10ms -> CheckKeys at close to real time, so the firmware's thresholds hold in wall clock as written: 20 ms to register a press, 400 ms to count as held. 2500 ms is ~250 ticks, six times past the long-press threshold, so every press was dispatched as a hold. MAIN_Key_MENU acts only on a short release and returns early when bKeyHeld is set, so nothing happened. Confirmed by reading gDebounceCounter mid-hold: 317 after a 3 s hold, which also proves the timeslice was running all along. Now HOLD_MS=200 and LONG_HOLD_MS=900. Verified with screenshots: the menu opens and UP/DOWN move through it. Also here: - Drop the three TRACE fprintfs. They fired on every keypad poll and buried the console; the matrix is confirmed working. - Drop a redundant forward declaration of py32_spi_xfer_byte, silencing the only build warning. - Document the real remaining gap: power save (~6 s after boot) stops the keypad scan and the model does not wake from it. Includes the two dead ends already ruled out by experiment, so nobody repeats them. - Add README screenshots captured from guest memory. |
||
|
|
c0a09827ed |
UV-K5 V3 emulator: QEMU machine for the PY32F071
Adds a QEMU machine for the Puya PY32F071 (Cortex-M0+) so Quansheng UV-K5 V3 firmware can run on a PC. The firmware boots to its main loop in about five seconds and the LCD contents are readable. Register layouts come from the vendor CMSIS header shipped with the firmware rather than guesswork. Modelled: RCC, GPIO, ADC, both SPI controllers, DMA1 and the PY25Q16 flash; everything else answers through a logging catch-all, which is how the next thing worth modelling gets identified. Seven things had to be right before it would boot, each found by watching where the firmware stopped: flash aliased at the application offset, clock ready bits, self-clearing ADC calibration, SPI transfer flags, DMA-driven flash reads, SysTick poll acceleration, and the bit-banged transceiver bus idling low. SysTick needs explanation. SYSTICK_DelayUs polls the counter and accumulates differences; under emulation a register read costs far more relative to guest time, so a measured 120 ms delay would have taken about 7.7 hours. Lowering the clock does not help because the bottleneck is loop iterations, not counter speed. Reporting a value that runs ahead of the real counter does, via a new poll-boost property on SysTick. Guest time therefore runs fast during delays: fine for exercising menus and control flow, wrong for judging signal timing. Also includes the host build of the CW timing chain (harness, stubs, shim, tests), which compiles app/cwkeyer.c and app/cwmacro.c unmodified against stub drivers with a virtual clock and scripted paddle input. Known gap: keypad rows reach the firmware's scan and KEYBOARD_Poll returns the right key code, but the UI does not react yet. Not modelled, and not intended to be: radio behaviour. The transceiver chip has no public datasheet, so keying envelopes and emissions need real hardware. |