use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::fmt;
use super::ehci::{Extra, Hcd, PORT_RESET, Params, narrow_read, word_write};
use crate::bus::usb::{MAX_PORTS, UsbBus, buses};
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::LazyHandle;
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::Width;
use crate::core::wire::WireSource;
use crate::machine::realize::{BindCtx, Instance};
const CLASS_NAME: &str = "usb.chipidea";
const STATE_VERSION: u32 = 1;
pub const REGISTER_BYTES: u64 = 0x200;
pub const CAPABILITY_BASE: u64 = 0x100;
pub const CAPLENGTH: u8 = 0x40;
pub const OPERATIONAL_BASE: u64 = CAPABILITY_BASE + CAPLENGTH as u64;
pub const CX92755_ID: u32 = 0xfa05;
const DCIVERSION: u32 = 0x0001;
const DEVICE_ENDPOINTS: u32 = 8;
const REG_ID: u64 = 0x000;
const REG_HWGENERAL: u64 = 0x004;
const REG_HWHOST: u64 = 0x008;
const REG_HWDEVICE: u64 = 0x00c;
const REG_HWTXBUF: u64 = 0x010;
const REG_HWRXBUF: u64 = 0x014;
const REG_DCIVERSION: u64 = 0x120;
const REG_DCCPARAMS: u64 = 0x124;
const REG_TTCTRL: u64 = 0x15c;
const REG_BURSTSIZE: u64 = 0x160;
const REG_TXFILLTUNING: u64 = 0x164;
const REG_ULPI_VIEWPORT: u64 = 0x170;
const REG_OTGSC: u64 = 0x1a4;
const REG_USBMODE: u64 = 0x1a8;
const REG_DEVICE_BLOCK: u64 = 0x1ac;
const ULPI_RUN: u32 = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct VariantRegs {
ttctrl: u32,
ulpi: u32,
}
#[derive(Debug)]
pub struct ChipIdea {
hcd: Arc<Hcd>,
id: u32,
regs: Arc<Mutex<VariantRegs>>,
region: RegionRef,
}
impl ChipIdea {
pub fn new(props: &Props) -> Result<ChipIdea> {
let mut r = props.reader();
let bus_name = r.require_str("bus")?.to_string();
let ports = r.or_range("ports", 1u64, 1..=MAX_PORTS as u64)?;
let microframe = r.or_range("microframe", 7500u64, 1..=u64::from(u32::MAX))?;
let id = r.or_range("id", u64::from(CX92755_ID), 0..=u64::from(u32::MAX))?;
r.finish()?;
let bus = buses::attach(props, &bus_name, ports as u8)?;
if bus.port_count() < ports as u8 {
return Err(Error::Config {
at: String::from(CLASS_NAME),
message: alloc::format!(
"the USB bus `{bus_name}` already has {} ports and this controller asked for \
{ports}; the first object to name a bus fixes its size",
bus.port_count()
),
});
}
Ok(ChipIdea::with_bus(
bus,
Params {
ports: ports as u8,
microframe_ticks: microframe,
caplength: CAPLENGTH,
dual_role: true,
},
id as u32,
))
}
#[must_use]
pub fn with_bus(bus: Arc<UsbBus>, params: Params, id: u32) -> ChipIdea {
let params = Params {
caplength: CAPLENGTH,
dual_role: true,
..params
};
let hcd = Arc::new(Hcd::new(bus, params));
let regs = Arc::new(Mutex::with_rank(LockRank::DEVICE, VariantRegs::default()));
let port = Arc::new(ChipIdeaPort {
hcd: Arc::clone(&hcd),
regs: Arc::clone(®s),
id,
});
let region = Arc::new(Region::io(
"chipidea",
REGISTER_BYTES,
port as Arc<dyn MemOps>,
));
ChipIdea {
hcd,
id,
regs,
region,
}
}
#[must_use]
pub fn hcd(&self) -> &Arc<Hcd> {
&self.hcd
}
#[must_use]
pub fn id(&self) -> u32 {
self.id
}
}
pub mod pin {
pub const IRQ: &str = "irq";
}
struct ChipIdeaPort {
hcd: Arc<Hcd>,
regs: Arc<Mutex<VariantRegs>>,
id: u32,
}
impl fmt::Debug for ChipIdeaPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ChipIdeaPort")
.field("id", &self.id)
.finish_non_exhaustive()
}
}
impl ChipIdeaPort {
fn read_hw(&self, offset: u64) -> u32 {
let ports = u32::from(self.hcd.params().ports);
match offset {
REG_ID => self.id,
REG_HWGENERAL => 0,
REG_HWHOST => 1 | ((ports - 1) << 1),
REG_HWDEVICE => 1 | (DEVICE_ENDPOINTS << 1),
REG_HWTXBUF | REG_HWRXBUF => 0,
_ => 0,
}
}
fn read_cap(&self, offset: u64) -> u32 {
match offset {
REG_DCIVERSION => DCIVERSION,
REG_DCCPARAMS => DEVICE_ENDPOINTS | (1 << 7) | (1 << 8),
_ => self.hcd.read_cap(offset - CAPABILITY_BASE),
}
}
fn read_op(&self, offset: u64) -> u32 {
match offset {
REG_TTCTRL => self.regs.lock().ttctrl,
REG_BURSTSIZE => self.hcd.read_extra(Extra::BurstSize),
REG_TXFILLTUNING => self.hcd.read_extra(Extra::TxFillTuning),
REG_ULPI_VIEWPORT => self.regs.lock().ulpi & !ULPI_RUN,
REG_OTGSC => self.hcd.read_extra(Extra::Otgsc),
REG_USBMODE => self.hcd.read_extra(Extra::UsbMode),
_ if offset >= REG_DEVICE_BLOCK => 0,
_ => self.hcd.read_op(offset - OPERATIONAL_BASE),
}
}
}
impl MemOps for ChipIdeaPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
self.hcd.sync_for(attrs);
let aligned = offset & !0x3;
let value = if aligned < CAPABILITY_BASE {
self.read_hw(aligned)
} else if aligned < OPERATIONAL_BASE {
self.read_cap(aligned)
} else {
self.read_op(aligned)
};
narrow_read(offset, value, dst)
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
if attrs.debug {
return Err(BusError::BadAccess);
}
let Some(value) = word_write(src) else {
return Err(BusError::BadAccess);
};
if offset < OPERATIONAL_BASE {
return Ok(());
}
self.hcd.sync_for(attrs);
match offset {
REG_TTCTRL => {
self.regs.lock().ttctrl = value & 0x7f00_0000;
return Ok(());
}
REG_BURSTSIZE => {
let after = self.hcd.write_extra(Extra::BurstSize, value);
self.hcd.act(after);
return Ok(());
}
REG_TXFILLTUNING => {
let after = self.hcd.write_extra(Extra::TxFillTuning, value);
self.hcd.act(after);
return Ok(());
}
REG_ULPI_VIEWPORT => {
self.regs.lock().ulpi = value & !ULPI_RUN;
return Ok(());
}
REG_OTGSC => {
let after = self.hcd.write_extra(Extra::Otgsc, value);
self.hcd.act(after);
return Ok(());
}
REG_USBMODE => {
let after = self.hcd.write_extra(Extra::UsbMode, value);
self.hcd.act(after);
return Ok(());
}
_ if offset >= REG_DEVICE_BLOCK => return Ok(()),
_ => {}
}
let op = offset - OPERATIONAL_BASE;
let resetting = Hcd::port_at(op)
.map(|port| (port, self.hcd.portsc(port)))
.filter(|(_, sc)| sc & PORT_RESET != 0);
let after = self.hcd.write_op(op, value);
self.hcd.act(after);
if let Some((port, _)) = resetting
&& self.hcd.portsc(port) & PORT_RESET == 0
{
self.hcd.finish_reset(port);
self.hcd.refresh_irq();
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO
.with_widths(Width::U8, Width::U32)
.with_natural_alignment(true)
}
}
impl Device for ChipIdea {
fn class(&self) -> &'static DeviceClass {
&CHIPIDEA_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, kind: ResetKind) {
*self.regs.lock() = VariantRegs::default();
self.hcd.reset(kind);
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
self.hcd.save(w)?;
let regs = *self.regs.lock();
w.write_u32(regs.ttctrl)?;
w.write_u32(regs.ulpi)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
self.hcd.load(r)?;
let regs = VariantRegs {
ttctrl: r.read_u32()?,
ulpi: r.read_u32()?,
};
*self.regs.lock() = regs;
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != pin::IRQ {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!(
"a ChipIdea USB controller drives `{}` and nothing else",
pin::IRQ
),
});
}
self.hcd.connect_irq(source);
Ok(())
}
fn announce(&self, _port: &str) {
self.hcd.refresh_irq();
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.hcd.ticks()
}
fn advance_to(&self, tick: u64) {
self.hcd.advance_to(tick);
}
fn next_event_tick(&self) -> Option<u64> {
self.hcd.next_event_tick()
}
fn attach_lazy(&self, handle: LazyHandle) {
self.hcd.attach_lazy(handle);
}
}
impl Instance for ChipIdea {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| Error::Config {
at: ctx.path().to_string(),
message: String::from(
"a ChipIdea USB controller masters the bus its queue heads live on: add \
`space = mem` to the object",
),
})?;
self.hcd.attach_space(space, ctx.requester());
Ok(())
}
}
pub static CHIPIDEA_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "the ChipIdea/ARC dual-role USB controller: an EHCI core with its operational \
registers at +0x140, an ID register and a USBMODE role select",
properties: &[
PropertySpec {
name: "bus",
kind: ValueKind::Str,
required: true,
summary: "the named USB bus this controller is the root of",
},
PropertySpec {
name: "ports",
kind: ValueKind::Uint,
required: false,
summary: "how many root ports, 1 to 15 (default 1, which is the CX92755's)",
},
PropertySpec {
name: "microframe",
kind: ValueKind::Uint,
required: false,
summary: "clock-domain ticks in one 125 us microframe (default 7500, exact at 60 MHz)",
},
PropertySpec {
name: "id",
kind: ValueKind::Uint,
required: false,
summary: "what the ID register reads (default 0xfa05, the CX92755's)",
},
],
construct: |props| Ok(Box::new(ChipIdea::new(props)?)),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&CHIPIDEA_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(ChipIdea::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("bus", ValueKind::Str).required())
.prop(PropSchema::new("ports", ValueKind::Uint).range(1, MAX_PORTS as u64))
.prop(PropSchema::new("microframe", ValueKind::Uint).range(1, u64::from(u32::MAX)))
.prop(PropSchema::new("id", ValueKind::Uint).range(0, u64::from(u32::MAX)))
.port(pin::IRQ, PortDir::Out)
.region("")
.region("regs")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::usb::UsbBus;
fn build() -> ChipIdea {
ChipIdea::with_bus(
Arc::new(UsbBus::new(1)),
Params {
ports: 1,
microframe_ticks: 7500,
caplength: CAPLENGTH,
dual_role: true,
},
CX92755_ID,
)
}
fn ops(device: &ChipIdea) -> Arc<dyn MemOps> {
let region = device.region("").expect("the register block");
match region.kind() {
crate::core::space::RegionKind::Io(ops) => Arc::clone(ops),
other => panic!("expected an io region, got {other:?}"),
}
}
fn read32(device: &ChipIdea, offset: u64) -> u32 {
let mut bytes = [0u8; 4];
ops(device)
.read(offset, &mut bytes, MemAttrs::DEFAULT)
.expect("a register read");
u32::from_le_bytes(bytes)
}
fn write32(device: &ChipIdea, offset: u64, value: u32) {
ops(device)
.write(offset, &value.to_le_bytes(), MemAttrs::DEFAULT)
.expect("a register write");
}
#[test]
fn the_operational_registers_are_at_0x140() {
let device = build();
assert_eq!(
read32(&device, CAPABILITY_BASE) & 0xff,
u32::from(CAPLENGTH)
);
assert_eq!(CAPABILITY_BASE + u64::from(CAPLENGTH), 0x140);
assert_eq!(read32(&device, 0x140) >> 16, 8);
assert_eq!(read32(&device, 0x180), 0);
assert_ne!(read32(&device, 0x184), 0, "PORTSC1 reports port power");
}
#[test]
fn the_identification_register_is_the_cx92755s() {
let device = build();
let id = read32(&device, REG_ID);
assert_eq!(id, 0xfa05);
let core_id = id & 0x3f;
let nid = (id >> 8) & 0x3f;
assert_eq!(core_id, 5);
assert_eq!(nid, !core_id & 0x3f, "NID is the complement of ID");
}
#[test]
fn a_board_may_report_a_different_core_id() {
let device = ChipIdea::with_bus(Arc::new(UsbBus::new(1)), Params::default(), 0xfb04);
assert_eq!(read32(&device, REG_ID), 0xfb04);
}
#[test]
fn the_role_select_is_write_once_and_stops_the_host_schedule() {
let device = build();
assert_eq!(
read32(&device, REG_USBMODE) & 0x3,
super::super::ehci::MODE_IDLE
);
write32(&device, REG_USBMODE, super::super::ehci::MODE_DEVICE);
assert_eq!(
read32(&device, REG_USBMODE) & 0x3,
super::super::ehci::MODE_DEVICE
);
write32(&device, REG_USBMODE, super::super::ehci::MODE_HOST);
assert_eq!(
read32(&device, REG_USBMODE) & 0x3,
super::super::ehci::MODE_DEVICE,
"USBMODE.CM is write-once after a reset"
);
write32(&device, 0x140, 1 | (1 << 5));
assert_eq!(
device.hcd().next_event_tick(),
None,
"a controller in device mode must not walk a host schedule"
);
}
#[test]
fn host_mode_starts_the_schedule() {
let device = build();
write32(&device, REG_USBMODE, super::super::ehci::MODE_HOST);
write32(&device, 0x140, 1);
assert_eq!(device.hcd().next_event_tick(), Some(7500));
}
#[test]
fn the_device_controller_capabilities_are_reported() {
let device = build();
let dcc = read32(&device, REG_DCCPARAMS);
assert_eq!(dcc & 0x1f, DEVICE_ENDPOINTS, "endpoint count");
assert_ne!(dcc & (1 << 7), 0, "device capable");
assert_ne!(dcc & (1 << 8), 0, "host capable");
assert_eq!(read32(&device, REG_DCIVERSION) & 0xffff, DCIVERSION);
assert_ne!(read32(&device, REG_HWHOST) & 1, 0, "host capable");
assert_ne!(read32(&device, REG_HWDEVICE) & 1, 0, "device capable");
}
#[test]
fn the_ulpi_window_never_leaves_a_poll_spinning() {
let device = build();
write32(&device, REG_ULPI_VIEWPORT, ULPI_RUN | 0x1234_0000 | 0x5a);
let value = read32(&device, REG_ULPI_VIEWPORT);
assert_eq!(value & ULPI_RUN, 0, "the access completed");
assert_eq!(value & 0xff, 0x5a, "the data byte reads back");
}
#[test]
fn a_debug_write_is_refused() {
let device = build();
assert!(
ops(&device)
.write(0x144, &0xffu32.to_le_bytes(), MemAttrs::DEBUG)
.is_err(),
"USBSTS is write-1-to-clear; a debugger must not acknowledge an interrupt"
);
}
const FW_USBCMD: u64 = 0x140;
const FW_USBSTS: u64 = 0x144;
const FW_USBINTR: u64 = 0x148;
const FW_USBMODE: u64 = 0x1a8;
const RUN_STOP: u32 = 1 << 0;
const HC_RESET: u32 = 1 << 1;
const HC_HALTED: u32 = 1 << 12;
#[test]
fn the_firmwares_reset_handshake_completes_in_the_documented_order() {
let device = build();
assert_eq!(FW_USBMODE, REG_USBMODE);
assert_eq!(FW_USBCMD, OPERATIONAL_BASE);
assert_ne!(
read32(&device, FW_USBSTS) & HC_HALTED,
0,
"step 1: a controller out of reset is halted"
);
write32(&device, FW_USBCMD, HC_RESET);
assert_eq!(
read32(&device, FW_USBCMD) & HC_RESET,
0,
"step 2: HCReset self-clears"
);
assert_ne!(
read32(&device, FW_USBSTS) & HC_HALTED,
0,
"step 2: and the reset leaves the controller halted"
);
write32(&device, FW_USBMODE, super::super::ehci::MODE_HOST);
assert_eq!(
read32(&device, FW_USBMODE) & 0x3,
super::super::ehci::MODE_HOST,
"step 3: the role select reads back what was written"
);
assert_eq!(
read32(&device, REG_ID) & 0xffff,
0xfa05,
"step 4: (ID & 0xFFFF) == 0xFA05"
);
write32(&device, FW_USBINTR, 0x3f);
assert_eq!(read32(&device, FW_USBINTR), 0x3f);
}
#[test]
fn hchalted_is_the_complement_of_runstop() {
let device = build();
write32(&device, FW_USBMODE, super::super::ehci::MODE_HOST);
assert_ne!(read32(&device, FW_USBSTS) & HC_HALTED, 0);
write32(&device, FW_USBCMD, RUN_STOP);
assert_eq!(read32(&device, FW_USBSTS) & HC_HALTED, 0);
write32(&device, FW_USBCMD, 0);
assert_ne!(read32(&device, FW_USBSTS) & HC_HALTED, 0);
}
#[test]
fn a_reset_re_arms_the_role_select_so_a_switch_works() {
let device = build();
write32(&device, FW_USBMODE, super::super::ehci::MODE_HOST);
assert_eq!(
read32(&device, FW_USBMODE) & 0x3,
super::super::ehci::MODE_HOST
);
write32(&device, FW_USBCMD, HC_RESET);
assert_eq!(
read32(&device, FW_USBMODE) & 0x3,
super::super::ehci::MODE_IDLE,
"HCReset re-arms the write-once role select"
);
write32(&device, FW_USBMODE, super::super::ehci::MODE_DEVICE);
assert_eq!(
read32(&device, FW_USBMODE) & 0x3,
super::super::ehci::MODE_DEVICE,
"so the read-back the firmware spins on returns the new role"
);
}
#[test]
fn the_block_base_is_the_id_register_not_the_operational_ones() {
let device = build();
assert_eq!(read32(&device, 0) & 0xffff, 0xfa05);
assert_eq!(OPERATIONAL_BASE, 0x140);
}
#[test]
fn the_variant_registers_round_trip() {
let device = build();
write32(&device, REG_USBMODE, super::super::ehci::MODE_HOST);
write32(&device, REG_TTCTRL, 0x7f00_0000);
write32(&device, REG_ULPI_VIEWPORT, 0x0012_0034);
write32(&device, REG_BURSTSIZE, 0x1010);
let mut first = alloc::vec::Vec::new();
device.hcd.save(&mut first).expect("it saves");
let regs = *device.regs.lock();
first.extend_from_slice(®s.ttctrl.to_le_bytes());
first.extend_from_slice(®s.ulpi.to_le_bytes());
let fresh = build();
{
let mut reader = crate::core::state::ChunkReader::new(&first);
fresh.hcd.load(&mut reader).expect("it loads");
let restored = VariantRegs {
ttctrl: reader.read_u32().expect("ttctrl"),
ulpi: reader.read_u32().expect("ulpi"),
};
*fresh.regs.lock() = restored;
}
assert_eq!(read32(&fresh, REG_TTCTRL), read32(&device, REG_TTCTRL));
assert_eq!(
read32(&fresh, REG_ULPI_VIEWPORT),
read32(&device, REG_ULPI_VIEWPORT)
);
assert_eq!(read32(&fresh, REG_USBMODE), read32(&device, REG_USBMODE));
assert_eq!(read32(&fresh, 0x140), read32(&device, 0x140));
}
}