-
Notifications
You must be signed in to change notification settings - Fork 5
Home
Bmputil is a tool for managing firmware for the Black Magic Probe hardware. It arose to provide an all-in-one solution for use cases that various scripts and dfu-util have historically provided, in additional to custom functionality like firmware settings. At the time of this writing, Bmputil is far from complete, but it already provides an out of the box "It Just Works" experience for flashing firmware to Black Magic Probe devices. See Usage for details on that.
While precompiled binaries and installers are not yet available for download, Bmputil is designed to be simple to build from source. As Bmputil is written in Rust, it will require a Rust toolchain setup to compile from source. The instructions on their website should get you started. After that, you can install this project with the following commands in a terminal:
git clone https://github.com/blackmagic-debug/bmputil
cd bmputil
cargo install --path .
On Windows, installation is the same — setting up a Rust toolchain, and running the above commands — but with one more prerequisite. You will need to install the Windows Driver Kit 8.0 redistributable components (link from this page).
This project is designed with seamless cross compilation in mind. Cross compiling for Windows is actually the simplest, as cargo-xwin will set up most of a Windows toolchain for you, though you will need a clang
installation (MinGW-based toolchains do not work, as they fail to compile the external dependency libusb1-sys
). You will only need to download and extract the WDK 8.0 redistributable components to some directory, and set the WDK_DIR
environment variable to that directory. For example:
rustup target add x86_64-pc-windows-msvc
wget --content-disposition "https://go.microsoft.com/fwlink/p/?LinkID=253170"
msiextract wdfcoinstaller.msi
mkdir -p ~/.local/opt/wdk
mv "Program Files/Windows Kits/8.0" ~/.local/opt/wdk/8.0
export WDK_DIR=~/.local/opt/wdk/8.0
cargo xwin build --release --target=x86_64-pc-windows-msvc
For Linux and macOS targets, you will unfortunately need to setup a cross toolchain yourself. However with that done compiling Bmputil should be as simple as e.g. cargo build --release --target=aarch64-apple-darwin
.
For advanced details on how the cross compilation works for Windows targets behind the scenes, take a look at Internals.
To start, you can see if Bmputil is working correctly by running bmputil info
on the command line to display information about all detected Black Magic Probe devices. If you're on Windows and this is the first time you've run Bmputil then it will pop up a UAC prompt in order to install drivers for the Black Magic Probe device. If you want to know how the driver installation works behind the scenes, take a look at at Internals.
The most useful operation Bmputil can perform at the moment is flashing firmware to a Black Magic Probe device. You can download firmware binaries at https://github.com/blackmagic-debug/blackmagic/releases. Bmputil supports binaries in raw binary (.bin
) or ELF (.elf
) formats. After downloading one of those, flashing that firmware can be done with the bmputil flash <file>
command. For example, if you downloaded blackmagic-native-v1_8_2.elf
to your "Downloads" folder, you could use the following commands:
cd Downloads
bmputil flash blackmagic-native-v1_8_2.elf
The core of the logic is in src/bmp.rs
, in the struct BmpDevice
. The way these structs are typically constructed is a little involved, however. There is a simple fn from_usb_device(device: rusb::Device<rusb::Context>) -> Result<Self, Error>
constructor, but Bmputil is designed to be able to handle cases where multiple Black Magic Probes are plugged in to the same machine, and to allow the user to filter these devices with command-line arguments, so construction in the codebase often involves the wrapper struct BmpMatcher
. This struct stores a device index, serial number, and port (all optional), and uses those to filter the BMP devices that are attached to the machine. The method find_matching_probes()
then constructs all matching BMP devices it found with from_usb_device()
. The value that find_matching_probes()
returns is also a helper struct, BmpMatchResults
, which contains all found devices, what devices it filtered out, and what errors it encountered along the way,
The struct BmpMatchResults
also contains helper functions for getting the found devices out of its results and printing helpful warnings to the console based on the results, so the user isn't confused e.g by a DeviceNotFound error because they passed the wrong --serial
.
For example, to find all attached BMPs with the serial number "FOOBARDEADBEEF":
let matcher = BmpMatcher {
index: None,
serial: Some(String::from("FOOBARDEADBEEF")),
port: None,
};
// Will print a warning to the console if the serial number filtered out the only BMP(s) connected to the system,
// so the user hopefully isn't so confused by a DeviceNotFound error if they got the serial number wrong in `--serial`.
let found_probes = matcher.find_matching_probes().pop_all()?;
Once a BmpDevice
is constructed, flashing is done with the BmpDevice::download()
method. This requires passing the firmware type, which can be detected automatically with FirmwareType::detect_from_firmware()
, which parses the ARM vector table at the beginning of the firmware to determine where the binary is linked.
After download()
has finished, however, the physical device represented by the BmpDevice
struct will reboot. With the device rebooted, the OS handles BmpDevice
has will no longer be valid. self
is not consumed by download()
, but any further methods that require IO to the device will fail. You can use the free function wait_for_probe_reboot()
with the port string the device was on (which can be retrieved with BmpDevice::port()
) to re-create a BmpDevice
for the newly rebooted Black Magic Probe device.
Logic for handling the DFU protocol itself largely is handled by the dfu-core and dfu-libusb external library crates.
Though only code for the native Black Magic Probe platform is implemented at the time of this writing, the codebase has scaffolding for supporting multiple different Black Magic Probe platforms, largely encapsulated in the enum BmpPlatform
. Adding a new platform should roughly involve the following:
- Add a variant for the new platform to the
BmpPlatform
enum - Add two new constants for the VID/PID pairs of the platform to match
pub const NATIVE_RUNTIME_VID_PID
andpub const NATIVE_DFU_VID_PID
- Add match arms for the new platform VID/PIDs to the match expressions in the
BmpPlatform
functionsfrom_vid_pid()
,runtime_ids()
, anddfu_ids()
- Add a match arm to the match expression in
BmpPlatform::load_address()
to indicate where firmware is loaded for that platform
Unlike some Rust projects, Bmputil does not use a crate like anyhow for error construction and reporting. Instead, in src/error.rs
, there is an Error
struct and an associated ErrorKind
enum like Rust's standard library io
module. ErrorKind
attempts to divide errors into top-level categories primarily meant for consumption by the user — the user should be able to understand what the error at least means, even if they don't understand why they're getting it, and they should at least be able to know what part of the attempted operation failed. The error types don't hide information, though — sources of errors (i.e. OS IO calls) are conserved in Error
structs, and when reported to the user are printed as part of the error chain (and part of the backtrace, if compiled with a supported Rust version).
Because of the separation between Error
and ErrorKind
, error construction looks a little different than in some Rust projects, but care has been taken to ensure it's both convenient and readable.
To create an Error
with no source error (i.e. a semantic error that doesn't come from something like a failed syscall), use the .error()
method on the relevant ErrorKind
variant. For example, to indicate that a BMP device was not found, you may write:
return Err(ErrorKind::DeviceNotFound.error());
To create an Error
with a source error (such as a failed syscall or IO operation), use the .error_from()
method on the relevant ErrorKind
variant. For example:
let open_dev = || Err(std::io::Error::from(std::io::ErrorKind::TimedOut));
open_dev().map_err(|io_error| ErrorKind::DeviceNotFound.error_from(io_error))?;
Some ErrorKind
variants have a data field to give a little bit of extra information about what thing the error is relevant to. For example, FirmwareFileIo
definition is FirmwareFileIo(/** filename **/ Option<String>)
, allowing the programmer to indicate the name of the file that couldn't be read/written to/opened/etc. Ex:
let filename = String::from("firmware.elf");
let firm_file = std::fs::File::open(&filename)
.map_err(|file_err| ErrorKind::FirmwareFileIo(Some(filename)))?;
Finally, Error
s can have context, which is a string that indicates to the user the operation the error occurred in. To complete the above example:
let filename = String::from("firmware.elf");
let firm_file = std::fs::File::open(&filename)
.map_err(|file_err| ErrorKind::FirmwareFileIo(Some(filename)).error_from(file_err))
.map_err(|e| e.with_ctx("determining firmware kind"))?;
On stable, this prints:
Error: (while determining firmware kind): failed to read firmware file firmware.elf
Caused by: entity not found
note: recompile with nightly toolchain and run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
When constructing your errors, consider adding context so the user knows what step failed (and, if reasonable, why that step was being performed), and consider if the error has a source error that caused it. Anything with the bounds of E: std::error::Error + Send + Sync + 'static
can be passed to .error_from()
.
Compilation for Windows targets (whether you're on Windows or not) is internally a fair bit more complicated. This is largely because Bmputil on Windows depends on libwdi, and the Rust bindings crate also builds and statically links in libwdi as part of the build process. libusb-sys's build.rs
contains most of the complexity, which comes mostly from the following:
- libwdi's build process includes executable artifacts, which cc-rs does not support
- cargo-xwin, when cross compiling, also does not provide the linker arguments we need to link executables, so we have to infer them from the arguments we do get
- libwdi's build process also includes a host executable artifact, which must be executed to generate headers
- Some of libwdi's sources require patching to build under the conditions we need
- On macOS, absolute paths tend to be misinterpreted by
clang-cl
, as paths like/Users/foobar/some/path
get interpreted as MSVC's/U
switch- This one we solve with a slightly terrible hack of simply prepending a second
/
to absolute paths on macOS, becoming things like//Users/foobar/some/path
- This one we solve with a slightly terrible hack of simply prepending a second
libwdi-sys's build script is well commented, but here is an overview of the steps it takes:
- Completely copy all needed source files from the libwdi-sys submodule, patching the files that need to be patched (and converting the patch files from CRLF to LF if need be, to be independent of the users
core.autocrlf
Git config value) - If cross compiling, copy libwdi's
config.h
to a separate directory so it can be included for host builds without overriding host system headers - If cross compiling, grab the path to the WDK redistributable components from a
WDK_DIR
environment variable - Compile the host binary
embedder
by askingcc-rs
to do most of the work, and then converting thecc::Build
to astd::process::Command
and adding the necessary arguments - Compile the target binary
installer_x64.exe
, using the same method as above, also parsing out the Windows SDK path from what cargo-xwin has given us if necessary, so the target binary can be linked - Run the host binary created in step 4, which generates a header used in step 7
- Finally, compile
wdi.lib