diff options
| author | Adrian Wowk <[email protected]> | 2025-07-02 20:35:18 -0500 |
|---|---|---|
| committer | Dario Nieuwenhuis <[email protected]> | 2025-09-05 20:35:48 +0200 |
| commit | 4cac3ac1d24d6b651d79bfca8401824c28f5102c (patch) | |
| tree | 28d05c46d27cb1e8e7a85b1ad728c8af68bfdc9e /embassy-rp/src | |
| parent | 676f9da58360627699736c79a4e24ef20a2b9f87 (diff) | |
rp: add new pio spi program
This commit adds a new PIO program which implements SPI. This allows
you to drive more than 2 SPI buses by using PIO state machines as
additional duplex SPI interfaces.
The driver supports both blocking and async modes of operation and
exclusively uses the DMA for async IO.
Diffstat (limited to 'embassy-rp/src')
| -rw-r--r-- | embassy-rp/src/pio_programs/mod.rs | 1 | ||||
| -rw-r--r-- | embassy-rp/src/pio_programs/spi.rs | 433 |
2 files changed, 434 insertions, 0 deletions
diff --git a/embassy-rp/src/pio_programs/mod.rs b/embassy-rp/src/pio_programs/mod.rs index 8eac328b3..d05ba3884 100644 --- a/embassy-rp/src/pio_programs/mod.rs +++ b/embassy-rp/src/pio_programs/mod.rs | |||
| @@ -6,6 +6,7 @@ pub mod i2s; | |||
| 6 | pub mod onewire; | 6 | pub mod onewire; |
| 7 | pub mod pwm; | 7 | pub mod pwm; |
| 8 | pub mod rotary_encoder; | 8 | pub mod rotary_encoder; |
| 9 | pub mod spi; | ||
| 9 | pub mod stepper; | 10 | pub mod stepper; |
| 10 | pub mod uart; | 11 | pub mod uart; |
| 11 | pub mod ws2812; | 12 | pub mod ws2812; |
diff --git a/embassy-rp/src/pio_programs/spi.rs b/embassy-rp/src/pio_programs/spi.rs new file mode 100644 index 000000000..27d401fc9 --- /dev/null +++ b/embassy-rp/src/pio_programs/spi.rs | |||
| @@ -0,0 +1,433 @@ | |||
| 1 | //! PIO backed SPi drivers | ||
| 2 | |||
| 3 | use core::marker::PhantomData; | ||
| 4 | |||
| 5 | use embassy_futures::join::join; | ||
| 6 | use embassy_hal_internal::Peri; | ||
| 7 | use embedded_hal_02::spi::{Phase, Polarity}; | ||
| 8 | use fixed::{traits::ToFixed, types::extra::U8}; | ||
| 9 | |||
| 10 | use crate::{ | ||
| 11 | clocks::clk_sys_freq, | ||
| 12 | dma::{AnyChannel, Channel}, | ||
| 13 | gpio::Level, | ||
| 14 | pio::{Common, Direction, Instance, LoadedProgram, PioPin, ShiftDirection, StateMachine}, | ||
| 15 | spi::{Async, Blocking, Mode}, | ||
| 16 | }; | ||
| 17 | |||
| 18 | /// This struct represents a uart tx program loaded into pio instruction memory. | ||
| 19 | pub struct PioSpiProgram<'d, PIO: crate::pio::Instance> { | ||
| 20 | prg: LoadedProgram<'d, PIO>, | ||
| 21 | } | ||
| 22 | |||
| 23 | impl<'d, PIO: crate::pio::Instance> PioSpiProgram<'d, PIO> { | ||
| 24 | /// Load the spi program into the given pio | ||
| 25 | pub fn new(common: &mut crate::pio::Common<'d, PIO>, phase: Phase) -> Self { | ||
| 26 | // These PIO programs are taken straight from the datasheet (3.6.1 in | ||
| 27 | // RP2040 datasheet, 11.6.1 in RP2350 datasheet) | ||
| 28 | |||
| 29 | // Pin assignments: | ||
| 30 | // - SCK is side-set pin 0 | ||
| 31 | // - MOSI is OUT pin 0 | ||
| 32 | // - MISO is IN pin 0 | ||
| 33 | // | ||
| 34 | // Autopush and autopull must be enabled, and the serial frame size is set by | ||
| 35 | // configuring the push/pull threshold. Shift left/right is fine, but you must | ||
| 36 | // justify the data yourself. This is done most conveniently for frame sizes of | ||
| 37 | // 8 or 16 bits by using the narrow store replication and narrow load byte | ||
| 38 | // picking behaviour of RP2040's IO fabric. | ||
| 39 | |||
| 40 | let prg = match phase { | ||
| 41 | Phase::CaptureOnFirstTransition => { | ||
| 42 | let prg = pio::pio_asm!( | ||
| 43 | r#" | ||
| 44 | .side_set 1 | ||
| 45 | |||
| 46 | ; Clock phase = 0: data is captured on the leading edge of each SCK pulse, and | ||
| 47 | ; transitions on the trailing edge, or some time before the first leading edge. | ||
| 48 | |||
| 49 | out pins, 1 side 0 [1] ; Stall here on empty (sideset proceeds even if | ||
| 50 | in pins, 1 side 1 [1] ; instruction stalls, so we stall with SCK low) | ||
| 51 | "# | ||
| 52 | ); | ||
| 53 | |||
| 54 | common.load_program(&prg.program) | ||
| 55 | } | ||
| 56 | Phase::CaptureOnSecondTransition => { | ||
| 57 | let prg = pio::pio_asm!( | ||
| 58 | r#" | ||
| 59 | .side_set 1 | ||
| 60 | |||
| 61 | ; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and | ||
| 62 | ; is captured on the trailing edge. | ||
| 63 | |||
| 64 | out x, 1 side 0 ; Stall here on empty (keep SCK deasserted) | ||
| 65 | mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping) | ||
| 66 | in pins, 1 side 0 ; Input data, deassert SCK | ||
| 67 | "# | ||
| 68 | ); | ||
| 69 | |||
| 70 | common.load_program(&prg.program) | ||
| 71 | } | ||
| 72 | }; | ||
| 73 | |||
| 74 | Self { prg } | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | /// PIO SPI errors. | ||
| 79 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 80 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||
| 81 | #[non_exhaustive] | ||
| 82 | pub enum Error { | ||
| 83 | // No errors for now | ||
| 84 | } | ||
| 85 | |||
| 86 | /// PIO based Spi driver. | ||
| 87 | /// | ||
| 88 | /// This driver is less flexible than the hardware backed one. Configuration can | ||
| 89 | /// not be changed at runtime. | ||
| 90 | pub struct Spi<'d, PIO: Instance, const SM: usize, M: Mode> { | ||
| 91 | sm: StateMachine<'d, PIO, SM>, | ||
| 92 | tx_dma: Option<Peri<'d, AnyChannel>>, | ||
| 93 | rx_dma: Option<Peri<'d, AnyChannel>>, | ||
| 94 | phantom: PhantomData<M>, | ||
| 95 | } | ||
| 96 | |||
| 97 | /// PIO SPI configuration. | ||
| 98 | #[non_exhaustive] | ||
| 99 | #[derive(Clone)] | ||
| 100 | pub struct Config { | ||
| 101 | /// Frequency (Hz). | ||
| 102 | pub frequency: u32, | ||
| 103 | /// Polarity. | ||
| 104 | pub polarity: Polarity, | ||
| 105 | } | ||
| 106 | |||
| 107 | impl Default for Config { | ||
| 108 | fn default() -> Self { | ||
| 109 | Self { | ||
| 110 | frequency: 1_000_000, | ||
| 111 | polarity: Polarity::IdleLow, | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { | ||
| 117 | #[allow(clippy::too_many_arguments)] | ||
| 118 | fn new_inner( | ||
| 119 | pio: &mut Common<'d, PIO>, | ||
| 120 | mut sm: StateMachine<'d, PIO, SM>, | ||
| 121 | clk_pin: Peri<'d, impl PioPin>, | ||
| 122 | mosi_pin: Peri<'d, impl PioPin>, | ||
| 123 | miso_pin: Peri<'d, impl PioPin>, | ||
| 124 | tx_dma: Option<Peri<'d, AnyChannel>>, | ||
| 125 | rx_dma: Option<Peri<'d, AnyChannel>>, | ||
| 126 | program: &PioSpiProgram<'d, PIO>, | ||
| 127 | config: Config, | ||
| 128 | ) -> Self { | ||
| 129 | let mut clk_pin = pio.make_pio_pin(clk_pin); | ||
| 130 | let mosi_pin = pio.make_pio_pin(mosi_pin); | ||
| 131 | let miso_pin = pio.make_pio_pin(miso_pin); | ||
| 132 | |||
| 133 | if let Polarity::IdleHigh = config.polarity { | ||
| 134 | clk_pin.set_output_inversion(true); | ||
| 135 | } else { | ||
| 136 | clk_pin.set_output_inversion(false); | ||
| 137 | } | ||
| 138 | |||
| 139 | sm.set_pins(Level::Low, &[&clk_pin, &mosi_pin]); | ||
| 140 | sm.set_pin_dirs(Direction::Out, &[&clk_pin, &mosi_pin]); | ||
| 141 | sm.set_pin_dirs(Direction::In, &[&miso_pin]); | ||
| 142 | |||
| 143 | let mut cfg = crate::pio::Config::default(); | ||
| 144 | |||
| 145 | cfg.use_program(&program.prg, &[&clk_pin]); | ||
| 146 | cfg.set_out_pins(&[&mosi_pin]); | ||
| 147 | cfg.set_in_pins(&[&miso_pin]); | ||
| 148 | |||
| 149 | cfg.shift_in.auto_fill = true; | ||
| 150 | cfg.shift_in.direction = ShiftDirection::Left; | ||
| 151 | cfg.shift_in.threshold = 8; | ||
| 152 | |||
| 153 | cfg.shift_out.auto_fill = true; | ||
| 154 | cfg.shift_out.direction = ShiftDirection::Left; | ||
| 155 | cfg.shift_out.threshold = 8; | ||
| 156 | |||
| 157 | let sys_freq = clk_sys_freq().to_fixed::<fixed::FixedU64<U8>>(); | ||
| 158 | let target_freq = (config.frequency * 4).to_fixed::<fixed::FixedU64<U8>>(); | ||
| 159 | cfg.clock_divider = (sys_freq / target_freq).to_fixed(); | ||
| 160 | |||
| 161 | sm.set_config(&cfg); | ||
| 162 | sm.set_enable(true); | ||
| 163 | |||
| 164 | Self { | ||
| 165 | sm, | ||
| 166 | tx_dma, | ||
| 167 | rx_dma, | ||
| 168 | phantom: PhantomData, | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | fn blocking_read_u8(&mut self) -> Result<u8, Error> { | ||
| 173 | while self.sm.rx().empty() {} | ||
| 174 | let value = self.sm.rx().pull() as u8; | ||
| 175 | |||
| 176 | Ok(value) | ||
| 177 | } | ||
| 178 | |||
| 179 | fn blocking_write_u8(&mut self, v: u8) -> Result<(), Error> { | ||
| 180 | let value = u32::from_be_bytes([v, 0, 0, 0]); | ||
| 181 | |||
| 182 | while !self.sm.tx().try_push(value) {} | ||
| 183 | |||
| 184 | // need to clear here for flush to work correctly | ||
| 185 | self.sm.tx().stalled(); | ||
| 186 | |||
| 187 | Ok(()) | ||
| 188 | } | ||
| 189 | |||
| 190 | /// Read data from SPI blocking execution until done. | ||
| 191 | pub fn blocking_read(&mut self, data: &mut [u8]) -> Result<(), Error> { | ||
| 192 | for v in data { | ||
| 193 | self.blocking_write_u8(0)?; | ||
| 194 | *v = self.blocking_read_u8()?; | ||
| 195 | } | ||
| 196 | self.flush()?; | ||
| 197 | Ok(()) | ||
| 198 | } | ||
| 199 | |||
| 200 | /// Write data to SPI blocking execution until done. | ||
| 201 | pub fn blocking_write(&mut self, data: &[u8]) -> Result<(), Error> { | ||
| 202 | for v in data { | ||
| 203 | self.blocking_write_u8(*v)?; | ||
| 204 | let _ = self.blocking_read_u8()?; | ||
| 205 | } | ||
| 206 | self.flush()?; | ||
| 207 | Ok(()) | ||
| 208 | } | ||
| 209 | |||
| 210 | /// Transfer data to SPI blocking execution until done. | ||
| 211 | pub fn blocking_transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Error> { | ||
| 212 | let len = read.len().max(write.len()); | ||
| 213 | for i in 0..len { | ||
| 214 | let wb = write.get(i).copied().unwrap_or(0); | ||
| 215 | self.blocking_write_u8(wb)?; | ||
| 216 | |||
| 217 | let rb = self.blocking_read_u8()?; | ||
| 218 | if let Some(r) = read.get_mut(i) { | ||
| 219 | *r = rb; | ||
| 220 | } | ||
| 221 | } | ||
| 222 | self.flush()?; | ||
| 223 | Ok(()) | ||
| 224 | } | ||
| 225 | |||
| 226 | /// Transfer data in place to SPI blocking execution until done. | ||
| 227 | pub fn blocking_transfer_in_place(&mut self, data: &mut [u8]) -> Result<(), Error> { | ||
| 228 | for v in data { | ||
| 229 | self.blocking_write_u8(*v)?; | ||
| 230 | *v = self.blocking_read_u8()?; | ||
| 231 | } | ||
| 232 | self.flush()?; | ||
| 233 | Ok(()) | ||
| 234 | } | ||
| 235 | |||
| 236 | /// Block execution until SPI is done. | ||
| 237 | pub fn flush(&mut self) -> Result<(), Error> { | ||
| 238 | // Wait for all words in the FIFO to have been pulled by the SM | ||
| 239 | while !self.sm.tx().empty() {} | ||
| 240 | |||
| 241 | // Wait for last value to be written out to the wire | ||
| 242 | while !self.sm.tx().stalled() {} | ||
| 243 | |||
| 244 | Ok(()) | ||
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Blocking> { | ||
| 249 | /// Create an SPI driver in blocking mode. | ||
| 250 | pub fn new_blocking( | ||
| 251 | pio: &mut Common<'d, PIO>, | ||
| 252 | sm: StateMachine<'d, PIO, SM>, | ||
| 253 | clk: Peri<'d, impl PioPin>, | ||
| 254 | mosi: Peri<'d, impl PioPin>, | ||
| 255 | miso: Peri<'d, impl PioPin>, | ||
| 256 | program: &PioSpiProgram<'d, PIO>, | ||
| 257 | config: Config, | ||
| 258 | ) -> Self { | ||
| 259 | Self::new_inner(pio, sm, clk, mosi, miso, None, None, program, config) | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 263 | impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Async> { | ||
| 264 | /// Create an SPI driver in async mode supporting DMA operations. | ||
| 265 | #[allow(clippy::too_many_arguments)] | ||
| 266 | pub fn new( | ||
| 267 | pio: &mut Common<'d, PIO>, | ||
| 268 | sm: StateMachine<'d, PIO, SM>, | ||
| 269 | clk: Peri<'d, impl PioPin>, | ||
| 270 | mosi: Peri<'d, impl PioPin>, | ||
| 271 | miso: Peri<'d, impl PioPin>, | ||
| 272 | tx_dma: Peri<'d, impl Channel>, | ||
| 273 | rx_dma: Peri<'d, impl Channel>, | ||
| 274 | program: &PioSpiProgram<'d, PIO>, | ||
| 275 | config: Config, | ||
| 276 | ) -> Self { | ||
| 277 | Self::new_inner( | ||
| 278 | pio, | ||
| 279 | sm, | ||
| 280 | clk, | ||
| 281 | mosi, | ||
| 282 | miso, | ||
| 283 | Some(tx_dma.into()), | ||
| 284 | Some(rx_dma.into()), | ||
| 285 | program, | ||
| 286 | config, | ||
| 287 | ) | ||
| 288 | } | ||
| 289 | |||
| 290 | /// Read data from SPI using DMA. | ||
| 291 | pub async fn read(&mut self, buffer: &mut [u8]) -> Result<(), Error> { | ||
| 292 | let (rx, tx) = self.sm.rx_tx(); | ||
| 293 | |||
| 294 | let len = buffer.len(); | ||
| 295 | |||
| 296 | let rx_ch = self.rx_dma.as_mut().unwrap().reborrow(); | ||
| 297 | let rx_transfer = rx.dma_pull(rx_ch, buffer, false); | ||
| 298 | |||
| 299 | let tx_ch = self.tx_dma.as_mut().unwrap().reborrow(); | ||
| 300 | let tx_transfer = tx.dma_push_repeated::<_, u8>(tx_ch, len); | ||
| 301 | |||
| 302 | join(tx_transfer, rx_transfer).await; | ||
| 303 | |||
| 304 | Ok(()) | ||
| 305 | } | ||
| 306 | |||
| 307 | /// Write data to SPI using DMA. | ||
| 308 | pub async fn write(&mut self, buffer: &[u8]) -> Result<(), Error> { | ||
| 309 | let (rx, tx) = self.sm.rx_tx(); | ||
| 310 | |||
| 311 | let rx_ch = self.rx_dma.as_mut().unwrap().reborrow(); | ||
| 312 | let rx_transfer = rx.dma_pull_repeated::<_, u8>(rx_ch, buffer.len()); | ||
| 313 | |||
| 314 | let tx_ch = self.tx_dma.as_mut().unwrap().reborrow(); | ||
| 315 | let tx_transfer = tx.dma_push(tx_ch, buffer, false); | ||
| 316 | |||
| 317 | join(tx_transfer, rx_transfer).await; | ||
| 318 | |||
| 319 | Ok(()) | ||
| 320 | } | ||
| 321 | |||
| 322 | /// Transfer data to SPI using DMA. | ||
| 323 | pub async fn transfer(&mut self, rx_buffer: &mut [u8], tx_buffer: &[u8]) -> Result<(), Error> { | ||
| 324 | self.transfer_inner(rx_buffer, tx_buffer).await | ||
| 325 | } | ||
| 326 | |||
| 327 | /// Transfer data in place to SPI using DMA. | ||
| 328 | pub async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Error> { | ||
| 329 | self.transfer_inner(words, words).await | ||
| 330 | } | ||
| 331 | |||
| 332 | async fn transfer_inner(&mut self, rx_buffer: *mut [u8], tx_buffer: *const [u8]) -> Result<(), Error> { | ||
| 333 | let (rx, tx) = self.sm.rx_tx(); | ||
| 334 | |||
| 335 | let mut rx_ch = self.rx_dma.as_mut().unwrap().reborrow(); | ||
| 336 | let rx_transfer = async { | ||
| 337 | rx.dma_pull(rx_ch.reborrow(), unsafe { &mut *rx_buffer }, false).await; | ||
| 338 | |||
| 339 | if tx_buffer.len() > rx_buffer.len() { | ||
| 340 | let read_bytes_len = tx_buffer.len() - rx_buffer.len(); | ||
| 341 | |||
| 342 | rx.dma_pull_repeated::<_, u8>(rx_ch, read_bytes_len).await; | ||
| 343 | } | ||
| 344 | }; | ||
| 345 | |||
| 346 | let mut tx_ch = self.tx_dma.as_mut().unwrap().reborrow(); | ||
| 347 | let tx_transfer = async { | ||
| 348 | tx.dma_push(tx_ch.reborrow(), unsafe { &*tx_buffer }, false).await; | ||
| 349 | |||
| 350 | if rx_buffer.len() > tx_buffer.len() { | ||
| 351 | let write_bytes_len = rx_buffer.len() - tx_buffer.len(); | ||
| 352 | |||
| 353 | tx.dma_push_repeated::<_, u8>(tx_ch, write_bytes_len).await; | ||
| 354 | } | ||
| 355 | }; | ||
| 356 | |||
| 357 | join(tx_transfer, rx_transfer).await; | ||
| 358 | |||
| 359 | Ok(()) | ||
| 360 | } | ||
| 361 | } | ||
| 362 | |||
| 363 | // ==================== | ||
| 364 | |||
| 365 | impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_02::blocking::spi::Transfer<u8> for Spi<'d, PIO, SM, M> { | ||
| 366 | type Error = Error; | ||
| 367 | fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> { | ||
| 368 | self.blocking_transfer_in_place(words)?; | ||
| 369 | Ok(words) | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 373 | impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_02::blocking::spi::Write<u8> for Spi<'d, PIO, SM, M> { | ||
| 374 | type Error = Error; | ||
| 375 | |||
| 376 | fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { | ||
| 377 | self.blocking_write(words) | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | impl embedded_hal_1::spi::Error for Error { | ||
| 382 | fn kind(&self) -> embedded_hal_1::spi::ErrorKind { | ||
| 383 | match *self {} | ||
| 384 | } | ||
| 385 | } | ||
| 386 | |||
| 387 | impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_1::spi::ErrorType for Spi<'d, PIO, SM, M> { | ||
| 388 | type Error = Error; | ||
| 389 | } | ||
| 390 | |||
| 391 | impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_1::spi::SpiBus<u8> for Spi<'d, PIO, SM, M> { | ||
| 392 | fn flush(&mut self) -> Result<(), Self::Error> { | ||
| 393 | Ok(()) | ||
| 394 | } | ||
| 395 | |||
| 396 | fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { | ||
| 397 | self.blocking_transfer(words, &[]) | ||
| 398 | } | ||
| 399 | |||
| 400 | fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { | ||
| 401 | self.blocking_write(words) | ||
| 402 | } | ||
| 403 | |||
| 404 | fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { | ||
| 405 | self.blocking_transfer(read, write) | ||
| 406 | } | ||
| 407 | |||
| 408 | fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { | ||
| 409 | self.blocking_transfer_in_place(words) | ||
| 410 | } | ||
| 411 | } | ||
| 412 | |||
| 413 | impl<'d, PIO: Instance, const SM: usize> embedded_hal_async::spi::SpiBus<u8> for Spi<'d, PIO, SM, Async> { | ||
| 414 | async fn flush(&mut self) -> Result<(), Self::Error> { | ||
| 415 | Ok(()) | ||
| 416 | } | ||
| 417 | |||
| 418 | async fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { | ||
| 419 | self.write(words).await | ||
| 420 | } | ||
| 421 | |||
| 422 | async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { | ||
| 423 | self.read(words).await | ||
| 424 | } | ||
| 425 | |||
| 426 | async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { | ||
| 427 | self.transfer(read, write).await | ||
| 428 | } | ||
| 429 | |||
| 430 | async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { | ||
| 431 | self.transfer_in_place(words).await | ||
| 432 | } | ||
| 433 | } | ||
