Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions tools/sorcerer/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,127 @@
# Sorcerer

Sorcerer is a suite of tools for visualizing and generating MicroZig register definitions. It
consists of two components:

- **Sorcerer (GUI)**: A graphical application for browsing and editing register definitions
- **sorcerer-cli**: A command-line tool for generating register code

Both tools work with MicroZig's register definition files (SVD, ATDF, Embassy formats) and use
[regz](../regz/) to generate type-safe Zig code.

## Building

From the `tools/sorcerer/` directory:

```bash
# Build both tools
zig build
```

## Sorcerer GUI

The GUI provides an interactive interface for:
- Browsing all MicroZig register definitions
- Viewing generated Zig code with syntax highlighting
- Opening custom SVD/ATDF files
- Searching chips, boards, and targets
- Viewing patch files that modify register definitions
- Statistics overview showing chip counts, formats, and patch coverage

### Running

```bash
zig build run
# or
./zig-out/bin/sorcerer
```

### Dependencies

The GUI requires additional dependencies:
- DVUI (SDL3-based UI framework)
- tree-sitter-zig (syntax highlighting)
- serial (serial port communication)

## `sorcerer-cli`

A lightweight CLI alternative that generates register definitions without GUI dependencies.

### Usage

```
sorcerer-cli <command> [options]

Commands:
list List all available targets
generate <chip> Generate register definitions for a chip

Options for 'list':
--port <name> Filter by port name (e.g., rp2xxx, ch32v)
--json Output in JSON format

Options for 'generate':
-o, --output <dir> Output directory (default: ./zig-out)

General options:
-h, --help Show help
```

### Examples

```bash
# List all available chips
./zig-out/bin/sorcerer-cli list

# Output:
# CHIP PORT BOARD
# ------------------------ ------------------------ --------------------------------
# RP2040 raspberrypi/rp2xxx Raspberry Pi Pico
# RP2350 raspberrypi/rp2xxx Raspberry Pi Pico 2
# STM32F103C8 stmicro/stm32 -
# CH32V003 wch/ch32v -
# ...

# Filter by port
./zig-out/bin/sorcerer-cli list --port rp2xxx

# JSON output (for scripting)
./zig-out/bin/sorcerer-cli list --json | jq '.[] | select(.port | contains("rp2xxx"))'

# Generate register definitions for a chip
./zig-out/bin/sorcerer-cli generate RP2040 -o ./my-regs/

# This generates:
# ./my-regs/RP2040.zig
# ./my-regs/types.zig
# ./my-regs/types/
```

### Running via build system

```bash
# Run CLI with arguments
zig build run-cli -- list --port ch32v
zig build run-cli -- generate CH32V003 -o /tmp/regs
```

## Architecture

Both tools share the same underlying data:

1. **Build time**: `build.zig` collects all register definitions from MicroZig ports
2. **Schema generation**: Schemas are embedded as Zig compile-time constants in a generated
`register_schemas.zig` file
3. **Both GUI and CLI**: Import the same `schemas` module - no runtime file loading or JSON parsing
needed

## Register Definition Formats

Sorcerer supports the following formats via regz:

