diff --git a/src/ch585.rs b/src/ch585.rs new file mode 100644 index 0000000..b0cc22f --- /dev/null +++ b/src/ch585.rs @@ -0,0 +1,227 @@ +//! CH585-specific ISP policy. +//! +//! Keep the CH585 BootROM workarounds here so the established flashing paths +//! for older WCH devices remain unchanged. + +use anyhow::{ensure, Result}; + +pub(crate) const CONFIG_MASK: u8 = 0x07; +pub(crate) const CONFIG_BYTES: usize = 12; +pub(crate) const USER_CFG_OFFSET: usize = 8; +pub(crate) const CFG_DEBUG_EN: u32 = 1 << 4; +pub(crate) const CFG_ROM_READ: u32 = 1 << 7; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FlashConfigTransition { + pub(crate) original: [u8; CONFIG_BYTES], + pub(crate) programmed: [u8; CONFIG_BYTES], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FlashSession { + pub(crate) config: FlashConfigTransition, + pub(crate) key: [u8; 8], +} + +pub(crate) struct IspKey { + pub(crate) payload: Vec, + pub(crate) xor: [u8; 8], +} + +impl FlashConfigTransition { + pub(crate) fn requires_restore(self) -> bool { + self.original != self.programmed + } +} + +pub(crate) fn parse_config_payload(payload: &[u8]) -> Result<[u8; CONFIG_BYTES]> { + ensure!( + payload.len() == CONFIG_BYTES + 2, + "CH585 read_config returned {} bytes, expected {}", + payload.len(), + CONFIG_BYTES + 2 + ); + ensure!( + payload[..2] == [CONFIG_MASK, 0], + "CH585 read_config mask echo mismatch: {}", + hex::encode(&payload[..2]) + ); + Ok(payload[2..] + .try_into() + .expect("CH585 configuration length was checked")) +} + +pub(crate) fn prepare_flash_config(original: [u8; CONFIG_BYTES]) -> Result { + let user_cfg = user_cfg(&original); + ensure!( + user_cfg >> 28 == 0x4, + "refusing invalid CH585 USER_CFG signature 0x{user_cfg:08x}" + ); + + let mut programmed = original; + programmed[USER_CFG_OFFSET..USER_CFG_OFFSET + 4] + .copy_from_slice(&(user_cfg & !(CFG_DEBUG_EN | CFG_ROM_READ)).to_le_bytes()); + Ok(FlashConfigTransition { + original, + programmed, + }) +} + +pub(crate) fn set_debug(original: [u8; CONFIG_BYTES], enabled: bool) -> Result<[u8; CONFIG_BYTES]> { + let user_cfg = user_cfg(&original); + ensure!( + user_cfg >> 28 == 0x4, + "refusing invalid CH585 USER_CFG signature 0x{user_cfg:08x}" + ); + let requested = if enabled { + user_cfg | CFG_DEBUG_EN + } else { + user_cfg & !CFG_DEBUG_EN + }; + let mut result = original; + result[USER_CFG_OFFSET..USER_CFG_OFFSET + 4].copy_from_slice(&requested.to_le_bytes()); + Ok(result) +} + +pub(crate) fn check_bootrom_status(payload: &[u8], operation: &str) -> Result<()> { + ensure!( + payload.len() == 2, + "{operation} returned unexpected payload: {}", + hex::encode(payload) + ); + let status = u16::from_le_bytes([payload[0], payload[1]]); + ensure!( + status == 0, + "{operation} rejected by BootROM with status 0x{status:04x}" + ); + Ok(()) +} + +pub fn pad_firmware(raw: &mut Vec) { + let remainder = raw.len() % 8; + if remainder != 0 { + raw.resize(raw.len() + 8 - remainder, 0xff); + } +} + +pub(crate) fn generate_isp_key(uid: &[u8], chip_id: u8) -> IspKey { + let length = 0x1e + usize::from(rand::random::() % 0x1f); + let payload: Vec = (0..length).map(|_| rand::random()).collect(); + let xor = derive_isp_xor_key(uid, chip_id, &payload); + IspKey { payload, xor } +} + +fn derive_isp_xor_key(uid: &[u8], chip_id: u8, payload: &[u8]) -> [u8; 8] { + debug_assert!((0x1e..=0x3c).contains(&payload.len())); + let uid_sum = uid + .iter() + .take(8) + .fold(0_u8, |sum, byte| sum.wrapping_add(*byte)); + let fifth = payload.len() / 5; + let mixed = payload.len() / 7 + (payload.len() - payload.len() / 7) / 2; + let quarter = mixed / 4; + let first = payload[quarter * 4] ^ uid_sum; + + [ + first, + payload[fifth] ^ uid_sum, + payload[quarter] ^ uid_sum, + payload[quarter * 6] ^ uid_sum, + payload[quarter * 3] ^ uid_sum, + payload[fifth * 3] ^ uid_sum, + payload[quarter * 5] ^ uid_sum, + first.wrapping_add(chip_id), + ] +} + +fn user_cfg(config: &[u8; CONFIG_BYTES]) -> u32 { + u32::from_le_bytes( + config[USER_CFG_OFFSET..USER_CFG_OFFSET + 4] + .try_into() + .expect("CH585 USER_CFG has a fixed width"), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + const DEBUG_ENABLED: [u8; CONFIG_BYTES] = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x3f, 0x0f, 0x45, + ]; + + #[test] + fn preparation_clears_only_isp_incompatible_bits() { + let transition = prepare_flash_config(DEBUG_ENABLED).unwrap(); + assert_eq!( + transition.programmed, + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4f, 0x3f, 0x0f, 0x45,] + ); + assert_eq!(transition.original, DEBUG_ENABLED); + assert!(transition.requires_restore()); + } + + #[test] + fn preparation_preserves_debug_disabled_config() { + let mut original = DEBUG_ENABLED; + original[USER_CFG_OFFSET] &= !(CFG_DEBUG_EN | CFG_ROM_READ) as u8; + let transition = prepare_flash_config(original).unwrap(); + assert_eq!(transition.programmed, original); + assert!(!transition.requires_restore()); + } + + #[test] + fn preparation_rejects_invalid_signature() { + let mut original = DEBUG_ENABLED; + original[USER_CFG_OFFSET + 3] = 0x55; + let error = prepare_flash_config(original).unwrap_err(); + assert!(error + .to_string() + .contains("invalid CH585 USER_CFG signature")); + } + + #[test] + fn debug_update_preserves_other_bits() { + let disabled = set_debug(DEBUG_ENABLED, false).unwrap(); + assert_eq!(disabled[USER_CFG_OFFSET], 0xcf); + assert_eq!(set_debug(disabled, true).unwrap(), DEBUG_ENABLED); + } + + #[test] + fn firmware_padding_is_ff_and_eight_byte_aligned() { + let mut raw = vec![1, 2, 3]; + pad_firmware(&mut raw); + assert_eq!(raw, [1, 2, 3, 0xff, 0xff, 0xff, 0xff, 0xff]); + } + + #[test] + fn config_parser_checks_echo_and_length() { + let mut response = vec![CONFIG_MASK, 0]; + response.extend_from_slice(&DEBUG_ENABLED); + assert_eq!(parse_config_payload(&response).unwrap(), DEBUG_ENABLED); + + response[0] = 0x1f; + assert!(parse_config_payload(&response).is_err()); + assert!(parse_config_payload(&response[..13]).is_err()); + } + + #[test] + fn bootrom_status_checks_both_bytes() { + check_bootrom_status(&[0, 0], "program").unwrap(); + let low = check_bootrom_status(&[0xfe, 0], "program").unwrap_err(); + assert!(low.to_string().contains("status 0x00fe")); + let high = check_bootrom_status(&[0, 1], "program").unwrap_err(); + assert!(high.to_string().contains("status 0x0100")); + assert!(check_bootrom_status(&[0], "program").is_err()); + } + + #[test] + fn isp_key_derivation_uses_generated_payload_and_uid() { + let uid = [0x98, 0x5b, 0x29, 0x5a, 0x04, 0xdc, 0xc5, 0x91]; + let payload: Vec = (0..0x1e).collect(); + assert_eq!( + derive_isp_xor_key(&uid, 0x85, &payload), + [0xbc, 0xaa, 0xa8, 0xb4, 0xa0, 0xbe, 0xb8, 0x41] + ); + } +} diff --git a/src/flashing.rs b/src/flashing.rs index 7ffb775..4be7f43 100644 --- a/src/flashing.rs +++ b/src/flashing.rs @@ -6,10 +6,11 @@ use indicatif::ProgressBar; use scroll::{Pread, Pwrite, LE}; use crate::{ + ch585::{self, FlashSession}, constants::{CFG_MASK_ALL, CFG_MASK_RDPR_USER_DATA_WPR}, device::{parse_number, ChipDB}, transport::{SerialTransport, UsbTransport}, - Baudrate, Chip, Command, Transport, + Baudrate, Chip, Command, Response, Transport, }; pub struct Flashing<'a> { @@ -197,7 +198,11 @@ impl<'a> Flashing<'a> { pub fn reset(&mut self) -> Result<()> { let isp_end = Command::isp_end(1); let resp = self.transport.transfer(isp_end)?; - anyhow::ensure!(resp.is_ok(), "isp_end failed"); + if self.is_ch585() { + self.ensure_operation_ok(resp, "isp_end")?; + } else { + anyhow::ensure!(resp.is_ok(), "isp_end failed"); + } log::info!("Device reset"); Ok(()) @@ -206,14 +211,11 @@ impl<'a> Flashing<'a> { // unprotect -> erase -> flash -> verify -> reset /// Program the code flash. pub fn flash(&mut self, raw: &[u8]) -> Result<()> { - let key = self.xor_key(); - let key_checksum = key.iter().fold(0_u8, |acc, &x| acc.overflowing_add(x).0); + let key = self.begin_encrypted_session()?; + self.flash_with_key(raw, key) + } - // NOTE: use all-zero key seed for now. - let isp_key = Command::isp_key(vec![0; 0x1e]); - let resp = self.transport.transfer(isp_key)?; - anyhow::ensure!(resp.is_ok(), "isp_key failed"); - anyhow::ensure!(resp.payload()[0] == key_checksum, "isp_key checksum failed"); + fn flash_with_key(&mut self, raw: &[u8], key: [u8; 8]) -> Result<()> { const CHUNK: usize = 56; let mut address = 0x0; @@ -260,13 +262,11 @@ impl<'a> Flashing<'a> { } pub fn verify(&mut self, raw: &[u8]) -> Result<()> { - let key = self.xor_key(); - let key_checksum = key.iter().fold(0_u8, |acc, &x| acc.overflowing_add(x).0); - // NOTE: use all-zero key seed for now. - let isp_key = Command::isp_key(vec![0; 0x1e]); - let resp = self.transport.transfer(isp_key)?; - anyhow::ensure!(resp.is_ok(), "isp_key failed"); - anyhow::ensure!(resp.payload()[0] == key_checksum, "isp_key checksum failed"); + let key = self.begin_encrypted_session()?; + self.verify_with_key(raw, key) + } + + fn verify_with_key(&mut self, raw: &[u8], key: [u8; 8]) -> Result<()> { const CHUNK: usize = 56; let mut address = 0x0; @@ -310,6 +310,10 @@ impl<'a> Flashing<'a> { } pub fn enable_debug(&mut self) -> Result<()> { + if self.is_ch585() { + return self.set_ch585_debug(true); + } + let read_conf = Command::read_config(CFG_MASK_RDPR_USER_DATA_WPR); let resp = self.transport.transfer(read_conf)?; anyhow::ensure!(resp.is_ok(), "read_config failed"); @@ -344,6 +348,10 @@ impl<'a> Flashing<'a> { } pub fn disable_debug(&mut self) -> Result<()> { + if self.is_ch585() { + return self.set_ch585_debug(false); + } + let read_conf = Command::read_config(CFG_MASK_RDPR_USER_DATA_WPR); let resp = self.transport.transfer(read_conf)?; anyhow::ensure!(resp.is_ok(), "read_config failed"); @@ -428,6 +436,9 @@ impl<'a> Flashing<'a> { .transport .transfer_with_wait(cmd, Duration::from_millis(300))?; anyhow::ensure!(resp.is_ok(), "program 0x{:08x} failed", address); + if self.is_ch585() { + self.ensure_operation_ok(resp, &format!("program 0x{address:08x}"))?; + } Ok(()) } @@ -448,8 +459,12 @@ impl<'a> Flashing<'a> { let padding = rand::random(); let cmd = Command::verify(address, padding, xored.collect()); let resp = self.transport.transfer(cmd)?; - anyhow::ensure!(resp.is_ok(), "verify response failed"); - anyhow::ensure!(resp.payload()[0] == 0x00, "Verify failed, mismatch"); + if self.is_ch585() { + self.ensure_operation_ok(resp, &format!("verify 0x{address:08x}"))?; + } else { + anyhow::ensure!(resp.is_ok(), "verify response failed"); + anyhow::ensure!(resp.payload()[0] == 0x00, "Verify failed, mismatch"); + } Ok(()) } @@ -466,7 +481,11 @@ impl<'a> Flashing<'a> { let resp = self .transport .transfer_with_wait(erase, Duration::from_millis(5000))?; - anyhow::ensure!(resp.is_ok(), "erase failed"); + if self.is_ch585() { + self.ensure_operation_ok(resp, "erase")?; + } else { + anyhow::ensure!(resp.is_ok(), "erase failed"); + } log::info!("Erased {} code flash sectors", sectors); Ok(()) @@ -531,6 +550,151 @@ impl<'a> Flashing<'a> { Ok(()) } + /// Apply the CH585 BootROM programming configuration without closing the + /// current ISP transport session. + /// + /// The CH585 BootROM rejects Program commands while CFG_DEBUG_EN or + /// CFG_ROM_READ is set. This clears only those bits, writes the complete + /// 12-byte config, and verifies the same-session readback. Retain the + /// returned transition and restore it only after a full successful + /// code-flash verify. + pub fn prepare_ch585_isp_flash(&mut self) -> Result { + anyhow::ensure!( + self.is_ch585(), + "CH585 ISP preparation requires a CH585 target" + ); + let transition = ch585::prepare_flash_config(self.read_ch585_config()?)?; + self.write_and_verify_ch585_config(transition.programmed, "prepare CH585 ISP flash")?; + // Establish the key before erase so preparation and programming remain + // in one validated ISP session. flash() establishes it again before + // Program, preserving the existing API behavior for direct callers. + let key = self.begin_encrypted_session()?; + Ok(FlashSession { + config: transition, + key, + }) + } + + /// Program CH585 using the key established before erase by + /// `prepare_ch585_isp_flash`; do not issue another ISP_KEY in between. + pub fn flash_prepared_ch585(&mut self, raw: &[u8], session: &FlashSession) -> Result<()> { + anyhow::ensure!( + self.is_ch585(), + "prepared CH585 flash requires a CH585 target" + ); + self.flash_with_key(raw, session.key) + } + + /// Verify CH585 using an already established standalone verify session. + pub fn verify_prepared_ch585(&mut self, raw: &[u8], session: &FlashSession) -> Result<()> { + anyhow::ensure!( + self.is_ch585(), + "prepared CH585 verify requires a CH585 target" + ); + self.verify_with_key(raw, session.key) + } + + /// Restore the exact CH585 configuration captured before programming. + /// Call this only after code-flash verification succeeds. + pub fn restore_ch585_flash_config( + &mut self, + session: FlashSession, + ) -> Result<()> { + anyhow::ensure!( + self.is_ch585(), + "CH585 config restoration requires a CH585 target" + ); + anyhow::ensure!( + self.read_ch585_config()? == session.config.programmed, + "CH585 config changed before post-verify restoration" + ); + if session.config.requires_restore() { + self.write_and_verify_ch585_config( + session.config.original, + "restore CH585 post-verify config", + )?; + } + Ok(()) + } + + fn is_ch585(&self) -> bool { + self.chip.name == "CH585" && self.chip.chip_id == 0x85 && self.chip.device_type == 0x16 + } + + fn read_ch585_config(&mut self) -> Result<[u8; ch585::CONFIG_BYTES]> { + let response = self + .transport + .transfer(Command::read_config(ch585::CONFIG_MASK))?; + anyhow::ensure!(response.is_ok(), "CH585 read_config failed: {response:?}"); + ch585::parse_config_payload(response.payload()) + } + + fn write_and_verify_ch585_config( + &mut self, + config: [u8; ch585::CONFIG_BYTES], + operation: &str, + ) -> Result<()> { + let response = self.transport.transfer(Command::write_config( + ch585::CONFIG_MASK, + config.to_vec(), + ))?; + self.ensure_operation_ok(response, operation)?; + // The CH585 BootROM needs the configuration write to settle before + // the readback and encrypted flash session are established. + std::thread::sleep(Duration::from_millis(20)); + anyhow::ensure!( + self.read_ch585_config()? == config, + "{operation} readback mismatch" + ); + Ok(()) + } + + fn set_ch585_debug(&mut self, enabled: bool) -> Result<()> { + let requested = ch585::set_debug(self.read_ch585_config()?, enabled)?; + self.write_and_verify_ch585_config( + requested, + if enabled { + "enable CH585 debug" + } else { + "disable CH585 debug" + }, + ) + } + + fn ensure_operation_ok(&self, response: Response, operation: &str) -> Result<()> { + anyhow::ensure!(response.is_ok(), "{operation} failed: {response:?}"); + ch585::check_bootrom_status(response.payload(), operation) + } + + fn begin_encrypted_session(&mut self) -> Result<[u8; 8]> { + let (payload, key) = if self.is_ch585() { + let generated = ch585::generate_isp_key(self.chip_uid(), self.chip.chip_id); + (generated.payload, generated.xor) + } else { + (vec![0; 0x1e], self.xor_key()) + }; + let key_checksum = key.iter().fold(0_u8, |acc, &x| acc.overflowing_add(x).0); + let response = self.transport.transfer(Command::isp_key(payload))?; + anyhow::ensure!(response.is_ok(), "isp_key failed: {response:?}"); + if self.is_ch585() { + // CH58x BootROM revisions may return either zero or the derived + // key checksum in byte zero; byte one is the operation status. + anyhow::ensure!( + response.payload() == [key_checksum, 0] || response.payload() == [0, 0], + "CH585 isp_key checksum mismatch: expected {:02x}00, got {}", + key_checksum, + hex::encode(response.payload()) + ); + } else { + anyhow::ensure!(!response.payload().is_empty(), "isp_key returned no checksum"); + anyhow::ensure!( + response.payload()[0] == key_checksum, + "isp_key checksum failed" + ); + } + Ok(key) + } + // NOTE: XOR key for all-zero key seed fn xor_key(&self) -> [u8; 8] { let checksum = self @@ -566,3 +730,206 @@ impl<'a> Flashing<'a> { Ok(()) } } + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, collections::VecDeque, rc::Rc, time::Duration}; + + use super::*; + use crate::constants::commands; + + const CH585_UID: [u8; 8] = [0x98, 0x5b, 0x29, 0x5a, 0x04, 0xdc, 0xc5, 0x91]; + const DEBUG_ENABLED: [u8; ch585::CONFIG_BYTES] = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x3f, 0x0f, 0x45, + ]; + const DEBUG_DISABLED: [u8; ch585::CONFIG_BYTES] = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4f, 0x3f, 0x0f, 0x45, + ]; + const DEBUG_ENABLED_ROM_READ_DISABLED: [u8; ch585::CONFIG_BYTES] = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x5f, 0x3f, 0x0f, 0x45, + ]; + + struct MockTransport { + requests: Rc>>>, + responses: VecDeque>, + } + + impl Transport for MockTransport { + fn send_raw(&mut self, raw: &[u8]) -> Result<()> { + self.requests.borrow_mut().push(raw.to_vec()); + Ok(()) + } + + fn recv_raw(&mut self, _timeout: Duration) -> Result> { + self.responses + .pop_front() + .ok_or_else(|| anyhow::format_err!("mock response queue is empty")) + } + } + + fn response(command: u8, payload: &[u8]) -> Vec { + let mut raw = vec![command, 0, payload.len() as u8, 0]; + raw.extend_from_slice(payload); + raw + } + + fn config_response(config: [u8; ch585::CONFIG_BYTES]) -> Vec { + let mut payload = vec![ch585::CONFIG_MASK, 0]; + payload.extend_from_slice(&config); + response(commands::READ_CONFIG, &payload) + } + + fn test_flashing( + chip_id: u8, + uid: Vec, + responses: Vec>, + ) -> (Flashing<'static>, Rc>>>) { + let requests = Rc::new(RefCell::new(Vec::new())); + let transport = MockTransport { + requests: requests.clone(), + responses: responses.into(), + }; + let chip = ChipDB::load().unwrap().find_chip(chip_id, 0x16).unwrap(); + ( + Flashing { + transport: Box::new(transport), + chip, + chip_uid: uid, + bootloader_version: [0, 2, 3, 0], + code_flash_protected: false, + }, + requests, + ) + } + + #[test] + fn ch585_prepare_restore_and_reset_use_one_exact_session() { + let responses = vec![ + config_response(DEBUG_ENABLED), + response(commands::WRITE_CONFIG, &[0, 0]), + config_response(DEBUG_DISABLED), + response(commands::ISP_KEY, &[0, 0]), + config_response(DEBUG_DISABLED), + response(commands::WRITE_CONFIG, &[0, 0]), + config_response(DEBUG_ENABLED), + response(commands::ISP_END, &[0, 0]), + ]; + let (mut flashing, requests) = test_flashing(0x85, CH585_UID.to_vec(), responses); + + let session = flashing.prepare_ch585_isp_flash().unwrap(); + assert_eq!(session.config.original, DEBUG_ENABLED); + assert_eq!(session.config.programmed, DEBUG_DISABLED); + flashing.restore_ch585_flash_config(session).unwrap(); + flashing.reset().unwrap(); + + let requests = requests.borrow(); + assert_eq!(requests.len(), 8); + assert_eq!(requests[0], Command::read_config(ch585::CONFIG_MASK).into_raw().unwrap()); + assert_eq!(requests[3][0], commands::ISP_KEY); + assert!((0x1e..=0x3c).contains(&usize::from(requests[3][1]))); + assert!(requests[3][3..].iter().any(|byte| *byte != 0)); + assert_eq!(requests[7], Command::isp_end(1).into_raw().unwrap()); + } + + #[test] + fn ch582_reset_keeps_accepting_the_existing_empty_payload() { + let responses = vec![response(commands::ISP_END, &[])]; + let (mut flashing, requests) = test_flashing(0x82, vec![0; 8], responses); + + flashing.reset().unwrap(); + + assert_eq!( + *requests.borrow(), + vec![Command::isp_end(1).into_raw().unwrap()] + ); + } + + #[test] + fn ch585_enable_debug_changes_only_the_debug_bit_and_reads_back() { + let responses = vec![ + config_response(DEBUG_DISABLED), + response(commands::WRITE_CONFIG, &[0, 0]), + config_response(DEBUG_ENABLED_ROM_READ_DISABLED), + ]; + let (mut flashing, requests) = test_flashing(0x85, CH585_UID.to_vec(), responses); + + flashing.enable_debug().unwrap(); + + assert_eq!( + *requests.borrow(), + vec![ + Command::read_config(ch585::CONFIG_MASK).into_raw().unwrap(), + Command::write_config( + ch585::CONFIG_MASK, + DEBUG_ENABLED_ROM_READ_DISABLED.to_vec(), + ) + .into_raw() + .unwrap(), + Command::read_config(ch585::CONFIG_MASK).into_raw().unwrap(), + ] + ); + } + + #[test] + fn ch585_program_and_verify_keep_the_standard_packet_shape() { + let responses = vec![ + response(commands::PROGRAM, &[0, 0]), + response(commands::VERIFY, &[0, 0]), + ]; + let (mut ch585, requests) = test_flashing(0x85, CH585_UID.to_vec(), responses); + + ch585.flash_chunk(0x20, &[0x11, 0x22], [0; 8]).unwrap(); + ch585.verify_chunk(0x20, &[0x11, 0x22], [0; 8]).unwrap(); + + let requests = requests.borrow(); + assert_eq!(requests.len(), 2); + assert_eq!(requests[0][0], commands::PROGRAM); + assert_eq!(&requests[0][3..7], &0x20_u32.to_le_bytes()); + assert_eq!(&requests[0][8..], &[0x11, 0x22]); + assert_eq!(requests[1][0], commands::VERIFY); + assert_eq!(&requests[1][3..7], &0x20_u32.to_le_bytes()); + assert_eq!(&requests[1][8..], &[0x11, 0x22]); + } + + #[test] + fn ch585_prepared_program_does_not_send_a_second_key_after_erase() { + let responses = vec![ + config_response(DEBUG_DISABLED), + response(commands::WRITE_CONFIG, &[0, 0]), + config_response(DEBUG_DISABLED), + response(commands::ISP_KEY, &[0, 0]), + response(commands::ERASE, &[0, 0]), + response(commands::PROGRAM, &[0, 0]), + response(commands::PROGRAM, &[0, 0]), + ]; + let (mut flashing, requests) = test_flashing(0x85, CH585_UID.to_vec(), responses); + + let session = flashing.prepare_ch585_isp_flash().unwrap(); + flashing.erase_code(8).unwrap(); + flashing + .flash_prepared_ch585(&[0x11; 8], &session) + .unwrap(); + + let requests = requests.borrow(); + assert_eq!( + requests + .iter() + .filter(|request| request[0] == commands::ISP_KEY) + .count(), + 1 + ); + let sequence: Vec = requests.iter().map(|request| request[0]).collect(); + assert_eq!( + sequence, + [ + commands::READ_CONFIG, + commands::WRITE_CONFIG, + commands::READ_CONFIG, + commands::ISP_KEY, + commands::ERASE, + commands::PROGRAM, + commands::PROGRAM, + ] + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 98a1f8f..5d1539d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ //! WCH ISP Protocol implementation. +pub mod ch585; pub mod constants; pub mod device; pub mod flashing; diff --git a/src/main.rs b/src/main.rs index 43452d2..60bfdb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -233,16 +233,46 @@ fn main() -> Result<()> { let mut flashing = get_flashing(&cli)?; flashing.dump_info()?; + let is_ch585 = flashing.chip.name == "CH585"; + if is_ch585 && *no_verify { + anyhow::bail!( + "CH585 flashing requires full verification before its debug configuration can be restored" + ); + } let mut binary = wchisp::format::read_firmware_from_file(path)?; - extend_firmware_to_sector_boundary(&mut binary); + if is_ch585 { + wchisp::ch585::pad_firmware(&mut binary); + } else { + extend_firmware_to_sector_boundary(&mut binary); + } + anyhow::ensure!(!binary.is_empty(), "firmware image is empty"); + anyhow::ensure!( + binary.len() <= flashing.chip.flash_size as usize, + "firmware image is {} bytes but {} code flash is {} bytes", + binary.len(), + flashing.chip.name, + flashing.chip.flash_size + ); log::info!("Firmware size: {}", binary.len()); + // The CH585 requires WRITE_CONFIG and all subsequent flash + // operations to remain in this exact transport session. + let ch585_session = if is_ch585 { + Some(flashing.prepare_ch585_isp_flash()?) + } else { + None + }; + if *no_erase { log::warn!("Skipping erase"); } else { log::info!("Erasing..."); - let sectors = binary.len() / SECTOR_SIZE + 1; + let sectors = if is_ch585 { + binary.len().div_ceil(SECTOR_SIZE) + } else { + binary.len() / SECTOR_SIZE + 1 + }; flashing.erase_code(sectors as u32)?; sleep(Duration::from_secs(1)); @@ -250,7 +280,11 @@ fn main() -> Result<()> { } log::info!("Writing to code flash..."); - flashing.flash(&binary)?; + if let Some(ref session) = ch585_session { + flashing.flash_prepared_ch585(&binary, session)?; + } else { + flashing.flash(&binary)?; + } sleep(Duration::from_millis(500)); if *no_verify { @@ -261,22 +295,58 @@ fn main() -> Result<()> { log::info!("Verify OK"); } + if let Some(session) = ch585_session { + flashing.restore_ch585_flash_config(session)?; + log::info!("CH585 pre-flash configuration restored after verify"); + } + if *no_reset { log::warn!("Skipping reset"); } else { - log::info!("Now reset device and skip any communication errors"); - let _ = flashing.reset(); + if is_ch585 { + log::info!("Resetting CH585 after verified configuration restore"); + flashing.reset()?; + } else { + log::info!("Now reset device and skip any communication errors"); + let _ = flashing.reset(); + } } } Some(Commands::Verify { path }) => { let mut flashing = get_flashing(&cli)?; let mut binary = wchisp::format::read_firmware_from_file(path)?; - extend_firmware_to_sector_boundary(&mut binary); + let is_ch585 = flashing.chip.name == "CH585"; + if is_ch585 { + wchisp::ch585::pad_firmware(&mut binary); + } else { + extend_firmware_to_sector_boundary(&mut binary); + } + anyhow::ensure!(!binary.is_empty(), "firmware image is empty"); + anyhow::ensure!( + binary.len() <= flashing.chip.flash_size as usize, + "firmware image is {} bytes but {} code flash is {} bytes", + binary.len(), + flashing.chip.name, + flashing.chip.flash_size + ); log::info!("Firmware size: {}", binary.len()); + let ch585_session = if is_ch585 { + Some(flashing.prepare_ch585_isp_flash()?) + } else { + None + }; log::info!("Verifying..."); - flashing.verify(&binary)?; + if let Some(ref session) = ch585_session { + flashing.verify_prepared_ch585(&binary, session)?; + } else { + flashing.verify(&binary)?; + } log::info!("Verify OK"); + if let Some(session) = ch585_session { + flashing.restore_ch585_flash_config(session)?; + log::info!("CH585 pre-verify configuration restored"); + } } Some(Commands::Eeprom { command }) => { let mut flashing = get_flashing(&cli)?; diff --git a/src/transport/usb.rs b/src/transport/usb.rs index 8aaca10..40af64e 100644 --- a/src/transport/usb.rs +++ b/src/transport/usb.rs @@ -128,7 +128,12 @@ impl UsbTransport { anyhow::bail!("USB Endpoints not found"); } - device_handle.set_active_configuration(1)?; + // Re-applying the already active configuration can reset or stall a + // freshly enumerated ISP endpoint on macOS. Configure only when the + // device is not already using configuration 1. + if device_handle.active_configuration()? != 1 { + device_handle.set_active_configuration(1)?; + } let _config = device.active_config_descriptor()?; let _descriptor = device.device_descriptor()?;