Introduce Rust FSBL
Some checks failed
ci / Check build (push) Has been cancelled
ci / Check formatting (push) Has been cancelled
ci / Check Documentation Build (push) Has been cancelled
ci / Clippy (push) Has been cancelled
ci / Check build (pull_request) Has been cancelled
ci / Check formatting (pull_request) Has been cancelled
ci / Check Documentation Build (pull_request) Has been cancelled
ci / Clippy (pull_request) Has been cancelled
Some checks failed
ci / Check build (push) Has been cancelled
ci / Check formatting (push) Has been cancelled
ci / Check Documentation Build (push) Has been cancelled
ci / Clippy (push) Has been cancelled
ci / Check build (pull_request) Has been cancelled
ci / Check formatting (pull_request) Has been cancelled
ci / Check Documentation Build (pull_request) Has been cancelled
ci / Clippy (pull_request) Has been cancelled
This PR introduces some major features while also changing the project structure to be more flexible for multiple platforms (e.g. host tooling). It also includes a lot of bugfixes, renamings for consistency purposes and dependency updates. Added features: 1. Pure Rust FSBL for the Zedboard. This first variant is simplistic. It is currently only capable of QSPI boot. It searches for a bitstream and ELF file inside the boot binary, flashes them and jumps to them. 2. QSPI flasher for the Zedboard. 3. DDR, QSPI, DEVC, private CPU timer and PLL configuration modules 3. Tooling to auto-generate board specific DDR and DDRIOB config parameters from the vendor provided ps7init.tcl file Changed project structure: 1. All target specific project are inside a dedicated workspace inside the `zynq` folder now. 2. All tool intended to be run on a host are inside a `tools` workspace 3. All other common projects are at the project root Major bugfixes: 1. SPI module: CPOL was not configured properly 2. Logger flush implementation was empty, implemented properly now.
This commit is contained in:
1
zynq7000-boot-image/.gitignore
vendored
Normal file
1
zynq7000-boot-image/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/Cargo.lock
|
||||
9
zynq7000-boot-image/Cargo.toml
Normal file
9
zynq7000-boot-image/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "zynq7000-boot-image"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2", default-features = false }
|
||||
arbitrary-int = "2"
|
||||
bitbybit = "1.4"
|
||||
437
zynq7000-boot-image/src/lib.rs
Normal file
437
zynq7000-boot-image/src/lib.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
#![no_std]
|
||||
|
||||
use core::str::Utf8Error;
|
||||
|
||||
/// ASCII 'XLNX'
|
||||
pub const IMAGE_ID_U32: u32 = 0x584C4E58;
|
||||
|
||||
/// This is the fixed size of the boot header.
|
||||
pub const FIXED_BOOT_HEADER_SIZE: usize = 0xA0;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum InvalidBootHeader {
|
||||
#[error("image ID invalid")]
|
||||
ImageIdInvalid,
|
||||
#[error("checksum is invalid")]
|
||||
ChecksumInvalid,
|
||||
#[error("provided data slice too small")]
|
||||
DataTooSmall,
|
||||
}
|
||||
|
||||
pub struct BootHeader<'a> {
|
||||
//base_addr: usize,
|
||||
// Snapshot of the boot header and following data (at least fixed header size)
|
||||
data: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> BootHeader<'a> {
|
||||
pub const FIXED_SIZED_PART: usize = FIXED_BOOT_HEADER_SIZE;
|
||||
|
||||
/// Create a new boot header parser structure without performing any additional checks.
|
||||
pub const fn new_unchecked(data: &'a [u8]) -> Self {
|
||||
BootHeader { data }
|
||||
}
|
||||
|
||||
/// Create a new boot header parser structure while also performing any additonal checks.
|
||||
///
|
||||
/// The passed buffer must have a minimal size of [Self::FIXED_SIZED_PART].
|
||||
/// This constructor calls [Self::check_image_id_validity] and [Self::verify_header_checksum]
|
||||
/// to verify whether the boot header structure is actually valid.
|
||||
pub fn new(data: &'a [u8]) -> Result<Self, InvalidBootHeader> {
|
||||
if data.len() < Self::FIXED_SIZED_PART {
|
||||
return Err(InvalidBootHeader::DataTooSmall);
|
||||
}
|
||||
let header = BootHeader { data };
|
||||
if !header.check_image_id_validity() {
|
||||
return Err(InvalidBootHeader::ImageIdInvalid);
|
||||
}
|
||||
if !header.verify_header_checksum() {
|
||||
return Err(InvalidBootHeader::ChecksumInvalid);
|
||||
}
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u32_le(&self, offset: usize) -> u32 {
|
||||
let bytes = &self.data[offset..offset + 4];
|
||||
u32::from_le_bytes(bytes.try_into().unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn image_id(&self) -> u32 {
|
||||
self.read_u32_le(0x24)
|
||||
}
|
||||
|
||||
/// Check whether the image ID has the mandatory [IMAGE_ID_U32] value.
|
||||
#[inline]
|
||||
pub fn check_image_id_validity(&self) -> bool {
|
||||
self.image_id() == IMAGE_ID_U32
|
||||
}
|
||||
|
||||
/// Offset to the FSBL image in bytes. This information can be used to only extract the boot
|
||||
/// binary metadata (everything except actual partition data).
|
||||
#[inline]
|
||||
pub fn source_offset(&self) -> usize {
|
||||
self.read_u32_le(0x30) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn header_checksum(&self) -> u32 {
|
||||
self.read_u32_le(0x48)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn image_header_table_offset(&self) -> usize {
|
||||
self.read_u32_le(0x98) as usize
|
||||
}
|
||||
|
||||
pub fn image_header_table(&self) -> Option<ImageHeaderTable<'a>> {
|
||||
let offset = self.image_header_table_offset();
|
||||
if offset + ImageHeaderTable::SIZE > self.data.len() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
ImageHeaderTable::new(
|
||||
&self.data[self.image_header_table_offset()
|
||||
..self.image_header_table_offset() + ImageHeaderTable::SIZE],
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn image_header_iterator(&self) -> Option<ImageHeaderIterator<'a>> {
|
||||
let first_header_offset = self.image_header_table()?.first_image_header_offset()?;
|
||||
ImageHeaderIterator::new(self.data, first_header_offset)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn partition_header_table_offset(&self) -> usize {
|
||||
self.read_u32_le(0x9C) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn verify_header_checksum(&self) -> bool {
|
||||
let checksum = self.header_checksum();
|
||||
let mut sum = 0u32;
|
||||
let mut ofs = 0x20;
|
||||
while ofs < 0x48 {
|
||||
sum = sum.wrapping_add(self.read_u32_le(ofs));
|
||||
ofs += 4;
|
||||
}
|
||||
!sum == checksum
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageHeaderTable<'a> {
|
||||
data: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> ImageHeaderTable<'a> {
|
||||
pub const SIZE: usize = 0x18;
|
||||
|
||||
pub const fn new(data: &'a [u8]) -> Option<Self> {
|
||||
if data.len() != Self::SIZE {
|
||||
return None;
|
||||
}
|
||||
Some(ImageHeaderTable { data })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u32_le(&self, offset: usize) -> u32 {
|
||||
let bytes = &self.data[offset..offset + 4];
|
||||
u32::from_le_bytes(bytes.try_into().unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn count_of_headers(&self) -> usize {
|
||||
self.read_u32_le(0x04) as usize
|
||||
}
|
||||
|
||||
/// Returns [None] if the number of words times 4 exeeds [u32::MAX].
|
||||
#[inline]
|
||||
pub fn first_image_header_offset(&self) -> Option<usize> {
|
||||
self.read_u32_le(0x0C).checked_mul(4).map(|v| v as usize)
|
||||
}
|
||||
|
||||
/// Returns [None] if the number of words times 4 exeeds [u32::MAX].
|
||||
#[inline]
|
||||
pub fn first_partition_header_offset(&self) -> Option<usize> {
|
||||
self.read_u32_le(0x08).checked_mul(4).map(|v| v as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageHeaderIterator<'a> {
|
||||
data: &'a [u8],
|
||||
current_header_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> ImageHeaderIterator<'a> {
|
||||
#[inline]
|
||||
pub const fn new(data: &'a [u8], first_header_offset: usize) -> Option<Self> {
|
||||
if first_header_offset + ImageHeader::MIN_SIZE > data.len() {
|
||||
return None;
|
||||
}
|
||||
Some(ImageHeaderIterator {
|
||||
data,
|
||||
current_header_offset: first_header_offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for ImageHeaderIterator<'a> {
|
||||
type Item = ImageHeader<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_header_offset == 0 {
|
||||
return None;
|
||||
}
|
||||
let next_image_header = ImageHeader::new(&self.data[self.current_header_offset..])?;
|
||||
self.current_header_offset = next_image_header.next_image_header_offset()?;
|
||||
Some(next_image_header)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageHeader<'a> {
|
||||
header_data: &'a [u8],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("buffer too small")]
|
||||
pub struct BufferTooSmallError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum NameParsingError {
|
||||
#[error("ut8 error")]
|
||||
Utf8(#[from] Utf8Error),
|
||||
#[error("buffer too small")]
|
||||
BufferTooSmall(#[from] BufferTooSmallError),
|
||||
}
|
||||
|
||||
impl<'a> ImageHeader<'a> {
|
||||
pub const MIN_SIZE: usize = 0x1C;
|
||||
|
||||
#[inline]
|
||||
pub fn new(data: &'a [u8]) -> Option<Self> {
|
||||
if data.len() < Self::MIN_SIZE {
|
||||
return None;
|
||||
}
|
||||
let mut current_offset = 0x14;
|
||||
let mut prev_word =
|
||||
u32::from_le_bytes(data[current_offset..current_offset + 4].try_into().unwrap());
|
||||
current_offset += 4;
|
||||
// TODO: Upper bound.
|
||||
loop {
|
||||
if current_offset + 4 > data.len() {
|
||||
return None;
|
||||
}
|
||||
let current_word =
|
||||
u32::from_le_bytes(data[current_offset..current_offset + 4].try_into().unwrap());
|
||||
current_offset += 4;
|
||||
if current_word == 0xffff_ffff || prev_word == 0x0000_0000 {
|
||||
break;
|
||||
}
|
||||
prev_word = current_word;
|
||||
}
|
||||
Some(ImageHeader {
|
||||
header_data: &data[0..current_offset],
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.header_data.len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u32_le(&self, offset: usize) -> u32 {
|
||||
let bytes = &self.header_data[offset..offset + 4];
|
||||
u32::from_le_bytes(bytes.try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn partition_header_iterator(&self, data: &'a [u8]) -> Option<PartitionHeaderIterator<'a>> {
|
||||
let first_partition_header = self.first_partition_header_offset()?;
|
||||
Some(PartitionHeaderIterator {
|
||||
data,
|
||||
current_partition_header_addr: first_partition_header,
|
||||
current_partition_index: 0,
|
||||
num_of_partitions: self.partition_count(),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn next_image_header_offset(&self) -> Option<usize> {
|
||||
self.read_u32_le(0x00).checked_mul(4).map(|v| v as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn partition_count(&self) -> usize {
|
||||
self.read_u32_le(0x0C) as usize
|
||||
}
|
||||
|
||||
pub fn first_partition_header_offset(&self) -> Option<usize> {
|
||||
self.read_u32_le(0x04).checked_mul(4).map(|v| v as usize)
|
||||
}
|
||||
|
||||
pub fn image_name_copied(&self, buf: &mut [u8]) -> Result<usize, BufferTooSmallError> {
|
||||
let mut current_offset = 0x10;
|
||||
let mut current_buf_idx = 0;
|
||||
let mut null_byte_found = false;
|
||||
loop {
|
||||
let next_bytes = &self.header_data[current_offset..current_offset + 4];
|
||||
for &byte in next_bytes.iter().rev() {
|
||||
if byte == 0 {
|
||||
null_byte_found = true;
|
||||
break;
|
||||
}
|
||||
if current_buf_idx >= buf.len() {
|
||||
return Err(BufferTooSmallError);
|
||||
}
|
||||
buf[current_buf_idx] = byte;
|
||||
current_buf_idx += 1;
|
||||
}
|
||||
if null_byte_found {
|
||||
break;
|
||||
}
|
||||
current_offset += 4;
|
||||
}
|
||||
Ok(current_buf_idx)
|
||||
}
|
||||
|
||||
pub fn image_name<'b>(&self, buf: &'b mut [u8]) -> Result<&'b str, NameParsingError> {
|
||||
let name_len = self.image_name_copied(buf)?;
|
||||
core::str::from_utf8(&buf[0..name_len]).map_err(NameParsingError::from)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PartitionHeaderIterator<'a> {
|
||||
data: &'a [u8],
|
||||
current_partition_header_addr: usize,
|
||||
current_partition_index: usize,
|
||||
num_of_partitions: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PartitionHeaderIterator<'a> {
|
||||
type Item = PartitionHeader<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_partition_index >= self.num_of_partitions {
|
||||
return None;
|
||||
}
|
||||
if self.current_partition_header_addr + PartitionHeader::SIZE > self.data.len() {
|
||||
return None;
|
||||
}
|
||||
let header = PartitionHeader::new(
|
||||
&self.data[self.current_partition_header_addr
|
||||
..self.current_partition_header_addr + PartitionHeader::SIZE],
|
||||
)
|
||||
.ok()?;
|
||||
self.current_partition_index += 1;
|
||||
self.current_partition_header_addr += 0x40;
|
||||
Some(header)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PartitionHeader<'a> {
|
||||
header_data: &'a [u8],
|
||||
}
|
||||
|
||||
#[bitbybit::bitenum(u2, exhaustive = false)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PartitionOwner {
|
||||
Fsbl = 0,
|
||||
Uboot = 1,
|
||||
}
|
||||
|
||||
#[bitbybit::bitenum(u3, exhaustive = false)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ChecksumType {
|
||||
None = 0,
|
||||
Md5 = 1,
|
||||
}
|
||||
|
||||
#[bitbybit::bitenum(u4, exhaustive = false)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum DestinationDevice {
|
||||
None = 0,
|
||||
Ps = 1,
|
||||
Pl = 2,
|
||||
Int = 3,
|
||||
}
|
||||
|
||||
#[bitbybit::bitfield(u32, debug)]
|
||||
pub struct SectionAttributes {
|
||||
#[bits(16..=17, rw)]
|
||||
partition_owner: Option<PartitionOwner>,
|
||||
#[bit(15, rw)]
|
||||
rsa_signature_present: bool,
|
||||
#[bits(12..=14, rw)]
|
||||
checksum_type: Option<ChecksumType>,
|
||||
#[bits(4..=7, rw)]
|
||||
destination_device: Option<DestinationDevice>,
|
||||
}
|
||||
|
||||
impl<'a> PartitionHeader<'a> {
|
||||
pub const SIZE: usize = 0x40;
|
||||
|
||||
// TODO: Checksum check.
|
||||
#[inline]
|
||||
pub const fn new(header_data: &'a [u8]) -> Result<Self, BufferTooSmallError> {
|
||||
if header_data.len() < Self::SIZE {
|
||||
return Err(BufferTooSmallError);
|
||||
}
|
||||
Ok(PartitionHeader { header_data })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u32_le(&self, offset: usize) -> u32 {
|
||||
let bytes = &self.header_data[offset..offset + 4];
|
||||
u32::from_le_bytes(bytes.try_into().unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn encrypted_partition_length(&self) -> u32 {
|
||||
self.read_u32_le(0x00)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn unencrypted_partition_length(&self) -> u32 {
|
||||
self.read_u32_le(0x04)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn total_partition_length(&self) -> Option<usize> {
|
||||
self.read_u32_le(0x08).checked_mul(4).map(|v| v as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn destination_load_address(&self) -> u32 {
|
||||
self.read_u32_le(0x0C)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn destination_exec_address(&self) -> u32 {
|
||||
self.read_u32_le(0x10)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn section_attributes(&self) -> SectionAttributes {
|
||||
SectionAttributes::new_with_raw_value(self.section_attributes_raw())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn section_attributes_raw(&self) -> u32 {
|
||||
self.read_u32_le(0x18)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn section_count(&self) -> usize {
|
||||
self.read_u32_le(0x1C) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn data_offset(&self) -> Option<usize> {
|
||||
self.read_u32_le(0x14).checked_mul(4).map(|v| v as usize)
|
||||
}
|
||||
}
|
||||
4
zynq7000-boot-image/staging/.gitignore
vendored
Normal file
4
zynq7000-boot-image/staging/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/fsbl.elf
|
||||
/fpga.bit
|
||||
/application.elf
|
||||
/boot.bin
|
||||
28
zynq7000-boot-image/staging/README.md
Normal file
28
zynq7000-boot-image/staging/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
Boot Image Creation Staging
|
||||
========
|
||||
|
||||
This folder provides the basic files required to create bare metal boot images.
|
||||
|
||||
This includes a simple `boot.bif` file which can be used by the AMD
|
||||
[bootgen](https://docs.amd.com/r/en-US/ug1283-bootgen-user-guide) utility
|
||||
to create a boot binary consisting of
|
||||
|
||||
- A first-stage bootloader (FSBL)
|
||||
- A FPGA bitstream which can be loaded to the Zynq PL by the FSBL.
|
||||
- A primary application which runs in DDR memory and is also copied to DDR by the FSBL.
|
||||
|
||||
## Example for the Zedboard
|
||||
|
||||
An example use-case for the Zedboard would be a `boot.bin` containing the `zedboard-fsbl` Rust
|
||||
FSBL or the Xilinx FSBL, a bitstream `zedboard-rust.bit` generated from the `zedboard-fpga-design`
|
||||
project or your own Vivado project and finally any primary application of your choice which
|
||||
should run in DDR.
|
||||
|
||||
You can copy the files into the staging area, using the file names `fsbl.elf`, `fpga.bit` and
|
||||
`application.elf`, which are also ignored by the VCS.
|
||||
|
||||
Then, you can simply run `bootgen` to generate the boot binary:
|
||||
|
||||
```sh
|
||||
bootgen -arch zynq -image boot.bif -o boot.bin -w on
|
||||
```
|
||||
6
zynq7000-boot-image/staging/boot.bif
Normal file
6
zynq7000-boot-image/staging/boot.bif
Normal file
@@ -0,0 +1,6 @@
|
||||
all:
|
||||
{
|
||||
[bootloader] fsbl.elf
|
||||
fpga.bit
|
||||
application.elf
|
||||
}
|
||||
Reference in New Issue
Block a user