rust·嵌入式开发

 

本文简介

本文简介

工具

  • rust
  • 交叉编译工具链

    # Cortex-M3
    rustup target add thumbv7m-none-eabi
    
    # Cortex-M4/M7
    rustup target add thumbv7em-none-eabi
    
  • cargo-generate
    • 生成嵌入式开发目录结构的工具
    • 设置了基本的链接脚本、链接选项
    • 安装

      cargo install cargo-generate
      
  • cargo-binutils
    • 嵌入式开发必需的二进制工具
    • 比如objectdumpnmsize等工具
    • 安装

      cargo install cargo-binutils
      rustup component add llvm-tools-preview
      
  • qemu-system-arm
    • 仿真ARM系统
  • GDB
    • 调试器
    • linux

      sudo apt install gdb-multiarch openocd qemu-system-arm
      
    • MacOS

      $ # GDB
      $ brew install armmbed/formulae/arm-none-eabi-gcc
      
      $ # OpenOCD
      $ brew install openocd
      
      $ # QEMU
      $ brew install qemu
      
      • 如果openocd安装失败,就安装最新版本的

        $ brew install --HEAD openocd
        

QEMU

  • 从github上下载工程样本

    cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
    
    • 将工程命名为 app
  • main.rs解读

    #![no_std]
    #![no_main]
    
    use panic_halt as _;
    
    use cortex_m_rt::entry;
    
    #[entry]
    fn main() -> ! {
        loop {
            // your code goes here
        }
    }
    
    • 不使用标准库
    • 不使用标准main函数,它是为命令行准备的
    • 引入库 panic_halt,它规定了界定 panic 行为的处理程序
    • entrycortex_m_rt 提供的属性,是程序的入口
    • main的返回值是 !,因为嵌入式程序不能退出
  • 交叉编译
    • .cargo/config.toml 中指定编译的对象,因为是cortex-m3:

      [build]
      # Pick ONE of these compilation targets
      # target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
      target = "thumbv7m-none-eabi"    # Cortex-M3
      # target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
      # target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
      
    • 编译

      cargo build
      
      # 命令行指定对象
      cargo build --target thumbv7m-none-eabi
      
  • 使用二进制工具检查

    # 打印 ELF 头
    cargo readobj --bin app -- --file-headers
    
    # 二进制的大小
    cargo size --bin app --release -- -A
    
    # 反汇编
    cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hex
    
    • --bin app 是检查二进制 target/$TRIPLE/debug/app 的语法糖
  • 在 QEMU 上运行

    • 编译

      cargo build --example hello
      
    • 运行

      qemu-system-arm \
        -cpu cortex-m3 \
        -machine lm3s6965evb \
        -nographic \
        -semihosting-config enable=on,target=native \
        -kernel target/thumbv7m-none-eabi/debug/examples/hello
      
      • 命令应该在打印文本后成功退出(exit code = 0)。
      • 检查: echo $0
      • -semihosting-config:半主机允许模拟设备使用主机 stdout、 stderr 和 stdin 并在主机上创建文件
    • 自动运行,编辑 .cargo/config.toml

      [target.thumbv7m-none-eabi]
      # uncomment this to make `cargo run` execute programs on QEMU
      runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
      
      • 运行命令

        cargo run --example hello --release
        
  • 调试
    • 启动QEMU

      qemu-system-arm \
        -cpu cortex-m3 \
        -machine lm3s6965evb \
        -nographic \
        -semihosting-config enable=on,target=native \
        -gdb tcp::3333 \
        -S \
        -kernel target/thumbv7m-none-eabi/debug/examples/hello
      
      • QEMU 在 TCP:3333 上等待 GDB 的连接
      • -S 启动时冻结
    • 启动GDB

      gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello
      
      • 在 QEMU 中连接服务端

        target remote :3333
        

MCU 板级包层次

