Skip to content

Instantly share code, notes, and snippets.

@DrSensor
Last active September 30, 2025 16:00
Show Gist options
  • Select an option

  • Save DrSensor/4f326af86e21d5fe625bbd8570e2ae70 to your computer and use it in GitHub Desktop.

Select an option

Save DrSensor/4f326af86e21d5fe625bbd8570e2ae70 to your computer and use it in GitHub Desktop.
unsafe ISR problem
[build]
target = "riscv32imac-unknown-none-elf"
[target.riscv32imac-unknown-none-elf]
runner = "qemu-runner.sh"
[package]
name = "isr-i2c-sim"
version = "0.1.0"
edition = "2021"
[dependencies]
riscv = "0.9" # riscv register helpers
riscv-rt = "0.11" # runtime + interrupt vectors
panic-halt = "0.2" # halting panic handler
cortex-m-semihosting = "0.5" # optional for semihost logging (works on some QEMU setups)
[profile.release]
opt-level = "z"
codegen-units = 1
lto = true
[package.metadata.cargo-targets]
using sysbus
mach create "i2c_isr_test"
# Use a generic RISC-V 'virt' machine
machine LoadPlatformDescription @platforms/cpus/riscv32-virt.repl
# Load our firmware ELF
sysbus LoadELF @target/riscv32imac-unknown-none-elf/release/isr-i2c-sim
# Create a timer that periodically triggers the machine timer interrupt
emulation CreateTimer "irqTimer" 0.01 # 10ms period
irqTimer -> irq 7 # IRQ7 is machine timer on virt
irqTimer Enabled true
# Start emulation
start
#![no_std]
#![no_main]
use core::cell::UnsafeCell;
use core::panic::PanicInfo;
use core::sync::atomic::{AtomicBool, Ordering};
use riscv::register::{mie, mstatus};
use riscv_rt::entry;
#[no_mangle]
pub static mut LOG_BUF: [u8; 4096] = [0; 4096];
#[no_mangle]
pub static mut LOG_POS: usize = 0;
fn log_bytes(bytes: &[u8]) {
unsafe {
let mut p = LOG_POS;
for &b in bytes {
LOG_BUF[p % LOG_BUF.len()] = b;
p += 1;
}
LOG_POS = p;
}
}
fn log_line(s: &str) {
log_bytes(s.as_bytes());
log_bytes(b"\n");
}
/// Very simple MockI2c
struct MockI2c {
tx_count: u32,
}
impl MockI2c {
const fn new() -> Self { Self { tx_count: 0 } }
fn start(&mut self, id: u32) {
self.tx_count = self.tx_count.wrapping_add(1);
log_line("START");
busy_delay(2000);
}
fn write(&mut self, id: u32, b: u8) {
let buf = [b];
log_bytes(b"BYTE:");
log_bytes(&buf);
log_bytes(b"\n");
busy_delay(2000);
}
fn stop(&mut self, id: u32) {
log_line("STOP");
busy_delay(1000);
}
}
fn busy_delay(n: u32) {
for _ in 0..n {
core::sync::atomic::compiler_fence(Ordering::SeqCst);
}
}
/// Shared I²C instance
static mut GLOBAL_I2C: UnsafeCell<MockI2c> = UnsafeCell::new(MockI2c::new());
/// Flag for safe mode ISR deferral
static ISR_FLAG: AtomicBool = AtomicBool::new(false);
#[entry]
fn main() -> ! {
unsafe {
mstatus::set_mie();
mie::set_mtimer();
}
log_line("boot");
for i in 0..200 {
// optional jitter
busy_delay(500);
// ✅ Safe mode: check ISR flag
#[cfg(feature = "safe")]
if ISR_FLAG.swap(false, Ordering::Relaxed) {
unsafe {
let p = &mut *GLOBAL_I2C.get();
p.start(0xDEAD);
p.write(0xDEAD, 0xEE);
p.stop(0xDEAD);
}
}
// Main transaction
unsafe {
let p = &mut *GLOBAL_I2C.get();
let txid = 1000 + i;
p.start(txid);
for b in 0..3 {
p.write(txid, b);
}
p.stop(txid);
}
}
log_line("done");
loop {}
}
#[export_name = "MachineTimer"]
pub extern "C" fn machine_timer() {
#[cfg(not(feature = "safe"))]
unsafe {
let p = &mut *GLOBAL_I2C.get();
p.start(0xDEAD);
p.write(0xDEAD, 0xEE);
p.stop(0xDEAD);
}
#[cfg(feature = "safe")]
{
ISR_FLAG.store(true, Ordering::Relaxed);
}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
log_line("panic");
loop {}
}
MEMORY
{
RAM : ORIGIN = 0x8000_0000, LENGTH = 64K
FLASH : ORIGIN = 0x200000, LENGTH = 512K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.text : { KEEP(*(.vectors)); *(.text*) } > FLASH
.rodata : { *(.rodata*) } > FLASH
.data : { *(.data*) } > RAM AT > FLASH
.bss : { *(.bss*) } > RAM
/DISCARD/ : { *(.eh_frame) }
}
#!/bin/sh
case $2 in
unsafe)
cargo build --release
;;
safe)
cargo build --release --features safe
;;
esac
case $1 in
qemu) # Example: using qemu-system-riscv32 for sifive_emulate (adjust as needed)
qemu-system-riscv32 -machine sifive_u -nographic -kernel target/riscv32imac-unknown-none-elf/release/isr-i2c-sim
;;
renode)
renode isr-i2c-sim.resc &
sysbus ReadBlockFromSymbol "LOG_BUF" 512
;;
test)
renode-test test_isr_i2c.robot
;;
esac
#!/bin/sh
rustup target add riscv32imac-unknown-none-elf
cargo install cargo-binutils
rustup component add llvm-tools-preview
*** Settings ***
Library RenodeLibrary
*** Variables ***
${ELF} @target/riscv32imac-unknown-none-elf/release/isr-i2c-sim
*** Test Cases ***
Unsafe ISR Should Show Interleaving
Start Emulation isr-i2c-sim.resc
Execute Command sysbus LoadELF ${ELF}
Start Emulation
Sleep 1s
${pos}= Execute Command sysbus ReadDoubleWordFromSymbol "LOG_POS"
${data}= Execute Command sysbus ReadBlockFromSymbol "LOG_BUF" 512
Log ${data}
Should Contain ${data} DEAD # check ISR marker appears mid-main
Safe ISR Should Not Interleave
Start Emulation isr-i2c-sim.resc
Execute Command sysbus LoadELF ${ELF}
Start Emulation
Sleep 1s
${pos}= Execute Command sysbus ReadDoubleWordFromSymbol "LOG_POS"
${data}= Execute Command sysbus ReadBlockFromSymbol "LOG_BUF" 512
Log ${data}
Should Not Contain ${data} Interleaved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment