mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-05 04:27:30 +00:00
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.
This commit is contained in:
1 parent
f114666b42
commit
798905f154
2 files changed
+269
-16
No files matched your search
+54
-16
@@ -684,28 +684,30 @@ static void py32_spi_write(void *opaque, hwaddr addr, uint64_t value, unsigned s
|
||||
PY32SpiState *s = opaque;
|
||||
|
||||
switch (addr) {
|
||||
/*
|
||||
* A DMA-driven transfer starts only once SPE and TXDMAEN are both set.
|
||||
*
|
||||
* TXDMAEN specifically, not "either direction": TX is what clocks the bus, so
|
||||
* it is the gate. Both driver paths set it last:
|
||||
*
|
||||
* arm RX, arm TX, RXDMAEN, SPE, TXDMAEN
|
||||
*
|
||||
* Starting at SPE, when only RXDMAEN was set, ran the whole transfer while the
|
||||
* TX channel was armed but not yet requesting. On the sector write-back that
|
||||
* meant sending 4096 bytes read from BlackHole (0x200003D4, four zero bytes,
|
||||
* no address increment) instead of SectorCache (0x200003D8), so the sector was
|
||||
* programmed with zeros -- wiping the per-band VFO frequencies at 0x9000 and
|
||||
* with them any frequency the user typed.
|
||||
*/
|
||||
case SPI_CR1:
|
||||
s->cr1 = value;
|
||||
/*
|
||||
* SPE can be the last thing enabled. The flash driver's read path arms both
|
||||
* channels, sets RXDMAEN, enables SPI, then sets TXDMAEN -- but the write
|
||||
* path enables SPI last, so both orders have to work.
|
||||
*/
|
||||
if ((value & SPI_CR1_SPE) && (s->cr2 & (SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN))
|
||||
&& s->dma_kick) {
|
||||
if ((value & SPI_CR1_SPE) && (s->cr2 & SPI_CR2_TXDMAEN) && s->dma_kick) {
|
||||
s->dma_kick(s->dma, s);
|
||||
}
|
||||
break;
|
||||
case SPI_CR2:
|
||||
s->cr2 = value;
|
||||
/*
|
||||
* A DMA request is what actually starts the transfer on hardware. Kick the
|
||||
* armed channels here rather than when they were enabled: at arm time the
|
||||
* read command has not been clocked out yet, so the reply would be read
|
||||
* before the device had anything to say.
|
||||
*/
|
||||
if ((value & (SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN))
|
||||
&& (s->cr1 & SPI_CR1_SPE) && s->dma_kick) {
|
||||
if ((value & SPI_CR2_TXDMAEN) && (s->cr1 & SPI_CR1_SPE) && s->dma_kick) {
|
||||
s->dma_kick(s->dma, s);
|
||||
}
|
||||
break;
|
||||
@@ -833,6 +835,25 @@ struct PY32DmaState {
|
||||
|
||||
/* Set by the SoC: lets the DMA clock bytes through an SPI controller. */
|
||||
PY32SpiState *spi[2];
|
||||
|
||||
/*
|
||||
* The address space DMA transfers move bytes through.
|
||||
*
|
||||
* Must be the CPU's, not address_space_memory. This SoC builds its own
|
||||
* container region and hands that to the ARMv7M core, and never registers it
|
||||
* with the global system memory, so address_space_memory cannot decode SRAM at
|
||||
* all: reads returned MEMTX_DECODE_ERROR with all-zero data and writes went
|
||||
* nowhere.
|
||||
*
|
||||
* That single mistake accounted for every "flash forgets things" symptom.
|
||||
* PY25Q16_WriteBuffer reads a 4 KB sector into SectorCache, patches it, and
|
||||
* programs the whole sector back. The read appeared to work -- the model
|
||||
* returned real 0xFF bytes -- but DMA dropped them on the floor, so the
|
||||
* write-back sourced 4096 zeros and cleared the sector, VFO frequencies at
|
||||
* 0x9000 included. Hence a typed frequency reverting to 18 MHz, which is
|
||||
* simply BX4819_band1_lower after RADIO_ConfigureChannel read a zero.
|
||||
*/
|
||||
AddressSpace *as;
|
||||
};
|
||||
|
||||
static void py32_dma_update_irq(PY32DmaState *s)
|
||||
@@ -882,9 +903,15 @@ static PY32SpiState *py32_dma_spi_for(PY32DmaState *s, uint32_t paddr)
|
||||
*/
|
||||
static void py32_dma_run_for_spi(PY32DmaState *s, PY32SpiState *spi)
|
||||
{
|
||||
AddressSpace *as = &address_space_memory;
|
||||
AddressSpace *as = s->as;
|
||||
int tx = -1, rx = -1;
|
||||
|
||||
if (!as) {
|
||||
/* Fail loudly rather than silently transferring zeros. */
|
||||
qemu_log_mask(LOG_GUEST_ERROR, "py32-dma: no address space configured\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int ch = 0; ch < PY32_DMA_CHANNELS; ch++) {
|
||||
PY32DmaChannel *c = &s->ch[ch];
|
||||
if (!(c->ccr & DMA_CCR_EN) || c->cndtr == 0) {
|
||||
@@ -1596,6 +1623,8 @@ struct PY32F071State {
|
||||
MemoryRegion sram;
|
||||
MemoryRegion *board_memory;
|
||||
MemoryRegion container;
|
||||
/* An address space over `container`, so DMA sees the same map as the CPU. */
|
||||
AddressSpace dma_as;
|
||||
};
|
||||
|
||||
/* Peripherals covered by the catch-all, in map order. */
|
||||
@@ -1736,6 +1765,15 @@ static void py32f071_soc_realize(DeviceState *dev_soc, Error **errp)
|
||||
s->spi[i].dma = &s->dma;
|
||||
s->spi[i].dma_kick = py32_dma_kick;
|
||||
}
|
||||
|
||||
/*
|
||||
* DMA must move bytes through the CPU's address space. The container above is
|
||||
* this SoC's whole memory map and is given only to the core, so the global
|
||||
* address_space_memory cannot see SRAM -- reads through it fail with
|
||||
* MEMTX_DECODE_ERROR and yield zeros.
|
||||
*/
|
||||
s->dma.as = &s->dma_as;
|
||||
address_space_init(&s->dma_as, &s->container, "py32f071-dma");
|
||||
if (!sysbus_realize(SYS_BUS_DEVICE(&s->dma), errp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A typed frequency must take effect and survive a power cycle.
|
||||
|
||||
This is the user-visible bug that took four separate model faults to explain:
|
||||
|
||||
1. Page-program did not wrap within its 256-byte page, so a 512-byte burst at
|
||||
0x008F00 spilled into the VFO frequency area at 0x009000.
|
||||
2. DMA transfers started when a channel was enabled rather than when the
|
||||
peripheral requested one, so a read ran before the command had been clocked.
|
||||
3. Each DMA channel ran to completion independently, so on a duplex transfer the
|
||||
TX side finished before RX ever sampled the bus.
|
||||
4. DMA used address_space_memory, which cannot decode this SoC's SRAM at all --
|
||||
the container region is handed only to the CPU. Reads returned
|
||||
MEMTX_DECODE_ERROR with zeros and writes went nowhere.
|
||||
|
||||
Any one of them zeroed the sector that holds per-band frequencies, and
|
||||
RADIO_ConfigureChannel substitutes the band's lower limit only for 0xFFFFFFFF, so a
|
||||
stored zero was taken literally and clamped to BX4819_band1_lower -- 18 MHz. Hence
|
||||
"the frequency will not change" and "it forgets after power off" were one bug.
|
||||
|
||||
Drives the emulator over QMP with no debugger attached: the frequency input box
|
||||
times out in about 2.5 s and a gdb attach takes longer, which silently clears the
|
||||
box and invalidates the run.
|
||||
"""
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
QEMU = os.path.expanduser("~/qemu-build/qemu-7.2+dfsg/build/qemu-system-arm")
|
||||
ELF = os.path.expanduser("~/uvk5-port/uvk5-sat/build/CW/nr7y.cw.elf")
|
||||
PRISTINE = os.path.join(ROOT, "assets", "pristine", "flash-pristine.img.gz")
|
||||
|
||||
BOOT_SECONDS = 20
|
||||
|
||||
# Flash layout, from App/driver/eeprom_compat.c: 14 VFO slots of 16 bytes at 0x9000,
|
||||
# two slots per band. The frequency is the first word, in units of 10 Hz.
|
||||
VFO_BASE = 0x9000
|
||||
BAND_STRIDE = 32
|
||||
WANT_MHZ = 435.0
|
||||
WANT_RAW = 43_500_000
|
||||
WANT_BAND = 5 # 400-470 MHz contains 435
|
||||
|
||||
|
||||
class Qmp:
|
||||
def __init__(self, path):
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(30)
|
||||
self.sock.connect(path)
|
||||
self.buf = b""
|
||||
self._read() # greeting
|
||||
self.cmd("qmp_capabilities")
|
||||
|
||||
def _read(self):
|
||||
while b"\n" not in self.buf:
|
||||
chunk = self.sock.recv(65536)
|
||||
if not chunk:
|
||||
raise RuntimeError("QMP closed")
|
||||
self.buf += chunk
|
||||
line, self.buf = self.buf.split(b"\n", 1)
|
||||
return json.loads(line)
|
||||
|
||||
def cmd(self, name, **args):
|
||||
msg = {"execute": name}
|
||||
if args:
|
||||
msg["arguments"] = args
|
||||
self.sock.sendall(json.dumps(msg).encode() + b"\n")
|
||||
while True:
|
||||
reply = self._read()
|
||||
if "return" in reply or "error" in reply:
|
||||
return reply
|
||||
|
||||
def press(self, key, hold=0.08):
|
||||
self.cmd("qom-set", path="/machine/keypad", property="press", value=key)
|
||||
time.sleep(hold)
|
||||
self.cmd("qom-set", path="/machine/keypad", property="press", value="")
|
||||
time.sleep(0.12)
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def boot(image, sock_path):
|
||||
if os.path.exists(sock_path):
|
||||
os.unlink(sock_path)
|
||||
proc = subprocess.Popen(
|
||||
[QEMU, "-M", f"uv-k5-v3,flash-image={image}", "-nographic",
|
||||
"-monitor", "none", "-qmp", f"unix:{sock_path},server=on,wait=off",
|
||||
"-kernel", ELF],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
for _ in range(300):
|
||||
if os.path.exists(sock_path):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
proc.kill()
|
||||
raise RuntimeError("QMP socket never appeared")
|
||||
time.sleep(BOOT_SECONDS)
|
||||
return proc
|
||||
|
||||
|
||||
def shutdown(proc, qmp):
|
||||
try:
|
||||
qmp.cmd("quit")
|
||||
except Exception:
|
||||
pass
|
||||
qmp.close()
|
||||
try:
|
||||
proc.wait(timeout=20)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
|
||||
def stored_frequency(image, band, vfo=0):
|
||||
with open(image, "rb") as fh:
|
||||
data = fh.read()
|
||||
off = VFO_BASE + band * BAND_STRIDE + vfo * 16
|
||||
return int.from_bytes(data[off:off + 4], "little")
|
||||
|
||||
|
||||
def main():
|
||||
for path, what in ((QEMU, "QEMU"), (ELF, "firmware"), (PRISTINE, "pristine image")):
|
||||
if not os.path.exists(path):
|
||||
sys.exit(f"missing {what}: {path}")
|
||||
|
||||
workdir = tempfile.mkdtemp(prefix="uvk5-freq-")
|
||||
image = os.path.join(workdir, "flash.img")
|
||||
sock_path = os.path.join(workdir, "qmp.sock")
|
||||
with gzip.open(PRISTINE, "rb") as src, open(image, "wb") as dst:
|
||||
shutil.copyfileobj(src, dst)
|
||||
|
||||
failures = []
|
||||
try:
|
||||
proc = boot(image, sock_path)
|
||||
qmp = Qmp(sock_path)
|
||||
|
||||
qmp.press("EXIT")
|
||||
time.sleep(0.8)
|
||||
|
||||
# F+1 leaves memory mode. It only does that while the VFO is on a memory
|
||||
# channel; in frequency mode the same shortcut cycles the band instead.
|
||||
qmp.press("F")
|
||||
time.sleep(0.5)
|
||||
qmp.press("1")
|
||||
time.sleep(1.5)
|
||||
|
||||
# Six digits back to back. The gap between them must stay well under
|
||||
# key_input_timeout_500ms / 3, roughly 2.5 s, or the box clears.
|
||||
start = time.time()
|
||||
for digit in "435000":
|
||||
qmp.press(digit)
|
||||
elapsed = time.time() - start
|
||||
print(f"typed 435000 in {elapsed:.2f}s")
|
||||
if elapsed > 2.0:
|
||||
failures.append(f"digits took {elapsed:.2f}s, close to the input timeout")
|
||||
|
||||
time.sleep(5) # let the save reach flash
|
||||
shutdown(proc, qmp)
|
||||
|
||||
raw = stored_frequency(image, WANT_BAND)
|
||||
print(f"stored in band{WANT_BAND}: "
|
||||
f"{'blank' if raw == 0xFFFFFFFF else f'{raw / 100000:.5f} MHz'}")
|
||||
if raw != WANT_RAW:
|
||||
failures.append(
|
||||
f"band{WANT_BAND} holds {raw:#x}, expected {WANT_RAW:#x} "
|
||||
f"({WANT_MHZ} MHz)")
|
||||
else:
|
||||
print(f"PASS {WANT_MHZ} MHz was stored")
|
||||
|
||||
# Other bands must be untouched, which is what a spilling write breaks.
|
||||
clobbered = [b for b in range(7)
|
||||
if b != WANT_BAND and stored_frequency(image, b) == 0]
|
||||
if clobbered:
|
||||
failures.append(
|
||||
f"bands {clobbered} were zeroed -- a write spilled across sectors")
|
||||
else:
|
||||
print("PASS no other band was zeroed")
|
||||
|
||||
# And it has to still be there after a power cycle.
|
||||
proc = boot(image, sock_path)
|
||||
qmp = Qmp(sock_path)
|
||||
time.sleep(2)
|
||||
shutdown(proc, qmp)
|
||||
|
||||
raw_after = stored_frequency(image, WANT_BAND)
|
||||
if raw_after != WANT_RAW:
|
||||
failures.append(
|
||||
f"after a power cycle band{WANT_BAND} holds {raw_after:#x}, "
|
||||
f"expected {WANT_RAW:#x}")
|
||||
else:
|
||||
print(f"PASS {WANT_MHZ} MHz survived a power cycle")
|
||||
|
||||
finally:
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
|
||||
if failures:
|
||||
print()
|
||||
for f in failures:
|
||||
print(f"FAIL {f}")
|
||||
sys.exit(1)
|
||||
print("\na typed frequency takes effect and persists")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user