Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
184
Cargo.lock
generated
Normal file
184
Cargo.lock
generated
Normal file
@@ -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"
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@@ -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"
|
87
src/args.rs
Normal file
87
src/args.rs
Normal file
@@ -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
|
||||
}
|
||||
|
139
src/ihex.rs
Normal file
139
src/ihex.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use std::io::{BufRead, BufReader, BufWriter, Error, Lines, Read, Write};
|
||||
|
||||
use crate::utils::Hex;
|
||||
|
||||
pub struct IntelHexWriter<T: Write> {
|
||||
writer: BufWriter<T>,
|
||||
data: Vec<u8>,
|
||||
addr: u16
|
||||
}
|
||||
|
||||
impl<T: Write> IntelHexWriter<T> {
|
||||
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<T: Write> Write for IntelHexWriter<T> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
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<u8>
|
||||
}
|
||||
|
||||
pub struct IntelHexReader<T: Read> {
|
||||
reader: Lines<BufReader<T>>
|
||||
}
|
||||
|
||||
impl<T: Read> IntelHexReader<T> {
|
||||
pub fn init(reader: T) -> Self {
|
||||
Self { reader: BufReader::new(reader).lines() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read> Iterator for IntelHexReader<T> {
|
||||
type Item = Result<DataRecord, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
225
src/main.rs
Normal file
225
src/main.rs
Normal file
@@ -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<Vec<u8>> = 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());
|
||||
}
|
||||
|
234
src/target.rs
Normal file
234
src/target.rs
Normal file
@@ -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<Path>, baud: u32) -> Result<Self, Error> {
|
||||
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<u8, Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
128
src/tests.rs
Normal file
128
src/tests.rs
Normal file
@@ -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<TestTargetOperation>
|
||||
}
|
||||
|
||||
impl TestTarget {
|
||||
pub fn init() -> Self {
|
||||
Self { ops: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn get_ops(&mut self) -> Vec<TestTargetOperation> {
|
||||
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 }
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
56
src/utils.rs
Normal file
56
src/utils.rs
Normal file
@@ -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<const N: usize> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user