Commit 5a2fa06b09 for qemu.org
commit 5a2fa06b0957adad46ba1abe923bca04aad9a4d2
Author: Tao Ding <dingtao0430@163.com>
Date: Tue Mar 24 14:02:29 2026 +0000
hw/dma/pl080: Fix transfer logic in PL080
The logic in the PL080 for transferring data has multiple bugs:
* The TransferSize field in the channel control register counts
in units of the source width; because our loop may do multiple
source loads if the destination width is greater than the
source width, we need to decrement it by (xsize / swidth),
not by 1, each loop
* It is documented in the TRM that it is a software error to program
the source and destination width such that SWidth < DWidth and
TransferSize * SWidth is not a multiple of DWidth. (This would
mean that there isn't enough data to do a full final destination
write.) We weren't doing anything sensible with this case.
The TRM doesn't document what the hardware actually does (though
it drops some hints that suggest that it probably over-reads
from the source).
* In the loop to write to the destination, each loop adds swidth
to ch->dest for each loop and also uses (ch->dest + n) as the
destination address. This moves the destination address on
further than we should each time round the loop, and also
is incrementing ch->dest by swidth when it should be dwidth.
This patch fixes these problems:
* decrement TransferSize by the correct amount
* log and ignore the transfer size mismatch case
* correct the loop logic for the destination writes
A repro case which exercises some of this is as follows. It
configures swidth to 1 byte, dwidth to 4 bytes, and transfer size 4,
for a transfer from 0x00000000 to 0x000010000. Examining the
destination memory in the QEMU monitor should show that the
source data 0x44332211 has all been copied, but before this
fix it is not:
./qemu-system-arm -M versatilepb -m 128M -nographic -S \
-device loader,addr=0x00000000,data=0x44332211,data-len=4 \
-device loader,addr=0x00001000,data=0x00000000,data-len=4 \
-device loader,addr=0x10130030,data=0x00000001,data-len=4 \
-device loader,addr=0x10130100,data=0x00000000,data-len=4 \
-device loader,addr=0x10130104,data=0x00001000,data-len=4 \
-device loader,addr=0x10130108,data=0x00000000,data-len=4 \
-device loader,addr=0x1013010C,data=0x9e47f004,data-len=4 \
-device loader,addr=0x10130110,data=0x0000c001,data-len=4
Without this patch the QEMU monitor shows:
(qemu) xp /1wx 0x00001000
00001000: 0x00002211
Correct result:
(qemu) xp /1wx 0x00001000
00001000: 0x44332211
Cc: qemu-stable@nongnu.org
Suggested-by: Peter Maydell <peter.maydell@linaro.org>
Signed-off-by: Tao Ding <dingtao0430@163.com>
[PMM: Wrote up what we are fixing in the commit message]
Reviewed-by: Peter Maydell <peter.maydell@linaro.org>
Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
diff --git a/hw/dma/pl080.c b/hw/dma/pl080.c
index 627ccbbd81..4a90c7bb27 100644
--- a/hw/dma/pl080.c
+++ b/hw/dma/pl080.c
@@ -179,23 +179,28 @@ again:
c, extract32(ch->ctrl, 21, 3));
continue;
}
-
- for (n = 0; n < dwidth; n+= swidth) {
+ if ((size * swidth) % dwidth) {
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "pl080: channel %d: transfer size mismatch: size=%d swidth=%d dwidth=%d\n",
+ c, size, swidth, dwidth);
+ continue;
+ }
+ xsize = MAX(swidth, dwidth);
+ for (n = 0; n < xsize; n += swidth) {
address_space_read(&s->downstream_as, ch->src,
MEMTXATTRS_UNSPECIFIED, buff + n, swidth);
if (ch->ctrl & PL080_CCTRL_SI)
ch->src += swidth;
}
- xsize = (dwidth < swidth) ? swidth : dwidth;
/* ??? This may pad the value incorrectly for dwidth < 32. */
for (n = 0; n < xsize; n += dwidth) {
- address_space_write(&s->downstream_as, ch->dest + n,
+ address_space_write(&s->downstream_as, ch->dest,
MEMTXATTRS_UNSPECIFIED, buff + n, dwidth);
if (ch->ctrl & PL080_CCTRL_DI)
- ch->dest += swidth;
+ ch->dest += dwidth;
}
- size--;
+ size -= xsize / swidth;
ch->ctrl = (ch->ctrl & 0xfffff000) | size;
if (size == 0) {
/* Transfer complete. */