Низкоуровневый ввод-вывод: как работать с регистрами
Есть два способа писать прошивку для ESP32-C3:
- первый — bare-metal, с использованием только
[no_std]Rust; - второй — с использованием
[std]Rust и C-биндингов к ESP-IDF.
[no_std]Rust — это Rust без стандартной библиотеки, только с core-библиотекой. Core — подмножество стандартной библиотеки, которое не зависит от наличия операционной системы.
Как устроены экосистемы?
[std] Rust и ESP-IDF
Этот путь опирается на C-биндинги к ESP-IDF. По этому пути можно использовать стандартную библиотеку Rust, поскольку у нас есть операционная система: ESP-IDF, основанная на FreeRTOS. Возможность пользоваться стандартной библиотекой даёт преимущества: можно использовать любые типы, независимо от того, размещены ли они на стеке или в куче. Можно использовать потоки, Mutex и другие примитивы синхронизации.
ESP-IDF в основном написан на C и открывается в Rust по классической схеме разделения на крейты:
- крейт
sys, предоставляющий собственноunsafe-биндинги (esp-idf-sys); - крейт более высокого уровня с безопасными и удобными абстракциями Rust (esp-idf-svc).
Последняя часть головоломки — низкоуровневый доступ к железу, который снова предоставляется в разделённом виде:
esp-idf-halреализует не зависящие от железа трейтыembedded-hal, такие как аналого-цифровое преобразование, цифровые выводы ввода-вывода или связь по SPI. Как следует из названия, он тоже использует ESP-IDF в качестве основы.
Подробнее — в главе об экосистеме книги The Rust on ESP Book.
На сегодняшний день этот путь даёт наибольшие возможности на чипах Espressif, если вы хотите использовать Rust. Весь материал этого курса основан на этом подходе.
Как записывать значения в регистры в этой экосистеме, мы рассмотрим в контексте упражнения с прерываниями.
Bare metal Rust с [no_std]
Как следует из названия bare metal, операционная система не используется. Поэтому мы не можем пользоваться возможностями языка, которые на ней основаны. Core-библиотека — это подмножество стандартной библиотеки, которое исключает такие возможности, как типы, размещаемые в куче, и потоки. Код, использующий только core-библиотеку, помечается #[no_std]. Код #[no_std] всегда может работать в окружении std, но обратное неверно.
В Rust связь между регистрами и кодом устроена так:
Регистры и их поля в устройстве описываются в файлах System View Description (SVD). С помощью svd2rust по этим файлам генерируются Peripheral Access Crates (PAC). PAC представляет собой тонкую обёртку над различными регистрами с отображением в память, определёнными для конкретной модели используемого микроконтроллера.
Хотя прошивку можно написать и только с PAC, часть кода получилась бы небезопасной или неудобной, так как PAC даёт лишь самый базовый доступ к периферии микроконтроллера. Поэтому есть ещё один слой — Hardware Abstraction Layer (HAL). HAL предоставляет более удобный API для чипа и часто реализует общие трейты, определённые в универсальном embedded-hal.
Микроконтроллеры обычно припаяны к какой-нибудь печатной плате (или просто плате), которая определяет подключения к каждому выводу. Для конкретной платы может быть написан Board Support Crate (BSC, также известный как Board Support Package или BSP). Он добавляет ещё один слой абстракции и может, например, предоставлять API для различных датчиков и светодиодов на плате, без необходимости знать, к каким выводам микроконтроллера они подключены.
Мы напишем частичный драйвер датчика в таком подходе, так как драйверы должны быть платформонезависимыми.