Skip to content
Merged
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
4 changes: 3 additions & 1 deletion build-internals/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub const Patch = regz.patch.Patch;
const uf2 = @import("uf2");
pub const FamilyId = uf2.FamilyId;
const esp_image = @import("esp-image");
const dfu = @import("dfu");

pub fn build(b: *Build) void {
_ = b.addModule("build-internals", .{
Expand Down Expand Up @@ -216,7 +217,8 @@ pub const BinaryFormat = union(enum) {
hex,

/// A [Device Firmware Upgrade](https://www.usb.org/sites/default/files/DFU_1.1.pdf) file.
dfu,
/// ST MicroElectronics [DfuSe](https://rc.fdr.hu/UM0391.pdf) format is also supported.
dfu: dfu.Options,

/// The [USB Flashing Format (UF2)](https://github.com/microsoft/uf2) designed by Microsoft.
uf2: uf2.Options,
Expand Down
1 change: 1 addition & 0 deletions build-internals/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.regz = .{ .path = "../tools/regz" },
.uf2 = .{ .path = "../tools/uf2" },
.@"esp-image" = .{ .path = "../tools/esp-image" },
.dfu = .{ .path = "../tools/dfu" },
},
.paths = .{
"build.zig",
Expand Down
8 changes: 7 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,13 @@ pub fn MicroBuild(port_select: PortSelect) type {
options,
),

.dfu => @panic("DFU is not implemented yet. See https://github.com/ZigEmbeddedGroup/microzig/issues/145 for more details!"),
.dfu => |options| @import("tools/dfu").from_elf(
fw.mb.dep.builder.dependency("tools/dfu", .{
.optimize = .ReleaseSafe,
}),
elf_file,
options,
),

.esp => |options| @import("tools/esp-image").from_elf(
fw.mb.dep.builder.dependency("tools/esp-image", .{
Expand Down
1 change: 1 addition & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
.@"tools/printer" = .{ .path = "tools/printer" },
.@"tools/regz" = .{ .path = "tools/regz" },
.@"tools/uf2" = .{ .path = "tools/uf2" },
.@"tools/dfu" = .{ .path = "tools/dfu" },

// modules
.@"modules/foundation-libc" = .{ .path = "modules/foundation-libc" },
Expand Down
106 changes: 106 additions & 0 deletions tools/dfu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# dfu

Device Firmware Upgrade (DFU) for your build.zig

This package provides tools for creating DFU files from ELF binaries or raw binary files. It supports two formats:

- **Standard DFU 1.1**: simple binary with suffix and CRC (use `bin2dfu`)
- **DfuSe**: STMicroelectronics format with address information for non-contiguous memory (use `elf2dfuse`)

See https://www.usb.org/sites/default/files/DFU_1.1.pdf for the DFU specification and https://rc.fdr.hu/UM0391.pdf for DfuSe.

# Usage

## With MicroZig

When using MicroZig, DFU generation is integrated into the build system. You can request a DFU file when calling `get_emitted_bin` or `install_firmware`:

```zig
const microzig = @import("microzig");

pub fn build(b: *Build) void {
// ...

const mz_dep = b.dependency("microzig", .{});
const mb = MicroBuild.init(b, mz_dep) orelse return;

const firmware = mb.add_firmware(b, .{
.name = "my_firmware",
.target = microzig.boards.stmicro.stm32.daisy_seed,
.optimize = optimize,
.root_source_file = b.path("src/main.zig"),
});

// Install DfuSe format (default)
firmware.install_firmware(b, .{ .format = .{ .dfu = .{} } });

// Or with custom options:
firmware.install_firmware(b, .{ .format = .{ .dfu = .{
.format = .dfuse,
.vendor_id = 0x0483,
.product_id = 0xDF11,
} } });

// Standard DFU format (from ELF, uses objcopy internally)
firmware.install_firmware(b, .{ .format = .{ .dfu = .{ .format = .standard } } });
}
```

## Standard build.zig usage

In build.zig:

```zig
const dfu = @import("dfu");

pub fn build(b: *Build) void {
// ...
const dfu_dep = b.dependency("dfu", .{});

// DfuSe format (default)
const dfu_file = dfu.from_elf(dfu_dep, elf_file, .{
.format = .dfuse,
.vendor_id = 0x0483,
.product_id = 0xDF11,
});
_ = b.addInstallFile(dfu_file, "bin/firmware.dfu");

// Standard DFU format (from ELF, uses objcopy internally)
const std_dfu_file = dfu.from_elf(dfu_dep, elf_file, .{
.format = .standard,
});
_ = b.addInstallFile(std_dfu_file, "bin/firmware.dfu");

// Standard DFU format (from binary)
const bin_dfu_file = dfu.from_bin(dfu_dep, bin_file, .{});
_ = b.addInstallFile(bin_dfu_file, "bin/firmware.dfu");
}
```

Execute tools manually:

```zig
pub fn build(b: *Build) void {
// ...

const dfu_dep = b.dependency("dfu", .{});

// elf2dfuse
const elf2dfuse_run = b.addRunArtifact(dfu_dep.artifact("elf2dfuse"));
elf2dfuse_run.addArgs(&.{ "--vendor-id", "0x0483" });
elf2dfuse_run.addArgs(&.{ "--product-id", "0xDF11" });
elf2dfuse_run.addArg("--elf-path");
elf2dfuse_run.addArtifactArg(exe);
elf2dfuse_run.addArg("--output-path");
const dfu_file = elf2dfuse_run.addOutputFileArg("firmware.dfu");
_ = b.addInstallFile(dfu_file, "bin/firmware.dfu");

// bin2dfu
const bin2dfu_run = b.addRunArtifact(dfu_dep.artifact("bin2dfu"));
bin2dfu_run.addArg("--bin-path");
bin2dfu_run.addFileArg(bin_file);
bin2dfu_run.addArg("--output-path");
const std_dfu_file = bin2dfu_run.addOutputFileArg("firmware.dfu");
_ = b.addInstallFile(std_dfu_file, "bin/firmware.dfu");
}
```
104 changes: 104 additions & 0 deletions tools/dfu/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const std = @import("std");
const Dependency = std.Build.Dependency;

pub const Format = @import("src/dfu.zig").Format;
pub const Options = @import("src/dfu.zig").Options;

/// Create a DFU file from an ELF file.
/// For DfuSe format, converts ELF directly.
/// For standard DFU format, uses objcopy to extract binary first.
pub fn from_elf(dep: *Dependency, elf_file: std.Build.LazyPath, opts: Options) std.Build.LazyPath {
const b = dep.builder;

return switch (opts.format) {
.dfuse => blk: {
const elf2dfuse = dep.artifact("elf2dfuse");
const run = b.addRunArtifact(elf2dfuse);

run.addArgs(&.{ "--vendor-id", b.fmt("0x{x:0>4}", .{opts.vendor_id}) });
run.addArgs(&.{ "--product-id", b.fmt("0x{x:0>4}", .{opts.product_id}) });
run.addArgs(&.{ "--device-id", b.fmt("0x{x:0>4}", .{opts.device_id}) });
run.addArg("--elf-path");
run.addFileArg(elf_file);
run.addArg("--output-path");
break :blk run.addOutputFileArg("output.dfu");
},
.standard => blk: {
// Use objcopy to extract binary from ELF
const objcopy = b.addObjCopy(elf_file, .{
.format = .bin,
});
const bin_file = objcopy.getOutput();

// Then convert binary to DFU
break :blk from_bin(dep, bin_file, opts);
},
};
}

/// Create a standard DFU file from a raw binary file.
pub fn from_bin(dep: *Dependency, bin_file: std.Build.LazyPath, opts: Options) std.Build.LazyPath {
const b = dep.builder;
const bin2dfu = dep.artifact("bin2dfu");
const run = b.addRunArtifact(bin2dfu);

run.addArgs(&.{ "--vendor-id", b.fmt("0x{x:0>4}", .{opts.vendor_id}) });
run.addArgs(&.{ "--product-id", b.fmt("0x{x:0>4}", .{opts.product_id}) });
run.addArgs(&.{ "--device-id", b.fmt("0x{x:0>4}", .{opts.device_id}) });
run.addArg("--bin-path");
run.addFileArg(bin_file);
run.addArg("--output-path");
return run.addOutputFileArg("output.dfu");
}

pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOptions(.{});

const dfu_mod = b.addModule("dfu", .{
.root_source_file = b.path("src/dfu.zig"),
});

const dfuse_mod = b.createModule(.{
.root_source_file = b.path("src/dfuse.zig"),
.imports = &.{
.{ .name = "dfu", .module = dfu_mod },
},
});

const elf2dfuse = b.addExecutable(.{
.name = "elf2dfuse",
.root_module = b.createModule(.{
.root_source_file = b.path("src/elf2dfuse.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "dfuse", .module = dfuse_mod },
},
}),
});
b.installArtifact(elf2dfuse);

const bin2dfu = b.addExecutable(.{
.name = "bin2dfu",
.root_module = b.createModule(.{
.root_source_file = b.path("src/bin2dfu.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "dfu", .module = dfu_mod },
},
}),
});
b.installArtifact(bin2dfu);

const main_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/dfu.zig"),
.target = target,
}),
});
const run_tests = b.addRunArtifact(main_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_tests.step);
}
10 changes: 10 additions & 0 deletions tools/dfu/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.{
.name = .mz_tools_dfu,
.fingerprint = 0x4efc005d2395ba30,
.version = "0.0.0",
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}
99 changes: 99 additions & 0 deletions tools/dfu/src/bin2dfu.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const std = @import("std");
const dfu = @import("dfu");

const usage =
\\bin2dfu [options] --bin-path <path> --output-path <path>
\\
\\Creates a standard DFU 1.1 file from a raw binary file.
\\
\\Options:
\\ --vendor-id <vid> USB Vendor ID in hex (default: 0x0483)
\\ --product-id <pid> USB Product ID in hex (default: 0xDF11)
\\ --device-id <did> Device version in hex (default: 0xFFFF)
\\ --bin-path <path> Path to input binary file (required)
\\ --output-path <path> Path to output DFU file (required)
\\ --help Show this message
\\
;

fn find_arg(args: []const []const u8, key: []const u8) !?[]const u8 {
const key_idx = for (args, 0..) |arg, i| {
if (std.mem.eql(u8, key, arg))
break i;
} else return null;

if (key_idx >= args.len - 1) {
std.log.err("missing value for {s}", .{key});
return error.MissingArgValue;
}

const value = args[key_idx + 1];
if (std.mem.startsWith(u8, value, "--")) {
std.log.err("missing value for {s}", .{key});
return error.MissingArgValue;
}

return value;
}

var input_reader_buf: [4096]u8 = undefined;

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

const args = try std.process.argsAlloc(gpa.allocator());
defer std.process.argsFree(gpa.allocator(), args);

for (args) |arg| if (std.mem.eql(u8, "--help", arg)) {
var writer = std.fs.File.stdout().writer(&.{});
try writer.interface.writeAll(usage);
return;
};

const bin_path = (try find_arg(args, "--bin-path")) orelse {
std.log.err("missing arg: --bin-path", .{});
return error.MissingArg;
};

const output_path = (try find_arg(args, "--output-path")) orelse {
std.log.err("missing arg: --output-path", .{});
return error.MissingArg;
};

var opts: dfu.Options = .{};

if (try find_arg(args, "--vendor-id")) |vid_str| {
opts.vendor_id = std.fmt.parseInt(u16, vid_str, 0) catch {
std.log.err("invalid vendor id: {s}", .{vid_str});
return error.InvalidVendorId;
};
}

if (try find_arg(args, "--product-id")) |pid_str| {
opts.product_id = std.fmt.parseInt(u16, pid_str, 0) catch {
std.log.err("invalid product id: {s}", .{pid_str});
return error.InvalidProductId;
};
}

if (try find_arg(args, "--device-id")) |did_str| {
opts.device_id = std.fmt.parseInt(u16, did_str, 0) catch {
std.log.err("invalid device id: {s}", .{did_str});
return error.InvalidDeviceId;
};
}

const bin_file = try std.fs.cwd().openFile(bin_path, .{});
defer bin_file.close();

var bin_reader = bin_file.reader(&input_reader_buf);

const dest_file = try std.fs.cwd().createFile(output_path, .{});
defer dest_file.close();

// Unbuffered because dfu uses a hashed writer, which suggests
// using an unbuffered underlying writer
var writer = dest_file.writer(&.{});
try dfu.from_bin(&bin_reader.interface, &writer.interface, opts);
}
Loading