Skip to content

Commit 84d7a7e

Browse files
authored
feat: add support for DFU and DfuSe tooling (#915)
Generate a standard DFU image or an STLink DfuSe image from the MicroZig build system. Tested by flashing the blinking LED example on a [Daisy Seed](https://electro-smith.com/products/daisy-seed) (the porting is still WIP so I will open a separate PR), and also checking that the resulting `.dfu` is bit-by-bit identical to the one generated by [this C tool](https://github.com/majbthrd/elf2dfuse) starting from the same ELF. Close #145
1 parent bd650e8 commit 84d7a7e

11 files changed

Lines changed: 764 additions & 2 deletions

File tree

build-internals/build.zig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub const Patch = regz.patch.Patch;
88
const uf2 = @import("uf2");
99
pub const FamilyId = uf2.FamilyId;
1010
const esp_image = @import("esp-image");
11+
const dfu = @import("dfu");
1112

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

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

221223
/// The [USB Flashing Format (UF2)](https://github.com/microsoft/uf2) designed by Microsoft.
222224
uf2: uf2.Options,

build-internals/build.zig.zon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
.regz = .{ .path = "../tools/regz" },
77
.uf2 = .{ .path = "../tools/uf2" },
88
.@"esp-image" = .{ .path = "../tools/esp-image" },
9+
.dfu = .{ .path = "../tools/dfu" },
910
},
1011
.paths = .{
1112
"build.zig",

build.zig

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,13 @@ pub fn MicroBuild(port_select: PortSelect) type {
678678
options,
679679
),
680680

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

683689
.esp => |options| @import("tools/esp-image").from_elf(
684690
fw.mb.dep.builder.dependency("tools/esp-image", .{

build.zig.zon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
.@"tools/printer" = .{ .path = "tools/printer" },
1515
.@"tools/regz" = .{ .path = "tools/regz" },
1616
.@"tools/uf2" = .{ .path = "tools/uf2" },
17+
.@"tools/dfu" = .{ .path = "tools/dfu" },
1718

1819
// modules
1920
.@"modules/foundation-libc" = .{ .path = "modules/foundation-libc" },

tools/dfu/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# dfu
2+
3+
Device Firmware Upgrade (DFU) for your build.zig
4+
5+
This package provides tools for creating DFU files from ELF binaries or raw binary files. It supports two formats:
6+
7+
- **Standard DFU 1.1**: simple binary with suffix and CRC (use `bin2dfu`)
8+
- **DfuSe**: STMicroelectronics format with address information for non-contiguous memory (use `elf2dfuse`)
9+
10+
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.
11+
12+
# Usage
13+
14+
## With MicroZig
15+
16+
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`:
17+
18+
```zig
19+
const microzig = @import("microzig");
20+
21+
pub fn build(b: *Build) void {
22+
// ...
23+
24+
const mz_dep = b.dependency("microzig", .{});
25+
const mb = MicroBuild.init(b, mz_dep) orelse return;
26+
27+
const firmware = mb.add_firmware(b, .{
28+
.name = "my_firmware",
29+
.target = microzig.boards.stmicro.stm32.daisy_seed,
30+
.optimize = optimize,
31+
.root_source_file = b.path("src/main.zig"),
32+
});
33+
34+
// Install DfuSe format (default)
35+
firmware.install_firmware(b, .{ .format = .{ .dfu = .{} } });
36+
37+
// Or with custom options:
38+
firmware.install_firmware(b, .{ .format = .{ .dfu = .{
39+
.format = .dfuse,
40+
.vendor_id = 0x0483,
41+
.product_id = 0xDF11,
42+
} } });
43+
44+
// Standard DFU format (from ELF, uses objcopy internally)
45+
firmware.install_firmware(b, .{ .format = .{ .dfu = .{ .format = .standard } } });
46+
}
47+
```
48+
49+
## Standard build.zig usage
50+
51+
In build.zig:
52+
53+
```zig
54+
const dfu = @import("dfu");
55+
56+
pub fn build(b: *Build) void {
57+
// ...
58+
const dfu_dep = b.dependency("dfu", .{});
59+
60+
// DfuSe format (default)
61+
const dfu_file = dfu.from_elf(dfu_dep, elf_file, .{
62+
.format = .dfuse,
63+
.vendor_id = 0x0483,
64+
.product_id = 0xDF11,
65+
});
66+
_ = b.addInstallFile(dfu_file, "bin/firmware.dfu");
67+
68+
// Standard DFU format (from ELF, uses objcopy internally)
69+
const std_dfu_file = dfu.from_elf(dfu_dep, elf_file, .{
70+
.format = .standard,
71+
});
72+
_ = b.addInstallFile(std_dfu_file, "bin/firmware.dfu");
73+
74+
// Standard DFU format (from binary)
75+
const bin_dfu_file = dfu.from_bin(dfu_dep, bin_file, .{});
76+
_ = b.addInstallFile(bin_dfu_file, "bin/firmware.dfu");
77+
}
78+
```
79+
80+
Execute tools manually:
81+
82+
```zig
83+
pub fn build(b: *Build) void {
84+
// ...
85+
86+
const dfu_dep = b.dependency("dfu", .{});
87+
88+
// elf2dfuse
89+
const elf2dfuse_run = b.addRunArtifact(dfu_dep.artifact("elf2dfuse"));
90+
elf2dfuse_run.addArgs(&.{ "--vendor-id", "0x0483" });
91+
elf2dfuse_run.addArgs(&.{ "--product-id", "0xDF11" });
92+
elf2dfuse_run.addArg("--elf-path");
93+
elf2dfuse_run.addArtifactArg(exe);
94+
elf2dfuse_run.addArg("--output-path");
95+
const dfu_file = elf2dfuse_run.addOutputFileArg("firmware.dfu");
96+
_ = b.addInstallFile(dfu_file, "bin/firmware.dfu");
97+
98+
// bin2dfu
99+
const bin2dfu_run = b.addRunArtifact(dfu_dep.artifact("bin2dfu"));
100+
bin2dfu_run.addArg("--bin-path");
101+
bin2dfu_run.addFileArg(bin_file);
102+
bin2dfu_run.addArg("--output-path");
103+
const std_dfu_file = bin2dfu_run.addOutputFileArg("firmware.dfu");
104+
_ = b.addInstallFile(std_dfu_file, "bin/firmware.dfu");
105+
}
106+
```

tools/dfu/build.zig

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const std = @import("std");
2+
const Dependency = std.Build.Dependency;
3+
4+
pub const Format = @import("src/dfu.zig").Format;
5+
pub const Options = @import("src/dfu.zig").Options;
6+
7+
/// Create a DFU file from an ELF file.
8+
/// For DfuSe format, converts ELF directly.
9+
/// For standard DFU format, uses objcopy to extract binary first.
10+
pub fn from_elf(dep: *Dependency, elf_file: std.Build.LazyPath, opts: Options) std.Build.LazyPath {
11+
const b = dep.builder;
12+
13+
return switch (opts.format) {
14+
.dfuse => blk: {
15+
const elf2dfuse = dep.artifact("elf2dfuse");
16+
const run = b.addRunArtifact(elf2dfuse);
17+
18+
run.addArgs(&.{ "--vendor-id", b.fmt("0x{x:0>4}", .{opts.vendor_id}) });
19+
run.addArgs(&.{ "--product-id", b.fmt("0x{x:0>4}", .{opts.product_id}) });
20+
run.addArgs(&.{ "--device-id", b.fmt("0x{x:0>4}", .{opts.device_id}) });
21+
run.addArg("--elf-path");
22+
run.addFileArg(elf_file);
23+
run.addArg("--output-path");
24+
break :blk run.addOutputFileArg("output.dfu");
25+
},
26+
.standard => blk: {
27+
// Use objcopy to extract binary from ELF
28+
const objcopy = b.addObjCopy(elf_file, .{
29+
.format = .bin,
30+
});
31+
const bin_file = objcopy.getOutput();
32+
33+
// Then convert binary to DFU
34+
break :blk from_bin(dep, bin_file, opts);
35+
},
36+
};
37+
}
38+
39+
/// Create a standard DFU file from a raw binary file.
40+
pub fn from_bin(dep: *Dependency, bin_file: std.Build.LazyPath, opts: Options) std.Build.LazyPath {
41+
const b = dep.builder;
42+
const bin2dfu = dep.artifact("bin2dfu");
43+
const run = b.addRunArtifact(bin2dfu);
44+
45+
run.addArgs(&.{ "--vendor-id", b.fmt("0x{x:0>4}", .{opts.vendor_id}) });
46+
run.addArgs(&.{ "--product-id", b.fmt("0x{x:0>4}", .{opts.product_id}) });
47+
run.addArgs(&.{ "--device-id", b.fmt("0x{x:0>4}", .{opts.device_id}) });
48+
run.addArg("--bin-path");
49+
run.addFileArg(bin_file);
50+
run.addArg("--output-path");
51+
return run.addOutputFileArg("output.dfu");
52+
}
53+
54+
pub fn build(b: *std.Build) void {
55+
const optimize = b.standardOptimizeOption(.{});
56+
const target = b.standardTargetOptions(.{});
57+
58+
const dfu_mod = b.addModule("dfu", .{
59+
.root_source_file = b.path("src/dfu.zig"),
60+
});
61+
62+
const dfuse_mod = b.createModule(.{
63+
.root_source_file = b.path("src/dfuse.zig"),
64+
.imports = &.{
65+
.{ .name = "dfu", .module = dfu_mod },
66+
},
67+
});
68+
69+
const elf2dfuse = b.addExecutable(.{
70+
.name = "elf2dfuse",
71+
.root_module = b.createModule(.{
72+
.root_source_file = b.path("src/elf2dfuse.zig"),
73+
.target = target,
74+
.optimize = optimize,
75+
.imports = &.{
76+
.{ .name = "dfuse", .module = dfuse_mod },
77+
},
78+
}),
79+
});
80+
b.installArtifact(elf2dfuse);
81+
82+
const bin2dfu = b.addExecutable(.{
83+
.name = "bin2dfu",
84+
.root_module = b.createModule(.{
85+
.root_source_file = b.path("src/bin2dfu.zig"),
86+
.target = target,
87+
.optimize = optimize,
88+
.imports = &.{
89+
.{ .name = "dfu", .module = dfu_mod },
90+
},
91+
}),
92+
});
93+
b.installArtifact(bin2dfu);
94+
95+
const main_tests = b.addTest(.{
96+
.root_module = b.createModule(.{
97+
.root_source_file = b.path("src/dfu.zig"),
98+
.target = target,
99+
}),
100+
});
101+
const run_tests = b.addRunArtifact(main_tests);
102+
const test_step = b.step("test", "Run tests");
103+
test_step.dependOn(&run_tests.step);
104+
}

tools/dfu/build.zig.zon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.{
2+
.name = .mz_tools_dfu,
3+
.fingerprint = 0x4efc005d2395ba30,
4+
.version = "0.0.0",
5+
.paths = .{
6+
"build.zig",
7+
"build.zig.zon",
8+
"src",
9+
},
10+
}

tools/dfu/src/bin2dfu.zig

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const std = @import("std");
2+
const dfu = @import("dfu");
3+
4+
const usage =
5+
\\bin2dfu [options] --bin-path <path> --output-path <path>
6+
\\
7+
\\Creates a standard DFU 1.1 file from a raw binary file.
8+
\\
9+
\\Options:
10+
\\ --vendor-id <vid> USB Vendor ID in hex (default: 0x0483)
11+
\\ --product-id <pid> USB Product ID in hex (default: 0xDF11)
12+
\\ --device-id <did> Device version in hex (default: 0xFFFF)
13+
\\ --bin-path <path> Path to input binary file (required)
14+
\\ --output-path <path> Path to output DFU file (required)
15+
\\ --help Show this message
16+
\\
17+
;
18+
19+
fn find_arg(args: []const []const u8, key: []const u8) !?[]const u8 {
20+
const key_idx = for (args, 0..) |arg, i| {
21+
if (std.mem.eql(u8, key, arg))
22+
break i;
23+
} else return null;
24+
25+
if (key_idx >= args.len - 1) {
26+
std.log.err("missing value for {s}", .{key});
27+
return error.MissingArgValue;
28+
}
29+
30+
const value = args[key_idx + 1];
31+
if (std.mem.startsWith(u8, value, "--")) {
32+
std.log.err("missing value for {s}", .{key});
33+
return error.MissingArgValue;
34+
}
35+
36+
return value;
37+
}
38+
39+
var input_reader_buf: [4096]u8 = undefined;
40+
41+
pub fn main() !void {
42+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
43+
defer _ = gpa.deinit();
44+
45+
const args = try std.process.argsAlloc(gpa.allocator());
46+
defer std.process.argsFree(gpa.allocator(), args);
47+
48+
for (args) |arg| if (std.mem.eql(u8, "--help", arg)) {
49+
var writer = std.fs.File.stdout().writer(&.{});
50+
try writer.interface.writeAll(usage);
51+
return;
52+
};
53+
54+
const bin_path = (try find_arg(args, "--bin-path")) orelse {
55+
std.log.err("missing arg: --bin-path", .{});
56+
return error.MissingArg;
57+
};
58+
59+
const output_path = (try find_arg(args, "--output-path")) orelse {
60+
std.log.err("missing arg: --output-path", .{});
61+
return error.MissingArg;
62+
};
63+
64+
var opts: dfu.Options = .{};
65+
66+
if (try find_arg(args, "--vendor-id")) |vid_str| {
67+
opts.vendor_id = std.fmt.parseInt(u16, vid_str, 0) catch {
68+
std.log.err("invalid vendor id: {s}", .{vid_str});
69+
return error.InvalidVendorId;
70+
};
71+
}
72+
73+
if (try find_arg(args, "--product-id")) |pid_str| {
74+
opts.product_id = std.fmt.parseInt(u16, pid_str, 0) catch {
75+
std.log.err("invalid product id: {s}", .{pid_str});
76+
return error.InvalidProductId;
77+
};
78+
}
79+
80+
if (try find_arg(args, "--device-id")) |did_str| {
81+
opts.device_id = std.fmt.parseInt(u16, did_str, 0) catch {
82+
std.log.err("invalid device id: {s}", .{did_str});
83+
return error.InvalidDeviceId;
84+
};
85+
}
86+
87+
const bin_file = try std.fs.cwd().openFile(bin_path, .{});
88+
defer bin_file.close();
89+
90+
var bin_reader = bin_file.reader(&input_reader_buf);
91+
92+
const dest_file = try std.fs.cwd().createFile(output_path, .{});
93+
defer dest_file.close();
94+
95+
// Unbuffered because dfu uses a hashed writer, which suggests
96+
// using an unbuffered underlying writer
97+
var writer = dest_file.writer(&.{});
98+
try dfu.from_bin(&bin_reader.interface, &writer.interface, opts);
99+
}

0 commit comments

Comments
 (0)