diff --git a/CLAUDE.md b/CLAUDE.md index 89fb0f8..7354202 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ cargo run --example multitask_rp_pico --features rp2040 --target thumbv6m-none-e A default target (`thumbv7em-none-eabihf`) and per-target `runner` (probe-run / gdb+openocd / elf2uf2-rs) are set in `.cargo/config.toml`; `cargo run` uses the runner to flash. OpenOCD configs and GDB scripts live in `debug//`. Chip HALs are mostly crates, but `gd32vf103xx-hal` is a local path dep at `hal2/gd32vf103xx-hal`. -Chip features: `gd32vf103`, `stm32f1`, `stm32f4`, `stm32h7`, `cm32m4`, `rp2040`, `ch32v103`/`ch32v203`/`ch32v307`, `esp32c3`, plus `qemu_riscv` (QEMU virt; **execution-verified** — `ci/gate.sh` runs it; others build-verified 2026-08-23; real-board verification pending for f4/f1 constants, h7 timeline, cm32m4/rp2040). Non-chip features: `timer` (software timers), `tlsf` (global allocator backend swap to the hand-written mini TLSF, ch28), `debug_task`, `fs` (fatfs), `net` (smoltcp), `usb`, `ble`, `rtt_log` / `stdout_log`, board BSPs (`longan_nano`, `bluepill`, `greenpill`, `rp_pico`). +Chip features: `gd32vf103`, `stm32f1`, `stm32f4`, `stm32h7`, `cm32m4`, `rp2040`, `ch32v103`/`ch32v203`/`ch32v307`, `ch583` (CH58x / QingKe V4A; build-verified 2026-09-19, ROM boot header checked by `ci/check_ch583_boot.py`), `esp32c3`, plus `qemu_riscv` (QEMU virt; **execution-verified** — `ci/gate.sh` runs it; others build-verified 2026-08-23; real-board verification pending for f4/f1 constants, h7 timeline, cm32m4/rp2040). Non-chip features: `timer` (software timers), `tlsf` (global allocator backend swap to the hand-written mini TLSF, ch28), `debug_task`, `fs` (fatfs), `net` (smoltcp), `usb`, `ble`, `rtt_log` / `stdout_log`, board BSPs (`longan_nano`, `bluepill`, `greenpill`, `rp_pico`). ## Architecture diff --git a/Cargo.toml b/Cargo.toml index 1ce0c71..ef223e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,8 @@ qemu_riscv = ["riscv-rt"] # 零 PAC:标准 CLINT/UART 直址 qemu_arm_r52 = [] # 零 PAC、零运行时:R5 无 cortex-r-rt,启动/链接脚本自备(chip 内) ch32v203 = ["ch32v2", "riscv-rt"] ch32v307 = ["ch32v3", "riscv-rt"] +# CH583(QingKe V4A):CH58x 无现成 ch32-rs PAC,零 PAC 直址访问(见 chip/ch583/mod.rs) +ch583 = ["riscv-rt"] #ble 蓝牙(第 24 章):协议栈在 E104-BT5032A 模组固件内——我们的活是 #AT 命令编解码 + GATT 服务建模,纯 Rust 零新依赖 ble=[] diff --git a/README.md b/README.md index 1899297..3bfdb52 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,10 @@ ### 验证体系 -- [x] 宿主回归:`cargo test --lib` 默认 features 实测 **80 passed**;门禁全特征组合(见 `ci/gate.sh`,含 fs/net/usb/ble)实测 **159 passed**(2026-09-03,rustc 1.97;旧计数 148 已按实测修正,历史见 issue #11;计数随新增测试浮动,以 `ci/gate.sh` 实测为准) +- [x] 宿主回归:`cargo test --lib` 默认 features 实测 **82 passed**(2026-09-19 实测,旧记 80 已修正);门禁全特征组合(见 `ci/gate.sh`,含 fs/net/usb/ble)实测 **159 passed**(2026-09-03,rustc 1.97;旧计数 148 已按实测修正,历史见 issue #11;计数随新增测试浮动,以 `ci/gate.sh` 实测为准) - [x] QEMU 执行级:`ci/gate.sh` 在 virt 机真跑内核——`qemu_pingpong` 200 轮乒乓 + `qemu_kernel_tests` 24 项全内核机制自测(抢占/时间片/阻塞类 IPC/定时器/时基/堆/总线/任务回收/可重入锁/优先级继承/完整 PI 多锁/PCP 天花板阻塞/PI 交叉持锁死锁确认/TLSF 碎片共限/TLSF 分配确定性/tickless 错峰唤醒/远期期限单次到点/UART RX 外部中断冻眠唤醒/早醒弹墙钟拍账/噪声风暴停留 idle 不漂移),全绿自退出;另有 tlsf 全局后端门禁(24/24 不变 = 分配器换引擎对内核透明)与 `qemu_smp` 9 项多核调度门禁 -- [ ] 真机验证:gd32vf103 已验;f4/f1 常数、h7 时序、cm32m4、rp2040、ch32 系、esp32c3 待上板 +- [x] 产物级:`ci/gate.sh` 另校验 `ch583`(QingKe V4A,无 QEMU 机器可跑)的 ROM 启动头——flash `0x00..0x17` 的入口跳转 + 向量表第 5 字 boot option `0xF3F9BDA9` @`0x14`(官方 `startup_CH583.S`/`Link.ld` 的位置);链接期 `ASSERT` 钉地址、`ci/check_ch583_boot.py` 钉产物字节,缺了它板上停在 ISP 而构建/门禁全绿 +- [ ] 真机验证:gd32vf103 已验;f4/f1 常数、h7 时序、cm32m4、rp2040、ch32 系、ch58x、esp32c3 待上板 ### 移植的芯片 @@ -39,6 +40,7 @@ - [x] CH32V3: ch32v307(QingKe V4F;构建级验证 2026-08-23,真机待验) - [x] CH32V2: ch32v203(QingKe V4B;构建级验证 2026-08-23,真机待验) - [x] CH32V1: ch32v103(QingKe V3A;构建级验证 2026-08-23,真机待验) + - [x] CH58x: ch583(WCH CH583,QingKe V4A;构建级验证 2026-09-19,真机待验——ROM 启动头已按官方 `.vector` 复刻:入口跳转 + boot option `0xF3F9BDA9` @flash 0x14,见 `ci/check_ch583_boot.py`) - CM7F - [x] STM32H7B0VBT6 - CM4F @@ -56,7 +58,7 @@ - **通道 A · 本机一条命令**(首次自动下载静态 QEMU 到 `.tools/`,约 90MB): ```bash - bash ci/gate.sh # host 回归 + 示例链接 + QEMU 执行级(24/24 等) + bash ci/gate.sh # host 回归 + 示例链接 + ch583 启动头校验 + QEMU 执行级(24/24 等) ``` - **通道 B · GitHub Codespaces**:仓库根有 `.devcontainer.json`,云端打开即跑,本机什么都不装 - **通道 C · 只看结果**:每次 push 的 CI 门禁(含 QEMU 执行级)全绿记录: diff --git a/build.rs b/build.rs index 91b8e17..16e018a 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,51 @@ use std::path::PathBuf; use std::{env, fs}; +/// 芯片接线守卫:`src/chip/<名字>/` 存在、且 `<名字>` feature 被启用时, +/// 该 feature 必须在 `src/port.rs` 的 `chip_portings!` 清单里登记。 +/// +/// 漏登记的后果不是编译错误而是**静默降级**:`Porting` 落到兜底桩 +/// `DefaultPorting`(各方法 `unimplemented!()`),构建与门禁全绿,直到运行期 +/// `xtask::start()` 才 panic(2026-09-18 ch583 骨架 PR 实测踩过,见 issue #14 / +/// PR #15)。这里用"目录 ⇄ feature ⇄ 登记表"三方交叉把它变成构建期失败。 +/// +/// 阳性对照:`Cargo.toml` 开 `--features ch583` 后删掉 `src/port.rs` 里 +/// `"ch583" => ...` 那一行,构建必须报本守卫(而不是编过)。 +fn check_chip_porting_wiring(manifest_dir: &str) { + let chip_dir = PathBuf::from(manifest_dir).join("src/chip"); + let port_rs = fs::read_to_string(PathBuf::from(manifest_dir).join("src/port.rs")) + .expect("读不到 src/port.rs(芯片接线守卫需要它)"); + let mut missing = Vec::new(); + for entry in fs::read_dir(&chip_dir).expect("读不到 src/chip/") { + let entry = entry.expect("src/chip/ 条目读取失败"); + if !entry.file_type().expect("条目类型读取失败").is_dir() { + continue; // env.rs / mod.rs 等文件跳过 + } + let name = entry.file_name().to_string_lossy().into_owned(); + // 该芯片 feature 是否启用(cargo 把启用的 feature 变成 CARGO_FEATURE_*) + let env_key = format!("CARGO_FEATURE_{}", name.to_uppercase().replace('-', "_")); + if env::var_os(&env_key).is_none() { + continue; + } + if !port_rs.contains(&format!("\"{name}\" =>")) { + missing.push(name); + } + } + if !missing.is_empty() { + panic!( + "芯片 feature {missing:?} 未在 src/port.rs 的 chip_portings! 清单登记 —— \ + Porting 会静默落到 DefaultPorting(unimplemented!),运行期才 panic" + ); + } +} + fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); println!("cargo:rustc-link-search={}", out_dir.display()); + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + check_chip_porting_wiring(&manifest_dir); + #[cfg(feature = "gd32vf103")] fs::copy("src/chip/gd32vf103/memory.x", out_dir.join("memory.x")).unwrap(); @@ -26,6 +67,8 @@ fn main() { fs::copy("src/chip/ch32v203/memory.x", out_dir.join("memory.x")).unwrap(); #[cfg(feature = "ch32v103")] fs::copy("src/chip/ch32v103/memory.x", out_dir.join("memory.x")).unwrap(); + #[cfg(feature = "ch583")] + fs::copy("src/chip/ch583/memory.x", out_dir.join("memory.x")).unwrap(); #[cfg(feature = "esp32c3")] fs::copy("src/chip/esp32c3/memory.x", out_dir.join("memory.x")).unwrap(); #[cfg(feature = "qemu_riscv")] @@ -40,6 +83,9 @@ fn main() { fs::copy("src/chip/cm32m4/memory.x", out_dir.join("memory.x")).unwrap(); // rerun-if-changed 指向真实被复制的源文件(指错路径时改 memory.x 不触发重建) + // src/port.rs 与 src/chip/* 供上面的接线守卫:改动必须触发本脚本重跑 + println!("cargo:rerun-if-changed=src/port.rs"); + println!("cargo:rerun-if-changed=src/chip"); println!("cargo:rerun-if-changed=src/chip/gd32vf103/memory.x"); println!("cargo:rerun-if-changed=src/chip/rp2040/memory.x"); println!("cargo:rerun-if-changed=src/chip/stm32f4/memory.x"); @@ -49,6 +95,7 @@ fn main() { println!("cargo:rerun-if-changed=src/chip/ch32v307/memory.x"); println!("cargo:rerun-if-changed=src/chip/ch32v203/memory.x"); println!("cargo:rerun-if-changed=src/chip/ch32v103/memory.x"); + println!("cargo:rerun-if-changed=src/chip/ch583/memory.x"); println!("cargo:rerun-if-changed=src/chip/esp32c3/memory.x"); println!("cargo:rerun-if-changed=src/chip/qemu_riscv/memory.x"); println!("cargo:rerun-if-changed=src/chip/qemu_arm_r52/memory.x"); diff --git a/ci/check_ch583_boot.py b/ci/check_ch583_boot.py new file mode 100644 index 0000000..bec2c4e --- /dev/null +++ b/ci/check_ch583_boot.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""校验 CH583 启动头字节布局(WCH ROM 认的硬要求)。 + +WCH ROM 从 flash 0x0 取指,并按"向量表第 5 个字 = boot option"识别有效用户 +程序:官方 EVT/EXAM/SRC/Startup/startup_CH583.S 的 `.vector` 段第 5 个字固定 +0xF3F9BDA9(注释 "boot option, can't modify"),官方 Ld/Link.ld 把 `.vector` +的 flash LMA 紧跟在 `.init` 的 `j handle_reset` 之后 → 魔数落在 flash 0x14。 + +本口用 `.bootvec` 段复刻同一布局(src/chip/ch583/port.S 内容 + memory.x 摆位, +另有链接期 ASSERT);本脚本做**产物级**终检:段地址/大小、入口跳转、魔数字节。 +少了它,板上表现是"停在 ISP、不进用户程序",而构建与门禁全绿。 + +用法: python3 ci/check_ch583_boot.py +""" +import struct +import sys + +MAGIC = 0xF3F9BDA9 +BOOT_LEN = 0x18 + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else \ + "target/riscv32imac-unknown-none-elf/release/examples/multitask_ch583" + data = open(path, "rb").read() + # 显式判断而非 assert:-O 下 assert 会被关掉,校验不能依赖它 + if len(data) < 0x34 or data[:4] != b"\x7fELF": + print("FAIL: 不是 ELF 文件") + return 1 + if data[4] != 1 or data[5] != 1: + print(f"FAIL: 期望 32 位小端 ELF,实测 class={data[4]} endian={data[5]}") + return 1 + + e_shoff = struct.unpack_from(" seg[1]: + print(f"FAIL: flash 0x{a:X} 超出 LOAD 段(0x{seg[1]:X} 字节)") + sys.exit(1) + return struct.unpack_from(" ! { + extern "C" { + /// 堆内存开始地址,在riscv-rt link.x文件里定义 + static _sheap: u8; + } + let start_addr = unsafe { &_sheap as *const u8 as usize }; + // 32K SRAM:静态(data 4.1K + bss 0.3K)后到 _stack_start 还有 ~27.6K。 + // 任务栈全部从堆上分配,实测(QEMU 探针:同一 32 位分配器/同一 Task 布局, + // 方法见 PR #15 评论——WCH 侧无法仿真,只能这样取数): + // 默认任务(256 字栈 + Task 112B + 名字)≈ 1.19K + // 软件定时器任务(1024 字,timer 特征)≈ 4.4K + // idle 任务(512 字)≈ 2.2K + // 12 个示例任务 + timer + idle ≈ 20.9K,故取 24K(堆顶 0x20007188, + // 距中断栈基 0x20007E00 还有 3.2K,首次上板余量约 3.6K)。 + // ⚠️ 不要再往本示例加任务:15 个任务时实测需 24.4K,24K 堆会在 start() + // 里 `panic!("memory out")`——会被误判成移植层的寄存器问题。 + xtask::init_heap(start_addr, 24 * 1024); + + //example_notify(); + //example_broadcast(); + example_semaphore(); + example_queue(); + example_task(); + xtask::start() +} + +fn example_notify() { + let notifier = Notifier::new(); + let waiter = notifier.clone(); + TaskBuilder::new().name("notifier").spawn(move || loop { + log::info!("发送通知信号 {}", xtask::tick()); + notifier.notify(); + xtask::sleep_ms(1000); + }); + TaskBuilder::new().name("waiter").spawn(move || loop { + waiter.wait(); + log::info!("收到通知信号 {}", xtask::tick()); + }); +} + +fn example_broadcast() { + let caster = Broadcast::new(); + let waiter1 = caster.clone(); + let waiter2 = caster.clone(); + let waiter3 = caster.clone(); + let waiter4 = caster.clone(); + let waiter5 = caster.clone(); + + TaskBuilder::new().name("caster").spawn(move || loop { + log::info!("发送广播信号 {}", xtask::tick()); + caster.notify(); + xtask::sleep_ms(1000); + }); + TaskBuilder::new().name("listener1").spawn(move || loop { + waiter1.wait(); + log::info!("1收到广播信号 {}", xtask::tick()); + }); + TaskBuilder::new().name("listener1").spawn(move || loop { + waiter2.wait(); + log::info!("2收到广播信号 {}", xtask::tick()); + }); + TaskBuilder::new().name("listener1").spawn(move || loop { + waiter3.wait(); + log::info!("3收到广播信号 {}", xtask::tick()); + }); + TaskBuilder::new().name("listener1").spawn(move || loop { + waiter4.wait(); + log::info!("4收到广播信号 {}", xtask::tick()); + }); + TaskBuilder::new().name("listener1").spawn(move || loop { + waiter5.wait(); + log::info!("5收到广播信号 {}", xtask::tick()); + }); +} +fn example_queue() { + #[derive(Debug, Clone)] + struct Message { + id: u64, + msg: String, + data: Vec, + } + let qsender = Queue::new(); + let qsender2 = qsender.clone(); + let qrecv = qsender.clone(); + let qrecv2 = qsender.clone(); + let qrecv3 = qsender.clone(); + TaskBuilder::new().name("queue.sender1").spawn(move || { + let mut id = 0; + loop { + id += 1; + let msg = Message { + id, + msg: format!("这是一条消息1 {}", xtask::tick()), + data: Vec::new(), + }; + qsender.push_back(msg); + xtask::sleep_ms(100); + } + }); + TaskBuilder::new().name("queue.sender2").spawn(move || { + let mut id = 0; + loop { + id += 1; + let msg = Message { + id, + msg: format!("这是一条消息2 {}", xtask::tick()), + data: Vec::new(), + }; + qsender2.push_back(msg); + xtask::sleep_ms(100); + } + }); + TaskBuilder::new().name("queue.recv1").spawn(move || loop { + if let Some(msg) = qrecv.pop_front() { + log::info!("收到消息1 {:?}", msg); + } + }); + TaskBuilder::new().name("queue.recv2").spawn(move || loop { + if let Some(msg) = qrecv2.pop_front() { + log::info!("收到消息2 {:?}", msg); + } + }); + TaskBuilder::new().name("queue.recv3").spawn(move || loop { + if let Some(msg) = qrecv3.pop_front() { + log::info!("收到消息3 {:?}", msg); + } + }); +} + +fn example_semaphore() { + let sender = Semaphore::new(); + let sender2 = sender.clone(); + let recver = sender.clone(); + let recver2 = sender.clone(); + let recver3 = sender.clone(); + TaskBuilder::new() + .name("semaphore.poster1") + .spawn(move || loop { + //log::info!("发送计数信号"); + sender.post(); + xtask::sleep_ms(100); + }); + TaskBuilder::new() + .name("semaphore.poster2") + .spawn(move || loop { + //log::info!("发送计数信号"); + sender2.post(); + xtask::sleep_ms(100); + }); + + TaskBuilder::new() + .name("semaphore.waiter1") + .spawn(move || loop { + recver.wait(); + log::info!("收到计数信号"); + }); + TaskBuilder::new() + .name("semaphore.waiter2") + .spawn(move || loop { + recver2.wait(); + log::info!("收到计数信号"); + }); + TaskBuilder::new() + .name("semaphore.waiter3") + .spawn(move || loop { + recver3.wait(); + log::info!("收到计数信号"); + }); +} + +fn example_task() { + // 只保留 2 个(原 ch32v103 示例是 5 个):32K SRAM 上 12 个示例任务 + + // timer + idle 实测 20.9K,再加任务会在 24K 堆上 OOM(见 main 里的实测注) + xtask::spawn(|| { + for i in 0..10 { + log::info!("{} 循环测试任务0", i + 1); + xtask::sleep_ms(1000); + } + }); + xtask::spawn(|| loop { + log::info!("死循环测试任务 {}", tick()); + xtask::sleep_ms(10000); + }); +} diff --git a/src/chip/ch583/memory.x b/src/chip/ch583/memory.x new file mode 100644 index 0000000..0360623 --- /dev/null +++ b/src/chip/ch583/memory.x @@ -0,0 +1,41 @@ +/* CH583(QingKe V4A)内存布局 —— 对齐官方 EVT `Ld/Link.ld`(SRC/Ld/Link.ld): + FLASH @0x00000000,LENGTH 448K(⚠️ 基址是 0,不是 0x08000000; + 尾部 64K(0x70000-0x7FFFF)留给 BootLoader/ISP/配置区,故只用 448K); + RAM @0x20000000,LENGTH 32K。 + 真机核对点:0x00000000 是否可写、是否存在 0x08000000 别名。 + + 前 0x18 字节是**启动头**(`.bootvec`,见 port.S):WCH ROM 从 0x0 取指, + 并按"向量表第 5 个字 = boot option 0xF3F9BDA9"识别有效用户程序(官方 + startup_CH583.S 的 `.vector` 段,注释 "boot option, can't modify"; + 官方 Link.ld 里该字落在 flash 0x14)。故 `.text` 顶到 0x18 起(见下面 + `_stext` 赋值),启动头用下面的 SECTIONS 显式摆到 flash 0x0。 */ +MEMORY +{ + FLASH : ORIGIN = 0x00000000, LENGTH = 448K + RAM : ORIGIN = 0x20000000, LENGTH = 32K +} + +REGION_ALIAS("REGION_TEXT", FLASH); +REGION_ALIAS("REGION_RODATA", FLASH); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); + +SECTIONS +{ + /* 启动头:flash 0x00..0x17(ROM 认的启动头字节,内容见 port.S) */ + .bootvec 0x00000000 : + { + KEEP(*(.bootvec)); + } > FLASH +} + +/* riscv-rt 的 link.x 用 `PROVIDE(_stext = ORIGIN(REGION_TEXT))` 兜底;本文件 + 先于 link.x 处理,这里先定义 `_stext`,把 `.text` 顶到启动头之后(0x18), + 避免与 `.bootvec` 抢 0x0(PROVIDE 不会覆盖已定义符号)。 */ +_stext = 0x00000018; + +/* 链接期守卫:启动头与 boot option 必须落在 ROM 认的地址上(符号见 port.S) */ +ASSERT(_bootvec == 0x00000000, "ERROR(ch583): 启动头 .bootvec 必须落在 flash 0x0"); +ASSERT(_boot_magic == 0x00000014, "ERROR(ch583): boot option 0xF3F9BDA9 必须落在 flash 0x14"); diff --git a/src/chip/ch583/mod.rs b/src/chip/ch583/mod.rs new file mode 100644 index 0000000..9b6fd08 --- /dev/null +++ b/src/chip/ch583/mod.rs @@ -0,0 +1,238 @@ +//! CH583 移植模块实现(QingKe V4A(第三代以上 PFIC 内核,SysTick 独立外设) 内核, +//! PFIC + WCH 自有 SysTick 模型)。以 `ch32v103`(V3A)为模板——两者架构同源: +//! 都用 `STK_CTLR.SWIE` 触发软中断、都是 36 字纯软件全保存帧、都是 Direct mtvec。 +//! +//! 与 `ch32v103` 的关键差异: +//! - **代码基址 = `0x00000000`**(不是 `0x08000000`!),见 `memory.x`; +//! 官方 EVT 只给 448K,尾部 64K 留给 BootLoader/ISP/配置区。 +//! - **私有 CSR `0xbc0`**:`_setup_interrupts` 多一条 `csrw 0xbc0, 0x1f` +//! (流水线控制 + 动态分支预测控制位,官方 `startup_CH583.S` 固定写法)。 +//! - **SysTick 是 64 位**(CH572 是 32 位),但寄存器字偏移与 V3A 一致: +//! CTLR@+0 SR@+4 CNT@+8(64 位,字 2/3) CMP@+16(64 位,字 4/5)。 +//! +//! 本口按**标准 RV32 机器模式**设计(与现有 RISC-V 口一致): +//! +//! - **HPE 关闭**:`_setup_interrupts` 显式写 `intsyscr(0x804)=0`(CH583 的 +//! HWSTKEN 在 bit0),上下文切换回到**纯软件全保存**模型(36 字帧)。 +//! - **mtvec = Direct 模式**:单入口 `_start_trap` 读 `mcause` 分发—— +//! 中断号 12 = SysTick;其余走 `DefaultHandler`。 +//! - **软中断(yield/抢占请求)复用 SysTick 入口**:`STK_CTLR.SWIE`(bit31) +//! 写 1 立即触发 SysTick 中断——ISR 读 `STK_SR.CNTIF` 区分真 tick 与 +//! 软中断请求。**与 ch32v103 完全同款**(CH572 的 SWIE 在 `SR` 而非 +//! `CTLR`,故 CH572 需要另写,本 CH583 口可直接复用)。 +//! ⚠️ 但官方 CH583 的 FreeRTOS / RT-Thread / HarmonyOS 三套移植**都不走 +//! `CTLR.SWIE`**(SDK 里只定义、零使用):它们一律用 QingKe 专用 +//! `SWI_IRQn=14` + `SW_Handler`(FreeRTOS `portYIELD()` = +//! `PFIC_SetPendingIRQ(SWI_IRQn)`)。本口沿用 ch32v103 模板的 SWIE 路线, +//! 上板须确认(示例头部核对点④);若 SWIE 无效果,备选即改走 IRQ 14。 +//! - **SysTick@0xE000F000**(WCH 自有,非 CLINT mtime);`SR.CNTIF` 写 0 清零。 +//! - **无 A 扩展(如 CH572)不是换个 target 的事**:`riscv32imc-unknown-none-elf` +//! 实测只有 `target_has_atomic_load_store`、没有 `target_has_atomic`,内核里 +//! `fetch_add`/`swap` 这类 RMW 编不过——需要先做"关中断/自旋锁"原子垫片; +//! CH583(V4A)有 A 扩展,本口不受影响。 +//! +//! ⚠️ 真机核对点(构建级验证,板上行为待验): +//! ①`0x00000000` 是否可写 / 是否有 `0x08000000` 别名; +//! ②默认主频 = 60MHz(官方 `CH58x_common.h` 的 `FREQ_SYS` 默认值;env 常数按此配, +//! PLL 配好后须同步改); +//! ③`csrw 0xbc0, 0x1f`(官方 `startup_CH583.S` 的 reset 序列同款,已按 SDK +//! 源码核对;上板只需确认不触发 Illegal Instruction); +//! ④`0x804=0` 后异常/中断入口是否确实不再硬件压栈(决定 36 字帧成立); +//! ⑤`STK_CTLR.SWIE` 置 1 后是否即刻进 `SysTick`、`CNTIF` 是否仍为 0; +//! ⑥`mcycle` 是否实现(决定 `delay_us`,见下)。 +//! BootLoader 默认开启:本口已在 flash `0x0` 复刻官方启动头(`.bootvec` = +//! 入口跳转 + 向量表前 5 字,第 5 字为 boot option `0xF3F9BDA9`;见 `port.S` +//! 与 `memory.x` 的链接期 ASSERT)。若上板仍停在 ISP,则改用关闭 BootLoader 的 +//! 配置字。 +//! +//! **零 PAC 依赖**:CH58x 无现成 `ch32-rs` PAC,本口全量直址访问 +//! (与 `qemu_riscv` 口的思路一致),`Cargo.toml` 只挂 `riscv-rt`。 + +mod port; + +use super::{CPU_CLOCK_HZ, SYSTICK_CLOCK_HZ, TICK_CLOCK_HZ}; +use crate::port::Portable; +use crate::prelude::CriticalSection; +use crate::task::Task; +use core::arch::asm; + +/// SysTick 寄存器基址(WCH 自有,非 CLINT;CH583 为 64 位计数器) +pub(crate) const STK_BASE: usize = 0xE000_F000; + +/// PFIC 寄存器基址(CH583 与 CH572/ch32v103/ch32v307 同址) +pub(crate) const PFIC_BASE: usize = 0xE000_E000; + +/// 配置 SysTick 与 PFIC(在 start_scheduler 里、restore_ctx 之前调用)。 +/// CH583 的 SysTick 与 V3A 同布局,只是计数器/比较值 64 位; +/// PFIC 无 PAC,直接访问 `IENR[0]`(offset 0x100)使能 SysTick。 +#[inline] +pub(crate) fn setup_intrrupt() { + let stk = STK_BASE as *mut u32; + unsafe { + // 1. 停计数(CTLR=0)、清 SR.CNTIF(写 0)、CNT/CMP 清零装初值(高字先写) + stk.write_volatile(0); + stk.add(1).write_volatile(0); // SR:CNTIF 写 0 清 + stk.add(2).write_volatile(0); // CNTL + stk.add(3).write_volatile(0); // CNTH + const TICKS: u32 = (SYSTICK_CLOCK_HZ / TICK_CLOCK_HZ) as u32 - 1; + stk.add(5).write_volatile(0); // CMPHR + stk.add(4).write_volatile(TICKS); // CMPLR + // 2. CTLR = INIT(5)|STRE(3)|STCLK(HCLK,2)|STIE(1)|STE(0) = 0x2F + // ——逐位对齐官方 `EVT/EXAM/SRC/RVMSIS/core_riscv.h` 的 + // `SysTick_Config()`:`SysTick->CMP = ticks - 1;` 然后 + // `CTLR = INIT|STRE|STCLK|STIE|STE`。 + // **官方不带 SWIE(bit31)**:SWIE 只在运行期用于请求切换(见 `irq()`)。 + // 若初始化时置 SWIE,第一个任务 `mret`(MIE←MPIE=1)后会立刻多进 + // 一次 trap,而该 trap 早于 restore_ctx.S 写 mscratch → 隐含依赖 + // "进入 setup_intrrupt 时 mstatus.MIE=0";去掉后无此依赖。 + stk.write_volatile(0x0000_002F); + } + // 3. PFIC:使能 SysTick(IENR[0] @0xE000E100,bit12 = 核中断号 12) + let ienr0 = (PFIC_BASE + 0x100) as *mut u32; + unsafe { + ienr0.write_volatile(1 << 12); + } +} + +/// 清 SysTick 的 CNTIF(写 0)——真 tick 路径调用 +#[inline] +pub(crate) fn reset_systick() { + let stk = STK_BASE as *mut u32; + unsafe { + stk.add(1).write_volatile(0); // SR @ +4 + } +} + +/// CH583 芯片移植层实现 +pub struct Ch583Porting; + +// port.S 蹦床 `_task_entry_trampoline` 依赖的 Task 布局偏移(失配编译期炸) +const _: () = assert!(core::mem::offset_of!(Task, sp) == 0); +const _: () = assert!(core::mem::offset_of!(Task, entry) == 8); + +impl Portable for Ch583Porting { + /// 完全内存屏障 + #[inline] + fn barrier() { + unsafe { + // CH583 无 MMU/虚存,通用 fence(iorw) 即可 + core::arch::asm!("fence iorw, iorw"); + } + } + /// 临界区保护(本核 mstatus.MIE——单核语义与 gd32 同) + #[inline] + fn free(f: F) -> R + where + F: FnOnce(&CriticalSection) -> R, + { + riscv::interrupt::free(f) + } + + /// 开全局中断 + #[inline] + fn enable_interrupt() { + unsafe { + riscv::interrupt::enable(); + } + } + /// 关全局中断 + #[inline] + fn disable_interrupt() { + unsafe { + riscv::interrupt::disable(); + } + } + + /// 启动调度器:配置 SysTick/PFIC → 恢复第一个任务(汇编,不返回) + fn start_scheduler() -> ! { + setup_intrrupt(); + log::info!("Start scheduler"); + unsafe { asm!(include_str!("restore_ctx.S"), options(noreturn, raw)) }; + } + + /// 软中断(调度请求):写 STK_CTLR.SWIE(bit31)触发 SysTick 入口。 + /// ISR 读 SR.CNTIF 区分真 tick 与本请求(见 port.rs 的 SysTick 处理)。 + /// 官方 CH583 三套移植走 `SWI_IRQn=14`(见模块头注),本口沿用模板的 SWIE。 + /// + /// 读改写时**显式掩掉 INIT(bit5)**:INIT 与 SWIE 同寄存器,而它是否为 + /// 自清触发位没有一手证据(官方 `SysTick_Config` 只在初始化写一次 CTLR、 + /// 之后从不读改写,SDK 给不出答案)。若它是电平位,回写会让每次 yield 都 + /// 重新装载 64 位计数器 → tick 账漂移;掩掉后两种情况都安全。 + #[inline] + fn irq() { + const SWIE: u32 = 1 << 31; + const INIT: u32 = 1 << 5; + let ctlr = STK_BASE as *mut u32; + unsafe { + ctlr.write_volatile((ctlr.read_volatile() & !INIT) | SWIE); + } + } + /// 关闭软中断(SWIE 是触发位,自清;防御性清一下——同样掩掉 INIT) + #[inline] + fn disable_irq() { + const SWIE: u32 = 1 << 31; + const INIT: u32 = 1 << 5; + let ctlr = STK_BASE as *mut u32; + unsafe { + ctlr.write_volatile(ctlr.read_volatile() & !(SWIE | INIT)); + } + } + + /// 读 SysTick 64 位计数器(高:低:高重读防翻转;CH583 是 64 位) + #[inline] + fn systick() -> u64 { + let stk = STK_BASE as *mut u32; + loop { + unsafe { + let hi = stk.add(3).read_volatile(); + let lo = stk.add(2).read_volatile(); + if hi == stk.add(3).read_volatile() { + return ((hi as u64) << 32) | lo as u64; + } + } + } + } + + /// 硬件延时,单位 us。 + /// + /// ⚠️ **暂沿 `ch32v103` 用 `mcycle` 实现**——CH583 是否实现 `mcycle` + /// (Zicntr)待上板核对:若未实现则读出恒为常量,本函数会死循环。届时 + /// **必须**改为 SysTick/TMR 计时(CH57x 无 DWT,不能照搬 ARM 的 DWT_CYCCNT + /// 或 ch32v307 的 DWT)。主频按 env 的 `CPU_CLOCK_HZ`(默认 60MHz)。 + #[inline] + fn delay_us(us: u64) { + let t0 = riscv::register::mcycle::read64(); + let clock = (us * (CPU_CLOCK_HZ as u64)) / 1_000_000; + while riscv::register::mcycle::read64().wrapping_sub(t0) <= clock {} + } + + /// 任务创建时为 CPU 准备任务现场(36 字帧,与 port.S 的保存宏互为镜像): + /// [35]=mcause(0x8000_000C)[34]=0(mcause 保留槽) + /// [33]=mepc=_task_entry_trampoline [32]=mstatus=0x1880(MPP=M|MPIE=1) + /// [10]=a0=args [1]=ra=task_exit,任务块 sp 指向帧底 + #[inline] + fn save_context(task: &mut Task) { + unsafe { + let sp = task.stack.add(task.stack_size - 1); + sp.offset(-1).write_volatile(0x8000_000C); // mcause:中断|12(SysTick) + sp.offset(-2).write_volatile(0); // 保留槽(无 msubm) + // mepc = 首调蹦床:经标准 jalr 进入 task.entry(mret 直入会被 + // 编译器 outlined 的入口 stub 坑到野跳,见 port.S 蹦床注) + unsafe extern "C" { + fn _task_entry_trampoline(); + } + sp.offset(-3) + .write_volatile((_task_entry_trampoline as *const ()).addr()); // mepc + sp.offset(-4).write_volatile(0x0000_1880); // mstatus:MPP=M, MPIE=1 + for i in 0..32usize { + if i != 1 && i != 10 { + sp.offset(i as isize - 36).write_volatile(0); + } + } + sp.offset(-26).write_volatile(task.args.addr()); // a0 + sp.offset(-35) + .write_volatile((port::task_exit as *const ()).addr()); // ra + task.sp = sp.offset(-36).addr(); + } + } +} diff --git a/src/chip/ch583/port.S b/src/chip/ch583/port.S new file mode 100644 index 0000000..489c1db --- /dev/null +++ b/src/chip/ch583/port.S @@ -0,0 +1,238 @@ +/* + CH583(QingKe V4A)上下文切换与中断分发 —— ch32v103/port.S 的同架构变体: + 同为「PFIC + WCH 自有 SysTick + 纯软件全保存」模型,HPE 显式关闭 + (0x804=0),mtvec=Direct 单入口分发。相对 ch32v103 的增量只有: + ①`_setup_interrupts` 多一条 `csrw 0xbc0, 0x1f`(CH57x 私有流水线控制位); + ②代码基址 0 由 memory.x 决定(本文件无感)。 + + 帧布局(36 字,与 mod.rs 的 save_context 互为镜像): + [0] x0 保留 [1] ra [2..4] x2-x4 保留 + [5..31] x5-x31 [32] mstatus [33] mepc + [34] 保留(mcause 槽,Bumblebee msubm 位置) [35] mcause +*/ + +// 保存上文宏 +.macro SAVE_CONTEXT + addi sp, sp, -36 * 4 + sw x1, 1 * 4(sp) // ra + sw x5, 5 * 4(sp) // t0 + sw x6, 6 * 4(sp) // t1 + sw x7, 7 * 4(sp) // t2 + sw x8, 8 * 4(sp) // s0/fp + sw x9, 9 * 4(sp) // s1 + sw x10, 10 * 4(sp) // a0 + sw x11, 11 * 4(sp) // a1 + sw x12, 12 * 4(sp) // a2 + sw x13, 13 * 4(sp) // a3 + sw x14, 14 * 4(sp) // a4 + sw x15, 15 * 4(sp) // a5 + sw x16, 16 * 4(sp) // a6 + sw x17, 17 * 4(sp) // a7 + sw x18, 18 * 4(sp) // s2 + sw x19, 19 * 4(sp) // s3 + sw x20, 20 * 4(sp) // s4 + sw x21, 21 * 4(sp) // s5 + sw x22, 22 * 4(sp) // s6 + sw x23, 23 * 4(sp) // s7 + sw x24, 24 * 4(sp) // s8 + sw x25, 25 * 4(sp) // s9 + sw x26, 26 * 4(sp) // s10 + sw x27, 27 * 4(sp) // s11 + sw x28, 28 * 4(sp) // t3 + sw x29, 29 * 4(sp) // t4 + sw x30, 30 * 4(sp) // t5 + sw x31, 31 * 4(sp) // t6 + csrr t0, mstatus + sw t0, 32 * 4(sp) + csrr t0, mepc // 被打断处的下一条指令 + sw t0, 33 * 4(sp) + sw zero, 34 * 4(sp) // 保留槽(无 msubm) + csrr t0, mcause + sw t0, 35 * 4(sp) +.endm + +// 恢复下文宏(与保存严格对称) +.macro RESTORE_CONTEXT + // ★ CSR 装载必须先于通用寄存器:CSR 段用 t0 作草稿,若先恢复 x5(t0) + // 再装 CSR,最后一次 lw t0(=mcause 槽)会把恢复好的 t0 覆盖——任务在 + // 编译器 outlined 序言(`jr t0` 结尾)中被抢占时,恢复后 jr t0 野跳 + // (qemu_riscv 口实测:跳 _start+6 → 整机重启,详见该口 port.S 注) + lw t0, 32 * 4(sp) + csrw mstatus, t0 + lw t0, 33 * 4(sp) + csrw mepc, t0 + // 34 槽跳过(保留) + lw t0, 35 * 4(sp) + csrw mcause, t0 + // 通用寄存器最后装载——t0(x5)在所有草稿使用之后恢复 + lw x1, 1 * 4(sp) + lw x5, 5 * 4(sp) + lw x6, 6 * 4(sp) + lw x7, 7 * 4(sp) + lw x8, 8 * 4(sp) + lw x9, 9 * 4(sp) + lw x10, 10 * 4(sp) + lw x11, 11 * 4(sp) + lw x12, 12 * 4(sp) + lw x13, 13 * 4(sp) + lw x14, 14 * 4(sp) + lw x15, 15 * 4(sp) + lw x16, 16 * 4(sp) + lw x17, 17 * 4(sp) + lw x18, 18 * 4(sp) + lw x19, 19 * 4(sp) + lw x20, 20 * 4(sp) + lw x21, 21 * 4(sp) + lw x22, 22 * 4(sp) + lw x23, 23 * 4(sp) + lw x24, 24 * 4(sp) + lw x25, 25 * 4(sp) + lw x26, 26 * 4(sp) + lw x27, 27 * 4(sp) + lw x28, 28 * 4(sp) + lw x29, 29 * 4(sp) + lw x30, 30 * 4(sp) + lw x31, 31 * 4(sp) + addi sp, sp, 4 * 36 +.endm + +// 当前任务全局指针(RUST 代码维护) +.extern CURRENT_TASK_PTR +// RUST 侧 SysTick 处理(真 tick 与 SWIE 软中断二合一) +.extern SysTick +// 未处理异常/中断的默认处理(riscv-rt 提供,弱符号——应用可强定义覆盖) +.extern DefaultHandler + +/* + 统一 trap 入口(mtvec = Direct 模式指向这里): + 中断(mcause bit31=1)号 12 → SysTick(RUST); + 其余中断/全部异常 → DefaultHandler。 + SysTick 返回后(或 DefaultHandler 返回后)统一走切换/恢复路径。 +*/ +.section .trap, "ax" +.option push +.option norelax +.align 6 +.option pop +.global _start_trap +_start_trap: + // 保存上文到当前任务栈 + SAVE_CONTEXT + // 任务块首字段存 sp + lw t0, CURRENT_TASK_PTR + sw sp, 0x0(t0) + // 切到中断栈:mscratch 恒持中断栈地址(restore_ctx.S 启动时写一次, + // 之后永不改)——必须 csrr 只读,csrrw 交换会把任务栈地址写进 mscratch, + // 下一次中断就跑在陈旧/已释放的任务栈上(任务退出+内存复用后现场错乱) + csrr sp, mscratch + + // 分发:中断? (mcause < 0 即 bit31=1) + csrr t0, mcause + bltz t0, _int_dispatch + + // ---- 异常路径 ---- +_exc_dispatch: + // 所有异常进 DefaultHandler(构建级:无 FPU/无虚存,异常即致命) + jal ra, DefaultHandler + j _switch_and_restore + + // ---- 中断路径 ---- +_int_dispatch: + slli t0, t0, 0 // 保位 + li t1, 0x7fffffff + and t0, t0, t1 // 取中断号(去 bit31) + li t1, 12 // SysTick + beq t0, t1, _isr_systick + // 未接入的外设中断/其它核心中断 → DefaultHandler(BSP 扩展点) + jal ra, DefaultHandler + j _switch_and_restore + +_isr_systick: + jal ra, SysTick // RUST:清 CNTIF/时间账/必要时置 SWIE 请求切换 + + // ---- 切换与恢复(公共出口) ---- +_switch_and_restore: + // 调度器在中断栈上运行(mscratch 恒持中断栈——绝不在任务栈上跑: + // ①小栈任务会被调度器自身栈帧撑爆;②csrrw 交换会污染 mscratch, + // 修前 bug:任务退出后 mscratch 残留已释放的任务栈地址,下一次 + // 中断在已释放内存上运行 → 内存复用后现场错乱 → guest 复位) + jal ra, switch_context + // 装载新任务 sp(调度器已更新 CURRENT_TASK) + lw t0, CURRENT_TASK_PTR + lw sp, 0x0(t0) + RESTORE_CONTEXT + mret + +/* + riscv-rt link.x 在启动时调用:配 mtvec(Direct)与 HPE 关闭。 +*/ +.section .text +.global _setup_interrupts +_setup_interrupts: + // 私有 CSR 0xbc0(流水线控制 + 动态分支预测控制位),取值与官方 + // `EVT/EXAM/SRC/Startup/startup_CH583.S` 的 reset 序列逐字一致: + // li t0, 0x1f + // csrw 0xbc0, t0 + // (CH572 为 0x25 且另有 0xbc1=1,本口不适用) + li t0, 0x1f + csrw 0xbc0, t0 + // 关闭 HPE:CH583 的 INTSYSCR(自定义 CSR 0x804)bit0=HWSTKEN、 + // bit1=INESTEN。官方 startup 写 0x3(开硬件压栈 + 中断嵌套,配 HPE 序言), + // 本口写 0 = 纯软件全保存模型(硬件序言是 WCH 私有工具链特性,与 + // riscv-rt/裸 Rust 不兼容),这也是本口 36 字帧成立的前提。 + csrw 0x804, zero + // mtvec = _start_trap,Direct 模式(低位 0) + la t0, _start_trap + andi t0, t0, -4 // 4 字节对齐(Direct 模式的最低要求) + csrw mtvec, t0 + ret + +/* + 任务首调蹦床:save_context 造的初始帧 mepc 指向这里(而非 task.entry)。 + mret 直入入口函数不可靠:编译器(LTO)会把闭包入口的序言/尾声 outlined + 成共享 stub(如 `sw..; jr t0` / `lw ra,0xc(sp); ret`),这些 stub 只在 + 调用方先设置好寄存器(jalr 调用模式)时成立,mret 路径跳过了调用方 → + stub 读到未初始化的帧槽(陈旧堆数据)→ 野跳(qemu_riscv 口实测曾跳进 + _start → 整机"重启")。task.entry 是地址已取的 fn 指针,经标准 jalr + 调用必然合法——蹦床恢复"调用方"角色,入口返回后接统一的退出路径。 +*/ +.section .text +.global _task_entry_trampoline +_task_entry_trampoline: + lw t0, CURRENT_TASK_PTR // CURRENT == 本任务(restore 前由 execute 设置) + lw t1, 8(t0) // Task.entry(offset 8,见 mod.rs 布局断言) + jalr t1 // 标准调用:a0=args(帧设置),ra=返回蹦床 + j task_exit // 入口跑完 → 退出路径(不返回) + +/* + 启动头(flash 0x00..0x17,由 memory.x 的 SECTIONS 显式摆位 + 链接期 ASSERT): + + WCH ROM 从 flash 0x0 取指,并按"向量表第 5 个字 = boot option"识别有效用户 + 程序。官方 `EVT/EXAM/SRC/Startup/startup_CH583.S` 的 `.vector` 段: + + .word 0 + .word 0 + .word NMI_Handler + .word HardFault_Handler + .word 0xF3F9BDA9 // boot option, can't modify + + 官方 `Ld/Link.ld` 把 `.vector` 的 flash LMA 紧跟在 `.init` 的 + `_start: j handle_reset` 之后(即 flash 0x04),所以魔数落在 flash **0x14**。 + 本口保持 Direct mtvec(CPU 不读这张表),这里只为对齐 ROM 的启动头布局: + 入口跳转接 riscv-rt 的 `_start`,随后 5 字复刻官方向量表前 5 项。 +*/ +.section .bootvec, "ax", @progbits +.option push +.option norvc // 4 字节定长(官方 .vector 亦 .option norvc) +.align 2 +.global _bootvec +_bootvec: + j _start // 0x00:ROM 入口(官方 .init 的 `j handle_reset` 同位) + .word 0 // 0x04:向量表[0](官方为 0) + .word 0 // 0x08:向量表[1](官方为 0) + .word _start_trap // 0x0c:向量表[2] NMI + .word _start_trap // 0x10:向量表[3] HardFault + .global _boot_magic +_boot_magic: + .word 0xF3F9BDA9 // 0x14:向量表[4] boot option, can't modify +.option pop diff --git a/src/chip/ch583/port.rs b/src/chip/ch583/port.rs new file mode 100644 index 0000000..0f570ce --- /dev/null +++ b/src/chip/ch583/port.rs @@ -0,0 +1,52 @@ +//! CH583 中断胶水:SysTick 二合一处理(真 tick / SWIE 软中断)与切换入口。 +//! 与 ch32v103/port.rs 同构——CH583 的 `SWIE` 也在 `CTLR`(bit31),可直接复用语义。 + +use super::Ch583Porting; +use crate::port::Portable; +use crate::task::scheduler; +use core::arch::global_asm; +use riscv::register::mcause; + +global_asm!(include_str!("port.S"), options(raw)); + +/// SysTick 处理(port.S `_start_trap` 经 mcause=12 进入;工作在中断栈)。 +/// +/// 一个入口两种来源,靠 `STK_SR.CNTIF` 区分: +/// - **CNTIF=1(真 tick)**:计数到点——清标志(WCH 写 0 清)+ 时间账 +/// (`scheduler::systick`),返回 true 则请求切换; +/// - **CNTIF=0(SWIE 触发)**:纯调度请求(`Porting::irq()` 的软中断)—— +/// 不做时间账(否则 yield 一次时间就快进一格)。 +/// +/// 两条路径都在返回后经 port.S 的公共出口 `switch_context` 完成切换。 +#[no_mangle] +unsafe extern "C" fn SysTick() { + // CH583:SR 在 SYSTICK+4(直址;零 PAC 依赖) + let sr = (super::STK_BASE + 4) as *const u32; + if unsafe { sr.read_volatile() & 1 } != 0 { + // 真 tick:清标志 + 时间账 + super::reset_systick(); + if scheduler::systick() { + // 需要切换——不需要 SWIE,trap 出口统一调度 + } + } + // SWIE 路径:什么都不做,直接调度(port.S 公共出口) +} + +/// 软中断等价入口:port.S 在 SysTick 处理后调用,执行调度与切换 +#[no_mangle] +unsafe extern "C" fn switch_context() { + Ch583Porting::disable_irq(); // 防 SWIE 残留立即重入 + scheduler::schedule(); +} + +/// 所有任务的退出函数 +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn task_exit() { + scheduler::exit_current_task(); +} + +/// 供汇编引用的符号占位(避免未使用警告;真正实现在上面) +#[allow(dead_code)] +fn _mcause_helper() { + let _ = mcause::read(); +} diff --git a/src/chip/ch583/restore_ctx.S b/src/chip/ch583/restore_ctx.S new file mode 100644 index 0000000..79041e9 --- /dev/null +++ b/src/chip/ch583/restore_ctx.S @@ -0,0 +1,52 @@ +// 调度器启动:恢复第一个任务(gd32 restore_ctx.S 的无 msubm 变体) +// —— 与 ch32v103/restore_ctx.S 完全一致(CH583 帧布局同款 36 字) + +// 关全局中断 +csrc mstatus, 0x00000008 +// 栈顶下留 512 字节作中断栈(启动函数可能已占一部分;硬件启动栈 2k) +// 存入 mscratch,trap 入口经 csrr 只读换栈(mscratch 恒持中断栈,永不改写) +la t0, _stack_start +addi t0, t0, -512 +csrw mscratch, t0 +// 装载第一个任务 +lw t0, CURRENT_TASK_PTR +lw sp, 0x0(t0) +// 恢复通用寄存器 +lw x1, 1 * 4(sp) +lw x5, 5 * 4(sp) +lw x6, 6 * 4(sp) +lw x7, 7 * 4(sp) +lw x8, 8 * 4(sp) +lw x9, 9 * 4(sp) +lw x10, 10 * 4(sp) +lw x11, 11 * 4(sp) +lw x12, 12 * 4(sp) +lw x13, 13 * 4(sp) +lw x14, 14 * 4(sp) +lw x15, 15 * 4(sp) +lw x16, 16 * 4(sp) +lw x17, 17 * 4(sp) +lw x18, 18 * 4(sp) +lw x19, 19 * 4(sp) +lw x20, 20 * 4(sp) +lw x21, 21 * 4(sp) +lw x22, 22 * 4(sp) +lw x23, 23 * 4(sp) +lw x24, 24 * 4(sp) +lw x25, 25 * 4(sp) +lw x26, 26 * 4(sp) +lw x27, 27 * 4(sp) +lw x28, 28 * 4(sp) +lw x29, 29 * 4(sp) +lw x30, 30 * 4(sp) +lw x31, 31 * 4(sp) +// CSR 出栈(34 槽保留,跳过) +lw t0, 32 * 4(sp) +csrw mstatus, t0 +lw t0, 33 * 4(sp) +csrw mepc, t0 +lw t0, 35 * 4(sp) +csrw mcause, t0 +// 释放帧,跳进任务(mret:PC=mepc,mie=mpie) +addi sp, sp, 4 * 36 +mret diff --git a/src/chip/env.rs b/src/chip/env.rs index 0bd3113..074ff7d 100644 --- a/src/chip/env.rs +++ b/src/chip/env.rs @@ -34,6 +34,13 @@ pub const SYSTICK_CLOCK_HZ: usize = 8_000_000; pub const CPU_CLOCK_HZ: usize = 8_000_000; #[cfg(feature = "ch32v103")] pub const SYSTICK_CLOCK_HZ: usize = 8_000_000; +/// CH583(QingKe V4A):默认主频取官方 `CH58x_common.h` 的 `FREQ_SYS` 默认值 +/// 60MHz(⚠️ 真机核对点——本口无 BSP/时钟初始化,按官方默认配; +/// PLL 配置后须同步改这里与 STCLK 语义,见 memory.x/mod.rs 注) +#[cfg(feature = "ch583")] +pub const CPU_CLOCK_HZ: usize = 60_000_000; +#[cfg(feature = "ch583")] +pub const SYSTICK_CLOCK_HZ: usize = 60_000_000; // STCLK=1 → HCLK(=60M) // ESP32-C3:复位默认 CPU 80MHz(TRM;PLL 160M 配好后同步改); // SYSTICK 是独立 16MHz 时基(与 CPU 时钟无关—— 调研已核) #[cfg(feature = "esp32c3")] diff --git a/src/chip/mod.rs b/src/chip/mod.rs index c07c59c..b9486c4 100644 --- a/src/chip/mod.rs +++ b/src/chip/mod.rs @@ -9,6 +9,8 @@ pub mod ch32v307; pub mod ch32v203; #[cfg(feature = "ch32v103")] pub mod ch32v103; +#[cfg(feature = "ch583")] +pub mod ch583; #[cfg(feature = "esp32c3")] pub mod esp32c3; #[cfg(feature = "qemu_riscv")] diff --git a/src/port.rs b/src/port.rs index 0227157..8b007bc 100644 --- a/src/port.rs +++ b/src/port.rs @@ -1,54 +1,60 @@ //! 移植层定义&配置 -#[cfg(all(feature = "gd32vf103", not(test)))] -pub use crate::chip::gd32vf103::Gd32vf103Porting as Porting; - -#[cfg(all(feature = "stm32f4", not(test)))] -pub use crate::chip::stm32f4::STM32F4Porting as Porting; - -#[cfg(all(feature = "stm32f1", not(test)))] -pub use crate::chip::stm32f1::STM32F1Porting as Porting; - -#[cfg(all(feature = "rp2040", not(test)))] -pub use crate::chip::rp2040::RP2040Porting as Porting; - -#[cfg(all(feature = "stm32h7", not(test)))] -pub use crate::chip::stm32h7::STM32H7Porting as Porting; - -#[cfg(all(feature = "cm32m4", not(test)))] -pub use crate::chip::cm32m4::CM32M4Porting as Porting; -#[cfg(all(feature = "ch32v307", not(test)))] -pub use crate::chip::ch32v307::Ch32v307Porting as Porting; -#[cfg(all(feature = "ch32v203", not(test)))] -pub use crate::chip::ch32v203::Ch32v203Porting as Porting; -#[cfg(all(feature = "ch32v103", not(test)))] -pub use crate::chip::ch32v103::Ch32v103Porting as Porting; -#[cfg(all(feature = "esp32c3", not(test)))] -pub use crate::chip::esp32c3::Esp32c3Porting as Porting; -#[cfg(all(feature = "qemu_riscv", not(test)))] -pub use crate::chip::qemu_riscv::QemuRiscvPorting as Porting; - -#[cfg(all(feature = "qemu_arm_r52", not(test)))] -pub use crate::chip::qemu_arm_r52::QemuArmR52Porting as Porting; - -#[cfg(all( - not(test), - not(any( - feature = "gd32vf103", - feature = "stm32f4", - feature = "stm32f1", - feature = "rp2040", - feature = "stm32h7", - feature = "cm32m4", - feature = "ch32v307", - feature = "ch32v203", - feature = "ch32v103", - feature = "esp32c3", - feature = "qemu_riscv", - feature = "qemu_arm_r52" - )) -))] -pub use DefaultPorting as Porting; +// ---- 芯片移植实现的**单一来源清单** ---------------------------------------- +// 每个芯片在这里登记一次(feature 名 => 实现类型),下面由宏一次性生成: +// ① 正选别名 `pub use <实现类型> as Porting`; +// ② 兜底桩的 feature 名单(`not(any(...))` → `DefaultPorting`); +// ③ 编译期守卫:`RealPorting` 标记实现 + "选了芯片就不许落兜底桩"断言。 +// +// 为什么收成一处:①与②原本是两份手写名单,新增芯片时只改一份,`Porting` +// 就静默落到 `DefaultPorting`(各方法 `unimplemented!()`),构建与门禁全绿、 +// 直到运行期 `xtask::start()` 才 panic——2026-09-18 的 ch583 骨架 PR 实测 +// 踩过此坑(见 PR #15 自审节 / issue #14)。 +macro_rules! chip_portings { + ($($feat:literal => $ty:path),* $(,)?) => { + $( + #[cfg(all(feature = $feat, not(test)))] + pub use $ty as Porting; + )* + + #[cfg(all(not(test), not(any($(feature = $feat),*))))] + pub use DefaultPorting as Porting; + + /// 真实芯片移植实现的标记 trait:兜底桩(`DefaultPorting`)与 host + /// mock(`HostPorting`)故意不实现——配合下面的编译期断言,把"选了 + /// 芯片 feature 却落到兜底桩"钉成编译错误(否则要等运行期 + /// `unimplemented!()` 才暴露)。 + pub trait RealPorting {} + + $( + #[cfg(all(feature = $feat, not(test)))] + impl RealPorting for $ty {} + )* + + // 编译期守卫:选了任一芯片 feature 时,`Porting` 必须是真实实现 + #[cfg(all(not(test), any($(feature = $feat),*)))] + const _: fn() = { + fn assert_real_porting() {} + assert_real_porting:: + }; + }; +} + +chip_portings! { + "gd32vf103" => crate::chip::gd32vf103::Gd32vf103Porting, + "stm32f4" => crate::chip::stm32f4::STM32F4Porting, + "stm32f1" => crate::chip::stm32f1::STM32F1Porting, + "rp2040" => crate::chip::rp2040::RP2040Porting, + "stm32h7" => crate::chip::stm32h7::STM32H7Porting, + "cm32m4" => crate::chip::cm32m4::CM32M4Porting, + "ch32v307" => crate::chip::ch32v307::Ch32v307Porting, + "ch32v203" => crate::chip::ch32v203::Ch32v203Porting, + "ch32v103" => crate::chip::ch32v103::Ch32v103Porting, + "ch583" => crate::chip::ch583::Ch583Porting, + "esp32c3" => crate::chip::esp32c3::Esp32c3Porting, + "qemu_riscv" => crate::chip::qemu_riscv::QemuRiscvPorting, + "qemu_arm_r52" => crate::chip::qemu_arm_r52::QemuArmR52Porting, +} // host 测试环境:提供一个可运行的 Porting mock,让纯逻辑(信号量、队列、总线、延时队列) // 能在 `cargo test` 下被驱动。单线程语义下临界区只是一个标记,无需真实关中断。