use std::num::{NonZeroU32, NonZeroU64};
use crate::chip::fram::Fm25v02;
use crate::chip::icm20608::Icm20608;
use crate::chip::lis3mdl::Lis3mdl;
use crate::chip::mpu9250::Mpu9250;
use crate::chip::ms5611::Ms5611;
use crate::chip::nmea::{self, Position};
use crate::chip::sd::MemoryCard;
use crate::chip::uart;
use crate::device::Level;
use crate::edge::{Coupled, Edge, Field, Pulse};
use crate::emulator::InterruptLine;
use crate::machine::armv7m;
use crate::machine::{Board, Crystal, HostWire, Supply, Target};
use crate::soc::Error;
use crate::soc::stm32f427::Stm32f427;
use crate::soc::stm32f427::peripherals::adc::Channel as AnalogChannel;
use crate::soc::stm32f427::peripherals::gpio::Pin;
use crate::soc::stm32f427::peripherals::spi::Select;
use crate::soc::stm32f427::peripherals::tim::Channel;
use crate::time::VirtualNanos;
pub const NAME: &str = "pixracer";
pub const MCU: &str = "stm32f427";
const CLOCK_HZ: NonZeroU64 = NonZeroU64::new(168_000_000).expect("a running clock");
const REFCLK_DIVIDER: NonZeroU32 = NonZeroU32::new(8).expect("a divider divides");
const PRIORITY_BITS: u8 = 4;
const BOOT: &str = "flash";
const ICOUNT_SHIFT: u32 = 2;
const FRAM_ON: usize = 1;
const FRAM_CS: Pin = Pin::new(10).expect("a port has sixteen pins");
const BARO_CS: Pin = Pin::new(7).expect("a port has sixteen pins");
const IMU_ON: usize = 0;
const IMU_CS: Pin = Pin::new(2).expect("a port has sixteen pins");
const SECOND_IMU_CS: Pin = Pin::new(15).expect("a port has sixteen pins");
const MAG_CS: Pin = Pin::new(15).expect("a port has sixteen pins");
const CARD_BLOCKS: usize = 8 * 1024 * 1024 * 1024 / crate::chip::sd::BLOCK;
const TELEMETRY: usize = 1;
const GPS: usize = 6;
const TELEMETRY_WIRE: HostWire = HostWire {
name: "serial1",
port: 5760,
};
const MPU9250_PEER: usize = 0;
const ICM20608_PEER: usize = 1;
const LIS3MDL_PEER: usize = 2;
const MS5611_PEER: usize = 1;
const MAIN_TIMER: usize = 0;
const MAIN_OUTPUTS: [(&str, Channel); 4] = [
("PWM1", channel(4)),
("PWM2", channel(3)),
("PWM3", channel(2)),
("PWM4", channel(1)),
];
const AUXILIARY_TIMER: usize = 2;
const AUXILIARY_OUTPUTS: [(&str, Channel); 2] = [("PWM5", channel(2)), ("PWM6", channel(3))];
const SPEED_CONTROLLER: Curve = Curve {
zero: VirtualNanos(1_000_000),
full: VirtualNanos(2_000_000),
};
const BATTERY_CONVERTER: usize = 0;
const BATTERY_VOLTAGE: AnalogChannel = analog(2);
const BATTERY_CURRENT: AnalogChannel = analog(3);
const PACK_PER_SENSED: u32 = 101;
const SENSED_PER_PACK: u32 = 10;
const MILLIAMPS_PER_MILLIVOLT: u32 = 17;
const FIVE_VOLT_SENSE: AnalogChannel = analog(4);
const FIVE_VOLT_RAIL: Supply = Supply(5000);
const fn channel(number: u8) -> Channel {
match Channel::new(number) {
Some(channel) => channel,
None => panic!("a timer has four channels"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Curve {
pub zero: VirtualNanos,
pub full: VirtualNanos,
}
pub const FULL: u32 = 1000;
impl Curve {
#[must_use]
pub fn command(self, pulse: Pulse) -> u32 {
let span = self.full.0.saturating_sub(self.zero.0).max(1);
let above = pulse.width.0.saturating_sub(self.zero.0).min(span);
u32::try_from(above * u64::from(FULL) / span).expect("at most the whole")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Actuator {
pub name: &'static str,
pub pulse: Option<Pulse>,
pub command: Option<u32>,
}
impl Actuator {
#[must_use]
pub fn on(name: &'static str, pulse: Option<Pulse>, curve: Curve) -> Self {
Self {
name,
pulse,
command: pulse.map(|pulse| curve.command(pulse)),
}
}
const fn idle(name: &'static str) -> Self {
Self {
name,
pulse: None,
command: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Outputs(pub [Actuator; 6]);
const IDLE: Outputs = Outputs([
Actuator::idle(MAIN_OUTPUTS[0].0),
Actuator::idle(MAIN_OUTPUTS[1].0),
Actuator::idle(MAIN_OUTPUTS[2].0),
Actuator::idle(MAIN_OUTPUTS[3].0),
Actuator::idle(AUXILIARY_OUTPUTS[0].0),
Actuator::idle(AUXILIARY_OUTPUTS[1].0),
]);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Signals {
pub angular_rate: [i32; 3],
pub specific_force: [i32; 3],
pub magnetic_field: [i32; 3],
pub pressure: u32,
pub temperature: i32,
pub battery_voltage: u32,
pub battery_current: i32,
pub position: Position,
pub velocity: [i32; 3],
}
pub const AT_REST: Signals = Signals {
angular_rate: [0; 3],
specific_force: [0, 0, -1_000_000],
magnetic_field: [22_000, 0, 42_000],
pressure: 101_325,
temperature: 2_000,
battery_voltage: 16_800,
battery_current: 0,
position: Position {
latitude: 398_283_000,
longitude: -985_795_000,
altitude: 500_000,
},
velocity: [0; 3],
};
const fn analog(number: u8) -> AnalogChannel {
match AnalogChannel::new(number) {
Some(channel) => channel,
None => panic!("a converter has sixteen pin channels"),
}
}
#[derive(Debug)]
pub struct Pixracer {
pub mcu: Stm32f427,
pub hse: Crystal,
pub lse: Crystal,
pub rail: Supply,
}
impl Pixracer {
pub fn new() -> Result<Self, Error> {
let mut board = Self {
mcu: Stm32f427::new()?,
hse: Crystal(24_000_000),
lse: Crystal(32_768),
rail: Supply(3300),
};
board.fit();
board.sense(&AT_REST);
Ok(board)
}
fn fit(&mut self) {
self.mcu.rcc.attach_crystals(self.hse.0, self.lse.0);
let rail = self.rail.0;
self.mcu.set_supply(rail);
for converter in &mut self.mcu.adc {
converter.set_reference(rail);
}
self.mcu.adc[BATTERY_CONVERTER].set_input(FIVE_VOLT_SENSE, FIVE_VOLT_RAIL.0 / 2);
self.mcu.usart[GPS].attach(nmea::Receiver::new());
self.mcu.spi[FRAM_ON].attach_selected(
Fm25v02::new(),
Select {
port: "GPIOD",
pin: FRAM_CS,
active: Level::Low,
},
);
self.mcu.spi[FRAM_ON].attach_selected(
Ms5611::new(),
Select {
port: "GPIOD",
pin: BARO_CS,
active: Level::Low,
},
);
self.mcu.spi[IMU_ON].attach_selected(
Mpu9250::new(),
Select {
port: "GPIOC",
pin: IMU_CS,
active: Level::Low,
},
);
self.mcu.spi[IMU_ON].attach_selected(
Icm20608::new(),
Select {
port: "GPIOC",
pin: SECOND_IMU_CS,
active: Level::Low,
},
);
self.mcu.spi[IMU_ON].attach_selected(
Lis3mdl::new(),
Select {
port: "GPIOE",
pin: MAG_CS,
active: Level::Low,
},
);
self.mcu.sdio.attach(MemoryCard::formatted(CARD_BLOCKS));
}
}
impl Board for Pixracer {
fn name(&self) -> &'static str {
NAME
}
fn target_names(&self) -> Vec<&'static str> {
vec![MCU]
}
fn mcu(&mut self, target: usize) -> Option<&mut dyn crate::mcu::Mcu> {
(target == 0).then_some(&mut self.mcu)
}
fn targets(&mut self) -> Vec<Target<'_>> {
let emulator = armv7m::Core {
model: armv7m::Model::M4,
clock_hz: CLOCK_HZ,
interrupt_lines: NonZeroU32::new(u32::from(
crate::mcu::Mcu::cpu(&self.mcu).interrupt_lines,
))
.expect("an MCU with interrupts"),
priority_bits: PRIORITY_BITS,
refclk_divider: REFCLK_DIVIDER,
icount_shift: ICOUNT_SHIFT,
boot: BOOT,
}
.command(&crate::cpu::armv7m::windows(crate::mcu::Mcu::bus(
&self.mcu,
)));
vec![Target {
name: MCU,
mcu: &mut self.mcu,
emulator,
interrupt_line: InterruptLine::new,
}]
}
fn edge(&mut self) -> Option<&mut dyn Coupled> {
Some(self)
}
fn hosts(&self) -> Vec<HostWire> {
vec![TELEMETRY_WIRE]
}
fn attach_host(&mut self, name: &str, peer: Box<dyn uart::Peer>) -> bool {
if name != TELEMETRY_WIRE.name {
return false;
}
self.mcu.usart[TELEMETRY].attach_boxed(peer);
true
}
fn host_mut(&mut self, name: &str) -> Option<&mut dyn uart::Peer> {
if name == TELEMETRY_WIRE.name {
self.mcu.usart[TELEMETRY].peer_mut()
} else {
None
}
}
}
impl Edge for Pixracer {
type Signals = Signals;
type Outputs = Outputs;
const OUTPUT_FIELDS: &'static [Field] = &[
Field::new("PWM1.width", "ns", 8, false),
Field::new("PWM1.period", "ns", 8, false),
Field::new("PWM1.command", "1e-3", 4, false),
Field::new("PWM2.width", "ns", 8, false),
Field::new("PWM2.period", "ns", 8, false),
Field::new("PWM2.command", "1e-3", 4, false),
Field::new("PWM3.width", "ns", 8, false),
Field::new("PWM3.period", "ns", 8, false),
Field::new("PWM3.command", "1e-3", 4, false),
Field::new("PWM4.width", "ns", 8, false),
Field::new("PWM4.period", "ns", 8, false),
Field::new("PWM4.command", "1e-3", 4, false),
Field::new("PWM5.width", "ns", 8, false),
Field::new("PWM5.period", "ns", 8, false),
Field::new("PWM5.command", "1e-3", 4, false),
Field::new("PWM6.width", "ns", 8, false),
Field::new("PWM6.period", "ns", 8, false),
Field::new("PWM6.command", "1e-3", 4, false),
];
const FIELDS: &'static [Field] = &[
Field::new("angular_rate.x", "mdeg/s", 4, true),
Field::new("angular_rate.y", "mdeg/s", 4, true),
Field::new("angular_rate.z", "mdeg/s", 4, true),
Field::new("specific_force.x", "1e-6 g", 4, true),
Field::new("specific_force.y", "1e-6 g", 4, true),
Field::new("specific_force.z", "1e-6 g", 4, true),
Field::new("magnetic_field.x", "nT", 4, true),
Field::new("magnetic_field.y", "nT", 4, true),
Field::new("magnetic_field.z", "nT", 4, true),
Field::new("pressure", "Pa", 4, false),
Field::new("temperature", "0.01 degC", 4, true),
Field::new("battery_voltage", "mV", 4, false),
Field::new("battery_current", "mA", 4, true),
Field::new("position.latitude", "1e-7 deg", 4, true),
Field::new("position.longitude", "1e-7 deg", 4, true),
Field::new("position.altitude", "mm", 4, true),
Field::new("velocity.north", "mm/s", 4, true),
Field::new("velocity.east", "mm/s", 4, true),
Field::new("velocity.down", "mm/s", 4, true),
];
const REST: Signals = AT_REST;
fn sense(&mut self, signals: &Signals) {
let [rate_x, rate_y, rate_z] = signals.angular_rate;
let [force_x, force_y, force_z] = signals.specific_force;
let [field_x, field_y, field_z] = signals.magnetic_field;
let imus = &mut self.mcu.spi[IMU_ON];
let unit: &mut Mpu9250 = imus.chip_mut(MPU9250_PEER).expect("fit hangs it");
unit.set_rate(rate_x, rate_y, rate_z);
unit.set_acceleration(force_x, force_y, force_z);
unit.set_temperature(signals.temperature);
unit.set_field(field_x, field_y, field_z);
let unit: &mut Icm20608 = imus.chip_mut(ICM20608_PEER).expect("fit hangs it");
unit.set_rate(rate_x, rate_y, rate_z);
unit.set_acceleration(force_x, force_y, force_z);
unit.set_temperature(signals.temperature);
let compass: &mut Lis3mdl = imus.chip_mut(LIS3MDL_PEER).expect("fit hangs it");
compass.set_field(field_x, field_y, field_z);
compass.set_temperature(signals.temperature);
let barometer: &mut Ms5611 = self.mcu.spi[FRAM_ON]
.chip_mut(MS5611_PEER)
.expect("fit hangs it");
barometer.set_pressure(signals.pressure);
barometer.set_temperature(signals.temperature);
let receiver: &mut nmea::Receiver = self.mcu.usart[GPS].chip_mut().expect("fit hangs it");
receiver.set_position(signals.position);
receiver.set_velocity(signals.velocity);
let converter = &mut self.mcu.adc[BATTERY_CONVERTER];
converter.set_input(
BATTERY_VOLTAGE,
signals.battery_voltage * SENSED_PER_PACK / PACK_PER_SENSED,
);
converter.set_input(
BATTERY_CURRENT,
u32::try_from(signals.battery_current).unwrap_or(0) / MILLIAMPS_PER_MILLIVOLT,
);
}
fn drive(&mut self) -> Outputs {
let (main, registers) = self.mcu.advanced_timer(MAIN_TIMER);
let [one, two, three, four] = MAIN_OUTPUTS.map(|(name, channel)| {
Actuator::on(name, main.pulse(channel, registers), SPEED_CONTROLLER)
});
let (auxiliary, registers) = self.mcu.timer(AUXILIARY_TIMER);
let [five, six] = AUXILIARY_OUTPUTS.map(|(name, channel)| {
Actuator::on(name, auxiliary.pulse(channel, registers), SPEED_CONTROLLER)
});
Outputs([one, two, three, four, five, six])
}
}
pub fn boxed() -> Result<Box<dyn Board>, Error> {
Ok(Box::new(Pixracer::new()?))
}
impl crate::link::Wire for Outputs {
fn encode(&self, out: &mut Vec<u8>) {
for actuator in &self.0 {
let (width, period) = actuator
.pulse
.map_or((0, 0), |pulse| (pulse.width.0, pulse.period.0));
width.encode(out);
period.encode(out);
actuator.command.unwrap_or(0).encode(out);
}
}
fn decode(bytes: &mut &[u8]) -> Result<Self, crate::link::WireError> {
let mut outputs = IDLE;
for actuator in &mut outputs.0 {
let width: VirtualNanos = crate::link::Wire::decode(bytes)?;
let period: VirtualNanos = crate::link::Wire::decode(bytes)?;
let command: u32 = crate::link::Wire::decode(bytes)?;
actuator.pulse = (period.0 != 0).then_some(Pulse { width, period });
actuator.command = actuator.pulse.map(|_| command);
}
Ok(outputs)
}
}
impl crate::link::Wire for Signals {
fn encode(&self, out: &mut Vec<u8>) {
self.angular_rate.encode(out);
self.specific_force.encode(out);
self.magnetic_field.encode(out);
self.pressure.encode(out);
self.temperature.encode(out);
self.battery_voltage.encode(out);
self.battery_current.encode(out);
self.position.encode(out);
self.velocity.encode(out);
}
fn decode(bytes: &mut &[u8]) -> Result<Self, crate::link::WireError> {
Ok(Self {
angular_rate: crate::link::Wire::decode(bytes)?,
specific_force: crate::link::Wire::decode(bytes)?,
magnetic_field: crate::link::Wire::decode(bytes)?,
pressure: crate::link::Wire::decode(bytes)?,
temperature: crate::link::Wire::decode(bytes)?,
battery_voltage: crate::link::Wire::decode(bytes)?,
battery_current: crate::link::Wire::decode(bytes)?,
position: crate::link::Wire::decode(bytes)?,
velocity: crate::link::Wire::decode(bytes)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chip::ak8963::{ADDRESS, Ak8963};
use crate::chip::i2c;
use crate::chip::spi::Peer;
use crate::device::DeviceContext;
use crate::link::{
Advance, LinkError, Set, Wire, attach_engine_ports, deliver, sent, values_of,
};
use crate::mcu::Mcu;
use crate::soc::stm32f427::generated::rcc::{Register as RccRegister, apb2enr};
use crate::soc::stm32f427::generated::tim1::{Register as Tim1Register, cr1, egr};
use crate::soc::stm32f427::generated::{RCC, TIM1};
use crate::soc::stm32f427::peripherals::rcc::Apb;
use crate::soc::{Layout, Width};
const MICROSECOND: u64 = 1_000;
const MILLISECOND: u64 = 1_000_000;
const READ: u8 = 0x80;
const PWR_MGMT_1: u8 = 0x6B;
const CLOCK_ON: u8 = 0x01;
const ACCEL_XOUT_H: u8 = 0x3B;
const COUNTS_PER_GRAVITY: i16 = 16_384;
const BURST: u8 = 0xC0;
const CTRL_REG1: u8 = 0x20;
const CTRL_REG3: u8 = 0x22;
const CONTINUOUS: u8 = 0x00;
const ODR_80_ULTRAHIGH: u8 = 0x7C;
const OUT_X_L: u8 = 0x28;
const COUNTS_PER_GAUSS: i16 = 6842;
const TEMP_EN: u8 = 0x80;
const GAUSS_NANOTESLA: i32 = 100_000;
const ONE_GRAVITY: i32 = 1_000_000;
const CNTL1: u8 = 0x0A;
const CONTINUOUS_2_SIXTEEN_BITS: u8 = 0x16;
const HXL: u8 = 0x03;
const RESET: u8 = 0x1E;
const CONVERT_D1: u8 = 0x48;
const CONVERT_D2: u8 = 0x58;
const ADC_READ: u8 = 0x00;
fn read(board: &mut Pixracer, at: u64, base: u64, offset: u32) -> u64 {
board
.mcu
.read(VirtualNanos(at), base + u64::from(offset), Width::W32)
.unwrap()
}
fn write(board: &mut Pixracer, at: u64, base: u64, offset: u32, value: u64) {
board
.mcu
.write(
VirtualNanos(at),
base + u64::from(offset),
Width::W32,
value,
)
.unwrap();
}
fn cycle(board: &mut Pixracer, at: u64, interface: usize, nth: usize, bytes: &[u8]) -> Vec<u8> {
board
.mcu
.drive_serial(VirtualNanos(at), interface, |spi, _, context| {
let peer = spi.peer_mut(nth).expect("fitted");
peer.select(context);
let answers = bytes
.iter()
.map(|&byte| u8::try_from(peer.transfer(u16::from(byte), context)).unwrap())
.collect();
peer.deselect(context);
answers
})
}
fn alike(
board: &mut Pixracer,
at: u64,
(interface, nth): (usize, usize),
reference: &mut dyn Peer,
bytes: &[u8],
) -> (Vec<u8>, Vec<u8>) {
board
.mcu
.drive_serial(VirtualNanos(at), interface, |spi, _, context| {
let answers = |peer: &mut dyn Peer| -> Vec<u8> {
peer.select(context);
let answers = bytes
.iter()
.map(|&byte| u8::try_from(peer.transfer(u16::from(byte), context)).unwrap())
.collect();
peer.deselect(context);
answers
};
(
answers(spi.peer_mut(nth).expect("fitted")),
answers(reference),
)
})
}
fn die_write(die: &mut dyn i2c::Peer, register: u8, byte: u8, context: &DeviceContext<'_>) {
die.start(ADDRESS, false, context);
die.write(register, context);
die.write(byte, context);
die.stop(context);
}
fn die_read(
die: &mut dyn i2c::Peer,
register: u8,
count: usize,
context: &DeviceContext<'_>,
) -> Vec<u8> {
die.start(ADDRESS, false, context);
die.write(register, context);
die.start(ADDRESS, true, context);
let bytes = (0..count)
.map(|index| die.read(index + 1 == count, context))
.collect();
die.stop(context);
bytes
}
fn compass_die_fields(board: &mut Pixracer, reference: &mut Mpu9250) -> (Vec<u8>, Vec<u8>) {
board
.mcu
.drive_serial(VirtualNanos(10 * MILLISECOND), IMU_ON, |spi, _, context| {
for die in [
spi.chip_mut::<Mpu9250>(MPU9250_PEER)
.expect("fit hangs it")
.magnetometer(),
reference.magnetometer(),
] {
die_write(die, CNTL1, CONTINUOUS_2_SIXTEEN_BITS, context);
}
});
board
.mcu
.drive_serial(VirtualNanos(30 * MILLISECOND), IMU_ON, |spi, _, context| {
let on_board: &mut Ak8963 = spi
.chip_mut::<Mpu9250>(MPU9250_PEER)
.expect("fit hangs it")
.magnetometer();
(
die_read(on_board, HXL, 6, context),
die_read(reference.magnetometer(), HXL, 6, context),
)
})
}
fn words(bytes: &[u8], big_endian: bool) -> [i16; 3] {
let word = |pair: &[u8]| {
if big_endian {
i16::from_be_bytes([pair[0], pair[1]])
} else {
i16::from_le_bytes([pair[0], pair[1]])
}
};
[word(&bytes[0..2]), word(&bytes[2..4]), word(&bytes[4..6])]
}
#[test]
fn the_declared_fields_walk_the_encoded_signals_one_by_one() {
let signals = Signals {
angular_rate: [-1, -2, -3],
specific_force: [-4, -5, -6],
magnetic_field: [-7, -8, -9],
pressure: 3_000_000_000,
temperature: -11,
battery_voltage: 3_000_000_001,
battery_current: -13,
position: Position {
latitude: -14,
longitude: -15,
altitude: -16,
},
velocity: [-17, -18, -19],
};
let mut bytes = Vec::new();
signals.encode(&mut bytes);
assert_eq!(
values_of(<Pixracer as Edge>::FIELDS, &bytes).unwrap(),
[
-1,
-2,
-3,
-4,
-5,
-6,
-7,
-8,
-9,
3_000_000_000,
-11,
3_000_000_001,
-13,
-14,
-15,
-16,
-17,
-18,
-19
],
"every signed field negative and every unsigned one past an i32, so a wrong sign shows"
);
}
#[test]
fn an_advance_sets_an_input_the_board_senses_whole_and_queues_a_ports_bytes() {
let mut board = Pixracer::new().unwrap();
assert_eq!(
attach_engine_ports(&mut board).unwrap(),
[TELEMETRY_WIRE.name]
);
let mut inputs = values_of(<Pixracer as Edge>::FIELDS, &board.rest_wire()).unwrap();
let pressure = <Pixracer as Edge>::FIELDS
.iter()
.position(|field| field.name == "pressure")
.unwrap();
deliver(
&mut board,
&mut inputs,
&[TELEMETRY_WIRE.name],
&Advance {
until: VirtualNanos(0),
inputs: vec![Set {
input: u16::try_from(pressure).unwrap(),
value: 90_000,
}],
ports: vec![b"x".to_vec()],
},
VirtualNanos(0),
)
.unwrap();
let mut ms5611 = Ms5611::new();
ms5611.set_pressure(90_000);
ms5611.set_temperature(AT_REST.temperature);
let mut at = 10 * MILLISECOND;
let mut barometer = |board: &mut Pixracer, bytes: &[u8]| {
at += 10 * MILLISECOND;
alike(board, at, (FRAM_ON, MS5611_PEER), &mut ms5611, bytes)
};
barometer(&mut board, &[RESET]);
barometer(&mut board, &[CONVERT_D1]);
let (on_board, expected) = barometer(&mut board, &[ADC_READ, 0, 0, 0]);
assert_eq!(
on_board, expected,
"the barometer converts the pressure set"
);
let port = board.host_mut(TELEMETRY_WIRE.name).unwrap();
assert_eq!(port.transmit(VirtualNanos(0)), Some(u16::from(b'x')));
}
#[test]
fn an_advance_for_another_boards_ports_or_inputs_is_refused_by_name() {
let mut board = Pixracer::new().unwrap();
attach_engine_ports(&mut board).unwrap();
let mut inputs = values_of(<Pixracer as Edge>::FIELDS, &board.rest_wire()).unwrap();
let too_many_ports = Advance {
until: VirtualNanos(0),
inputs: Vec::new(),
ports: vec![Vec::new(), Vec::new()],
};
assert!(matches!(
deliver(
&mut board,
&mut inputs,
&[TELEMETRY_WIRE.name],
&too_many_ports,
VirtualNanos(0),
),
Err(LinkError::Misfit(_))
));
let no_such_input = Advance {
until: VirtualNanos(0),
inputs: vec![Set {
input: 99,
value: 0,
}],
ports: vec![Vec::new()],
};
assert!(matches!(
deliver(
&mut board,
&mut inputs,
&[TELEMETRY_WIRE.name],
&no_such_input,
VirtualNanos(0),
),
Err(LinkError::Misfit(_))
));
}
#[test]
fn the_engine_s_port_on_serial1_carries_a_byte_each_way() {
use crate::soc::stm32f427::generated::USART2;
use crate::soc::stm32f427::generated::rcc::apb1enr;
use crate::soc::stm32f427::generated::usart6::{
Register as UsartRegister, cr1 as usart_cr1, sr as usart_sr,
};
let mut board = Pixracer::new().unwrap();
let ports = attach_engine_ports(&mut board).unwrap();
let usart = |register: UsartRegister| register.spec().offset;
write(
&mut board,
0,
RCC.base,
RccRegister::Apb1enr.spec().offset,
apb1enr::USART2EN.mask(),
);
write(&mut board, 0, USART2.base, usart(UsartRegister::Brr), 0x10);
write(
&mut board,
0,
USART2.base,
usart(UsartRegister::Cr1),
usart_cr1::UE.mask() | usart_cr1::TE.mask() | usart_cr1::RE.mask(),
);
let mut inputs = values_of(<Pixracer as Edge>::FIELDS, &board.rest_wire()).unwrap();
let advance = Advance {
until: VirtualNanos(MILLISECOND),
inputs: Vec::new(),
ports: vec![b"k".to_vec()],
};
deliver(&mut board, &mut inputs, &ports, &advance, VirtualNanos(0)).unwrap();
board.mcu.poll(VirtualNanos(0));
let frame = 10 * MICROSECOND;
board.mcu.advance_to(VirtualNanos(frame));
let status = read(&mut board, frame, USART2.base, usart(UsartRegister::Sr));
assert!(usart_sr::RXNE.is_set(status), "the engine's byte arrived");
assert_eq!(
read(&mut board, frame, USART2.base, usart(UsartRegister::Dr)),
u64::from(b'k')
);
write(
&mut board,
frame,
USART2.base,
usart(UsartRegister::Dr),
u64::from(b'q'),
);
board.mcu.advance_to(VirtualNanos(3 * frame));
assert_eq!(sent(&mut board, &ports).unwrap(), [b"q".to_vec()]);
assert_eq!(
sent(&mut board, &ports).unwrap(),
[Vec::<u8>::new()],
"each byte is told once"
);
}
#[test]
fn the_outputs_are_the_description_s_six_pwm_pins_read_off_their_timers() {
let mut board = Pixracer::new().unwrap();
assert_eq!(
board.drive(),
IDLE,
"nothing drives a pin before the firmware"
);
let named: Vec<String> = IDLE
.0
.iter()
.flat_map(|actuator| {
["width", "period", "command"].map(|part| format!("{}.{part}", actuator.name))
})
.collect();
let fields: Vec<&str> = <Pixracer as Edge>::OUTPUT_FIELDS
.iter()
.map(|field| field.name.as_ref())
.collect();
assert_eq!(
fields, named,
"the fields name the outputs in the order they cross"
);
let timer_hz = {
let (clocks, registers) = board.mcu.clocks();
clocks.timer_clock(registers, Apb::Two)
};
let tim1 = |register: Tim1Register| register.spec().offset;
write(
&mut board,
0,
RCC.base,
RccRegister::Apb2enr.spec().offset,
apb2enr::TIM1EN.mask(),
);
write(
&mut board,
0,
TIM1.base,
tim1(Tim1Register::Psc),
timer_hz / 1_000_000 - 1,
);
write(&mut board, 0, TIM1.base, tim1(Tim1Register::Arr), 19_999);
write(&mut board, 0, TIM1.base, tim1(Tim1Register::Ccr4), 1_500);
write(
&mut board,
0,
TIM1.base,
tim1(Tim1Register::Ccmr2Output),
0x6800,
);
write(&mut board, 0, TIM1.base, tim1(Tim1Register::Ccer), 0x1000);
write(&mut board, 0, TIM1.base, tim1(Tim1Register::Bdtr), 0x8000);
write(
&mut board,
0,
TIM1.base,
tim1(Tim1Register::Egr),
egr::UG.mask(),
);
write(
&mut board,
0,
TIM1.base,
tim1(Tim1Register::Cr1),
cr1::CEN.mask(),
);
let outputs = board.drive();
assert_eq!(
outputs.0[0],
Actuator {
name: "PWM1",
pulse: Some(Pulse {
width: VirtualNanos(1_500 * MICROSECOND),
period: VirtualNanos(20 * MILLISECOND),
}),
command: Some(500),
}
);
assert!(
outputs.0[1..]
.iter()
.all(|actuator| actuator.pulse.is_none())
);
let mut bytes = Vec::new();
outputs.encode(&mut bytes);
let values = values_of(<Pixracer as Edge>::OUTPUT_FIELDS, &bytes).unwrap();
assert_eq!(values[..6], [1_500_000, 20_000_000, 500, 0, 0, 0]);
assert_eq!(
Outputs::decode(&mut bytes.as_slice()).unwrap(),
outputs,
"an output with no period comes back with no pulse and no command"
);
}
const SERVO: Curve = Curve {
zero: VirtualNanos(1_000_000),
full: VirtualNanos(2_000_000),
};
fn width(nanos: u64) -> Pulse {
Pulse {
width: VirtualNanos(nanos),
period: VirtualNanos(20_000_000),
}
}
#[test]
fn a_command_is_where_the_width_falls_between_the_ends_and_never_past_them() {
assert_eq!(SERVO.command(width(1_000_000)), 0);
assert_eq!(SERVO.command(width(1_250_000)), 250);
assert_eq!(SERVO.command(width(2_000_000)), FULL);
assert_eq!(SERVO.command(width(0)), 0, "narrower than nothing");
assert_eq!(
SERVO.command(width(3_000_000)),
FULL,
"wider than everything"
);
}
#[test]
fn an_output_without_a_pulse_commands_nothing() {
let off = Actuator::on("OUT", None, SERVO);
assert_eq!(off.command, None);
let on = Actuator::on("OUT", Some(width(1_500_000)), SERVO);
assert_eq!(on.command, Some(500));
}
#[test]
fn the_telemetry_wire_is_the_second_serial_port() {
let mut board = Pixracer::new().unwrap();
assert_eq!(board.hosts(), [TELEMETRY_WIRE]);
assert!(!board.attach_host(
"serial9",
Box::new(crate::soc::stm32f427::peripherals::usart::Pattern::default())
));
assert!(!board.mcu.usart[TELEMETRY].attached());
assert!(board.attach_host(
"serial1",
Box::new(crate::soc::stm32f427::peripherals::usart::Pattern::default())
));
assert!(board.mcu.usart[TELEMETRY].attached());
assert_eq!(
crate::soc::stm32f427::peripherals::SERIAL_PORTS[TELEMETRY].instance,
"USART2"
);
}
#[test]
fn the_receiver_on_the_gps_port_reports_where_the_board_is() {
let mut board = Pixracer::new().unwrap();
assert_eq!(
crate::soc::stm32f427::peripherals::SERIAL_PORTS[GPS].instance,
"UART4"
);
board.sense(&Signals {
position: Position {
latitude: 515_074_000,
longitude: -1_278_000,
altitude: 11_000,
},
velocity: [10_000, 0, 0],
..AT_REST
});
let receiver = board.mcu.usart[GPS].peer_mut().expect("fit hangs it");
let epoch: Vec<u8> = std::iter::from_fn(|| receiver.transmit(VirtualNanos(0)))
.map(|word| u8::try_from(word).expect("a byte"))
.collect();
let epoch = String::from_utf8(epoch).unwrap();
assert!(
epoch.starts_with("$GPGGA,000000.00,5130.44400,N,00007.66800,W,1,10,0.90,11.000,M,"),
"{epoch}"
);
assert!(
epoch.contains("$GPRMC,000000.00,A,5130.44400,N,00007.66800,W,19.43,0.00,"),
"ten metres a second north is 19.43 knots on course zero: {epoch}"
);
}
#[test]
fn the_battery_reaches_the_first_converter_through_the_descriptions_dividers() {
use crate::soc::stm32f427::generated::ADC1;
use crate::soc::stm32f427::generated::adc1::{
Register as AdcRegister, cr1 as adc_cr1, cr2, sqr1, sqr3, sr,
};
let mut board = Pixracer::new().unwrap();
board.sense(&Signals {
battery_voltage: 16_800,
battery_current: 17_000,
..AT_REST
});
let adc = |register: AdcRegister| register.spec().offset;
write(
&mut board,
0,
RCC.base,
RccRegister::Apb2enr.spec().offset,
apb2enr::ADC1EN.mask(),
);
write(
&mut board,
0,
ADC1.base,
adc(AdcRegister::Sqr3),
sqr3::SQ3.set(sqr3::SQ2.set(sqr3::SQ1.set(0, 2), 3), 4),
);
write(
&mut board,
0,
ADC1.base,
adc(AdcRegister::Sqr1),
sqr1::L.set(0, 2),
);
write(
&mut board,
0,
ADC1.base,
adc(AdcRegister::Cr1),
adc_cr1::SCAN.mask(),
);
let running = cr2::ADON.mask() | cr2::EOCS.mask();
write(&mut board, 0, ADC1.base, adc(AdcRegister::Cr2), running);
write(
&mut board,
0,
ADC1.base,
adc(AdcRegister::Cr2),
running | cr2::SWSTART.mask(),
);
let words: Vec<u64> = (1..=3)
.map(|nth| {
let now = nth * 1_875;
board.mcu.advance_to(VirtualNanos(now));
assert!(sr::EOC.is_set(read(&mut board, now, ADC1.base, adc(AdcRegister::Sr))));
read(&mut board, now, ADC1.base, adc(AdcRegister::Dr))
})
.collect();
assert_eq!(words, [2063, 1240, 3102]);
}
#[test]
fn the_board_is_built_at_rest() {
let mut board = Pixracer::new().unwrap();
for unit in [MPU9250_PEER, ICM20608_PEER] {
cycle(
&mut board,
MILLISECOND,
IMU_ON,
unit,
&[PWR_MGMT_1, CLOCK_ON],
);
let mut frame = vec![ACCEL_XOUT_H | READ];
frame.extend([0; 6]);
let bytes = cycle(&mut board, 5 * MILLISECOND, IMU_ON, unit, &frame);
assert_eq!(
words(&bytes[1..], true),
[0, 0, -COUNTS_PER_GRAVITY],
"unit {unit}"
);
}
cycle(
&mut board,
MILLISECOND,
IMU_ON,
LIS3MDL_PEER,
&[CTRL_REG1, ODR_80_ULTRAHIGH],
);
cycle(
&mut board,
MILLISECOND,
IMU_ON,
LIS3MDL_PEER,
&[CTRL_REG3, CONTINUOUS],
);
let mut frame = vec![OUT_X_L | BURST];
frame.extend([0; 6]);
let bytes = cycle(&mut board, 30 * MILLISECOND, IMU_ON, LIS3MDL_PEER, &frame);
let [north, east, down] = words(&bytes[1..], false);
assert!(
north > 0 && east == 0 && down > 0,
"north and down: {north} {east} {down}"
);
}
#[test]
fn sensing_hands_each_chip_the_body_s_axes() {
const RATE: i32 = 100_000;
let mut board = Pixracer::new().unwrap();
board.sense(&Signals {
angular_rate: [RATE, 0, -RATE],
specific_force: [ONE_GRAVITY, 0, -ONE_GRAVITY],
magnetic_field: [GAUSS_NANOTESLA, 0, GAUSS_NANOTESLA],
pressure: 95_000,
temperature: 3_000,
..AT_REST
});
let mut mpu9250 = Mpu9250::new();
mpu9250.set_rate(RATE, 0, -RATE);
mpu9250.set_acceleration(ONE_GRAVITY, 0, -ONE_GRAVITY);
mpu9250.set_temperature(3_000);
mpu9250.set_field(GAUSS_NANOTESLA, 0, GAUSS_NANOTESLA);
let mut icm20608 = Icm20608::new();
icm20608.set_rate(RATE, 0, -RATE);
icm20608.set_acceleration(ONE_GRAVITY, 0, -ONE_GRAVITY);
icm20608.set_temperature(3_000);
let mut lis3mdl = Lis3mdl::new();
lis3mdl.set_field(GAUSS_NANOTESLA, 0, GAUSS_NANOTESLA);
lis3mdl.set_temperature(3_000);
let mut ms5611 = Ms5611::new();
ms5611.set_pressure(95_000);
ms5611.set_temperature(3_000);
let mut sample = vec![ACCEL_XOUT_H | READ];
sample.extend([0; 14]);
let units: [(usize, &mut dyn Peer); 2] =
[(MPU9250_PEER, &mut mpu9250), (ICM20608_PEER, &mut icm20608)];
for (nth, unit) in units {
alike(
&mut board,
MILLISECOND,
(IMU_ON, nth),
unit,
&[PWR_MGMT_1, CLOCK_ON],
);
let (on_board, expected) =
alike(&mut board, 5 * MILLISECOND, (IMU_ON, nth), unit, &sample);
assert_eq!(on_board, expected, "unit {nth}");
assert_eq!(
words(&expected[1..7], true),
[COUNTS_PER_GRAVITY, 0, -COUNTS_PER_GRAVITY]
);
assert_ne!(&expected[7..9], [0, 0], "a temperature");
assert_ne!(words(&expected[9..15], true), [0; 3], "a rate");
}
let (on_board, expected) = compass_die_fields(&mut board, &mut mpu9250);
assert_eq!(on_board, expected, "the compass die inside the first unit");
assert_ne!(expected, [0; 6]);
alike(
&mut board,
MILLISECOND,
(IMU_ON, LIS3MDL_PEER),
&mut lis3mdl,
&[CTRL_REG1, TEMP_EN | ODR_80_ULTRAHIGH],
);
alike(
&mut board,
MILLISECOND,
(IMU_ON, LIS3MDL_PEER),
&mut lis3mdl,
&[CTRL_REG3, CONTINUOUS],
);
let mut frame = vec![OUT_X_L | BURST];
frame.extend([0; 8]);
let (on_board, expected) = alike(
&mut board,
30 * MILLISECOND,
(IMU_ON, LIS3MDL_PEER),
&mut lis3mdl,
&frame,
);
assert_eq!(on_board, expected, "the magnetometer");
assert_eq!(
words(&expected[1..7], false),
[COUNTS_PER_GAUSS, 0, COUNTS_PER_GAUSS]
);
assert_ne!(&expected[7..9], [0, 0], "the magnetometer's temperature");
let mut at = 40 * MILLISECOND;
let mut barometer = |board: &mut Pixracer, bytes: &[u8]| {
at += 10 * MILLISECOND;
alike(board, at, (FRAM_ON, MS5611_PEER), &mut ms5611, bytes)
};
barometer(&mut board, &[RESET]);
barometer(&mut board, &[CONVERT_D1]);
let (pressure, expected) = barometer(&mut board, &[ADC_READ, 0, 0, 0]);
assert_eq!(pressure, expected, "the barometer's pressure conversion");
assert_ne!(&expected[1..], [0, 0, 0]);
barometer(&mut board, &[CONVERT_D2]);
let (temperature, expected) = barometer(&mut board, &[ADC_READ, 0, 0, 0]);
assert_eq!(
temperature, expected,
"the barometer's temperature conversion"
);
assert_ne!(&expected[1..], [0, 0, 0]);
}
#[test]
fn the_board_has_its_mcu_and_reaches_a_model_by_field() {
let mut board = Pixracer::new().unwrap();
assert_eq!(board.name(), NAME);
let (clocks, registers) = board.mcu.clocks();
assert_eq!(
clocks.sysclk(registers),
16_000_000,
"the part wakes on its own oscillator"
);
assert_eq!(board.hse, Crystal(24_000_000));
}
#[test]
fn a_board_can_hang_a_chip_on_one_of_its_interfaces() {
#[derive(Debug, Default)]
struct Counter(u16);
impl Peer for Counter {
fn transfer(&mut self, mosi: u16, _context: &DeviceContext<'_>) -> u16 {
self.0 = mosi.wrapping_add(1);
self.0
}
}
let mut board = Pixracer::new().unwrap();
assert!(
!board.mcu.spi[3].attached(),
"a Pixracer fits nothing on its fourth interface"
);
board.mcu.spi[3].attach(Counter::default());
assert!(board.mcu.spi[3].attached());
}
#[test]
fn the_machine_routes_every_cpu_line_and_no_more() {
let mut board = Pixracer::new().unwrap();
let lines = board.mcu.cpu().interrupt_lines;
let targets = board.targets();
let [target] = &targets[..] else {
panic!("one MCU on the board");
};
assert!((target.interrupt_line)(lines - 1).is_some());
}
#[test]
fn the_machine_takes_its_interrupt_count_from_the_part() {
let mut board = Pixracer::new().unwrap();
let lines = board.mcu.cpu().interrupt_lines;
let targets = board.targets();
let declared = targets[0]
.emulator
.properties
.iter()
.find(|property| property.name == "num-irq")
.expect("the machine is told its interrupt count");
assert_eq!(
declared.value,
lines.to_string(),
"the count the engine is given is the count the MCU has"
);
}
}