Для того, чтобы подружить между собой указанные в заголовке технологии нам понадобятся:
Идея состоит в том, чтобы с скомпилировать написанную на Rust программу в библиотеку, которую можно будет слинковать с помощью тулчейна для ARM.
В итоге мы сможем даже вполне комфортно дебажить смешанный код на Rust и С.
Воспользуемся для этого утилитой STM32CubeMX. Для демо-проекта нам понадобится:

Проверим настройки тактирования. Тут при желании можем указать тактирование от внешнего кварца и его частоту.

Сгенерируем проект. Назовем его “HwApi”, т.к. этот слой кода у нас будет представлять собой абстракцию над железом, который мы будем использовать при написании кода на Rust. В качестве IDE выбираем SW4STM32.

Если Workbench установлен, то можем открыть сгенерированный проект и проверить, что он успешно компилируется.

Хоть System Workbench и основан на Eclipse, нам придется создать новый проект в свежей мажорной версии Eclipse (Neon), т.к. RustDT несовместим с той версией Eclipse.
Также нам понадобится шаблон проекта, который устанавливается вместе с GNU ARM Eclipse Plugin.

Для того, чтобы успешно слинковать либу, сгенерированную rust компилятором, нам понадобится заранее установленная свежая версия GNU ARM Embedded Toolchain.

Начинаем процесс переноса проекта из System Workbench в Eclipse CDT. В интернете можно найти скрипты, которые этот процесс автоматизируют, но я буду это делать вручную, т.к. собираюсь переиспользовать HwApiLib в других проектах, изменяя только написанную на Rust часть кода.
Копируем следующие папки/файлы в новый проект:
Если Workbench установлен, то разворачиваем два окна настроек проектов (из старого и нового Eclipse) так, чтобы было удобно копировать значения из одного окна в другое. Окна немного отличаются, поэтому при копировании ориентируемся на флаги, которые указаны в скобках.
Если Workbench не установлен, можно просто скопировать настройки со скриншотов, приложенных ниже.
Копируем Defined Symbols:

Пути к папкам, содержащие *.h файлы:

На вкладке “Optimization” можно включить оптимизацию Optimize size(-Os).
Далее указываем, что нам нужны все предупреждения компилятора:

Указываем путь к скрипту линкера + отмечаем чекбокс для удаления из результата линковки неиспользуемых в коде секций:

На следующей вкладке важно отметить чекбокс “Use newlib-nano” и вручную указать флаг

Указываем пути к папкам с файлами для компиляции:

Нажимаем Ок. После чего меняем расширение startup файла на заглавную .S, чтобы файл успешно подхватился компилятором. Проверяем, что проект компилируется.

Теперь нужно настроить дебаггер (Run — Debug Configurations — GDB OpenOCD Debugging). Создаем файл для OpenOCD с описанием железа, в котором будет запускаться программа (в моем случае файл называется STM32F103C8x_SWD.cfg):
Если у вас используется другой микроконтроллер или другой способ подключения к нему, то корректный файл для OpenOCD можно сгенерировать в Workbench (с помощью Debugging options — Ac6).
В Config options указываем флаг -f и путь к созданному в предыдущем шаге файлу.

Жмем Debug. Проверяем, что дебаггер успешно залил код в микроконтроллер и началась отладка.

Пришло время создавать Rust проект. Т.к. нам понадобятся инструкции компилятора, которые не поддерживаются в stable версии, нам нужно будет переключиться нам nightly версию компилятора, запустив в cmd следующие команды:
Далее нужно получить текущую версию компилятора:

Затем склонировать себе исходники rust и переключится на коммит, который использовался для сборки этого компилятора (указан в commit-hash).
Следующим шагом скомпилируем необходимые нам библиотеки под ARM.
В будущем, при каждом обновлении компилятора (rustup update) нужно будет переключаться на актуальную версию исходников и перекомпилировать библиотеки для ARM, иначе потеряется возможность дебажить код на rust.
Наконец-то можно приступить к создают Rust-проекта в eclipse.


Eclipse просит указать путь к компилятору, исходникам и утилитам для работы с rust-кодом.

Обычно эти компоненты можно найти в C:\Users\%username%\.cargo. Rust src — путь к папке src в исходниках, которые мы скачали ранее.
Теперь основной код:
lib.rs
api.rs — прослойка для интеграции между собой Rust и C кода
runtime_support.rs — для поддержки низкоуровневых функций Rust
Также в корне проекта необходимо создать файл конфигурации целевой платформы
thumbv7m-none-eabi.json grossws подсказал, что теперь этот файл включен в компилятор и можно его не создавать.
Копируем в папку Rust проекта папку libs-arm содержащую скомпилированные для работы под ARM компоненты из стандартной библиотеки Rust.
Изменяем Debug target, так чтобы он запускал компиляцию с нужными нам параметрами


Компилируем Rust-проект. В результате в папке проекта появится файл lib.o.
Теперь в С-проекте создаем файлы api.h/api.c, в которых объявляем и реализуем функции, которые используются в api.rs.
api.h
api.c
Добавляем вызов demo_main_loop() внутри функции main.
main.c
Осталось всё слинковать. Для этого открываем свойства проекта на 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 для программирования под микроконтроллеры, т.к. это действительно удобный и современный язык, несмотря на то, что у него довольно высокий порог вхождения.
- Свежий 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 для программирования под микроконтроллеры, т.к. это действительно удобный и современный язык, несмотря на то, что у него довольно высокий порог вхождения.