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