Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the ravedude chip command for bare MCU development #647

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
44 changes: 44 additions & 0 deletions ravedude/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ravedude/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 2 additions & 2 deletions ravedude/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ where
}

impl RavedudeConfig {
pub fn from_args(args: &crate::Args) -> anyhow::Result<Self> {
pub fn from_args(args: &crate::BoardArgs) -> anyhow::Result<Self> {
Ok(Self {
general_options: RavedudeGeneralConfig {
open_console: args.open_console,
Expand Down Expand Up @@ -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;
}
Expand Down
180 changes: 178 additions & 2 deletions ravedude/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
///
Expand All @@ -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,
Expand Down Expand Up @@ -160,7 +188,73 @@ struct Args {
#[clap(name = "LEGACY BINARY", value_parser)]
bin_legacy: Option<std::path::PathBuf>,
}
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<String>,

/// 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<std::path::PathBuf>,

/// The baudrate used to connect to the programmer. Autodetected when possible if unspecified.
#[clap(long = "programmer-baudrate", env = "AVR_PROGRAMMER_BAUDRATE")]
programmer_baudrate: Option<NonZero<u32>>,

/// 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<std::path::PathBuf>,

/// 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<NonZero<u32>>,

/// 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<String> {
Expand Down Expand Up @@ -216,8 +310,27 @@ fn find_manifest() -> anyhow::Result<Option<std::path::PathBuf>> {
}

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()) {
Expand Down Expand Up @@ -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(())
}
42 changes: 42 additions & 0 deletions ravedude/src/target_detect.rs
Original file line number Diff line number Diff line change
@@ -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<Path>) -> Result<String> {
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())
}
Loading