Skip to content

Commit 1577c83

Browse files
committed
Add USB support to arduino-micro
Fixes #40. The purpose of this PR is to add USB support for the `atmega32u4`. This is done using rust-embedded-community/usb-device GitHub repo, since that appears to be the most popular crate this sort of thing. When using `usb-device`, there are three main components to be aware of: * For `avr-hal`, the main thing we care about is `UsbBus`. This trait is a hardware abstraction layer for the actual USB hardware. We implement this trait in `mcu/atmega-hal/src/usb.rs`. * The `UsbClass` trait is used to implement support for each different type of USB device (e.g. speaker, keyboard, or serial port). These implementations are portable, so we can just use existing implementations from other crates. For `micro-usb-serial.rs`, we create a serial port using `usbd-serial`'s implementation of `UsbClass`. `usbd-serial` was chosen simply because it is the only serial class implementation that is mentioned in `usb-device`'s readme file. * `UsbDevice` is used by our users (and our example code) to actually configure the USB device. Basically, to create `UsbDevice` the user just needs to combine our `UsbBus` implementation with the list of `UsbClass`es that they want to use.
1 parent ff4909c commit 1577c83

File tree

10 files changed

+260
-1
lines changed

10 files changed

+260
-1
lines changed

arduino-hal/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,22 @@ pub mod eeprom {
190190
#[cfg(feature = "board-selected")]
191191
pub use eeprom::Eeprom;
192192

193+
#[doc(no_inline)]
194+
#[cfg(any(
195+
feature = "arduino-micro",
196+
feature = "arduino-leonardo",
197+
feature = "sparkfun-promicro"
198+
))]
199+
pub use atmega_hal::default_usb_bus_with_pll;
200+
201+
#[doc(no_inline)]
202+
#[cfg(any(
203+
feature = "arduino-micro",
204+
feature = "arduino-leonardo",
205+
feature = "sparkfun-promicro"
206+
))]
207+
pub use atmega_hal::default_usb_bus_with_pll_macro;
208+
193209
#[cfg(feature = "board-selected")]
194210
pub mod simple_pwm {
195211
#[cfg(feature = "mcu-atmega")]

examples/arduino-leonardo/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ panic-halt = "1.0.0"
1010
ufmt = "0.2.0"
1111
nb = "1.1.0"
1212
embedded-hal = "1.0"
13+
usb-device = "0.3.2"
14+
usbd-serial = "0.2.2"
1315

1416
[dependencies.arduino-hal]
1517
path = "../../arduino-hal/"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../arduino-micro/src/bin/micro-usb-serial.rs

examples/arduino-micro/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ publish = false
88
panic-halt = "1.0.0"
99
ufmt = "0.2.0"
1010
nb = "1.1.0"
11+
usb-device = "0.3.2"
12+
usbd-serial = "0.2.2"
1113

1214
[dependencies.arduino-hal]
1315
path = "../../arduino-hal/"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![no_std]
2+
#![no_main]
3+
use arduino_hal::Peripherals;
4+
use panic_halt as _;
5+
use usb_device::bus::UsbBusAllocator;
6+
use usb_device::device::StringDescriptors;
7+
use usb_device::device::UsbDeviceBuilder;
8+
use usb_device::device::UsbVidPid;
9+
use usb_device::LangID;
10+
use usbd_serial::SerialPort;
11+
12+
#[arduino_hal::entry]
13+
fn main() -> ! {
14+
let dp: Peripherals = arduino_hal::Peripherals::take().unwrap();
15+
16+
let usb_bus = arduino_hal::default_usb_bus_with_pll_macro!(dp);
17+
let usb_bus_allocator = UsbBusAllocator::new(usb_bus);
18+
let mut serial = SerialPort::new(&usb_bus_allocator);
19+
20+
let string_descriptors = StringDescriptors::new(LangID::EN_US)
21+
.manufacturer("test manufacturer")
22+
.product("test product")
23+
.serial_number("test serial number");
24+
25+
let rand_ids = UsbVidPid(0x1ea7, 0x4a09);
26+
27+
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus_allocator, rand_ids)
28+
.strings(&[string_descriptors])
29+
.unwrap()
30+
.max_packet_size_0(64)
31+
.unwrap()
32+
.device_class(usbd_serial::USB_CLASS_CDC)
33+
.build();
34+
35+
loop {
36+
// Wait until we have data
37+
if !usb_dev.poll(&mut [&mut serial]) {
38+
continue;
39+
}
40+
41+
// Read the data into this buffer
42+
let mut read_buf = [0u8; 10];
43+
let Ok(read_count) = serial.read(&mut read_buf) else {
44+
continue;
45+
};
46+
if read_count == 0 {
47+
continue;
48+
}
49+
50+
let len = serial
51+
.write(&read_buf[0..read_count])
52+
.expect("The host should be reading data faster than the arduino can write it");
53+
assert_eq!(len, read_count);
54+
}
55+
}