MCU

  • Micro-architecture
    • MCU 内核的通用例程(启用/禁用中断)和通用外设(SysTick外设)
    • 比如 cortex-m
  • PAC
    • cortex-m 内核包含的基本外设
    • 比如 tm4c123x
    • 举例

      #![no_std]
      #![no_main]
      
      use panic_halt as _; // panic handler
      
      use cortex_m_rt::entry;
      use tm4c123x;
      
      #[entry]
      pub fn init() -> (Delay, Leds) {
          let cp = cortex_m::Peripherals::take().unwrap();
          let p = tm4c123x::Peripherals::take().unwrap();
      
          let pwm = p.PWM0;
          pwm.ctl.write(|w| w.globalsync0().clear_bit());
          // Mode = 1 => Count up/down mode
          pwm._2_ctl.write(|w| w.enable().set_bit().mode().set_bit());
          pwm._2_gena.write(|w| w.actcmpau().zero().actcmpad().one());
          // 528 cycles (264 up and down) = 4 loops per video line (2112 cycles)
          pwm._2_load.write(|w| unsafe { w.load().bits(263) });
          pwm._2_cmpa.write(|w| unsafe { w.compa().bits(64) });
          pwm.enable.write(|w| w.pwm4en().set_bit());
      }
      
      • read()函数返回一个对象
        • 该对象提供对此寄存器中各个子字段的只读访问
        • 这些子字段由该芯片的制造商的 SVD 文件定义
        • tm4c123x文档
      • write()函数使用带有单个参数的闭包
        • 这个参数提供对这个寄存器中的各个子字段的读写访问
        • 注意:我们没有设置的所有子字段都将被设置为默认值——寄存器中的任何现有内容都将丢失
      • modify() 只更改这个寄存器中的一个特定子字段,并保持其他子字段不变
        • 函数接受带有两个参数的闭包-一个用于读取,一个用于写入
  • HAL
    • 硬件的共用 trait,比如串口、GPIO等
    • 通过为 PAC 公开的原始结构实现自定义 Trait 来工作
    • 比如为单个外设实现 constrain() 函数
    • 为包含多个pin的GPIO端口定义一个 split() 函数
    • 举例

      let p = hal::Peripherals::take().unwrap();
      // 将SYSCTL结构包装成具有更高层次API的对象
      let mut sc = p.SYSCTL.constrain();
      
      let mut porta = p.GPIO_PORTA.split(&sc.power_control);
      
  • BSC
    • 板级支持包
    • 比 HAL 更近一步,预先配置好各种外设和GPIO引脚,以支持当下的板子
    • 比如:stm32f3-discovery开发板的BSC

半主机

  • 半主机是一种允许嵌入式设备在主机上执行 I/O 操作的机制,主要用于将消息记录到主机控制台
  • cortex-m-semihosting 提供了一个 API,用于在 Cortex-M 设备上执行半托管操作

    #![no_main]
    #![no_std]
    
    use panic_halt as _;
    
    use cortex_m_rt::entry;
    use cortex_m_semihosting::{debug, hprintln};
    
    #[entry]
    fn main() -> ! {
        hprintln!("hello world").unwrap();
    
        let roses = "blue";
    
        if roses == "red" {
            debug::exit(debug::EXIT_SUCCESS);
        } else {
            debug::exit(debug::EXIT_FAILURE);
        }
    
        loop {}
    }
    
    • 可以将 panic 设置为 exit(EXIT_FAILURE),这样就可以在 QEMU 上运行 assert 的测试

      #![no_main]
      #![no_std]
      
      use panic_semihosting as _; // features = ["exit"]
      
      use cortex_m_rt::entry;
      use cortex_m_semihosting::debug;
      
      #[entry]
      fn main() -> ! {
          let roses = "blue";
      
          assert_eq!(roses, "red");
      
          loop {}
      }
      
      
      • 注意在 Cargo.toml 中引入 panic-semihosting

        panic-semihosting = { version = "VERSION", features = ["exit"] }
        

panic

  • 标准库的 panic 定义了行为:展开 panic 的线程堆栈
  • 对于 no_std 需要声明一个 #[panic_handler] 属性的函数来定义行为
    • 且函数的签名为 fn(&PanicInfo) -> !
  • 可以在 crates.io 中搜索关键字 panic-handler
  • 可以在程序中指定链接到的包来选择 panic 的行为

    #![no_main]
    #![no_std]
    
    // dev profile: easier to debug panics; can put a breakpoint on `rust_begin_unwind`
    #[cfg(debug_assertions)]
    use panic_halt as _;
    
    // release profile: minimize the binary size of the application
    #[cfg(not(debug_assertions))]
    use panic_abort as _;
    
    // ..
    

