aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorDario Nieuwenhuis <[email protected]>2023-07-28 11:58:07 +0000
committerGitHub <[email protected]>2023-07-28 11:58:07 +0000
commitb1242226494bfdeed13c2f80c5df239687a398ac (patch)
tree502932f0d25b988ff10910e122c21717386a2d8b /examples
parent8d8c642845987b1e47156fc60c0f105a747c09ee (diff)
parentd5f9d17b7c12c73cf16aa7414ce819bbd06efc2e (diff)
Merge pull request #1699 from mvniekerk/main
RP2040: PIO UART example
Diffstat (limited to 'examples')
-rw-r--r--examples/rp/src/bin/pio_uart.rs413
1 files changed, 413 insertions, 0 deletions
diff --git a/examples/rp/src/bin/pio_uart.rs b/examples/rp/src/bin/pio_uart.rs
new file mode 100644
index 000000000..ca1c7f394
--- /dev/null
+++ b/examples/rp/src/bin/pio_uart.rs
@@ -0,0 +1,413 @@
1//! This example shows how to use the PIO module in the RP2040 chip to implement a duplex UART.
2//! The PIO module is a very powerful peripheral that can be used to implement many different
3//! protocols. It is a very flexible state machine that can be programmed to do almost anything.
4//!
5//! This example opens up a USB device that implements a CDC ACM serial port. It then uses the
6//! PIO module to implement a UART that is connected to the USB serial port. This allows you to
7//! communicate with a device connected to the RP2040 over USB serial.
8
9#![no_std]
10#![no_main]
11#![feature(type_alias_impl_trait)]
12#![feature(async_fn_in_trait)]
13
14use defmt::{info, panic, trace};
15use embassy_executor::Spawner;
16use embassy_futures::join::{join, join3};
17use embassy_rp::bind_interrupts;
18use embassy_rp::peripherals::{PIO0, USB};
19use embassy_rp::pio::InterruptHandler as PioInterruptHandler;
20use embassy_rp::usb::{Driver, Instance, InterruptHandler};
21use embassy_sync::blocking_mutex::raw::NoopRawMutex;
22use embassy_sync::pipe::Pipe;
23use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State};
24use embassy_usb::driver::EndpointError;
25use embassy_usb::{Builder, Config};
26use embedded_io::asynch::{Read, Write};
27use {defmt_rtt as _, panic_probe as _};
28
29use crate::uart::PioUart;
30use crate::uart_rx::PioUartRx;
31use crate::uart_tx::PioUartTx;
32
33bind_interrupts!(struct Irqs {
34 USBCTRL_IRQ => InterruptHandler<USB>;
35 PIO0_IRQ_0 => PioInterruptHandler<PIO0>;
36});
37
38#[embassy_executor::main]
39async fn main(_spawner: Spawner) {
40 info!("Hello there!");
41
42 let p = embassy_rp::init(Default::default());
43
44 // Create the driver, from the HAL.
45 let driver = Driver::new(p.USB, Irqs);
46
47 // Create embassy-usb Config
48 let mut config = Config::new(0xc0de, 0xcafe);
49 config.manufacturer = Some("Embassy");
50 config.product = Some("PIO UART example");
51 config.serial_number = Some("12345678");
52 config.max_power = 100;
53 config.max_packet_size_0 = 64;
54
55 // Required for windows compatibility.
56 // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
57 config.device_class = 0xEF;
58 config.device_sub_class = 0x02;
59 config.device_protocol = 0x01;
60 config.composite_with_iads = true;
61
62 // Create embassy-usb DeviceBuilder using the driver and config.
63 // It needs some buffers for building the descriptors.
64 let mut device_descriptor = [0; 256];
65 let mut config_descriptor = [0; 256];
66 let mut bos_descriptor = [0; 256];
67 let mut control_buf = [0; 64];
68
69 let mut state = State::new();
70
71 let mut builder = Builder::new(
72 driver,
73 config,
74 &mut device_descriptor,
75 &mut config_descriptor,
76 &mut bos_descriptor,
77 &mut control_buf,
78 );
79
80 // Create classes on the builder.
81 let class = CdcAcmClass::new(&mut builder, &mut state, 64);
82
83 // Build the builder.
84 let mut usb = builder.build();
85
86 // Run the USB device.
87 let usb_fut = usb.run();
88
89 // PIO UART setup
90 let uart = PioUart::new(9600, p.PIO0, p.PIN_4, p.PIN_5);
91 let (mut uart_tx, mut uart_rx) = uart.split();
92
93 // Pipe setup
94 let usb_pipe: Pipe<NoopRawMutex, 20> = Pipe::new();
95 let mut usb_pipe_writer = usb_pipe.writer();
96 let mut usb_pipe_reader = usb_pipe.reader();
97
98 let uart_pipe: Pipe<NoopRawMutex, 20> = Pipe::new();
99 let mut uart_pipe_writer = uart_pipe.writer();
100 let mut uart_pipe_reader = uart_pipe.reader();
101
102 let (mut usb_tx, mut usb_rx) = class.split();
103
104 // Read + write from USB
105 let usb_future = async {
106 loop {
107 info!("Wait for USB connection");
108 usb_rx.wait_connection().await;
109 info!("Connected");
110 let _ = join(
111 usb_read(&mut usb_rx, &mut uart_pipe_writer),
112 usb_write(&mut usb_tx, &mut usb_pipe_reader),
113 )
114 .await;
115 info!("Disconnected");
116 }
117 };
118
119 // Read + write from UART
120 let uart_future = join(
121 uart_read(&mut uart_rx, &mut usb_pipe_writer),
122 uart_write(&mut uart_tx, &mut uart_pipe_reader),
123 );
124
125 // Run everything concurrently.
126 // If we had made everything `'static` above instead, we could do this using separate tasks instead.
127 join3(usb_fut, usb_future, uart_future).await;
128}
129
130struct Disconnected {}
131
132impl From<EndpointError> for Disconnected {
133 fn from(val: EndpointError) -> Self {
134 match val {
135 EndpointError::BufferOverflow => panic!("Buffer overflow"),
136 EndpointError::Disabled => Disconnected {},
137 }
138 }
139}
140
141/// Read from the USB and write it to the UART TX pipe
142async fn usb_read<'d, T: Instance + 'd>(
143 usb_rx: &mut Receiver<'d, Driver<'d, T>>,
144 uart_pipe_writer: &mut embassy_sync::pipe::Writer<'_, NoopRawMutex, 20>,
145) -> Result<(), Disconnected> {
146 let mut buf = [0; 64];
147 loop {
148 let n = usb_rx.read_packet(&mut buf).await?;
149 let data = &buf[..n];
150 trace!("USB IN: {:x}", data);
151 uart_pipe_writer.write(data).await;
152 }
153}
154
155/// Read from the USB TX pipe and write it to the USB
156async fn usb_write<'d, T: Instance + 'd>(
157 usb_tx: &mut Sender<'d, Driver<'d, T>>,
158 usb_pipe_reader: &mut embassy_sync::pipe::Reader<'_, NoopRawMutex, 20>,
159) -> Result<(), Disconnected> {
160 let mut buf = [0; 64];
161 loop {
162 let n = usb_pipe_reader.read(&mut buf).await;
163 let data = &buf[..n];
164 trace!("USB OUT: {:x}", data);
165 usb_tx.write_packet(&data).await?;
166 }
167}
168
169/// Read from the UART and write it to the USB TX pipe
170async fn uart_read(
171 uart_rx: &mut PioUartRx<'_>,
172 usb_pipe_writer: &mut embassy_sync::pipe::Writer<'_, NoopRawMutex, 20>,
173) -> ! {
174 let mut buf = [0; 64];
175 loop {
176 let n = uart_rx.read(&mut buf).await.expect("UART read error");
177 if n == 0 {
178 continue;
179 }
180 let data = &buf[..n];
181 trace!("UART IN: {:x}", buf);
182 usb_pipe_writer.write(data).await;
183 }
184}
185
186/// Read from the UART TX pipe and write it to the UART
187async fn uart_write(
188 uart_tx: &mut PioUartTx<'_>,
189 uart_pipe_reader: &mut embassy_sync::pipe::Reader<'_, NoopRawMutex, 20>,
190) -> ! {
191 let mut buf = [0; 64];
192 loop {
193 let n = uart_pipe_reader.read(&mut buf).await;
194 let data = &buf[..n];
195 trace!("UART OUT: {:x}", data);
196 let _ = uart_tx.write(&data).await;
197 }
198}
199
200mod uart {
201 use embassy_rp::peripherals::PIO0;
202 use embassy_rp::pio::{Pio, PioPin};
203 use embassy_rp::Peripheral;
204
205 use crate::uart_rx::PioUartRx;
206 use crate::uart_tx::PioUartTx;
207 use crate::Irqs;
208
209 pub struct PioUart<'a> {
210 tx: PioUartTx<'a>,
211 rx: PioUartRx<'a>,
212 }
213
214 impl<'a> PioUart<'a> {
215 pub fn new(
216 baud: u64,
217 pio: impl Peripheral<P = PIO0> + 'a,
218 tx_pin: impl PioPin,
219 rx_pin: impl PioPin,
220 ) -> PioUart<'a> {
221 let Pio {
222 mut common, sm0, sm1, ..
223 } = Pio::new(pio, Irqs);
224
225 let (tx, origin) = PioUartTx::new(&mut common, sm0, tx_pin, baud, None);
226 let (rx, _) = PioUartRx::new(&mut common, sm1, rx_pin, baud, Some(origin));
227
228 PioUart { tx, rx }
229 }
230
231 pub fn split(self) -> (PioUartTx<'a>, PioUartRx<'a>) {
232 (self.tx, self.rx)
233 }
234 }
235}
236
237mod uart_tx {
238 use core::convert::Infallible;
239
240 use embassy_rp::gpio::Level;
241 use embassy_rp::peripherals::PIO0;
242 use embassy_rp::pio::{Common, Config, Direction, FifoJoin, PioPin, ShiftDirection, StateMachine};
243 use embassy_rp::relocate::RelocatedProgram;
244 use embedded_io::asynch::Write;
245 use embedded_io::Io;
246 use fixed::traits::ToFixed;
247 use fixed_macro::types::U56F8;
248
249 pub struct PioUartTx<'a> {
250 sm_tx: StateMachine<'a, PIO0, 0>,
251 }
252
253 impl<'a> PioUartTx<'a> {
254 pub fn new(
255 common: &mut Common<'a, PIO0>,
256 mut sm_tx: StateMachine<'a, PIO0, 0>,
257 tx_pin: impl PioPin,
258 baud: u64,
259 origin: Option<u8>,
260 ) -> (Self, u8) {
261 let mut prg = pio_proc::pio_asm!(
262 r#"
263 .side_set 1 opt
264
265 ; An 8n1 UART transmit program.
266 ; OUT pin 0 and side-set pin 0 are both mapped to UART TX pin.
267
268 pull side 1 [7] ; Assert stop bit, or stall with line in idle state
269 set x, 7 side 0 [7] ; Preload bit counter, assert start bit for 8 clocks
270 bitloop: ; This loop will run 8 times (8n1 UART)
271 out pins, 1 ; Shift 1 bit from OSR to the first OUT pin
272 jmp x-- bitloop [6] ; Each loop iteration is 8 cycles.
273 "#
274 );
275 prg.program.origin = origin;
276 let tx_pin = common.make_pio_pin(tx_pin);
277 sm_tx.set_pins(Level::High, &[&tx_pin]);
278 sm_tx.set_pin_dirs(Direction::Out, &[&tx_pin]);
279
280 let relocated = RelocatedProgram::new(&prg.program);
281
282 let mut cfg = Config::default();
283
284 cfg.set_out_pins(&[&tx_pin]);
285 cfg.use_program(&common.load_program(&relocated), &[&tx_pin]);
286 cfg.shift_out.auto_fill = false;
287 cfg.shift_out.direction = ShiftDirection::Right;
288 cfg.fifo_join = FifoJoin::TxOnly;
289 cfg.clock_divider = (U56F8!(125_000_000) / (8 * baud)).to_fixed();
290 sm_tx.set_config(&cfg);
291 sm_tx.set_enable(true);
292
293 // The 4 state machines of the PIO each have their own program counter that starts taking
294 // instructions at an offset (origin) of the 32 instruction "space" the PIO device has.
295 // It is up to the programmer to sort out where to place these instructions.
296 // From the pio_asm! macro you get a ProgramWithDefines which has a field .program.origin
297 // which takes an Option<u8>.
298 //
299 // When you load more than one RelocatedProgram into the PIO,
300 // you load your first program at origin = 0.
301 // The RelocatedProgram has .code().count() which returns a usize,
302 // for which you can then use as your next program's origin.
303 let offset = relocated.code().count() as u8 + origin.unwrap_or_default();
304 (Self { sm_tx }, offset)
305 }
306
307 pub async fn write_u8(&mut self, data: u8) {
308 self.sm_tx.tx().wait_push(data as u32).await;
309 }
310 }
311
312 impl Io for PioUartTx<'_> {
313 type Error = Infallible;
314 }
315
316 impl Write for PioUartTx<'_> {
317 async fn write(&mut self, buf: &[u8]) -> Result<usize, Infallible> {
318 for byte in buf {
319 self.write_u8(*byte).await;
320 }
321 Ok(buf.len())
322 }
323 }
324}
325
326mod uart_rx {
327 use core::convert::Infallible;
328
329 use embassy_rp::gpio::Level;
330 use embassy_rp::peripherals::PIO0;
331 use embassy_rp::pio::{Common, Config, Direction, FifoJoin, PioPin, ShiftDirection, StateMachine};
332 use embassy_rp::relocate::RelocatedProgram;
333 use embedded_io::asynch::Read;
334 use embedded_io::Io;
335 use fixed::traits::ToFixed;
336 use fixed_macro::types::U56F8;
337
338 pub struct PioUartRx<'a> {
339 sm_rx: StateMachine<'a, PIO0, 1>,
340 }
341
342 impl<'a> PioUartRx<'a> {
343 pub fn new(
344 common: &mut Common<'a, PIO0>,
345 mut sm_rx: StateMachine<'a, PIO0, 1>,
346 rx_pin: impl PioPin,
347 baud: u64,
348 origin: Option<u8>,
349 ) -> (Self, u8) {
350 let mut prg = pio_proc::pio_asm!(
351 r#"
352 ; Slightly more fleshed-out 8n1 UART receiver which handles framing errors and
353 ; break conditions more gracefully.
354 ; IN pin 0 and JMP pin are both mapped to the GPIO used as UART RX.
355
356 start:
357 wait 0 pin 0 ; Stall until start bit is asserted
358 set x, 7 [10] ; Preload bit counter, then delay until halfway through
359 rx_bitloop: ; the first data bit (12 cycles incl wait, set).
360 in pins, 1 ; Shift data bit into ISR
361 jmp x-- rx_bitloop [6] ; Loop 8 times, each loop iteration is 8 cycles
362 jmp pin good_rx_stop ; Check stop bit (should be high)
363
364 irq 4 rel ; Either a framing error or a break. Set a sticky flag,
365 wait 1 pin 0 ; and wait for line to return to idle state.
366 jmp start ; Don't push data if we didn't see good framing.
367
368 good_rx_stop: ; No delay before returning to start; a little slack is
369 push ; important in case the TX clock is slightly too fast.
370 "#
371 );
372 prg.program.origin = origin;
373 let relocated = RelocatedProgram::new(&prg.program);
374 let mut cfg = Config::default();
375 cfg.use_program(&common.load_program(&relocated), &[]);
376
377 let rx_pin = common.make_pio_pin(rx_pin);
378 sm_rx.set_pins(Level::High, &[&rx_pin]);
379 cfg.set_in_pins(&[&rx_pin]);
380 cfg.set_jmp_pin(&rx_pin);
381 sm_rx.set_pin_dirs(Direction::In, &[&rx_pin]);
382
383 cfg.clock_divider = (U56F8!(125_000_000) / (8 * baud)).to_fixed();
384 cfg.shift_out.auto_fill = false;
385 cfg.shift_out.direction = ShiftDirection::Right;
386 cfg.fifo_join = FifoJoin::RxOnly;
387 sm_rx.set_config(&cfg);
388 sm_rx.set_enable(true);
389
390 let offset = relocated.code().count() as u8 + origin.unwrap_or_default();
391 (Self { sm_rx }, offset)
392 }
393
394 pub async fn read_u8(&mut self) -> u8 {
395 self.sm_rx.rx().wait_pull().await as u8
396 }
397 }
398
399 impl Io for PioUartRx<'_> {
400 type Error = Infallible;
401 }
402
403 impl Read for PioUartRx<'_> {
404 async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Infallible> {
405 let mut i = 0;
406 while i < buf.len() {
407 buf[i] = self.read_u8().await;
408 i += 1;
409 }
410 Ok(i)
411 }
412 }
413}