Skip to content

Commit cf4d0df

Browse files
NanaHighluojia65
andcommitted
feat(all): add runtime, firmware converter, and PBP blinky example
- Implement GPIO HAL with output support and embedded-hal traits. - Add PBP runtime with entry macro and startup code. - Create firmware converter (aicfwc) for PBP and image packing. - Add PBP blinky example demonstrating GPIO output on PA5. - Update CI workflows for comprehensive testing and coverage. Signed-off-by: Chongbing Yu <[email protected]> Co-authored-by: Zhouqi Jiang <[email protected]>
1 parent 1d4baf0 commit cf4d0df

File tree

24 files changed

+1465
-12
lines changed

24 files changed

+1465
-12
lines changed

.github/workflows/Cargo.yml

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,36 @@ jobs:
1818
- name: Run cargo fmt
1919
run: cargo fmt --all -- --check
2020

21-
test:
22-
name: Run Tests
21+
test-hal:
22+
name: Test artinchip-hal
2323
needs: fmt
2424
runs-on: ubuntu-latest
2525
strategy:
2626
matrix:
27-
PACKAGE: [artinchip-rt, artinchip-hal]
2827
FEATURES: [d12x, d13x, d21x, g73x, m6800]
2928
steps:
3029
- uses: actions/checkout@v4
3130
- uses: actions-rust-lang/setup-rust-toolchain@v1
3231
with:
3332
toolchain: stable
3433
- name: Run cargo test
35-
run: cargo test -p ${{ matrix.PACKAGE }} --features ${{ matrix.FEATURES }}
34+
run: cargo test -p artinchip-hal --features ${{ matrix.FEATURES }}
3635

37-
test-coverage:
38-
name: Test Coverage
39-
needs: [fmt, test]
36+
test-aicfwc:
37+
name: Test aicfwc
38+
needs: fmt
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
- uses: actions-rust-lang/setup-rust-toolchain@v1
43+
with:
44+
toolchain: stable
45+
- name: Run cargo test
46+
run: cargo test -p aicfwc --all-features
47+
48+
test-hal-coverage:
49+
name: Test artinchip-hal Coverage
50+
needs: fmt
4051
runs-on: ubuntu-latest
4152
steps:
4253
- uses: actions/checkout@v4
@@ -46,7 +57,7 @@ jobs:
4657
- name: Install cargo-tarpaulin
4758
run: cargo install cargo-tarpaulin
4859
- name: Generate code coverage
49-
run: cargo tarpaulin --engine llvm --all-features --workspace --out Html --output-dir ./coverage-report
60+
run: cargo tarpaulin --engine llvm -p artinchip-hal --all-features --out Html --output-dir ./coverage-report
5061
- name: Upload coverage report
5162
uses: actions/upload-artifact@v4
5263
with:
@@ -55,13 +66,33 @@ jobs:
5566

