mboot/mboot.rs
1// Copyright 2025-2026 NXP
2//
3// SPDX-License-Identifier: BSD-3-Clause
4
5use color_print::cstr;
6use indicatif::{ProgressBar, ProgressStyle};
7use log::{info, trace};
8use packets::{
9 Packet, PacketParse,
10 command::{CmdResponse, CommandHeader, CommandPacket},
11 data_phase::DataPhasePacket,
12};
13use protocols::Protocol;
14use tags::{
15 ToAddress,
16 command::{CommandTag, CommandToParams, KeyProvOperation, TrustProvOperation},
17 command_flag::CommandFlag,
18 command_response::CmdResponseTag,
19 property::{PropertyTag, PropertyTagDiscriminants},
20 status::StatusCode,
21};
22
23use crate::CommunicationError;
24
25mod formatters;
26pub mod memory;
27pub mod packets;
28pub mod protocols;
29pub mod tags;
30
31/// Response structure for [`CommandTag::GetProperty`] command
32///
33/// Contains the status code, raw response words, and parsed property value.
34#[derive(Clone, Debug)]
35pub struct GetPropertyResponse {
36 /// Status code of the operation
37 pub status: StatusCode,
38 /// Raw response words from the device
39 pub response_words: Box<[u32]>,
40 /// Parsed property value
41 pub property: PropertyTag,
42}
43
44/// Response structure for [`CommandTag::ReadMemory`] command
45///
46/// Contains the status code, response metadata, and actual data bytes read.
47#[derive(Clone, Debug)]
48pub struct ReadMemoryResponse {
49 /// Status code of the operation
50 pub status: StatusCode,
51 /// Response metadata (typically contains the byte count)
52 pub response_words: Box<[u32]>,
53 /// Actual data bytes read from memory
54 pub bytes: Box<[u8]>,
55}
56
57/// Response types for [`CommandTag::KeyProvisioning`] operations
58#[derive(Clone, Debug)]
59pub enum KeyProvisioningResponse {
60 /// Simple status response for most key provisioning operations
61 Status(StatusCode),
62 /// Extended response for [`KeyProvOperation::ReadKeyStore`] operation containing key data
63 KeyStore {
64 /// Status code of the operation
65 status: StatusCode,
66 /// Response metadata
67 response_words: Box<[u32]>,
68 /// Key store data bytes
69 bytes: Box<[u8]>,
70 },
71}
72
73trait InvalidData<T> {
74 /// Convert a type to [`Result`] of [`CommunicationError`].
75 fn or_invalid(self) -> Result<T, CommunicationError>;
76}
77
78impl<T, E> InvalidData<T> for Result<T, E> {
79 /// Transforms `E` into [`CommunicationError::InvalidData`]
80 fn or_invalid(self) -> Result<T, CommunicationError> {
81 self.or(Err(CommunicationError::InvalidData))
82 }
83}
84
85/// Main MCU Boot communication structure
86///
87/// Provides high-level interface for bootloader communication over various protocols.
88///
89/// # Type Parameters
90///
91/// * `T` - The underlying communication protocol (UART, USB, etc.)
92pub struct McuBoot<T>
93where
94 T: Protocol,
95{
96 device: T,
97 /// Enable/disable progress bar for data transfers
98 pub progress_bar: bool,
99 pub mask_read_data_phase: bool,
100}
101
102/// Result type for communication operations returning a value
103pub type ResultComm<T> = Result<T, CommunicationError>;
104/// Result type for operations returning only a status code
105pub type ResultStatus = ResultComm<StatusCode>;
106
107impl<T> McuBoot<T>
108where
109 T: Protocol,
110{
111 /// Creates a new [`McuBoot`] instance with the specified protocol
112 ///
113 /// # Arguments
114 ///
115 /// * `device` - The communication protocol instance
116 ///
117 /// # Returns
118 ///
119 /// A new [`McuBoot`] instance
120 #[must_use]
121 pub fn new(device: T) -> Self {
122 info!(
123 "Initialized MCU Boot with device identifier: {}",
124 device.get_identifier()
125 );
126 McuBoot {
127 device,
128 progress_bar: false,
129 mask_read_data_phase: false,
130 }
131 }
132
133 /// Get a specific property value from the device
134 ///
135 /// # Arguments
136 ///
137 /// * `tag` - The property tag to query
138 /// * `memory_index` - External memory ID or internal memory region index
139 ///
140 /// # Returns
141 ///
142 /// Property response containing the value and status
143 ///
144 /// # Errors
145 ///
146 /// Returns [`CommunicationError`] if:
147 /// - Communication with device fails
148 /// - Invalid response is received
149 /// - Property is not supported
150 pub fn get_property(
151 &mut self,
152 tag: PropertyTagDiscriminants,
153 memory_index: u32,
154 ) -> ResultComm<GetPropertyResponse> {
155 let command = CommandPacket::new_none_flag(CommandTag::GetProperty { tag, memory_index });
156 self.send_command(&command)?;
157
158 let response = self.read_cmd_response()?;
159
160 if let CmdResponseTag::GetProperty(val) = response.tag {
161 Ok(GetPropertyResponse {
162 status: response.status,
163 property: PropertyTag::from_code(tag, &val),
164 response_words: val,
165 })
166 } else {
167 Err(CommunicationError::InvalidPacketReceived)
168 }
169 }
170
171 /// Set a property value on the device
172 ///
173 /// # Arguments
174 ///
175 /// * `tag` - The property tag to set
176 /// * `value` - The value to set
177 ///
178 /// # Returns
179 ///
180 /// Status code indicating success or failure
181 ///
182 /// # Errors
183 ///
184 /// Returns [`CommunicationError`] if communication fails
185 pub fn set_property(&mut self, tag: PropertyTagDiscriminants, value: u32) -> ResultStatus {
186 let command = CommandPacket::new_none_flag(CommandTag::SetProperty { tag, value });
187 self.send_command(&command)?;
188
189 let response = self.read_cmd_response()?;
190 Ok(response.status)
191 }
192
193 /// Reset the MCU
194 ///
195 /// Sends a reset command to the device. Note that the connection may be lost
196 /// after reset and need to be re-established.
197 ///
198 /// # Returns
199 ///
200 /// Status code (may be [`StatusCode::NoResponse`] if device resets successfully)
201 ///
202 /// # Errors
203 ///
204 /// Any [`CommunicationError`], almost all variants are possible.
205 pub fn reset(&mut self) -> ResultStatus {
206 let command = CommandPacket::new_none_flag(CommandTag::Reset);
207 self.send_command(&command)?;
208 let response = self.read_cmd_response()?;
209 Ok(response.status)
210 }
211
212 /// Call a function at the specified address
213 ///
214 /// # Arguments
215 ///
216 /// * `start_address` - Function address to call (must be word aligned)
217 /// * `argument` - Function argument
218 ///
219 /// # Returns
220 ///
221 /// Status code indicating success or failure
222 ///
223 /// # Errors
224 ///
225 /// Any [`CommunicationError`], almost all variants are possible.
226 pub fn call(&mut self, start_address: u32, argument: u32) -> ResultStatus {
227 let command = CommandPacket::new_none_flag(CommandTag::Call {
228 start_address,
229 argument,
230 });
231 self.send_command(&command)?;
232 let response = self.read_cmd_response()?;
233 Ok(response.status)
234 }
235
236 /// Execute program at the specified address
237 ///
238 /// # Arguments
239 ///
240 /// * `start_address` - Jump address (must be word aligned)
241 /// * `argument` - Function argument address
242 /// * `stackpointer` - Stack pointer address
243 ///
244 /// # Returns
245 ///
246 /// Status code indicating success or failure
247 ///
248 /// # Errors
249 ///
250 /// Any [`CommunicationError`], almost all variants are possible.
251 pub fn execute(&mut self, start_address: u32, argument: u32, stackpointer: u32) -> ResultStatus {
252 let command = CommandPacket::new_none_flag(CommandTag::Execute {
253 start_address,
254 argument,
255 stackpointer,
256 });
257 self.send_command(&command)?;
258 let response = self.read_cmd_response()?;
259 Ok(response.status)
260 }
261
262 /// Fill memory region with a pattern
263 ///
264 /// # Arguments
265 ///
266 /// * `start_address` - Start address (must be word aligned)
267 /// * `byte_count` - Number of bytes to fill (must be word aligned)
268 /// * `pattern` - 32-bit pattern to fill with
269 ///
270 /// # Returns
271 ///
272 /// Status code indicating success or failure
273 ///
274 /// # Errors
275 ///
276 /// Any [`CommunicationError`], almost all variants are possible.
277 pub fn fill_memory(&mut self, start_address: u32, byte_count: u32, pattern: u32) -> ResultStatus {
278 let command = CommandPacket::new_none_flag(CommandTag::FillMemory {
279 start_address,
280 byte_count,
281 pattern,
282 });
283 self.send_command(&command)?;
284 let response = self.read_cmd_response()?;
285 Ok(response.status)
286 }
287
288 /// Write data to MCU memory
289 ///
290 /// # Arguments
291 ///
292 /// * `start_address` - Start address for writing
293 /// * `memory_id` - Memory ID (0 for internal memory, see [`memory::mem_id`] for external)
294 /// * `bytes` - Data to write
295 ///
296 /// # Returns
297 ///
298 /// Status code indicating success or failure
299 ///
300 /// # Note
301 ///
302 /// Data will be automatically split into chunks based on max packet size
303 ///
304 /// # Errors
305 ///
306 /// Any [`CommunicationError`], almost all variants are possible.
307 pub fn write_memory(&mut self, start_address: u32, memory_id: u32, bytes: &[u8]) -> ResultStatus {
308 let command = CommandPacket::new_data_phase(CommandTag::WriteMemory {
309 start_address,
310 memory_id,
311 bytes,
312 });
313 self.send_command(&command)?;
314
315 let response = self.read_cmd_response()?;
316 Ok(response.status)
317 }
318
319 /// Erase all flash memory
320 ///
321 /// # Arguments
322 ///
323 /// * `memory_id` - Memory ID (0 for internal flash)
324 ///
325 /// # Returns
326 ///
327 /// Status code indicating success or failure
328 ///
329 /// # Warning
330 ///
331 /// This operation will erase the entire flash memory without recovering
332 /// the flash security section.
333 ///
334 /// # Errors
335 ///
336 /// Any [`CommunicationError`], almost all variants are possible.
337 pub fn flash_erase_all(&mut self, memory_id: u32) -> ResultStatus {
338 let command = CommandPacket::new_none_flag(CommandTag::FlashEraseAll { memory_id });
339 self.send_command(&command)?;
340 let response = self.read_cmd_response()?;
341 Ok(response.status)
342 }
343
344 /// Erase a specific flash region
345 ///
346 /// # Arguments
347 ///
348 /// * `start_address` - Start address of region to erase
349 /// * `byte_count` - Number of bytes to erase
350 /// * `memory_id` - Memory ID (0 for internal flash)
351 ///
352 /// # Returns
353 ///
354 /// Status code indicating success or failure
355 ///
356 /// # Errors
357 ///
358 /// Any [`CommunicationError`], almost all variants are possible.
359 pub fn flash_erase_region(&mut self, start_address: u32, byte_count: u32, memory_id: u32) -> ResultStatus {
360 let command = CommandPacket::new_none_flag(CommandTag::FlashEraseRegion {
361 start_address,
362 byte_count,
363 memory_id,
364 });
365 self.send_command(&command)?;
366 let response = self.read_cmd_response()?;
367 Ok(response.status)
368 }
369
370 /// Erase all flash and recover security section
371 ///
372 /// This command erases the entire flash memory and recovers the flash security section,
373 /// effectively unsecuring the device.
374 ///
375 /// # Returns
376 ///
377 /// Status code indicating success or failure
378 ///
379 /// # Errors
380 ///
381 /// Any [`CommunicationError`], almost all variants are possible.
382 pub fn flash_erase_all_unsecure(&mut self) -> ResultStatus {
383 let command = CommandPacket::new_none_flag(CommandTag::FlashEraseAllUnsecure);
384 self.send_command(&command)?;
385 let response = self.read_cmd_response()?;
386 Ok(response.status)
387 }
388
389 /// Read data from MCU memory
390 ///
391 /// # Arguments
392 ///
393 /// * `start_address` - Start address to read from
394 /// * `byte_count` - Number of bytes to read
395 /// * `memory_id` - Memory ID (0 for internal memory)
396 ///
397 /// # Returns
398 ///
399 /// Response containing the read data and status
400 ///
401 /// # Errors
402 ///
403 /// Returns [`CommunicationError`] if:
404 /// - Communication fails
405 /// - Invalid response is received
406 /// - Memory is protected or inaccessible
407 pub fn read_memory(
408 &mut self,
409 start_address: u32,
410 byte_count: u32,
411 memory_id: u32,
412 ) -> ResultComm<ReadMemoryResponse> {
413 let command = CommandPacket::new_none_flag(CommandTag::ReadMemory {
414 start_address,
415 byte_count,
416 memory_id,
417 });
418 self.send_command(&command)?;
419
420 // allowing one more status code when reading memory
421 let response = self.read_command()?;
422 let status = &response.status;
423 if !(status.is_success() || status.is_memory_blank_page_read_disallowed()) {
424 return Err((*status).into());
425 }
426
427 if let CmdResponseTag::ReadMemory(bytes) = response.tag {
428 Ok(ReadMemoryResponse {
429 status: response.status,
430 response_words: Box::new([bytes.len() as u32]),
431 bytes,
432 })
433 } else {
434 Err(CommunicationError::InvalidPacketReceived)
435 }
436 }
437
438 /// Generate a key blob by wrapping a DEK with the selected BKEK.
439 ///
440 /// # Arguments
441 ///
442 /// * `dek_data` - Data Encryption Key bytes to wrap
443 /// * `key_sel` - BKEK selector (0-3)
444 ///
445 /// # Returns
446 ///
447 /// The generated key blob.
448 ///
449 /// # Errors
450 ///
451 /// Returns [`CommunicationError`] if communication fails or the device rejects the request.
452 pub fn generate_key_blob(&mut self, dek_data: &[u8], key_sel: u32) -> ResultComm<Box<[u8]>> {
453 let dek_length = u32::try_from(dek_data.len()).or(Err(CommunicationError::InvalidData))?;
454 self.send_command(&CommandPacket::new_data_phase(CommandTag::GenerateKeyBlob {
455 key_sel,
456 length: dek_length,
457 option: 0,
458 data: Some(dek_data),
459 }))?;
460 // final generic response of the DEK data phase
461 self.read_cmd_response()?;
462
463 self.send_command(&CommandPacket::new_none_flag(CommandTag::GenerateKeyBlob {
464 key_sel,
465 length: 72,
466 option: 1,
467 data: None,
468 }))?;
469 let response = self.read_cmd_response()?;
470 match response.tag {
471 CmdResponseTag::KeyBlob(bytes) => Ok(bytes),
472 _ => Err(CommunicationError::InvalidPacketReceived),
473 }
474 }
475
476 /// Configure external memory
477 ///
478 /// # Arguments
479 ///
480 /// * `memory_id` - Memory ID to configure
481 /// * `address` - Address containing configuration data
482 ///
483 /// # Returns
484 ///
485 /// Status code indicating success or failure
486 ///
487 /// # Errors
488 ///
489 /// Any [`CommunicationError`], almost all variants are possible.
490 pub fn configure_memory(&mut self, memory_id: u32, address: u32) -> ResultStatus {
491 let command = CommandPacket::new_none_flag(CommandTag::ConfigureMemory { memory_id, address });
492 self.send_command(&command)?;
493 let response = self.read_cmd_response()?;
494 Ok(response.status)
495 }
496
497 /// Receive and process a Secure Binary (SB) file
498 ///
499 /// # Arguments
500 ///
501 /// * `bytes` - SB file data
502 ///
503 /// # Returns
504 ///
505 /// Status code indicating success or failure
506 ///
507 /// # Note
508 ///
509 /// The SB file will be processed and executed by the bootloader.
510 /// Progress bar will be shown if enabled.
511 ///
512 /// # Errors
513 ///
514 /// Any [`CommunicationError`], almost all variants are possible.
515 pub fn receive_sb_file(&mut self, bytes: &[u8]) -> ResultStatus {
516 let command = CommandPacket::new_data_phase(CommandTag::ReceiveSBFile { bytes });
517 match self.send_command(&command) {
518 Ok(()) | Err(CommunicationError::Aborted) => {
519 let response = self.read_cmd_response()?;
520 Ok(response.status)
521 }
522 Err(err) => Err(err),
523 }
524 }
525
526 /// Execute trust provisioning operation
527 ///
528 /// Performs various trust provisioning operations on the device, such as
529 /// proving genuinity, setting wrapped data, or other security-related operations.
530 ///
531 /// # Arguments
532 ///
533 /// * `operation` - The trust provisioning operation to execute
534 ///
535 /// # Returns
536 ///
537 /// A tuple containing:
538 /// - Status code indicating success or failure
539 /// - Response data specific to the operation
540 ///
541 /// # Errors
542 ///
543 /// Any [`CommunicationError`], almost all variants are possible.
544 pub fn trust_provisioning(&mut self, operation: &TrustProvOperation) -> ResultComm<(StatusCode, Box<[u32]>)> {
545 let command = CommandPacket::new_none_flag(CommandTag::TrustProvisioning(operation));
546 self.send_command(&command)?;
547
548 let response = self.read_cmd_response()?;
549 match response.tag {
550 CmdResponseTag::TrustProvisioning(data) => Ok((response.status, data)),
551 _ => Err(CommunicationError::InvalidPacketReceived),
552 }
553 }
554
555 /// Execute key provisioning operation
556 ///
557 /// Handles various key provisioning operations including enrolling PUF,
558 /// setting intrinsic keys, writing to non-volatile memory, and managing user keys.
559 /// The behavior varies based on the operation type:
560 /// - [`KeyProvOperation::SetUserKey`] operations include a data phase for key transmission
561 /// - [`KeyProvOperation::ReadKeyStore`] operations return the actual key store data
562 /// - Other operations return simple status responses
563 ///
564 /// # Arguments
565 ///
566 /// * `operation` - The key provisioning operation to execute
567 ///
568 /// # Returns
569 ///
570 /// Response type depends on the operation:
571 /// - [`KeyProvisioningResponse::Status`] for most operations
572 /// - [`KeyProvisioningResponse::KeyStore`] for [`KeyProvOperation::ReadKeyStore`] with key data
573 ///
574 /// # Errors
575 ///
576 /// Returns [`CommunicationError`] if:
577 /// - Communication with device fails
578 /// - Invalid response is received
579 /// - Data phase transmission fails for `SetUserKey`
580 pub fn key_provisioning(
581 &mut self,
582 operation: &KeyProvOperation,
583 ) -> Result<KeyProvisioningResponse, CommunicationError> {
584 let command = CommandPacket::new_none_flag(CommandTag::KeyProvisioning(operation));
585 if let KeyProvOperation::ReadKeyStore { .. } = operation {
586 self.send_command(&command)?;
587 let response = self.read_cmd_response()?;
588 // Extract the data based on the response tag
589 match response.tag {
590 CmdResponseTag::KeyProvisioning(data, data_phase) => {
591 // The data phase should contain the actual key store data
592 Ok(KeyProvisioningResponse::KeyStore {
593 status: response.status,
594 response_words: data,
595 bytes: data_phase.unwrap_or_default(),
596 })
597 }
598 _ => Err(CommunicationError::InvalidPacketReceived),
599 }
600 } else {
601 self.mask_read_data_phase = true;
602 self.send_command(&command)?;
603 self.mask_read_data_phase = false;
604 let response = self.read_cmd_response()?;
605 Ok(KeyProvisioningResponse::Status(response.status))
606 }
607 }
608
609 /// Read from MCU flash program once region (eFuse/OTP)
610 ///
611 /// Reads the raw bytes from the one-time programmable (OTP) memory region.
612 /// This memory can only be written once and is typically used for storing
613 /// permanent configuration or security keys.
614 ///
615 /// # Arguments
616 ///
617 /// * `index` - Start index of the eFuse/OTP region
618 /// * `count` - Number of bytes to read (must be 4 or 8)
619 ///
620 /// # Returns
621 ///
622 /// The read value as raw little-endian bytes, `count` bytes long. This matches blhost's
623 /// `FlashReadOnceResponse.data`, which also returns the full requested payload instead of
624 /// truncating 8-byte reads down to a single 32-bit word.
625 ///
626 /// # Errors
627 ///
628 /// Returns [`CommunicationError::ParseError`] if `count` is not 4 or 8. Otherwise, returns
629 /// [`CommunicationError`] if:
630 /// - Communication with device fails
631 /// - Invalid response type is received
632 /// - The specified index is out of range
633 /// - The OTP region is locked or inaccessible
634 pub fn flash_read_once(&mut self, index: u32, count: u32) -> ResultComm<Box<[u8]>> {
635 if count != 4 && count != 8 {
636 return Err(CommunicationError::ParseError(format!(
637 "invalid count {count}: flash-read-once only supports 4 or 8 byte reads, matching blhost"
638 )));
639 }
640
641 let command = CommandPacket::new_none_flag(CommandTag::FlashReadOnce { index, count });
642 self.send_command(&command)?;
643
644 let response = self.read_cmd_response()?;
645 match response.tag {
646 CmdResponseTag::FlashReadOnce(value) => Ok(value),
647 _ => Err(CommunicationError::InvalidPacketReceived),
648 }
649 }
650
651 /// Write into MCU once program region (eFuse/OTP)
652 ///
653 /// Programs a `count`-byte value into the one-time programmable memory region.
654 /// This operation is irreversible - once programmed, the memory cannot be
655 /// erased or reprogrammed. In OTP memory, bits can only change from 0 to 1,
656 /// never from 1 to 0.
657 ///
658 /// # Arguments
659 ///
660 /// * `index` - Start index of the eFuse/OTP region
661 /// * `count` - Number of bytes to write (must be 4 or 8, matching blhost's
662 /// `byte_count` choice)
663 /// * `data` - Value to write. Only the low `count` bytes are sent on the wire,
664 /// little-endian, matching blhost's `data.to_bytes(byte_count, "little")`
665 /// * `verify` - If true, reads back and verifies the written value
666 ///
667 /// # Returns
668 ///
669 /// Status code indicating success or failure. If verification is enabled
670 /// and fails, returns [`StatusCode::OtpVerifyFail`].
671 ///
672 /// # Notes
673 ///
674 /// - The verification process checks if all bits that were supposed to be
675 /// set to 1 are actually set. It uses bitwise AND to accommodate the fact
676 /// that some bits might have already been programmed.
677 /// - The index is masked to 24 bits during verification read
678 /// - ROM might not report errors when attempting to write to locked OTP,
679 /// so verification is recommended for critical operations
680 ///
681 /// # Errors
682 ///
683 /// Returns [`CommunicationError::ParseError`] if `count` is not 4 or 8, or if `data` doesn't
684 /// fit into `count` bytes. Otherwise, any
685 /// other [`CommunicationError`] variant may be returned; almost all are possible.
686 /// Verification fail is not an error.
687 pub fn flash_program_once(&mut self, index: u32, count: u32, data: u64, verify: bool) -> ResultStatus {
688 validate_flash_program_once_args(count, data)?;
689
690 let command = CommandPacket::new_none_flag(CommandTag::FlashProgramOnce { index, count, data });
691 self.send_command(&command)?;
692
693 let response = self.read_cmd_response()?;
694
695 if verify && response.status.is_success() {
696 // For verification, we read back the value and check if the bits we set are still set
697 // Note: In OTP, we can only set bits from 0 to 1, not vice versa
698 match self.flash_read_once(index & (EFUSE_LOCK_FLAG - 1), count) {
699 Ok(read_bytes) => {
700 let mut word = [0u8; 8];
701 let len = read_bytes.len().min(8);
702 word[..len].copy_from_slice(&read_bytes[..len]);
703 let read_value = u64::from_le_bytes(word);
704
705 if read_value & data == data {
706 Ok(response.status)
707 } else {
708 // Custom status code for verification failure
709 Ok(StatusCode::OtpVerifyFail)
710 }
711 }
712 Err(e) => Err(e),
713 }
714 } else {
715 Ok(response.status)
716 }
717 }
718
719 /// Read a 32-bit word from the eFuse/OTP region
720 ///
721 /// Convenience wrapper over [`McuBoot::flash_read_once`] with a fixed 4-byte count, matching
722 /// blhost's `efuse-read-once` command (`McuBoot.efuse_read_once` in SPSDK).
723 ///
724 /// # Arguments
725 ///
726 /// * `index` - Index of the OTP word (not the shadowed memory address)
727 ///
728 /// # Returns
729 ///
730 /// The read 32-bit value, decoded as little-endian.
731 ///
732 /// # Errors
733 ///
734 /// Same as [`McuBoot::flash_read_once`].
735 pub fn efuse_read_once(&mut self, index: u32) -> ResultComm<u32> {
736 let bytes = self.flash_read_once(index, 4)?;
737 let mut word = [0u8; 4];
738 let len = bytes.len().min(word.len());
739 word[..len].copy_from_slice(&bytes[..len]);
740 Ok(u32::from_le_bytes(word))
741 }
742
743 /// Program a 32-bit word into the eFuse/OTP region
744 ///
745 /// Convenience wrapper over [`McuBoot::flash_program_once`] with a fixed 4-byte count, matching
746 /// blhost's `efuse-program-once` command (`McuBoot.efuse_program_once` in SPSDK). This operation
747 /// is irreversible.
748 ///
749 /// # Arguments
750 ///
751 /// * `index` - Index of the OTP word (not the shadowed memory address). Set bit 24
752 /// ([`EFUSE_LOCK_FLAG`]) to request a permanent lock of the word, as blhost's `lock` option does.
753 /// * `data` - 32-bit value to program
754 /// * `verify` - If true, reads back and verifies the written value
755 ///
756 /// # Returns
757 ///
758 /// Status code indicating success or failure. If verification is enabled
759 /// and fails, returns [`StatusCode::OtpVerifyFail`].
760 ///
761 /// # Errors
762 ///
763 /// Same as [`McuBoot::flash_program_once`].
764 pub fn efuse_program_once(&mut self, index: u32, data: u32, verify: bool) -> ResultStatus {
765 self.flash_program_once(index, 4, u64::from(data), verify)
766 }
767
768 /// Read fuse data
769 ///
770 /// Reads data from the device's fuse memory region. Fuses are one-time
771 /// programmable memory bits used for permanent configuration, security
772 /// settings, and device-specific information.
773 ///
774 /// # Arguments
775 ///
776 /// * `start_address` - Starting address in the fuse memory region
777 /// * `byte_count` - Number of bytes to read
778 /// * `memory_id` - Memory identifier (device-specific)
779 ///
780 /// # Returns
781 ///
782 /// [`ReadMemoryResponse`] containing:
783 /// - Status code of the operation
784 /// - Response metadata (byte count)
785 /// - Actual fuse data bytes
786 ///
787 /// # Errors
788 ///
789 /// Returns [`CommunicationError`] if:
790 /// - The operation fails (converted from status code)
791 /// - Invalid response type is received
792 /// - Fuse region is inaccessible or protected
793 pub fn fuse_read(&mut self, start_address: u32, byte_count: u32, memory_id: u32) -> ResultComm<ReadMemoryResponse> {
794 let command = CommandPacket::new_none_flag(CommandTag::FuseRead {
795 start_address,
796 byte_count,
797 memory_id,
798 });
799 self.send_command(&command)?;
800 let response = self.read_cmd_response()?;
801 let status = &response.status;
802 if !status.is_success() {
803 return Err((*status).into());
804 }
805 match response.tag {
806 CmdResponseTag::ReadMemory(bytes) => Ok(ReadMemoryResponse {
807 status: response.status,
808 response_words: Box::new([bytes.len() as u32]),
809 bytes,
810 }),
811 _ => Err(CommunicationError::InvalidPacketReceived),
812 }
813 }
814
815 /// Program fuse data
816 ///
817 /// Writes data to the device's fuse memory region. This operation is
818 /// permanent and irreversible. Once a fuse is programmed (blown), it
819 /// cannot be restored to its original state.
820 ///
821 /// # Arguments
822 ///
823 /// * `start_address` - Starting address in the fuse memory region
824 /// * `memory_id` - Memory identifier (device-specific)
825 /// * `bytes` - Data to write to the fuses
826 ///
827 /// # Returns
828 ///
829 /// Status code indicating success or failure
830 ///
831 /// # Warning
832 ///
833 /// This operation permanently modifies the device hardware. Incorrect
834 /// fuse programming can render the device unusable. Always verify the
835 /// correct fuse addresses and values before programming.
836 ///
837 /// # Errors
838 ///
839 /// Any [`CommunicationError`], almost all variants are possible.
840 pub fn fuse_program(&mut self, start_address: u32, memory_id: u32, bytes: &[u8]) -> ResultStatus {
841 let command = CommandPacket::new_data_phase(CommandTag::FuseProgram {
842 start_address,
843 memory_id,
844 bytes,
845 });
846 self.send_command(&command)?;
847 let response = self.read_cmd_response()?;
848 Ok(response.status)
849 }
850
851 /// Load image data directly to the device
852 ///
853 /// Sends raw image data to the device without a specific command header.
854 ///
855 /// # Arguments
856 ///
857 /// * `bytes` - Raw image data to be loaded
858 ///
859 /// # Returns
860 ///
861 /// Status code indicating success or failure
862 ///
863 /// # Errors
864 ///
865 /// Any [`CommunicationError`], almost all variants are possible.
866 pub fn load_image(&mut self, bytes: &[u8]) -> ResultStatus {
867 let command = CommandPacket::new_data_phase(CommandTag::NoCommand { bytes });
868 self.send_command(&command)?;
869 Ok(StatusCode::Success)
870 }
871
872 /// Read command response and validate status
873 ///
874 /// Internal helper method that reads a command response from the device
875 /// and validates its status. If the status indicates an error, it converts
876 /// the status code into a [`CommunicationError`].
877 ///
878 /// # Returns
879 ///
880 /// The command response if status indicates success
881 ///
882 /// # Errors
883 ///
884 /// Returns [`CommunicationError`] converted from the status code if the
885 /// operation was not successful
886 fn read_cmd_response(&mut self) -> ResultComm<CmdResponse> {
887 let response = self.read_command()?;
888 info!("{}: {response:02X?}", cstr!("<bold>Received"));
889 if response.status.is_success() {
890 Ok(response)
891 } else {
892 Err(response.status.into())
893 }
894 }
895 /// Send a command packet to the device
896 ///
897 /// Internal helper method that handles the complete command transmission
898 /// process, including data phase handling for commands that require it.
899 /// For commands with data phases, it automatically queries the device's
900 /// maximum packet size and splits the data accordingly.
901 ///
902 /// # Arguments
903 ///
904 /// * `command` - The command packet to send
905 ///
906 /// # Returns
907 ///
908 /// Ok(()) if the command was sent successfully
909 ///
910 /// # Errors
911 ///
912 /// Returns [`CommunicationError`] if:
913 /// - Failed to get max packet size property
914 /// - Device communication fails
915 /// - Data phase transmission fails
916 ///
917 /// # Workflow
918 ///
919 /// 1. Extracts parameters and data phase from the command
920 /// 2. Constructs and sends the initial command packet
921 /// 3. If data phase exists:
922 /// - Queries max packet size from device
923 /// - Reads intermediate response
924 /// - Splits data into chunks
925 /// - Sends each chunk with optional progress tracking
926 fn send_command(&mut self, command: &CommandPacket) -> ResultComm<()> {
927 let tag = &command.tag;
928 let (params, data_phase) = tag.to_params();
929 let packet = command.header.construct_frame(¶ms, tag.code());
930 info!("{}: {command:02X?}", cstr!("<bold>Sending"));
931
932 if let Some(data) = data_phase {
933 info!("Sending data phase: {data:02X?}");
934 let max_packet_size: u32 = {
935 let response = self.get_property(PropertyTagDiscriminants::MaxPacketSize, 0)?;
936 match response.property {
937 PropertyTag::MaxPacketSize(size) => size,
938 _ => return Err(CommunicationError::InvalidData),
939 }
940 };
941 if !matches!(tag, CommandTag::NoCommand { .. }) {
942 self.device.write_packet_raw(&packet)?;
943 // this is the intermediate generic response
944 self.read_cmd_response()?;
945 }
946 // Block for progress bar
947 {
948 let progress_bar = self.create_progress_bar(data.len() as u64, "Sending data");
949 for bytes in data.chunks(
950 max_packet_size
951 .try_into()
952 .expect("pointer size of this platform is too small"),
953 ) {
954 self.device.write_packet_concrete(DataPhasePacket::parse(bytes)?)?;
955 if let Some(bar) = progress_bar.as_ref() {
956 bar.inc(max_packet_size.into());
957 }
958 }
959 }
960 } else {
961 self.device.write_packet_raw(&packet)?;
962 }
963 Ok(())
964 }
965
966 /// Read a command response from the device
967 ///
968 /// Internal helper method that reads and parses command responses,
969 /// handling both simple responses and those with data phases.
970 ///
971 /// # Returns
972 ///
973 /// Parsed command response
974 ///
975 /// # Errors
976 ///
977 /// Returns [`CommunicationError`] if:
978 /// - Communication timeout occurs
979 /// - Invalid data format is received
980 /// - Command flag is unrecognized
981 /// - Data phase read fails
982 ///
983 /// # Data Phase Handling
984 ///
985 /// When a response includes a data phase:
986 /// 1. Reads the initial response header
987 /// 2. Extracts the data phase length
988 /// 3. Reads data packets until complete
989 /// 4. Shows progress bar if enabled
990 /// 5. Reads final status response
991 fn read_command(&mut self) -> ResultComm<CmdResponse> {
992 trace!("Starting to read command");
993 let data = self.device.read_packet_raw(CmdResponse::get_code())?;
994 let params_slice = &data[8..];
995
996 // data[3] = param count
997 if params_slice.len() % 4 != 0 && params_slice.len() != 4 * data[3] as usize {
998 return Err(CommunicationError::InvalidData);
999 }
1000
1001 let header = CommandHeader {
1002 flag: CommandFlag::try_from(data[1]).or(Err(CommunicationError::InvalidData))?,
1003 reserved: data[2],
1004 };
1005 let status = parse_status(data[4..8].try_into().or_invalid()?)?;
1006
1007 // If we dont expect data phase, we can force to return response without data
1008 // This is necessary for key-provisionning commands since their intermediate
1009 // generic response commands have data phase flag set, which is incorrect
1010 if self.mask_read_data_phase {
1011 return Ok(CmdResponse {
1012 header,
1013 status,
1014 tag: CmdResponseTag::from_code(data[0], params_slice, None).ok_or(CommunicationError::InvalidData)?,
1015 });
1016 }
1017
1018 match header.flag {
1019 CommandFlag::NoData => Ok(CmdResponse {
1020 header,
1021 status,
1022 tag: CmdResponseTag::from_code(data[0], params_slice, None).ok_or(CommunicationError::InvalidData)?,
1023 }),
1024 CommandFlag::HasDataPhase => {
1025 let length = u32::from_le_bytes(params_slice[0..4].try_into().or_invalid()?);
1026 trace!("Data phase length: {length}");
1027
1028 let mut data_phase = Vec::new();
1029 // Block for progress bar
1030 {
1031 let progress_bar = self.create_progress_bar(length.into(), "Receiving data");
1032 while data_phase.len() != length as usize {
1033 trace!("Reading data phase packet");
1034 data_phase.extend(match self.device.read_packet_concrete::<DataPhasePacket>() {
1035 Ok(data) => {
1036 if let Some(bar) = progress_bar.as_ref() {
1037 bar.inc(data.data.len() as u64);
1038 }
1039 data.data
1040 }
1041 Err(CommunicationError::Aborted) => break,
1042 Err(err) => return Err(err),
1043 });
1044 }
1045 }
1046
1047 trace!("Reading final response");
1048 let final_response = self.device.read_packet_raw(CmdResponse::get_code())?;
1049 let status = parse_status(final_response[4..8].try_into().or_invalid()?)?;
1050
1051 Ok(CmdResponse {
1052 header: CommandHeader {
1053 flag: CommandFlag::NoData,
1054 reserved: data[2],
1055 },
1056 status,
1057 tag: CmdResponseTag::from_code(data[0], params_slice, Some(&data_phase))
1058 .ok_or(CommunicationError::InvalidData)?,
1059 })
1060 }
1061 }
1062 }
1063
1064 /// Create a progress bar for data transfers
1065 ///
1066 /// Internal helper method that creates a progress bar if progress tracking is enabled.
1067 /// The progress bar displays the transfer status with binary size formatting.
1068 ///
1069 /// # Arguments
1070 ///
1071 /// * `len` - Total length of data to transfer in bytes
1072 /// * `prefix` - Descriptive prefix for the progress bar
1073 ///
1074 /// # Returns
1075 ///
1076 /// Optional progress bar instance:
1077 /// - Some(ProgressBar) if progress tracking is enabled
1078 /// - None if progress tracking is disabled
1079 ///
1080 /// # Progress Bar Format
1081 ///
1082 /// The progress bar displays:
1083 /// - Custom prefix text
1084 /// - Visual progress indicator (40 characters wide)
1085 /// - Current bytes transferred / total bytes
1086 fn create_progress_bar(&self, len: u64, prefix: &'static str) -> Option<ProgressBar> {
1087 if self.progress_bar {
1088 let bar = ProgressBar::new(len);
1089 bar.set_style(
1090 ProgressStyle::with_template("{prefix} [{bar:40}] {binary_bytes:>}/{binary_total_bytes}")
1091 .unwrap()
1092 .progress_chars("##-"),
1093 );
1094 bar.set_prefix(prefix);
1095 Some(bar)
1096 } else {
1097 None
1098 }
1099 }
1100}
1101
1102/// Bit of the eFuse/OTP index requesting a permanent lock of the word, set by blhost's
1103/// `efuse-program-once ... lock` option.
1104pub const EFUSE_LOCK_FLAG: u32 = 1 << 24;
1105
1106/// Checks `flash-program-once` arguments before anything is sent to the device: `count` must be
1107/// 4 or 8, and `data` must fit into `count` bytes. OTP writes are irreversible, so upper bits that
1108/// don't fit into a 4-byte write are rejected instead of silently dropped (blhost raises an error
1109/// there too).
1110fn validate_flash_program_once_args(count: u32, data: u64) -> ResultComm<()> {
1111 if count != 4 && count != 8 {
1112 return Err(CommunicationError::ParseError(format!(
1113 "invalid count {count}: flash-program-once only supports 4 or 8 byte writes, matching blhost"
1114 )));
1115 }
1116 if count == 4 && data > u64::from(u32::MAX) {
1117 return Err(CommunicationError::ParseError(format!(
1118 "data 0x{data:X} does not fit into {count} bytes; use count 8 or a 32-bit value"
1119 )));
1120 }
1121 Ok(())
1122}
1123
1124/// Parse status code from raw bytes
1125///
1126/// Converts a 4-byte little-endian value into a [`StatusCode`] enum.
1127/// This function is used throughout the module to interpret device responses.
1128///
1129/// # Arguments
1130///
1131/// * `data` - 4 bytes containing the status code in little-endian format
1132///
1133/// # Returns
1134///
1135/// Parsed status code enum variant
1136///
1137/// # Errors
1138///
1139/// Returns [`CommunicationError::UnexpectedStatus`] if:
1140/// - The status code value is not recognized
1141/// - The discriminant doesn't match any known [`StatusCode`] variant
1142///
1143/// The error includes both the unknown status placeholder and the actual
1144/// numeric value for debugging purposes.
1145fn parse_status(data: [u8; 4]) -> ResultComm<StatusCode> {
1146 let discriminant = u32::from_le_bytes(data);
1147 StatusCode::try_from(discriminant).or(Err(CommunicationError::UnexpectedStatus(
1148 StatusCode::UnknownStatusCode,
1149 discriminant,
1150 )))
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use crate::mboot::{
1156 McuBoot,
1157 protocols::{ProtocolOpen, uart::UARTProtocol},
1158 tags::property::{PropertyTag, PropertyTagDiscriminants},
1159 };
1160
1161 #[test]
1162 fn test_validate_flash_program_once_args() {
1163 use super::validate_flash_program_once_args as validate;
1164 assert!(validate(4, 0xFFFF_FFFF).is_ok());
1165 assert!(validate(8, 0x1122_3344_5566_7788).is_ok());
1166 assert!(validate(4, 0x1_0000_0000).is_err());
1167 assert!(validate(0x12, 4).is_err());
1168 assert!(validate(5, 0).is_err());
1169 }
1170
1171 /// Device replaying scripted responses and recording written packets
1172 struct ScriptedDevice {
1173 responses: std::collections::VecDeque<Vec<u8>>,
1174 written: Vec<Vec<u8>>,
1175 }
1176
1177 impl crate::mboot::protocols::Protocol for ScriptedDevice {
1178 fn get_timeout(&self) -> std::time::Duration {
1179 std::time::Duration::from_secs(1)
1180 }
1181
1182 fn get_polling_interval(&self) -> std::time::Duration {
1183 std::time::Duration::from_millis(1)
1184 }
1185
1186 fn get_identifier(&self) -> &'static str {
1187 "scripted"
1188 }
1189
1190 fn read(&mut self, _bytes: usize) -> crate::mboot::ResultComm<Vec<u8>> {
1191 unimplemented!()
1192 }
1193
1194 fn write_packet_raw(&mut self, data: &[u8]) -> crate::mboot::ResultComm<()> {
1195 self.written.push(data.to_vec());
1196 Ok(())
1197 }
1198
1199 fn read_packet_raw(&mut self, _packet_code: u8) -> crate::mboot::ResultComm<Vec<u8>> {
1200 self.responses.pop_front().ok_or(crate::CommunicationError::Timeout)
1201 }
1202 }
1203
1204 /// Command response packet: tag, flag, reserved, parameter count, status and parameters
1205 fn response(tag: u8, flag: u8, params: &[u32]) -> Vec<u8> {
1206 let mut packet = vec![tag, flag, 0, u8::try_from(params.len() + 1).unwrap(), 0, 0, 0, 0];
1207 for param in params {
1208 packet.extend_from_slice(¶m.to_le_bytes());
1209 }
1210 packet
1211 }
1212
1213 #[test]
1214 fn test_generate_key_blob_reads_data_phase_response() {
1215 const GENERATE_KEY_BLOB: u32 = 0x13;
1216 let blob: Vec<u8> = (0..72).collect();
1217 let responses = [
1218 // max packet size
1219 response(0xA7, 0, &[32]),
1220 // DEK command accepted, then DEK data phase done
1221 response(0xA0, 0, &[GENERATE_KEY_BLOB]),
1222 response(0xA0, 0, &[GENERATE_KEY_BLOB]),
1223 // key blob with data phase, then final response
1224 response(0xB3, 1, &[72]),
1225 blob.clone(),
1226 response(0xA0, 0, &[GENERATE_KEY_BLOB]),
1227 ];
1228 let mut boot = McuBoot::new(ScriptedDevice {
1229 responses: responses.into(),
1230 written: Vec::new(),
1231 });
1232
1233 let key_blob = boot.generate_key_blob(&[0xAB; 16], 0).unwrap();
1234
1235 assert_eq!(&*key_blob, &blob[..]);
1236 assert!(boot.device.responses.is_empty());
1237 }
1238
1239 const DEVICE: &str = "COM3";
1240 fn get_boot() -> McuBoot<UARTProtocol> {
1241 McuBoot::new(UARTProtocol::open(DEVICE).unwrap())
1242 }
1243
1244 #[test]
1245 #[ignore = "Requires hardware connection to board"]
1246 fn test_board_get_version() {
1247 let mut boot = get_boot();
1248 let version = boot.get_property(PropertyTagDiscriminants::CurrentVersion, 0).unwrap();
1249 if let PropertyTag::CurrentVersion(ver) = version.property {
1250 assert_eq!(ver.mark, 'K');
1251 assert_eq!(ver.major, 3);
1252 assert_eq!(ver.minor, 1);
1253 assert_eq!(ver.fixation, 1);
1254 } else {
1255 panic!()
1256 }
1257 }
1258}