diff --git a/ravedude/Cargo.lock b/ravedude/Cargo.lock index 08d673366e..5cec5e46de 100644 --- a/ravedude/Cargo.lock +++ b/ravedude/Cargo.lock @@ -241,6 +241,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "goblin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -310,6 +321,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + [[package]] name = "mach" version = "0.1.2" @@ -381,6 +398,12 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -461,6 +484,7 @@ dependencies = [ "ctrlc", "either", "git-version", + "goblin", "serde", "serialport", "tempfile", @@ -502,6 +526,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "serde" version = "1.0.197" diff --git a/ravedude/Cargo.toml b/ravedude/Cargo.toml index 9644255d99..3e12b89074 100644 --- a/ravedude/Cargo.toml +++ b/ravedude/Cargo.toml @@ -21,3 +21,4 @@ serde = { version = "1.0.197", features = ["serde_derive"] } toml = "0.8.11" either = "1.10.0" clap = { version = "4.0.0", features = ["derive", "env"] } +goblin = "0.9.3" diff --git a/ravedude/src/config.rs b/ravedude/src/config.rs index 8e688f22ff..e0c22bc9d6 100644 --- a/ravedude/src/config.rs +++ b/ravedude/src/config.rs @@ -34,7 +34,7 @@ where } impl RavedudeConfig { - pub fn from_args(args: &crate::Args) -> anyhow::Result { + pub fn from_args(args: &crate::BoardArgs) -> anyhow::Result { Ok(Self { general_options: RavedudeGeneralConfig { open_console: args.open_console, @@ -67,7 +67,7 @@ pub struct RavedudeGeneralConfig { impl RavedudeGeneralConfig { /// Apply command-line overrides to this configuration. Command-line arguments take priority over Ravedude.toml - pub fn apply_overrides_from(&mut self, args: &crate::Args) -> anyhow::Result<()> { + pub fn apply_overrides_from(&mut self, args: &crate::BoardArgs) -> anyhow::Result<()> { if args.open_console { self.open_console = true; } diff --git a/ravedude/src/main.rs b/ravedude/src/main.rs index 28ce389b8d..7fcf0096a0 100644 --- a/ravedude/src/main.rs +++ b/ravedude/src/main.rs @@ -92,7 +92,9 @@ //! //! For reference, take a look at [`boards.toml`](https://github.com/Rahix/avr-hal/blob/main/ravedude/src/boards.toml). use anyhow::Context as _; +use clap::Parser; use colored::Colorize as _; +use std::num::NonZero; use std::path::Path; use std::thread; @@ -102,11 +104,22 @@ mod avrdude; mod board; mod config; mod console; +mod target_detect; mod ui; /// This represents the minimum (Major, Minor) version raverdude requires avrdude to meet. const MIN_VERSION_AVRDUDE: (u8, u8) = (6, 3); +// ravedude has two subcommands: the `board` subcommand, which flashes a binary to a board, and a +// `chip` command, which flashes a binary to a chip. The differ in that +// * The `chip` command determines the target chip from the elf binary metadata, where as the +// `board` command requires that you specify the board by using a TOML config file +// * The `chip` command allows more of its options to be specified through environment variables, +// whereas the board command reads its options from the TOML config and command-line arguments + +// A unification between the two is desirable, but I am keeping them separate initially in order to +// maintain backwards compatibility in `board` while doing new development in `chip`. + /// ravedude is a rust wrapper around avrdude for providing the smoothest possible development /// experience with rust on AVR microcontrollers. /// @@ -120,6 +133,21 @@ const MIN_VERSION_AVRDUDE: (u8, u8) = (6, 3); fallback = "unknown" ))] struct Args { + #[command(subcommand)] + subcommand: Subcommand, +} + +#[derive(clap::Parser, Debug)] +enum Subcommand { + Board(BoardArgs), + Chip(ChipArgs), +} + +/// High-level flashing command. Use this if you are working on a standalone binary that will be +/// flashed to a specific off-the shelf board, such as Arduino Uno, Adafruit Trinket, or Arduino Pro +/// Mini. +#[derive(clap::Parser, Debug)] +struct BoardArgs { /// Utility flag for dumping a config of a named board to TOML. #[clap(long = "dump-config")] dump_config: bool, @@ -160,7 +188,73 @@ struct Args { #[clap(name = "LEGACY BINARY", value_parser)] bin_legacy: Option, } -impl Args { + +#[derive(clap::Parser, clap::ValueEnum, Debug, Clone)] +enum ConsoleMode { + /// Do not connect to the chip after flashing the binary + None, + /// Connect to the chip using a plain serial console + Plain, +} + +impl std::fmt::Display for ConsoleMode { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ConsoleMode::None => write!(f, "none"), + ConsoleMode::Plain => write!(f, "plain"), + } + } +} + +/// Low-level flashing command. Use this if you are flashing to a bare chip or a chip on a custom board. +#[derive(clap::Parser, Debug)] +struct ChipArgs { + /// The name of the AVR programmer you are using. Run `avrdude "-c?"` for the full list. + #[clap(long = "programmer", env = "AVR_PROGRAMMER_NAME")] + programmer_name: Option, + + /// The serial port used to connect to the programmer. Autodetected when possible if unspecified. + #[clap(long = "programmer-port", env = "AVR_PROGRAMMER_PORT", value_parser)] + programmer_port: Option, + + /// The baudrate used to connect to the programmer. Autodetected when possible if unspecified. + #[clap(long = "programmer-baudrate", env = "AVR_PROGRAMMER_BAUDRATE")] + programmer_baudrate: Option>, + + /// If set, erase the target before flashing. + #[clap( + long = "erase-target", + env = "AVR_PROGRAMMER_ERASE_TARGET", + default_value_t = true + )] + erase_target: bool, + + /// Serial connection mode, used to connect to the chip after flashing the binary + #[clap( + long = "console-mode", + env = "AVR_CONSOLE_MODE", + default_value_t = ConsoleMode::None + )] + console_mode: ConsoleMode, + + /// The serial port used to connect to the chip after flashing, if different from the programmer serial port. + #[clap(long = "console-port", env = "AVR_CONSOLE_PORT", value_parser)] + console_port: Option, + + /// The baudrate used to connect to the chip after flashing, if different from the programmer baudrate. + #[clap(short = 'b', long = "console-baudrate", env = "AVR_CONSOLE_BAUDRATE")] + console_baudrate: Option>, + + /// Print verbose information + #[clap(short = 'v', long = "verbose")] + verbose: bool, + + #[clap(value_parser)] + /// The binary to be flashed. + binary: std::path::PathBuf, +} + +impl BoardArgs { /// Get the board name for legacy configurations. /// `None` if the configuration isn't a legacy configuration or the board name doesn't exist. fn legacy_board_name(&self) -> Option { @@ -216,8 +310,27 @@ fn find_manifest() -> anyhow::Result> { } fn ravedude() -> anyhow::Result<()> { - let args: Args = clap::Parser::parse(); + let mut args: Args = match Args::try_parse() { + Ok(args) => args, + Err(_) => { + // If no command is specified, try parsing as just `BoardArgs` + match BoardArgs::try_parse() { + Ok(boardArgs) => Args { + subcommand: Subcommand::Board(boardArgs), + }, + // If `BoardArgs` parsing doesn't work either, go back to parsing as `Args` to force the default help to be printed + Err(_) => Args::parse(), + } + } + }; + + match &mut args.subcommand { + Subcommand::Board(args) => ravedude_board(args), + Subcommand::Chip(ref mut args) => ravedude_chip(args), + } +} +fn ravedude_board(args: &BoardArgs) -> anyhow::Result<()> { let manifest_path = find_manifest()?; let mut ravedude_config = match (manifest_path.as_deref(), args.legacy_board_name()) { @@ -346,3 +459,66 @@ fn ravedude() -> anyhow::Result<()> { Ok(()) } + +fn ravedude_chip(args: &mut ChipArgs) -> anyhow::Result<()> { + avrdude::Avrdude::require_min_ver(MIN_VERSION_AVRDUDE)?; + + // Some programmers require an explicit port, and we could hardcode that here for a better error message. + if let Some(port) = args.programmer_port.as_ref() { + task_message!( + "Programming", + "{} {} {}", + args.binary.display(), + "=>".blue().bold(), + port.display() + ); + } else { + task_message!("Programming", "{}", args.binary.display(),); + } + + let target = target_detect::target_name_from_binary(&args.binary)?; + + let avrdude_options = config::BoardAvrdudeOptions { + programmer: args.programmer_name.clone(), + partno: Some(target), + baudrate: Some(args.programmer_baudrate), + do_chip_erase: Some(args.erase_target), + }; + + let mut avrdude = avrdude::Avrdude::run( + &avrdude_options, + args.programmer_port.as_ref(), + &args.binary, + args.verbose, + )?; + avrdude.wait()?; + + task_message!("Programmed", "{}", args.binary.display()); + + match args.console_mode { + ConsoleMode::None => Ok(()), + ConsoleMode::Plain => { + let port = args.console_port.take().or(args.programmer_port.take()); + let baudrate = args + .console_baudrate + .take() + .or(args.programmer_baudrate.take()); + + match (port, baudrate) { + (Some(port), Some(baudrate)) => { + task_message!("Console", "{} at {} baud", port.display(), baudrate); + task_message!("", "{}", "CTRL+C to exit.".dimmed()); + // Empty line for visual consistency + eprintln!(); + console::open(&port, baudrate.get()) + } + + _ => Err(anyhow::anyhow!( + "Console port and baudrate have to be specified." + )), + } + } + }?; + + Ok(()) +} diff --git a/ravedude/src/target_detect.rs b/ravedude/src/target_detect.rs new file mode 100644 index 0000000000..c6a1e659ca --- /dev/null +++ b/ravedude/src/target_detect.rs @@ -0,0 +1,42 @@ +use std::ffi::{c_char, CStr}; +use std::marker::PhantomData; +use std::mem::offset_of; +use std::path::Path; + +use anyhow::Result; + +use goblin::elf::Elf; + +#[repr(C, packed)] +#[derive(Debug)] +struct AvrDeviceInfoDesc<'a> { + flash_start: u32, + flash_size: u32, + sram_start: u32, + sram_size: u32, + eeprom_start: u32, + eeprom_size: u32, + offset_table_size: u32, + offset_table: [u32; 1], + strtab: PhantomData<&'a [u8]>, +} + +// https://avrdudes.github.io/avr-libc/avr-libc-user-manual/mem_sections.html#sec_dot_note +pub fn target_name_from_binary(binary: impl AsRef) -> Result { + let file_data = std::fs::read(binary)?; + let note = Elf::parse(&file_data)? + .iter_note_sections(&file_data, Some(".note.gnu.avr.deviceinfo")) + .map(|mut it| it.nth(0)) + .flatten() + .transpose()? + .ok_or_else(|| anyhow::anyhow!("AVR device info section not found"))?; + + let device_info_p = note.desc.as_ptr() as *const AvrDeviceInfoDesc; + let device_info = unsafe { &*device_info_p }; + let device_name_offset = + offset_of!(AvrDeviceInfoDesc, strtab) as isize + device_info.offset_table[0] as isize; + let device_name_p = unsafe { note.desc.as_ptr().offset(device_name_offset) } as *const c_char; + let device_name = unsafe { CStr::from_ptr(device_name_p) }.to_str()?; + + Ok(device_name.to_string()) +}