-
Notifications
You must be signed in to change notification settings - Fork 146
Unify interface, add memory mapping #704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
sim/aviron/build.zig
Outdated
// Run unit tests for low-level library components (e.g., memory.zig) | ||
const memory_tests = b.addTest(.{ | ||
.root_module = b.createModule(.{ | ||
.root_source_file = b.path("src/lib/memory.zig"), | ||
.target = host_target, | ||
.optimize = optimize, | ||
}), | ||
.use_llvm = true, | ||
}); | ||
test_step.dependOn(&b.addRunArtifact(memory_tests).step); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you add a separate executable? You can just pull in memory.zig
in main.zig
:
test {
_ = @import("lib/memory.zig");
}
This will compile much much faster than using a separate unit test per file and won't make problems in the future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I tried this it complained that the file was already imported in aviron.zig
sim/aviron/src/lib/io.zig
Outdated
pub const VTable = struct { | ||
read8: *const fn (ctx: *anyopaque, idx: usize) u8, | ||
write8: *const fn (ctx: *anyopaque, idx: usize, v: u8) void, | ||
write_masked: *const fn (ctx: *anyopaque, idx: usize, mask: u8, v: u8) void, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
write_masked
can have a default impl which does a read-modify-write cycle by default.
If an implementation requires true atomics, they can implement it manually
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the overall changes, but i want to make this incredibly good so we can build a nice and well suited test infrastructure on top of this
pub fn read(mem: Flash, addr: Address) u16 { | ||
std.debug.assert(addr < mem.size); | ||
return mem.vtable.readFn(mem.ctx, addr); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could add a usage_hint: UsageHint
here where UsageHint = enum { code, data}
.
This would allow us logging memory access patterns in traces, which is sometimes incredibly useful to debug hard to find bugs or optimize algorithms to go as fast as possible
|
||
pub const IO = struct { | ||
// Some AVR families (e.g., XMEGA) expose extended I/O up to 0xFFF (12 bits). | ||
pub const Address = u12; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still use u6
here as that's the width of the IO
bus. If we actually need the "extended I/O", it would go through the Bus
class anyways, so we have to expose two interfaces (and it may be interesting for logging to see how I/O is performed)
sim/aviron/src/lib/bus.zig
Outdated
pub const Bus = struct { | ||
pub const Address = u24; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we should rename this into Data
, as bus.Data
reads really fine
Otherwise we could also use Flash_Bus
, IO_Bus
and Data_Bus
sim/aviron/src/lib/bus.zig
Outdated
pub fn write_masked(self: *const Bus, addr: Address, mask: u8, v: u8) void { | ||
self.vtable.write_masked(self.ctx, addr, mask, v); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A data bus member doesn't need this function, as we can't have masked writes on the data bus, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but as it stands the Cpu doesn't access IO through anything but the IO bus, so e.g. sbi
wants this method on the interface.
sim/aviron/src/lib/bus.zig
Outdated
/// A mapping entry within a MemoryMapping. | ||
pub const Segment = struct { | ||
/// Base address within the enclosing memory space. | ||
at: Bus.Address, | ||
/// Size in bytes of the mapped range. | ||
size: Bus.Address, | ||
/// Backend handling the mapped range starting at index 0. | ||
backend: Bus, | ||
}; | ||
|
||
/// A logical memory space composed of non-overlapping segments. | ||
pub const MemoryMapping = struct { | ||
const Self = @This(); | ||
|
||
segments: []const Segment, // sorted by .at, owned by this struct | ||
|
||
pub const InitError = error{OverlappingSegments} || std.mem.Allocator.Error; | ||
pub const AccessError = error{ Unmapped, ReadOnly, OutOfRange }; | ||
|
||
pub fn init(alloc: std.mem.Allocator, segs: []const Segment) InitError!Self { | ||
// Copy segments to owned memory | ||
const owned = try alloc.alloc(Segment, segs.len); | ||
@memcpy(owned, segs); | ||
|
||
// Sort by base address | ||
std.sort.block(Segment, owned, {}, struct { | ||
fn lessThan(_: void, a: Segment, b: Segment) bool { | ||
return a.at < b.at; | ||
} | ||
}.lessThan); | ||
|
||
// Validate non-overlap | ||
if (owned.len > 1) { | ||
var i: usize = 0; | ||
while (i + 1 < owned.len) : (i += 1) { | ||
const cur_end = owned[i].at + owned[i].size; | ||
if (cur_end > owned[i + 1].at) { | ||
alloc.free(owned); | ||
return error.OverlappingSegments; | ||
} | ||
} | ||
} | ||
|
||
return .{ .segments = owned }; | ||
} | ||
|
||
pub fn deinit(self: *const Self, alloc: std.mem.Allocator) void { | ||
alloc.free(self.segments); | ||
} | ||
|
||
pub fn read(self: *const Self, addr: Bus.Address) AccessError!u8 { | ||
const seg = self.find(addr) orelse return error.Unmapped; | ||
const idx = addr - seg.at; | ||
if (idx >= seg.size) return error.OutOfRange; | ||
return seg.backend.read(idx); | ||
} | ||
|
||
pub fn write(self: *const Self, addr: Bus.Address, v: u8) AccessError!void { | ||
const seg = self.find(addr) orelse return error.Unmapped; | ||
const idx = addr - seg.at; | ||
if (idx >= seg.size) return error.OutOfRange; | ||
seg.backend.write(idx, v); | ||
return; | ||
} | ||
|
||
pub fn write_masked(self: *const Self, addr: Bus.Address, mask: u8, v: u8) AccessError!void { | ||
const seg = self.find(addr) orelse return error.Unmapped; | ||
const idx = addr - seg.at; | ||
if (idx >= seg.size) return error.OutOfRange; | ||
seg.backend.write_masked(idx, mask, v); | ||
return; | ||
} | ||
|
||
fn find(self: *const Self, addr: Bus.Address) ?*const Segment { | ||
// Linear scan is fine initially; segments are sorted. | ||
for (self.segments) |*s| { | ||
if (addr >= s.at and addr < s.at + s.size) return s; | ||
} | ||
return null; | ||
} | ||
|
||
/// Returns a Bus interface for this MemoryMapping. | ||
/// The returned Bus uses absolute addressing within this space (not segment-relative). | ||
pub fn bus(self: *Self) Bus { | ||
return .{ | ||
.ctx = @as(*anyopaque, @ptrCast(self)), | ||
.vtable = &bus_vtable, | ||
}; | ||
} | ||
|
||
const bus_vtable = Bus.VTable{ | ||
.read = bus_read, | ||
.write = bus_write, | ||
.write_masked = bus_write_masked, | ||
.check_exit = bus_check_exit, | ||
}; | ||
|
||
fn bus_read(ctx: *anyopaque, addr: Bus.Address) u8 { | ||
const self: *const Self = @ptrCast(@alignCast(ctx)); | ||
return self.read(addr) catch |e| switch (e) { | ||
error.Unmapped => @panic("Read from unmapped memory address"), | ||
error.OutOfRange => @panic("Read out of range"), | ||
error.ReadOnly => @panic("ReadOnly error on read"), | ||
}; | ||
} | ||
|
||
fn bus_write(ctx: *anyopaque, addr: Bus.Address, v: u8) void { | ||
const self: *const Self = @ptrCast(@alignCast(ctx)); | ||
self.write(addr, v) catch |e| switch (e) { | ||
error.Unmapped => @panic("Write to unmapped memory address"), | ||
error.OutOfRange => @panic("Write out of range"), | ||
error.ReadOnly => @panic("Write to read-only memory"), | ||
}; | ||
} | ||
|
||
fn bus_write_masked(ctx: *anyopaque, addr: Bus.Address, mask: u8, v: u8) void { | ||
const self: *const Self = @ptrCast(@alignCast(ctx)); | ||
self.write_masked(addr, mask, v) catch |e| switch (e) { | ||
error.Unmapped => @panic("Masked write to unmapped memory address"), | ||
error.OutOfRange => @panic("Masked write out of range"), | ||
error.ReadOnly => @panic("Masked write to read-only memory"), | ||
}; | ||
} | ||
|
||
fn bus_check_exit(ctx: *anyopaque) ?u8 { | ||
const self: *const Self = @ptrCast(@alignCast(ctx)); | ||
// Check all segments for exit condition | ||
for (self.segments) |*seg| { | ||
if (seg.backend.check_exit()) |code| { | ||
return code; | ||
} | ||
} | ||
return null; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can make the MemoryMapping
generic over a bus type, this way it keeps the right properties:
pub fn MemoryMapping(comptime BusType: type) type {
return struct {
pub const Segment = struct {
…
};
…
};
}
This way we would keep type confusion at a minimum while also keeping the amount of implementation at a minimum.
We can even safe-guard the implementation by only allowing Data_Bus
, Flash_Bus
and IO_Bus
as the parameter (or we could pass an enum {flash,data,io}
)
sim/aviron/src/main.zig
Outdated
var flash_storage = aviron.Flash.Static(mcu_config.flash_size){}; | ||
var sram = aviron.FixedSizedMemory(mcu_config.sram_size){}; | ||
var eeprom = aviron.FixedSizedMemory(mcu_config.eeprom_size){}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could make FixedSizedMemory
also expose a .flash()
interface where the function call invokes a @compileError
if the size isn't divisible by 2.
This would remove redundant code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the interface would have to be expanded for returning u16.
sim/aviron/src/main.zig
Outdated
const segment_buf = try allocator.alloc(u8, phdr.p_filesz); | ||
defer allocator.free(segment_buf); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That buffer can be deleted and we can read byte-by-byte from the reader interface as it already performs buffering internally and we have a 4096 byte large buffer above, so it's already quite optimal and the additional copy would just take time and could fail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what method do you use to read a byte from a reader?
build_spaces
helper that uses an mcu config to create data, io, and eeprom data spaces