Kotlin в embedded: пишем код для микроконтроллеров без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: А что, так можно было?

Представьте: вы пишете под микроконтроллер, но при этом используете лямбды, корутины и type-safe билдеры. Звучит как научная фантастика? А вот и нет. Embedded разработка на Kotlin — это не шутка, а вполне рабочий инструмент, который набирает обороты.

Традиционно мир «железа» — это царство C и ассемблера. Потом туда робко зашел C++, но с оговорками. И вот — Kotlin. Язык, который мы привыкли ассоциировать с Android и бэкендом, вдруг заговорил на языке регистров и прерываний. Как? Через Kotlin/Native — технологию, которая компилирует ваш код напрямую в машинный код, минуя JVM.

В этой статье мы разберем, как использовать Kotlin для embedded-систем, напишем реальный код для мигания светодиодом (да, куда без него) и посмотрим, стоит ли овчинка выделки. Если вы устали от сишных макросов и хотите современного синтаксиса на уровне регистров — вам сюда.

Kotlin/Native: Как это работает под капотом?

Главный вопрос, который возникает у любого embedded-инженера: «А не будет ли это тормозить?».

Kotlin/Native использует LLVM в качестве бэкенда. Это означает, что ваш код компилируется в нативный бинарник, который можно запустить на голом металле (bare-metal) или под RTOS. Никакой сборки мусора в рантайме (если вы её отключили), никакой виртуальной машины. Только вы и процессор.

С точки зрения производительности, Kotlin для микроконтроллеров практически не уступает C. Да, есть небольшой оверхед от объектной модели, но он нивелируется удобством и безопасностью кода.

Что нужно для старта?

  • Установленный Kotlin/Native (через Kotlin Multiplatform plugin или вручную)
  • Тулчейн для вашей архитектуры (ARM, RISC-V, x86) — обычно это набор кросс-компиляторов
  • Библиотека для работы с железом — например, kotlinx-c-interop или ручные биндинги
  • Терпение — первые шаги будут непривычными

Пишем код: Мигаем светодиодом на Kotlin

Давайте сразу к делу. Предположим, у нас есть плата на STM32 (ARM Cortex-M). Нам нужно зажечь светодиод на пине PA5.

Для начала подключимся к регистрам через Kotlin/Native interop:

import kotlinx.cinterop.*
import platform.posix.*

// Адреса регистров для STM32F4 (пример)
val RCC_AHB1ENR: UIntPointer = 0x40023830.toUInt().ptr
val GPIOA_MODER: UIntPointer = 0x40020000.toUInt().ptr
val GPIOA_ODR: UIntPointer = 0x40020014.toUInt().ptr

fun main() {
    // Включаем тактирование GPIOA
    RCC_AHB1ENR.pointed.value = RCC_AHB1ENR.pointed.value or (1u shl 0)
    
    // Настраиваем PA5 как выход (01 в битах 10-11)
    GPIOA_MODER.pointed.value = GPIOA_MODER.pointed.value and (3u shl 10).inv()
    GPIOA_MODER.pointed.value = GPIOA_MODER.pointed.value or (1u shl 10)
    
    // Бесконечный цикл: мигаем
    while (true) {
        GPIOA_ODR.pointed.value = GPIOA_ODR.pointed.value xor (1u shl 5)
        // Задержка (грубая)
        for (i in 0 until 100000) {
            // пусто
        }
    }
}

Выглядит почти как C, но с синтаксическим сахаром Kotlin. Обратите внимание на u (беззнаковые числа) и работу с указателями через ptr. Это не баг, а фича — так мы явно говорим компилятору, что работаем с памятью напрямую.

А можно ли красивее?

Да! Мы можем обернуть работу с регистрами в DSL. Посмотрите, как элегантно выглядит код с использованием extension-функций:

// Определяем удобный API
fun UIntPointer.setBit(bit: Int) {
    this.pointed.value = this.pointed.value or (1u shl bit)
}

fun UIntPointer.clearBit(bit: Int) {
    this.pointed.value = this.pointed.value and (1u shl bit).inv()
}

fun UIntPointer.toggleBit(bit: Int) {
    this.pointed.value = this.pointed.value xor (1u shl bit)
}

fun delay(cycles: Int) {
    for (i in 0 until cycles) { /* NOP */ }
}

fun main() {
    RCC_AHB1ENR.setBit(0)       // Включаем тактирование
    
    // PA5 как выход (сброс битов 10-11, установка бита 10)
    GPIOA_MODER.clearBit(10)
    GPIOA_MODER.clearBit(11)
    GPIOA_MODER.setBit(10)
    
    while (true) {
        GPIOA_ODR.toggleBit(5)
        delay(100_000)
    }
}

Согласитесь, читать такой код гораздо приятнее, чем копаться в битовых масках. А если добавить еще и корутины для асинхронной работы с периферией — получится почти высокоуровневое программирование, но на уровне металла.

Плюсы и минусы Kotlin в embedded

Давайте будем честными: Kotlin для embedded-систем — это не серебряная пуля. У него есть свои сильные и слабые стороны. Разберем их.

Плюсы (то, за что мы любим Kotlin)

  • Null-безопасность. Забудьте о segmentation fault из-за нулевого указателя. Компилятор не даст вам выстрелить себе в ногу.
  • Функциональщина. Лямбды, функции высшего порядка, неизменяемые данные — всё это снижает количество багов.
  • DSL и билдеры. Вы можете написать свой язык описания конфигурации периферии. Например: gpio { pin(5).mode(OUTPUT) }.
  • Корутины. Легковесные потоки для обработки событий без тяжелых RTOS.
  • Кроссплатформенность. Один и тот же код бизнес-логики можно запустить на ARM, x86 и даже в браузере (через Kotlin/JS).

Минусы (то, что нужно учитывать)

  • Размер бинарника. Kotlin/Native генерирует код с рантаймом. Для маленьких микроконтроллеров (1-2 КБ RAM) это может быть критично.
  • Отладка. Инструменты отладки пока сыроваты. GDB с Kotlin работает, но не так гладко, как с C.
  • Сообщество. Мало примеров и

Блоги

Рекомендаций по книгам