Skip to content

Commit 0039283

Browse files
authored
Aviron: Add breakpoint, set gas, detect infinite loop (#703)
* Update README * Cleanup tests * Add breakpoint and gas command line options * tests: Make the expected value output hex * aviron: Detect infinite loop used as halt * Add check_exit method to IO to capture exit instead of killing the process
1 parent e249642 commit 0039283

File tree

19 files changed

+130
-39
lines changed

19 files changed

+130
-39
lines changed

sim/aviron/README.md

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ AVR simulator in Zig.
88

99
```sh
1010
.
11-
├── doc # Documents for Aviron or AVR
11+
├── doc # Documents for Aviron or AVR
1212
├── samples # Source of examples that can be run on the simulator
1313
├── src # Source code
1414
│ ├── lib # - Aviron emulator
1515
│ ├── libtestsuite # - Source code of the testsuite library
1616
│ └── shared # - Shared code between the tools, generated code and simulator
1717
├── testsuite # Contains the test suite of Aviron
1818
│ ├── instructions # - Tests for single instructions
19-
│ ├── lib # - Tests for the libtestsuie
20-
│ ├── simulator # - Tests for the simulator per se
19+
│ ├── lib # - Tests for the libtestsuite
20+
│ ├── simulator # - Tests for the simulator per se
2121
│ └── testrunner # - Tests for the test runner (conditions, checks, ...)
2222
├── testsuite.avr-gcc # Contains code for the test suite that cannot be built with LLVM right now
2323
│ └── instructions
@@ -29,8 +29,7 @@ AVR simulator in Zig.
2929
Run the test suite by invoking
3030

3131
```sh-session
32-
[~/projects/aviron]$ zig build test
33-
[~/projects/aviron]$
32+
$ zig build test
3433
```
3534

3635
The `test` step will recursively scan the folder `testsuite` for files of the following types:
@@ -42,7 +41,9 @@ The `test` step will recursively scan the folder `testsuite` for files of the fo
4241
- *load* `.bin`
4342
- *load* `.elf`
4443

45-
File extensions marked *compile* will be compiled or assembled with the Zig compiler, then executed with the test runner. Those files allow embedding a JSON configuration via Zig file documentation comments:
44+
File extensions marked *compile* will be compiled or assembled with the Zig compiler, then executed
45+
with the test runner. Those files allow embedding a JSON configuration via Zig file documentation
46+
comments:
4647

4748
```zig
4849
//! {
@@ -58,15 +59,19 @@ export fn _start() callconv(.c) noreturn {
5859
}
5960
```
6061

61-
For files marked as *load*, another companion file `.bin.json` or similar contains the configuration for this test.
62+
For files marked as *load*, another companion file `.bin.json` or similar contains the configuration
63+
for this test.
6264

63-
The [JSON schema](src/testconfig.zig) allows description of how the file is built and the test runner is executed. It also contains a set of pre- and postconditions that can be used to set up the CPU and validate it after the program exits.
65+
The [JSON schema](src/testconfig.zig) allows description of how the file is built and the test
66+
runner is executed. It also contains a set of pre- and postconditions that can be used to set up the
67+
CPU and validate it after the program exits.
6468

65-
If you're not sure if your code compiled correctly, you can use the `debug-testsuite` build step to inspect the generated files:
69+
If you're not sure if your code compiled correctly, you can use the `debug-testsuite` build step to
70+
inspect the generated files:
6671

6772
```sh-session
68-
[~/projects/aviron]$ zig build debug-testsuite
69-
[~/projects/aviron]$ tree zig-out/
73+
$ zig build debug-testsuite
74+
$ tree zig-out/
7075
zig-out/
7176
├── bin
7277
│ └── aviron-test-runner
@@ -90,7 +95,7 @@ zig-out/
9095
You can then disassemble those files with either `llvm-objdump` or `avr-objdump`:
9196

9297
```sh-session
93-
[~/projects/aviron]$ llvm-objdump -d zig-out/testsuite/instructions/out-exit-0.elf
98+
$ llvm-objdump -d zig-out/testsuite/instructions/out-exit-0.elf
9499

95100
zig-out/testsuite/instructions/out-exit-0.elf: file format elf32-avr
96101

@@ -99,8 +104,8 @@ Disassembly of section .text:
99104
00000000 <_start>:
100105
0: 00 27 clr r16
101106
2: 00 b9 out 0x0, r16
102-
103-
[~/projects/aviron]$ avr-objdump -d zig-out/testsuite/instructions/out-exit-0.elf
107+
108+
$ avr-objdump -d zig-out/testsuite/instructions/out-exit-0.elf
104109

105110
zig-out/testsuite/instructions/out-exit-0.elf: file format elf32-avr
106111

@@ -110,23 +115,30 @@ Disassembly of section .text:
110115
0: 00 27 eor r16, r16
111116
2: 00 b9 out 0x00, r16 ; 0
112117

113-
[~/projects/aviron]$
118+
$
114119
```
115120

116-
The test runner is located at [src/testrunner.zig](src/testrunner.zig) and behaves similar to the main `aviron` executable, but introduces a good amount of checks we can use to inspect and validate the simulation.
121+
The test runner is located at [src/testrunner.zig](src/testrunner.zig) and behaves similar to the
122+
main `aviron` executable, but introduces a good amount of checks we can use to inspect and validate
123+
the simulation.
117124

118125
### Updating AVR-GCC tests
119126

120-
To prevent a hard dependency on the `avr-gcc` toolchain, we vendor the binaries for all tests defined in the folder `testsuite.avr-gcc`. To update the files, you need to invoke the `update-testsuite` build step:
127+
To prevent a hard dependency on the `avr-gcc` toolchain, we vendor the binaries for all tests
128+
defined in the folder `testsuite.avr-gcc`. To update the files, you need to invoke the
129+
`update-testsuite` build step:
121130

122131
```sh-session
123-
[~/projects/aviron]$ zig build update-testsuite
124-
[~/projects/aviron]$
132+
$ zig build update-testsuite
133+
$
125134
```
126135

127136
After that, `zig build test` will run the regenerated tests.
128137

129-
**NOTE:** The build will not detect changed files, so you have no guarantee that running `zig build update-testsuite test` will actually do the right thing. If you're working on the test suite, just use `zig build update-testsuite && zig build test` for this.
138+
> [!NOTE]
139+
> The build will not detect changed files, so you have no guarantee that running `zig build
140+
> update-testsuite test` will actually do the right thing. If you're working on the test suite, just
141+
> use `zig build update-testsuite && zig build test` for this.
130142
131143
## Links
132144

sim/aviron/src/lib/Cpu.zig

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ const InstructionEffect = enum {
4444
sleep,
4545

4646
watchdog_reset,
47+
48+
/// Detected infinite loop (typically __stop_program after main returns)
49+
infinite_loop,
4750
};
4851

4952
// Options:
@@ -62,6 +65,7 @@ io: IO,
6265
pc: u24 = 0,
6366
regs: [32]u8 = [1]u8{0} ** 32,
6467
sreg: SREG = @bitCast(@as(u8, 0)),
68+
instr_count: u64 = 0,
6569

6670
instr_effect: InstructionEffect = .none,
6771

@@ -77,9 +81,11 @@ pub const RunResult = enum {
7781
enter_sleep_mode,
7882
reset_watchdog,
7983
out_of_gas,
84+
infinite_loop,
85+
program_exit,
8086
};
8187

82-
pub fn run(cpu: *Cpu, mileage: ?u64) RunError!RunResult {
88+
pub fn run(cpu: *Cpu, mileage: ?u64, breakpoint: ?u24) RunError!RunResult {
8389
var rest_gas = mileage;
8490

8591
while (true) {
@@ -97,11 +103,21 @@ pub fn run(cpu: *Cpu, mileage: ?u64) RunError!RunResult {
97103
.breakpoint => return .breakpoint,
98104
.sleep => return .enter_sleep_mode,
99105
.watchdog_reset => return .reset_watchdog,
106+
.infinite_loop => return .infinite_loop,
100107
};
101108
};
102109

103110
const pc = cpu.pc;
111+
112+
// Check breakpoint
113+
if (breakpoint) |bp_addr| {
114+
if (pc == bp_addr) {
115+
return .breakpoint;
116+
}
117+
}
118+
104119
const inst = try isa.decode(cpu.fetch_code());
120+
cpu.instr_count += 1;
105121

106122
if (cpu.trace) {
107123
// std.debug.print("TRACE {s} {} 0x{X:0>6}: {}\n", .{
@@ -111,7 +127,8 @@ pub fn run(cpu: *Cpu, mileage: ?u64) RunError!RunResult {
111127
// fmtInstruction(inst),
112128
// });
113129

114-
std.debug.print("TRACE {s} {f} [", .{
130+
std.debug.print("TRACE #{d: >6} {s} {f} [", .{
131+
cpu.instr_count,
115132
if (skip) "SKIP" else " ",
116133
cpu.sreg,
117134
});
@@ -141,6 +158,12 @@ pub fn run(cpu: *Cpu, mileage: ?u64) RunError!RunResult {
141158
}
142159
},
143160
}
161+
162+
// Check if the program requested exit via I/O
163+
if (cpu.io.check_exit()) |exit_code| {
164+
_ = exit_code; // The exit code is stored in the IO context for main() to use
165+
return .program_exit;
166+
}
144167
}
145168
}
146169
}
@@ -868,7 +891,14 @@ const instructions = struct {
868891
/// Program memory not exceeding 4K words (8KB) this instruction can address the entire memory from
869892
/// every address location. See also JMP.
870893
inline fn rjmp(cpu: *Cpu, bits: isa.opinfo.k12) void {
871-
cpu.shift_program_counter(@as(i12, @bitCast(bits.k)));
894+
const offset = @as(i12, @bitCast(bits.k));
895+
// Detect infinite loop pattern (rjmp .-2, which is rjmp with k=-1)
896+
// This is commonly used in __stop_program after main returns
897+
if (offset == -1) {
898+
cpu.instr_effect = .infinite_loop;
899+
return;
900+
}
901+
cpu.shift_program_counter(offset);
872902
}
873903

874904
/// RCALL – Relative Call to Subroutine

sim/aviron/src/lib/io.zig

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,23 @@ pub const IO = struct {
154154
return mem.vtable.writeFn(mem.ctx, addr, mask, value);
155155
}
156156

157+
pub fn check_exit(mem: IO) ?u8 {
158+
return mem.vtable.checkExitFn(mem.ctx);
159+
}
160+
157161
pub const VTable = struct {
158162
readFn: *const fn (ctx: ?*anyopaque, addr: Address) u8,
159163
writeFn: *const fn (ctx: ?*anyopaque, addr: Address, mask: u8, value: u8) void,
164+
checkExitFn: *const fn (ctx: ?*anyopaque) ?u8,
160165
};
161166

162167
pub const empty = IO{
163168
.ctx = null,
164-
.vtable = &VTable{ .readFn = empty_read, .writeFn = empty_write },
169+
.vtable = &VTable{
170+
.readFn = empty_read,
171+
.writeFn = empty_write,
172+
.checkExitFn = empty_check_exit,
173+
},
165174
};
166175

167176
fn empty_read(ctx: ?*anyopaque, addr: Address) u8 {
@@ -176,4 +185,9 @@ pub const IO = struct {
176185
_ = addr;
177186
_ = ctx;
178187
}
188+
189+
fn empty_check_exit(ctx: ?*anyopaque) ?u8 {
190+
_ = ctx;
191+
return null;
192+
}
179193
};

sim/aviron/src/main.zig

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,15 @@ pub fn main() !u8 {
117117
}
118118
}
119119

120-
const result = try cpu.run(null);
120+
const result = try cpu.run(null, cli.options.breakpoint);
121121

122122
std.debug.print("STOP: {s}\n", .{@tagName(result)});
123123

124+
// Handle program exit - the defer block will still run
125+
if (result == .program_exit) {
126+
return io.exit_code.?;
127+
}
128+
124129
return 0;
125130
}
126131

@@ -143,13 +148,17 @@ const Cli = struct {
143148
mcu: MCU = .atmega328p,
144149
info: bool = false,
145150
format: FileFormat = .elf,
151+
breakpoint: ?u24 = null,
152+
gas: ?u64 = null,
146153

147154
pub const shorthands = .{
148155
.h = "help",
149156
.t = "trace",
150157
.m = "mcu",
151158
.I = "info",
152159
.f = "format",
160+
.b = "breakpoint",
161+
.g = "gas",
153162
};
154163
pub const meta = .{
155164
.summary = "[-h] [-t] [-m <mcu>] <file> ...",
@@ -166,6 +175,8 @@ const Cli = struct {
166175
.mcu = "Selects the emulated MCU.",
167176
.info = "Prints information about the given MCUs memory.",
168177
.format = "Specify file format.",
178+
.breakpoint = "Break when PC reaches this address (hex or dec)",
179+
.gas = "Stop after N instructions executed",
169180
},
170181
};
171182
};
@@ -176,6 +187,9 @@ const IO = struct {
176187
sp: u16,
177188
sreg: *aviron.Cpu.SREG,
178189

190+
// Exit status tracking
191+
exit_code: ?u8 = null,
192+
179193
pub fn memory(self: *IO) aviron.IO {
180194
return aviron.IO{
181195
.ctx = self,
@@ -186,6 +200,7 @@ const IO = struct {
186200
pub const vtable = aviron.IO.VTable{
187201
.readFn = read,
188202
.writeFn = write,
203+
.checkExitFn = check_exit,
189204
};
190205

191206
// This is our own "debug" device with it's own debug addresses:
@@ -262,7 +277,9 @@ const IO = struct {
262277
const io: *IO = @ptrCast(@alignCast(ctx.?));
263278
const reg: Register = @enumFromInt(addr);
264279
switch (reg) {
265-
.exit => std.process.exit(value & mask),
280+
.exit => {
281+
io.exit_code = value & mask;
282+
},
266283
.stdio => {
267284
var stdout = std.fs.File.stdout().writer(&.{});
268285
stdout.interface.writeByte(value & mask) catch @panic("i/o failure");
@@ -317,4 +334,12 @@ const IO = struct {
317334
dst.* &= ~mask;
318335
dst.* |= (val & mask);
319336
}
337+
338+
fn check_exit(ctx: ?*anyopaque) ?u8 {
339+
const io: *IO = @ptrCast(@alignCast(ctx.?));
340+
if (io.exit_code) |code| {
341+
return code;
342+
}
343+
return null;
344+
}
320345
};

sim/aviron/src/testconfig.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ pub const ExitType = enum {
55
enter_sleep_mode,
66
reset_watchdog,
77
out_of_gas,
8+
infinite_loop,
9+
program_exit,
810
system_exit,
911
};
1012

sim/aviron/src/testrunner.zig

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const ExitMode = union(testconfig.ExitType) {
3232
enter_sleep_mode,
3333
reset_watchdog,
3434
out_of_gas,
35+
infinite_loop,
36+
program_exit,
3537
system_exit: u8,
3638
};
3739

@@ -40,7 +42,7 @@ fn validate_reg(ok: *bool, ts: *SystemState, comptime reg: aviron.Register) void
4042

4143
const actual = ts.cpu.regs[reg.num()];
4244
if (expected != actual) {
43-
std.debug.print("Invalid register value for register {s}: Expected {}, but got {}.\n", .{
45+
std.debug.print("Invalid register value for register {s}: Expected 0x{X:0>2}, but got 0x{X:0>2}.\n", .{
4446
@tagName(reg),
4547
expected,
4648
actual,
@@ -217,7 +219,7 @@ pub fn main() !u8 {
217219
}
218220
}
219221

220-
const result = try test_system.cpu.run(null);
222+
const result = try test_system.cpu.run(null, null);
221223
validate_syste_and_exit(switch (result) {
222224
inline else => |tag| @unionInit(ExitMode, @tagName(tag), {}),
223225
});
@@ -246,6 +248,7 @@ const IO = struct {
246248
pub const vtable = aviron.IO.VTable{
247249
.readFn = read,
248250
.writeFn = write,
251+
.checkExitFn = check_exit,
249252
};
250253

251254
// This is our own "debug" device with it's own debug addresses:
@@ -373,4 +376,9 @@ const IO = struct {
373376
dst.* &= ~mask;
374377
dst.* |= (val & mask);
375378
}
379+
380+
fn check_exit(ctx: ?*anyopaque) ?u8 {
381+
_ = ctx;
382+
return null; // Testrunner handles exits differently via validate_syste_and_exit
383+
}
376384
};

0 commit comments

Comments
 (0)