exceptions

  • 异常和中断是处理器处理异步事件和致命错误(例如执行无效指令)的硬件机制
  • cortex_m_rt 提供了一个异常属性来声明异常处理程序

    // Exception handler for the SysTick (System Timer) exception
    #[exception]
    fn SysTick() {
        // ..
        static mut COUNT: u32 = 0;
    
        // `COUNT` has transformed to type `&mut u32` and it's safe to use
        *COUNT += 1;
    }
    
    • cortex_m_rtexception 宏可以让在处理程序中声明的静态 mut 可以安全使用
  • 在异常中断的打印

    #![deny(unsafe_code)]
    #![no_main]
    #![no_std]
    
    use panic_halt as _;
    
    use core::fmt::Write;
    
    use cortex_m::peripheral::syst::SystClkSource;
    use cortex_m_rt::{entry, exception};
    use cortex_m_semihosting::{
        debug,
        hio::{self, HStdout},
    };
    
    #[entry]
    fn main() -> ! {
        let p = cortex_m::Peripherals::take().unwrap();
        let mut syst = p.SYST;
    
        // configures the system timer to trigger a SysTick exception every second
        syst.set_clock_source(SystClkSource::Core);
        // this is configured for the LM3S6965 which has a default CPU clock of 12 MHz
        syst.set_reload(12_000_000);
        syst.clear_current();
        syst.enable_counter();
        syst.enable_interrupt();
    
        // read a nonexistent memory location
        unsafe {
            // 引发一个 HardFault 异常
            ptr::read_volatile(0x3FFF_FFFE as *const u32);
        }
    
        loop {}
    }
    
    #[exception]
    fn SysTick() {
        static mut COUNT: u32 = 0;
        static mut STDOUT: Option<HStdout> = None;
    
        *COUNT += 1;
    
        // Lazy initialization
        if STDOUT.is_none() {
            *STDOUT = hio::hstdout().ok();
        }
    
        if let Some(hstdout) = STDOUT.as_mut() {
            write!(hstdout, "{}", *COUNT).ok();
        }
    
        // IMPORTANT omit this `if` block if running on real hardware or your
        // debugger will end in an inconsistent state
        if *COUNT == 9 {
            // This will terminate the QEMU process
            debug::exit(debug::EXIT_SUCCESS);
        }
    }
    
    #[exception]
    fn HardFault(ef: &ExceptionFrame) -> ! {
        if let Ok(mut hstdout) = hio::hstdout() {
            writeln!(hstdout, "{:#?}", ef).ok();
        }
    
        loop {}
    }
    

中断

  • 异常是由 Cortex-M 架构定义的,但中断在命名和功能上总是特定于供应商(通常甚至是芯片)的实现
  • 中断初始化步骤
    • 配置外设,生成中断请求
    • 中断控制器中设置中断优先级
    • 启用中断控制器中的中断处理程序
  • cortex_m_rt 提供了 interrupt 属性

    #[interrupt]
    fn TIM2() {
        static mut COUNT: u32 = 0;
    
        // `COUNT` has type `&mut u32` and it's safe to use
        *COUNT += 1;
    }
    

外设

  • MCU 核心是 CPU、RAM和FLASH
  • MCU 与外界的交互是通过外设
  • 线性和真实内存空间
    • 桌面系统对内存的访问是通过 MMU 控制的(访问权限、地址映射)
    • MCU 没有映射
    • 外设的寄存器也是映射到内存地址上的

控制外设

  • 对于 systick 这外设,定义结构体

    #[repr(C)]
    struct SysTick {
      pub csr: u32, 
      pub rvr: u32,
      pub cvr: u32,
      pub calib: u32,
    }
    
    let systick = 0XE000_E010 as *mut SysTick;
    let time = unsafe { (*systick).cvr };
    
    • 问题
      • 没有单例模式
      • unsafe 代码
      • 没有分离 WO/RO
      • 编译器优化,需要 volaitle 修饰,否则不工作

        let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };
        
        • 借助工具包

          use volaitle_register::{RW, RO};
          
          struct SysTick {
            pub csr: RW<u32>, 
            pub rvr: RW<u32>,
            pub cvr: RW<u32>,
            pub calib: RO<u32>,
          }
          
          fn get_systick() -> &'static mut SysTick {
            unsafe {&mut *(0XE000_E010 as *mut SysTick)}
          }
          
          fn get_time() -> u32 {
              let systick = get_systick();
              systick.cvr.read()
          }
          

单例模式

  • 用全局变量

    static mut THE_SERIAL_PORT: SerialPort = SerialPort;
    
    fn main() {
        let _ = unsafe {
            THE_SERIAL_PORT.read_speed();
        };
    }
    
    • 全局变量数据竞争
    • unsafe,导致 rust 的借用检查器不会工作(跟踪它的引用和所有权)
  • Rust 的全局变量思路
    • 创建一个全局变量,它包含所有的外设,而不是一个外设一个全局变量
    struct Peripherals {
        serial: Option<SerialPort>,
    }
    impl Peripherals {
        fn take_serial(&mut self) -> SerialPort {
            let p = replace(&mut self.serial, None);
            p.unwrap()
        }
    }
    static mut PERIPHERALS: Peripherals = Peripherals {
        serial: Some(SerialPort),
    };
    
    • 使用 take() 这样多次使用时会 panic
  • 像对待数据一样对待硬件
    • 把硬件分成 RO(&self)/WO(&mut self)

