Привет! Хочу познакомить вас с проектом Rust Embedded. Он позволяет нам использовать язык программирования Rust для разработки под встроенные платформы (Embedded Linux / RTOS / Bare Metal).
В этой статье, мы рассмотрим компоненты, которые необходимы для начала разработки под микропроцессоры Cortex-M3. После этого, напишем простой пример — моргание встроенным светодиодом.
Для этого нам понадобится — доступная и дешевая, китайская отладочная плата STM32F103C8T6 или Black Pill, из за черного цвета и небольшого размера. Существует также версия платы в синем цвете — Blue Pill. Её я не рекомендую, так как я слышал что она имеет неправильный резистор, который вызывает проблемы при использовании порта USB. Второе её отличие от Black Pill в расположении пинов (выводов). Но об этом позже.
Также нам будет необходим программатор для отладочных плат STM32. В этой статье, мы будем использовать дешевый и доступный, китайский программатор ST-Link V2.
Версия компилятора Rust
Для начала, вам необходимо убедится что версия вашего компилятора 1.31 или более свежая. Проверить вашу версию компилятора можно введя команду:
> rustc --version
Установка компонентов
Теперь мы можем приступить к установке необходимых компонентов.
GNU Arm Embedded Toolchain
Нам будет необходим отладчик для чипов ARM — arm-none-eabi-gdb. Запустите установщик и следуйте инструкциям. В конце установки не забудьте отметить опцию "Add path to environment variable".
После установки, вы можете проверить, всё ли в порядке, введя в командной строке:
> arm-none-eabi-gdb -v
Если всё в порядке, то вы увидите версию установленного компонента.
Драйвер ST-Link
Перейдём к установке драйвера для программатора ST-Link.
Следуйте инструкциям установщика и убедитесь что вы устанавливаете правильную (шестидесяти-четырёх битную или тридцати-двух битную) версию драйвера в зависимости от разрядности вашей операционной системы.
ST-Link Tools
Следующий шаг — установка инструментов, необходимых для прошивки — ST-Link Tools. Скачайте архив и распакуйте в любое удобное место, также необходимо указать путь к вложенной папке bin (stlink-1.3.0bin) в переменную среды "PATH".
cargo-binutils
И наконец, установим пакет cargo-binutils, это делается двумя командами в консоли.
> cargo install cargo-binutils
> rustup component add llvm-tools-preview
На этом установка компонентов окончена.
Создание и настройка проекта
Чтобы продолжить, нам необходимо создать новый проект с именем "stm32f103c8t6". Напомню, делается это командой:
> cargo new stm32f103c8t6
Зависимости проекта и оптимизации
Подключим необходимые библиотеки в файле Cargo.toml:
[package]
name = "stm32f103c8t6"
version = "0.1.0"
authors = ["Nick"]
edition = "2018"
# Зависимости для разработки под процессор Cortex-M3
[dependencies]
cortex-m = "*"
cortex-m-rt = "*"
cortex-m-semihosting = "*"
panic-halt = "*"
nb = "0.1.2"
embedded-hal = "0.2.3"
# Пакет для разработки под отладочные платы stm32f1
[dependencies.stm32f1xx-hal]
version = "0.5.2"
features = ["stm32f100", "rt"]
# Позволяет использовать `cargo fix`!
[[bin]]
name = "stm32f103c8t6"
test = false
bench = false
# Включение оптимизации кода
[profile.release]
codegen-units = 1 # Лучшая оптимизация
debug = true # Нормальные символы, не увеличивающие размер на Flash памяти
lto = true # Лучшая оптимизация
Также, вы можете видеть что, в конце файла, была включена оптимизация кода.
Библиотека cortex-m-rt требует от нас создать файл в корневом каталоге проекта, его необходимо назвать "memory.x". В нём указывается сколько памяти имеет наше устройство и её адрес:
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
Установка целевой платформы и цели компиляции по-умолчанию
Необходимо установить целевую платформу для компилятора, это делается командой:
> rustup target add thumbv7m-none-eabi
После этого, создайте папку ".cargo", в корневом каталоге, а в ней файл "config".
Содержимое файла config:
[target.thumbv7m-none-eabi]
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = ["-C", "link-arg=-Tlink.x"]
[build]
target = "thumbv7m-none-eabi" # Cortex-M3
В нём мы обозначили цель компиляции по умолчанию, это позволяет компилировать наш код, в ARM .elf файл, простой и привычной командой:
> cargo build --release
Пример
Чтобы убедиться в работоспособности, рассмотрим простейший пример — моргание светодиодом.
Содержимое файла main.rs:
#![deny(unsafe_code)]
#![no_std]
#![no_main]
use panic_halt as _;
use nb::block;
use stm32f1xx_hal::{
prelude::*,
pac,
timer::Timer,
};
use cortex_m_rt::entry;
use embedded_hal::digital::v2::OutputPin;
// Определяем входную функцию.
#[entry]
fn main() -> ! {
// Получаем управление над аппаратными средствами
let cp = cortex_m::Peripherals::take().unwrap();
let dp = pac::Peripherals::take().unwrap();
let mut flash = dp.FLASH.constrain();
let mut rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze(&mut flash.acr);
let mut gpiob = dp.GPIOB.split(&mut rcc.apb2);
// Конфигурируем пин b12 как двухтактный выход.
// Регистр "crh" передаётся в функцию для настройки порта.
// Для пинов 0-7, необходимо передавать регистр "crl".
let mut led = gpiob.pb12.into_push_pull_output(&mut gpiob.crh);
// Конфигурируем системный таймер на запуск обновления каждую секунду.
let mut timer = Timer::syst(cp.SYST, &clocks)
.start_count_down(1.hz());
// Ждём пока таймер запустит обновление
// и изменит состояние светодиода.
loop {
block!(timer.wait()).unwrap();
led.set_high().unwrap();
block!(timer.wait()).unwrap();
led.set_low().unwrap();
}
}
В первой строке, с помощью атрибута — #![deny(unsafe_code)]
, мы убираем возможность использования небезопасного кода.
Во второй строке, мы разместили атрибут #![no_std]
, он требуется, потому что мы создаём приложение для голого железа, а стандартной библиотеке необходима операционная система.
Далее идёт атрибут #![no_main]
, который сообщает компилятору, что мы не используем основную функцию по умолчанию с вектором аргументов и типом возвращаемого значения. Это не имеет смысла, поскольку у нас нет операционной системы или другой среды выполнения, которая вызывала бы функцию и обрабатывала возвращаемое значение.
После атрибутов, мы подключаем необходимые модули в область видимости.
Далее следует функция, в нашем случае мы, по привычке, назвали её — main()
. Это точка входа в программу, она определяется атрибутом #[entry]
.
Внутри основной функции мы создаем дескриптор периферийного объекта, который «владеет» всеми периферийными устройствами.
После этого мы можем установить пин, отвечающий за встроенный в плату светодиод, на выход. Так как в моём случае, это плата Black Pill, светодиод в ней подключен к пину B12. Если у вас плата Blue Pill, то в ней за встроенный светодиод отвечает пин C13. Поэтому мы устанавливаем пин B12 как двухтактный выход и присваиваем его переменной led
.
Теперь мы можем задать время запуска события обновления для системного таймера, и сохранить его в переменную timer
. В нашем примере это время равняется одной секунде.
Затем, в бесконечном цикле, мы можем описать логику работы нашей программы. Она очень проста, сначала timer
блокирует выполнение программы, пока не пройдёт указанное в нём время. Следующим шагом, на пин встроенного светодиода подаётся значение high, соответственно он загорается. После чего выполнение программы снова блокируется тем же таймером, на одну секунду. И пин светодиода устанавливается в значение low, следовательно он гаснет. Далее программа циклически повторяется.
Компиляция
Теперь, когда мы разобрались с кодом и логикой программы, мы можем скомпилировать её в .elf файл командой:
> cargo build --release
Этот формат содержит не только двоичный код, но также некоторые заголовки и прочее. Это полезно, когда файл запускается другим программным обеспечением, таким как операционная система или загрузчик.
Но, для запуска программы на голом железе, нам нужен не файл .elf, а файл .bin. Файл .bin это изображение программного обеспечения, которое может быть записано побайтно в память микроконтроллера. Файл .elf может быть конвертирован в файл .bin с помощью команды:
> cargo objcopy --bin stm32f103c8t6 --target thumbv7m-none-eabi --release -- -O binary stm32f103c8t6.bin
Теперь наш бинарный файл готов для прошивки.
Подключение
Прошивать плату мы будем с помощью программатора ST-Link V2. Для этого нужно сначала подсоединить его к плате.
На корпусе программатора есть схема расположения пинов разъема. Необходимо обратить внимание на вырез в разъёме, он также отображен на схеме, что позволяет понять как производить подключение.
Подключите пины 3.3V и GND программатора с соответствующими пинами на плате. Пин SWDIO подключите к пину DIO, а пин SWCLK к пину CLK.
После подключения платы к программатору, мы можем подключить ST-Link к компьютеру.
Внимание! Перед подключением отключите все другие источники питания от платы, если таковые имеются, в противном случае это может привести к повреждению платы или компьютера.
Теперь мы можем проверить подключение:
> st-info --descr
В консоли, мы должны увидеть строку "F1 Medium-density device".
Прошивка
Опционально, перед прошивкой, можно очистить память платы, если на ней что то записано:
> st-flash erase
И наконец, мы можем начать прошивку:
> st-flash write stm32f1.bin 0x8000000
Если всё выполнено правильно, вы должны увидеть мигающий светодиод. Поздравляю!
Заключение
Итак, чтобы прошить плату, необходимо использовать данный набор команд введённых последовательно:
> cargo build --release
> cargo objcopy --bin stm32f103c8t6 --target thumbv7m-none-eabi --release -- -O binary stm32f103c8t6.bin
> st-flash erase
> st-flash write stm32f1.bin 0x8000000
Чтобы упростить себе жизнь, мы можем создать .bat файл с данными командами, и запускать его из консоли.
От автора
Если вас интересуют подобные руководства, добро пожаловать на мой YouTube канал.
Также, недавно, я завёл Telegram канал, на котором публикую различные переводы руководств и книг, новости, юмор и другие вещи связанные с языком программирования Rust. Там вы можете найти ссылки на несколько чатрумов для того, чтобы задать свои вопросы и получить помощь, представить свои проекты или просто пообщаться с единомышленниками.
Спасибо за внимание!
Автор: Николай Калугин