Для того, чтобы подружить между собой указанные в заголовке технологии нам понадобятся:
- Свежий 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("rn");
}
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
}
Также в корне проекта необходимо создать файл конфигурации целевой платформы grossws подсказал, что теперь этот файл включен в компилятор и можно его не создавать.
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 для программирования под микроконтроллеры, т.к. это действительно удобный и современный язык, несмотря на то, что у него довольно высокий порог вхождения.
Автор: build_your_web