类型系统

  • rust的类型系统可以在编译时就防止数据竞争、检查数据属性
  • 可以利用类型系统在编译时的静态检查,强制完成I/O接口的转换、设置好引脚类型
  • 所有权应用于外设,确保只有某些程序可以修改外设

typestate 类型状态

思想:将系统的状态表达为类型,并将系统状态的变换表达为类型之间的转换

外设状态机

  • 外设可以看成状态机
  • GPIO引脚的配置可以看成一棵状态树

  • 外设在 disabled 状态要配置为 Input: High Resistance 状态,状态转换过程
    • disabled
    • enabled
    • Config as Input
    • Input: High Resistance
  • 在传统的设置过程中,我们只要把值写到寄存器就算完事儿
    • 问题
      • 我们可以修改各种寄存器(RO/RW)
      • 我们可以在GPIO是 Output 模式时,配置 Input 参数
  • 在 rust 中,通过类型系统设计要进行约束设计
    • 首先,可以在操作外设时,进行约束检查(不足:大量的运行时检查)

        /// GPIO interface
        struct GpioConfig {
            /// GPIO Configuration structure generated by svd2rust
            periph: GPIO_CONFIG,
        }
      
        impl GpioConfig {
            pub fn set_enable(&mut self, is_enabled: bool) {
                self.periph.modify(|_r, w| {
                    w.enable().set_bit(is_enabled)
                });
            }
      
            pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> {
                if self.periph.read().enable().bit_is_clear() {
                    // Must be enabled to set direction
                    return Err(());
                }
      
                self.periph.modify(|r, w| {
                    w.direction().set_bit(is_output)
                });
      
                Ok(())
            }
        }
      
    • 用类型系统改进,在编译时就进行检查

      • 思想:通过类型转换来表示状态转换,在编译期执行约束检查
      • 应用

          /*
          * Example 1: Unconfigured to High-Z input
          */
          let pin: GpioConfig<Disabled, _, _> = get_gpio();
        
          // pin 没有启用
          // pin.into_input_pull_down();
        
          // 将pin从unconfig转换为high-z的input模式
          let input_pin = pin.into_enabled_input();
        
          // 读取pin状态
          let pin_state = input_pin.bit_is_set();
        
          // 不能通过,input模式的pin没有这个接口
          // input_pin.set_bit(true);
        
          /*
          * Example 2: High-Z input 转换为 Pulled Low input
          */
          let pulled_low = input_pin.into_input_pull_down();
          let pin_state = pulled_low.bit_is_set();
        
          /*
          * Example 3: Pulled Low input to Output, set high
          */
          let output_pin = pulled_low.into_enabled_output();
          output_pin.set_bit(true);
        
          // Can't do this, output pins don't have this interface!
          // output_pin.into_input_pull_down();
        
      • 实现

          /// GPIO 接口
          struct GpioConfig<ENABLED, DIRECTION, MODE> {
              /// GPIO Configuration structure generated by svd2rust
              periph: GPIO_CONFIG,
              enabled: ENABLED,
              direction: DIRECTION,
              mode: MODE,
          }
        
          // Type states for MODE in GpioConfig
          struct Disabled;
          struct Enabled;
          struct Output;
          struct Input;
          struct PulledLow;
          struct PulledHigh;
          struct HighZ;
          struct DontCare;
        
          /// These functions may be used on any GPIO Pin
          impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> {
              pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare, DontCare> {
                  self.periph.modify(|_r, w| w.enable.disabled());
                  GpioConfig {
                      periph: self.periph, enabled: Disabled,
                      direction: DontCare, mode: DontCare,
                  }
              }
        
              pub fn into_enabled_input(self) -> GpioConfig<Enabled, Input, HighZ> {
                  self.periph.modify(|_r, w| {
                      w.enable.enabled()
                      .direction.input()
                      .input_mode.high_z()
                  });
                  GpioConfig {
                      periph: self.periph, enabled: Enabled,
                      direction: Input, mode: HighZ,
                  }
              }
        
              pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> {
                  self.periph.modify(|_r, w| {
                      w.enable.enabled()
                      .direction.output()
                      .input_mode.set_high()
                  });
                  GpioConfig {
                      periph: self.periph, enabled: Enabled,
                      direction: Output, mode: DontCare,
                  }
              }
          }
        
          /// This function may be used on an Output Pin
          impl GpioConfig<Enabled, Output, DontCare> {
              pub fn set_bit(&mut self, set_high: bool) {
                  self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
              }
          }
        
          /// These methods may be used on any enabled input GPIO
          impl<IN_MODE> GpioConfig<Enabled, Input, IN_MODE> {
              pub fn bit_is_set(&self) -> bool {
                  self.periph.read().input_status.bit_is_set()
              }
        
              pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
                  self.periph.modify(|_r, w| w.input_mode().high_z());
                  GpioConfig {
                      periph: self.periph,
                      enabled: Enabled,
                      direction: Input,
                      mode: HighZ,
                  }
              }
        
              pub fn into_input_pull_down(self) -> GpioConfig<Enabled, Input, PulledLow> {
                  self.periph.modify(|_r, w| w.input_mode().pull_low());
                  GpioConfig {
                      periph: self.periph,
                      enabled: Enabled,
                      direction: Input,
                      mode: PulledLow,
                  }
              }
        
              pub fn into_input_pull_up(self) -> GpioConfig<Enabled, Input, PulledHigh> {
                  self.periph.modify(|_r, w| w.input_mode().pull_high());
                  GpioConfig {
                      periph: self.periph,
                      enabled: Enabled,
                      direction: Input,
                      mode: PulledHigh,
                  }
              }
          }
        
  • 零成本抽象
    • 类型状态不包含实际数据,在运行时它们在内存中没有实际的表示形式

        use core::mem::size_of;
      
        let _ = size_of::<Enabled>();    // == 0
        let _ = size_of::<Input>();      // == 0
        let _ = size_of::<PulledHigh>(); // == 0
        let _ = size_of::<GpioConfig<Enabled, Input, PulledHigh>>(); // == 0
      
    • into_input_high_z接口,最后只是一条汇编指令