| Format | Extension | Description |
|--------|-----------|-------------|
| SVD | `.svd` | ARM CMSIS System View Description |
| ATDF | `.atdf` | Microchip ATDF (AVR/SAM devices) |
| Embassy | - | Embassy HAL register definitions |
| TargetDB | - | TI TargetDB format |
174 changes: 159 additions & 15 deletions tools/sorcerer/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,9 @@ pub fn build(b: *std.Build) void {
const mb = MicroBuild.init(b, mz_dep) orelse return;
const register_schemas = get_register_schemas(b, mb) catch @panic("OOM");
const write_files = b.addWriteFiles();
const register_schema = write_files.add("register_schemas.json", std.json.Stringify.valueAlloc(b.allocator, register_schemas, .{}) catch @panic("OOM"));
const register_schema_install = b.addInstallFile(register_schema, "data/register_schemas.json");
b.getInstallStep().dependOn(&register_schema_install.step);

const dvui_dep = b.dependency("dvui", .{
.target = target,
.optimize = optimize,
});
// Generate Zig file with embedded schemas (used by both CLI and GUI)
const register_schema_zig = write_files.add("register_schemas.zig", generate_zig_schema_literal(b.allocator, register_schemas) catch @panic("OOM"));

const regz_dep = mz_dep.builder.dependency("tools/regz", .{
.target = target,
Expand All @@ -32,13 +27,63 @@ pub fn build(b: *std.Build) void {
.optimize = .ReleaseSafe,
});

const regz_mod = regz_dep.module("regz");

// Shared module for RegisterSchemaUsage (used by both schemas_mod and cli_mod)
const register_schema_usage_mod = b.createModule(.{
.root_source_file = b.path("src/RegisterSchemaUsage.zig"),
});

// Create schemas module from generated Zig file
const schemas_mod = b.createModule(.{
.root_source_file = register_schema_zig,
.imports = &.{
.{ .name = "RegisterSchemaUsage", .module = register_schema_usage_mod },
},
});

// ─────────────────────────────────────────────────────────────────────────
// CLI executable
// ─────────────────────────────────────────────────────────────────────────
const cli_mod = b.createModule(.{
.root_source_file = b.path("src/cli.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "regz", .module = regz_mod },
.{ .name = "schemas", .module = schemas_mod },
.{ .name = "RegisterSchemaUsage", .module = register_schema_usage_mod },
},
});

const cli_exe = b.addExecutable(.{
.name = "sorcerer-cli",
.root_module = cli_mod,
});
b.installArtifact(cli_exe);

const run_cli_cmd = b.addRunArtifact(cli_exe);
run_cli_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cli_cmd.addArgs(args);
}
const run_cli_step = b.step("run-cli", "Run the CLI tool");
run_cli_step.dependOn(&run_cli_cmd.step);

// ─────────────────────────────────────────────────────────────────────────
// GUI executable
// ─────────────────────────────────────────────────────────────────────────
const dvui_dep = b.dependency("dvui", .{
.target = target,
.optimize = optimize,
});

const serial_dep = b.dependency("serial", .{
.target = target,
.optimize = optimize,
});

const dvui_mod = dvui_dep.module("dvui_sdl3");
const regz_mod = regz_dep.module("regz");
const serial_mod = serial_dep.module("serial");

const exe_mod = b.createModule(.{
Expand All @@ -58,6 +103,14 @@ pub fn build(b: *std.Build) void {
.name = "serial",
.module = serial_mod,
},
.{
.name = "schemas",
.module = schemas_mod,
},
.{
.name = "RegisterSchemaUsage",
.module = register_schema_usage_mod,
},
},
});

Expand Down Expand Up @@ -94,7 +147,7 @@ pub fn build(b: *std.Build) void {
run_cmd.addArgs(args);
}

const run_step = b.step("run", "Run the app");
const run_step = b.step("run", "Run the GUI app");
run_step.dependOn(&run_cmd.step);

