Pull to refresh

Rust, Eclipse и STM32

Reading time 8 min
Views 21K
Для того, чтобы подружить между собой указанные в заголовке технологии нам понадобятся:


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

1. Генерация проекта на C


Воспользуемся для этого утилитой STM32CubeMX. Для демо-проекта нам понадобится:

  • SYS = Serial Wire (если у устройство подключено по SWD) либо JTAG
  • USART2 в конфигурации Asynchronous
  • Несколько пинов на одном порту в режиме GPIO_Output (назовем их LED_R, LED_G, LED_B)

image

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

image

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

image

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

image

2. Создаем проект для свежей версии Eclipse


Хоть System Workbench и основан на Eclipse, нам придется создать новый проект в свежей мажорной версии Eclipse (Neon), т.к. RustDT несовместим с той версией Eclipse.

Также нам понадобится шаблон проекта, который устанавливается вместе с GNU ARM Eclipse Plugin.

image

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

image

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

  • Drivers
  • Inc
  • Src
  • startup
  • STM32F103C8Tx_FLASH.ld

Если Workbench установлен, то разворачиваем два окна настроек проектов (из старого и нового Eclipse) так, чтобы было удобно копировать значения из одного окна в другое. Окна немного отличаются, поэтому при копировании ориентируемся на флаги, которые указаны в скобках.

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

Копируем Defined Symbols:

image

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

image

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

image

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

image

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

image

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

image

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

image

Теперь нужно настроить дебаггер (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 и путь к созданному в предыдущем шаге файлу.

image

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

image

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

rustup update
rustup default nightly

Далее нужно получить текущую версию компилятора:

rustc -v --version

image

Затем склонировать себе исходники 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.

image

image

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

image

Обычно эти компоненты можно найти в 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
grossws подсказал, что теперь этот файл включен в компилятор и можно его не создавать.

{
    "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

image

image

Компилируем 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 файлы.

image

Компилируем. Бинарник сильно прибавил в весе, но все еще умещается в STM32F103C8.

image

Запускаем Debug и видим, что Eclipse без проблем переходит из C-кода в Rust.

image

В завершении статьи хочу выразить благодарность авторам следующих постов, без них я бы не осилил этот процесс:

www.hashmismatch.net/pragmatic-bare-metal-rust
spin.atomicobject.com/2015/02/20/rust-language-c-embedded
github.com/japaric/rust-cross

Статью писал с надеждой на то, что это послужит дополнительным шагом в появлении комьюнити разработчиков использующих Rust для программирования под микроконтроллеры, т.к. это действительно удобный и современный язык, несмотря на то, что у него довольно высокий порог вхождения.
Tags:
Hubs:
+23
Comments 24
Comments Comments 24

Articles