可移植性 HAL

  • HAL 是一组 trait,它在系统中的层级如下

    HAL

并发

  • 在嵌入式系统中,并发发生在
    • 中断
    • 多任务/线程
    • 多核
  • 全局可变数据
    • 因为嵌入式系统通常没有动态堆内存分配的功能,所以一般使用全局静态可变内存
    • 但使用 static mut 必须用 unsafe,会导致 rust 在编译期无法数据竞争
      • 对同一数据的读-改-写流程会被其他任务或中断介入
  • 临界区
    • 嵌入式采用临界区来处理数据竞争,它关闭中断,直到对数据的访问完成

        static mut COUNTER: u32 = 0;
      
        #[entry]
        fn main() -> ! {
            set_timer_1hz();
            let mut last_state = false;
            loop {
                let state = read_signal_level();
                // 当信号跳变时,COUNTER 加 1
                if state && !last_state {
                    // 用临界区保证对 COUNTER 的同步访问
                    cortex_m::interrupt::free(|_| { unsafe { COUNTER += 1 }; });
                }
                last_state = state;
            }
        }
      
        #[interrupt]
        fn timer() {
            // 这里不用临界区,因为 
            // 1. 不用读COUNTER的值
            // 2. 它永远不会被 main 中断
            unsafe { COUNTER = 0; }
        }
      
    • 问题

      • 大量使用了 unsafe
      • 随意使用了临界区,导致更高的中断延迟和抖动
      • 注意:在多核系统中,其他核并不会被打断,它仍然可以访问共享内存
  • 原子操作
    • thumbv7(Cortex-M3及以上)提供完整的 Compare 和 Swap (CAS)指令,为严格禁用所有中断提供了一个替代方案:
      • 我们可以尝试增量操作,它在大多数情况下会成功
      • 但是如果它被中断,它会自动重试整个增量操作
    • 利用原子操作来访问共享内存

        use core::sync::atomic::{AtomicUsize, Ordering};
      
        // COUNTER 成为了安全的全局静态变量
        // COUNTER 可以安全的从中断和线程中修改内存,且不用禁止中断
        static COUNTER: AtomicUsize = AtomicUsize::new(0);
      
        #[entry]
        fn main() -> ! {
            set_timer_1hz();
            let mut last_state = false;
            loop {
                let state = read_signal_level();
                if state && !last_state {
                    // Use `fetch_add` to atomically add 1 to COUNTER
                    COUNTER.fetch_add(1, Ordering::Relaxed);
                }
                last_state = state;
            }
        }
      
        #[interrupt]
        fn timer() {
            // Use `store` to write 0 directly to COUNTER
            COUNTER.store(0, Ordering::Relaxed)
        }
      
    • 问题
      • 不是面向对象的形式,不人性
  • 为COUNTER设计一个类型,把COUNTER包装在UnsafeCell

      use core::cell::UnsafeCell;
      use cortex_m::interrupt;
    
      // UnsafeCell是rust内部可变性的核心结构,Cell和RefCell是对它的包装
      // 有了内部可变性,就可以用 `static` 代替 `static mut`,但同样可以改变值
      struct CSCounter(UnsafeCell<u32>);
      impl CSCounter {
          // 显示的给出cs参数,说明这里是临界区(虽然代码没用到)
          pub fn reset(&self, _cs: &interrupt::CriticalSection) {
              unsafe { *self.0.get() = 0 };
          }
    
          pub fn increment(&self, _cs: &interrupt::CriticalSection) {
              unsafe { *self.0.get() += 1 };
          }
      }
    
      // 为了在中断和任务中共享,必须声明sync特性
      unsafe impl Sync for CSCounter {}
    
      // 因为 COUNTER 不再是 `static mut`,所以对它的访问不需要 `unsafe`
      static COUNTER: CSCounter = CSCounter(UnsafeCell::new(0));
    
      #[entry]
      fn main() -> ! {
          set_timer_1hz();
          let mut last_state = false;
          loop {
              let state = read_signal_level();
              if state && !last_state {
                  // No unsafe here!
                  interrupt::free(|cs| COUNTER.increment(cs));
              }
              last_state = state;
          }
      }
    
      #[interrupt]
      fn timer() {
          // 需要进入临界区得到cs
          interrupt::free(|cs| COUNTER.reset(cs));
    
          // 可以通过 `unsafe` 代码来生成一个cs,以减少开销
          // let cs = unsafe { interrupt::CriticalSection::new() };
      }
    
    • 问题
      • 我们用 UnsafeCell 和临界区实现了并发安全的机制,但这是非标的实现
      • 有没有更通用的标准设计方法?
  • 标准件:互斥锁
    • 线程可以尝试锁定(或获取)互斥对象
      • 要么立即成功
      • 要么阻塞等待锁定
      • 或者返回无法锁定互斥对象的错误
    • 问题
      • 中断中不允许阻塞
    • 解决
      • 使用临界区创建互斥锁
        use core::cell::Cell;
        use cortex_m::interrupt::Mutex;
      
        // Cell 返回的是一个拷贝,并且不能实现 Sync,所以它是安全的,不用 unsafe
        // 为了可 Sync,所以加入了 Mutex
        static COUNTER: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
      
        #[entry]
        fn main() -> ! {
            set_timer_1hz();
            let mut last_state = false;
            loop {
                let state = read_signal_level();
                if state && !last_state {
                    interrupt::free(|cs|
                        // mutex 借用 cs 实现互斥
                        COUNTER.borrow(cs).set(COUNTER.borrow(cs).get() + 1));
                }
                last_state = state;
            }
        }
      
        #[interrupt]
        fn timer() {
            // We still need to enter a critical section here to satisfy the Mutex.
            interrupt::free(|cs| COUNTER.borrow(cs).set(0));
        }
      
    • 问题
      • Cell对于简单类型支持很好,但不支持复杂类型,不支持非 Copy 类型
  • 使用 Mutex 和 RefCell 的组合
    • RefCell 使用引用而非副本