const exe_unit_tests = b.addTest(.{
Expand Down Expand Up @@ -320,11 +373,18 @@ fn get_register_schemas(b: *std.Build, mb: *MicroBuild) ![]const RegisterSchemaU
const patch_files = try convert_patch_files(b, t.chip.patch_files);

if (chips.getEntry(lazy_path)) |entry| {
try entry.value_ptr.append(b.allocator, .{
.name = t.chip.name,
.target_name = twp.path,
.patch_files = patch_files,
});
// Check if this chip name already exists (deduplicate by chip name)
const chip_exists = for (entry.value_ptr.items) |existing_chip| {
if (std.mem.eql(u8, existing_chip.name, t.chip.name)) break true;
} else false;

if (!chip_exists) {
try entry.value_ptr.append(b.allocator, .{
.name = t.chip.name,
.target_name = twp.path,
.patch_files = patch_files,
});
}
} else {
var chip_list: std.ArrayList(RegisterSchemaUsage.Chip) = .{};
try chip_list.append(b.allocator, .{
Expand Down Expand Up @@ -372,7 +432,9 @@ fn get_port_name(path: []const u8) []const u8 {
var i: u32 = 0;
var slash_count: u32 = 0;
while (i < path.len) : (i += 1) {
if (path[path.len - i - 1] == '/') {
const c = path[path.len - i - 1];
// Handle both forward and backslashes for cross-platform compatibility
if (c == '/' or c == '\\') {
switch (slash_count) {
0 => slash_count += 1,
1 => {
Expand All @@ -386,3 +448,85 @@ fn get_port_name(path: []const u8) []const u8 {

unreachable;
}

/// Normalize a path to use forward slashes (for cross-platform compatibility in generated Zig code).
/// Windows backslashes would be interpreted as escape sequences in Zig string literals.
fn normalize_path(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
const result = try allocator.alloc(u8, path.len);
for (path, 0..) |c, i| {
result[i] = if (c == '\\') '/' else c;
}
return result;
}

/// Generate a Zig source file containing the register schemas as compile-time constants.
fn generate_zig_schema_literal(allocator: std.mem.Allocator, schemas: []const RegisterSchemaUsage) ![]const u8 {
var buf: std.ArrayList(u8) = .{};
const writer = buf.writer(allocator);

try writer.writeAll(
\\// Auto-generated file - do not edit manually.
\\// Generated by tools/sorcerer/build.zig
\\
\\const RegisterSchemaUsage = @import("RegisterSchemaUsage");
\\
\\pub const schemas: []const RegisterSchemaUsage = &.{
\\
);

for (schemas) |schema| {
try writer.writeAll(" .{\n");

// Format
try writer.print(" .format = .{s},\n", .{@tagName(schema.format)});

// Chips
try writer.writeAll(" .chips = &.{");
for (schema.chips, 0..) |chip, i| {
if (i > 0) try writer.writeAll(", ");
try writer.print(".{{ .name = \"{s}\" }}", .{chip.name});
}
try writer.writeAll("},\n");

// Boards
try writer.writeAll(" .boards = &.{");
for (schema.boards, 0..) |board, i| {
if (i > 0) try writer.writeAll(", ");
try writer.print(".{{ .name = \"{s}\" }}", .{board.name});
}
try writer.writeAll("},\n");

// Location
try writer.writeAll(" .location = ");
switch (schema.location) {
.src_path => |src| {
const sub_path = try normalize_path(allocator, src.sub_path);
const build_root = try normalize_path(allocator, src.build_root);
try writer.writeAll(".{ .src_path = .{\n");
try writer.print(" .port_name = \"{s}\",\n", .{src.port_name});
try writer.print(" .sub_path = \"{s}\",\n", .{sub_path});
try writer.print(" .build_root = \"{s}\",\n", .{build_root});
try writer.writeAll(" } },\n");
},
.dependency => |dep| {
const sub_path = try normalize_path(allocator, dep.sub_path);
const build_root = try normalize_path(allocator, dep.build_root);
try writer.writeAll(".{ .dependency = .{\n");
try writer.print(" .sub_path = \"{s}\",\n", .{sub_path});
try writer.print(" .build_root = \"{s}\",\n", .{build_root});
try writer.print(" .dep_name = \"{s}\",\n", .{dep.dep_name});
try writer.print(" .port_name = \"{s}\",\n", .{dep.port_name});
try writer.writeAll(" } },\n");
},
}

try writer.writeAll(" },\n");
}

try writer.writeAll(
\\};
\\
);

return buf.toOwnedSlice(allocator);
}
2 changes: 1 addition & 1 deletion tools/sorcerer/src/RegzWindow.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const Allocator = std.mem.Allocator;

const regz = @import("regz");
const VirtualFilesystem = regz.VirtualFilesystem;
const RegisterSchemaUsage = @import("RegisterSchemaUsage.zig");
const RegisterSchemaUsage = @import("RegisterSchemaUsage");

const dvui = @import("dvui");

Expand Down
Loading
Loading