elf2flash_core/
lib.rs

1use std::{collections::HashSet, error::Error, io::{Read, Write, Seek}};
2
3use assert_into::AssertInto;
4use log::debug;
5use static_assertions::const_assert;
6use zerocopy::IntoBytes;
7
8use crate::{address_range::{FLASH_SECTOR_ERASE_SIZE, MAIN_RAM_END, MAIN_RAM_START, RP2040_ADDRESS_RANGES_FLASH, RP2040_ADDRESS_RANGES_RAM, XIP_SRAM_END, XIP_SRAM_START}, elf::{realize_page, AddressRangesExt, Elf32Header, PAGE_SIZE}, uf2::{Uf2BlockData, Uf2BlockFooter, Uf2BlockHeader, RP2040_FAMILY_ID, UF2_FLAG_FAMILY_ID_PRESENT, UF2_MAGIC_END, UF2_MAGIC_START0, UF2_MAGIC_START1}};
9
10pub mod address_range;
11pub mod elf;
12pub mod uf2;
13
14pub trait ProgressReporter {
15    fn start(&mut self, total_bytes: usize);
16    fn advance(&mut self, bytes: usize);
17    fn finish(&mut self);
18}
19
20pub struct NoProgress;
21impl ProgressReporter for NoProgress {
22    fn start(&mut self, _total_bytes: usize) {}
23    fn advance(&mut self, _bytes: usize) {}
24    fn finish(&mut self) {}
25}
26
27/// Convert a file to a uf2 file. Give an input, and it generates an output. If you don't want to provide a family_id or reporter, then the family_id defaults to
28/// the rp2040's family id. Just pass in the NoProgress struct to reporter you do not wish to have progress reporting.
29///
30/// # Examples
31///
32/// ```
33/// use std::io::Cursor;
34/// use elf2flash_core::{elf2uf2, NoProgress};
35///
36/// let bytes_in = std::io::Cursor::new(&include_bytes!("../tests/rp2040/hello_usb.elf")[..]);
37/// let mut bytes_out = Vec::new();
38/// elf2uf2(bytes_in, &mut bytes_out, None, NoProgress).unwrap();
39/// ```
40pub fn elf2uf2(mut input: impl Read + Seek, mut output: impl Write, family_id: Option<u32>, mut reporter: impl ProgressReporter) -> Result<(), Box<dyn Error>> {
41    let eh = Elf32Header::from_read(&mut input)?;
42
43    let entries = eh.read_elf32_ph_entries(&mut input)?;
44
45    let ram_style = eh
46        .is_ram_binary(&entries)
47        .ok_or("entry point is not in mapped part of file".to_string())?;
48
49    if ram_style {
50        debug!("Detected RAM binary");
51    } else {
52        debug!("Detected FLASH binary");
53    }
54
55    let valid_ranges = if ram_style {
56        RP2040_ADDRESS_RANGES_RAM
57    } else {
58        RP2040_ADDRESS_RANGES_FLASH
59    };
60
61    let mut pages = valid_ranges.check_elf32_ph_entries(&entries)?;
62
63    if pages.is_empty() {
64        return Err("The input file has no memory pages".into());
65    }
66
67    if ram_style {
68        let mut expected_ep_main_ram = u32::MAX;
69        let mut expected_ep_xip_sram = u32::MAX;
70
71        #[allow(clippy::manual_range_contains)]
72        pages.keys().copied().for_each(|addr| {
73            if addr >= MAIN_RAM_START && addr <= MAIN_RAM_END {
74                expected_ep_main_ram = expected_ep_main_ram.min(addr) | 0x1;
75            } else if addr >= XIP_SRAM_START && addr < XIP_SRAM_END {
76                expected_ep_xip_sram = expected_ep_xip_sram.min(addr) | 0x1;
77            }
78        });
79
80        let expected_ep = if expected_ep_main_ram != u32::MAX {
81            expected_ep_main_ram
82        } else {
83            expected_ep_xip_sram
84        };
85
86        if expected_ep == expected_ep_xip_sram {
87            return Err("B0/B1 Boot ROM does not support direct entry into XIP_SRAM".into());
88        } else if eh.entry != expected_ep {
89            #[allow(clippy::unnecessary_cast)]
90            return Err(format!(
91                "A RAM binary should have an entry point at the beginning: {:#08x} (not {:#08x})",
92                expected_ep, eh.entry as u32
93            )
94            .into());
95        }
96        const_assert!(0 == (MAIN_RAM_START & (PAGE_SIZE - 1)));
97
98        // TODO: check vector table start up
99        // currently don't require this as entry point is now at the start, we don't know where reset vector is
100    } else {
101        // Fill in empty dummy uf2 pages to align the binary to flash sectors (except for the last sector which we don't
102        // need to pad, and choose not to to avoid making all SDK UF2s bigger)
103        // That workaround is required because the bootrom uses the block number for erase sector calculations:
104        // https://github.com/raspberrypi/pico-bootrom/blob/c09c7f08550e8a36fc38dc74f8873b9576de99eb/bootrom/virtual_disk.c#L205
105
106        let touched_sectors: HashSet<u32> = pages
107            .keys()
108            .map(|addr| addr / FLASH_SECTOR_ERASE_SIZE)
109            .collect();
110
111        let last_page_addr = *pages.last_key_value().unwrap().0;
112        for sector in touched_sectors {
113            let mut page = sector * FLASH_SECTOR_ERASE_SIZE;
114
115            while page < (sector + 1) * FLASH_SECTOR_ERASE_SIZE {
116                if page < last_page_addr && !pages.contains_key(&page) {
117                    pages.insert(page, Vec::new());
118                }
119                page += PAGE_SIZE;
120            }
121        }
122    }
123
124    let mut block_header = Uf2BlockHeader {
125        magic_start0: UF2_MAGIC_START0,
126        magic_start1: UF2_MAGIC_START1,
127        flags: UF2_FLAG_FAMILY_ID_PRESENT,
128        target_addr: 0,
129        payload_size: PAGE_SIZE,
130        block_no: 0,
131        num_blocks: pages.len().assert_into(),
132        file_size: family_id.unwrap_or(RP2040_FAMILY_ID),
133    };
134
135    let mut block_data: Uf2BlockData = [0; 476];
136
137    let block_footer = Uf2BlockFooter {
138        magic_end: UF2_MAGIC_END,
139    };
140
141    log::debug!("Writing program");
142
143    reporter.start(pages.len() * 512);
144
145    let last_page_num = pages.len() - 1;
146
147    for (page_num, (target_addr, fragments)) in pages.into_iter().enumerate() {
148        block_header.target_addr = target_addr;
149        block_header.block_no = page_num.assert_into();
150
151        debug!(
152            "Page {} / {} {:#08x}",
153            block_header.block_no as u32,
154            block_header.num_blocks as u32,
155            block_header.target_addr as u32
156        );
157
158        block_data.iter_mut().for_each(|v| *v = 0);
159
160        realize_page(&mut input, &fragments, &mut block_data)?;
161
162        output.write_all(block_header.as_bytes())?;
163        output.write_all(block_data.as_bytes())?;
164        output.write_all(block_footer.as_bytes())?;
165
166        if page_num != last_page_num {
167            reporter.advance(512);
168        }
169    }
170
171    // Drop the output before the progress bar is allowd to finish
172    drop(output);
173
174    reporter.advance(512);
175
176    reporter.finish();
177
178    Ok(())
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::io;
185
186    #[test]
187    pub fn hello_usb() {
188        log::set_max_level(log::LevelFilter::Debug);
189        let bytes_in = io::Cursor::new(&include_bytes!("../tests/rp2040/hello_usb.elf")[..]);
190        let mut bytes_out = Vec::new();
191        elf2uf2(bytes_in, &mut bytes_out, None, NoProgress).unwrap();
192
193        assert_eq!(bytes_out, include_bytes!("../tests/rp2040/hello_usb.uf2"));
194    }
195
196    #[test]
197    pub fn hello_serial() {
198        log::set_max_level(log::LevelFilter::Debug);
199        let bytes_in = io::Cursor::new(&include_bytes!("../tests/rp2040/hello_serial.elf")[..]);
200        let mut bytes_out = Vec::new();
201        elf2uf2(bytes_in, &mut bytes_out, None, NoProgress).unwrap();
202
203        assert_eq!(bytes_out, include_bytes!("../tests/rp2040/hello_serial.uf2"));
204    }
205}