On Wed, Sep 09, 2026 at 03:23:55PM +0800, Zephyr Li wrote:
> Since commit cfc96df65e01, riscv_pmu_ctr_get_fixed_counters_val()
> returns the complete 64-bit fixed-counter value. The RV32 counter
> access paths, however, still perform parts of the offset calculation
> on separately extracted 32-bit halves.
>
> In particular, riscv_pmu_write_ctrh() deposits the low 32 bits of the
> complete fixed-counter value into the high half of mhpmcounter_prev.
> riscv_pmu_read_ctr() also subtracts a 32-bit half of the previous value
> from the complete 64-bit fixed-counter value. Consequently, writes to
> mcycleh can be lost and carries between the low and high halves are not
> handled correctly.
>
> Keep the fixed-counter offset calculation entirely in 64 bits. Before
> a running counter is partially written, materialize its current
> architectural value and reset the fixed-counter baseline. On reads,
> calculate the complete 64-bit counter value before extracting the half
> requested by RV32.
>
> Register RV32 system TCG tests with the Meson build. Add a test for
> high-half writes, low-to-high carry, and preserving the carried high
> half across a subsequent low-half write.
>
> Fixes: cfc96df65e01 ("target/riscv: Remove upper_half from
> riscv_pmu_ctr_get_fixed_counters_val")
> Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/4219
> Signed-off-by: Zephyr Li <[email protected]>
Reviewed-by: Chao Liu <[email protected]>
Thanks,
Chao
>
> ---
> Changes in v2:
> - Move the test to a new tests/tcg/riscv32 directory.
> - Convert the test registration to the Meson TCG test framework.
> - Add RV32 TCG cross-compiler options and cover the test directory in
> MAINTAINERS.
> - Add a riscv32-local semihosting linker script.
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 7183babd6a..5f75b2684f 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -377,6 +377,7 @@ F: include/hw/riscv/
> F: common-user/host/riscv*
> F: tests/functional/riscv32
> F: tests/functional/riscv64
> +F: tests/tcg/riscv32/
> F: tests/tcg/riscv64/
> F: tests/qtest/iommu-riscv-test.c
>
> diff --git a/meson_options.txt b/meson_options.txt
> index 292625af08..2ceebe7a44 100644
> --- a/meson_options.txt
> +++ b/meson_options.txt
> @@ -452,6 +452,10 @@ option('tcg_tests_cross_cc_ppc64le', type: 'string',
> description: 'cc for ppc64le tcg tests')
> option('tcg_tests_cross_cflags_ppc64le', type: 'string',
> description: 'cflags for ppc64le tcg tests')
> +option('tcg_tests_cross_cc_riscv32', type: 'string',
> + description: 'cc for riscv32 tcg tests')
> +option('tcg_tests_cross_cflags_riscv32', type: 'string',
> + description: 'cflags for riscv32 tcg tests')
> option('tcg_tests_cross_cc_riscv64', type: 'string',
> description: 'cc for riscv64 tcg tests')
> option('tcg_tests_cross_cflags_riscv64', type: 'string',
> diff --git a/target/riscv/tcg/csr.c b/target/riscv/tcg/csr.c
> index bd4b6dc114..65985efb22 100644
> --- a/target/riscv/tcg/csr.c
> +++ b/target/riscv/tcg/csr.c
> @@ -1337,23 +1337,23 @@ static RISCVException
> riscv_pmu_write_ctr(CPURISCVState *env, target_ulong val,
> int deposit_size = rv32 ? 32 : 64;
> uint64_t ctr;
>
> - counter->mhpmcounter_val = deposit64(counter->mhpmcounter_val,
> - 0, deposit_size, val);
> -
> if (!get_field(env->mcountinhibit, BIT(ctr_idx)) &&
> (riscv_pmu_ctr_monitor_cycles(env, ctr_idx) ||
> riscv_pmu_ctr_monitor_instructions(env, ctr_idx))) {
> ctr = riscv_pmu_ctr_get_fixed_counters_val(env, ctr_idx);
> - counter->mhpmcounter_prev = deposit64(counter->mhpmcounter_prev,
> - 0, deposit_size, ctr);
> + counter->mhpmcounter_val += ctr - counter->mhpmcounter_prev;
> + counter->mhpmcounter_val = deposit64(counter->mhpmcounter_val,
> + 0, deposit_size, val);
> + counter->mhpmcounter_prev = ctr;
> if (ctr_idx > 2) {
> riscv_pmu_setup_timer(env, counter->mhpmcounter_val, ctr_idx);
> }
> } else {
> + counter->mhpmcounter_val = deposit64(counter->mhpmcounter_val,
> + 0, deposit_size, val);
> /* Other counters can keep incrementing from the given value */
> counter->mhpmcounter_prev = deposit64(counter->mhpmcounter_prev,
> 0, deposit_size, val);
> -
> }
>
> return RISCV_EXCP_NONE;
> @@ -1363,20 +1363,22 @@ static RISCVException
> riscv_pmu_write_ctrh(CPURISCVState *env, target_ulong val,
> uint32_t ctr_idx)
> {
> PMUCTRState *counter = &env->pmu_ctrs[ctr_idx];
> - uint64_t ctrh;
> + uint64_t ctr;
>
> - counter->mhpmcounter_val = deposit64(counter->mhpmcounter_val,
> - 32, 32, val);
> if (!get_field(env->mcountinhibit, BIT(ctr_idx)) &&
> (riscv_pmu_ctr_monitor_cycles(env, ctr_idx) ||
> riscv_pmu_ctr_monitor_instructions(env, ctr_idx))) {
> - ctrh = riscv_pmu_ctr_get_fixed_counters_val(env, ctr_idx);
> - counter->mhpmcounter_prev = deposit64(counter->mhpmcounter_prev,
> - 32, 32, ctrh);
> + ctr = riscv_pmu_ctr_get_fixed_counters_val(env, ctr_idx);
> + counter->mhpmcounter_val += ctr - counter->mhpmcounter_prev;
> + counter->mhpmcounter_val = deposit64(counter->mhpmcounter_val,
> + 32, 32, val);
> + counter->mhpmcounter_prev = ctr;
> if (ctr_idx > 2) {
> riscv_pmu_setup_timer(env, counter->mhpmcounter_val, ctr_idx);
> }
> } else {
> + counter->mhpmcounter_val = deposit64(counter->mhpmcounter_val,
> + 32, 32, val);
> counter->mhpmcounter_prev = deposit64(counter->mhpmcounter_prev,
> 32, 32, val);
> }
> @@ -1407,12 +1409,11 @@ RISCVException riscv_pmu_read_ctr(CPURISCVState *env,
> target_ulong *val,
> bool rv32 = riscv_cpu_mxl(env) == MXL_RV32;
> int start = upper_half ? 32 : 0;
> int length = rv32 ? 32 : 64;
> - uint64_t ctr_prev, ctr_val;
> + uint64_t ctr_val;
>
> /* Ensure upper_half is only set for XLEN == 32 */
> g_assert(rv32 || !upper_half);
>
> - ctr_prev = extract64(counter->mhpmcounter_prev, start, length);
> ctr_val = extract64(counter->mhpmcounter_val, start, length);
>
> if (get_field(env->mcountinhibit, BIT(ctr_idx))) {
> @@ -1431,7 +1432,8 @@ RISCVException riscv_pmu_read_ctr(CPURISCVState *env,
> target_ulong *val,
> if (riscv_pmu_ctr_monitor_cycles(env, ctr_idx) ||
> riscv_pmu_ctr_monitor_instructions(env, ctr_idx)) {
> uint64_t cntr = riscv_pmu_ctr_get_fixed_counters_val(env, ctr_idx) -
> - ctr_prev +
> ctr_val;
> + counter->mhpmcounter_prev +
> + counter->mhpmcounter_val;
> *val = extract64(cntr, start, length);
> } else {
> *val = ctr_val;
> diff --git a/tests/tcg/meson.build b/tests/tcg/meson.build
> index d41a228fb3..60b5ce2529 100644
> --- a/tests/tcg/meson.build
> +++ b/tests/tcg/meson.build
> @@ -169,6 +169,7 @@ subdir('mips64el')
> subdir('or1k')
> subdir('ppc64')
> subdir('ppc64le')
> +subdir('riscv32')
> subdir('riscv64')
> subdir('s390x')
> subdir('sh4')
> diff --git a/tests/tcg/riscv32/meson.build b/tests/tcg/riscv32/meson.build
> new file mode 100644
> index 0000000000..c08dfd772d
> --- /dev/null
> +++ b/tests/tcg/riscv32/meson.build
> @@ -0,0 +1,7 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +
> +cc = 'riscv64-linux-gnu-gcc'
> +cc_dockerfile = 'debian-all-test-cross'
> +cc_docker_host_arch = ['aarch64', 'x86_64']
> +
> +subdir('system')
> diff --git a/tests/tcg/riscv32/semihost.ld b/tests/tcg/riscv32/semihost.ld
> new file mode 100644
> index 0000000000..874838a865
> --- /dev/null
> +++ b/tests/tcg/riscv32/semihost.ld
> @@ -0,0 +1,23 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> +ENTRY(_start)
> +
> +SECTIONS
> +{
> + /* virt machine, RAM starts at 2gb */
> + . = 0x80000000;
> + .text : {
> + *(.text)
> + }
> + .rodata : {
> + *(.rodata)
> + }
> + /* align r/w section to next 2mb */
> + . = ALIGN(1 << 21);
> + .data : {
> + *(.data)
> + }
> + .bss : {
> + *(.bss)
> + }
> +}
> diff --git a/tests/tcg/riscv32/system/meson.build
> b/tests/tcg/riscv32/system/meson.build
> new file mode 100644
> index 0000000000..16f9a06c94
> --- /dev/null
> +++ b/tests/tcg/riscv32/system/meson.build
> @@ -0,0 +1,37 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +
> +tests = []
> +
> +link_script = files('../semihost.ld')[0]
> +cflags = ['-march=rv32im_zicsr',
> + '-mabi=ilp32',
> + '-nostdlib',
> + '-ffreestanding',
> + '-Wa,--noexecstack',
> + '-Wl,-T', link_script]
> +qemu_args = ['-M', 'virt',
> + '-display', 'none',
> + '-serial', 'stdio',
> + '-semihosting',
> + '-bios']
> +
> +tests += {
> + 'test-mcycle.S': {
> + 'cflags': cflags,
> + 'qemu_args': ['-icount', 'shift=1', qemu_args],
> + },
> +}
> +
> +if 'qemu-system-riscv32' in emulators
> + tcg_tests += {
> + 'riscv32-softmmu': {
> + 'cc': cc,
> + 'cc_dockerfile': cc_dockerfile,
> + 'cc_docker_host_arch': cc_docker_host_arch,
> + 'folder': 'riscv32',
> + 'gdb_arch': 'riscv32',
> + 'qemu': emulators['qemu-system-riscv32'],
> + 'tests': tests,
> + }
> + }
> +endif
> diff --git a/tests/tcg/riscv32/test-mcycle.S b/tests/tcg/riscv32/test-mcycle.S
> new file mode 100644
> index 0000000000..189d1e13a2
> --- /dev/null
> +++ b/tests/tcg/riscv32/test-mcycle.S
> @@ -0,0 +1,45 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> + .option norvc
> +
> + .text
> + .global _start
> +_start:
> + /* Exercise writes while mcycle is running. */
> + csrw mcountinhibit, zero
> + csrw mcycle, zero
> +
> + /* A write to the high half must be immediately observable. */
> + li s0, 0x1234ffff
> + csrw mcycleh, s0
> + csrr t0, mcycleh
> + bne t0, s0, fail
> +
> + /* Check carry from the low half into the high half. */
> + li s0, 0x12345678
> + csrw mcycleh, s0
> + li t0, 0xfffffff0
> + csrw mcycle, t0
> + .rept 32
> + nop
> + .endr
> + csrr t0, mcycleh
> + addi s0, s0, 1
> + bne t0, s0, fail
> +
> + /* A low-half write must preserve the carried high half. */
> + li t0, 0x22222222
> + csrw mcycle, t0
> + csrr t0, mcycleh
> + bne t0, s0, fail
> +
> + li t0, 0x100000
> + li t1, 0x5555 /* FINISHER_PASS */
> + sw t1, 0(t0)
> + j .
> +
> +fail:
> + li t0, 0x100000
> + li t1, 0x13333 /* status = FINISHER_FAIL, code = 1 */
> + sw t1, 0(t0)
> + j .
> --
> 2.43.0