diff --git a/.github/workflows/rpi.yml b/.github/workflows/rpi.yml index 824a149b..e3bd7a62 100644 --- a/.github/workflows/rpi.yml +++ b/.github/workflows/rpi.yml @@ -67,7 +67,7 @@ jobs: echo "CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV - name: Build binary - run: cargo build --release --all --target=${{ inputs.target }} + run: cargo build --release --all --target=${{ inputs.target }} --features=raspberry - uses: papeloto/action-zip@v1 with: diff --git a/Cargo.lock b/Cargo.lock index a8a985db..12030293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,6 +526,7 @@ dependencies = [ "miette", "parse_int", "regex", + "rppal", "serde", "serde-hex", "serde_json", @@ -1104,6 +1105,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "rppal" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c88c9c6248de4d337747b619d8f671055ef48a87dc21b97998833f189a0bbd4f" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/cargo-espflash/src/main.rs b/cargo-espflash/src/main.rs index b9007599..52e32cb6 100644 --- a/cargo-espflash/src/main.rs +++ b/cargo-espflash/src/main.rs @@ -222,7 +222,7 @@ fn flash( if args.flash_args.monitor { let pid = flasher.get_usb_pid()?; monitor( - flasher.into_serial(), + flasher.into_interface(), Some(&elf_data), pid, args.connect_args.monitor_baud.unwrap_or(115_200), diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml index 37e6a736..01bc4ada 100644 --- a/espflash/Cargo.toml +++ b/espflash/Cargo.toml @@ -53,6 +53,7 @@ md5 = "0.7.0" miette = { version = "5.3.0", features = ["fancy"] } parse_int = "0.6.0" regex = "1.6.0" +rppal = { version = "0.13", optional = true } serde = { version = "1.0.144", features = ["derive"] } serde-hex = "0.1.0" serde_json = "1.0.85" @@ -70,3 +71,4 @@ xmas-elf = "0.8.0" [features] default = ["cli"] cli = ["clap", "crossterm", "dialoguer", "update-informer"] +raspberry = ["rppal"] diff --git a/espflash/README.md b/espflash/README.md index b882a24a..8057cbb8 100644 --- a/espflash/README.md +++ b/espflash/README.md @@ -43,6 +43,12 @@ SUBCOMMANDS: write-bin Writes a binary file to a specific address in the chip's flash ``` +## Compile-time features + + - `raspberry`: enables configuring DTR and RTS GPIOs which are necessary to use a Raspberry Pi's + internal UART peripherals. This feature is optional (external USB <-> UART converters work + without it) and adds a dependency on [`rppal`](https://crates.io/crates/rppal). + ## Configuration You can also specify the serial port and/or expected VID/PID values by setting them in the configuration file. This file is in different locations depending on your operating system: diff --git a/espflash/src/bin/espflash.rs b/espflash/src/bin/espflash.rs index 4e832467..9d2a47ac 100644 --- a/espflash/src/bin/espflash.rs +++ b/espflash/src/bin/espflash.rs @@ -159,7 +159,7 @@ fn flash(mut args: FlashArgs, config: &Config) -> Result<()> { let pid = flasher.get_usb_pid()?; monitor( - flasher.into_serial(), + flasher.into_interface(), Some(&elf_data), pid, args.connect_args.monitor_baud.unwrap_or(115_200), diff --git a/espflash/src/cli/config.rs b/espflash/src/cli/config.rs index fe662cc7..44a726bd 100644 --- a/espflash/src/cli/config.rs +++ b/espflash/src/cli/config.rs @@ -19,6 +19,10 @@ pub struct Config { #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct Connection { pub serial: Option, + #[cfg(feature = "raspberry")] + pub rts: Option, + #[cfg(feature = "raspberry")] + pub dtr: Option, } #[derive(Debug, Deserialize, Serialize, Default, Clone)] diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index fc20a67e..da0b095c 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -11,15 +11,16 @@ use std::{ use clap::Args; use config::Config; use miette::{IntoDiagnostic, Result, WrapErr}; -use serialport::{FlowControl, SerialPortType, UsbPortInfo}; +use serialport::{SerialPortType, UsbPortInfo}; use strum::VariantNames; use crate::{ cli::{monitor::monitor, serial::get_serial_port_info}, elf::ElfFirmwareImage, - error::{Error, NoOtadataError}, + error::NoOtadataError, flasher::{FlashFrequency, FlashMode, FlashSize}, image_format::ImageFormatType, + interface::Interface, partition_table, Chip, Flasher, ImageFormatId, InvalidPartitionTable, MissingPartitionTable, PartitionTable, }; @@ -40,6 +41,17 @@ pub struct ConnectArgs { /// Serial port connected to target device #[clap(short = 'p', long)] pub port: Option, + + /// DTR pin to use for the internal UART hardware. Uses BCM numbering. + #[cfg(feature = "raspberry")] + #[cfg_attr(feature = "raspberry", clap(long))] + pub dtr: Option, + + /// RTS pin to use for the internal UART hardware. Uses BCM numbering. + #[cfg(feature = "raspberry")] + #[cfg_attr(feature = "raspberry", clap(long))] + pub rts: Option, + /// Use RAM stub for loading #[clap(long)] pub use_stub: bool, @@ -126,10 +138,8 @@ pub fn connect(args: &ConnectArgs, config: &Config) -> Result { // Attempt to open the serial port and set its initial baud rate. println!("Serial port: {}", port_info.port_name); println!("Connecting...\n"); - let serial = serialport::new(&port_info.port_name, 115_200) - .flow_control(FlowControl::None) - .open() - .map_err(Error::from) + + let interface = Interface::new(&port_info, args, config) .wrap_err_with(|| format!("Failed to open serial port {}", port_info.port_name))?; // NOTE: since `get_serial_port_info` filters out all non-USB serial ports, we @@ -150,7 +160,7 @@ pub fn connect(args: &ConnectArgs, config: &Config) -> Result { }; Ok(Flasher::connect( - serial, + interface, port_info, args.baud, args.use_stub, @@ -169,7 +179,7 @@ pub fn serial_monitor(args: ConnectArgs, config: &Config) -> Result<()> { let pid = flasher.get_usb_pid()?; monitor( - flasher.into_serial(), + flasher.into_interface(), None, pid, args.monitor_baud.unwrap_or(115_200), diff --git a/espflash/src/cli/monitor.rs b/espflash/src/cli/monitor.rs index 56e104e8..684cc003 100644 --- a/espflash/src/cli/monitor.rs +++ b/espflash/src/cli/monitor.rs @@ -1,5 +1,5 @@ use std::{ - io::{stdout, ErrorKind, Read, Write}, + io::{stdout, ErrorKind}, time::Duration, }; @@ -9,9 +9,8 @@ use crossterm::{ }; use espmonitor::{handle_serial, load_bin_context, SerialState}; use miette::{IntoDiagnostic, Result}; -use serialport::SerialPort; -use crate::connection::reset_after_flash; +use crate::{connection::reset_after_flash, interface::Interface}; /// Converts key events from crossterm into appropriate character/escape /// sequences which are then sent over the serial connection. @@ -84,7 +83,7 @@ impl Drop for RawModeGuard { } pub fn monitor( - mut serial: Box, + mut serial: Interface, elf: Option<&[u8]>, pid: u16, baud: u32, @@ -96,8 +95,10 @@ pub fn monitor( // Explicitly set the baud rate when starting the serial monitor, to allow using // different rates for flashing. - serial.set_baud_rate(baud)?; - serial.set_timeout(Duration::from_millis(5))?; + serial.serial_port_mut().set_baud_rate(baud)?; + serial + .serial_port_mut() + .set_timeout(Duration::from_millis(5))?; let _raw_mode = RawModeGuard::new(); @@ -109,12 +110,12 @@ pub fn monitor( serial_state = SerialState::new(symbols); } else { serial_state = SerialState::new(None); - reset_after_flash(&mut *serial, pid)?; + reset_after_flash(&mut serial, pid)?; } let mut buff = [0; 1024]; loop { - let read_count = match serial.read(&mut buff) { + let read_count = match serial.serial_port_mut().read(&mut buff) { Ok(count) => Ok(count), Err(e) if e.kind() == ErrorKind::TimedOut => Ok(0), Err(e) if e.kind() == ErrorKind::Interrupted => continue, @@ -131,7 +132,7 @@ pub fn monitor( match key.code { KeyCode::Char('c') => break, KeyCode::Char('r') => { - reset_after_flash(&mut *serial, pid)?; + reset_after_flash(&mut serial, pid)?; continue; } _ => {} @@ -139,8 +140,8 @@ pub fn monitor( } if let Some(bytes) = handle_key_event(key) { - serial.write_all(&bytes)?; - serial.flush()?; + serial.serial_port_mut().write_all(&bytes)?; + serial.serial_port_mut().flush()?; } } } diff --git a/espflash/src/connection.rs b/espflash/src/connection.rs index fbdfb363..dad07d30 100644 --- a/espflash/src/connection.rs +++ b/espflash/src/connection.rs @@ -1,18 +1,15 @@ -use std::{ - io::{BufWriter, Write}, - thread::sleep, - time::Duration, -}; +use std::{io::BufWriter, thread::sleep, time::Duration}; use binread::{io::Cursor, BinRead, BinReaderExt}; use bytemuck::{Pod, Zeroable}; -use serialport::{SerialPort, UsbPortInfo}; +use serialport::UsbPortInfo; use slip_codec::SlipDecoder; use crate::{ command::{Command, CommandType}, encoder::SlipEncoder, error::{ConnectionError, Error, ResultExt, RomError, RomErrorKind}, + interface::Interface, }; const DEFAULT_CONNECT_ATTEMPTS: usize = 7; @@ -29,7 +26,7 @@ pub struct CommandResponse { } pub struct Connection { - serial: Box, + serial: Interface, port_info: UsbPortInfo, decoder: SlipDecoder, } @@ -44,7 +41,7 @@ struct WriteRegParams { } impl Connection { - pub fn new(serial: Box, port_info: UsbPortInfo) -> Self { + pub fn new(serial: Interface, port_info: UsbPortInfo) -> Self { Connection { serial, port_info, @@ -119,7 +116,7 @@ impl Connection { pub fn reset(&mut self) -> Result<(), Error> { let pid = self.port_info.pid; - Ok(reset_after_flash(&mut *self.serial, pid)?) + Ok(reset_after_flash(&mut self.serial, pid)?) } pub fn reset_to_flash(&mut self, extra_delay: bool) -> Result<(), Error> { @@ -161,18 +158,18 @@ impl Connection { } pub fn set_timeout(&mut self, timeout: Duration) -> Result<(), Error> { - self.serial.set_timeout(timeout)?; + self.serial.serial_port_mut().set_timeout(timeout)?; Ok(()) } pub fn set_baud(&mut self, speed: u32) -> Result<(), Error> { - self.serial.set_baud_rate(speed)?; + self.serial.serial_port_mut().set_baud_rate(speed)?; Ok(()) } pub fn get_baud(&self) -> Result { - Ok(self.serial.baud_rate()?) + Ok(self.serial.serial_port().baud_rate()?) } pub fn with_timeout Result>( @@ -180,10 +177,17 @@ impl Connection { timeout: Duration, mut f: F, ) -> Result { - let old_timeout = self.serial.timeout(); - self.serial.set_timeout(timeout)?; + let old_timeout = { + let serial = self.serial.serial_port_mut(); + let old_timeout = serial.timeout(); + serial.set_timeout(timeout)?; + old_timeout + }; + let result = f(self); - self.serial.set_timeout(old_timeout)?; + + self.serial.serial_port_mut().set_timeout(old_timeout)?; + result } @@ -199,8 +203,10 @@ impl Connection { } pub fn write_command(&mut self, command: Command) -> Result<(), Error> { - self.serial.clear(serialport::ClearBuffer::Input)?; - let mut writer = BufWriter::new(&mut self.serial); + let serial = self.serial.serial_port_mut(); + + serial.clear(serialport::ClearBuffer::Input)?; + let mut writer = BufWriter::new(serial); let mut encoder = SlipEncoder::new(&mut writer)?; command.write(&mut encoder)?; encoder.finish()?; @@ -261,11 +267,11 @@ impl Connection { } pub fn flush(&mut self) -> Result<(), Error> { - self.serial.flush()?; + self.serial.serial_port_mut().flush()?; Ok(()) } - pub fn into_serial(self) -> Box { + pub fn into_interface(self) -> Interface { self.serial } @@ -274,7 +280,7 @@ impl Connection { } } -pub fn reset_after_flash(serial: &mut dyn SerialPort, pid: u16) -> Result<(), serialport::Error> { +pub fn reset_after_flash(serial: &mut Interface, pid: u16) -> Result<(), serialport::Error> { sleep(Duration::from_millis(100)); if pid == USB_SERIAL_JTAG_PID { diff --git a/espflash/src/error.rs b/espflash/src/error.rs index 6880f173..9b17f224 100644 --- a/espflash/src/error.rs +++ b/espflash/src/error.rs @@ -12,6 +12,7 @@ use crate::{ command::CommandType, flasher::{FlashFrequency, FlashMode, FlashSize}, image_format::ImageFormatId, + interface::SerialConfigError, partition_table::{CoreType, SubType, Type}, Chip, }; @@ -90,6 +91,12 @@ https://github.com/espressif/esp32c3-direct-boot-example" help("Make sure the correct device is connected to the host system") )] SerialNotFound(String), + #[error("Incorrect serial port configuration")] + #[diagnostic( + code(espflash::serial_config), + help("Make sure you have specified the DTR signal if you are using an internal UART peripherial") + )] + SerialConfiguration(SerialConfigError), #[error("Canceled by user")] Canceled, #[error("The flash mode '{0}' is not valid")] @@ -245,6 +252,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: SerialConfigError) -> Self { + Self::SerialConfiguration(err) + } +} + #[derive(Copy, Clone, Debug, Error, Diagnostic)] #[allow(dead_code)] #[repr(u8)] diff --git a/espflash/src/flasher.rs b/espflash/src/flasher.rs index 8bd7245e..f8d5433b 100644 --- a/espflash/src/flasher.rs +++ b/espflash/src/flasher.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, str::FromStr, thread::sleep}; use bytemuck::{Pod, Zeroable, __core::time::Duration}; use log::debug; -use serialport::{SerialPort, UsbPortInfo}; +use serialport::UsbPortInfo; use strum_macros::{Display, EnumVariantNames}; use crate::{ @@ -12,6 +12,7 @@ use crate::{ elf::{ElfFirmwareImage, FirmwareImage, RomSegment}, error::{ConnectionError, FlashDetectError, ResultExt}, image_format::ImageFormatId, + interface::Interface, stubs::FlashStub, Error, PartitionTable, }; @@ -277,7 +278,7 @@ pub struct Flasher { impl Flasher { pub fn connect( - serial: Box, + serial: Interface, port_info: UsbPortInfo, speed: Option, use_stub: bool, @@ -709,10 +710,6 @@ impl Flasher { Ok(()) } - pub fn into_serial(self) -> Box { - self.connection.into_serial() - } - pub fn get_usb_pid(&self) -> Result { self.connection.get_usb_pid() } @@ -728,6 +725,10 @@ impl Flasher { self.connection.flush()?; Ok(()) } + + pub fn into_interface(self) -> Interface { + self.connection.into_interface() + } } pub(crate) fn get_erase_size(offset: usize, size: usize) -> usize { diff --git a/espflash/src/interface.rs b/espflash/src/interface.rs new file mode 100644 index 00000000..2468eeae --- /dev/null +++ b/espflash/src/interface.rs @@ -0,0 +1,141 @@ +use std::io::Read; + +use crate::{cli::ConnectArgs, Config, Error}; +use miette::{Context, Result}; +use serialport::{FlowControl, SerialPort, SerialPortInfo}; + +#[cfg(feature = "raspberry")] +use rppal::gpio::{Gpio, OutputPin}; + +#[derive(thiserror::Error, Debug)] +pub enum SerialConfigError { + #[cfg(feature = "raspberry")] + #[error("You need to specify both DTR and RTS pins when using an internal UART peripheral")] + MissingDtrRtsForInternalUart, + + #[cfg(feature = "raspberry")] + #[error("GPIO {0} is not available")] + GpioUnavailable(u8), +} + +/// Wrapper around SerialPort where platform-specific modifications can be implemented. +pub struct Interface { + pub serial_port: Box, + #[cfg(feature = "raspberry")] + pub dtr: Option, + #[cfg(feature = "raspberry")] + pub rts: Option, +} + +#[cfg(feature = "raspberry")] +fn write_gpio(gpio: &mut OutputPin, level: bool) { + if level { + gpio.set_high(); + } else { + gpio.set_low(); + } +} + +fn open_port(port_info: &SerialPortInfo) -> Result> { + serialport::new(&port_info.port_name, 115_200) + .flow_control(FlowControl::None) + .open() + .map_err(Error::from) + .wrap_err_with(|| format!("Failed to open serial port {}", port_info.port_name)) +} + +impl Interface { + #[cfg(feature = "raspberry")] + pub(crate) fn new( + port_info: &SerialPortInfo, + args: &ConnectArgs, + config: &Config, + ) -> Result { + let rts_gpio = args.rts.or(config.connection.rts); + let dtr_gpio = args.dtr.or(config.connection.dtr); + + if port_info.port_type == serialport::SerialPortType::Unknown + && (dtr_gpio.is_none() || rts_gpio.is_none()) + { + // Assume internal UART, which has no DTR pin and usually no RTS either. + return Err(Error::from(SerialConfigError::MissingDtrRtsForInternalUart).into()); + } + + let gpios = Gpio::new().unwrap(); + + let rts = if let Some(gpio) = rts_gpio { + match gpios.get(gpio) { + Ok(pin) => Some(pin.into_output()), + Err(_) => return Err(Error::from(SerialConfigError::GpioUnavailable(gpio)).into()), + } + } else { + None + }; + + let dtr = if let Some(gpio) = dtr_gpio { + match gpios.get(gpio) { + Ok(pin) => Some(pin.into_output()), + Err(_) => return Err(Error::from(SerialConfigError::GpioUnavailable(gpio)).into()), + } + } else { + None + }; + + Ok(Self { + serial_port: open_port(port_info)?, + rts, + dtr, + }) + } + + #[cfg(not(feature = "raspberry"))] + pub(crate) fn new( + port_info: &SerialPortInfo, + _args: &ConnectArgs, + _config: &Config, + ) -> Result { + Ok(Self { + serial_port: open_port(port_info)?, + }) + } + + pub fn write_data_terminal_ready(&mut self, pin_state: bool) -> serialport::Result<()> { + #[cfg(feature = "raspberry")] + if let Some(gpio) = self.dtr.as_mut() { + write_gpio(gpio, pin_state); + return Ok(()); + } + + self.serial_port.write_data_terminal_ready(pin_state) + } + + pub fn write_request_to_send(&mut self, pin_state: bool) -> serialport::Result<()> { + #[cfg(feature = "raspberry")] + if let Some(gpio) = self.rts.as_mut() { + write_gpio(gpio, pin_state); + return Ok(()); + } + + self.serial_port.write_request_to_send(pin_state) + } + + pub fn into_serial(self) -> Box { + self.serial_port + } + + pub fn serial_port(&self) -> &dyn SerialPort { + self.serial_port.as_ref() + } + + pub fn serial_port_mut(&mut self) -> &mut dyn SerialPort { + self.serial_port.as_mut() + } +} + +// Note(dbuga): this impl is necessary because using `dyn SerialPort` as `dyn Read` +// requires trait_upcasting which isn't stable yet. +impl Read for Interface { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.serial_port.read(buf) + } +} diff --git a/espflash/src/lib.rs b/espflash/src/lib.rs index ed7e8d74..7ea3d2a5 100644 --- a/espflash/src/lib.rs +++ b/espflash/src/lib.rs @@ -19,6 +19,7 @@ pub mod error; pub mod flash_target; pub mod flasher; pub mod image_format; +pub mod interface; pub mod partition_table; pub mod stubs;