diff --git a/arduino-hal/Cargo.toml b/arduino-hal/Cargo.toml index 1f400c73f3..ac1e0b793a 100644 --- a/arduino-hal/Cargo.toml +++ b/arduino-hal/Cargo.toml @@ -20,7 +20,7 @@ board-selected = [] mcu-atmega = [] mcu-attiny = [] arduino-diecimila = ["mcu-atmega", "atmega-hal/atmega168", "board-selected"] -arduino-leonardo = ["mcu-atmega", "atmega-hal/atmega32u4", "board-selected"] +arduino-leonardo = ["mcu-atmega", "atmega-hal/atmega32u4", "board-selected", "usb-device"] arduino-mega2560 = ["mcu-atmega", "atmega-hal/atmega2560", "board-selected"] arduino-mega1280 = ["mcu-atmega", "atmega-hal/atmega1280", "board-selected"] arduino-nano = ["mcu-atmega", "atmega-hal/atmega328p", "atmega-hal/enable-extra-adc", "board-selected"] @@ -39,6 +39,7 @@ docsrs = ["arduino-uno"] cfg-if = "1" embedded-hal = "1.0" ufmt = "0.2.0" +usb-device = { version = "0.3.2", optional = true } [dependencies.embedded-hal-v0] version = "0.2.3" diff --git a/arduino-hal/src/lib.rs b/arduino-hal/src/lib.rs index 5e78dc3318..6ce4a98ad6 100644 --- a/arduino-hal/src/lib.rs +++ b/arduino-hal/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] #![feature(doc_cfg)] +#![feature(decl_macro)] //! `arduino-hal` //! ============= @@ -175,11 +176,23 @@ pub mod usart { pub type UsartReader = crate::hal::usart::UsartReader; } - #[doc(no_inline)] #[cfg(feature = "mcu-atmega")] pub use usart::Usart; +#[cfg(feature = "arduino-leonardo")] +use usb_device::{bus::UsbBusAllocator, device::{UsbDeviceBuilder, UsbVidPid}}; +#[cfg(feature = "arduino-leonardo")] +pub mod usb { + pub use crate::hal::usb::*; + + pub type AvrUsbBus = crate::hal::usb::AvrUsbBus; +} +#[doc(no_inline)] +#[cfg(feature = "arduino-leonardo")] +pub use usb::AvrUsbBus; + + #[cfg(feature = "board-selected")] pub mod eeprom { pub use crate::hal::eeprom::{Eeprom, EepromOps, OutOfBoundsError}; @@ -340,3 +353,34 @@ macro_rules! default_serial { ) }; } + +/// Convenience macro to instantiate the [`UsbBus`] driver for this board. +/// +/// # Example +/// ```no_run +/// TODO +/// ``` +#[cfg(feature = "arduino-leonardo")] +pub macro default_usb_bus ($usb:expr, $pll:expr) { + unsafe { + static mut USB_BUS: Option> = None; + &*USB_BUS.insert($crate::AvrUsbBus::with_suspend_notifier($usb, $pll)) + }; +} + +/// Convenience macro to instantiate the [`UsbDevice`] driver for this board. +/// +/// # Example +/// ```no_run +/// TODO +/// ``` +#[cfg(feature = "arduino-leonardo")] +pub macro default_usb_device ($usb_bus:expr, $vid:expr, $pid:expr, $strings:expr) { + UsbDeviceBuilder::new( + $usb_bus, + UsbVidPid($vid, $pid) + ) + .strings(&[$strings]) + .unwrap() + .build() +} \ No newline at end of file diff --git a/arduino-hal/src/port/leonardo.rs b/arduino-hal/src/port/leonardo.rs index c25b5a89b9..4460fdf271 100644 --- a/arduino-hal/src/port/leonardo.rs +++ b/arduino-hal/src/port/leonardo.rs @@ -1,5 +1,6 @@ pub use atmega_hal::port::{mode, Pin, PinMode, PinOps}; +#[allow(non_camel_case_types)] // Mute LedRx and LedTx complaints avr_hal_generic::renamed_pins! { /// Pins of the **Arduino Leonardo**. /// diff --git a/avr-hal-generic/Cargo.toml b/avr-hal-generic/Cargo.toml index 16f81a3638..994b27ae01 100644 --- a/avr-hal-generic/Cargo.toml +++ b/avr-hal-generic/Cargo.toml @@ -26,6 +26,7 @@ embedded-storage = "0.2" embedded-hal = "1.0" embedded-hal-bus = "0.1" unwrap-infallible = "0.1.5" +usb-device = "0.3.2" [dependencies.embedded-hal-v0] version = "0.2.3" diff --git a/avr-hal-generic/src/lib.rs b/avr-hal-generic/src/lib.rs index 41d3c9d688..9560a2c817 100644 --- a/avr-hal-generic/src/lib.rs +++ b/avr-hal-generic/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] #![cfg_attr(avr_hal_asm_macro, feature(asm_experimental_arch))] #![cfg_attr(not(avr_hal_asm_macro), feature(llvm_asm))] +#![feature(decl_macro)] pub use embedded_hal as hal; pub use embedded_hal_v0 as hal_v0; @@ -22,6 +23,7 @@ pub mod simple_pwm; pub mod spi; pub mod usart; pub mod wdt; +pub mod usb; /// Prelude containing all HAL traits pub mod prelude { diff --git a/avr-hal-generic/src/usb.rs b/avr-hal-generic/src/usb.rs new file mode 100644 index 0000000000..f0c91cb9b6 --- /dev/null +++ b/avr-hal-generic/src/usb.rs @@ -0,0 +1,567 @@ +use core::{cell::Cell, cmp::max}; + +use usb_device::{bus::{UsbBus, PollResult}, UsbDirection, UsbError, class_prelude::UsbBusAllocator, endpoint::{EndpointAddress, EndpointType}, Result as UsbResult}; +use avr_device::{ + asm::delay_cycles, + interrupt::{self, CriticalSection, Mutex as AvrDMutex} +}; + +// REVIEW: Do any of the guys below here need to be abstracted? + +const EP_TYPE_CONTROL: u8 = 0b00; +const EP_TYPE_ISOCHRONOUS: u8 = 0b01; +const EP_TYPE_BULK: u8 = 0b10; +const EP_TYPE_INTERRUPT: u8 = 0b11; + +const EP_DIR_IN: bool = true; +const EP_DIR_OUT: bool = false; + +const EP_SIZE_8: u8 = 0b000; +const EP_SIZE_16: u8 = 0b001; +const EP_SIZE_32: u8 = 0b010; +const EP_SIZE_64: u8 = 0b011; +const EP_SIZE_128: u8 = 0b100; +const EP_SIZE_256: u8 = 0b101; +const EP_SIZE_512: u8 = 0b110; + +#[derive(Default)] +pub struct EndpointTableEntry { // REVIEW: what should the scoping be here? + is_allocated: bool, // REVIEW: i dont think this should be pub + eptype_bits: u8, + epdir_bit: bool, + epsize_bits: u8, +} + +impl EndpointTableEntry { + pub fn buffer_size(&self) -> usize { + match self.epsize_bits { + EP_SIZE_8 => 8, + EP_SIZE_16 => 16, + EP_SIZE_32 => 32, + EP_SIZE_64 => 64, + EP_SIZE_128 => 128, + EP_SIZE_256 => 256, + EP_SIZE_512 => 512, + _ => unreachable!(), + } + } +} + +// Using Macro 2.0 here while not stable yet makes this code a lot more readable and easier to write +pub macro create_usb_bus ( + $AvrGenericUsbBus:ident, // TODO: get rid of this when hygiene escaping is a thing (see #39412) + $new:ident, // literally the keyword `new` // what cruel world do we live in + $with_suspend_notifier:ident, + $USB_DEVICE:ty, + $SuspendNotifier:path, + $MAX_ENDPOINTS:ident, + $ENDPOINT_MAX_BUFSIZE:ident, + $DPRAM_SIZE:ident, + $limited_inter_and_vbus:meta, + $not_limited_inter_and_vbus:meta + // TODO: add a visibility restriction here so not everything is just blatantly `pub` +) { // could stand to make the above a bit more readable + + // MARK: - AvrGenericUsbBus + + pub struct $AvrGenericUsbBus { + pub usb: AvrDMutex<$USB_DEVICE>, + pub suspend_notifier: AvrDMutex, + pub pending_ins: AvrDMutex>, + pub endpoints: [EndpointTableEntry; $MAX_ENDPOINTS], + pub dpram_usage: u16, + } + + impl $AvrGenericUsbBus<()> { + /// Create a new UsbBus without power-saving functionality. + /// + /// If you would like to disable the PLL when the USB peripheral is + /// suspended, then construct the bus with [`UsbBus::with_suspend_notifier`]. + pub fn $new(usb: $USB_DEVICE) -> UsbBusAllocator { + Self::$with_suspend_notifier(usb, ()) + } + } + + impl $AvrGenericUsbBus { + /// Create a UsbBus with a suspend and resume handler. + /// + /// If you want the PLL to be automatically disabled when the USB peripheral + /// is suspended, then you can pass the PLL resource here; for example: + /// + /// ``` + /// use avr_device::atmega32u4::Peripherals; + /// use atmega_usbd::UsbBus; + /// + /// let dp = Peripherals.take().unwrap(); + /// // ... (other initialization stuff) + /// let bus = UsbBus::with_suspend_notifier(dp.USB_DEVICE, dp.PLL); + /// ``` + /// + /// **Note: If you are using the PLL output for other peripherals like the + /// high-speed timer, then disabling the PLL may affect the behavior of + /// those peripherals.** In such cases, you can either use [`UsbBus::new`] + /// to leave the PLL running, or implement [`SuspendNotifier`] yourself, + /// with some custom logic to gracefully shut down the PLL in cooperation + /// with your other peripherals. + pub fn $with_suspend_notifier(usb: $USB_DEVICE, suspend_notifier: S) -> UsbBusAllocator { + UsbBusAllocator::new(Self { + usb: AvrDMutex::new(usb), + suspend_notifier: AvrDMutex::new(suspend_notifier), + pending_ins: AvrDMutex::new(Cell::new(0)), + endpoints: Default::default(), + dpram_usage: 0, + }) + } + + pub fn active_endpoints(&self) -> impl Iterator { + self.endpoints + .iter() + .enumerate() // why enumerate then immediately drop? + .filter(|&(_, ep)| ep.is_allocated) + } + + pub fn set_current_endpoint(&self, cs: CriticalSection, index: usize) -> Result<(), UsbError> { + if index >= $MAX_ENDPOINTS { + return Err(UsbError::InvalidEndpoint); + } + let usb = self.usb.borrow(cs); + // TODO: the rest of this needs to be abstracted + if usb.usbcon.read().frzclk().bit_is_set() { + return Err(UsbError::InvalidState); + } + + #[$limited_inter_and_vbus] + unsafe { // TODO: investigate unsafety here (only occurs in atmega8u2?) + usb.uenum.write(|w| w.bits(index as u8)); + } + #[$not_limited_inter_and_vbus] + usb.uenum.modify(|_, w| w.bits(index as u8)); + + if usb.uenum.read().bits() & 0b111 != (index as u8) { + return Err(UsbError::InvalidState); + } + Ok(()) + } + + pub fn endpoint_byte_count(&self, cs: CriticalSection) -> u16 { // REVIEW: should this conditionally be a u8 + let usb = self.usb.borrow(cs); + // FIXME: Potential for desync here? LUFA doesn't seem to care. + #[$not_limited_inter_and_vbus] + { + return ((usb.uebchx.read().bits() as u16) << 8) | (usb.uebclx.read().bits() as u16) + } + #[$limited_inter_and_vbus] + { // REVIEW: is this the correct formula? + return (usb.uebclx.read().bits() as u16) // u16? + } + } + + pub fn configure_endpoint(&self, cs: CriticalSection, index: usize) -> Result<(), UsbError> { + let usb = self.usb.borrow(cs); + self.set_current_endpoint(cs, index)?; + let endpoint = &self.endpoints[index]; + + usb.ueconx.modify(|_, w| w.epen().set_bit()); + usb.uecfg1x.modify(|_, w| w.alloc().clear_bit()); + + usb.uecfg0x.write(|w| { + w.epdir() + .bit(endpoint.epdir_bit) + .eptype() + .bits(endpoint.eptype_bits) + }); + usb.uecfg1x + .write(|w| w.epbk().bits(0).epsize().bits(endpoint.epsize_bits)); + usb.uecfg1x.modify(|_, w| w.alloc().set_bit()); + + assert!( + usb.uesta0x.read().cfgok().bit_is_set(), + "could not configure endpoint {}", + index + ); + + usb.ueienx + .modify(|_, w| w.rxoute().set_bit().rxstpe().set_bit()); + Ok(()) + } + } + + impl UsbBus for $AvrGenericUsbBus { + fn alloc_ep( + &mut self, + ep_dir: UsbDirection, + ep_addr: Option, + ep_type: EndpointType, + max_packet_size: u16, + _interval: u8, + ) -> Result { + // Ignore duplicate ep0 allocation by usb_device. + // Endpoints can only be configured once, and + // control endpoint must be configured as "OUT". + if ep_addr == Some(EndpointAddress::from_parts(0, UsbDirection::In)) { + return Ok(ep_addr.unwrap()); + } + + let ep_addr = match ep_addr { + Some(addr) if !self.endpoints[addr.index()].is_allocated => addr, + _ => { + // Find next free endpoint + let index = self + .endpoints + .iter() + .enumerate() + .skip(1) + .find_map(|(index, ep)| { + if !ep.is_allocated && max_packet_size <= $ENDPOINT_MAX_BUFSIZE[index] { + Some(index) + } else { + None + } + }) + .ok_or(UsbError::EndpointOverflow)?; + EndpointAddress::from_parts(index, ep_dir) + } + }; + let entry = &mut self.endpoints[ep_addr.index()]; + entry.eptype_bits = match ep_type { + EndpointType::Control => EP_TYPE_CONTROL, + EndpointType::Isochronous { .. } => EP_TYPE_ISOCHRONOUS, + EndpointType::Bulk => EP_TYPE_BULK, + EndpointType::Interrupt => EP_TYPE_INTERRUPT, + }; + entry.epdir_bit = match ep_dir { + UsbDirection::Out => EP_DIR_OUT, + UsbDirection::In => EP_DIR_IN, + }; + let ep_size = max(8, max_packet_size.next_power_of_two()); + if $DPRAM_SIZE - self.dpram_usage < ep_size { + return Err(UsbError::EndpointMemoryOverflow); + } + entry.epsize_bits = match ep_size { + 8 => EP_SIZE_8, + 16 => EP_SIZE_16, + 32 => EP_SIZE_32, + 64 => EP_SIZE_64, + 128 => EP_SIZE_128, + 256 => EP_SIZE_256, + 512 => EP_SIZE_512, + _ => return Err(UsbError::EndpointMemoryOverflow), + }; + + // Configuration succeeded, commit/finalize: + entry.is_allocated = true; + self.dpram_usage += ep_size; + Ok(ep_addr) + } + + fn enable(&mut self) { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + #[$not_limited_inter_and_vbus] + { + usb.uhwcon.modify(|_, w| w.uvrege().set_bit()); // REVIEW: what is the uhwcon and the pad regulator? + usb.usbcon + .modify(|_, w| w.usbe().set_bit().otgpade().set_bit()); + // NB: FRZCLK cannot be set/cleared when USBE=0, and + // cannot be modified at the same time. + usb.usbcon + .modify(|_, w| w.frzclk().clear_bit().vbuste().set_bit()); + } + #[$limited_inter_and_vbus] + { + usb.usbcon + .modify(|_, w| w.usbe().set_bit()); + // NB: FRZCLK cannot be set/cleared when USBE=0, and + // cannot be modified at the same time. + usb.usbcon.modify(|_, w| w.frzclk().clear_bit()); + } + + for (index, _ep) in self.active_endpoints() { + self.configure_endpoint(cs, index).unwrap(); + } + + usb.udcon.modify(|_, w| w.detach().clear_bit()); + usb.udien + .modify(|_, w| w.eorste().set_bit().sofe().set_bit()); + }); + } + + fn reset(&self) { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + usb.udint.modify(|_, w| w.eorsti().clear_bit()); + + for (index, _ep) in self.active_endpoints() { + self.configure_endpoint(cs, index).unwrap(); + } + + usb.udint + .clear_interrupts(|w| w.wakeupi().clear_bit().suspi().clear_bit()); + usb.udien + .modify(|_, w| w.wakeupe().clear_bit().suspe().set_bit()); + }) + } + + fn set_device_address(&self, addr: u8) { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + usb.udaddr.modify(|_, w| w.uadd().bits(addr)); + // NB: ADDEN and UADD shall not be written at the same time. + usb.udaddr.modify(|_, w| w.adden().set_bit()); + }); + } + + fn write(&self, ep_addr: EndpointAddress, buf: &[u8]) -> UsbResult { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + self.set_current_endpoint(cs, ep_addr.index())?; + let endpoint = &self.endpoints[ep_addr.index()]; + + // Different logic is needed for control endpoints: + // - The FIFOCON and RWAL fields are irrelevant with CONTROL endpoints. + // - TXINI ... shall be cleared by firmware to **send the + // packet and to clear the endpoint bank.** + if endpoint.eptype_bits == EP_TYPE_CONTROL { + if usb.ueintx.read().txini().bit_is_clear() { + return Err(UsbError::WouldBlock); + } + + let buffer_size = endpoint.buffer_size(); + if buf.len() > buffer_size { + return Err(UsbError::BufferOverflow); + } + + for &byte in buf { + usb.uedatx.write(|w| w.bits(byte)) + } + + usb.ueintx.clear_interrupts(|w| w.txini().clear_bit()); + } else { + if usb.ueintx.read().txini().bit_is_clear() { + return Err(UsbError::WouldBlock); + } + //NB: RXOUTI serves as KILLBK for IN endpoints and needs to stay zero: + usb.ueintx + .clear_interrupts(|w| w.txini().clear_bit().rxouti().clear_bit()); + + for &byte in buf { + if usb.ueintx.read().rwal().bit_is_clear() { + return Err(UsbError::BufferOverflow); + } + usb.uedatx.write(|w| w.bits(byte)); + } + + //NB: RXOUTI serves as KILLBK for IN endpoints and needs to stay zero: + usb.ueintx + .clear_interrupts(|w| w.fifocon().clear_bit().rxouti().clear_bit()); + } + + let pending_ins = self.pending_ins.borrow(cs); + pending_ins.set(pending_ins.get() | 1 << ep_addr.index()); + + Ok(buf.len()) + }) + } + + fn read(&self, ep_addr: EndpointAddress, buf: &mut [u8]) -> UsbResult { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + self.set_current_endpoint(cs, ep_addr.index())?; + let endpoint = &self.endpoints[ep_addr.index()]; + + // Different logic is needed for control endpoints: + // - The FIFOCON and RWAL fields are irrelevant with CONTROL endpoints. + // - RXSTPI/RXOUTI ... shall be cleared by firmware to **send the + // packet and to clear the endpoint bank.** + if endpoint.eptype_bits == EP_TYPE_CONTROL { + let ueintx = usb.ueintx.read(); + if ueintx.rxouti().bit_is_clear() && ueintx.rxstpi().bit_is_clear() { + return Err(UsbError::WouldBlock); + } + + let bytes_to_read = self.endpoint_byte_count(cs) as usize; + if bytes_to_read > buf.len() { + return Err(UsbError::BufferOverflow); + } + + for slot in &mut buf[..bytes_to_read] { + *slot = usb.uedatx.read().bits(); + } + usb.ueintx + .clear_interrupts(|w| w.rxouti().clear_bit().rxstpi().clear_bit()); + + Ok(bytes_to_read) + } else { + if usb.ueintx.read().rxouti().bit_is_clear() { + return Err(UsbError::WouldBlock); + } + usb.ueintx.clear_interrupts(|w| w.rxouti().clear_bit()); + + let mut bytes_read = 0; + for slot in buf { + if usb.ueintx.read().rwal().bit_is_clear() { + break; + } + *slot = usb.uedatx.read().bits(); + bytes_read += 1; + } + + if usb.ueintx.read().rwal().bit_is_set() { + return Err(UsbError::BufferOverflow); + } + + usb.ueintx.clear_interrupts(|w| w.fifocon().clear_bit()); + Ok(bytes_read) + } + }) + } + + fn set_stalled(&self, ep_addr: EndpointAddress, stalled: bool) { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + if self.set_current_endpoint(cs, ep_addr.index()).is_ok() { + usb.ueconx + .modify(|_, w| w.stallrq().bit(stalled).stallrqc().bit(!stalled)); + } + }); + } + + fn is_stalled(&self, ep_addr: EndpointAddress) -> bool { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + if self.set_current_endpoint(cs, ep_addr.index()).is_ok() { + // NB: The datasheet says STALLRQ is write-only but LUFA reads from it... + usb.ueconx.read().stallrq().bit_is_set() + } else { + false + } + }) + } + + fn suspend(&self) { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + usb.udint + .clear_interrupts(|w| w.suspi().clear_bit().wakeupi().clear_bit()); + usb.udien + .modify(|_, w| w.wakeupe().set_bit().suspe().clear_bit()); + usb.usbcon.modify(|_, w| w.frzclk().set_bit()); + + self.suspend_notifier.borrow(cs).suspend(); + }); + } + + fn resume(&self) { + interrupt::free(|cs| { + self.suspend_notifier.borrow(cs).resume(); + + let usb = self.usb.borrow(cs); + usb.usbcon.modify(|_, w| w.frzclk().clear_bit()); + usb.udint + .clear_interrupts(|w| w.wakeupi().clear_bit().suspi().clear_bit()); + usb.udien + .modify(|_, w| w.wakeupe().clear_bit().suspe().set_bit()); + }); + } + + fn poll(&self) -> PollResult { + interrupt::free(|cs| { + let usb = self.usb.borrow(cs); + + #[$not_limited_inter_and_vbus] + let usbint = usb.usbint.read(); + + let udint = usb.udint.read(); + let udien = usb.udien.read(); + + #[$not_limited_inter_and_vbus] + { + if usbint.vbusti().bit_is_set() { + usb.usbint.clear_interrupts(|w| w.vbusti().clear_bit()); + if usb.usbsta.read().vbus().bit_is_set() { + return PollResult::Resume; + } else { + return PollResult::Suspend; + } + } + } + + if udint.suspi().bit_is_set() && udien.suspe().bit_is_set() { + return PollResult::Suspend; + } + if udint.wakeupi().bit_is_set() && udien.wakeupe().bit_is_set() { + return PollResult::Resume; + } + if udint.eorsti().bit_is_set() { + return PollResult::Reset; + } + if udint.sofi().bit_is_set() { + usb.udint.clear_interrupts(|w| w.sofi().clear_bit()); + } + + // Can only query endpoints while clock is running + // (e.g. not in suspend state) + if usb.usbcon.read().frzclk().bit_is_clear() { + let mut ep_out = 0u8; + let mut ep_setup = 0u8; + let mut ep_in_complete = 0u8; + let pending_ins = self.pending_ins.borrow(cs); + + for (index, _ep) in self.active_endpoints() { + if self.set_current_endpoint(cs, index).is_err() { + // Endpoint selection has stopped working... + break; + } + + let ueintx = usb.ueintx.read(); + if ueintx.rxouti().bit_is_set() { + ep_out |= 1 << index; + } + if ueintx.rxstpi().bit_is_set() { + ep_setup |= 1 << index; + } + if pending_ins.get() & (1 << index) != 0 && ueintx.txini().bit_is_set() { + ep_in_complete |= 1 << index; + pending_ins.set(pending_ins.get() & !(1 << index)); + } + } + if ep_out | ep_setup | ep_in_complete != 0 { + return PollResult::Data { + ep_out: ep_out as u16, + ep_in_complete: ep_in_complete as u16, + ep_setup: ep_setup as u16, + }; + } + } + + PollResult::None + }) + } + + fn force_reset(&self) -> UsbResult<()> { + // 22.9 "It is possible to re-enumerate a device, simply by setting and + // clearing the DETACH bit (but firmware must take in account a + // debouncing delay of some milliseconds)." + + interrupt::free(|cs| { + self.usb + .borrow(cs) + .udcon + .modify(|_, w| w.detach().set_bit()); + }); + + // Delay for at least 1ms (exactly 1ms at 16 MHz) + // to allow the host to detect the change. + delay_cycles(16000); + + interrupt::free(|cs| { + self.usb + .borrow(cs) + .udcon + .modify(|_, w| w.detach().clear_bit()); + }); + + Ok(()) + } + } +} diff --git a/examples/arduino-leonardo/Cargo.toml b/examples/arduino-leonardo/Cargo.toml index 832e5d4318..7a61e37abe 100644 --- a/examples/arduino-leonardo/Cargo.toml +++ b/examples/arduino-leonardo/Cargo.toml @@ -10,6 +10,9 @@ panic-halt = "0.2.0" ufmt = "0.2.0" nb = "1.1.0" embedded-hal = "1.0" +avr-device = "0.5.4" +usb-device = "0.3" +usbd-hid = "0.8" [dependencies.embedded-hal-v0] version = "0.2.3" diff --git a/examples/arduino-leonardo/src/bin/leonardo-usb.rs b/examples/arduino-leonardo/src/bin/leonardo-usb.rs new file mode 100644 index 0000000000..b0f0ddd476 --- /dev/null +++ b/examples/arduino-leonardo/src/bin/leonardo-usb.rs @@ -0,0 +1,190 @@ +//! A "Hello World" example that can be run on an Arduino Leonardo. +//! +//! # Usage +//! +//! 1. (Optional) Connect a pushbutton switch to the D2 pin of the Leonardo, and +//! connect the other pin of the switch to GND. +//! +//! 2. Connect the Leonardo to the computer with a USB cable. +//! +//! 3. Make sure [Ravedude](https://github.com/Rahix/avr-hal/tree/main/ravedude) +//! is installed. Then "run" the example to deploy it to the Arduino: +//! +//! ``` +//! cargo run --release --example arduino_keyboard +//! ``` +//! +//! 4. Open Notepad (or whatever editor or text input of your choosing). Press +//! the button (or if you are not using one, short D2 to GND with a jumper). You +//! should see it type "Hello World" + +#![no_std] +#![no_main] +#![feature(lang_items)] +#![feature(abi_avr_interrupt)] +#![deny(unsafe_op_in_unsafe_fn)] + +mod std_stub; + +use arduino_hal::{pac::PLL, port::{mode::{Input, Output, PullUp}, Pin}, usb::{AvrGenericUsbBus, SuspendNotifier}, AvrUsbBus}; + +use avr_device::{asm::sleep, interrupt}; + +use usb_device::{bus::UsbBusAllocator, descriptor::lang_id::LangID, device::UsbDevice, prelude::StringDescriptors}; +use usbd_hid::{ + descriptor::{KeyboardReport, SerializedDescriptor}, + hid_class::HIDClass, +}; + +const PAYLOAD: &[u8] = b"Hello World"; + +#[arduino_hal::entry] +fn main() -> ! { + let dp = arduino_hal::Peripherals::take().unwrap(); + let pins = arduino_hal::pins!(dp); + let pll = dp.PLL; + let usb = dp.USB_DEVICE; + + let status = pins.d13.into_output(); + let trigger = pins.d2.into_pull_up_input(); + + // Configure PLL interface + // prescale 16MHz crystal -> 8MHz + pll.pllcsr.write(|w| w.pindiv().set_bit()); + // 96MHz PLL output; /1.5 for 64MHz timers, /2 for 48MHz USB + pll.pllfrq + .write(|w| w.pdiv().mhz96().plltm().factor_15().pllusb().set_bit()); + + // Enable PLL + pll.pllcsr.modify(|_, w| w.plle().set_bit()); + + // Check PLL lock + while pll.pllcsr.read().plock().bit_is_clear() {} + + let usb_bus: &UsbBusAllocator> = arduino_hal::default_usb_bus!(usb, pll); + + let hid_class: HIDClass> = HIDClass::new(usb_bus, KeyboardReport::desc(), 1); + let strings = StringDescriptors::new(LangID::EN) + .manufacturer("Foo") + .product("Bar"); + let usb_device: UsbDevice> = arduino_hal::default_usb_device!(usb_bus, 0x1209, 0x0001, strings); + + unsafe { + USB_CTX = Some(UsbContext { + usb_device, + hid_class, + current_index: 0, + pressed: false, + indicator: status.downgrade(), + trigger: trigger.downgrade(), + }); + } + + unsafe { interrupt::enable() }; + loop { + sleep(); + } +} + +static mut USB_CTX: Option = None; + +#[interrupt(atmega32u4)] +fn USB_GEN() { + unsafe { poll_usb() }; +} + +#[interrupt(atmega32u4)] +fn USB_COM() { + unsafe { poll_usb() }; +} + +/// # Safety +/// +/// This function assumes that it is being called within an +/// interrupt context. +unsafe fn poll_usb() { + // Safety: There must be no other overlapping borrows of USB_CTX. + // - By the safety contract of this function, we are in an interrupt + // context. + // - The main thread is not borrowing USB_CTX. The only access is the + // assignment during initialization. It cannot overlap because it is + // before the call to `interrupt::enable()`. + // - No other interrupts are accessing USB_CTX, because no other interrupts + // are in the middle of execution. GIE is automatically cleared for the + // duration of the interrupt, and is not re-enabled within any ISRs. + let ctx = unsafe { USB_CTX.as_mut().unwrap() }; + ctx.poll(); +} + +struct UsbContext { + usb_device: UsbDevice<'static, AvrGenericUsbBus>, + hid_class: HIDClass<'static, AvrGenericUsbBus>, + current_index: usize, + pressed: bool, + indicator: Pin, + trigger: Pin>, +} + +impl UsbContext { + fn poll(&mut self) { + if self.trigger.is_low() { + let next_report = if self.pressed { + BLANK_REPORT + } else { + PAYLOAD + .get(self.current_index) + .copied() + .and_then(ascii_to_report) + .unwrap_or(BLANK_REPORT) + }; + + if self.hid_class.push_input(&next_report).is_ok() { + if self.pressed && self.current_index < PAYLOAD.len() { + self.current_index += 1; + } + self.pressed = !self.pressed; + } + } else { + self.current_index = 0; + self.pressed = false; + self.hid_class.push_input(&BLANK_REPORT).ok(); + } + + if self.usb_device.poll(&mut [&mut self.hid_class]) { + let mut report_buf = [0u8; 1]; + + if self.hid_class.pull_raw_output(&mut report_buf).is_ok() { + if report_buf[0] & 2 != 0 { + self.indicator.set_high(); + } else { + self.indicator.set_low(); + } + } + } + } +} + +const BLANK_REPORT: KeyboardReport = KeyboardReport { + modifier: 0, + reserved: 0, + leds: 0, + keycodes: [0; 6], +}; + +fn ascii_to_report(c: u8) -> Option { + let (keycode, shift) = if c.is_ascii_alphabetic() { + (c.to_ascii_lowercase() - b'a' + 0x04, c.is_ascii_uppercase()) + } else { + match c { + b' ' => (0x2c, false), + _ => return None, + } + }; + + let mut report = BLANK_REPORT; + if shift { + report.modifier |= 0x2; + } + report.keycodes[0] = keycode; + Some(report) +} diff --git a/examples/arduino-leonardo/src/bin/std_stub/mod.rs b/examples/arduino-leonardo/src/bin/std_stub/mod.rs new file mode 100644 index 0000000000..5fd154a7ba --- /dev/null +++ b/examples/arduino-leonardo/src/bin/std_stub/mod.rs @@ -0,0 +1,65 @@ +#![feature(panic_info_message)] +//! Replacement for avr-std-stub with a custom panic handler. + +// use core::fmt::{self, Debug}; +use core::panic::PanicInfo; + +use arduino_hal::hal::port::{PD2, PD3}; +use arduino_hal::hal::Atmega; +use arduino_hal::pac::USART1; +use arduino_hal::port::mode::{Input, Output}; +use arduino_hal::port::Pin; +use arduino_hal::{prelude::*, Usart}; +use arduino_hal::{delay_ms, pins, Peripherals}; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + // use ::core::fmt::Write as _; + + let dp = unsafe { Peripherals::steal() }; + let pins = pins!(dp); + let mut status = pins.d13.into_output(); + // let mut serial = arduino_hal::default_serial!(dp, pins, 57600); + + // struct UartWriter { + // uart: Usart, Pin> + // } + // impl ::core::fmt::Write for UartWriter { + // fn write_str(&mut self, s: &str) -> ::core::fmt::Result { + // ufmt::uwriteln!(&mut self.uart, "{}", s).unwrap_infallible(); + // Ok(()) + // } + // } + + // ufmt::uwriteln!(&mut serial, "I panicked at {}!\r", _info.location().unwrap().file()).unwrap_infallible(); + + // delay_ms(100); + + // ufmt::uwriteln!(&mut serial, "I panicked on {}!\r", _info.location().unwrap().line()).unwrap_infallible(); + + // if let Some(s) = _info.payload().downcast_ref::<&str>() { + // ufmt::uwriteln!(&mut serial, "More info: {}!\r", s).unwrap_infallible(); + // } + + // let mut uart = UartWriter { uart: serial }; + // ::core::writeln!(uart, "{}", _info).ok(); + + loop { + status.set_high(); + delay_ms(100); + status.set_low(); + delay_ms(100); + status.set_high(); + delay_ms(100); + status.set_low(); + delay_ms(100); + status.set_high(); + delay_ms(400); + status.set_low(); + delay_ms(1000); + } +} + +#[lang = "eh_personality"] +#[no_mangle] +pub unsafe extern "C" fn rust_eh_personality() {} \ No newline at end of file diff --git a/mcu/atmega-hal/src/lib.rs b/mcu/atmega-hal/src/lib.rs index e2ce9bf2e1..0403f9da45 100644 --- a/mcu/atmega-hal/src/lib.rs +++ b/mcu/atmega-hal/src/lib.rs @@ -145,6 +145,17 @@ pub mod eeprom; #[cfg(feature = "device-selected")] pub use eeprom::Eeprom; +#[cfg(any( + feature = "atmega8u2", + feature = "atmega32u4", +))] +pub mod usb; +#[cfg(any( + feature = "atmega8u2", + feature = "atmega32u4", +))] +pub use usb::AvrUsbBus; + pub struct Atmega; #[cfg(any(feature = "atmega48p", feature = "atmega168", feature = "atmega328p"))] diff --git a/mcu/atmega-hal/src/usb.rs b/mcu/atmega-hal/src/usb.rs new file mode 100644 index 0000000000..1598bf59b9 --- /dev/null +++ b/mcu/atmega-hal/src/usb.rs @@ -0,0 +1,139 @@ +use avr_hal_generic::usb::create_usb_bus; + +// MARK: - Type Imports + +#[cfg(any(feature = "atmega32u4", feature = "atmega8u2"))] +use crate::pac::{ + usb_device::{udint, UDINT, ueintx, UEINTX}, + USB_DEVICE, PLL, +}; + +#[cfg(feature = "atmega32u4")] +use crate::pac::usb_device::{usbint, USBINT}; + +// MARK: - Constants by device + +#[cfg(feature = "atmega32u4")] +const MAX_ENDPOINTS: usize = 7; +#[cfg(feature = "atmega32u4")] +const ENDPOINT_MAX_BUFSIZE: [u16; MAX_ENDPOINTS] = [64, 256, 64, 64, 64, 64, 64]; +#[cfg(feature = "atmega32u4")] +const DPRAM_SIZE: u16 = 832; + +#[cfg(feature = "atmega8u2")] +const MAX_ENDPOINTS: usize = 5; +#[cfg(feature = "atmega8u2")] +const ENDPOINT_MAX_BUFSIZE: [u16; MAX_ENDPOINTS] = [64, 64, 64, 64, 64]; +#[cfg(feature = "atmega8u2")] +const DPRAM_SIZE: u16 = 176; + +create_usb_bus! { + AvrGenericUsbBus, // we have to pass the name along for visibility reasons + new, + with_suspend_notifier, + USB_DEVICE, + SuspendNotifier, + MAX_ENDPOINTS, + ENDPOINT_MAX_BUFSIZE, + DPRAM_SIZE, + cfg(feature = "atmega8u2"), + cfg(feature = "atmega32u4") +} + +/// Extension trait for conveniently clearing AVR interrupt flag registers. +/// +/// To clear an interrupt flag, a zero bit must be written. However, there are +/// several other hazards to take into consideration: +/// +/// 1. If you read-modify-write, it is possible that an interrupt flag will be +/// set by hardware in between the read and write, and writing the zero that +/// you previously read will clear that flag as well. So, use a default value +/// of all ones and specifically clear the bits you want. HOWEVER: +/// +/// 2. Some bits of the interrupt flag register are reserved, and it is +/// specified that they should not be written as ones. +/// +/// Implementers of this trait should provide an initial value to the callback +/// with all _known_ interrupt flags set to the value that has no effect (which +/// is 1, in most cases) +pub trait ClearInterrupts { // This trait must live here due to the orphan rule + type Writer; + + fn clear_interrupts(&self, f: F) + where + for<'w> F: FnOnce(&mut Self::Writer) -> &mut Self::Writer; +} + +impl ClearInterrupts for UDINT { + type Writer = udint::W; + + fn clear_interrupts(&self, f: F) + where + for<'w> F: FnOnce(&mut Self::Writer) -> &mut Self::Writer, + { + // Bits 1,7 reserved as do not set. Setting all other bits has no effect + self.write(|w| f(unsafe { w.bits(0x7d) })) + } +} + +impl ClearInterrupts for UEINTX { + type Writer = ueintx::W; + + fn clear_interrupts(&self, f: F) + where + for<'w> F: FnOnce(&mut Self::Writer) -> &mut Self::Writer, + { + // Bit 5 read-only. Setting all other bits has no effect, EXCEPT: + // - RXOUTI/KILLBK should not be set for "IN" endpoints (XXX end-user beware) + self.write(|w| f(unsafe { w.bits(0xdf) })) + } +} + +#[cfg(not(feature = "atmega8u2"))] +impl ClearInterrupts for USBINT { + type Writer = usbint::W; + + fn clear_interrupts(&self, f: F) + where + for<'w> F: FnOnce(&mut Self::Writer) -> &mut Self::Writer, + { + // Bits 7:1 are reserved as do not set. + self.write(|w| f(unsafe { w.bits(0x01) })) + } +} + +/// Receiver for handling suspend and resume events from the USB device. +/// +/// See [`UsbBus::with_suspend_notifier`] for more details. +pub trait SuspendNotifier: Send + Sized + 'static { + /// Called by `UsbBus` when the USB peripheral has been suspended and the + /// PLL is safe to shut down. + fn suspend(&self) {} + + /// Called by `UsbBus` when the USB peripheral is about to resume and is + /// waiting for PLL to be enabled. + /// + /// This function should block until PLL lock has been established. + fn resume(&self) {} +} + +impl SuspendNotifier for () {} + +impl SuspendNotifier for PLL { + fn suspend(&self) { + self.pllcsr.modify(|_, w| w.plle().clear_bit()); + } + + fn resume(&self) { + #[cfg(feature = "atmega32u4")] + self.pllcsr + .modify(|_, w| w.pindiv().set_bit().plle().set_bit()); + #[cfg(feature = "atmega8u2")] + self.pllcsr + .modify(|_, w| w.pllp().val_0x05().plle().set_bit()); // REVIEW: is val_0x05 or val_0x03 correct? or something else? + + while self.pllcsr.read().plock().bit_is_clear() {} + } +} + +pub type AvrUsbBus = AvrGenericUsbBus; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 270cc88989..3b2e62d072 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2024-03-22" +channel = "nightly-2024-07-22" components = [ "rust-src" ] profile = "minimal"