use embassy_executor::Spawner;
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig, InterruptHandler as AdcIrq};
use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Input, Pull};
use embassy_rp::peripherals::USB;
use embassy_rp::usb::{Driver, InterruptHandler as UsbIrq};
use embassy_usb::class::cdc_acm::{CdcAcmClass, State as CdcState};
use embassy_usb::class::hid::{HidReaderWriter, ReportId, RequestHandler, State as HidState};
use embassy_usb::control::OutResponse;
use embassy_usb::UsbDevice;
use static_cell::StaticCell;
bind_interrupts!(struct Irqs {
USBCTRL_IRQ => UsbIrq<USB>;
// ── HID Report Descriptor ──────────────────────────────────────────
const KEYBOARD_REPORT_DESC: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
// Modifier keys (8 bits: L-Ctrl, L-Shift, L-Alt, L-GUI, R-Ctrl, ...)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0xE0, // Usage Minimum (Left Control)
0x29, 0xE7, // Usage Maximum (Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
// Reserved byte (padding)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant)
// Key codes (6 key rollover)
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x81, 0x00, // Input (Data, Array)
// ── Keyboard Report Structure ──────────────────────────────────────
const fn empty() -> Self {
fn as_bytes(&self) -> &[u8; 8] {
unsafe { &*(self as *const Self as *const [u8; 8]) }
// ── HID Request Handler ────────────────────────────────────────────
impl RequestHandler for MyRequestHandler {
fn get_report(&mut self, _id: ReportId, _buf: &mut [u8]) -> Option<usize> {
fn set_report(&mut self, _id: ReportId, _data: &[u8]) -> OutResponse {
fn set_idle_ms(&mut self, _id: Option<ReportId>, _dur: u32) {}
fn get_idle_ms(&mut self, _id: Option<ReportId>) -> Option<u32> {
// ── USB Background Task ────────────────────────────────────────────
#[embassy_executor::task]
async fn usb_task(mut usb: UsbDevice<'static, Driver<'static, USB>>) {
// ── CDC Serial Task ────────────────────────────────────────────────
#[embassy_executor::task]
mut cdc: CdcAcmClass<'static, Driver<'static, USB>>,
mut adc: Adc<'static, embassy_rp::adc::Async>,
mut pot_pin: Channel<'static>,
cdc.wait_connection().await;
info!("CDC: host connected");
let raw = adc.read(&mut pot_pin).await.unwrap_or(0);
let mv = (raw as u32 * 3300) / 4095;
let len = format_reading(&mut buf, raw, mv);
if cdc.write_packet(&buf[..len]).await.is_err() {
info!("CDC: host disconnected");
Timer::after_millis(500).await;
fn format_reading(buf: &mut [u8], raw: u16, mv: u32) -> usize {
buf[pos..pos + prefix.len()].copy_from_slice(prefix);
pos += write_u32(&mut buf[pos..], raw as u32);
buf[pos..pos + mid.len()].copy_from_slice(mid);
pos += write_u32(&mut buf[pos..], mv);
buf[pos..pos + suffix.len()].copy_from_slice(suffix);
fn write_u32(buf: &mut [u8], val: u32) -> usize {
tmp[i] = b'0' + (n % 10) as u8;
// ── HID Keyboard Task ─────────────────────────────────────────────
#[embassy_executor::task]
hid: HidReaderWriter<'static, Driver<'static, USB>, 1, 8>,
mut button: Input<'static>,
let (reader, mut writer) = hid.split();
// We only use the writer; spawn a task to drain the reader
// so the host does not stall on SET_REPORT/OUTPUT reports.
// The string to type when the button is pressed
let message: &[u8] = b"Hello from Pico!";
// Wait for button press (falling edge: high to low)
button.wait_for_falling_edge().await;
info!("Button pressed, typing message");
let keycode = ascii_to_hid(ch);
let needs_shift = ch.is_ascii_uppercase()
|| matches!(ch, b'!' | b'@' | b'#' | b'$' | b'%'
| b'^' | b'&' | b'*' | b'(' | b')');
let report = KeyboardReport {
modifier: if needs_shift { 0x02 } else { 0x00 },
keycodes: [keycode, 0, 0, 0, 0, 0],
let _ = writer.write(report.as_bytes()).await;
// Small delay between key down and key up
Timer::after_millis(10).await;
let empty = KeyboardReport::empty();
let _ = writer.write(empty.as_bytes()).await;
// Delay between characters
Timer::after_millis(30).await;
// Debounce: wait before accepting the next press
Timer::after_millis(300).await;
/// Convert an ASCII character to a USB HID keycode.
fn ascii_to_hid(ch: u8) -> u8 {
b'a'..=b'z' => ch - b'a' + 0x04,
b'A'..=b'Z' => ch - b'A' + 0x04,
b'1'..=b'9' => ch - b'1' + 0x1E,
b'!' => 0x1E, // Shift + 1
b'@' => 0x1F, // Shift + 2
b'#' => 0x20, // Shift + 3
// ── Main ───────────────────────────────────────────────────────────
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// ── USB Device Configuration ───────────────────────────────────
let driver = Driver::new(p.USB, Irqs);
let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
config.manufacturer = Some("Pico Workshop");
config.product = Some("Pico CDC+Keyboard");
config.serial_number = Some("RUST-001");
config.max_packet_size_0 = 64;
// Composite device: class defined at interface level
config.device_class = 0xEF; // Miscellaneous
config.device_sub_class = 0x02; // Common Class
config.device_protocol = 0x01; // Interface Association Descriptor
// ── Static Buffers ─────────────────────────────────────────────
static CONFIG_DESC: StaticCell<[u8; 256]> = StaticCell::new();
static BOS_DESC: StaticCell<[u8; 256]> = StaticCell::new();
static MSOS_DESC: StaticCell<[u8; 256]> = StaticCell::new();
static CONTROL_BUF: StaticCell<[u8; 64]> = StaticCell::new();
static CDC_STATE: StaticCell<CdcState> = StaticCell::new();
static HID_STATE: StaticCell<HidState> = StaticCell::new();
let config_desc = CONFIG_DESC.init([0; 256]);
let bos_desc = BOS_DESC.init([0; 256]);
let msos_desc = MSOS_DESC.init([0; 256]);
let control_buf = CONTROL_BUF.init([0; 64]);
let cdc_state = CDC_STATE.init(CdcState::new());
let hid_state = HID_STATE.init(HidState::new());
// ── Build USB Device ───────────────────────────────────────────
let mut builder = embassy_usb::Builder::new(
// Add CDC ACM class (virtual serial port)
let cdc = CdcAcmClass::new(&mut builder, cdc_state, 64);
// Add HID class (keyboard)
let hid_config = embassy_usb::class::hid::Config {
report_descriptor: KEYBOARD_REPORT_DESC,
let hid = HidReaderWriter::<_, 1, 8>::new(&mut builder, hid_state, hid_config);
let usb = builder.build();
// ── Peripherals ────────────────────────────────────────────────
let adc = Adc::new(p.ADC, Irqs, AdcConfig::default());
let pot_pin = Channel::new_pin(p.PIN_26, Pull::None);
// Button on GP15 with internal pull-up
let button = Input::new(p.PIN_15, Pull::Up);
// ── Spawn Tasks ────────────────────────────────────────────────
spawner.spawn(usb_task(usb)).unwrap();
spawner.spawn(cdc_task(cdc, adc, pot_pin)).unwrap();
spawner.spawn(hid_task(hid, button)).unwrap();
info!("USB composite device started: CDC + HID Keyboard");
// Main task has nothing else to do; it can idle forever.
// The executor will run the spawned tasks.
Timer::after_millis(1000).await;
Comments