1
1
//! 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.
2
19
use anyhow:: { anyhow, Context , Result } ;
3
20
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 } ;
5
23
6
24
use super :: { jlink:: read_elf, Arch , DebugProbe , Target } ;
7
25
use crate :: utils:: retry_on_fail_with_delay;
@@ -36,13 +54,65 @@ impl Target for RaspberryPiPico {
36
54
}
37
55
38
56
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
+ } ) )
42
104
}
43
105
}
44
106
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
+ }
46
116
47
117
impl DebugProbe for RaspberryPiPicoUsbDebugProbe {
48
118
fn program_and_get_output (
@@ -51,21 +121,90 @@ impl DebugProbe for RaspberryPiPicoUsbDebugProbe {
51
121
) -> std:: pin:: Pin < Box < dyn Future < Output = Result < super :: DynAsyncRead < ' _ > > > + ' _ > > {
52
122
let exe = exe. to_owned ( ) ;
53
123
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
+
54
139
program_and_run_by_picoboot ( & exe) . await . with_context ( || {
55
140
format ! (
56
141
"Failed to execute the ELF ƒile '{}' on the target." ,
57
142
exe. display( )
58
143
)
59
144
} ) ?;
60
145
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,
62
156
// and then issue a reboot request by sending `r`
63
157
64
158
todo ! ( )
65
159
} )
66
160
}
67
161
}
68
162
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
+
69
208
/// Program and execute the specified ELF file by PICOBOOT protocol.
70
209
async fn program_and_run_by_picoboot ( exe : & std:: path:: Path ) -> Result < ( ) > {
71
210
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<()> {
134
273
135
274
const DEFAULE_TIMEOUT : std:: time:: Duration = std:: time:: Duration :: from_secs ( 5 ) ;
136
275
276
+ const DEFAULT_PAUSE : std:: time:: Duration = std:: time:: Duration :: from_secs ( 1 ) ;
277
+
137
278
async fn write_bulk_all (
138
279
device_handle : rusb:: DeviceHandle < rusb:: GlobalContext > ,
139
280
endpoint : u8 ,
@@ -187,7 +328,8 @@ struct PicobootInterface {
187
328
in_endpoint_i : u8 ,
188
329
}
189
330
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.
191
333
fn open_picoboot ( ) -> Result < PicobootInterface > {
192
334
// Locate the RP2040 bootrom device
193
335
log:: debug!( "Looking for the RP2040 bootrom device" ) ;
0 commit comments