在這篇文章中,我們将深入探讨編程微控制器的關鍵概念。然後,為了進一步揭開它們的神秘面紗,我們将編寫一個小的 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 構建很酷的東西。
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!