Collections

  • std 有 Vec, HashMap, String 等集合,
  • 嵌入式的core不提供内存分配功能,所以没有动态集合
    • 编译器带了 alloc
    • 第三方 heapless 库,提供固定内存的集合
  • crate alloc
    • main.rs 中引入

        #![feature(alloc)]
      
        extern crate alloc;
      
        use alloc::vec::Vec;
      
    • 为了能够使用任何集合,首先需要使用 global_allocator 属性来声明程序将使用的全局分配器
    • 《RUST嵌入式》 实现了一个全局内存分配器
    • 实现了 global_allocator 内存分配器和 alloc_error_handler 内存分配失败处理程序后,就可以使用 alloc 包中的集合了

        #[entry]
        fn main() -> ! {
            let mut xs = Vec::new();
      
            xs.push(42);
            assert!(xs.pop(), Some(42));
      
            loop {}
        }
      
  • 使用 heapless
    • 它的集合不依赖于全局内存分配器

        extern crate heapless; // v0.4.x
      
        use heapless::Vec;
        use heapless::consts::*;
      
        #[entry]
        fn main() -> ! {
            // 固定8个元素的空间
            let mut xs: Vec<_, 8> = Vec::new();
      
            // 它是在栈上分配空间的,也可以重定位到全局静态变量上
            // 每次push都要检查是不是越界,因为它不会动态增加容量
            xs.push(42).unwrap();
            assert_eq!(xs.pop(), Some(42));
        }
      
  • allocheapless的优劣

