use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use super::{BitOrder, ChipSelect, Format, Link, MAX_CHIP_SELECTS, Mode, SpiBus, buses};
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind, SinkPin};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicBool, AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireId, WireSink, WireSource};
use crate::machine::realize::Instance;
const CLASS_NAME: &str = "spi.controller";
const STATE_VERSION: u32 = 1;
pub const REGISTER_BYTES: u64 = 0x18;
const CTRL_EN: u32 = 1 << 0;
const CTRL_CPOL: u32 = 1 << 1;
const CTRL_CPHA: u32 = 1 << 2;
const CTRL_LSB: u32 = 1 << 3;
const CTRL_BITS_SHIFT: u32 = 8;
const CTRL_BITS_MASK: u32 = 0x1f;
const CTRL_MASK: u32 =
CTRL_EN | CTRL_CPOL | CTRL_CPHA | CTRL_LSB | (CTRL_BITS_MASK << CTRL_BITS_SHIFT);
const STATUS_BUSY: u32 = 1 << 0;
const STATUS_RXVALID: u32 = 1 << 1;
const LINES_SCK: u32 = 1 << 0;
const LINES_MOSI: u32 = 1 << 1;
const LINES_CS: u32 = 1 << 2;
const LINES_MISO: u32 = 1 << 8;
const LINES_WRITABLE: u32 = LINES_SCK | LINES_MOSI | LINES_CS;
pub mod pin {
pub const SCK: &str = "sck";
pub const MOSI: &str = "mosi";
pub const MISO: &str = "miso";
pub const CS_PREFIX: &str = "cs";
pub const MISO_LINE: u32 = 0;
}
const NO_EVENT: u64 = u64::MAX;
#[derive(Debug)]
pub struct SpiController {
shared: Arc<Shared>,
region: RegionRef,
}
struct Shared {
state: Mutex<State>,
link: Link,
bus: Option<Arc<SpiBus>>,
chip_selects: u8,
ticks: AtomicU64,
next_event: AtomicU64,
miso: AtomicBool,
pins: Mutex<Pins>,
miso_pin: Mutex<Option<Arc<MisoSink>>>,
lazy: Mutex<Option<LazyHandle>>,
}
#[derive(Debug, Default)]
struct Pins {
sck: Option<WireSource>,
mosi: Option<WireSource>,
cs: [Option<WireSource>; MAX_CHIP_SELECTS],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct State {
ticks: u64,
ctrl: u32,
clkdiv: u32,
cs: u32,
lines: u32,
tx: u32,
rx: u32,
rx_valid: bool,
busy: bool,
started: u64,
edges: u32,
shift_in: u32,
}
impl Default for State {
fn default() -> State {
State {
ticks: 0,
ctrl: (7 << CTRL_BITS_SHIFT),
clkdiv: 0,
cs: 0,
lines: LINES_CS,
tx: 0,
rx: 0,
rx_valid: false,
busy: false,
started: 0,
edges: 0,
shift_in: 0,
}
}
}
impl State {
fn format(&self) -> Format {
Format::new(
Mode::from_cpol_cpha(self.ctrl & CTRL_CPOL != 0, self.ctrl & CTRL_CPHA != 0),
((self.ctrl >> CTRL_BITS_SHIFT) & CTRL_BITS_MASK) as u8 + 1,
if self.ctrl & CTRL_LSB != 0 {
BitOrder::LsbFirst
} else {
BitOrder::MsbFirst
},
)
}
fn half_period(&self) -> u64 {
u64::from(self.clkdiv) + 1
}
fn total_edges(&self) -> u32 {
u32::from(self.format().bits) * 2
}
fn end_tick(&self) -> u64 {
self.started
.saturating_add(u64::from(self.total_edges()) * self.half_period())
}
fn selected(&self) -> Option<ChipSelect> {
if self.cs == 0 {
return None;
}
Some(ChipSelect(self.cs.trailing_zeros() as u8))
}
fn status(&self) -> u32 {
let mut s = 0;
if self.busy {
s |= STATUS_BUSY;
}
if self.rx_valid {
s |= STATUS_RXVALID;
}
s
}
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Shared");
s.field("link", &self.link);
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl SpiController {
pub fn new(props: &Props) -> Result<SpiController> {
let mut r = props.reader();
let link_name = r.require_str("link")?.to_string();
let bus_name = r.optional_str("bus")?.map(ToString::to_string);
let chip_selects: u64 = r.or("chip-selects", 1)?;
r.finish()?;
let link = Link::from_name(&link_name).ok_or_else(|| Error::Config {
at: String::from(CLASS_NAME),
message: alloc::format!(
"`link` is `{link_name}`; it must be one of {:?} — see docs/buses/low-speed.md \
for which to pick",
Link::NAMES
),
})?;
if !(1..=MAX_CHIP_SELECTS as u64).contains(&chip_selects) {
return Err(Error::Config {
at: String::from(CLASS_NAME),
message: alloc::format!(
"`chip-selects` is {chip_selects}; an SPI bus routes 1 to {MAX_CHIP_SELECTS}"
),
});
}
if link == Link::Transactional && bus_name.is_none() {
return Err(Error::Config {
at: String::from(CLASS_NAME),
message: String::from(
"a `transactional` controller reaches its slaves through a named bus; \
give it `bus = \"spi0\"` and name the same bus on each slave",
),
});
}
let bus = bus_name
.as_deref()
.map(|name| buses::attach(props, name))
.transpose()?;
Ok(SpiController::with_bus(link, bus, chip_selects as u8))
}
#[must_use]
pub fn with_bus(link: Link, bus: Option<Arc<SpiBus>>, chip_selects: u8) -> SpiController {
let chip_selects = chip_selects.clamp(1, MAX_CHIP_SELECTS as u8);
let shared = Arc::new(Shared {
state: Mutex::with_rank(LockRank::DEVICE, State::default()),
link,
bus,
chip_selects,
ticks: AtomicU64::new(0),
next_event: AtomicU64::new(NO_EVENT),
miso: AtomicBool::new(true),
pins: Mutex::with_rank(LockRank::WIRE, Pins::default()),
miso_pin: Mutex::with_rank(LockRank::WIRE, None),
lazy: Mutex::with_rank(LockRank::WIRE, None),
});
let port = Arc::new(ControllerPort {
shared: Arc::clone(&shared),
});
let region = Arc::new(Region::io("spi", REGISTER_BYTES, port as Arc<dyn MemOps>));
SpiController { shared, region }
}
#[must_use]
pub fn link(&self) -> Link {
self.shared.link
}
#[must_use]
pub fn bus(&self) -> Option<&Arc<SpiBus>> {
self.shared.bus.as_ref()
}
#[must_use]
pub fn ticks(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
#[must_use]
pub fn busy(&self) -> bool {
self.shared.state.lock().busy
}
#[must_use]
pub fn rx(&self) -> (u32, bool) {
let state = self.shared.state.lock();
(state.rx, state.rx_valid)
}
#[must_use]
pub fn format(&self) -> Format {
self.shared.state.lock().format()
}
pub fn advance_to(&self, target: u64) {
self.shared.advance_to(target);
}
}
#[derive(Debug, Clone, Copy)]
enum Emit {
Sck(Level),
Mosi(Level),
Cs(ChipSelect, Level),
}
impl Shared {
fn publish(&self, state: &State) {
self.ticks.store(state.ticks, Ordering::Relaxed);
self.next_event
.store(Shared::next_event(state), Ordering::Relaxed);
}
fn next_event(state: &State) -> u64 {
if !state.busy {
return NO_EVENT;
}
let half = state.half_period();
let next = state
.started
.saturating_add((u64::from(state.edges) + 1) * half);
next.max(state.ticks.saturating_add(1))
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
fn emit(&self, action: Emit) {
let port = {
let pins = self.pins.lock();
match action {
Emit::Sck(_) => pins.sck.clone(),
Emit::Mosi(_) => pins.mosi.clone(),
Emit::Cs(cs, _) => pins.cs.get(usize::from(cs.0)).and_then(|p| p.clone()),
}
};
let level = match action {
Emit::Sck(l) | Emit::Mosi(l) => l,
Emit::Cs(_, l) => l,
};
if let Some(port) = port {
port.set(level);
}
}
fn announce_all(&self) {
let (sck, mosi, cs, selected, count) = {
let state = self.state.lock();
(
Level::from_bool(state.lines & LINES_SCK != 0),
Level::from_bool(state.lines & LINES_MOSI != 0),
Level::from_bool(state.lines & LINES_CS != 0),
state.selected(),
self.chip_selects,
)
};
self.emit(Emit::Sck(sck));
self.emit(Emit::Mosi(mosi));
for i in 0..count {
let this = ChipSelect(i);
let level = if selected == Some(this) || (i == 0 && cs.is_low()) {
Level::Low
} else {
Level::High
};
self.emit(Emit::Cs(this, level));
}
}
fn drive_chip_selects(&self, selected: Option<ChipSelect>) {
for i in 0..self.chip_selects {
let this = ChipSelect(i);
let level = if selected == Some(this) {
Level::Low
} else {
Level::High
};
self.emit(Emit::Cs(this, level));
}
}
fn select_transactional(&self, selected: Option<ChipSelect>) {
if let Some(bus) = &self.bus {
bus.select(selected);
}
}
fn begin(state: &mut State, word: u32) -> bool {
if state.ctrl & CTRL_EN == 0 || state.busy {
return false;
}
let format = state.format();
state.tx = format.truncate(word);
state.busy = true;
state.started = state.ticks;
state.edges = 0;
state.shift_in = 0;
true
}
fn tx_bit(state: &State, n: u32) -> Level {
let format = state.format();
let bit = match format.order {
BitOrder::MsbFirst => (state.tx >> (u32::from(format.bits) - 1 - n)) & 1,
BitOrder::LsbFirst => (state.tx >> n) & 1,
};
Level::from_bool(bit != 0)
}
fn capture(state: &mut State, n: u32, level: Level) {
let format = state.format();
if !level.as_bool() {
return;
}
match format.order {
BitOrder::MsbFirst => state.shift_in |= 1 << (u32::from(format.bits) - 1 - n),
BitOrder::LsbFirst => state.shift_in |= 1 << n,
}
}
fn advance_to(&self, target: u64) {
loop {
enum Step {
Done,
Edges(Vec<Emit>),
Word(u32),
}
let step = {
let mut state = self.state.lock();
if target <= state.ticks && !state.busy {
state.ticks = state.ticks.max(target);
self.publish(&state);
Step::Done
} else if !state.busy {
state.ticks = target;
self.publish(&state);
Step::Done
} else {
let half = state.half_period();
match self.link {
Link::Transactional => {
let end = state.end_tick();
if end > target {
state.ticks = target;
self.publish(&state);
Step::Done
} else {
state.ticks = end;
let word = state.tx;
Step::Word(word)
}
}
Link::Wired => {
let edge_at = state
.started
.saturating_add((u64::from(state.edges) + 1) * half);
if edge_at > target {
state.ticks = target;
self.publish(&state);
Step::Done
} else {
state.ticks = edge_at;
let k = state.edges;
let format = state.format();
let idle = format.mode.idle_level();
let level = if k.is_multiple_of(2) {
idle.inverted()
} else {
idle
};
let bit = k / 2;
let mut out = Vec::new();
if format.mode.samples_on(level) {
let miso = Level::from_bool(self.miso.load(Ordering::Relaxed));
Shared::capture(&mut state, bit, miso);
} else {
let next = k.div_ceil(2).min(u32::from(format.bits) - 1);
out.push(Emit::Mosi(Shared::tx_bit(&state, next)));
}
out.push(Emit::Sck(level));
state.lines = (state.lines & !LINES_SCK)
| if level.is_high() { LINES_SCK } else { 0 };
state.edges += 1;
if state.edges >= state.total_edges() {
state.busy = false;
state.rx = format.truncate(state.shift_in);
state.rx_valid = true;
}
self.publish(&state);
Step::Edges(out)
}
}
}
}
};
match step {
Step::Done => return,
Step::Edges(actions) => {
for action in actions {
self.emit(action);
}
}
Step::Word(word) => {
let reply = self.bus.as_ref().map_or(u32::MAX, |bus| bus.transfer(word));
let mut state = self.state.lock();
let format = state.format();
state.rx = format.truncate(reply);
state.rx_valid = true;
state.busy = false;
self.publish(&state);
}
}
}
}
fn present_first_bit(&self) {
if self.link != Link::Wired {
return;
}
let level = {
let state = self.state.lock();
if !state.busy {
return;
}
Shared::tx_bit(&state, 0)
};
{
let mut state = self.state.lock();
state.lines =
(state.lines & !LINES_MOSI) | if level.is_high() { LINES_MOSI } else { 0 };
}
self.emit(Emit::Mosi(level));
}
}
struct ControllerPort {
shared: Arc<Shared>,
}
impl fmt::Debug for ControllerPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ControllerPort").finish_non_exhaustive()
}
}
impl ControllerPort {
fn read_register(&self, offset: u64, debug: bool) -> u32 {
let mut state = self.shared.state.lock();
match offset {
0x00 => state.ctrl,
0x04 => state.clkdiv,
0x08 => state.cs,
0x0c => state.status(),
0x10 => {
let value = state.rx;
if !debug {
state.rx_valid = false;
}
value
}
0x14 => {
let mut lines = state.lines & LINES_WRITABLE;
if self.shared.miso.load(Ordering::Relaxed) {
lines |= LINES_MISO;
}
lines
}
_ => 0,
}
}
fn write_register(&self, offset: u64, value: u32) -> AfterWrite {
let mut state = self.shared.state.lock();
match offset {
0x00 => {
let was = state.format();
state.ctrl = value & CTRL_MASK;
if state.busy && state.format() != was {
state.busy = false;
state.edges = 0;
}
let idle = state.format().mode.idle_level();
state.lines =
(state.lines & !LINES_SCK) | if idle.is_high() { LINES_SCK } else { 0 };
self.shared.publish(&state);
AfterWrite::Announce
}
0x04 => {
state.clkdiv = value;
self.shared.publish(&state);
AfterWrite::Nothing
}
0x08 => {
state.cs = value & ((1u32 << self.shared.chip_selects) - 1);
let selected = state.selected();
state.lines =
(state.lines & !LINES_CS) | if selected.is_some() { 0 } else { LINES_CS };
self.shared.publish(&state);
AfterWrite::Select(selected)
}
0x0c => AfterWrite::Nothing,
0x10 => {
let started = Shared::begin(&mut state, value);
self.shared.publish(&state);
if started {
AfterWrite::Started
} else {
AfterWrite::Nothing
}
}
0x14 => {
state.lines = (state.lines & !LINES_WRITABLE) | (value & LINES_WRITABLE);
let sck = Level::from_bool(state.lines & LINES_SCK != 0);
let mosi = Level::from_bool(state.lines & LINES_MOSI != 0);
let cs = Level::from_bool(state.lines & LINES_CS != 0);
AfterWrite::Lines { sck, mosi, cs }
}
_ => AfterWrite::Nothing,
}
}
}
#[derive(Debug, Clone, Copy)]
enum AfterWrite {
Nothing,
Announce,
Select(Option<ChipSelect>),
Started,
Lines {
sck: Level,
mosi: Level,
cs: Level,
},
}
impl MemOps for ControllerPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
if dst.len() != 4 || !offset.is_multiple_of(4) {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
let value = self.read_register(offset, attrs.debug);
dst.copy_from_slice(&value.to_le_bytes());
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
if src.len() != 4 || !offset.is_multiple_of(4) {
return Err(BusError::BadAccess);
}
if attrs.debug {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
let value = u32::from_le_bytes([src[0], src[1], src[2], src[3]]);
match self.write_register(offset, value) {
AfterWrite::Nothing => {}
AfterWrite::Announce => self.shared.announce_all(),
AfterWrite::Select(selected) => match self.shared.link {
Link::Transactional => self.shared.select_transactional(selected),
Link::Wired => self.shared.drive_chip_selects(selected),
},
AfterWrite::Started => self.shared.present_first_bit(),
AfterWrite::Lines { sck, mosi, cs } => {
self.shared.emit(Emit::Mosi(mosi));
let selected = self.shared.state.lock().selected();
match (self.shared.link, selected) {
(Link::Wired, _) => {
self.shared.emit(Emit::Cs(ChipSelect(0), cs));
}
(Link::Transactional, _) => {
}
}
self.shared.emit(Emit::Sck(sck));
}
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U32, Endian::Little)
}
}
struct MisoSink {
shared: Arc<Shared>,
}
impl fmt::Debug for MisoSink {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MisoSink").finish_non_exhaustive()
}
}
impl WireSink for MisoSink {
fn set_level(&self, _src: WireId, _line: u32, level: Level) {
self.shared.miso.store(level.as_bool(), Ordering::Relaxed);
}
}
impl Device for SpiController {
fn class(&self) -> &'static DeviceClass {
&SPI_CONTROLLER_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
{
let mut state = self.shared.state.lock();
let ticks = state.ticks;
*state = State {
ticks,
..State::default()
};
self.shared.publish(&state);
}
self.shared.miso.store(true, Ordering::Relaxed);
self.shared.select_transactional(None);
self.shared.announce_all();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = *self.shared.state.lock();
w.write_u64(state.ticks)?;
w.write_u32(state.ctrl)?;
w.write_u32(state.clkdiv)?;
w.write_u32(state.cs)?;
w.write_u32(state.lines)?;
w.write_u32(state.tx)?;
w.write_u32(state.rx)?;
w.write_bool(state.rx_valid)?;
w.write_bool(state.busy)?;
w.write_u64(state.started)?;
w.write_u32(state.edges)?;
w.write_u32(state.shift_in)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let state = State {
ticks: r.read_u64()?,
ctrl: r.read_u32()?,
clkdiv: r.read_u32()?,
cs: r.read_u32()?,
lines: r.read_u32()?,
tx: r.read_u32()?,
rx: r.read_u32()?,
rx_valid: r.read_bool()?,
busy: r.read_bool()?,
started: r.read_u64()?,
edges: r.read_u32()?,
shift_in: r.read_u32()?,
};
{
let mut slot = self.shared.state.lock();
*slot = state;
self.shared.publish(&slot);
}
self.shared.select_transactional(state.selected());
self.shared.announce_all();
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn sink(&self, port: &str, _sources: &[WireId]) -> Option<SinkPin> {
if port != pin::MISO {
return None;
}
let pin = Arc::new(MisoSink {
shared: Arc::clone(&self.shared),
});
*self.shared.miso_pin.lock() = Some(Arc::clone(&pin));
Some(SinkPin {
sink: pin as Arc<dyn WireSink>,
line: pin::MISO_LINE,
})
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
let mut pins = self.shared.pins.lock();
match port {
pin::SCK => pins.sck = Some(source),
pin::MOSI => pins.mosi = Some(source),
_ => {
let index = port
.strip_prefix(pin::CS_PREFIX)
.and_then(|n| n.parse::<usize>().ok())
.filter(|n| *n < usize::from(self.shared.chip_selects));
match index {
Some(index) => pins.cs[index] = Some(source),
None => {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!(
"an SPI controller drives `{}`, `{}` and `{}0`..`{}{}`",
pin::SCK,
pin::MOSI,
pin::CS_PREFIX,
pin::CS_PREFIX,
self.shared.chip_selects - 1
),
});
}
}
}
}
drop(pins);
self.shared.announce_all();
Ok(())
}
fn announce(&self, _port: &str) {
self.shared.announce_all();
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
SpiController::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
match self.shared.next_event.load(Ordering::Relaxed) {
NO_EVENT => None,
tick => Some(tick),
}
}
fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
}
impl Instance for SpiController {}
pub static SPI_CONTROLLER_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "a generic memory-mapped SPI controller: four modes, 1-32 bit words, \
eight chip selects, transactional or wired",
properties: &[
PropertySpec {
name: "link",
kind: ValueKind::Str,
required: true,
summary: "how words reach the slaves: `transactional` or `wired`",
},
PropertySpec {
name: "bus",
kind: ValueKind::Str,
required: false,
summary: "the named SPI bus this controller drives, for `transactional`",
},
PropertySpec {
name: "chip-selects",
kind: ValueKind::Uint,
required: false,
summary: "how many `csN` outputs, 1 to 8 (default 1)",
},
],
construct: |props| Ok(Box::new(SpiController::new(props)?)),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&SPI_CONTROLLER_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(SpiController::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
let mut schema = ClassSchema::new(CLASS_NAME)
.prop(
PropSchema::new("link", ValueKind::Str)
.required()
.values(Link::NAMES),
)
.prop(PropSchema::new("bus", ValueKind::Str))
.prop(PropSchema::new("chip-selects", ValueKind::Uint).range(1, MAX_CHIP_SELECTS as u64))
.port(pin::SCK, PortDir::Out)
.port(pin::MOSI, PortDir::Out)
.port(pin::MISO, PortDir::In)
.region("")
.region("regs");
for i in 0..MAX_CHIP_SELECTS {
schema = schema.port(alloc::format!("{}{i}", pin::CS_PREFIX), PortDir::Out);
}
schema
}