Skip to content

Commit cddf56e

Browse files
committed
feat(test_runner): connect to the RP2040 test driver serial interface
1 parent bbc2f58 commit cddf56e

File tree

4 files changed

+155
-7
lines changed

4 files changed

+155
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/r3_port_arm_m_test_driver/src/board_rp2040.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ impl usbstdio::Options for Options {
103103
}
104104
}
105105
}
106+
107+
fn product_name() -> &'static str {
108+
"R3 Test Driver Port"
109+
}
106110
}
107111

108112
#[repr(C)]

src/r3_test_runner/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ lazy_static = "1.4.0"
1818
env_logger = "0.7.1"
1919
mio-serial = "3.3.1"
2020
serde_json = "1.0.57"
21+
serialport = "3.3.0"
2122
itertools = "0.9.0"
2223
structopt = "0.3.15"
2324
thiserror = "1.0.20"

src/r3_test_runner/src/targets/rp_pico.rs

Lines changed: 149 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
//! Raspberry Pi Pico testing support
2+
//!
3+
//! This test runner target module communicates with the target through one USB
4+
//! connection. The target side is RP2040 bootrom's PICOBOOT interface if the
5+
//! target is in BOOTSEL mode or the test driver serial interface if the test
6+
//! driver is currently running. It uses the PICOBOOT interface to transfer the
7+
//! test driver to the target's on-chip RAM. After the test driver completes
8+
//! execution, the test runner requests the test driver to reset the target into
9+
//! BOOTSEL mode, preparing for the next test run.
10+
//!
11+
//! # Prerequisites
12+
//!
13+
//! One Raspberry Pi Pico board or any compatible board. The USB port must be
14+
//! connected to the host computer. This test runner only uses the USB port to
15+
//! simplify the usage.
16+
//!
17+
//! The Pico must first be placed into BOOTSEL mode so that the test runner can
18+
//! load a program.
219
use anyhow::{anyhow, Context, Result};
320
use std::future::Future;
4-
use tokio::task::spawn_blocking;
21+
use tokio::{io::AsyncWriteExt, task::spawn_blocking, time::delay_for};
22+
use tokio_serial::{Serial, SerialPortSettings};
523

624
use super::{jlink::read_elf, Arch, DebugProbe, Target};
725
use crate::utils::retry_on_fail_with_delay;
@@ -36,13 +54,65 @@ impl Target for RaspberryPiPico {
3654
}
3755

3856
fn connect(&self) -> std::pin::Pin<Box<dyn Future<Output = Result<Box<dyn DebugProbe>>>>> {
39-
Box::pin(std::future::ready(Ok(
40-
Box::new(RaspberryPiPicoUsbDebugProbe) as _,
41-
)))
57+
Box::pin(retry_on_fail_with_delay(|| async {
58+
// Try connecting to the target. This is important if a test
59+
// driver is currently running because we have to reboot the
60+
// target before loading the new test driver.
61+
log::debug!("Attempting to connect to the target by two methods simultaneously.");
62+
let serial_async = spawn_blocking(open_serial);
63+
let picoboot_interface_async = spawn_blocking(open_picoboot);
64+
let (serial, picoboot_interface) = tokio::join!(serial_async, picoboot_interface_async);
65+
// ignore `JoinError`
66+
let (serial, picoboot_interface) = (serial.unwrap(), picoboot_interface.unwrap());
67+
68+
let serial = match (serial, picoboot_interface) {
69+
(Ok(serial), Err(e)) => {
70+
log::debug!(
71+
"Connected to a test driver serial interface. Connecting to \
72+
a PICOBOOT USB interface failed with the following error: {}",
73+
e
74+
);
75+
Some(serial)
76+
}
77+
(Err(e), Ok(_picoboot_interface)) => {
78+
log::debug!(
79+
"Connected to a PICOBOOT USB interface. Connecting to \
80+
a test driver serial interface failed with the following \
81+
error: {}",
82+
e
83+
);
84+
None
85+
}
86+
(Err(e1), Err(e2)) => anyhow::bail!(
87+
"Could not connect to any of a test driver \
88+
serial interface and a PICOBOOT USB interface. \n\
89+
\n\
90+
Serial interface error: {}\n\n\
91+
PICOBOOT interface error: {}",
92+
e1,
93+
e2,
94+
),
95+
(Ok(_), Ok(_)) => anyhow::bail!(
96+
"Connected to both of a test driver serial \
97+
interface and a PICOBOOT USB interface. \
98+
This is unexpected."
99+
),
100+
};
101+
102+
Ok(Box::new(RaspberryPiPicoUsbDebugProbe { serial }) as _)
103+
}))
42104
}
43105
}
44106

