Compare commits

...

6 Commits

Author SHA1 Message Date
814d081ea0 chore: update to Zig v0.13.0 2024-07-03 20:13:01 -05:00
0010029783 fix: LDR(S)H behaviour differs between ARMv4/ARMv5TE 2024-03-11 10:52:28 -05:00
6f0e271360 fix: update to 0.12.0-dev.2063+804cee3b9 2024-02-08 23:09:04 -06:00
bdc4bfc642 dbg: add dbgRead and dbgWrite fns to cpu struct 2024-01-13 16:10:05 -06:00
580e7baca9 fix: make Bank.spsrIndex public 2023-12-27 18:52:28 -06:00
aad3bdc9ea feat: pass nds arm7wrestler
- impl behaviour of running v5te instrs on v4t cpu
- impl undefined instruction exception handler
- panic on what I think are still unimplemented v5te opcodes
2023-12-27 00:13:06 -06:00
9 changed files with 206 additions and 80 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
zig-out/
zig-cache/
.zig-cache/

View File

@@ -15,63 +15,33 @@ pub fn build(b: *std.Build) void {
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const util_dep = b.dependency("zba-util", .{});
const bitfield_mod = b.createModule(.{ .source_file = .{ .path = "lib/bitfield.zig" }, .dependencies = &.{} });
const util_dep = b.dependency("zba-util", .{}); // https://git.musuka.dev/paoda/zba-util
const bitfield_mod = b.createModule(.{ .root_source_file = b.path("lib/bitfield.zig") }); // https://github.com/FlorenceOS/Florence
_ = b.addModule("arm32", .{
.source_file = .{ .path = "src/lib.zig" },
.dependencies = &.{
.{
.name = "zba-util",
.module = util_dep.module("zba-util"),
},
.{
.name = "bitfield",
.module = bitfield_mod,
},
.root_source_file = b.path("src/lib.zig"),
.imports = &.{
.{ .name = "zba-util", .module = util_dep.module("zba-util") },
.{ .name = "bitfield", .module = bitfield_mod },
},
});
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const lib_tests = b.addTest(.{
.root_source_file = .{ .path = "src/lib.zig" },
const lib_unit_tests = b.addTest(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
lib_tests.addModule("zba-util", util_dep.module("zba-util")); // https://git.musuka.dev/paoda/zba-util
lib_tests.addModule("bitfield", bitfield_mod);
lib_unit_tests.root_module.addImport("zba-util", util_dep.module("zba-util"));
lib_unit_tests.root_module.addImport("bitfield", bitfield_mod);
const run_lib_tests = b.addRunArtifact(lib_tests);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
// This creates a build step. It will be visible in the `zig build --help` menu,
// and can be selected like this: `zig build test`
// This will evaluate the `test` step rather than the default, which is "install".
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&run_lib_tests.step);
}
/// `arm32` will expect the depender to supply the `zba-util` library via the package maanger
pub fn module(b: *std.Build) *std.Build.Module {
const bitfield = b.createModule(.{ .source_file = .{ .path = path("/lib/bitfield.zig") }, .dependencies = &.{} });
const zba_util = b.dependency("zba-util", .{}).module("zba-util");
return b.createModule(.{
.source_file = .{ .path = path("/src/lib.zig") },
.dependencies = &.{
.{ .name = "zba-util", .module = zba_util },
.{ .name = "bitfield", .module = bitfield },
},
});
}
// https://github.com/MasterQ32/SDL.zig/blob/4d565b54227b862c1540719e0e21a36d649e87d5/build.zig#L114-L120
fn path(comptime suffix: []const u8) []const u8 {
if (suffix[0] != '/') @compileError("relToPath requires an absolute path!");
return comptime blk: {
const root_dir = std.fs.path.dirname(@src().file) orelse ".";
break :blk root_dir ++ suffix;
};
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
}

View File

@@ -1,10 +1,77 @@
.{
.name = "arm32",
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = "zba-util",
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.1.0",
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
.@"zba-util" = .{
.url = "https://git.musuka.dev/paoda/zba-util/archive/322c798e384a0d24cc84ffcfa2e4a3ca807798a0.tar.gz",
.hash = "12209ce0e729460b997706e47a53a32f1842672cd120189e612f4871731780a30ed0",
.url = "https://git.musuka.dev/paoda/zba-util/archive/bf0e744047ce1ec90172dbcc0c72bfcc29a063e3.tar.gz",
.hash = "1220d044ecfbeacc3b3cebeff131d587e24167d61435a3cb96dffd4d4521bb06aed0",
},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"lib/bitfield.zig",
// For example...
//"LICENSE",
//"README.md",
},
}

View File

@@ -142,7 +142,7 @@ pub fn Arm32(comptime isa: Architecture) type {
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
}
inline fn spsrIdx(mode: Mode) usize {
pub inline fn spsrIdx(mode: Mode) usize {
return switch (mode) {
.Supervisor => 0,
.Abort => 1,
@@ -217,6 +217,24 @@ pub fn Arm32(comptime isa: Architecture) type {
}
};
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
if (is_v5te) {
if (self.itcm.read(T, address)) |val| return val;
if (self.dtcm.read(T, address)) |val| return val;
}
return self.bus.dbgRead(T, address);
}
pub fn dbgWrite(self: *Self, comptime T: type, address: u32, value: T) void {
if (is_v5te) {
if (self.itcm.write(T, address, value)) return;
if (self.dtcm.write(T, address, value)) return;
}
return self.bus.dbgWrite(T, address, value);
}
// CPU needs it's own read/write fns due to ICTM and DCTM present in v5te
// I considered implementing Bus.cpu_read and Bus.cpu_write but ended up considering that a bit too leaky
pub fn read(self: *Self, comptime T: type, address: u32) T {
@@ -409,6 +427,29 @@ pub fn Arm32(comptime isa: Architecture) type {
std.debug.panic(format, args);
}
// TODO: Rename
pub fn undefinedInstructionTrap(self: *Self) void {
// Copy Values from Current Mode
const ret_addr = self.r[15] - @as(u32, if (self.cpsr.t.read()) 2 else 4);
const cpsr = self.cpsr.raw;
// Switch Mode
self.changeMode(.Undefined);
self.cpsr.t.write(false); // Force ARM Mode
self.cpsr.i.write(true); // Disable normal interrupts
self.r[14] = ret_addr; // Resume Execution
self.spsr.raw = cpsr; // Previous mode CPSR
self.r[15] = switch (Self.arch) {
.v4t => 0x0000_0004,
.v5te => blk: {
const ctrl = self.cp15.read(0, 1, 0, 0);
break :blk if (ctrl >> 13 & 1 == 1) 0xFFFF_0004 else 0x0000_0004;
},
};
self.pipe.reload(self);
}
pub fn interface(self: *Self) Interpreter {
return switch (isa) {
.v4t => .{ .v4t = self },
@@ -439,8 +480,6 @@ fn Tcm(comptime count: usize, comptime default_addr: u32) type {
///
/// The caller doesn't particularly care about "why" though.
pub fn read(self: *const @This(), comptime T: type, address: u32) ?T {
const readInt = std.mem.readIntSliceLittle;
if (!self.enabled) return null;
if (self.load_mode) return null;
@@ -448,7 +487,7 @@ fn Tcm(comptime count: usize, comptime default_addr: u32) type {
const end_addr = self.base_address + self.virt.size;
if (start_addr <= address and address < end_addr) {
return readInt(T, self.buf[address & self.virt.mask ..][0..@sizeOf(T)]);
return std.mem.readInt(T, self.buf[address & self.virt.mask ..][0..@sizeOf(T)], .little);
}
return null;
@@ -462,14 +501,13 @@ fn Tcm(comptime count: usize, comptime default_addr: u32) type {
///
/// The caller doesn't particularly care about "why" though.
pub fn write(self: *@This(), comptime T: type, address: u32, value: T) bool {
const writeInt = std.mem.writeIntSliceLittle;
if (!self.enabled) return false;
const start_addr = self.base_address;
const end_addr = self.base_address + self.virt.size;
if (start_addr <= address and address < end_addr) {
writeInt(T, self.buf[address & self.virt.mask ..][0..@sizeOf(T)], value);
std.mem.writeInt(T, self.buf[address & self.virt.mask ..][0..@sizeOf(T)], value, .little);
return true;
}

View File

@@ -51,14 +51,13 @@ pub fn dataTransfer(
// TODO: Increment address + 4 (and perform op) until coprocessor says stop
if (L) {
log.debug("TODO: ldc{s} p{}, c{}, 0x{X:0>8}", .{ [_]u8{if (N) 'l' else ' '}, cp_num, crd, start_address });
cpu.panic("TODO: ldc{s} p{}, c{}, 0x{X:0>8}", .{ [_]u8{if (N) 'l' else ' '}, cp_num, crd, start_address });
} else {
log.debug("TODO: stc{s} p{}, c{}, 0x{X:0>8}", .{ [_]u8{if (N) 'l' else ' '}, cp_num, crd, start_address });
cpu.panic("TODO: stc{s} p{}, c{}, 0x{X:0>8}", .{ [_]u8{if (N) 'l' else ' '}, cp_num, crd, start_address });
}
}
fn copExt(cpu: *Arm32, opcode: u32) void {
_ = cpu;
const cp_num = opcode >> 8 & 0xF;
const rd = opcode >> 12 & 0xF;
const rn = opcode >> 16 & 0xF;
@@ -71,10 +70,10 @@ pub fn dataTransfer(
if (L) {
// MRRC
log.debug("TODO: mrrc p{}, {}, r{}, r{}, c{}", .{ cp_num, cp_opcode, rd, rn, crm });
cpu.panic("TODO: mrrc p{}, {}, r{}, r{}, c{}", .{ cp_num, cp_opcode, rd, rn, crm });
} else {
// MCRR
log.debug("TODO: mcrr p{}, {}, r{}, r{}, c{}", .{ cp_num, cp_opcode, rd, rn, crm });
cpu.panic("TODO: mcrr p{}, {}, r{}, r{}, c{}", .{ cp_num, cp_opcode, rd, rn, crm });
}
}
}.inner;
@@ -90,7 +89,11 @@ pub fn registerTransfer(comptime InstrFn: type, comptime opcode1: u3, comptime L
const cp_num = opcode >> 8 & 0xF;
const crm: u4 = @intCast(opcode & 0xF);
std.debug.assert(cp_num == 0xF); // There's no other coprocessor on NDS9;
switch (cp_num) {
14 => return,
15 => if (Arm32.arch == .v4t) return cpu.undefinedInstructionTrap(),
else => cpu.panic("MRC: unexpected coprocessor #: {}", .{cp_num}),
}
if (L) {
// MRC
@@ -148,9 +151,7 @@ pub fn dataProcessing(comptime InstrFn: type, comptime opcode1: u4, comptime opc
return struct {
fn inner(cpu: *Arm32, opcode: u32) void {
_ = cpu;
log.err("TODO: handle 0x{X:0>8} which is a coprocessor data processing instr", .{opcode});
cpu.panic("TODO: handle 0x{X:0>8} which is a coprocessor data processing instr", .{opcode});
}
}.inner;
}

View File

@@ -1,6 +1,9 @@
const std = @import("std");
const sext = @import("zba-util").sext;
const rotr = @import("zba-util").rotr;
const log = std.log.scoped(.half_and_signed_data_transfer);
pub fn halfAndSignedDataTransfer(comptime InstrFn: type, comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
const Arm32 = @typeInfo(@typeInfo(@typeInfo(InstrFn).Pointer.child).Fn.params[0].type.?).Pointer.child;
@@ -24,8 +27,10 @@ pub fn halfAndSignedDataTransfer(comptime InstrFn: type, comptime P: bool, compt
switch (op) {
0b01 => {
// LDRH
const value = cpu.read(u16, address);
result = rotr(u32, value, 8 * (address & 1));
result = switch (Arm32.arch) {
.v4t => rotr(u32, cpu.read(u16, address), 8 * (address & 1)),
.v5te => cpu.read(u16, address),
};
},
0b10 => {
// LDRSB
@@ -33,10 +38,17 @@ pub fn halfAndSignedDataTransfer(comptime InstrFn: type, comptime P: bool, compt
},
0b11 => {
// LDRSH
const value = cpu.read(u16, address);
result = switch (Arm32.arch) {
.v4t => blk: {
const value = cpu.read(u16, address);
// FIXME: I shouldn't have to use @as(u8, ...) here
result = if (address & 1 == 1) sext(u32, u8, @as(u8, @truncate(value >> 8))) else sext(u32, u16, value);
break :blk switch (address & 1 == 1) {
true => sext(u32, u8, @as(u8, @truncate(value >> 8))),
false => sext(u32, u16, value),
};
},
.v5te => sext(u32, u16, cpu.read(u16, address)),
};
},
0b00 => unreachable,
}
@@ -66,9 +78,9 @@ pub fn halfAndSignedDataTransfer(comptime InstrFn: type, comptime P: bool, compt
// FIXME: I shouldn't have to use @as(u16, ...) here
cpu.write(u16, address, @as(u16, @truncate(cpu.r[rd])));
},
0b10 => {
0b10 => blk: {
// LDRD
if (Arm32.arch != .v5te) cpu.panic("LDRD: unsupported on arm{s}", .{@tagName(Arm32.arch)});
if (Arm32.arch == .v4t) break :blk;
if (rd & 0 != 0) cpu.panic("LDRD: UNDEFINED behaviour when Rd is not even", .{});
if (rd == 0xE) cpu.panic("LDRD: UNPREDICTABLE behaviour when rd == 14", .{});
if (address & 0x7 != 0b000) cpu.panic("LDRD: UNPREDICTABLE when address (0x{X:0>8} is not double (64-bit) aligned", .{address});

View File

@@ -30,6 +30,7 @@ pub fn control(comptime InstrFn: type, comptime I: bool, comptime op: u6) InstrF
},
0b01_0001 => cpu.panic("TODO: implement v5TE BX", .{}),
0b11_0001 => { // CLZ
if (Arm32.arch == .v4t) return cpu.undefinedInstructionTrap();
const rd = opcode >> 12 & 0xF;
const rm = opcode & 0xF;
@@ -50,6 +51,7 @@ pub fn control(comptime InstrFn: type, comptime I: bool, comptime op: u6) InstrF
cpu.pipe.reload(cpu);
},
0b00_0101, 0b01_0101, 0b10_0101, 0b11_0101 => { // QADD / QDADD / QSUB / QDSUB
if (Arm32.arch == .v4t) return cpu.undefinedInstructionTrap();
const U = op >> 4 & 1 == 1;
const D = op >> 5 & 1 == 1;
@@ -84,6 +86,7 @@ pub fn control(comptime InstrFn: type, comptime I: bool, comptime op: u6) InstrF
},
0b01_0111 => cpu.panic("TODO: handle BKPT", .{}),
0b00_1000, 0b00_1010, 0b00_1100, 0b00_1110 => { // SMLA<x><y>
if (Arm32.arch == .v4t) return; // no-op
const X = op >> 1 & 1;
const Y = op >> 2 & 1;

View File

@@ -41,15 +41,24 @@ pub fn fmt78(comptime InstrFn: type, comptime op: u2, comptime T: bool) InstrFn
},
0b10 => {
// LDRH
const value = cpu.read(u16, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
cpu.r[rd] = switch (Arm32.arch) {
.v4t => rotr(u32, cpu.read(u16, address), 8 * (address & 1)),
.v5te => cpu.read(u16, address),
};
},
0b11 => {
// LDRSH
const value = cpu.read(u16, address);
cpu.r[rd] = switch (Arm32.arch) {
.v4t => blk: {
const value = cpu.read(u16, address);
// FIXME: I shouldn't have to use @as(u8, ...) here
cpu.r[rd] = if (address & 1 == 1) sext(u32, u8, @as(u8, @truncate(value >> 8))) else sext(u32, u16, value);
break :blk switch (address & 1 == 1) {
true => sext(u32, u8, @as(u8, @truncate(value >> 8))),
false => sext(u32, u16, value),
};
},
.v5te => sext(u32, u16, cpu.read(u16, address)),
};
},
}
} else {
@@ -128,8 +137,10 @@ pub fn fmt10(comptime InstrFn: type, comptime L: bool, comptime offset: u5) Inst
if (L) {
// LDRH
const value = cpu.read(u16, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
cpu.r[rd] = switch (Arm32.arch) {
.v4t => rotr(u32, cpu.read(u16, address), 8 * (address & 1)),
.v5te => cpu.read(u16, address),
};
} else {
// STRH

View File

@@ -20,6 +20,8 @@ pub const arm = struct {
/// Arithmetic Instruction Extension Space
const multiplyExt = @import("cpu/arm/multiply.zig").multiply;
const cop = @import("cpu/arm/coprocessor.zig");
/// Determine index into ARM InstrFn LUT
pub fn idx(opcode: u32) u12 {
// FIXME: omit these?
@@ -87,8 +89,29 @@ pub const arm = struct {
const L = i >> 8 & 1 == 1;
break :blk branch(InstrFn, L);
},
0b10 => und, // COP Data Transfer
0b11 => if (i >> 8 & 1 == 1) swi(InstrFn) else und, // COP Data Operation + Register Transfer
0b10 => blk: {
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const N = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk cop.dataTransfer(InstrFn, P, U, N, W, L);
},
0b11 => blk: {
if (i >> 8 & 1 == 1) break :blk swi(InstrFn);
const data_opcode1 = i >> 4 & 0xF; // bits 20 -> 23
const reg_opcode1 = i >> 5 & 0x7; // bits 21 -> 23
const opcode2 = i >> 1 & 0x7; // bits 5 -> 7
const L = i >> 4 & 1 == 1; // bit 20
// Bit 4 (index pos of 0) distinguishes between these classes of instructions
break :blk switch (i & 1 == 1) {
true => cop.registerTransfer(InstrFn, reg_opcode1, L, opcode2),
false => cop.dataProcessing(InstrFn, data_opcode1, opcode2),
};
},
},
};
}