[PATCH v2] target/riscv: fix RV32 fixed counter accesses

Zephyr Li posted 1 patch 2 weeks, 3 days ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/qemu tags/patchew/20260909072356.42784-1-fritchleybohrer@gmail.com
Maintainers: Paolo Bonzini <pbonzini@redhat.com>, "Marc-André Lureau" <marcandre.lureau@redhat.com>, "Daniel P. Berrangé" <berrange@redhat.com>, Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com>, Palmer Dabbelt <palmer@dabbelt.com>, Alistair Francis <alistair.francis@wdc.com>, Weiwei Li <liwei1518@gmail.com>, Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>, Liu Zhiwei <zhiwei_liu@linux.alibaba.com>, Chao Liu <chao.liu@processmission.com>, "Alex Bennée" <alex.bennee@linaro.org>
[PATCH v2] target/riscv: fix RV32 fixed counter accesses
Posted by Zephyr Li 2 weeks, 3 days ago
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 <fritchleybohrer@gmail.com>

---
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
Re: [PATCH v2] target/riscv: fix RV32 fixed counter accesses
Posted by Alistair Francis 4 days, 18 hours ago
On Wed, 2026-09-09 at 15:23 +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 <fritchleybohrer@gmail.com>

Thanks!

Applied to riscv-to-apply.next

Alistair

> 
> ---
> 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	.
Re: [PATCH v2] target/riscv: fix RV32 fixed counter accesses
Posted by Chao Liu 2 weeks, 1 day ago
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 <fritchleybohrer@gmail.com>
Reviewed-by: Chao Liu <chao.liu@processmission.com>

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
Re: [PATCH v2] target/riscv: fix RV32 fixed counter accesses
Posted by Daniel Henrique Barboza 2 weeks, 2 days ago

On 9/9/2026 4:23 AM, 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 <fritchleybohrer@gmail.com>
> 
> ---

Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>


And thanks for creating the meson scaffolding for riscv32 tcg tests.

Cheers,
Daniel

> 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	.