Skip to main content

stm_32/peripherals/
vcp.rs

1// ******************************************************************************
2// * File     : platforms/stm_32/src/peripherals/vcp.rs
3// * Date     : June 28, 2026
4// ******************************************************************************
5// *
6// * Copyright (c) 2023, AeroVironment, Inc.
7// * All rights reserved.
8// *
9// * Redistribution and use in source and binary forms, with or without
10// * modification, are permitted provided that the following conditions are met:
11// *
12// * 1.Redistributions of source code must retain the above copyright notice, this
13// * list of conditions and the following disclaimer.
14// *
15// * 2.Redistributions in binary form must reproduce the above copyright notice,
16// * this list of conditions and the following disclaimer in the documentation
17// * and/or other materials provided with the distribution.
18// *
19// * 3.Neither the name of the copyright holder nor the names of its
20// * contributors may be used to endorse or promote products derived from
21// * this software without specific prior written permission.
22// *
23// * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24// * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25// * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26// * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27// * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28// * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29// * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30// * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31// * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32// * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33// *
34// ******************************************************************************
35
36use embassy_futures::join::join;
37use embassy_stm32::peripherals::USB_OTG_FS;
38use embassy_stm32::usb::{Driver, Instance};
39use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
40use embassy_sync::pipe::Pipe;
41use embassy_usb::Builder;
42use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State};
43use veloxity_core::comm::interface::EmbeddedComInterface;
44
45#[cfg(feature = "runtime-diagnostics")]
46use embassy_time::Instant;
47
48#[cfg(feature = "runtime-diagnostics")]
49use core::sync::atomic::{AtomicU32, Ordering};
50
51pub const VCP_TX_BUFF_SIZE: usize = 2048;
52pub const VCP_RX_BUFF_SIZE: usize = 2048;
53const USB_CDC_FS_PACKET_SIZE: usize = 64;
54
55pub static VCP_TX: Pipe<CriticalSectionRawMutex, VCP_TX_BUFF_SIZE> = Pipe::new();
56pub static VCP_RX: Pipe<CriticalSectionRawMutex, VCP_RX_BUFF_SIZE> = Pipe::new();
57
58#[cfg(feature = "runtime-diagnostics")]
59#[unsafe(no_mangle)]
60pub static VELOXITY_DIAG_VCP_FRAME_ATTEMPT: AtomicU32 = AtomicU32::new(0);
61#[cfg(feature = "runtime-diagnostics")]
62#[unsafe(no_mangle)]
63pub static VELOXITY_DIAG_VCP_FRAME_ENQUEUED: AtomicU32 = AtomicU32::new(0);
64#[cfg(feature = "runtime-diagnostics")]
65#[unsafe(no_mangle)]
66pub static VELOXITY_DIAG_VCP_FRAME_REJECTED: AtomicU32 = AtomicU32::new(0);
67#[cfg(feature = "runtime-diagnostics")]
68#[unsafe(no_mangle)]
69pub static VELOXITY_DIAG_VCP_PARTIAL_FRAME_FAILURE: AtomicU32 = AtomicU32::new(0);
70#[cfg(feature = "runtime-diagnostics")]
71#[unsafe(no_mangle)]
72pub static VELOXITY_DIAG_VCP_TX_MIN_FREE: AtomicU32 = AtomicU32::new(VCP_TX_BUFF_SIZE as u32);
73#[cfg(feature = "runtime-diagnostics")]
74#[unsafe(no_mangle)]
75pub static VELOXITY_DIAG_VCP_IMU_ATTEMPT: AtomicU32 = AtomicU32::new(0);
76#[cfg(feature = "runtime-diagnostics")]
77#[unsafe(no_mangle)]
78pub static VELOXITY_DIAG_VCP_IMU_ENQUEUED: AtomicU32 = AtomicU32::new(0);
79#[cfg(feature = "runtime-diagnostics")]
80#[unsafe(no_mangle)]
81pub static VELOXITY_DIAG_VCP_IMU_REJECTED: AtomicU32 = AtomicU32::new(0);
82#[cfg(feature = "runtime-diagnostics")]
83#[unsafe(no_mangle)]
84pub static VELOXITY_DIAG_VCP_DEQUEUE_CALLS: AtomicU32 = AtomicU32::new(0);
85#[cfg(feature = "runtime-diagnostics")]
86#[unsafe(no_mangle)]
87pub static VELOXITY_DIAG_VCP_DEQUEUE_BYTES: AtomicU32 = AtomicU32::new(0);
88#[cfg(feature = "runtime-diagnostics")]
89#[unsafe(no_mangle)]
90pub static VELOXITY_DIAG_VCP_USB_PACKETS: AtomicU32 = AtomicU32::new(0);
91#[cfg(feature = "runtime-diagnostics")]
92#[unsafe(no_mangle)]
93pub static VELOXITY_DIAG_VCP_USB_BYTES: AtomicU32 = AtomicU32::new(0);
94#[cfg(feature = "runtime-diagnostics")]
95#[unsafe(no_mangle)]
96pub static VELOXITY_DIAG_VCP_USB_ERRORS: AtomicU32 = AtomicU32::new(0);
97#[cfg(feature = "runtime-diagnostics")]
98#[unsafe(no_mangle)]
99pub static VELOXITY_DIAG_VCP_RX_USB_PACKETS: AtomicU32 = AtomicU32::new(0);
100#[cfg(feature = "runtime-diagnostics")]
101#[unsafe(no_mangle)]
102pub static VELOXITY_DIAG_VCP_RX_USB_BYTES: AtomicU32 = AtomicU32::new(0);
103#[cfg(feature = "runtime-diagnostics")]
104#[unsafe(no_mangle)]
105pub static VELOXITY_DIAG_VCP_RX_PIPE_MIN_FREE: AtomicU32 = AtomicU32::new(VCP_RX_BUFF_SIZE as u32);
106#[cfg(feature = "runtime-diagnostics")]
107#[unsafe(no_mangle)]
108pub static VELOXITY_DIAG_VCP_RX_WAIT_COUNT: AtomicU32 = AtomicU32::new(0);
109#[cfg(feature = "runtime-diagnostics")]
110#[unsafe(no_mangle)]
111pub static VELOXITY_DIAG_VCP_RX_WAIT_SUM_US: AtomicU32 = AtomicU32::new(0);
112#[cfg(feature = "runtime-diagnostics")]
113#[unsafe(no_mangle)]
114pub static VELOXITY_DIAG_VCP_RX_WAIT_MAX_US: AtomicU32 = AtomicU32::new(0);
115
116#[cfg(feature = "runtime-diagnostics")]
117pub fn record_frame_attempt(message_id: Option<u8>) {
118    VELOXITY_DIAG_VCP_FRAME_ATTEMPT.fetch_add(1, Ordering::Relaxed);
119    VELOXITY_DIAG_VCP_TX_MIN_FREE.fetch_min(VCP_TX.free_capacity() as u32, Ordering::Relaxed);
120    if message_id == Some(181) {
121        VELOXITY_DIAG_VCP_IMU_ATTEMPT.fetch_add(1, Ordering::Relaxed);
122    }
123}
124
125#[cfg(feature = "runtime-diagnostics")]
126pub fn record_frame_enqueued(message_id: Option<u8>) {
127    VELOXITY_DIAG_VCP_FRAME_ENQUEUED.fetch_add(1, Ordering::Relaxed);
128    if message_id == Some(181) {
129        VELOXITY_DIAG_VCP_IMU_ENQUEUED.fetch_add(1, Ordering::Relaxed);
130    }
131}
132
133#[cfg(feature = "runtime-diagnostics")]
134pub fn record_frame_rejected(message_id: Option<u8>, partial: bool) {
135    VELOXITY_DIAG_VCP_FRAME_REJECTED.fetch_add(1, Ordering::Relaxed);
136    if partial {
137        VELOXITY_DIAG_VCP_PARTIAL_FRAME_FAILURE.fetch_add(1, Ordering::Relaxed);
138    }
139    if message_id == Some(181) {
140        VELOXITY_DIAG_VCP_IMU_REJECTED.fetch_add(1, Ordering::Relaxed);
141    }
142}
143
144pub struct BasicProcessor;
145
146impl EmbeddedComInterface for BasicProcessor {
147    async fn process_bytes(&mut self, buf: &[u8], num_bytes: usize) {
148        #[cfg(feature = "runtime-diagnostics")]
149        let (started, had_to_wait) = (Instant::now(), VCP_RX.free_capacity() < num_bytes);
150        VCP_RX.write_all(&buf[0..num_bytes]).await;
151        #[cfg(feature = "runtime-diagnostics")]
152        {
153            VELOXITY_DIAG_VCP_RX_PIPE_MIN_FREE
154                .fetch_min(VCP_RX.free_capacity() as u32, Ordering::Relaxed);
155            if had_to_wait {
156                let elapsed_us = started.elapsed().as_micros().min(u32::MAX as u64) as u32;
157                VELOXITY_DIAG_VCP_RX_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
158                VELOXITY_DIAG_VCP_RX_WAIT_SUM_US.fetch_add(elapsed_us, Ordering::Relaxed);
159                VELOXITY_DIAG_VCP_RX_WAIT_MAX_US.fetch_max(elapsed_us, Ordering::Relaxed);
160            }
161        }
162    }
163}
164
165pub struct Vcp<ECI: EmbeddedComInterface> {
166    pub driver: Driver<'static, USB_OTG_FS>,
167    pub byte_processor: ECI,
168}
169
170impl<ECI: EmbeddedComInterface> Vcp<ECI> {
171    pub async fn run(self) {
172        // Adapted from Embassy STM32H7 examples
173
174        let driver = self.driver;
175        let mut byte_processor = self.byte_processor;
176
177        // Create embassy-usb Config
178        let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
179        config.manufacturer = Some("Embassy");
180        config.product = Some("USB-serial example");
181        config.serial_number = Some("12345678");
182
183        // Create embassy-usb DeviceBuilder using the driver and config.
184        // It needs some buffers for building the descriptors.
185        let mut config_descriptor = [0; 256];
186        let mut bos_descriptor = [0; 256];
187        let mut control_buf = [0; 64];
188
189        let mut state = State::new();
190
191        let mut builder = Builder::new(
192            driver,
193            config,
194            &mut config_descriptor,
195            &mut bos_descriptor,
196            &mut [], // no msos descriptors
197            &mut control_buf,
198        );
199
200        // Create classes on the builder.
201        let class = CdcAcmClass::new(&mut builder, &mut state, 64);
202        // Build the builder.
203        let mut usb = builder.build();
204        // Run the USB device.
205        let usb_fut = usb.run();
206
207        // Keep both USB endpoints armed independently. A single alternating RX/TX
208        // loop can block RX indefinitely while it waits for outbound pipe data.
209        let (mut sender, mut receiver) = class.split();
210        let vcp_fut = async {
211            join(
212                Self::run_rx(&mut byte_processor, &mut receiver),
213                Self::run_tx(&mut sender),
214            )
215            .await;
216        };
217
218        join(usb_fut, vcp_fut).await;
219    }
220
221    async fn run_rx<'d, T: Instance + 'd>(
222        byte_processor: &mut ECI,
223        receiver: &mut Receiver<'d, Driver<'d, T>>,
224    ) {
225        let mut rx_buf = [0u8; VCP_RX_BUFF_SIZE];
226
227        loop {
228            receiver.wait_connection().await;
229            loop {
230                match receiver.read_packet(&mut rx_buf).await {
231                    Ok(n) if n > 0 => {
232                        #[cfg(feature = "runtime-diagnostics")]
233                        {
234                            VELOXITY_DIAG_VCP_RX_USB_PACKETS.fetch_add(1, Ordering::Relaxed);
235                            VELOXITY_DIAG_VCP_RX_USB_BYTES.fetch_add(n as u32, Ordering::Relaxed);
236                        }
237                        byte_processor.process_bytes(&rx_buf[..n], n).await;
238                    }
239                    Ok(_) => {}
240                    Err(_) => break,
241                }
242            }
243        }
244    }
245
246    async fn run_tx<'d, T: Instance + 'd>(sender: &mut Sender<'d, Driver<'d, T>>) {
247        let mut tx_buf = [0u8; VCP_TX_BUFF_SIZE];
248
249        loop {
250            sender.wait_connection().await;
251            'connected: loop {
252                let n = VCP_TX.read(&mut tx_buf).await;
253                #[cfg(feature = "runtime-diagnostics")]
254                {
255                    VELOXITY_DIAG_VCP_DEQUEUE_CALLS.fetch_add(1, Ordering::Relaxed);
256                    VELOXITY_DIAG_VCP_DEQUEUE_BYTES.fetch_add(n as u32, Ordering::Relaxed);
257                }
258                for packet in tx_buf[..n].chunks(USB_CDC_FS_PACKET_SIZE) {
259                    if sender.write_packet(packet).await.is_err() {
260                        #[cfg(feature = "runtime-diagnostics")]
261                        VELOXITY_DIAG_VCP_USB_ERRORS.fetch_add(1, Ordering::Relaxed);
262                        break 'connected;
263                    }
264                    #[cfg(feature = "runtime-diagnostics")]
265                    {
266                        VELOXITY_DIAG_VCP_USB_PACKETS.fetch_add(1, Ordering::Relaxed);
267                        VELOXITY_DIAG_VCP_USB_BYTES
268                            .fetch_add(packet.len() as u32, Ordering::Relaxed);
269                    }
270                }
271            }
272        }
273    }
274}
275
276#[embassy_executor::task]
277pub async fn task(vcp: Vcp<BasicProcessor>) {
278    vcp.run().await;
279}