Skip to main content

tinyboot_protocol/
lib.rs

1#![no_std]
2#![warn(missing_docs)]
3
4//! Wire protocol for the tinyboot bootloader.
5//!
6//! Defines the frame format, commands, status codes, and CRC used for
7//! host-device communication over UART / RS-485.
8
9/// CRC16-CCITT implementation.
10pub mod crc;
11/// Frame encoding, decoding, and typed payload access.
12pub mod frame;
13pub(crate) mod sync;
14
15pub use frame::{Data, EraseData, InfoData, MAX_PAYLOAD, VerifyData};
16
17/// Pack a semantic version into a `u16` using 5.5.6 encoding.
18///
19/// Layout: `(major << 11) | (minor << 6) | patch`
20/// - major: 0–31, minor: 0–31, patch: 0–63
21/// - `0xFFFF` is reserved as "no version" (erased flash sentinel).
22pub const fn pack_version(major: u8, minor: u8, patch: u8) -> u16 {
23    ((major as u16) << 11) | ((minor as u16) << 6) | (patch as u16)
24}
25
26/// Unpack a 5.5.6-encoded `u16` into `(major, minor, patch)`.
27pub const fn unpack_version(v: u16) -> (u8, u8, u8) {
28    let major = (v >> 11) as u8 & 0x1F;
29    let minor = (v >> 6) as u8 & 0x1F;
30    let patch = v as u8 & 0x3F;
31    (major, minor, patch)
32}
33
34/// `const fn` parse of a `&str` decimal digit sequence into `u8`.
35/// Panics at compile time if the string is empty or contains non-digit chars.
36pub const fn const_parse_u8(s: &str) -> u8 {
37    let bytes = s.as_bytes();
38    let mut i = 0;
39    let mut result: u16 = 0;
40    while i < bytes.len() {
41        let d = bytes[i];
42        assert!(d >= b'0' && d <= b'9', "non-digit in version string");
43        result = result * 10 + (d - b'0') as u16;
44        i += 1;
45    }
46    assert!(result <= 255, "version component exceeds u8");
47    result as u8
48}
49
50/// Expands to `pack_version(MAJOR, MINOR, PATCH)` using the **calling crate's**
51/// `Cargo.toml` version fields. Zero runtime cost — evaluates to a `u16` constant.
52///
53/// Usage: `static VERSION: u16 = tinyboot_protocol::pkg_version!();`
54#[macro_export]
55macro_rules! pkg_version {
56    () => {
57        $crate::pack_version(
58            $crate::const_parse_u8(env!("CARGO_PKG_VERSION_MAJOR")),
59            $crate::const_parse_u8(env!("CARGO_PKG_VERSION_MINOR")),
60            $crate::const_parse_u8(env!("CARGO_PKG_VERSION_PATCH")),
61        )
62    };
63}
64
65/// Commands (host to device).
66#[repr(u8)]
67#[derive(Debug, Clone, Copy, PartialEq)]
68#[cfg_attr(feature = "defmt", derive(defmt::Format))]
69pub enum Cmd {
70    /// Query device info (capacity, erase size, versions, mode).
71    Info = 0x00,
72    /// Erase flash at address. First erase transitions Idle to Updating.
73    Erase = 0x01,
74    /// Write data at address. Only valid in Updating state.
75    Write = 0x02,
76    /// Compute CRC16 over app region and transition to Validating.
77    Verify = 0x03,
78    /// Reset the device. `addr=0`: boot app, `addr=1`: enter bootloader.
79    Reset = 0x04,
80}
81
82impl Cmd {
83    /// Returns true if `b` is a valid command code.
84    pub fn is_valid(b: u8) -> bool {
85        b <= 0x04
86    }
87}
88
89/// Response status codes (device to host).
90#[repr(u8)]
91#[derive(Debug, Clone, Copy, PartialEq)]
92#[cfg_attr(feature = "defmt", derive(defmt::Format))]
93pub enum Status {
94    /// Frame is a request (not a response).
95    Request = 0x00,
96    /// Success.
97    Ok = 0x01,
98    /// Flash write or erase failed.
99    WriteError = 0x02,
100    /// CRC verification failed.
101    CrcMismatch = 0x03,
102    /// Address or length out of range.
103    AddrOutOfBounds = 0x04,
104    /// Command not valid in current state.
105    Unsupported = 0x05,
106    /// Frame payload exceeds maximum size.
107    PayloadOverflow = 0x06,
108}
109
110impl Status {
111    /// Returns true if `b` is a valid status code.
112    pub fn is_valid(b: u8) -> bool {
113        b <= 0x06
114    }
115}
116
117/// Transport IO error.
118///
119/// Returned by [`Frame::read`](frame::Frame::read) when the underlying
120/// transport fails. Protocol-level errors (bad CRC, invalid frame) are
121/// reported via [`Status`] instead.
122#[derive(Debug, PartialEq)]
123pub struct ReadError;
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn cmd_is_valid() {
131        assert!(Cmd::is_valid(Cmd::Info as u8));
132        assert!(Cmd::is_valid(Cmd::Reset as u8));
133        assert!(!Cmd::is_valid(0x05));
134        assert!(!Cmd::is_valid(0xFF));
135    }
136
137    #[test]
138    fn status_is_valid() {
139        assert!(Status::is_valid(Status::Request as u8));
140        assert!(Status::is_valid(Status::Unsupported as u8));
141        assert!(Status::is_valid(Status::PayloadOverflow as u8));
142        assert!(!Status::is_valid(0x07));
143        assert!(!Status::is_valid(0xFF));
144    }
145
146    #[test]
147    fn pack_unpack_round_trip() {
148        assert_eq!(unpack_version(pack_version(0, 0, 1)), (0, 0, 1));
149        assert_eq!(unpack_version(pack_version(1, 2, 3)), (1, 2, 3));
150        assert_eq!(unpack_version(pack_version(31, 31, 63)), (31, 31, 63));
151        assert_eq!(pack_version(0, 0, 0), 0);
152    }
153
154    #[test]
155    fn erased_flash_sentinel() {
156        // 0xFFFF must not collide with any valid version
157        let (m, n, p) = unpack_version(0xFFFF);
158        assert_eq!((m, n, p), (31, 31, 63));
159    }
160
161    #[test]
162    fn pkg_version_macro() {
163        let v = pkg_version!();
164        let expected = pack_version(
165            const_parse_u8(env!("CARGO_PKG_VERSION_MAJOR")),
166            const_parse_u8(env!("CARGO_PKG_VERSION_MINOR")),
167            const_parse_u8(env!("CARGO_PKG_VERSION_PATCH")),
168        );
169        assert_eq!(v, expected);
170    }
171}