- 2025-04-06
-
发表了主题帖:
嵌入式 Rust 修炼营:初级修炼任务三「烧录小工具」
本帖最后由 scgummy 于 2025-4-7 15:23 编辑
# 简介
基于 `serial` 库开发单片机串口下载工具([串口下载工具参考](https://github.com/hysonglet/pyisp)),可参考 demo 增加芯片识别,芯片 ID 打印、HEX 烧录、ELF 烧录等功能.
项目地址:https://codeberg.org/scgummy/eeworld-rust-2025/src/branch/main/novice-isp
# 效果
为了简化烧录时按键的需求,我把 CH340N 上的 `RTS#` 针脚对 MCU 的复位引脚串联了一个 4.7 kΩ 的电阻,这样烧录程序就可以通过断言 `request_to_send` 达到自动重置的效果. 由于 CH340N 只有 8 个引脚,MODEM 信号只引出了 `RTS#` 引脚,没有办法能简单自动化 BOOT0 信号,因此 BOOT0 按键还是需要长按住的.
# 任务详情
我们打算编写一个针对 PY32F030 的烧录工具,根据其手册上关于启动模式的内容可以知道 BOOT0 置高后将进入 bootloader,此时可以通过串口将程序烧入 Flash 中. 至于进入 bootloader 模式后具体的通信内容,我参考了 [puyaisp.py](https://github.com/wagiminator/MCU-Flash-Tools/blob/19f45bcef36ddcae4dfeac48ee3e40eb93fd01d4/puyaisp.py) 和 [pyisp](https://github.com/hysonglet/pyisp),发现其实很大程度上复用了 STM32 bootloader 的 UART 协议,具体的协议规范可以参见 https://www.st.com/resource/en/application_note/an3155-usart-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf
# 任务实施
## 声明式的串口数据帧
我们可以利用 `binrw` 实现对串口数据帧读写的声明式拼接,这样有利于今后的数据扩展,同时一目了然. 下面以命令,以及芯片信息的响应作为例子展现.
```rust
#[binwrite]
#[derive(Debug, Clone)]
enum Command {
Get(#[bw(calc = Opcode::GET)] Opcode),
Version(#[bw(calc = Opcode::VERSION)] Opcode),
Id(#[bw(calc = Opcode::ID)] Opcode),
Read {
#[bw(calc = Opcode::READ)]
opcode: Opcode,
address: Address,
size: Size,
},
Write {
#[bw(calc = Opcode::WRITE)]
opcode: Opcode,
address: Address,
size: Size,
data: Data,
},
Erase {
#[bw(calc = Opcode::ERASE)]
opcode: Opcode,
address: Address,
},
Go(#[bw(calc = Opcode::GO)] Opcode, Address),
WriteLock(#[bw(calc = Opcode::WRITE_LOCK)] Opcode),
WriteUnlock(#[bw(calc = Opcode::WRITE_UNLOCK)] Opcode),
ReadLock(#[bw(calc = Opcode::READ_LOCK)] Opcode),
ReadUnlock(#[bw(calc = Opcode::READ_UNLOCK)] Opcode),
#[bw(magic = 0x7fu8)]
Synchronize,
}
#[derive(BinRead, Debug, Clone, Copy)]
enum Reply {
#[brw(magic = 0x79u8)]
Ack,
#[brw(magic = 0x1fu8)]
NAck,
#[brw(magic = 0xaau8)]
Busy,
}
#[binread]
#[derive(Debug, Clone)]
struct ChipInfo {
#[br(temp)]
opcode_count: u8,
version: u8,
#[br(count = opcode_count, map = |data: Vec| { data.into_iter().map(|v| v.into()).collect() } )]
opcodes: Vec,
}
impl ChipInfo {
fn major(&self) -> u8 { self.version >> 4 }
fn minor(&self) -> u8 { self.version & 0xf }
fn supported_opcodes(&self) -> &[Opcode] { &self.opcodes }
}
```
## 断言 RTS 来复位 MCU
我们可以用断言 `request_to_send` 一段时间来对 MCU 重置,配合其它型号的 CH340 可以实现完全自动烧录.
```rust
fn reset(&mut self) -> Result {
use std::thread::sleep;
self.port.write_request_to_send(false)?;
sleep(Duration::from_millis(5));
self.port.write_request_to_send(true)?;
sleep(Duration::from_millis(10));
self.port.write_request_to_send(false)?;
sleep(Duration::from_millis(10));
Ok(())
}
```
- 2025-04-03
-
加入了学习《直播回放: 嵌入式Rust入门基础知识、解析动手实战Rust的三个任务》,观看 嵌入式Rust入门基础知识、解析动手实战Rust的三个任务
-
加入了学习《直播回放: 嵌入式Rust入门基础知识、解析动手实战Rust的三个任务》,观看 答疑内容
- 2025-03-31
-
回复了主题帖:
【 嵌入式Rust修炼营】macOS高效安装rust-使用清华源
补充一下,cargo 也可以换源,在一些 crates 依赖比较多的项目上就可以节省时间了,具体的换源方法可以参考各镜像下的 crates.io 部分:
crates.io-index | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror
上海交通大学 Linux 用户组 软件源镜像服务
- 2025-03-25
-
发表了主题帖:
嵌入式 Rust 修炼营:初级修炼任务二「cat 文本输出小工具」
# 简介
使用 `std::fs` 模块和 `Read` 等 trait 打印文本文件(如 `Cargo.toml`)的每行数据(检验基本语法、文件系统接口,trait的理解)
我的修炼营 Git 仓库 https://codeberg.org/scgummy/eeworld-rust-2025
# 效果
命令行帮助
输出多个文件,以 `Cargo.toml` 和 `/etc/hosts` 为例
# 任务详情
复刻一个简单的 `cat` 小工具,为了方便起见这里就不再像 [嵌入式 Rust 修炼营:初级修炼任务一「使用朴素排序算法的 sort 小工具」](https://bbs.eeworld.com.cn/thread-1310228-1-1.html) 的复刻版 `sort` 一样默认从标准输入读取了.
# 任务实施
## Iterator 流式读取
相比较之前任务一的 `sort` 需要让按行读取的 `Iterator` 进行 `collect` 操作,我们的 `cat` 工具并不需要一口气读入全部数据,因此我们可以保留 `lines` 返回的 `Iterator` 然后直接利用 `for` 循环进行输出,以达到流式 I/O 操作,按需读取节省内存.
```rust
let files = options
.files
.iter()
.map(|p| File::open(p))
.collect::()
.context("cannot open file")?;
let lines = files.into_iter().flat_map(|r| BufReader::new(r).lines());
for line in lines {
println!("{}", line?);
}
```
# 总结
熟悉了如何利用 `Iterator` 实现流式的惰性按需加载,节省文件 I/O 的开销,项目源代码在 https://codeberg.org/scgummy/eeworld-rust-2025/src/branch/main/novice-cat
-
发表了主题帖:
嵌入式 Rust 修炼营:初级修炼任务一「使用朴素排序算法的 sort 小工具」
本帖最后由 scgummy 于 2025-3-25 09:11 编辑
# 简介
使用冒泡排序处理数组并打印(检验基本数据类型,数组、判断、循环等语法
我的修炼营 Git 仓库 https://codeberg.org/scgummy/eeworld-rust-2025
# 效果
命令行帮助
字典序模式
数字模式
逆序模式
# 任务详情
我们准备复刻一个极简的类似于 GNU `coreutils` 体验的 `sort` 工具,其命令行参数接受一些文件路径,读取每一行数据并排序输出(路径缺省时数据直接从标准输入读取). 比方说 `data.in` 中包含了名字列表,`sort` 默认就将其按字典序升序进行排序输出.
具体的排序算法在 Hunter 的 slides 上有展示(不过按这种非相邻的交换方式严格来说是选择排序,而不是冒泡排序)
我们打算支持原始 `sort` 的一些开关选项,分别是
`sort -n` 表示以数字的方式比较每一行数据,将从每一行以第一个读取的数字作为排序依据
`sort -r` 可以进行反序排序,由于默认为升序,`-r` 即以降序输出
# 任务实施
## CLI 选项
我们使用 `clap` 作为 CLI 选项解析库,只需要通过 derive 过程宏就可以声明式地解析 CLI 选项.
```rust
#[derive(Parser, Debug, Clone)]
#[clap(version, about)]
struct Cli {
/// Files to sort
files: Vec,
/// Number mode
#[clap(short, long)]
numeric_sort: bool,
/// Reverse the sort order
#[clap(short, long)]
reverse: bool,
}
```
然后就可以通过 `parse` 构造出解析后的选项
```rust
fn main() {
let options = Cli::parse();
}
```
## 通过 trait 为 slice 类型添加通用排序方法
在 Rust 中并不是所有的类型都能够比较大小的,数学上的序关系(偏序、全序)对应 Rust 的 trait 系统中的 `PartialOrd` 和 `Ord` 这两个 trait. 为了展示 Rust 常见的多态范式,我们希望能为任何全序的类型(即任意实例均可互相比较大小)的 mutable 切片实现一个朴素的排序方法.
为了让设计边界清晰,Rust 并不允许为 module 外的类型直接实现自定义方法. 事实上这也是大多数现代编程语言的共识,只不过许多语言通过继承来解决这一问题,而 Rust 和 Go 这类语言却偏爱组合,在 Rust 中就体现为 `into_inner` 的设计模式和 trait 系统.
我们可以编写 `NaiveSort` 的 `trait` 包含一个 `naive_sort` 的方法,然后为切片类型实现该 trait,这一设计模式称为 [extension traits](http://xion.io/post/code/rust-extension-traits.html).
```rust
trait NaiveSort {
fn naive_sort(&mut self);
}
impl NaiveSort for [T] {
fn naive_sort(&mut self) {
for i in 0..self.len() {
for j in i + 1..self.len() {
if self[j] < self {
self.swap(i, j);
}
}
}
}
}
```
## 单元测试
Rust 的编译器 `rustc` 自带了[测试框架](https://doc.rust-lang.org/rustc/tests/index.html),通过更加友好的 `cargo test` 子命令一键编译并运行所有测试,这大大降低了开发者编写测试的惰性,俗话说未测的代码就是错的,这也很大程度上敦促了开发者编写出健壮的代码并使用单元测试确保其可靠性.
这里我们针对 `NaiveSort` 编写一个简单的单元测试,其会在一个切片上分别调用我们的 `naive_sort` 以及标准库的 `sort` 并进行断言比较.
```rust
#[test]
fn simple() {
let data = [77, 1, -1, 2, -2, 3, -3, 4, 44];
let actual = {
let mut data = data.clone();
data.naive_sort();
data
};
let expected = {
let mut data = data.clone();
data.sort();
data
};
assert_eq!(actual, expected);
}
```
## 行结构
我们希望模仿 `sort -n` 的数字模式,其能够读取每一行的第一个数并保留整行文本进行排序. 为此我们可以实现一个 `OrdLine` 的结构,其携带一个支持 `Ord` trait 的 `tag` 字段以及整行文本. 这里事实上就非常类似 `into_inner` 的组合设计模式,我们基于 `tag` 字段支持的 trait 来实现结构自身的一些 trait,这样 `OrdLine` 在切片中就可以参与排序,同时每一行对应的文本将一起跟随移动.
```rust
struct OrdLine {
tag: Option,
line: String,
}
impl std::fmt::Display for OrdLine {
fn fmt(&self, f: &mut std::fmt::Formatter
- 2025-03-13
-
回复了主题帖:
嵌入式Rust修炼营入围名单来啦,一起度过嵌入式Rust学习时光的小伙伴集合啦~
个人信息无误
- 2025-02-26
-
回复了主题帖:
嵌入式Rust修炼营:动手写串口烧录工具和MCU例程,Rust达人Hunter直播带你入门Rust
参与理由 & 个人编程基础:
Rust 有非常多吸引人的优势:安全性(borrow checker、默认不可变等小特性)以及编译期宏魔法带来的高抽象低开销的高性能目标代码(最典型的就是 print! format! 与各类 derive 过程宏),非常适合现代高可靠性要求的嵌入式环境,此前使用 Rust 编写过一个 Linux 上 TUN 接口的三层隧道,同时也在 STM32G0 系列上用 embassy 框架写过 I2C 传感器采集的下位机
预估进度:初级、中级、高级
如探索过Rust,请说明Rust学习过程遇到难点,希望在参与活动中收获什么?
我觉得特别针对嵌入式 Rust 而言,最大的难点是要扭转以往在 C 等语言中随处可见的全局可变量;此外就是异步,至今也未能完全理解透彻;希望在活动中了解更多 Rust 在嵌入式环境中常见的设计模式,多踩坑多进步吧