嵌入式 C 开发的技巧

  • feature
    • #ifdef 等于 rust 中的 feature
    • rust中每个crate都例出了所有的feature,并且只能打开/关闭
    • 把crate放在Cargo.toml的依赖项中时,它的默认feature都打开
    • 为 Cargo.toml 中的每个组件声明一个 Cargo 特性

        [features]
        FIR = []
        IIR = []
      
    • 在代码中,使用 #[cfg(Feature="FIR")] 控制包含

        #![allow(unused)]
        fn main() {
        /// In your top-level lib.rs
      
        #[cfg(feature="FIR")]
        pub mod fir;
      
        #[cfg(feature="IIR")]
        pub mod iir;
        }
      
  • 编译期size和编译期计算
    • rust 的 ` const fn` 实现编译期计算

        #![allow(unused)]
        fn main() {
      
            const fn array_size() -> usize {
                #[cfg(feature="use_more_ram")]
                { 1024 }
                #[cfg(not(feature="use_more_ram"))]
                { 128 }
            }
      
            static BUF: [u32; array_size()] = [0u32; array_size()];
        }
      
  • rust的宏
  • Cargo 的构建脚本
    • 自定义构建过程
  • 交叉编译
    • Cargo 参数 --target thumbv6m-none-eabi 指定构建对象
    • 结果放在 target/thumbv6m-none-eabi/debug/myapp
  • 迭代器与数组访问
    • 在 rust 中对数组的每次访问,都会进行越界关断,效率低
    • 推荐用迭代器
  • 引用与指针
    • Rust 中,指针(称为原始指针)存在,但只在特定情况下使用,因为解引用总是 unsafe
    • Rust 使用由 & 符号表示引用,或由 & mut 表示可变引用
    • Rust 对引用有严格的规定,是 Rust 所有权系统的关键部分
      • 强制要求在任何给定的时间内,对同一值只能有一个可变引用或多个不可变引用
  • volatile 易失性访问
    • 在 C 语言中,单个变量可能被标记为 volatile
    • Rust 使用方法来访问

        static mut SIGNALLED: bool = false;
      
        #[interrupt]
        fn ISR() {
            unsafe { core::ptr::write_volatile(&mut SIGNALLED, true) };
        }
      
        fn driver() {
            loop {
                while unsafe { !core::ptr::read_volatile(&SIGNALLED) } {}
      
                unsafe { core::ptr::write_volatile(&mut SIGNALLED, false) };
                run_task();
            }
        }
      
  • Packed and Aligned Types
    • rust内存布局
    • Rust 用 struct 或 union 上的 repr 属性控制对齐
    • 与C语言一致的自动对齐(2字节)

        #[repr(C)]
        struct Foo { x: u16, y: u8, z: u16, }
      
        fn main() {
            let v = Foo { x: 0, y: 0, z: 0 };
            println!("{:p} {:p} {:p}", &v.x, &v.y, &v.z);
        }
      
        // 0x7fffd0d84c60 0x7fffd0d84c62 0x7fffd0d84c64
        // Ordering is preserved and the layout will not change over time.
        // `z` is two-byte aligned so a byte of padding exists between `y` and `z`.
      
    • 单字节对齐(不填充)

        #[repr(packed)]
        struct Foo { x: u16, y: u8, z: u16, }
      
        fn main() {
            let v = Foo { x: 0, y: 0, z: 0 };
            // References must always be aligned, so to check the addresses of the
            // struct's fields, we use `std::ptr::addr_of!()` to get a raw pointer
            // instead of just printing `&v.x`.
            let px = std::ptr::addr_of!(v.x);
            let py = std::ptr::addr_of!(v.y);
            let pz = std::ptr::addr_of!(v.z);
            println!("{:p} {:p} {:p}", px, py, pz);
        }
      
        // 0x7ffd33598490 0x7ffd33598492 0x7ffd33598493
        // No padding has been inserted between `y` and `z`, so now `z` is unaligned.
      
    • 指定对齐

        #[repr(C)]
        #[repr(align(4096))]
        struct Foo { x: u16, y: u8, z: u16, }
      
        fn main() {
            let v = Foo { x: 0, y: 0, z: 0 };
            let u = Foo { x: 0, y: 0, z: 0 };
            println!("{:p} {:p} {:p}", &v.x, &v.y, &v.z);
            println!("{:p} {:p} {:p}", &u.x, &u.y, &u.z);
        }
      
        // 0x7ffec909a000 0x7ffec909a002 0x7ffec909a004
        // 0x7ffec909b000 0x7ffec909b002 0x7ffec909b004
        // The two instances `u` and `v` have been placed on 4096-byte alignments,
        // evidenced by the `000` at the end of their addresses.
      

与其他语言的交互

  • rust和c的交互
    • std提供了std::ffistd::os::raw模块
    • core 使用第三方库 cstr_corecty
    • 数据转换

        unsafe fn foo(num: u32) {
            let c_num: c_uint = num;
            let r_num: u32 = c_num;
        }
      
  • 与make/cmake构建系统的交互
  • 与RTOS的交互

