rust嵌入式开发零基础入门教程(五)
好的,欢迎来到 Rust 嵌入式开发零基础入门教程的第五部分!在之前的教程中,我们已经掌握了环境搭建、Rust 核心概念以及通过 GPIO 和中断控制 LED 和按钮。现在,我们将进入嵌入式系统中的一个核心通信方式:串行通信,具体来说是 UART (Universal Asynchronous Receiver/Transmitter)。
14. 深入理解串行通信 (UART/USART)
串行通信是嵌入式设备与电脑、其他微控制器或外设(如 GPS 模块、蓝牙模块)进行数据交换最常用的方式之一。其中,UART 是最简单也是最常见的异步串行通信协议。
14.1 什么是 UART?
UART (Universal Asynchronous Receiver/Transmitter) 是一种硬件模块,用于在两个设备之间发送和接收数据。它的特点是:
异步 (Asynchronous):收发双方不需要共享同一个时钟信号。它们各自有自己的时钟,并通过数据中的起始位 (Start Bit) 和 停止位 (Stop Bit) 来同步。
点对点 (Point-to-Point):通常用于两个设备之间的直接通信。
全双工 (Full-Duplex):可以同时发送和接收数据,通过独立的发送 (TX) 和接收 (RX) 线实现。
低成本,简单实现:硬件开销小,软件实现相对简单。
14.2 UART 的工作原理
数据通过串行方式传输,即一次传输一个比特位。以下是发送和接收的基本步骤:
发送方:
数据帧化:并行数据(例如一个字节)被转换成串行数据流。
添加起始位:发送一个低电平(逻辑 0)作为起始位,通知接收方数据即将到来。
发送数据位:发送 5 到 9 个数据位(通常是 8 位),先发送最低位 (LSB) 或最高位 (MSB)。
添加奇偶校验位 (可选):用于错误检测。
添加停止位:发送 1 个、1.5 个或 2 个高电平(逻辑 1)作为停止位,表示一个数据帧结束。
空闲状态:在数据帧之间,线路保持高电平。
接收方:
检测到起始位的下降沿,开始接收。
根据预设的波特率 (Baud Rate),在每个比特位的中间采样电平。
接收所有数据位、奇偶校验位和停止位。
检查停止位和奇偶校验位,判断是否有错误。
将串行数据转换回并行数据。
14.3 关键参数
使用 UART 时,收发双方必须就以下参数达成一致:
波特率 (Baud Rate):每秒传输的比特数,例如 9600 bps, 115200 bps。这是最重要的参数,收发双方必须一致。
数据位 (Data Bits):每个数据帧中包含的数据位数,通常是 8 位。
奇偶校验位 (Parity Bit):可选,用于简单的错误检测(奇校验或偶校验)。
停止位 (Stop Bits):表示数据帧结束的位数,通常是 1 位。
在大多数嵌入式应用中,常见的配置是 8N1:8 数据位,无奇偶校验 (N),1 停止位。
14.4 UART 的引脚
通常,每个 UART 外设至少有两根引脚:
TX (Transmit):数据发送引脚。
RX (Receive):数据接收引脚。
在 STM32 微控制器上,通常会有多个 UART/USART 外设(例如 USART1, USART2, UART4 等),每个外设对应一组特定的 GPIO 引脚。你需要查阅芯片的数据手册或开发板原理图,确定哪个 GPIO 引脚对应你想要使用的 UART 的 TX 和 RX。
15. 编写 UART 回显 (Echo) 程序
我们将编写一个程序,通过 USB 转串口模块(或者开发板内置的虚拟串口)接收电脑发送过来的字符,然后将接收到的字符原样发送回电脑。这被称为“回显”程序。
15.1 硬件准备
STM32 Nucleo-64 系列开发板: (如 STM32F401RE 或 STM32F411RE)。这些板子通常内置一个 ST-Link 调试器,它也提供了一个虚拟 COM 端口 (VCP),可以通过 USB 线与电脑进行串行通信。你无需额外购买 USB 转串口模块。
TX (发送):PA2 (USART2_TX)
RX (接收):PA3 (USART2_RX)
串口终端软件: 在电脑上,你需要一个串口终端软件来发送和接收数据。
Windows: PuTTY, Xshell, SecureCRT, Termite 等。
macOS: CoolTerm, Screen (命令行工具,
screen /dev/cu.usbmodemXXXX 115200
)。Linux: minicom, picocom,
screen
。
15.2 修改 Cargo.toml
我们需要确保 Cargo.toml
中包含了正确的 HAL 库。如果没有,请添加或更新:
Ini, TOML
# hello-cortex-m/Cargo.toml[package]
name = "hello-cortex-m"
version = "0.1.0"
authors = ["Your Name <your@email.com>"]
edition = "2021"[dependencies]
cortex-m-rt = "0.7.0"
# panic-halt = "0.2.0" # 如果需要调试信息,也可以替换为 panic-probe 或 panic-semihosting
panic-halt = "0.2.0" # 保持简单,panic 时停止# --- 确保这一行存在且正确 ---
# 替换为你的芯片对应的 HAL 库及 features
# 例如,STM32F401RE 芯片
stm32f4xx-hal = { version = "0.15.0", features = ["stm32f401", "rt"] }
# --- 确保这一行存在且正确 ---# cortex-m-semihosting = "0.5.0" # 回显程序中通常通过 UART 打印,这个可以暂时注释掉
# cortex-m = "0.7.0" # 如果用 hprintln 需要
15.3 编写 src/main.rs
(UART 回显程序)
现在,打开 src/main.rs
文件,并将其内容替换为以下代码:
Rust
#![no_std] // 不使用 Rust 标准库
#![no_main] // 不使用 Rust 的默认 main 函数use panic_halt as _; // panic 时停止 CPU// 导入核心运行时
use cortex_m_rt::entry;
// 导入 HAL 库及外设模块
use stm32f4xx_hal::{pac,prelude::*,serial::{Config, DataBits, Parity, StopBits, Serial}, // 导入串口相关模块
};#[entry]
fn main() -> ! {// 1. 获取对外设的访问权限let dp = pac::Peripherals::take().unwrap();// let cp = cortex_m::Peripherals::take().unwrap(); // 回显程序不需要内核外设,可以省略// 2. 配置时钟let rcc = dp.RCC.constrain();// 注意:这里的时钟配置需要和你的开发板以及串口波特率计算匹配// 对于 F401RE,84MHz 的系统时钟,USART2 默认连接到 APB1 总线 (通常是 42MHz)// 115200 波特率在 42MHz 下是可行的let clocks = rcc.cfgr.use_hse(8.MHz()).sysclk(84.MHz()).freeze();// 3. 配置 GPIO 引脚为 USART2 功能let gpioa = dp.GPIOA.split();// PA2: USART2_TX (发送) - 复用推挽输出let tx_pin = gpioa.pa2.into_alternate();// PA3: USART2_RX (接收) - 复用输入let rx_pin = gpioa.pa3.into_alternate();// 4. 配置并启用 USART2// 传入 USART2 外设实例、TX/RX 引脚、串口配置和时钟let serial = Serial::new(dp.USART2, // 选择 USART2 外设(tx_pin, rx_pin), // 传入 TX 和 RX 引脚Config::default() // 使用默认配置.baudrate(115_200.bps()) // 设置波特率为 115200.data_bits(DataBits::DataBits8) // 8 数据位.parity(Parity::ParityNone) // 无奇偶校验.stop_bits(StopBits::Stop1), // 1 停止位clocks, // 时钟信息).unwrap();// 将串口实例分解为发送器 (tx) 和接收器 (rx)let (mut tx, mut rx) = serial.split();// 5. 主循环:回显接收到的字符loop {// 尝试从串口接收一个字节// `read` 方法是非阻塞的,如果 RX 缓冲区没有数据会返回 `Err(nb::WouldBlock)`// `block!` 宏来自 `nb` crate,它会阻塞当前线程直到操作完成// 对于初学者,`read().unwrap()` 会在出错时 panic,实际项目中需要更完善的错误处理if let Ok(byte) = rx.read() {// 如果成功接收到字节,则将其发送回去// `write` 方法同样是非阻塞的,`block!` 宏会阻塞直到发送完成tx.write(byte).unwrap(); // 将接收到的字节发送回去}}
}
代码解释:
导入串口相关模块:
use stm32f4xx_hal::{serial::{Config, DataBits, Parity, StopBits, Serial}, ...};
:从 HAL 库中导入Serial
结构体、配置参数等。时钟配置 (
clocks
): 再次强调,时钟配置至关重要。UART 的波特率是根据外设总线时钟来计算的。对于 STM32F4xx 系列,USART2
通常连接到 APB1 总线。如果sysclk
是 84MHz,那么 APB1 的时钟通常是 42MHz(如果 APB1 分频器设置为 /2)。115200 bps 在 42MHz 下是常用的、精确度较好的波特率。GPIO 引脚复用配置:
tx_pin = gpioa.pa2.into_alternate();
:将 PA2 配置为复用功能 (Alternate Function)。对于 STM32,GPIO 引脚可以作为通用输入/输出,也可以被复用为其他外设(如 UART、SPI、I2C)的功能。into_alternate()
就是将其配置为复用功能模式。HAL 库会自动处理具体的复用功能选择。rx_pin = gpioa.pa3.into_alternate();
:同理,将 PA3 配置为复用功能。
初始化串口 (
Serial::new
):dp.USART2
: 传入要使用的 UART 外设实例。(tx_pin, rx_pin)
: 传入配置好的 TX 和 RX 引脚。Config::default().baudrate(115_200.bps()).data_bits(DataBits::DataBits8).parity(Parity::ParityNone).stop_bits(StopBits::Stop1)
: 配置串口的波特率、数据位、奇偶校验和停止位。这必须和你的电脑串口终端软件的设置一致!clocks
: 传入时钟配置,用于计算波特率分频值。
拆分串口实例 (
serial.split()
):let (mut tx, mut rx) = serial.split();
:Serial
实例被拆分为一个Tx
(发送器)和一个Rx
(接收器)对象,这样你就可以独立地进行发送和接收操作。主循环 (
loop
) 中的回显逻辑:if let Ok(byte) = rx.read()
: 尝试从rx
接收一个字节。read()
方法通常是非阻塞的,意味着如果当前没有数据,它不会等待,而是立即返回一个错误(nb::WouldBlock
)。这里我们用了if let Ok(...)
来处理成功接收的情况。tx.write(byte).unwrap();
: 如果成功接收到字节,就使用tx.write()
方法将其发送出去。unwrap()
在这里是为了简化代码,但在实际项目中,应该使用更健壮的错误处理方式(例如match
语句)。
16. 构建、烧录和测试
16.1 构建项目
保存 src/main.rs
文件后,在项目根目录(hello-cortex-m
)下,运行构建命令:
Bash
cargo build --release
如果编译成功,则生成固件文件。
16.2 烧录并运行
确保你的开发板已通过 USB 连接到电脑,并且 probe-run
配置正确:
Bash
cargo run --release
probe-run
会自动编译(如果需要),烧录,并运行你的程序。
16.3 测试 UART 回显功能
打开设备管理器 (Windows) / 运行
ls /dev/tty.*
(macOS) /ls /dev/ttyS*
或ls /dev/ttyUSB*
(Linux),查找你的开发板对应的串口号。对于 STM32 Nucleo 板,它通常会显示为 STMicroelectronics STLink Virtual COM Port,并分配一个 COM 端口号(例如
COMx
在 Windows 上,/dev/ttyACM0
或/dev/ttyUSB0
在 Linux 上,/dev/cu.usbmodemXXXX
在 macOS 上)。记住这个串口号。
打开你的串口终端软件 (如 PuTTY)。
选择 "Serial" 连接类型。
Serial line (串口号):输入你刚刚找到的串口号(例如
COMx
)。Speed (波特率):设置为
115200
(与代码中设置的115_200.bps()
保持一致)。Data bits (数据位):
8
Stop bits (停止位):
1
Parity (奇偶校验):
None
Flow control (流控制):
None
点击 "Open" 连接串口。
发送和接收数据:
在串口终端的输入区域输入一些字符(例如 "Hello, Rust!")。
按下回车或发送按钮。
你应该能在终端的输出区域看到你刚刚发送的字符被原样回显回来。
如果一切正常,恭喜你!你已经成功实现了 Rust 嵌入式项目中的 UART 串行通信。
恭喜你!
你已经掌握了 Rust 嵌入式开发中一个非常实用的技能:UART 串行通信!这是与外部世界交互的关键,为你的嵌入式项目打开了更多可能性。
下一步可以尝试:
实现简单的命令解析: 不仅仅是回显,尝试接收特定的命令字符串(例如 "LED_ON" 或 "LED_OFF"),然后根据命令执行相应的操作(例如点亮或熄灭 LED)。
发送传感器数据: 如果你有传感器(例如温度传感器,我们后续教程可能会涉及),可以将传感器数据通过 UART 发送到电脑进行显示。
探索其他 UART 配置: 尝试改变波特率、数据位、奇偶校验或停止位,并在代码和串口终端软件中同步修改,看看它们如何影响通信。
异步读取: 了解
nb
(non-blocking) crate 的WouldBlock
错误,并尝试在不阻塞loop
循环的情况下读取数据。
在下一个教程中,我们可能会深入探讨定时器或者更复杂的外设通信协议(如 I2C 或 SPI),这取决于大家的需求。