examples/sparkfun-promicro/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ panic-halt = "1.0.0"
1010
ufmt = "0.2.0"
1111
nb = "1.1.0"
1212
embedded-hal = "1.0"
13+
usb-device = "0.3.2"
14+
usbd-serial = "0.2.2"
1315

1416
[dependencies.arduino-hal]
1517
path = "../../arduino-hal/"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../arduino-micro/src/bin/micro-usb-serial.rs

mcu/atmega-hal/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ atmega168 = ["avr-device/atmega168", "device-selected"]
2121
atmega328p = ["avr-device/atmega328p", "device-selected"]
2222
atmega328pb = ["avr-device/atmega328pb", "device-selected"]
2323
atmega32a = ["avr-device/atmega32a", "device-selected"]
24-
atmega32u4 = ["avr-device/atmega32u4", "device-selected"]
24+
atmega32u4 = ["avr-device/atmega32u4", "device-selected", "usb-device"]
2525
atmega2560 = ["avr-device/atmega2560", "device-selected"]
2626
atmega128a = ["avr-device/atmega128a", "device-selected"]
2727
atmega1280 = ["avr-device/atmega1280", "device-selected"]
@@ -37,6 +37,7 @@ docsrs = ["atmega328p"]
3737

3838
[dependencies]
3939
avr-hal-generic = { path = "../../avr-hal-generic/" }
40+
usb-device = { version = "0.3.2", optional = true }
4041

4142
[dependencies.avr-device]
4243
version = "0.8"

mcu/atmega-hal/src/lib.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![no_std]
2+
#![feature(asm_experimental_arch)]
23

34
//! `atmega-hal`
45
//! =============
@@ -119,6 +120,93 @@ pub use avr_hal_generic::clock;
119120
pub use avr_hal_generic::delay;
120121
pub use avr_hal_generic::prelude;
121122

123+
#[cfg(feature = "atmega32u4")]
124+
mod usb;
125+
126+
// TODO: fix bad usb-device::UsbBus link
127+
/// This function provides a safe abstraction layer over the USB hardware, by way of the
128+
/// [UsbBus](usb-device::UsbBus) trait.
129+
///
130+
/// There are a few notable limitations, however:
131+
///
132+
/// * This implementation requires exclusive access to the PLL, even though on a hardware
133+
/// level it is possible for the PLL output to be used by both the USB controller and
134+
/// the high-speed timer (TC4) simultaneously. Refer to GitHub issue #TBD for details.
135+
///
136+
/// TODO if Rahix agrees that this limitation isn't something we need to worry about
137+
/// as part of PR, then create a GitHub issue so that someone else can fix it later:
138+
///
139+
/// > **Title**
140+
/// >
141+
/// > Allow the USB and TC4 hardware to both use the PLL output simultaneously
142+
/// >
143+
/// > **Description**
144+
/// >
145+
/// > Our current UsbBus implementation prevents TC4 from using PLL output, even though
146+
/// > the hardware supports it. There are two main
147+
/// > problems that we need to solve first:
148+
/// >
149+
/// > 1. The current UsbBus implementation sets the PLL output to 48MHz. This could
150+
/// > cause problems if the user has already configured TC4 to expect a different
151+
/// > clock speed from the PLL.
152+
/// >
153+
/// > 2. We need to make the USB suspend state configurable. Currently when the USB
154+
/// > bus is idle for 3ms or longer, it will disable the PLL to reduce power usage.
155+
/// > However, this may not be desirable if TC4 is also using the PLL.
156+
/// >
157+
/// > **Comment**
158+
/// >
159+
/// > I think we *might* be able to solve this by splitting the constructor's
160+
/// > argument into two separate parts. Instead of passing ownership of the entire PLL
161+
/// > configuration (`pll: avr_device::atmega32u4::PLL`), we'd have one argument for
162+
/// > the registers that config the PLL clock speed (e.g. `pll_config: &PLLFRQ`) and one
163+
/// > optional argument for the registers that we use to turn the PLL on and off
164+
/// > (e.g. `pll_suspend: Option<&mut pllcsr>`). A value of `None` would indicate that
165+
/// > the user wants us to keep the PLL running while USB is idle.
166+
/// >
167+
/// > A few disclaimers:
168+
/// >
169+
/// > * This is a simplification. Instead of `pll_suspend: Option<&mut pllcsr>` we'd
170+
/// > probably want to define a new trait,
171+
/// > similar to what is done [in the `agausmann/atmega-usbd` repo](https://github.com/agausmann/atmega-usbd/blob/5fc68ca813ce0a37dab65dd4d66efe1ec125f2a8/src/lib.rs#L590-L618).
172+
/// >
173+
/// > * This is just one possible solution; there are others.
174+
/// >
175+
/// > * I've not spent much time investigating this, so this proposed solution might not work.
176+
///
177+
/// * The current implementation does not attempt to minimize power usage. For details,
178+
/// see GitHub issue #TBD.
179+
///
180+
/// TODO if Rahix agrees that this limitation isn't something we need to worry about
181+
/// as part of PR, then create a GitHub issue so that someone else can fix it later:
182+
///
183+
/// * Add support for using interrupts, in addition to polling.
184+
/// Similar to `agausmann/atmega-usbd`.
185+
///
186+
/// * Shutdown the PLL when the USB module is suspended (TODO: do in this PR?)
187+
///
188+
/// * and more?
189+
///
190+
/// * The underlying struct that implements `UsbBus` is private. This is done intentionally
191+
/// in order to make it easier to address the other issues without breaking backwards
192+
/// compatibility.
193+
#[cfg(feature = "atmega32u4")]
194+
pub fn default_usb_bus_with_pll(
195+
usb: avr_device::atmega32u4::USB_DEVICE,
196+
pll: avr_device::atmega32u4::PLL,
197+
) -> impl usb_device::class_prelude::UsbBus {
198+
return usb::UsbdBus::new(usb, pll);
199+
}
200+
201+
/// This macro is exactly equivalent to [default_usb_bus_with_pll](default_usb_bus_with_pll).
202+
#[cfg(feature = "atmega32u4")]
203+
#[macro_export]
204+
macro_rules! default_usb_bus_with_pll_macro {
205+
($p:expr) => {
206+
$crate::default_usb_bus_with_pll($p.USB_DEVICE, $p.PLL)
207+
};
208+
}
209+
122210
#[cfg(feature = "device-selected")]
123211
pub mod adc;
124212
#[cfg(feature = "device-selected")]

