在这篇文章中,我们将深入探讨编程微控制器的关键概念。然后,为了进一步揭开它们的神秘面纱,我们将编写一个小的 Rust 程序并在 ARM 微控制器上运行它。
这就是峰值性能的样子。
系统可以实现的最理想化的物理形式之一是“盒子”。你给它一些输入,它给你一些输出。用户不需要知道胆量或实现细节——这是黑匣子的物理表现。
如果您想构建一个盒子,您可以先使用商品服务器硬件,将其与您的软件一起加载,然后在其上贴上徽标。但是如果你想做一个小盒子呢?或者对于商品硬件来说过于专业化的东西?
Blackmagic Mini Converter的内部
Blackmagic Mini Converters(如这款 HDMI 转 SDI 转换器)是使用定制 PCB 专门构建的。在它的字面上,有一个Xilinx Zynq 7000片上系统 (SoC)。该芯片具有两个 ARM Cortex A9 处理器、一些内存、类似 FPGA 的可编程逻辑以及内置的一些其他外围设备。
像这样的盒子通常不运行我们所知道的操作系统。那里没有Linux发行版。开发人员无需将可执行文件复制到文件系统,而是将软件直接“闪存”到芯片的闪存中。而不是通过系统调用与内核接口,软件直接通过读取和写入芯片数据表中定义的称为“寄存器”的地址与芯片接口。
你好,微控制器!我们将编写“Hello, world!” 嵌入式系统:闪烁的 LED。我们的目标:Arduino MKR Vidor 4000。我们不会使用 Arduino 库或开发工具,但 Arduino 硬件是一个很好的起点,因为它广泛可用并附带原理图。
这个 Arduino 特别有趣,因为它包含一个 ARM Cortex-M0 微控制器和一个 Intel Cyclone FPGA。不过,对于这篇文章,我们只会使用微控制器。
微控制器是ATSAMD21G18A。它的数据表将是我们的主要 API 参考。Cortex-M0 通用用户指南在这里也很有用。根据数据表:
释放复位后,CPU 开始从复位地址 0x00000000 获取 PC 和 SP 值。
Cortex 用户指南更具体地说明:
复位时,处理器从地址 0x00000000 加载 MSP。
复位时,处理器向 PC 加载复位向量的值,即地址 0x00000004。
所以我们要做的就是……
闪烁
- 将我们的程序放入微控制器的闪存中。
- 在地址 0x00000000 处放置一个指向堆栈的指针。
- 将指向我们程序的指针放在地址 0x00000004 处。
代码是如何进入芯片的?根据数据表,它是使用双引脚串行线调试 (SWD) 编程完成的。要使用 SWD 接口对其进行编程,我们需要获取一个调试器并将其物理连接到芯片。Arduino MKR 板上有裸露的焊盘,如果您愿意焊接或装配设备以将电线固定在其上,可用于此目的:
但这会很痛苦,而且 Arduinos 通常是通过 USB 编程的。这是由 Arduino 引导加载程序启用的。
引导加载程序通常是您希望在微控制器上安装的第一件事,因为它的主要工作是使设备编程更容易。Arduinos 带有一个预装的引导加载程序,如果用户双击重置按钮,它允许通过 USB 和 SAM-BA 协议读取或写入设备的内存。否则,它将跳转到地址 0x00002000 处的代码。
假设我们的微控制器预装了这个引导加载程序,我们的新目标是:
- 将我们的程序放入微控制器的闪存中。
- 在地址 0x00002000 处放置一个指向堆栈的指针。
- 将指向我们程序的指针放在地址 0x00002004 处。
我们可以通过双击重置按钮来做到这一点,然后使用 SAM-BA 协议来操作内存。
该程序我们没有与之交互的内核。这意味着没有进程、没有线程、没有文件系统、没有网络等等。我们根本不能使用 Rust 标准库。而且我们不能使用通常的main入口点,因为我们只需要将入口点放在正确的位置。如果考虑到这些因素,我们将我们的 hello world 程序存根,它看起来像这样:
#![no_std] #![no_main] fn enable_led() { unimplemented !() } fn turn_led_on() { unimplemented !() } fn turn_led_off() { unimplemented !() } fn delay() { unimplemented !() } fn 运行()->!{ enable_led(); 循环 { turn_led_on(); 延迟(); turn_led_off(); 延迟(); } }
为了编译它,我们还必须定义一个恐慌处理程序:
#[panic_handler] fn panic_handler(_info: &core::panic::PanicInfo) -> ! { // 我们现在可以做的不多... loop {} }
第一个存根是enable_led. 我们使用的 Arduino 有一个内置在板上的 LED,连接到引脚“PB08”。在我们可以控制它之前,我们需要将它配置为输出。就像我们将与微控制器进行的所有其他交互一样,这是通过写入预定义内存位置的寄存器来完成的。具体来说,要控制微控制器的引脚,我们需要写入“PORT”外设的寄存器。SAMD21 为 PORT 定义寄存器和地址,如下所示:
#[repr(C)] pub struct PortGroup { pub data_direction: u32, pub data_direction_clear: u32, pub data_direction_set: u32, pub data_direction_toggle: u32, pub data_output_value: u32, pub data_output_value_clear: u32, pub data_output_value_set: u32, pub data_output_uvalue_toggle: , pub data_input_value: u32, pub control: u32, pub write_configuration: u32, pub reserved_0: [u8; 4]、 pub peripheral_multiplexing: [u8; 16], pub pin_configuration: [u8; 32], 酒吧保留_1:[u8;32], } #[repr(C)] pub struct Port { pub groups: [PortGroup; 2], 常量PORT_ADDRESS : u32 = 0x41004400;
引脚分为两组(A 和 B),因此要将 PB08 配置为输出,我们使用core::ptr::write_volatile写入寄存器(我们使用Rust 核心库代替标准库。) :
unsafe fn enable_led() { let port = &mut *(PORT_ADDRESS as *mut Port); write_volatile(&mut port.groups[1].pin_configuration[8], 0b00000010); write_volatile(&mut port.groups[1].data_direction_set, 1 << 8); }
通过写入这些寄存器也可以启用和禁用 LED:
unsafe fn turn_led_on() { let port = &mut *(PORT_ADDRESS as *mut Port); write_volatile( &mut port.groups[1].data_output_value_clear as *mut u32, 1 << 8, ) } unsafe fn turn_led_off() { let port = &mut *(PORT_ADDRESS as *mut Port); write_volatile( &mut port.groups[1].data_output_value_set as *mut u32, 1 << 8, ) }
最后,实施延迟……
不安全的 fn 延迟() { for i in 0..100000 { read_volatile(&i); } }
这只是一个繁忙的循环。但由于我们在微控制器上运行,因此这是一个非常可预测的繁忙循环。一旦编译完成,它总是会花费完全相同的时间来执行。
内存布局我们需要一个指向我们代码的指针,使其位于一个非常特定的地址(0x00002004)。我们还需要将堆栈指针放在一个非常具体的地址(0x00002000)。这些地址实际上是称为异常表的更大指针列表的一部分:
在代码中,这看起来像这样:
pub type Handler = unsafe extern "C" fn(); #[repr(C, packed)] pub struct ExceptionTable { pub initial_stack: *const u32, pub reset: unsafe extern "C" fn() -> !, pub nmi: Handler, pub hard_fault: unsafe extern "C" fn( ) -> !, pub reserved_0: [Option<Handler>; 7]、 pub sv_call: Handler, pub reserved_1: [Option<Handler>; 2], pub pend_sv: Handler, pub sys_tick: Option<Handler>, pub external: [Option<Handler>; 32], }
我们不需要定义任何这些,除了复位处理程序和初始堆栈指针,它指向芯片 RAM 的末尾:
不安全的外部“C” fn reset_handler() -> !{ run() } unsafe extern "C" fn nop_handler() {} unsafe extern "C" fn trap() -> !{ loop {} } #[link_section = ".isr_vector"] pub static ISR_VECTORS: ExceptionTable = ExceptionTable { initial_stack: 0x20008000 as _, reset: reset_handler, nmi: nop_handler, hard_fault: trap, reserved_0: [None; 7]、 sv_call:nop_handler、 reserved_1:[无;2], pend_sv:nop_handler, sys_tick:无, 外部:[无;32], };
现在我们只需要告诉链接器将我们指定为“.isr_vector”的部分放在地址0x00002000 处。这是使用链接描述文件完成的:
MEMORY { FLASH_FPGA (r) : ORIGIN = 0x40000, LENGTH = 2M FLASH (rx) : ORIGIN = 0x2000, LENGTH = 0x00040000-0x2000 /* 引导加载程序使用的前 8KB */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x0000800 }} ENTRY(reset_handler) 部分{ .text : { . =对齐(0x2000); KEEP(*(.isr_vector)) *(.text*) } > FLASH /DISCARD/ : { *(.ARM.exidx .ARM.exidx.*); } }
可以将此文件提供给链接器,以告诉它如何布置所有内容。
跑步您可以通过像这样传递目标和链接器脚本来使用 Cargo 构建程序:
RUSTFLAGS="-Clink-arg=-Tlayout.ld" 货物构建 --target thumbv6m-none-eabi
安装ARM 嵌入式工具链后,您可以将其从 ELF 可执行文件转换为二进制文件,您可以使用以下命令刷新微控制器:
arm-none-eabi-objcopy -O 二进制目标/thumbv6m-none-eabi/debug/hello-microcontroller program.bin
最后,如果您双击 Arduino 的重置按钮,您可以使用BOSSA对其进行闪烁:
bossac -i -d --port /dev/cu.usbmodem* -o 0x2000 -e -o 0x2000 -w program.bin -R
你好,微控制器!
下一步,
- 从我们的GitHub 存储库中查看代码并自己运行它。
- 查看一些很棒的嵌入式 Rust 资源。
- 如果您对构建优雅的定制硬件感兴趣,请下载 Arduino 的原理图并构建自定义 PCB。
- 如果您喜欢从事此类工作,请加入 Tempus Ex并获得报酬,以使用 Rust 构建很酷的东西。