Download fpga/host/src/protocol.rs from Snapkitty/bit-string-accelerator: direct link, hf CLI and curl.
- Browser
- Download file 5.69 kB
-
https://huggingface.co/Snapkitty/bit-string-accelerator/resolve/main/fpga/host/src/protocol.rs
- Command line
-
hf download hf://Snapkitty/bit-string-accelerator/fpga/host/src/protocol.rs
-
curl -L -o protocol.rs https://huggingface.co/Snapkitty/bit-string-accelerator/resolve/main/fpga/host/src/protocol.rs
5.69 kB
| //! Byte-level protocol of `fpga/rtl/fpga_top.sv`. Multi-byte fields are little-endian. | |
| /// Opcodes accepted by the accelerator. | |
| pub enum Op { | |
| Get = 0, | |
| Test = 1, | |
| Set = 2, | |
| Clear = 3, | |
| Toggle = 4, | |
| } | |
| impl Op { | |
| pub fn parse(s: &str) -> Option<Op> { | |
| match s.to_ascii_lowercase().as_str() { | |
| "get" => Some(Op::Get), | |
| "test" => Some(Op::Test), | |
| "set" => Some(Op::Set), | |
| "clear" => Some(Op::Clear), | |
| "toggle" => Some(Op::Toggle), | |
| _ => None, | |
| } | |
| } | |
| } | |
| pub const PING: u8 = b'P'; | |
| pub const PONG: u8 = b'B'; | |
| pub const WRITE: u8 = b'W'; | |
| pub const WRITE_ACK: u8 = b'K'; | |
| pub const READ: u8 = b'R'; | |
| pub const EXEC: u8 = b'X'; | |
| pub const UNKNOWN: u8 = b'?'; | |
| pub const STATUS_TAG: u8 = 0xA0; | |
| pub const STATUS_TAG_MASK: u8 = 0xFC; | |
| /// Number of 64-bit words in the FPGA build's RAM; byte addresses 0..WORDS*8 are in | |
| /// range. The Tiny Tapeout chip has 4 (`--words 4`). | |
| pub const WORDS: usize = 256; | |
| pub fn write_frame(index: u8, value: u64) -> Vec<u8> { | |
| let mut f = vec![WRITE, index]; | |
| f.extend_from_slice(&value.to_le_bytes()); | |
| f | |
| } | |
| pub fn read_frame(index: u8) -> Vec<u8> { | |
| vec![READ, index] | |
| } | |
| /// `opcode` is sent as given (3 bits), so undefined opcodes 5..=7 can be exercised. | |
| pub fn exec_frame(opcode: u8, base: u64, offset: u64) -> Vec<u8> { | |
| let mut f = vec![EXEC, opcode & 0x07]; | |
| f.extend_from_slice(&base.to_le_bytes()); | |
| f.extend_from_slice(&offset.to_le_bytes()); | |
| f | |
| } | |
| /// Result of one accelerator operation. | |
| pub struct Status { | |
| pub error: bool, | |
| /// Meaningful only when `error` is false. | |
| pub bit: bool, | |
| } | |
| /// Describe a reply byte that was not the one expected. | |
| pub fn unexpected(what: &str, byte: u8) -> String { | |
| if byte == UNKNOWN { | |
| format!("{what}: the FPGA did not recognize the command (reply '?')") | |
| } else { | |
| format!("{what}: unexpected reply 0x{byte:02x}") | |
| } | |
| } | |
| pub fn parse_status(byte: u8) -> Result<Status, String> { | |
| if byte & STATUS_TAG_MASK != STATUS_TAG { | |
| return Err(format!("unexpected status byte 0x{byte:02x}")); | |
| } | |
| Ok(Status { | |
| error: byte & 0x02 != 0, | |
| bit: byte & 0x01 != 0, | |
| }) | |
| } | |
| /// Host-side reference model of one operation on a RAM image, matching the RTL: | |
| /// effective bit = (base << 3) + offset (mod 2^64), byte address = (bit >> 6) << 3, | |
| /// out-of-range addresses and undefined opcodes report an error and do not write. | |
| pub fn model_exec(ram: &mut [u64], opcode: u8, base: u64, offset: u64) -> Status { | |
| let eff = (base << 3).wrapping_add(offset); | |
| let word = eff >> 6; | |
| let bit = (eff & 63) as u32; | |
| if word >= ram.len() as u64 || opcode > 4 { | |
| return Status { | |
| error: true, | |
| bit: false, | |
| }; | |
| } | |
| let w = &mut ram[word as usize]; | |
| let old = (*w >> bit) & 1 == 1; | |
| let mask = 1u64 << bit; | |
| let new_bit = match opcode { | |
| 2 => { | |
| *w |= mask; | |
| true | |
| } | |
| 3 => { | |
| *w &= !mask; | |
| false | |
| } | |
| 4 => { | |
| *w ^= mask; | |
| !old | |
| } | |
| _ => old, | |
| }; | |
| Status { | |
| error: false, | |
| bit: new_bit, | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| fn frames_are_little_endian() { | |
| assert_eq!( | |
| write_frame(3, 0x0102_0304_0506_0708), | |
| vec![b'W', 3, 8, 7, 6, 5, 4, 3, 2, 1] | |
| ); | |
| assert_eq!(read_frame(255), vec![b'R', 255]); | |
| let x = exec_frame(Op::Toggle as u8, 0x11, 0x2233); | |
| assert_eq!(x.len(), 18); | |
| assert_eq!(&x[..2], &[b'X', 4]); | |
| assert_eq!(&x[2..10], &0x11u64.to_le_bytes()); | |
| assert_eq!(&x[10..], &0x2233u64.to_le_bytes()); | |
| } | |
| fn status_byte() { | |
| assert_eq!( | |
| parse_status(0xA0), | |
| Ok(Status { | |
| error: false, | |
| bit: false | |
| }) | |
| ); | |
| assert_eq!( | |
| parse_status(0xA1), | |
| Ok(Status { | |
| error: false, | |
| bit: true | |
| }) | |
| ); | |
| assert!(parse_status(0xA2).unwrap().error); | |
| assert!(parse_status(b'K').is_err()); | |
| assert!(parse_status(0xB1).is_err()); | |
| } | |
| fn model_matches_rtl_rules() { | |
| let mut ram = [0u64; WORDS]; | |
| ram[0] = 0x8000_0000_0000_0001; | |
| assert_eq!( | |
| model_exec(&mut ram, 0, 0, 63), | |
| Status { | |
| error: false, | |
| bit: true | |
| } | |
| ); | |
| assert_eq!( | |
| model_exec(&mut ram, 2, 8, 6), | |
| Status { | |
| error: false, | |
| bit: true | |
| } | |
| ); | |
| assert_eq!(ram[1], 0x40); | |
| // (7 << 3) + 9 = bit 65 = word 1, bit 1: was 0, toggles to 1. | |
| assert_eq!( | |
| model_exec(&mut ram, 4, 7, 9), | |
| Status { | |
| error: false, | |
| bit: true | |
| } | |
| ); | |
| assert_eq!(ram[1], 0x42); | |
| assert!(model_exec(&mut ram, 0, 2048, 0).error); // byte 2048 is past the RAM | |
| assert!(model_exec(&mut ram, 5, 0, 0).error); // undefined opcode | |
| assert!(model_exec(&mut ram, 0, u64::MAX, 0).error); | |
| let mut small = [0u64; 4]; // Tiny Tapeout RAM: bytes 0..31 | |
| assert!(!model_exec(&mut small, 2, 31, 7).error); | |
| assert_eq!(small[3], 1 << 63); | |
| assert!(model_exec(&mut small, 0, 32, 0).error); | |
| } | |
| fn op_names() { | |
| assert_eq!(Op::parse("TOGGLE"), Some(Op::Toggle)); | |
| assert_eq!(Op::parse("nope"), None); | |
| } | |
| } | |