From 8f6b7fbc1fcd12e9fb268a64a0bdb65e02291073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Proch=C3=A1zka?= Date: Thu, 18 Sep 2025 13:17:47 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 184 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 9 ++ src/args.rs | 87 +++++++++++++++++++ src/ihex.rs | 139 ++++++++++++++++++++++++++++++ src/main.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++++++ src/target.rs | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/tests.rs | 128 +++++++++++++++++++++++++++ src/utils.rs | 56 ++++++++++++ 9 files changed, 1063 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/args.rs create mode 100644 src/ihex.rs create mode 100644 src/main.rs create mode 100644 src/target.rs create mode 100644 src/tests.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9a52f21 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,184 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "argp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7409aa6f1dd8464eac2e56cf538e1e5f7f79678caa32f198d214a3db8d5075c1" +dependencies = [ + "argp_derive", +] + +[[package]] +name = "argp_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9b949411282939e3f7d8923127e3f18aa474b46da4e8bb0ddf2cb8c81f963a" +dependencies = [ + "proc-macro2", + "pulldown-cmark", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "pbr" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5827dfa0d69b6c92493d6c38e633bbaa5937c153d0d7c28bf12313f8c6d514" +dependencies = [ + "crossbeam-channel", + "libc", + "winapi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serial2" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349bb79c63bd2690fe4ca8ef7072a6d9ca36b3a1687a08393e733ea56d573911" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tulflash" +version = "0.1.0" +dependencies = [ + "argp", + "pbr", + "serial2", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1c5e732 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "tulflash" +version = "0.1.0" +edition = "2024" + +[dependencies] +argp = "0.4.0" +pbr = "1.1.1" +serial2 = "0.2.32" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..8db4424 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,87 @@ +use std::ffi::OsString; + +use argp::FromArgs; + +#[derive(FromArgs)] +#[argp(description = "tulflash - Flashing utility for the TUL Výukový Přípravek v1.1™")] +pub struct Args { + #[argp(option)] + #[argp(description = "Path to the serial port. Probably will be `/dev/ttyUSBx` on Linux, `/dev/tty.usbserial-xxx` on macOS or `COMx` on Windows.")] + pub port: OsString, + + #[argp(switch)] + #[argp(description = "Try this if it does not work the first time.")] + pub slow: bool, + + #[argp(subcommand)] + pub command: ArgCommand +} + +#[derive(FromArgs)] +#[argp(subcommand)] +pub enum ArgCommand { + Read(ArgRead), + Write(ArgWrite), + Erase(ArgErase) +} + +#[derive(FromArgs)] +#[argp(description = "Reads the Flash memory contents into an Intel HEX file.")] +#[argp(subcommand, name = "read")] +pub struct ArgRead { + #[argp(positional)] + #[argp(description = "Path to the destination Intel HEX file.")] + pub path: OsString, + + #[argp(option, default = "0")] + #[argp(description = "Start address where to read from. Defaults to 0.")] + pub start: usize, + + #[argp(option, default = "65535")] + #[argp(description = "End address where to read to. Defaults to 65535.")] + pub end: usize, + + #[argp(switch)] + #[argp(description = "Output into a raw binary rather than an Intel HEX.")] + pub bin: bool +} + +#[derive(FromArgs)] +#[argp(description = "Writes the contents of an Intel HEX file into the Flash memory.")] +#[argp(subcommand, name = "write")] +pub struct ArgWrite { + #[argp(positional)] + #[argp(description = "Path to the source Intel HEX file.")] + pub path: OsString, + + #[argp(option, default = "0")] + #[argp(description = "Start address where to write from. Only for raw binaries, defaults to 0.")] + pub start: usize, + + #[argp(option, default = "65535")] + #[argp(description = "End address where to write to. Only for raw binaries, defaults to 65535.")] + pub end: usize, + + #[argp(switch)] + #[argp(description = "The input file is a binary rather than an Intel HEX.")] + pub bin: bool, + + #[argp(switch)] + #[argp(description = "Skip verification of the written file. Not really recommended.")] + pub skip_verify: bool, + + #[argp(switch)] + #[argp(description = "Write individual sections to Flash rather than performing a single write. Only applies to Intel HEX.")] + pub write_individual: bool +} + +#[derive(FromArgs)] +#[argp(description = "Erases the flash memory. You will probably never need this, as the chip will do absolutely nothing when reset after erasing.")] +#[argp(subcommand, name = "erase")] +pub struct ArgErase {} + +pub fn parse() -> Args { + let args: Args = argp::parse_args_or_exit(argp::DEFAULT); + args +} + diff --git a/src/ihex.rs b/src/ihex.rs new file mode 100644 index 0000000..9856657 --- /dev/null +++ b/src/ihex.rs @@ -0,0 +1,139 @@ +use std::io::{BufRead, BufReader, BufWriter, Error, Lines, Read, Write}; + +use crate::utils::Hex; + +pub struct IntelHexWriter { + writer: BufWriter, + data: Vec, + addr: u16 +} + +impl IntelHexWriter { + pub fn init(writer: T, addr: u16) -> Self { + Self { + writer: BufWriter::new(writer), + data: Vec::with_capacity(16), + addr + } + } + + fn write_data(&mut self, val: u8) -> Result<(), Error> { + if self.data.len() >= self.data.capacity() { + self.flush_data_record()?; + } + + self.data.push(val); + Ok(()) + } + + fn flush_data_record(&mut self) -> Result<(), Error> { + let mut data = [ + &[self.data.len() as u8], + self.addr.to_be_bytes().as_slice(), + &[0x00], + self.data.as_slice() + ].concat(); + + data.push(data.iter().fold(0, |acc: u8, val| acc.wrapping_add(*val)).wrapping_neg()); + + write!(self.writer, ":")?; + + for val in data { + write!(self.writer, "{val:02X}")?; + } + + writeln!(self.writer)?; + + self.addr = self.addr.wrapping_add(self.data.len() as u16); + self.data.clear(); + + Ok(()) + } + + pub fn finish(mut self) -> Result<(), Error> { + self.flush_data_record()?; + writeln!(self.writer, ":00000001FF")?; // hardcoded end of file record + self.writer.flush()?; + Ok(()) + } +} + +impl Write for IntelHexWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + for val in buf { + self.write_data(*val)?; + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } +} + +pub struct DataRecord { + pub addr: usize, + pub data: Vec +} + +pub struct IntelHexReader { + reader: Lines> +} + +impl IntelHexReader { + pub fn init(reader: T) -> Self { + Self { reader: BufReader::new(reader).lines() } + } +} + +impl Iterator for IntelHexReader { + type Item = Result; + + fn next(&mut self) -> Option { + loop { + let line = match self.reader.next()? { + Ok(line) => line, + Err(err) => return Some(Err(err)) + }; + + if !line.starts_with(":") { + return Some(Err(Error::other("Intel HEX record not starting with a colon"))) + } + + let line = &line.as_bytes()[1..]; // can't do this with matlab, can you? + + if line.len() % 2 != 0 { + return Some(Err(Error::other("Nibble count in Intel HEX record not even"))) + } + + let count = u8::hex_decode(line[..2].try_into().unwrap()); + let addr = u16::hex_decode(line[2..6].try_into().unwrap()) as usize; + let rectype = u8::hex_decode(line[6..8].try_into().unwrap()); + + let expected_bytes = count as usize; + let received_bytes = line.len() / 2 - 5; + + if expected_bytes != received_bytes { // Count (1), address (2), type (1), checksum (1) + return Some(Err(Error::other(format!("Byte count in Intel HEX not matching actual length (expected {expected_bytes}, got {received_bytes})")))) + } + + if rectype != 0 { + continue + } + + let data = (0..count as usize) + .map(|idx| { + let base = 8 + idx * 2; + u8::hex_decode(line[base..(base + 2)].try_into().unwrap()) + }) + .collect(); + + // fuck the checksum + + break Some(Ok(DataRecord { + addr, + data + })) + } + } +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..adb807c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,225 @@ +use std::{io::{Cursor, Error, Write}, time::{Duration, SystemTime}}; + +use pbr::ProgressBar; + +mod args; +use args::*; + +mod ihex; +use ihex::{IntelHexWriter, IntelHexReader, DataRecord}; + +mod target; +use target::{PhysicalTarget, Target}; + +mod utils; + +#[cfg(test)] +mod tests; + +pub fn read_chip_into(writer: &mut impl Write, target: &mut impl Target, start: usize, end: usize) -> Result<(), Error> { + let mut pb = ProgressBar::new((end - start) as u64 + 1); + pb.message("reading chip flash "); + pb.set_units(pbr::Units::Bytes); + + let mut last = start; + + for i in (start..=end).step_by(1024).chain(std::iter::once(end + 1)) { + if i == last { continue } + + let size = i - last; + + let mut buf = [0u8; 1024]; + + target.read_flash(last as u16, &mut buf[..size])?; + writer.write_all(&buf[..size])?; + + pb.add(size as u64); + + last = i; + } + + pb.finish_println(""); + Ok(()) +} + +pub fn read_chip(args: ArgRead, target: &mut impl Target) -> Result<(), Error> { + let mut file = std::fs::File::create(args.path)?; + + match args.bin { + true => read_chip_into(&mut file, target, args.start, args.end)?, + false => { + let mut writer = IntelHexWriter::init(file, args.start as u16); + + read_chip_into(&mut writer, target, args.start, args.end)?; + + writer.finish()?; + } + } + + Ok(()) +} + +pub fn write_chip_chunk(data: &[u8], target: &mut impl Target, mut start: usize, verify: bool) -> Result<(), Error> { + let mut head = Vec::new(); + + if start & 0x7F != 0 { + head.resize(start & 0x7F, 0); + + let mut pb = ProgressBar::new(head.len() as u64); + pb.message("reading chip flash "); + pb.set_units(pbr::Units::Bytes); + + target.read_flash(start as u16 & !0x7F, &mut head)?; + + pb.add(head.len() as u64); + pb.finish_println(""); + } + + let mut tail = Vec::new(); + + let end = start + data.len(); + + if end & 0x7F != 0 { + tail.resize(128 - (end & 0x7F), 0); + + let mut pb = ProgressBar::new(tail.len() as u64); + pb.message("reading chip flash "); + pb.set_units(pbr::Units::Bytes); + + target.read_flash(end as u16, &mut tail)?; + + pb.add(tail.len() as u64); + pb.finish_println(""); + } + + let data = [ &head, data, &tail ].concat(); + assert_eq!(data.len() & 0x7F, 0); + + start = start & !0x7F; + + if start + data.len() > 0x10000 { + Err(Error::other("Data does not fit into memory"))? + } + + let mut page = start / 128; + + let mut pb = ProgressBar::new(data.len() as u64); + pb.message("writing chip flash "); + pb.set_units(pbr::Units::Bytes); + + for chunk in data.as_chunks::<128>().0.iter() { + target.write_flash(page as u16, chunk)?; + + pb.add(chunk.len() as u64); + page += 1; + } + + pb.finish_println(""); + + if verify { + let mut verify: Cursor> = Cursor::new(Vec::new()); + read_chip_into(&mut verify, target, start, start + data.len() - 1)?; + + if verify.get_ref() != &data { + Err(Error::other("Write mismatch"))? + } + } + + Ok(()) +} + +pub fn write_chip(args: ArgWrite, target: &mut impl Target) -> Result<(), Error> { + if args.bin { + let mut file = std::fs::read(args.path)?; + + let size = args.end - args.start + 1; + + if size < file.len() { + file.resize(size, 0); + } + + return write_chip_chunk(&file, target, args.start, !args.skip_verify) + } + + let hexreader = IntelHexReader::init(std::fs::File::open(args.path)?); + + if args.write_individual { + let mut last_addr = 0; + let mut next_expected_addr = 0; + let mut last_data = Vec::new(); + + for record in hexreader { + let DataRecord { addr, mut data } = record?; + + if addr != next_expected_addr { + if !last_data.is_empty() { + write_chip_chunk(&last_data, target, last_addr, !args.skip_verify)?; + last_data.clear(); + } + + last_addr = addr; + next_expected_addr = addr; + } + + next_expected_addr += data.len(); + last_data.append(&mut data); + } + + if !last_data.is_empty() { + write_chip_chunk(&last_data, target, last_addr, !args.skip_verify)?; + } + + return Ok(()) + } + + let mut first_addr = 0xFFFF; + let mut last_addr = 0; + + let mut buf = vec![0u8; 0x10000]; + + for record in hexreader { + let DataRecord { addr, data } = record?; + + if addr < first_addr { + first_addr = addr; + } + + if data.len() + addr - 1 > last_addr { + last_addr = addr + data.len() - 1; + } + + buf[addr..addr + data.len()].copy_from_slice(&data); + } + + write_chip_chunk(&buf[first_addr..=last_addr], target, first_addr, !args.skip_verify) +} + +pub fn erase_chip(_args: ArgErase, target: &mut impl Target) -> Result<(), Error> { + target.erase()?; + + Ok(()) +} + +fn main() { + let args = args::parse(); + + let start = SystemTime::now(); + + let mut target = PhysicalTarget::init(args.port, if args.slow { 57600 } else { 312500 }).unwrap(); + + let res = match args.command { + ArgCommand::Read(args) => read_chip(args, &mut target), + ArgCommand::Write(args) => write_chip(args, &mut target), + ArgCommand::Erase(args) => erase_chip(args, &mut target) + }; + + if let Err(err) = res { + println!("An error occured during the operation: {}", err); + } + + let end = SystemTime::now(); + let dur = end.duration_since(start).unwrap_or(Duration::ZERO); + + println!("Finished operation in {} ms.", dur.as_millis()); +} + diff --git a/src/target.rs b/src/target.rs new file mode 100644 index 0000000..4289ef8 --- /dev/null +++ b/src/target.rs @@ -0,0 +1,234 @@ +use std::io::Error; +use std::path::Path; +use std::time::Duration; + +use serial2::SerialPort; + +use crate::utils::Hex; + +pub trait Target { + fn read_flash(&mut self, offset: u16, out: &mut [u8]) -> Result<(), Error>; + fn write_flash(&mut self, page: u16, data: &[u8; 128]) -> Result<(), Error>; + fn erase(&mut self) -> Result<(), Error>; +} + +pub struct PhysicalTarget { + port: SerialPort +} + +impl PhysicalTarget { + pub fn init(path: impl AsRef, baud: u32) -> Result { + let mut port = SerialPort::open(path, baud)?; + + port.set_read_timeout(Duration::from_millis(100))?; + + port.set_dtr(false)?; + std::thread::sleep(Duration::from_millis(100)); + + for i in (0..5).rev() { + port.write(b"U")?; + + let mut buf = [0u8; 1]; + port.read(&mut buf)?; + + if buf[0] == b'U' { break } + + if i == 0 { + Err(Error::other(format!("Invalid sync response: 0x{:02X}", buf[0])))? + } + } + + let mut new = Self { port }; + + let manufacturer = new.read_info(0x00, 0x00).unwrap(); + let family = new.read_info(0x00, 0x01).unwrap(); + let name = new.read_info(0x00, 0x02).unwrap(); + let rev = new.read_info(0x00, 0x03).unwrap(); + + println!("Detected chip ID: {manufacturer:02X} {family:02X} {name:02X} {rev:02X} (whatever the fuck that means)"); + + Ok(new) + } + + pub fn read_info(&mut self, category: u8, idx: u8) -> Result { + self.send_command(0x05, 0, &[category, idx])?; + + let mut resp_hex_digits = Vec::new(); + + loop { + let mut tmp = [0u8; 1]; + + self.port.read_exact(&mut tmp)?; + + match tmp[0] { + x @ (b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F') => resp_hex_digits.push(x), + b'.' | b'\r' => {}, + b'\n' => break, + b'P' => Err(Error::other("Security error"))?, + b'X' => Err(Error::other("Checksum error"))?, + x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))? + } + } + + let mut rx = [0u8; 1]; + + let resp = resp_hex_digits.as_chunks::<2>().0.iter() + .map(u8::hex_decode); + + if resp.len() != rx.len() { + Err(Error::other(format!("Response size mismatch, got {} bytes, expected {} bytes", resp.len(), rx.len())))? + } + + rx.iter_mut() + .zip(resp) + .for_each(|(dest, src)| *dest = src); + + Ok(u8::from_le_bytes(rx)) + } + + fn send_command(&mut self, cmd_type: u8, offset: u16, tx: &[u8]) -> Result<(), Error> { + if tx.len() >= 256 { + Err(Error::other(format!("Payload too large: {} bytes", tx.len())))? + } + + // Why, the ACTUAL FUCK, ARE THEY USING ASCII FOR TRANSFERING BINARIES?! + + let mut ascii_buf = Vec::with_capacity(1 + 2 + 4 + 2 + tx.len() * 2 + 1); // Mark, Length, Offset, Type, Data, Checksum + + ascii_buf.push(b':'); + ascii_buf.extend_from_slice(&(tx.len() as u8).hex_encode()); + ascii_buf.extend_from_slice(&offset.hex_encode()); + ascii_buf.extend_from_slice(&cmd_type.hex_encode()); + + let mut checksum: u8 = 0; + + checksum = checksum.wrapping_add(cmd_type); + checksum = checksum.wrapping_add(tx.len() as u8); + checksum = checksum.wrapping_add(offset as u8); + checksum = checksum.wrapping_add((offset >> 8) as u8); + + for val in tx { + ascii_buf.extend_from_slice(&val.hex_encode()); + checksum = checksum.wrapping_add(*val); + } + + ascii_buf.extend_from_slice(&checksum.wrapping_neg().hex_encode()); + + self.port.write_all(&ascii_buf)?; + + let mut echo = vec![0u8; ascii_buf.len()]; + + self.port.read_exact(&mut echo)?; + + if echo != ascii_buf { + Err(Error::other("Echo response mismatch"))? + } + + Ok(()) + } +} + +impl Target for PhysicalTarget { + fn read_flash(&mut self, offset: u16, out: &mut [u8]) -> Result<(), Error> { + self.send_command(0x04, 0, &[ + offset.to_be_bytes().as_slice(), + offset.saturating_add(out.len() as u16 - 1).to_be_bytes().as_slice(), + &[0] // Flash + ].concat())?; + + let mut resp_hex_digits = Vec::with_capacity(out.len() * 2); + + let mut ignore = false; + let mut finish_line = false; + + loop { + let mut tmp = [0u8; 1]; + + self.port.read_exact(&mut tmp)?; + + match tmp[0] { + x @ (b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F') => { + if !ignore { + resp_hex_digits.push(x); + if resp_hex_digits.len() >= resp_hex_digits.capacity() { + ignore = true; + finish_line = true; + } + } + }, + b'=' => { + ignore = false; + } + b'.' | b'\r' => {}, + b'\n' => { + ignore = true; + if finish_line { break } + }, + b'P' => Err(Error::other("Security error"))?, + b'X' => Err(Error::other("Checksum error"))?, + x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))? + } + } + + let resp = resp_hex_digits.as_chunks::<2>().0.iter() + .map(u8::hex_decode); + + out.iter_mut() + .zip(resp) + .for_each(|(dest, src)| *dest = src); + + Ok(()) + } + + fn write_flash(&mut self, page: u16, data: &[u8; 128]) -> Result<(), Error> { + self.send_command(0x00, page * 128, data)?; + + loop { + let mut tmp = [0u8; 1]; + + self.port.read_exact(&mut tmp)?; + + match tmp[0] { + b'.' | b'\r' => {}, + b'\n' => break, + b'P' => Err(Error::other("Security error"))?, + b'X' => Err(Error::other("Checksum error"))?, + x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))? + } + } + + Ok(()) + } + + fn erase(&mut self) -> Result<(), Error> { + self.send_command(0x03, 0, &[0x07])?; + + let old_timeout = self.port.get_read_timeout()?; + self.port.set_read_timeout(Duration::from_secs(15))?; + + loop { + let mut tmp = [0u8; 1]; + + self.port.read_exact(&mut tmp)?; + + match tmp[0] { + b'.' | b'\r' => {}, + b'\n' => break, + b'P' => Err(Error::other("Security error"))?, + b'X' => Err(Error::other("Checksum error"))?, + x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))? + } + } + + self.port.set_read_timeout(old_timeout)?; + + Ok(()) + } +} + +impl Drop for PhysicalTarget { + fn drop(&mut self) { + let _ = self.port.set_dtr(true); + } +} + diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..d78000b --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,128 @@ +use crate::target::Target; + +use crate::write_chip_chunk; + +#[derive(Debug, PartialEq, Eq)] +enum TestTargetOperation { + Read { offset: u16, len: usize }, + Write { page: u16 }, + Erase +} + +struct TestTarget { + ops: Vec +} + +impl TestTarget { + pub fn init() -> Self { + Self { ops: Vec::new() } + } + + pub fn get_ops(&mut self) -> Vec { + std::mem::take(&mut self.ops) + } +} + +impl Target for TestTarget { + fn read_flash(&mut self, offset: u16, out: &mut [u8]) -> Result<(), std::io::Error> { + self.ops.push(TestTargetOperation::Read { offset, len: out.len() }); + Ok(()) + } + + fn write_flash(&mut self, page: u16, _data: &[u8; 128]) -> Result<(), std::io::Error> { + self.ops.push(TestTargetOperation::Write { page }); + Ok(()) + } + + fn erase(&mut self) -> Result<(), std::io::Error> { + self.ops.push(TestTargetOperation::Erase); + Ok(()) + } +} + +#[test] +fn aligned_chunk_write() { + let mut target = TestTarget::init(); + + for page in 0..=511 { + write_chip_chunk(&[0u8; 128], &mut target, page * 128, false).unwrap(); + + assert_eq!(target.get_ops().as_slice(), [ + TestTargetOperation::Write { page: page as u16 } + ]); + } + + assert!(write_chip_chunk(&[0u8; 128], &mut target, 65536, false).is_err()); +} + +#[test] +fn aligned_multi_chunk_write() { + let mut target = TestTarget::init(); + + for page in 0..=508 { + write_chip_chunk(&[0u8; 512], &mut target, page * 128, false).unwrap(); + + assert_eq!(target.get_ops().as_slice(), [ + TestTargetOperation::Write { page: page as u16 }, + TestTargetOperation::Write { page: page as u16 + 1 }, + TestTargetOperation::Write { page: page as u16 + 2 }, + TestTargetOperation::Write { page: page as u16 + 3 } + ]); + } +} + +#[test] +fn unaligned_chunk_write() { + let mut target = TestTarget::init(); + + for page in (0..=510).step_by(10) { + for start in 1..=127 { + write_chip_chunk(&[0u8; 128], &mut target, page * 128 + start, false).unwrap(); + + assert_eq!(target.get_ops().as_slice(), [ + TestTargetOperation::Read { offset: page as u16 * 128, len: start }, + TestTargetOperation::Read { offset: page as u16 * 128 + start as u16 + 128, len: 128 - start }, + TestTargetOperation::Write { page: page as u16 }, + TestTargetOperation::Write { page: page as u16 + 1 } + ]); + } + } +} + +#[test] +fn aligned_missized_chunk_write() { + let mut target = TestTarget::init(); + + for page in (0..=510).step_by(10) { + for len in 1..=127 { + write_chip_chunk(&vec![0u8; len], &mut target, page * 128, false).unwrap(); + + assert_eq!(target.get_ops().as_slice(), [ + TestTargetOperation::Read { offset: page as u16 * 128 + len as u16, len: 128 - len }, + TestTargetOperation::Write { page: page as u16 } + ]); + } + } +} + +#[test] +fn unaligned_multi_chunk_write() { + let mut target = TestTarget::init(); + + for page in (0..=507).step_by(10) { + for start in 1..=127 { + write_chip_chunk(&[0u8; 512], &mut target, page * 128 + start, false).unwrap(); + + assert_eq!(target.get_ops().as_slice(), [ + TestTargetOperation::Read { offset: page as u16 * 128, len: start }, + TestTargetOperation::Read { offset: (page + 3) as u16 * 128 + start as u16 + 128, len: 128 - start }, + TestTargetOperation::Write { page: page as u16 }, + TestTargetOperation::Write { page: page as u16 + 1 }, + TestTargetOperation::Write { page: page as u16 + 2 }, + TestTargetOperation::Write { page: page as u16 + 3 }, + TestTargetOperation::Write { page: page as u16 + 4 } + ]); + } + } +} + diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ebb645a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,56 @@ +fn hex_digit(val: usize) -> u8 { + // Disgusting? Yes. + // Could I be fucked to fix it? No. + b"0123456789ABCDEF"[val & 0xF] +} + +fn from_hex_digit(val: u8) -> usize { + (match val { + val @ b'0'..=b'9' => val - b'0', + val @ b'a'..=b'f' => val - b'a' + 10, + val @ b'A'..=b'F' => val - b'A' + 10, + _ => unreachable!() + }) as usize +} + +pub trait Hex { + fn hex_encode(&self) -> [u8; N]; + fn hex_decode(buf: &[u8; N]) -> Self where Self: Sized; +} + +impl Hex<2> for u8 { + fn hex_encode(&self) -> [u8; 2] { + [ + hex_digit(*self as usize >> 4), + hex_digit(*self as usize) + ] + } + + fn hex_decode(buf: &[u8; 2]) -> Self { + ( + from_hex_digit(buf[0]) << 4 | + from_hex_digit(buf[1]) + ) as u8 + } +} + +impl Hex<4> for u16 { + fn hex_encode(&self) -> [u8; 4] { + [ + hex_digit(*self as usize >> 12), + hex_digit(*self as usize >> 8), + hex_digit(*self as usize >> 4), + hex_digit(*self as usize) + ] + } + + fn hex_decode(buf: &[u8; 4]) -> Self { + ( + from_hex_digit(buf[0]) << 12 | + from_hex_digit(buf[1]) << 8 | + from_hex_digit(buf[2]) << 4 | + from_hex_digit(buf[3]) + ) as u16 + } +} +