45-
struct RaspberryPiPicoUsbDebugProbe;
107+
struct RaspberryPiPicoUsbDebugProbe {
108+
/// Contains a handle to the serial port if the test driver is currently
109+
/// running.
110+
///
111+
/// Even if this field is set, the test driver's current state is
112+
/// indeterminate in general, so the target must be rebooted before doing
113+
/// anything meaningful.
114+
serial: Option<Serial>,
115+
}
46116

47117
impl DebugProbe for RaspberryPiPicoUsbDebugProbe {
48118
fn program_and_get_output(
@@ -51,21 +121,90 @@ impl DebugProbe for RaspberryPiPicoUsbDebugProbe {
51121
) -> std::pin::Pin<Box<dyn Future<Output = Result<super::DynAsyncRead<'_>>> + '_>> {
52122
let exe = exe.to_owned();
53123
Box::pin(async move {
124+
if let Some(mut serial) = self.serial.take() {
125+
// Reboot the target into BOOTSEL mode. This will sever the
126+
// serial connection.
127+
log::debug!(
128+
"We know that a test driver is currently running on the target. \
129+
We will request a reboot first."
130+
);
131+
serial.write_all(b"r").await.with_context(|| {
132+
"Could not send a command to the test driver serial interface."
133+
})?;
134+
135+
// Wait until the host operating system recognizes the USB device...
136+
delay_for(DEFAULT_PAUSE).await;
137+
}
138+
54139
program_and_run_by_picoboot(&exe).await.with_context(|| {
55140
format!(
56141
"Failed to execute the ELF ƒile '{}' on the target.",
57142
exe.display()
58143
)
59144
})?;
60145

61-
// TODO: Attach to the USB serial, give a 'go' signal, grab the output,
146+
// Wait until the host operating system recognizes the USB device...
147+
delay_for(DEFAULT_PAUSE).await;
148+
149+
self.serial = Some(
150+
retry_on_fail_with_delay(|| async { spawn_blocking(open_serial).await.unwrap() })
151+
.await
152+
.with_context(|| "Failed to connect to the test driver serial interface.")?,
153+
);
154+
155+
// TODO: give a 'go' signal, grab the output,
62156
// and then issue a reboot request by sending `r`
63157

64158
todo!()
65159
})
66160
}
67161
}
68162

163+
/// Locate and open the test driver serial interface. A test driver must be
164+
/// running for this function to succeed.
165+
fn open_serial() -> Result<Serial> {
166+
log::debug!("Looking for the test driver serial port");
167+
let ports = serialport::available_ports()?;
168+
let port = ports
169+
.iter()
170+
.find(|port_info| {
171+
log::trace!(" ...{:?}", port_info);
172+
173+
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
174+
matches!(
175+
port_info,
176+
SerialPortInfo {
177+
port_type: SerialPortType::UsbPort(UsbPortInfo {
178+
product: Some(product),
179+
..
180+
}),
181+
..
182+
}
183+
if product.contains("R3 Test Driver Port")
184+
) ||
185+
// FIXME: Apple M1 work-around
186+
// (`available_ports` returns incorrect `SerialPortType`)
187+
port_info.port_name.starts_with("/dev/tty.usbmodem")
188+
})
189+
.ok_or_else(|| anyhow!("Could not locate the test driver serial port."))?;
190+
log::debug!("Test driver serial port = {:?}", port);
191+
192+
// Open the serial port
193+
Serial::from_path(
194+
&port.port_name,
195+
&SerialPortSettings {
196+
timeout: DEFAULE_TIMEOUT,
197+
..Default::default()
198+
},
199+
)
200+
.with_context(|| {
201+
format!(
202+
"Could not open the test driver serial port at path '{}'.",
203+
port.port_name
204+
)
205+
})
206+
}
207+
69208
/// Program and execute the specified ELF file by PICOBOOT protocol.
70209
async fn program_and_run_by_picoboot(exe: &std::path::Path) -> Result<()> {
71210
let picoboot_interface_async = retry_on_fail_with_delay(|| async {
@@ -134,6 +273,8 @@ async fn program_and_run_by_picoboot(exe: &std::path::Path) -> Result<()> {
134273

135274
const DEFAULE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
136275

276+
const DEFAULT_PAUSE: std::time::Duration = std::time::Duration::from_secs(1);
277+
137278
async fn write_bulk_all(
138279
device_handle: rusb::DeviceHandle<rusb::GlobalContext>,
139280
endpoint: u8,
@@ -187,7 +328,8 @@ struct PicobootInterface {
187328
in_endpoint_i: u8,
188329
}
189330

190-
/// Locate the PICOBOOT interface.
331+
/// Locate and open the PICOBOOT interface. The device must be in BOOTSEL mode
332+
/// for this function to succeed.
191333
fn open_picoboot() -> Result<PicobootInterface> {
192334
// Locate the RP2040 bootrom device
193335
log::debug!("Looking for the RP2040 bootrom device");

0 commit comments

Comments
 (0)