5667
clippy:
5768
name: Clippy Check
58-
needs: [fmt, test]
69+
needs: fmt
5970
runs-on: ubuntu-latest
71+
strategy:
72+
matrix:
73+
PACKAGE: [artinchip-hal, artinchip-rt, aicfwc]
6074
steps:
6175
- uses: actions/checkout@v4
6276
- uses: actions-rust-lang/setup-rust-toolchain@v1
6377
with:
6478
toolchain: stable
6579
components: clippy
6680
- name: Run cargo clippy
67-
run: cargo clippy --all-targets --all-features -- -D warnings
81+
run: cargo clippy -p ${{ matrix.PACKAGE }} --all-targets --all-features -- -D warnings
82+
83+
build-rv32-examples:
84+
name: Build Examples for RV32
85+
needs: fmt
86+
runs-on: ubuntu-latest
87+
strategy:
88+
matrix:
89+
FEATURES: [d13x]
90+
EXAMPLES: [pbp-blinky]
91+
steps:
92+
- uses: actions/checkout@v4
93+
- uses: actions-rust-lang/setup-rust-toolchain@v1
94+
with:
95+
toolchain: nightly
96+
target: riscv32imac-unknown-none-elf
97+
- name: Build examples
98+
run: cargo build -p ${{ matrix.EXAMPLES }} --target riscv32imac-unknown-none-elf --release --features ${{ matrix.FEATURES }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/target
22
/Cargo.lock
3+
/coverage-report/
34
.vscode
4-
.idea
5+
.idea

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
members = [
33
"artinchip-rt",
44
"artinchip-hal",
5+
"aicfwc",
6+
"examples/pbp/pbp-blinky",
57
]
68
resolver = "3"

aicfwc/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "aicfwc"
3+
description = "Artinchip firmware converter"
4+
version = "0.0.0"
5+
edition = "2024"
6+
authors = [
7+
"Zhouqi Jiang <[email protected]>",
8+
"Chongbing Yu <[email protected]>",
9+
]
10+
11+
[dependencies]
12+
clap = { version = "4", features = ["derive"] }
13+
anyhow = "1"
14+
md-5 = "0.10.6"

aicfwc/src/main.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use anyhow::{Context, Result, bail};
2+
use clap::{ArgAction, Parser};
3+
use md5::{Digest, Md5};
4+
use std::{fs, io::Write, path::PathBuf};
5+
6+
/// AIC firmware converter.
7+
#[derive(Parser, Debug)]
8+
#[command(author, version, about)]
9+
struct Cli {
10+
/// The input binary file.
11+
input: PathBuf,
12+
13+
/// Output as PBP (Pre-Boot Program) format.
14+
///
15+
/// PBP format:
16+
/// [0..=3] = b"PBP ",
17+
/// [4..=7] = checksum (u32 LE),
18+
/// [8..end] = original binary 4-byte aligned with zero paddings.
19+
#[arg(long = "pbp", action = ArgAction::SetTrue)]
20+
pbp: bool,
21+
22+
/// Output file path.
23+
#[arg(short = 'o', long = "output")]
24+
output: PathBuf,
25+
}
26+
27+
fn main() -> Result<()> {
28+
let cli = Cli::parse();
29+
30+
// Read the input binary file
31+
let bin_data = fs::read(&cli.input)
32+
.with_context(|| format!("Failed to read input file {:?}", cli.input))?;
33+
34+
// Currently only supports -pbp mode; error if not specified
35+
if !cli.pbp {
36+
bail!("Currently only supports -pbp preprocessing, please add -pbp flag");
37+
}
38+
39+
let pbp_bytes = build_pbp(&bin_data)?;
40+
41+
// Write the PBP file
42+
let mut f = fs::File::create(&cli.output)
43+
.with_context(|| format!("Failed to create output file {:?}", cli.output))?;
44+
f.write_all(&pbp_bytes)
45+
.with_context(|| format!("Failed to write output file {:?}", cli.output))?;
46+
47+
// Pack into full image format and write .pk_pbp file
48+
let image_bytes = pack_pbp(&pbp_bytes)?;
49+
let pk_pbp_path = cli.output.with_extension("pk_pbp");
50+
let mut f_pk = fs::File::create(&pk_pbp_path)
51+
.with_context(|| format!("Failed to create .pk_pbp file {pk_pbp_path:?}"))?;
52+
f_pk.write_all(&image_bytes)
53+
.with_context(|| format!("Failed to write .pk_pbp file {pk_pbp_path:?}"))?;
54+
55+
Ok(())
56+
}
57+
58+
/// Build PBP format:
59+
/// 0..=3: "PBP "
60+
/// 4..=7: checksum (written back later)
61+
/// 8..end: bin content + zero padding to 4-byte alignment
62+
///
63+
/// Checksum calculation:
64+
/// Sum all words of the final PBP file as u32 little-endian (mod 2^32),
65+
/// requiring the final sum == 0x0fffffff.
66+
/// First calculate partial_sum with checksum position as 0,
67+
/// then derive the checksum.
68+
fn build_pbp(bin: &[u8]) -> Result<Vec<u8>> {
69+
// 1. Align bin to 4 bytes
70+
let mut aligned = bin.to_vec();
71+
while !aligned.len().is_multiple_of(4) {
72+
aligned.push(0);
73+
}
74+
75+
// 2. Assemble a placeholder PBP: magic + checksum placeholder + data
76+
let total_len = aligned.len();
77+
let mut out = Vec::with_capacity(total_len);
78+
79+
// Data
80+
out.extend_from_slice(&aligned);
81+
82+
// 3. Calculate the current 32-bit sum (with placeholder checksum=0)
83+
let mut sum: u64 = 0;
84+
for chunk in out.chunks_exact(4) {
85+
let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
86+
sum = (sum + word as u64) & 0xffff_ffff;
87+
}
88+
89+
// 4. Derive the actual checksum
90+
let target: u32 = 0xffff_ffff;
91+
let checksum = target.wrapping_sub(sum as u32);
92+
93+
// 5. Write back checksum (little-endian)
94+
out[4..8].copy_from_slice(&checksum.to_le_bytes());
95+
96+
// 6. Optional: Assert correctness (for debug)
97+
debug_assert!(verify_checksum(&out, target));
98+
99+
Ok(out)
100+
}
101+
102+
/// Verify if the 32-bit sum of the final file is the target
103+
fn verify_checksum(buf: &[u8], target: u32) -> bool {
104+
if !buf.len().is_multiple_of(4) {
105+
return false;
106+
}
107+
let mut sum: u64 = 0;
108+
for chunk in buf.chunks_exact(4) {
109+
let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
110+
sum = (sum + word as u64) & 0xffff_ffff;
111+
}
112+
(sum as u32) == target
113+
}
114+
115+
/// Pack PBP data into a complete Artinchip boot image format
116+
/// Format: HEAD1 + HEAD2 + DATA1 (PBP) + DATA2 (empty) + SIGN (MD5)
117+
fn pack_pbp(pbp_data: &[u8]) -> Result<Vec<u8>> {
118+
// Constants
119+
const HEAD1_SIZE: usize = 8;
120+
const HEAD2_SIZE: usize = 248;
121+
const SIGN_SIZE: usize = 16; // MD5
122+
const ALIGNMENT: usize = 256;
123+
124+
// Calculate sizes
125+
let pbp_total_len = pbp_data.len(); // PBP header + content
126+
let data1_len = pbp_total_len.div_ceil(ALIGNMENT) * ALIGNMENT; // Align to 256 bytes
127+
let data2_len = 0; // TODO: Implement DATA2
128+
let signed_area_len = HEAD1_SIZE + HEAD2_SIZE + data1_len + data2_len;
129+
let total_len = signed_area_len + SIGN_SIZE;
130+
131+
// Build HEAD1
132+
let mut head1 = vec![0u8; HEAD1_SIZE];
133+
head1[0..4].copy_from_slice(b"AIC "); // Magic
134+
// Checksum will be calculated later
135+
136+
// Build HEAD2
137+
let mut head2 = vec![0u8; HEAD2_SIZE];
138+
// Header version: 0x00010001
139+
head2[0..4].copy_from_slice(&0x00010001u32.to_le_bytes());
140+
// Image length
141+
head2[4..8].copy_from_slice(&(total_len as u32).to_le_bytes());
142+
// Firmware version: 0.0.0 (anti_rollback=0, revision=0, minor=0, major=0)
143+
// Already 0
144+
// Loader length: 0
145+
// Load address: 0
146+
// Entry point: 0
147+
// Sign algo: 0 (no signature)
148+
// Enc algo: 0
149+
// Sign result offset
150+
head2[32..36].copy_from_slice(&(signed_area_len as u32).to_le_bytes());
151+
// Sign result length: 16
152+
head2[36..40].copy_from_slice(&16u32.to_le_bytes());
153+
// Other offsets: 0
154+
// PBP offset: 256
155+
head2[64..68].copy_from_slice(&256u32.to_le_bytes());
156+
// PBP length: pbp_data.len()
157+
head2[68..72].copy_from_slice(&(pbp_data.len() as u32).to_le_bytes());
158+
// Padding: already 0
159+
160+
// Build DATA1: PBP + padding
161+
let mut data1 = vec![0u8; data1_len];
162+
data1[0..pbp_data.len()].copy_from_slice(pbp_data);
163+
164+
// TODO DATA2: empty
165+
166+
// SIGN: MD5 of HEAD2 + DATA1 + DATA2
167+
let signed_data = [head2.as_slice(), data1.as_slice()].concat();
168+
let mut hasher = Md5::new();
169+
hasher.update(&signed_data);
170+
let sign = hasher.finalize().to_vec();
171+
172+
let signed_area = [head1.as_slice(), head2.as_slice(), data1.as_slice()].concat();
173+
174+
let mut result = vec![0u8; total_len];
175+
result[0..signed_area_len].copy_from_slice(&signed_area);
176+
result[signed_area_len..].copy_from_slice(&sign);
177+
178+
// Calculate HEAD1 checksum
179+
let mut sum: u64 = 0;
180+
for chunk in result.chunks_exact(4) {
181+
let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
182+
sum = (sum + word as u64) & 0xffff_ffff;
183+
}
184+
185+
let target: u32 = 0xffff_ffff;
186+
let head1_checksum = target.wrapping_sub(sum as u32);
187+
188+
result[4..8].copy_from_slice(&head1_checksum.to_le_bytes());
189+
190+
Ok(result)
191+
}

artinchip-hal/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
name = "artinchip-hal"
33
version = "0.0.0"
44
edition = "2024"
5+
authors = [
6+
"Zhouqi Jiang <[email protected]>",
7+
"Chongbing Yu <[email protected]>",
8+
]
59

610
[dependencies]
711
volatile-register = "0.2.2"
812
uart16550 = "0.0.1"
13+
embedded-hal = "1.0.0"
914

1015
[features]
1116
d12x = []

artinchip-hal/src/gpio.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
//! General Purpose Input Output (GPIO).
22
3+
mod output;
4+
mod pad_ext;
35
mod register;
6+
mod set_mode;
47

8+
pub use output::Output;
9+
pub use pad_ext::PadExt;
510
pub use register::*;

0 commit comments

Comments
 (0)