rust嵌入式开发零基础入门教程(五)

发布于:2025-07-24 ⋅ 阅读:(18) ⋅ 点赞:(0)

好的,欢迎来到 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 的工作原理

数据通过串行方式传输,即一次传输一个比特位。以下是发送和接收的基本步骤:

发送方:

  1. 数据帧化:并行数据(例如一个字节)被转换成串行数据流。

  2. 添加起始位:发送一个低电平(逻辑 0)作为起始位,通知接收方数据即将到来。

  3. 发送数据位:发送 5 到 9 个数据位(通常是 8 位),先发送最低位 (LSB) 或最高位 (MSB)。

  4. 添加奇偶校验位 (可选):用于错误检测。

  5. 添加停止位:发送 1 个、1.5 个或 2 个高电平(逻辑 1)作为停止位,表示一个数据帧结束。

  6. 空闲状态:在数据帧之间,线路保持高电平。

接收方:

  1. 检测到起始位的下降沿,开始接收。

  2. 根据预设的波特率 (Baud Rate),在每个比特位的中间采样电平。

  3. 接收所有数据位、奇偶校验位和停止位。

  4. 检查停止位和奇偶校验位,判断是否有错误。

  5. 将串行数据转换回并行数据。

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(); // 将接收到的字节发送回去
        }
    }
}

代码解释:

  1. 导入串口相关模块: use stm32f4xx_hal::{serial::{Config, DataBits, Parity, StopBits, Serial}, ...};:从 HAL 库中导入 Serial 结构体、配置参数等。

  2. 时钟配置 (clocks): 再次强调,时钟配置至关重要。UART 的波特率是根据外设总线时钟来计算的。对于 STM32F4xx 系列,USART2 通常连接到 APB1 总线。如果 sysclk 是 84MHz,那么 APB1 的时钟通常是 42MHz(如果 APB1 分频器设置为 /2)。115200 bps 在 42MHz 下是常用的、精确度较好的波特率。

  3. 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 配置为复用功能。

  4. 初始化串口 (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: 传入时钟配置,用于计算波特率分频值。

  5. 拆分串口实例 (serial.split()): let (mut tx, mut rx) = serial.split();Serial 实例被拆分为一个 Tx(发送器)和一个 Rx(接收器)对象,这样你就可以独立地进行发送和接收操作。

  6. 主循环 (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 回显功能

  1. 打开设备管理器 (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 上)。

    • 记住这个串口号。

  2. 打开你的串口终端软件 (如 PuTTY)。

    • 选择 "Serial" 连接类型。

    • Serial line (串口号):输入你刚刚找到的串口号(例如 COMx)。

    • Speed (波特率):设置为 115200(与代码中设置的 115_200.bps() 保持一致)。

    • Data bits (数据位)8

    • Stop bits (停止位)1

    • Parity (奇偶校验)None

    • Flow control (流控制)None

    • 点击 "Open" 连接串口。

  3. 发送和接收数据:

    • 在串口终端的输入区域输入一些字符(例如 "Hello, Rust!")。

    • 按下回车或发送按钮。

    • 你应该能在终端的输出区域看到你刚刚发送的字符被原样回显回来。

如果一切正常,恭喜你!你已经成功实现了 Rust 嵌入式项目中的 UART 串行通信。


恭喜你!

你已经掌握了 Rust 嵌入式开发中一个非常实用的技能:UART 串行通信!这是与外部世界交互的关键,为你的嵌入式项目打开了更多可能性。

下一步可以尝试:

  • 实现简单的命令解析: 不仅仅是回显,尝试接收特定的命令字符串(例如 "LED_ON" 或 "LED_OFF"),然后根据命令执行相应的操作(例如点亮或熄灭 LED)。

  • 发送传感器数据: 如果你有传感器(例如温度传感器,我们后续教程可能会涉及),可以将传感器数据通过 UART 发送到电脑进行显示。

  • 探索其他 UART 配置: 尝试改变波特率、数据位、奇偶校验或停止位,并在代码和串口终端软件中同步修改,看看它们如何影响通信。

  • 异步读取: 了解 nb (non-blocking) crate 的 WouldBlock 错误,并尝试在不阻塞 loop 循环的情况下读取数据。

在下一个教程中,我们可能会深入探讨定时器或者更复杂的外设通信协议(如 I2C 或 SPI),这取决于大家的需求。


网站公告

今日签到

点亮在社区的每一天
去签到