Для того, чтобы подружить между собой указанные в заголовке технологии нам понадобятся:
- Свежий GNU ARM Embedded Toolchain
- System Workbench for STM32 (опционально)
- Свежий Eclipse CDT
- GNU ARM Eclipse Plugin
- Rust
- RustDT. Для комфортной разработки также рекомендуется установить Racer, Rainicorn и rustfmt.
Идея состоит в том, чтобы с скомпилировать написанную на Rust программу в библиотеку, которую можно будет слинковать с помощью тулчейна для ARM.
В итоге мы сможем даже вполне комфортно дебажить смешанный код на Rust и С.
1. Генерация проекта на C
Воспользуемся для этого утилитой STM32CubeMX. Для демо-проекта нам понадобится:
- SYS = Serial Wire (если у устройство подключено по SWD) либо JTAG
- USART2 в конфигурации Asynchronous
- Несколько пинов на одном порту в режиме GPIO_Output (назовем их LED_R, LED_G, LED_B)
Проверим настройки тактирования. Тут при желании можем указать тактирование от внешнего кварца и его частоту.
Сгенерируем проект. Назовем его “HwApi”, т.к. этот слой кода у нас будет представлять собой абстракцию над железом, который мы будем использовать при написании кода на Rust. В качестве IDE выбираем SW4STM32.
Если Workbench установлен, то можем открыть сгенерированный проект и проверить, что он успешно компилируется.
2. Создаем проект для свежей версии Eclipse
Хоть System Workbench и основан на Eclipse, нам придется создать новый проект в свежей мажорной версии Eclipse (Neon), т.к. RustDT несовместим с той версией Eclipse.
Также нам понадобится шаблон проекта, который устанавливается вместе с GNU ARM Eclipse Plugin.
Для того, чтобы успешно слинковать либу, сгенерированную rust компилятором, нам понадобится заранее установленная свежая версия GNU ARM Embedded Toolchain.
Начинаем процесс переноса проекта из System Workbench в Eclipse CDT. В интернете можно найти скрипты, которые этот процесс автоматизируют, но я буду это делать вручную, т.к. собираюсь переиспользовать HwApiLib в других проектах, изменяя только написанную на Rust часть кода.
Копируем следующие папки/файлы в новый проект:
- Drivers
- Inc
- Src
- startup
- STM32F103C8Tx_FLASH.ld
Если Workbench установлен, то разворачиваем два окна настроек проектов (из старого и нового Eclipse) так, чтобы было удобно копировать значения из одного окна в другое. Окна немного отличаются, поэтому при копировании ориентируемся на флаги, которые указаны в скобках.
Если Workbench не установлен, можно просто скопировать настройки со скриншотов, приложенных ниже.
Копируем Defined Symbols:
Пути к папкам, содержащие *.h файлы:
На вкладке “Optimization” можно включить оптимизацию Optimize size(-Os).
Далее указываем, что нам нужны все предупреждения компилятора:
Указываем путь к скрипту линкера + отмечаем чекбокс для удаления из результата линковки неиспользуемых в коде секций:
На следующей вкладке важно отметить чекбокс “Use newlib-nano” и вручную указать флаг -specs=nosys.specs
:
Указываем пути к папкам с файлами для компиляции:
Нажимаем Ок. После чего меняем расширение startup файла на заглавную .S, чтобы файл успешно подхватился компилятором. Проверяем, что проект компилируется.
Теперь нужно настроить дебаггер (Run — Debug Configurations — GDB OpenOCD Debugging). Создаем файл для OpenOCD с описанием железа, в котором будет запускаться программа (в моем случае файл называется STM32F103C8x_SWD.cfg):
source [find interface/stlink-v2.cfg]
set WORKAREASIZE 0x5000
transport select "hla_swd"
set CHIPNAME STM32F103C8Tx
source [find target/stm32f1x.cfg]
# use hardware reset, connect under reset
reset_config none
Если у вас используется другой микроконтроллер или другой способ подключения к нему, то корректный файл для OpenOCD можно сгенерировать в Workbench (с помощью Debugging options — Ac6).
В Config options указываем флаг -f и путь к созданному в предыдущем шаге файлу.
Жмем Debug. Проверяем, что дебаггер успешно залил код в микроконтроллер и началась отладка.
Пришло время создавать Rust проект.
Т.к. нам понадобятся инструкции компилятора, которые не поддерживаются в stable версии, нам нужно будет переключиться нам nightly версию компилятора, запустив в cmd следующие команды:
rustup update
rustup default nightly
Далее нужно получить текущую версию компилятора:
rustc -v --version
Затем склонировать себе исходники rust и переключится на коммит, который использовался для сборки этого компилятора (указан в commit-hash).
git clone git@github.com:rust-lang/rust.git
cd rust
git checkout cab4bff3de1a61472f3c2e7752ef54b87344d1c9
Следующим шагом скомпилируем необходимые нам библиотеки под ARM.
mkdir libs-arm
rustc -C opt-level=2 -Z no-landing-pads --target thumbv7m-none-eabi -g src/libcore/lib.rs --out-dir libs-arm --emit obj,link
rustc -C opt-level=2 -Z no-landing-pads --target thumbv7m-none-eabi -g src/liballoc/lib.rs --out-dir libs-arm -L libs-arm --emit obj,link
rustc -C opt-level=2 -Z no-landing-pads --target thumbv7m-none-eabi -g src/libstd_unicode/lib.rs --out-dir libs-arm -L libs-arm --emit obj,link
rustc -C opt-level=2 -Z no-landing-pads --target thumbv7m-none-eabi -g src/libcollections/lib.rs --out-dir libs-arm -L libs-arm --emit obj,link
В будущем, при каждом обновлении компилятора (rustup update) нужно будет переключаться на актуальную версию исходников и перекомпилировать библиотеки для ARM, иначе потеряется возможность дебажить код на rust.
Наконец-то можно приступить к создают Rust-проекта в eclipse.
Eclipse просит указать путь к компилятору, исходникам и утилитам для работы с rust-кодом.
Обычно эти компоненты можно найти в C:\Users\%username%\.cargo.
Rust src — путь к папке src в исходниках, которые мы скачали ранее.
Теперь основной код:
lib.rs
#![feature(macro_reexport)] #![feature(unboxed_closures)] #![feature(lang_items, asm)] #![no_std] #![feature(alloc, collections)] #![allow(dead_code)] #![allow(non_snake_case)] extern crate alloc; pub mod runtime_support; pub mod api; #[macro_reexport(vec, format)] pub extern crate collections; use api::*; #[no_mangle] pub extern fn demo_main_loop() -> ! { let usart2 = Stm32Usart::new(Stm32UsartDevice::Usart2); loop { let u2_byte = usart2.try_read_byte(); match u2_byte { Some(v) => { let c = v as char; match c { 'r' => { toggle_led(Stm32Led::Red); } 'g' => { toggle_led(Stm32Led::Green); } 'b' => { toggle_led(Stm32Led::Blue); } _ => { usart2.print("cmd not found"); } } } _ => {} } delay(1); } }
api.rs — прослойка для интеграции между собой Rust и C кода
use collections::Vec; extern { fn stm32_delay(millis: u32); fn usart2_send_string(str: *const u8, len: u16); fn usart2_send_byte(byte: u8); fn usart2_try_get_byte() -> i16; fn stm32_toggle_led(led: u8); fn stm32_enable_led(led: u8); fn stm32_disable_led(led: u8); } pub fn delay(millis: u32) { unsafe { stm32_delay(millis); } } #[derive(Copy, Clone)] pub enum Stm32UsartDevice { Usart2 } #[derive(Copy, Clone)] pub struct Stm32Usart { device: Stm32UsartDevice } impl Stm32Usart { pub fn new(device: Stm32UsartDevice) -> Stm32Usart { Stm32Usart { device: device } } pub fn print(&self, str: &str) { let bytes = str.bytes().collect::<Vec<u8>>(); self.print_bytes(bytes.as_slice()); } pub fn print_bytes(&self, bytes: &[u8]) { unsafe { match self.device { Stm32UsartDevice::Usart2 => usart2_send_string(bytes.as_ptr(), bytes.len() as u16) } } } pub fn println(&self, str: &str) { self.print(str); self.print("\r\n"); } pub fn send_byte(&self, byte: u8) { unsafe { match self.device { Stm32UsartDevice::Usart2 => usart2_send_byte(byte) } } } pub fn try_read_byte(&self) -> Option<u8> { unsafe { let r = usart2_try_get_byte(); if r == -1 { return None; } return Some(r as u8); } } } pub enum Stm32Led { Red, Green, Blue, Orange } impl Stm32Led { fn to_api(&self) -> u8 { match *self { Stm32Led::Green => 2, Stm32Led::Blue => 3, Stm32Led::Red => 1, Stm32Led::Orange => 0 } } } pub fn toggle_led(led: Stm32Led) { unsafe { stm32_toggle_led(led.to_api()); } } pub fn enable_led(led: Stm32Led) { unsafe { stm32_enable_led(led.to_api()); } } pub fn disable_led(led: Stm32Led) { unsafe { stm32_disable_led(led.to_api()); } }
runtime_support.rs — для поддержки низкоуровневых функций Rust
extern crate core; /// Call the debugger and halts execution. #[no_mangle] pub extern "C" fn abort() -> ! { loop {} } #[cfg(not(test))] #[inline(always)] /// NOP instruction pub fn nop() { unsafe { asm!("nop" :::: "volatile"); } } #[cfg(test)] /// NOP instruction (mock) pub fn nop() {} #[cfg(not(test))] #[inline(always)] /// WFI instruction pub fn wfi() { unsafe { asm!("wfi" :::: "volatile"); } } #[cfg(test)] /// WFI instruction (mock) pub fn wfi() {} #[lang = "panic_fmt"] fn panic_fmt(_: core::fmt::Arguments, _: &(&'static str, usize)) -> ! { loop {} } #[lang = "eh_personality"] extern "C" fn eh_personality() {} // Memory allocator support, via C's stdlib #[repr(u8)] #[allow(non_camel_case_types)] pub enum c_void { __variant1, __variant2, } extern "C" { pub fn malloc(size: u32) -> *mut c_void; pub fn realloc(p: *mut c_void, size: u32) -> *mut c_void; pub fn free(p: *mut c_void); } #[no_mangle] #[allow(unused_variables)] pub unsafe extern "C" fn __rust_allocate(size: usize, align: usize) -> *mut u8 { malloc(size as u32) as *mut u8 } #[no_mangle] #[allow(unused_variables)] pub unsafe extern "C" fn __rust_deallocate(ptr: *mut u8, old_size: usize, align: usize) { free(ptr as *mut c_void); } #[no_mangle] #[allow(unused_variables)] pub unsafe extern "C" fn __rust_reallocate(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> *mut u8 { realloc(ptr as *mut c_void, size as u32) as *mut u8 }
Также в корне проекта необходимо создать файл конфигурации целевой платформы
thumbv7m-none-eabi.json
{ "arch": "arm", "cpu": "cortex-m3", "data-layout": "e-m:e-p:32:32-i1:8:32-i8:8:32-i16:16:32-i64:64-v128:64:128-a:0:32-n32-S64", "disable-redzone": true, "executables": true, "llvm-target": "thumbv7m-none-eabi", "morestack": false, "os": "none", "relocation-model": "static", "target-endian": "little", "target-pointer-width": "32" }
Копируем в папку Rust проекта папку libs-arm содержащую скомпилированные для работы под ARM компоненты из стандартной библиотеки Rust.
Изменяем Debug target, так чтобы он запускал компиляцию с нужными нам параметрами
rustc -C opt-level=2 -Z no-landing-pads --target thumbv7m-none-eabi -g --crate-type lib -L libs-arm src/lib.rs --emit obj,link
Компилируем Rust-проект. В результате в папке проекта появится файл lib.o.
Теперь в С-проекте создаем файлы api.h/api.c, в которых объявляем и реализуем функции, которые используются в api.rs.
api.h
#ifndef SERIAL_DEMO_API_H_
#define SERIAL_DEMO_API_H_
#include "stm32f1xx_hal.h"
void stm32_delay(uint32_t milli);
void usart2_send_string(uint8_t* str, uint16_t len);
void usart2_send_byte(uint8_t byte);
int16_t usart2_try_get_byte(void);
void stm32_toggle_led(uint8_t led);
void stm32_enable_led(uint8_t led);
void stm32_disable_led(uint8_t led);
#endif
api.c
#include "api.h"
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_uart.h"
#include "main.h"
void stm32_delay(uint32_t milli) {
HAL_Delay(milli);
}
extern UART_HandleTypeDef huart2;
void usart2_send_string(uint8_t* str, uint16_t len) {
HAL_UART_Transmit(&huart2, str, len, 1000);
}
void usart2_send_byte(uint8_t byte) {
while (!(USART2->SR & UART_FLAG_TXE));
USART2->DR = (byte & 0xFF);
}
int16_t usart2_try_get_byte(void) {
volatile unsigned int vsr;
vsr = USART2->SR;
if (vsr & UART_FLAG_RXNE) {
USART2->SR &= ~(UART_FLAG_RXNE);
return (USART2->DR & 0x1FF);
}
return -1;
}
uint16_t stm32_led_to_pin(uint8_t led);
void stm32_toggle_led(uint8_t led) {
HAL_GPIO_TogglePin(LED_R_GPIO_Port, stm32_led_to_pin(led));
}
void stm32_enable_led(uint8_t led) {
HAL_GPIO_WritePin(LED_R_GPIO_Port, stm32_led_to_pin(led), GPIO_PIN_SET);
}
void stm32_disable_led(uint8_t led) {
HAL_GPIO_WritePin(LED_R_GPIO_Port, stm32_led_to_pin(led), GPIO_PIN_RESET);
}
uint16_t stm32_led_to_pin(uint8_t led) {
switch (led) {
case 1:
return LED_R_Pin;
case 2:
return LED_G_Pin;
case 3:
return LED_B_Pin;
default:
return LED_B_Pin;
}
}
Добавляем вызов demo_main_loop() внутри функции main.
main.c
...
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
demo_main_loop();
}
/* USER CODE END 3 */
...
Осталось всё слинковать. Для этого открываем свойства проекта на C и укажем линковщику где взять недостающие obj файлы.
Компилируем. Бинарник сильно прибавил в весе, но все еще умещается в STM32F103C8.
Запускаем Debug и видим, что Eclipse без проблем переходит из C-кода в Rust.
В завершении статьи хочу выразить благодарность авторам следующих постов, без них я бы не осилил этот процесс:
www.hashmismatch.net/pragmatic-bare-metal-rust
spin.atomicobject.com/2015/02/20/rust-language-c-embedded
github.com/japaric/rust-cross
Статью писал с надеждой на то, что это послужит дополнительным шагом в появлении комьюнити разработчиков на Rust под микроконтроллеры, т.к. этот язык действительно очень удобен, несмотря на то, что у него довольно высокий порог вхождения.
ссылка на оригинал статьи https://habrahabr.ru/post/324646/