Compare commits
6 Commits
0e3328aac1
...
5b62dd300b
Author | SHA1 | Date | |
---|---|---|---|
5b62dd300b | |||
32eeab2c66 | |||
0aef4f295d | |||
0034d2e9e7 | |||
aebb3a58f0 | |||
8e6e483f92 |
33
README.md
33
README.md
@ -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
7
firmware/Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
97
firmware/src/command_interface.rs
Normal file
97
firmware/src/command_interface.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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
229
firmware/src/proto.rs
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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
23
pyproject.toml
Normal 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
2
pysonar/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
def main():
|
||||
print("Hello world!")
|
108
pysonar/command_packets.py
Normal file
108
pysonar/command_packets.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user