mcu/atmega-hal/src/usb.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use core::cell::Cell;
2+
3+
use avr_device::atmega32u4::PLL;
4+
use avr_device::atmega32u4::USB_DEVICE;
5+
use avr_device::interrupt::Mutex;
6+
use usb_device::bus::PollResult;
7+
use usb_device::bus::UsbBus;
8+
use usb_device::endpoint::EndpointAddress;
9+
use usb_device::endpoint::EndpointType;
10+
use usb_device::UsbDirection;
11+
use usb_device::UsbError;
12+
13+
const MAX_ENDPOINTS: usize = 7;
14+
15+
struct EndpointTableEntry {
16+
_ep_type: EndpointType,
17+
_direction: UsbDirection,
18+
_max_packet_size: u16,
19+
}
20+
21+
pub struct UsbdBus {
22+
_usb: Mutex<USB_DEVICE>,
23+
_pll: Mutex<PLL>,
24+
_pending_ins: Mutex<Cell<u8>>,
25+
_endpoints: [Option<EndpointTableEntry>; MAX_ENDPOINTS],
26+
}
27+
28+
impl UsbdBus {
29+
pub fn new(_usb: USB_DEVICE, _pll: PLL) -> Self {
30+
todo!();
31+
}
32+
}
33+
34+
impl UsbBus for UsbdBus {
35+
fn alloc_ep(
36+
&mut self,
37+
_direction: UsbDirection,
38+
_ep_addr: Option<EndpointAddress>,
39+
_ep_type: EndpointType,
40+
_max_packet_size: u16,
41+
_interval: u8,
42+
) -> Result<EndpointAddress, UsbError> {
43+
todo!();
44+
}
45+
46+
fn enable(&mut self) {
47+
todo!();
48+
}
49+
50+
fn reset(&self) {
51+
todo!();
52+
}
53+
54+
fn set_device_address(&self, _addr: u8) {
55+
todo!();
56+
}
57+
58+
fn write(&self, _ep_addr: EndpointAddress, _buf: &[u8]) -> Result<usize, UsbError> {
59+
todo!();
60+
}
61+
62+
fn read(&self, _ep_addr: EndpointAddress, _buf: &mut [u8]) -> Result<usize, UsbError> {
63+
todo!();
64+
}
65+
66+
fn set_stalled(&self, _ep_addr: EndpointAddress, _stalled: bool) {
67+
todo!();
68+
}
69+
70+
fn is_stalled(&self, _ep_addr: EndpointAddress) -> bool {
71+
todo!();
72+
}
73+
74+
fn suspend(&self) {
75+
todo!();
76+
}
77+
78+
fn resume(&self) {
79+
todo!();
80+
}
81+
82+
fn poll(&self) -> PollResult {
83+
todo!();
84+
}
85+
}
86+
87+
impl Drop for UsbdBus {
88+
fn drop(&mut self) {
89+
todo!()
90+
}
91+
}

0 commit comments

Comments
 (0)