Compare commits

...

6 Commits

Author SHA1 Message Date
5b62dd300b gw: fix synthesis
Still unclear what exactly this fixes, I think mostly bugs in
migen/LiteX. Not sure if it's synthesizing the memory elements for the
sampler modules either.
2023-06-03 11:44:17 -06:00
32eeab2c66 fw: do most of command interface 2023-06-03 11:43:39 -06:00
0aef4f295d Start python library 2023-06-03 11:42:56 -06:00
0034d2e9e7 fw: Define simple command protocol 2023-05-31 21:16:58 -06:00
aebb3a58f0 gw: add ADC2-4 pinouts and update changed pins 2023-05-27 13:30:17 -06:00
8e6e483f92 gw: switch to strobe-style control bits for controller 2023-05-27 13:20:26 -06:00
14 changed files with 761 additions and 36 deletions

View File

@ -5,6 +5,39 @@ improved passive sonar data acquisition system. The goal of this system is
simply to be able to capture pinger data, filtered by the preprocessor board,
via ethernet, at significantly faster speed than the previous system.
## Pysonar
This provides a library to interface with the FPGA, both pulling data and configuring it. To install, simply `cd`
into this repo and:
```shell
pip install ./
```
A CLI/console to test configuration is provided as `sonar_config`. Usage can be found with
```shell
sonar_config --help
```
*Note: This is a pure python package. In an ideal world, I would have implemented all protocol
implementation in rust, and exported via PyO3, to keep potential changes in sync, but it's not
worth the implementation effort and potential installation difficulty here.*
## Hacky hacks to patch into migen
migen.fhdl.simplify.py (line 81): add the attributeerror exception
```
for port in mem.ports:
try:
sync = f.sync[port.clock.cd]
except KeyError:
sync = f.sync[port.clock.cd] = []
except AttributeError:
sync = f.sync["sys"]
```
## Repo Layout
```

7
firmware/Cargo.lock generated
View File

@ -50,6 +50,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crc-any"
version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774646b687f63643eb0f4bf13dc263cb581c8c9e57973b6ddf78bda3994d88df"
[[package]]
name = "critical-section"
version = "1.1.1"
@ -102,6 +108,7 @@ dependencies = [
name = "fw"
version = "0.1.0"
dependencies = [
"crc-any",
"defmt",
"embedded-hal",
"panic-halt",

View File

@ -10,12 +10,13 @@ riscv-rt = "0.11.0"
riscv = "0.10.1"
panic-halt = "0.2.0"
embedded-hal = "0.2.7"
defmt = {version = "0.3.4", features = ["encoding-raw"] }
defmt = { version = "0.3.4", features = ["encoding-raw"] }
crc-any = { version = "2.4", default-features = false }
[dependencies.smoltcp]
version = "0.9.1"
default-features = false
features = ["medium-ethernet", "proto-ipv4", "socket-icmp", "defmt"]
features = ["medium-ethernet", "proto-ipv4", "socket-tcp", "socket-icmp", "defmt"]
[profile.release]
debug = true

View File

@ -0,0 +1,97 @@
use smoltcp::socket::tcp::{Socket, State};
use crate::proto::{
serialize_response_error, serialize_response_value, PacketParser, ResponsePacket, Settings, ErrorCodes
};
struct CommandInterface {
parser: PacketParser,
}
impl CommandInterface {
pub fn new() -> Self {
Self {
parser: PacketParser::new(),
}
}
/// Run the command interface
pub fn run(&mut self, sock: &mut Socket) {
// Note that we don't attempt to check status of the connection. All of
// the smoltcp functions handle the error cases gracefully. Recv
// provides a buffer of length 0 when there is nothing to receive, so we
// are good there. We just reset on any smoltcp errors.
// TODO should put packet parsing behind a fn with the smoltcp result, then on smoltcp error
// we reconfigure here instead of calling it 4 times
// Try and parse a packet
let res = sock.recv(|rx_buf| {
let (res, processed_bytes) = self.parser.try_parse(rx_buf);
(processed_bytes, res.ok())
});
// Check for socket errors
let packet = match res {
Ok(Some(packet)) => packet,
// No packet to process
Ok(None) => return,
// Any socket error means we just want to drop the connection and re-connect
Err(_) => {
Self::reestablish_socket(sock);
return;
}
};
// Check that setting is valid
let setting = match Settings::try_from(packet.setting) {
Ok(v) => v,
Err(()) => {
// Write out an error packet
let result = sock.send(|tx_buf| {
if tx_buf.len() < 8 {
return (0, ());
}
let response = serialize_response_error(packet.setting, ErrorCodes::InvalidSetting);
&tx_buf[0..8].copy_from_slice(&response);
return (8, ());
});
if let Err(_) = result {
// TX error, close socket and reconnect
Self::reestablish_socket(sock);
}
return;
}
};
// TODO validate setting values
// TODO handle actually changing/getting settings in all the ways
// For temp testing, return values sent
match sock.send(|tx_buf| {
if tx_buf.len() < 8 {
// Since this is a low-BW configuration socket, we assume we have space, and drop
// the response otherwise. If this is an issue, may re-think this and queue commands but
// for now this is fine.
return (0, ());
}
let response = serialize_response_value(setting, packet.value);
&tx_buf[0..8].copy_from_slice(&response);
return (8, ());
}) {
Ok(()) => (),
Err(_) => {
Self::reestablish_socket(sock);
}
};
}
fn reestablish_socket(sock: &mut Socket) {
sock.abort();
sock.listen(2000);
}
}

View File

@ -28,6 +28,8 @@ mod mcp4726;
mod uart;
mod litex_uart;
mod logging;
mod proto;
mod command_interface;
const MAC: [u8; 6] = [0xA0, 0xBB, 0xCC, 0xDD, 0xEE, 0xF0];

229
firmware/src/proto.rs Normal file
View File

@ -0,0 +1,229 @@
//! # Configuration protocol
//!
//! Configuration and runtime management is done over a separate TCP port, to
//! simplify management. It follows a simple binary protocol, described here.
//!
//! The protocol is packet-based, 2 bytes start sequence, 1 byte command, 4 byte
//! data, 1 byte CRC. CRC includes the start sequence, and is MAXIM-DOW. Chosen
//! because the crc python package had it as well.
//!
//! The 4 bytes of data are little-endian.
//!
//! For the settings that can be set, see [Settings].
//!
//! ## Sending a command
//!
//! The command is simply the index of the setting to read or write, with the
//! top bit set to indicate a write. Set the value to 0 during a read.
//!
//! ## Receiving a response
//!
//! The command is the index of the setting given, with the top bit set if it's
//! an error. The value is either the value saved in the setting, or an error
//! code. Error codes are defined in [ErrorCodes]
/// Start sequence to indicate the beginning of a packet.
const START_SEQUENCE: [u8; 2] = [0xDE, 0xAD];
/// Possible return values from a command
#[derive(Clone, Copy, Debug)]
pub enum ErrorCodes {
/// Command accepted
Success = 0,
/// Value was not valid for the provided setting
ValueOutOfRange = 1,
/// Setting index provided does not exist
InvalidSetting = 2,
}
/// Setting indices
#[derive(Clone, Copy, Debug)]
pub enum Settings {
/// Trigger threshold (in ADC counts, peak to peak is 1024)
TriggerThreshold = 0,
/// Number of samples that diff must be higher than threshold to trigger
TriggerPeriod = 1,
/// Value to decay difference every DecayPeriod cycles
DecayValue = 2,
/// The number of cycles between every decay
DecayPeriod = 3,
/// Gain setting of preprocessor (value in dB)
Gain = 4,
/// Center frequency to set preprocessor filter to (value in Hz)
CenterFreq = 5,
/// Sampling enabled, 1 to enable, 0 to disable
SamplingEnabled = 6,
}
#[derive(Clone, Copy, Debug)]
pub enum Error {
/// Start sequence incorrect, no packet
BadStartSequence,
/// CRC was bad
BadCrc,
/// State machine error
StateMachineError,
/// No packet found in data
NoPacket,
}
impl TryFrom<u8> for Settings {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::TriggerThreshold),
1 => Ok(Self::TriggerPeriod),
2 => Ok(Self::DecayValue),
3 => Ok(Self::DecayPeriod),
4 => Ok(Self::Gain),
5 => Ok(Self::CenterFreq),
6 => Ok(Self::SamplingEnabled),
_ => Err(()),
}
}
}
pub struct CommandPacket {
pub is_write: bool,
pub setting: u8,
pub value: u32,
}
pub struct ResponsePacket {
pub is_error: bool,
pub setting: u8,
pub value: u32,
}
/// Serialize a valid response
pub fn serialize_response_value(setting: Settings, value: u32) -> [u8; 8] {
serialize_response(setting as u8, value)
}
/// Serialize an error response
pub fn serialize_response_error(setting: u8, error: ErrorCodes) -> [u8; 8] {
serialize_response(0x80 | setting, error as u32)
}
fn serialize_response(command: u8, value: u32) -> [u8; 8] {
let mut crc = crc_any::CRCu8::crc8maxim();
let mut buf = [0u8; 8];
// Construct packet data
&buf[0..2].copy_from_slice(&START_SEQUENCE);
buf[2] = command;
for i in 0..4 {
buf[3 + i] = (value >> (8 * i)) as u8;
}
// Run CRC and append value
crc.digest(&buf[0..7]);
buf[7] = crc.get_crc();
buf
}
pub struct PacketParser {
packet_index: u8,
crc: crc_any::CRCu8,
is_write: bool,
setting: u8,
value: u32,
}
impl PacketParser {
pub fn new() -> Self {
Self {
packet_index: 0,
crc: crc_any::CRCu8::crc8maxim(),
is_write: false,
setting: 0,
value: 0,
}
}
/// Try and parse a packet from a received buffer. Returns Some(packet) if a
/// packet was found, and the number of bytes processed from the buffer. Bytes
/// processed may be smaller than the buffer length if a packet was found.
pub fn try_parse(&mut self, buf: &[u8]) -> (Result<CommandPacket, Error>, usize) {
let mut bytes_processed = 0;
for byte in buf {
bytes_processed += 1;
match self.process_byte(*byte) {
Ok(packet) => return (Ok(packet), bytes_processed),
// We don't care to pass errors to the user
// TODO log
_ => (),
}
}
(Err(Error::NoPacket), bytes_processed)
}
/// Process incoming byte using a simple state machine.
pub fn process_byte(&mut self, byte: u8) -> Result<CommandPacket, Error> {
match self.packet_index {
// Check the start sequence
0 => {
if byte != START_SEQUENCE[0] {
self.reset();
return Err(Error::BadStartSequence);
}
}
1 => {
if byte != START_SEQUENCE[1] {
self.reset();
return Err(Error::BadStartSequence);
}
}
// Save data
2 => {
self.is_write = byte & 0x80 != 0;
self.setting = byte & 0x7F;
}
3..=6 => {
self.value |= (byte << (8 * (self.packet_index - 3))) as u32;
}
// Check CRC
7 => {
if self.crc.get_crc() == byte {
return Ok(CommandPacket {
is_write: self.is_write,
setting: self.setting,
value: self.value,
});
} else {
self.reset();
return Err(Error::BadCrc);
}
}
// Should be unreachable, just reset
_ => {
self.reset();
return Err(Error::StateMachineError);
}
}
// Increment index
self.packet_index += 1;
// Process CRC (sucks I can't just give it one byte)
self.crc.digest(&[byte]);
Err(Error::NoPacket)
}
/// Reset values to start parsing a packet again
fn reset(&mut self) {
self.packet_index = 0;
self.crc = crc_any::CRCu8::crc8maxim();
self.value = 0;
}
}

View File

@ -27,7 +27,7 @@ from litedram.phy import GENSDRPHY, HalfRateGENSDRPHY
from liteeth.phy.ecp5rgmii import LiteEthPHYRGMII
from sampler import Sampler
from sampler import SamplerController, Sampler
from litex.soc.integration.soc import SoCRegion
from test import run_test, TestResult, skip_suite
@ -143,11 +143,11 @@ class BaseSoC(SoCCore):
if with_video_framebuffer:
self.add_video_framebuffer(phy=self.videophy, timings="800x600@60Hz", clock_domain="hdmi")
self.submodules.sampler = Sampler(platform.request("adc"), self.crg.cd_sample_clock.clk)
sampler_region = SoCRegion(origin=None, size=0x1000, cached=False)
#self.add_wb_slave(0x9000_0000, self.sampler.bus, 0x1000)
## TODO better way to do this?
#self.bus.add_slave(name="sampler", slave=self.sampler.bus, region=sampler_region)
#samplers = [Sampler(platform.request("adc", i)) for i in range(3)]
#self.submodules.sampler_controller = SamplerController(samplers, buffer_len=2048 * 10)
### TODO better way to do this?
#sampler_region = SoCRegion(origin=None, size=0x4000, cached=False)
#self.bus.add_slave(name="sampler", slave=self.sampler_controller.bus, region=sampler_region)
# Build --------------------------------------------------------------------------------------------
@ -185,6 +185,7 @@ def main():
results.append(run_test("SamplerController", controller.test_bus_access))
results.append(run_test("SamplerController", controller.test_simple_waveform))
results.append(run_test("SamplerController", controller.test_simple_waveform_capture_offset))
results.append(run_test("SamplerController", controller.test_multiple_reads))
results.append(run_test("PeakDetector", peak_detector.test_simple_waveform))
results.append(run_test("PeakDetector", peak_detector.test_scrunched_simple_waveform))
results.append(run_test("PeakDetector", peak_detector.test_decay_simple_waveform))
@ -199,8 +200,9 @@ def main():
print(f"{passed}/{passed + failed} passed ({skipped} skipped)")
# TODO maybe don't do this?
return
if failed > 0 or not args.build:
# Don't also build after this
return
# Build firmware
import subprocess as sp

View File

@ -19,7 +19,7 @@ from litex.build.lattice.programmer import EcpDapProgrammer
# IOs ----------------------------------------------------------------------------------------------
_io_v7_0 = [ # Documented by @smunaut
_io_v7_0 = [ # Colorlight i9 documented by @smunaut
# Clk
("clk25", 0, Pins("P3"), IOStandard("LVCMOS33")),
@ -117,13 +117,42 @@ _io_v7_0 = [ # Documented by @smunaut
# High speed parallel ADCs
("adc", 0,
Subsignal("data", Pins("M18 N18 N17 P18 U17 U18 T17 M17 P17 R17")),
# TODO ???? what other pins are changed in 7.2
Subsignal("refclk", Pins("L2")),
Subsignal("oen_b", Pins("K18")),
Subsignal("standby", Pins("C18")),
Subsignal("dfs", Pins("T18")),
Subsignal("otr", Pins("R18")),
# Rev A pins
#Subsignal("data", Pins("M18 N18 N17 P18 U17 U18 T17 M17 P17 R17")),
# Rev B pins
Subsignal("data", Pins("L20 M18 N18 N17 P18 U17 U18 T17 M17 P17")),
Subsignal("refclk", Pins("K18")),
Subsignal("oen_b", Pins("C18")),
Subsignal("standby", Pins("T18")),
Subsignal("dfs", Pins("R18")),
Subsignal("otr", Pins("R17")),
IOStandard("LVCMOS33")
),
("adc", 1,
Subsignal("data", Pins("R1 T1 U1 Y2 W1 V1 M1 N2 N3 T2")),
Subsignal("refclk", Pins("M4")),
Subsignal("oen_b", Pins("N4")),
Subsignal("standby", Pins("R3")),
Subsignal("dfs", Pins("T3")),
Subsignal("otr", Pins("M3")),
IOStandard("LVCMOS33")
),
("adc", 2,
Subsignal("data", Pins("A18 C17 A19 B18 B19 B20 C20 D19 D20 E19")),
Subsignal("refclk", Pins("J19")),
Subsignal("oen_b", Pins("H20")),
Subsignal("standby", Pins("G20")),
Subsignal("dfs", Pins("G19")),
Subsignal("otr", Pins("F20")),
IOStandard("LVCMOS33")
),
("adc", 3,
Subsignal("data", Pins("E4 F1 F3 G3 H3 H4 H5 J4 J5 K3")),
Subsignal("refclk", Pins("M4")),
Subsignal("oen_b", Pins("N4")),
Subsignal("standby", Pins("B3")),
Subsignal("dfs", Pins("K5")),
Subsignal("otr", Pins("K4")),
IOStandard("LVCMOS33")
),
]
@ -146,9 +175,7 @@ _io_v7_2 = copy.deepcopy(_io_v7_0)
for i, x in enumerate(_io_v7_2):
if x[:2] == ("user_led_n", 0):
# TODO fix in HW
#_io_v7_2[i] = ("user_led_n", 0, Pins("L2"), IOStandard("LVCMOS33"))
_io_v7_2[i] = ("user_led_n", 0, Pins("J19"), IOStandard("LVCMOS33"))
_io_v7_2[i] = ("user_led_n", 0, Pins("L2"), IOStandard("LVCMOS33"))
break
# optional, alternative uart location

View File

@ -34,14 +34,23 @@ class CircularBuffer(Module):
rd_ptr = Signal(ptr_width)
empty = Signal(reset=1) # Extra signal to distinguish between full and empty condition
# TODO this shouldn't be needed. Bug in migen IMO, I don't use this signal
dat_r = Signal(width)
# Hook write input signals to memory
wr_port = storage.get_port(write_capable=True)
# TODO hacky bullshit because migen is broken or I'm using it wrong
if not hasattr(wr_port.clock, "cd"):
wr_port.clock.cd = "sys"
# Always ready to write data into memory, so hook these signals straight in
self.comb += [
wr_port.adr.eq(wr_ptr),
wr_port.dat_w.eq(self.wr_data),
wr_port.we.eq(self.wr_valid),
self.wr_ready.eq(1), # We are always ready to write data in
dat_r.eq(wr_port.dat_r)
]
# Advance write (and potentially read)
@ -66,6 +75,10 @@ class CircularBuffer(Module):
# TODO should I actually set async_read?
rd_port = storage.get_port(async_read=True)
# TODO hacky bullshit because migen is broken or I'm using it wrong
if not hasattr(rd_port.clock, "cd"):
rd_port.clock.cd = "sys"
# Set read addr so 0 starts at rd_ptr and wraps around, and connect read data up
self.comb += [
If(self.rd_addr + rd_ptr < depth,

View File

@ -27,11 +27,14 @@ class SamplerController(Module):
Registers
--------
0x00: Control Register (RW)
Bit 0 - Begin capture. Resets all FIFOs and starts the peak detector
0x00: Control Register (WO)
Bit 0 - Start capture
Bit 1 - Stop capture. Does nothing if capture is not ongoing
Bit 2 - Clear sample buffers
0x01: Status Register (RO)
Bit 0 - Capture complete. Set by peak detection block and cleared when capture is began
Bit 1 - Sampling running
0x02: trigger_run_len (RW)
Number of samples to acquire after triggering sample.
@ -138,7 +141,7 @@ class SamplerController(Module):
# Handle explicit config registers
cases = {
0: rw_register(control_register),
0: rw_register(control_register, read=False),
1: rw_register(status_register, write=False),
2: rw_register(trigger_run_len),
3: rw_register(self.peak_detector.thresh_value),
@ -156,6 +159,8 @@ class SamplerController(Module):
# Connect up control registers bus
self.sync += [
self.control_regs_bus.ack.eq(0),
# Hold control register low to use as strobe functionality
control_register.eq(0),
If(self.control_regs_bus.cyc & self.control_regs_bus.stb,
self.control_regs_bus.ack.eq(1),
Case(self.control_regs_bus.adr, cases)),
@ -180,13 +185,20 @@ class SamplerController(Module):
# We have sampled enough, update status and stop sampling
If(post_trigger_count + 1 >= trigger_run_len,
status_register[0].eq(1),
control_register[0].eq(0))),
sample_enable.eq(0))),
]
# Update register storage
self.comb += [
sample_enable.eq(control_register[0]),
self.sync += [
status_register[1].eq(sample_enable),
If(control_register[0], sample_enable.eq(1)),
If(control_register[1], sample_enable.eq(0)),
]
for buffer in self.buffers:
self.sync += [
buffer.clear.eq(0),
If(control_register[2], buffer.clear.eq(1)),
]
def write_wishbone(bus, address, value):
# Set up bus
@ -341,6 +353,11 @@ def test_simple_waveform():
sample = (yield dut.bus.dat_r)
data.append(sample)
# Manually validated, this is what we should read on a correct
# run
assert data[15] == 138
assert data[16] == 132
# Test pass
return
@ -418,3 +435,172 @@ def test_simple_waveform_capture_offset():
assert False, "We should have triggered"
run_simulation(dut, test_fn())
def test_multiple_reads():
"""
Testing multiple triggers/captures in succession to ensure typical (i.e. repeated) operation
works correctly.
"""
# Enable, trigger works correctly
# Enable, tick a bit of data in, should not trigger, and trigger should have reset immediately
# Enable again, and tick in lots of data, should trigger again now
"""Test a simple waveform captured at an offset"""
from .peak_detector import create_waveform
_, data = create_waveform()
data = [int(d) for d in data]
dut = TestSoC(data, buffer_len=32)
def test_fn():
# Set settings
yield from write_wishbone(dut.bus, 2, 0) # trigger_run_len = 0
yield from write_wishbone(dut.bus, 3, 800) # thresh_value = 800
yield from write_wishbone(dut.bus, 4, 10) # thresh_time = 10
yield from write_wishbone(dut.bus, 5, 1) # decay_value = 1
yield from write_wishbone(dut.bus, 5, 0) # decay_period = 0
# Start controller
yield from write_wishbone(dut.bus, 0, 1)
triggered_yet = False
triggered_num = 0
for i in range(1000):
(yield dut.samplers[0].index.eq(i))
(yield dut.samplers[0].valid.eq(1))
yield
(yield dut.samplers[0].valid.eq(0))
yield
# Total of 6 clocks per sample clock
yield
yield
yield
yield
if not triggered_yet and (yield dut.controller.peak_detector.triggered) == 1:
# Triggered, now we need to run some number of cycles
triggered_yet = True
if triggered_yet:
triggered_num += 1
if triggered_num > 16:
# We should now have collected all our samples
yield from read_wishbone(dut.bus, 1)
assert (yield dut.bus.dat_r) == 1, "Trigger did not propogate to WB!"
# Check that length is correct
yield from read_wishbone(dut.bus, 0x100)
len = (yield dut.bus.dat_r)
assert len == 32, f"Len ({len}) not correct!"
# Read data in
data = []
for i in range(32):
yield from read_wishbone(dut.bus, 0x800 + i)
sample = (yield dut.bus.dat_r)
data.append(sample)
# Manually validated from test above to be offset into the
# data
assert data[15] == 138
assert data[16] == 132
break
assert triggered_yet, "We should have triggered"
# Clear out sampler and re-enable
yield from write_wishbone(dut.bus, 0, 0b101)
yield
assert (yield dut.controller.peak_detector.triggered) == 0, "Trigger should have been cleared"
assert (yield dut.controller.buffers[0].len) == 0, "Buffers should have been cleared"
# Tick a few clocks through, and we shouldn't have triggered
for i in range(10):
(yield dut.samplers[0].index.eq(i))
(yield dut.samplers[0].valid.eq(1))
yield
(yield dut.samplers[0].valid.eq(0))
yield
# Total of 6 clocks per sample clock
yield
yield
yield
yield
assert (yield dut.controller.peak_detector.triggered) == 0, "We didn't push enough data through to trigger"
# Disable sampler, run lots of data through, we should not trigger
yield from write_wishbone(dut.bus, 0, 0b010)
for i in range(1000):
(yield dut.samplers[0].index.eq(i))
(yield dut.samplers[0].valid.eq(1))
yield
(yield dut.samplers[0].valid.eq(0))
yield
# Total of 6 clocks per sample clock
yield
yield
yield
yield
# Enable sampler and run again, we should get another trigger
yield from write_wishbone(dut.bus, 2, 16) # trigger_run_len = 16
yield from write_wishbone(dut.bus, 0, 1)
triggered_yet = False
triggered_num = 0
for i in range(1000):
(yield dut.samplers[0].index.eq(i))
(yield dut.samplers[0].valid.eq(1))
yield
(yield dut.samplers[0].valid.eq(0))
yield
# Total of 6 clocks per sample clock
yield
yield
yield
yield
if not triggered_yet and (yield dut.controller.peak_detector.triggered) == 1:
# Triggered, now we need to run some number of cycles
triggered_yet = True
if triggered_yet:
triggered_num += 1
if triggered_num > 16:
# We should now have collected all our samples
yield from read_wishbone(dut.bus, 1)
assert (yield dut.bus.dat_r) == 1, "Trigger did not propogate to WB!"
# Check that length is correct
yield from read_wishbone(dut.bus, 0x100)
len = (yield dut.bus.dat_r)
assert len == 32, f"Len ({len}) not correct!"
# Read data in
data = []
for i in range(32):
yield from read_wishbone(dut.bus, 0x800 + i)
sample = (yield dut.bus.dat_r)
data.append(sample)
# Manually validated from test above to be offset into the
# data
assert data[0] == 138
assert data[1] == 132
# Test pass
return
assert triggered_yet, "We should have triggered"
run_simulation(dut, test_fn(), vcd_name="controller.vcd")

View File

@ -3,14 +3,9 @@ from migen.genlib.cdc import PulseSynchronizer
class Sampler(Module):
def __init__(self, adc_pins: Record, sampler_clock: Signal):
# self.clock_domains.foo = ClockDomain() is how to add a new clock domain, accessible at self.foo
# Connect sampler clock domain
self.clock_domains.sample_clock = ClockDomain("sample_clock")
self.comb += self.sample_clock.clk.eq(sampler_clock)
def __init__(self, adc_pins: Record):
# Hook up ADC REFCLK to sample_clock
self.comb += adc_pins.refclk.eq(sampler_clock)
self.comb += adc_pins.refclk.eq(ClockDomain("sample_clock").clk)
# We can synchronize to the sampler clock, whenever it goes high we can
# strobe a single valid signal
@ -21,10 +16,10 @@ class Sampler(Module):
self.data = Signal(10)
self.comb += [
synchronizer.i.eq(self.sample_clock.clk),
synchronizer.i.eq(ClockDomain("sample_clock").clk),
self.valid.eq(synchronizer.o),
self.data.eq(adc_pins.data),
]
self.sync += self.data.eq(adc_pins.data)
# Set config pins to constant values
self.comb += adc_pins.oen_b.eq(0) # Data pins enable

23
pyproject.toml Normal file
View File

@ -0,0 +1,23 @@
# Built from this documentation: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
[project]
name = "pysonar"
description = "Library to communicate with ARVP's sonar FPGA"
version = "0.1.0"
authors = [
{ name = "David Lenfesty", email = "lenfesty@ualberta.ca" }
]
requires-python = ">=3.9"
dependencies = ["crc>=4.2"]
[project.scripts]
# Export CLI to configure FPGA
sonar_config = "pysonar:main"
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
# Just include the python directory
packages = ["pysonar"]

2
pysonar/__init__.py Normal file
View File

@ -0,0 +1,2 @@
def main():
print("Hello world!")

108
pysonar/command_packets.py Normal file
View File

@ -0,0 +1,108 @@
from crc import Calculator, Crc8
from typing import Optional, Tuple
from dataclasses import dataclass
from enum import IntEnum
from struct import pack
PKT_START_SEQUENCE = [0xDE, 0xAD]
class Settings(IntEnum):
TriggerThreshold = 0
TriggerPeriod = 1
DecayValue = 2
DecayPeriod = 3
Gain = 4
CenterFreq = 5
SamplingEnabled = 6
@dataclass
class CommandPacket:
is_write: bool
setting: Settings
value: int
def serialize(self) -> bytearray:
buf = bytearray()
# TODO raise exception if setting is invalid here
buf.extend(PKT_START_SEQUENCE)
command = int(self.setting)
if self.is_write:
command |= 1 << 7
buf.append(command)
buf.extend(pack("<I", self.value))
# Append CRC
crc = Calculator(Crc8.MAXIM_DOW).checksum(buf)
buf.append(crc)
return buf
@dataclass
class ResponsePacket:
# Deserialization
is_error: bool
setting: Settings
value: int
class PacketParser:
def __init__(self):
self.crc = Calculator(Crc8.MAXIM_DOW)
self.reset()
def parse_byte(self, b: int) -> Optional[ResponsePacket]:
byte_index = len(self.packet_data)
if byte_index == 0:
if b != PKT_START_SEQUENCE[0]:
self.reset()
return
elif byte_index == 1:
if b != PKT_START_SEQUENCE[1]:
self.reset()
return
elif byte_index == 7:
# Final CRC byte, check and generate data
packet = None
if self.crc.verify(self.packet_data, b):
packet = self.build_response_packet()
self.reset()
return packet
# Pull packet data in otherwise
self.packet_data.append(b)
def parse_bytearray(self, bytes: bytearray) -> Tuple[bytearray, Optional[ResponsePacket]]:
"""
Parse a bytearray for a packet. Returns a bytearray of bytes that haven't been processed.
"""
for offset, byte in enumerate(bytes):
packet = self.parse_byte(byte)
if packet is not None:
# Return packet and consumed bytearray
return (bytes[offset:], packet)
# No packets found, entire buffer has been read
return (bytearray(), None)
def build_response_packet(self) -> ResponsePacket:
"""Builds a response packet out of the data we have received"""
if len(self.packet_data) < 7:
raise Exception("Invalid amount of packet data received!")
def reset(self):
self.packet_data = bytearray()