///|
/// SPI clock polarity used by a serial peripheral interface capture.
pub enum SpiPolarity {
IdleLow
IdleHigh
} derive(Debug, Eq)
///|
/// SPI clock phase used when sampling and changing data.
pub enum SpiPhase {
SampleLeading
SampleTrailing
} derive(Debug, Eq)
///|
/// Bit ordering on the wire.
pub enum SpiBitOrder {
MsbFirst
LsbFirst
} derive(Debug, Eq)
///|
/// Configuration for decoding one chip-select framed SPI stream.
pub struct SpiConfig {
clock : String
chip_select : String
mosi : String?
miso : String?
polarity : SpiPolarity
phase : SpiPhase
order : SpiBitOrder
word_bits : Int
active_low : Bool
} derive(Debug)
///|
/// One decoded SPI word, retaining both wire values and timing metadata.
pub struct SpiWord {
index : Int
start_time : Int64
end_time : Int64
clock_edges : Int
mosi_bits : String
miso_bits : String
mosi_value : Int64?
miso_value : Int64?
complete : Bool
chip_select_active : Bool
} derive(Debug)
///|
/// Aggregate SPI decode statistics.
pub struct SpiReport {
config : SpiConfig
words : Array[SpiWord]
complete_words : Int
incomplete_words : Int
active_edges : Int
total_bits : Int
first_time : Int64?
last_time : Int64?
} derive(Debug)
///|
/// List every supported SPI polarity.
pub fn spi_polarity_catalog() -> Array[SpiPolarity] {
[IdleLow, IdleHigh]
}
///|
/// List every supported SPI sampling phase.
pub fn spi_phase_catalog() -> Array[SpiPhase] {
[SampleLeading, SampleTrailing]
}
///|
/// List every supported SPI bit order.
pub fn spi_bit_order_catalog() -> Array[SpiBitOrder] {
[MsbFirst, LsbFirst]
}
///|
/// Create a conventional mode-0, MSB-first SPI configuration.
pub fn spi_mode0(clock : String, chip_select : String) -> SpiConfig {
{
clock,
chip_select,
mosi: None,
miso: None,
polarity: IdleLow,
phase: SampleLeading,
order: MsbFirst,
word_bits: 8,
active_low: true,
}
}
///|
/// Attach a MOSI and MISO pair to an SPI configuration.
pub fn SpiConfig::with_data(
self : SpiConfig,
mosi : String,
miso : String?,
) -> SpiConfig {
{ ..self, mosi: Some(mosi), miso }
}
///|
/// Set clock polarity and sampling phase.
pub fn SpiConfig::with_mode(
self : SpiConfig,
polarity : SpiPolarity,
phase : SpiPhase,
) -> SpiConfig {
{ ..self, polarity, phase }
}
///|
/// Set wire order and word width, clamping width to a useful range.
pub fn SpiConfig::with_word_format(
self : SpiConfig,
order : SpiBitOrder,
word_bits : Int,
) -> SpiConfig {
{ ..self, order, word_bits: Int::max(1, Int::min(64, word_bits)) }
}
///|
/// Configure chip-select polarity.
pub fn SpiConfig::with_active_low(
self : SpiConfig,
active_low : Bool,
) -> SpiConfig {
{ ..self, active_low, }
}
///|
/// Human-readable polarity label.
pub fn spi_polarity_name(value : SpiPolarity) -> String {
match value {
IdleLow => "idle-low"
IdleHigh => "idle-high"
}
}
///|
/// Human-readable phase label.
pub fn spi_phase_name(value : SpiPhase) -> String {
match value {
SampleLeading => "leading"
SampleTrailing => "trailing"
}
}
///|
/// Human-readable bit-order label.
pub fn spi_bit_order_name(value : SpiBitOrder) -> String {
match value {
MsbFirst => "msb-first"
LsbFirst => "lsb-first"
}
}
///|
/// Return the active level for a configured chip-select.
pub fn spi_chip_select_active(config : SpiConfig, value : Value?) -> Bool {
let level = match value {
Some(Scalar(ch)) => ch == '1'
Some(Vector(bits)) => bits == "1"
_ => false
}
if config.active_low {
!level
} else {
level
}
}
///|
/// Decode all chip-select framed words in a trace.
pub fn spi_decode(trace : Trace, config : SpiConfig) -> Array[SpiWord] {
let edge_times = spi_sample_times(trace, config)
let result : Array[SpiWord] = []
let mut bits_mosi : Array[String] = []
let mut bits_miso : Array[String] = []
let mut start : Int64? = None
let mut previous_active = false
for time in edge_times {
let active = spi_chip_select_active(
config,
value_at(trace, config.chip_select, time),
)
if active && !previous_active {
bits_mosi = []
bits_miso = []
start = Some(time)
}
if active {
bits_mosi.push(spi_sample_text(trace, config.mosi, time))
bits_miso.push(spi_sample_text(trace, config.miso, time))
if bits_mosi.length() == config.word_bits {
let first = match start {
Some(value) => value
None => time
}
result.push(
spi_make_word(
result.length(),
first,
time,
bits_mosi,
bits_miso,
true,
),
)
bits_mosi = []
bits_miso = []
start = Some(time)
}
} else if previous_active && bits_mosi.length() > 0 {
let first = match start {
Some(value) => value
None => time
}
result.push(
spi_make_word(result.length(), first, time, bits_mosi, bits_miso, false),
)
bits_mosi = []
bits_miso = []
start = None
}
previous_active = active
}
if previous_active && bits_mosi.length() > 0 {
let first = match start {
Some(value) => value
None => trace.end_time
}
result.push(
spi_make_word(
result.length(),
first,
trace.end_time,
bits_mosi,
bits_miso,
false,
),
)
}
result
}
///|
/// Decode and summarize SPI activity.
pub fn spi_report(trace : Trace, config : SpiConfig) -> SpiReport {
let words = spi_decode(trace, config)
let complete = words.filter(fn(item) { item.complete }).length()
let bits = words.fold(init=0, fn(total, item) { total + item.clock_edges })
let first = words.fold(init=None, fn(current, item) {
match current {
Some(value) => Some(Int64::min(value, item.start_time))
None => Some(item.start_time)
}
})
let last = words.fold(init=None, fn(current, item) {
match current {
Some(value) => Some(Int64::max(value, item.end_time))
None => Some(item.end_time)
}
})
{
config,
words,
complete_words: complete,
incomplete_words: words.length() - complete,
active_edges: bits,
total_bits: bits,
first_time: first,
last_time: last,
}
}
///|
/// Return the number of complete words in a report.
pub fn SpiReport::word_count(self : SpiReport) -> Int {
self.words.length()
}
///|
/// Return the number of incomplete words in a report.
pub fn SpiReport::incomplete_count(self : SpiReport) -> Int {
self.words.filter(fn(item) { !item.complete }).length()
}
///|
/// Produce a stable CSV table for decoded words.
pub fn spi_words_csv(words : Array[SpiWord]) -> String {
let out = StringBuilder()
out.write_string(
"index,start,end,edges,mosi,miso,mosi_value,miso_value,complete\n",
)
for item in words {
out.write_string(item.index.to_string())
out.write_char(',')
out.write_string(item.start_time.to_string())
out.write_char(',')
out.write_string(item.end_time.to_string())
out.write_char(',')
out.write_string(item.clock_edges.to_string())
out.write_char(',')
out.write_string(item.mosi_bits)
out.write_char(',')
out.write_string(item.miso_bits)
out.write_char(',')
out.write_string(optional_spi_int(item.mosi_value))
out.write_char(',')
out.write_string(optional_spi_int(item.miso_value))
out.write_char(',')
out.write_string(item.complete.to_string())
out.write_char('\n')
}
out.to_string()
}
///|
/// Render a report for command-line inspection.
pub fn SpiReport::to_text(self : SpiReport) -> String {
"SPI " +
self.config.clock +
" words=" +
self.words.length().to_string() +
" complete=" +
self.complete_words.to_string() +
" incomplete=" +
self.incomplete_count().to_string() +
" bits=" +
self.total_bits.to_string() +
" mode=" +
spi_polarity_name(self.config.polarity) +
"/" +
spi_phase_name(self.config.phase) +
" order=" +
spi_bit_order_name(self.config.order)
}
///|
/// Render a report as deterministic JSON.
pub fn SpiReport::to_json(self : SpiReport) -> String {
let words = self.words.map(spi_word_json).join(",")
"{\"clock\":\"" +
escape_text(self.config.clock) +
"\",\"chip_select\":\"" +
escape_text(self.config.chip_select) +
"\",\"complete_words\":" +
self.complete_words.to_string() +
",\"incomplete_words\":" +
self.incomplete_count().to_string() +
",\"total_bits\":" +
self.total_bits.to_string() +
",\"words\":[" +
words +
"]}"
}
///|
/// Select the clock edge on which data is sampled for the configured mode.
fn spi_sample_times(trace : Trace, config : SpiConfig) -> Array[Int64] {
let result : Array[Int64] = []
let mut previous : Char? = None
let leading = match config.polarity {
IdleLow => '0'
IdleHigh => '1'
}
let active_edge_is_rising = match config.polarity {
IdleLow => true
IdleHigh => false
}
for change in trace.changes {
if change.id != config.clock {
continue
}
match change.value {
Scalar(current) => {
let is_edge = match previous {
Some(before) if active_edge_is_rising =>
before == '0' && current == '1'
Some(before) => before == '1' && current == '0'
None => false
}
let sample = match config.phase {
SampleLeading => is_edge
SampleTrailing => {
let opposite = match previous {
Some(before) if active_edge_is_rising =>
before == '1' && current == '0'
Some(before) => before == '0' && current == '1'
None => false
}
opposite
}
}
if sample && current != leading {
result.push(change.time)
}
previous = Some(current)
}
_ => previous = None
}
}
result
}
///|
/// Sample an optional data signal as a single wire token.
fn spi_sample_text(trace : Trace, signal_id : String?, time : Int64) -> String {
match signal_id {
Some(id) =>
match value_at(trace, id, time) {
Some(value) =>
match value {
Scalar(ch) => ch.to_string()
Vector(bits) if bits.length() > 0 => bits
Vector(_) => "?"
Real(number) => number.to_string()
}
None => "?"
}
None => "?"
}
}
///|
/// Build one word and decode numeric fields only when all bits are known.
fn spi_make_word(
index : Int,
start_time : Int64,
end_time : Int64,
mosi : Array[String],
miso : Array[String],
complete : Bool,
) -> SpiWord {
let mosi_text = mosi.join("")
let miso_text = miso.join("")
{
index,
start_time,
end_time,
clock_edges: mosi.length(),
mosi_bits: mosi_text,
miso_bits: miso_text,
mosi_value: spi_bits_int(mosi),
miso_value: spi_bits_int(miso),
complete,
chip_select_active: true,
}
}
///|
/// Parse a sequence of one-bit wire tokens as an unsigned integer.
fn spi_bits_int(bits : Array[String]) -> Int64? {
let mut result = 0L
if bits.length() == 0 {
return None
}
for bit in bits {
result *= 2L
if bit == "1" {
result += 1L
} else if bit != "0" {
return None
}
}
Some(result)
}
///|
/// Encode an optional SPI integer for CSV.
fn optional_spi_int(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode one SPI word.
fn spi_word_json(item : SpiWord) -> String {
"{\"index\":" +
item.index.to_string() +
",\"start\":" +
item.start_time.to_string() +
",\"end\":" +
item.end_time.to_string() +
",\"edges\":" +
item.clock_edges.to_string() +
",\"mosi\":\"" +
escape_text(item.mosi_bits) +
"\",\"miso\":\"" +
escape_text(item.miso_bits) +
"\",\"mosi_value\":" +
optional_spi_json(item.mosi_value) +
",\"miso_value\":" +
optional_spi_json(item.miso_value) +
",\"complete\":" +
item.complete.to_string() +
"}"
}
///|
/// Encode an optional integer as JSON null or a number.
fn optional_spi_json(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => "null"
}
}