Rust 使用 C

  • 在rust中定义要用到的c接口
    • c语言的头文件

        /* File: cool.h */
        typedef struct CoolStruct {
            int x;
            int y;
        } CoolStruct;
      
        void cool_function(int i, char c, CoolStruct* cs);
      
    • 在rust中声明c数据和接口

        /* File: cool_bindings.rs */
        #[repr(C)]
        pub struct CoolStruct {
            pub x: cty::c_int,
            pub y: cty::c_int,
        }
      
        extern "C" {
            pub fn cool_function( i: cty::c_int, c: cty::c_char, cs: *mut CoolStruct);
        }
      
      • extern "C":声明一个使用 C ABI 的函数签名
    • 可以使用bindgen自动为rust完成声明
      • 收集所有C或c++头文件(定义了与Rust一起使用的接口或数据类型)
      • bindings.h 文件,#include … 第一步中收集的每个文件
      • 将这个 bindings.h 文件,以及任何用于编译你的代码的编译标志送入 bindgen
        • 使用 Builder.ctypes_prefix("cty") / --ctypes-prefix=cty
        • Builder.use_core() /--use-core 来使生成 #![no_std] 兼容的代码
      • bindgen将产生生成的Rust代码到终端窗口的输出
        • 这个文件可以被输送到你的项目中的一个文件,比如 bindings.rs
        • 你可以在你的Rust项目中使用这个文件来与作为外部库编译和链接的C/C++代码进行交互
        • 提示:如果你在生成的绑定中的类型以cty为前缀,别忘了使用cty crate
    • 构建 C/C++ 代码为 .a 文件
    • 通过 cc 库来构建
      • cc为宿主提供的编译器提供惯用的 Rust 接口
      • 使用 cc 来构建

          extern crate cc;
        
          fn main() {
              cc::Build::new()
                  .file("foo.c")
                  .compile("libfoo.a");
          }
        

C 使用 Rust

  • 创建一个库项目

      [lib]
      name = "your_crate"
      crate-type = ["cdylib"]      # Creates dynamic lib
      # crate-type = ["staticlib"] # Creates static lib
    
  • 创建 API
    • #[no_mangle]
      • 告知rust不要破坏符号名
    • extern "C"
      • 使用 C ABI 形式
    • API 形式

        #[no_mangle]
        pub extern "C" fn rust_function() {
      
        }
      
  • 构建
    • cargo 可以构建出 my_lib.so/my_lib.dll或者my_lib.a
  • C语言需要一个头文件来声明函数,可以使用工具 cbindgen 自动生成

      void rust_function();
    
  • C 中调用

      #include "my-rust-project.h"
    
      rust_function();
    

优化

  • 无优化
    • 这是默认设置
    • 当您调用 cargo build 时,使用开发dev配置文件
    • 此配置文件针对调试进行了优化,因此它启用调试信息,而不启用任何优化
    • 即它使用 -C opt level=0
    • 裸金属开发来说,debug 信息是零成本的,因为它不会占用 Flash/ROM 中的空间,推荐打开

        [profile.release]
        # symbols are nice and they don't increase the size on Flash
        debug = true
      
  • 优化依赖
    • profile-override的 cargo 特性可以优化所有依赖项的大小

        # ..
      
        # don't optimize the `cortex-m-rt` crate
        [profile.dev.package.cortex-m-rt] # +
        opt-level = 0 # +
      
        # but do optimize all the other dependencies
        [profile.dev.package."*"]
        codegen-units = 1 # better optimizations
        opt-level = "z"
      
  • 优化速度
    • 支持3级速度优化 opt-level = 1, 2 , 3,默认3
    • 2和3都以牺牲二进制大小为代价来优化速度
    • 3比级别2做更多的向量化和内联
    • 2、3会进行循环展开,26字节会展开成194字节
  • 优化大小
    • 优化等级:opt-level = "s" , "z",z更小

        [profile.release]
        # or "z"
        opt-level = "s"
      
    • 拉低 rust 内联操作的阈值
    • 低内联阈值会使 LLVM 错过优化机会(例如,消除死分支,对闭包的内联调用)
    • 可以手动设置内联阈值

        # .cargo/config.toml
      
        # this assumes that you are using the cortex-m-quickstart template
        [target.'cfg(all(target_arch = "arm", target_os = "none"))']
        rustflags = [
        # ..
        "-C", "inline-threshold=123", # +
        ]
      
    • 内联阈值

        opt-level = 3 内联阈值:275
        opt-level = 2 uses 225
        opt-level = "s" uses 75 
        opt-level = "z" uses 25