CDEblog
86 subscribers
17 photos
5 videos
4 files
86 links
Circuit Design Engineer BLOG

Программирование, схемотехника может быть что-то ещё 🤷🏻‍♂️

Для личного контакта: @devprodest
Блог в интернете: https://cdeblog.ru
Копия в дзене: https://dzen.ru/cdeblog

Немного о кулинарии: @cdefood
Download Telegram
Книга с залипательными картинками внутренностей различных компонентов
2👍1😱1
👩‍💻👩‍💻 Полезный Forward declaration (Предварительное объявление)

Это объявление сущности без указания подробностей её внутренностях. По сути позволяет компилятору понять размер и разрешить ссылки при сборке.


Типичным примером является прототип функции
int func(int val);

Таким же образом можно указывать и типы
typedef struct st_type_t st_type_t;
typedef enum e_type_t e_type_t;

Для чего это может быть полезно.

1. Уменьшение связности модулей программы

Представим ситуацию что есть несколько заголовочников.

Файл a.h содержит описание типов структуры sta_t и перечисления eta_t.
Файл b.h - stb_t и etb_t.
Представим, что нам нужен ещё файл c.h, в котором функция будет принимать указатели на sta_t и stb_t. Можно написать два инклюда a.h и b.h. Но тогда получается что файл который будет инклюдить c.h получает все зависимости всех файлов.
Если у нас не только три файла, а большое дерево зависимостей, то в итоге весь код становиться просто спутанным клубком.

Для того что бы развязать можно использовать инклюды только в файлах .c и ограничить их использование в .h, тогда будут появляться только нужные зависимости.

2. Ускорение сборки

Данный пункт напрямую вытекает из предыдущего. Объясняется это тем, что компилируемый файл имеет меньший размер, так как директива include просто вставляет содержимое файла.

В моём текущем проекте это позволило снизить размеры файлов после препроцессора с 4-6 мегабайт до 500-800 килобайт. Что в общем случае ускорило сборку проекта на 30%.

Ограничения

Для тог что бы это всё работало, компилятор должен смочь вывести размер сущности и то как её использовать. Для указателей (функции в том числе), стандартных типов, и перечислений (не всегда) это делается очень просто и очевидно.
Для структур не получится и в таком случае include внутри хедера будет оправдан
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Каркас для прошивки: почему архитектура решает все

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

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

Почему это критически важно?
Выбор архитектуры определяет:
🟡Скорость реакции на события. Мгновенно или когда дойдут руки?
🟡Предсказуемость. Можно ли точно рассчитать время отклика?
🟡Стоимость железа. Будет ли достаточно дешевого 8-битного МК или нужен мощный ARM?
🟡Сроки разработки и поддержки. Можно ли легко добавлять фичи через полгода или каждый раз всё переписывать?
🟡Надежность. Насколько система устойчива к ошибкам и сложным внешним условиям?

Основных пути, о которых пойдет речь:
🔴Суперлуп (Super Loop) - фундамент. Бесконечный цикл, где задачи выполняются строго по очереди. Простота, но беспощадная ограниченность.
🔴Прерывания (Interrupt-Driven) - мир асинхронности. Позволяет мгновенно реагировать на внешние события, прорывая основной цикл.
🔴Планировщик на таймере (Timer-Based Scheduler) - вводит порядок и периодичность. Шаг от хаоса к системе.
🔴RTOS (Real-Time Operating System) - промышленная мощь. Полноценная многозадачность с приоритетами, синхронизацией и четким планированием.
🔴Событийно-ориентированная архитектура (Event-Driven) - философия слабой связанности. Компоненты общаются через сообщения, не зная друг о друге.

Как выбирать?
🟢Насколько сложна моя система? (одна задача vs. множество независимых процессов)
🟢Критично ли время отклика? (мгновенно vs. в течение секунды)
🟢Каковы ресурсы моего МК? (память, частота)
🟢Будет ли проект масштабироваться?
🟢Какой опыт у команды?

В следующих постах разберу каждый архитектурный паттерн в деталях: как он устроен изнутри, где сияет, а где терпит поражение, и для какого типа проектов создан.

Необходимо понимать, что выбор в начале пути сэкономит месяцы работы в будущем. Это решение, которое отделяет любительский проект от профессионального продукта.

#встраиваемыесистемы #архитектурапо #прошивка #embedded #разработка #микроконтроллеры #инженерия #впо
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Суперлуп: первый и самый простой каркас для прошивки

Представьте, что вы учитесь готовить. Вы не начинаете со сложного торта Прага. Вы начинаете с бутерброда: взял хлеб, намазал масло, положил колбасу. Суперлуп (Super Loop) - это тот самый бутерброд в мире встраиваемого ПО. Это фундамент, на котором всё начинается.

Технически, это бесконечный цикл while(1), в котором ваши функции (задачи) вызываются строго одна за другой, по кругу. Никакой магии, только четкая последовательность.

while(1) // Вечный двигатель вашей прошивки
{
check_buttons(); // 1. Опрос кнопок
read_sensors(); // 2. Чтение датчиков
process_logic(); // 3. Вычисление логики
update_display();// 4. Вывод на экран
}

Как это работает на практике?

Весь код существует в одном потоке выполнения. Микроконтроллер последовательно переходит от одной функции к другой, и когда доходит до конца цикла - начинает сначала. Порядок выполнения жёстко прописан разработчиком.

Ключевой принцип: Синхронность. Система не реагирует на внешние события мгновенно, а обрабатывает их только когда дойдёт до соответствующей строки в цикле.

Когда выбирать Суперлуп?
🟡Система тривиальна? (Мигание светодиодом, простейший термометр).
🟡Задачи всегда выполняются быстро? (Ни одна функция не занимает больше пары миллисекунд).
🟡Ресурсы микроконтроллера исчезающе малы? (Мало памяти, низкая частота).
🟡Время отклика не критично? (Не страшно, если система отреагирует на кнопку через 100 мс, а не через 1 мс).

Идеальные кандидаты: Электронные таймеры, простые термостаты, мигалки, детские игрушки, системы с одним основным действием.

Сильные стороны:
Предельная простота. Понять и написать может даже новичок. Это главный обучающий инструмент.
Нулевые накладные расходы. Нет планировщика, переключения контекста, стеков для задач. Весь ресурс МК - на полезную работу.
Абсолютная предсказуемость. Время отклика системы = сумме времен выполнения всех функций до нужной. Всё считается на бумажке.
Никаких проблем синхронизации. Нет прерываний, нет разделяемых ресурсов между задачами - значит, нет гонок данных (race condition) и deadlock'ов.
Полный контроль. Разработчик точно знает, что и в какой момент выполняется.

Слабые стороны:
Катастрофическая отзывчивость. Если функция read_sensors() внезапно займет 500 мс на измерение, всё встанет. Нажатия кнопок не обработаются, дисплей не обновится.
Спагетти-код. При добавлении новых функций код быстро превращается в запутанный ком, где всё зависит от всего. Сопровождать такой код - мучение.
Неэффективное использование процессора. Пока система ждет события (например, нажатия кнопки), процессор бесполезно крутит пустой цикл, тратя энергию, вместо того чтобы уйти в режим сна.
Нет реального параллелизма. Полная неспособность обрабатывать несколько внешних событий, требующих внимания одновременно.
Жёсткая связанность. Изменение в одной функции часто влечёт за собой правки в других.

Суперцикл - это не плохая архитектура. Это правильная архитектура для очень простых задач. Она даёт бесценный опыт понимания работы МК на голом железе. Но как только ваши амбиции и требования к системе перерастают уровень мигающего светодиода, пора смотреть в сторону более мощных инструментов.

#суперлуп #superloop #архитектурапрошивки #embedded #встраиваемыесистемы #программированиемк #новичкам
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Прерывания: как заставить систему реагировать мгновенно

Чтобы система реагировала на события мгновенно, переходим к архитектуре на основе Прерываний (Interrupt-Driven).

Основная логика переносится в обработчики прерываний (ISR), которые асинхронно вызываются аппаратурой микроконтроллера.

Суперлуп часто остается для фоновых, не срочных задач.

volatile bool button_pressed = false;

void button_isr(void) {
button_pressed = true;
}

int main(void) {
setup_hardware();
enable_interrupts();

while(1) {
update_clock();

if(button_pressed) {
button_pressed = false;
handle_button_action();
}
sleep_mode();
}
}

Как это работает на практике?
Аппаратура МК генерирует сигнал прерывания при событии. Процессор немедленно сохраняет контекст, выполняет ISR и возвращается к прерванному коду. Ключевой принцип - асинхронность и приоритет.

Когда выбирать архитектуру на прерываниях?
🟢Требуется ли мгновенная реакция? (Нажатие кнопки, аварийный датчик)
🟢Нужна ли максимальная энергоэффективность? (Устройство на батарейке)
🟢Система в основном "спит и ждет" событий? (Пульт ДУ, датчик двери)
🟢Готовы ли вы к сложной отладке асинхронного кода?

Идеальные кандидаты: Пульты, коммуникационные шлюзы (UART, SPI), системы аварийного останова.

Сильные стороны:
Максимальная отзывчивость. Реакция за микросекунды
Энергоэффективность. Основной цикл может спать (WFI)
Реальная асинхронность. Система живет в ритме внешнего мира
Четкое разделение. Код реакции изолирован в ISR

Слабые стороны:
😀Кошмар отладки. Ошибки трудно воспроизвести
😀Проблемы синхронизации. Общие данные требуют защиты
😀Риск переполнения стека. Длинные или вложенные ISR опасны
😀Непредсказуемость. При высокой нагрузке время отклика неопределенно
😀"Размазанная" логика. Код разбросан по множеству ISR

Прерывания - мощнейший инструмент для создания отзывчивых и энергоэффективных систем, требующий высочайшей дисциплины и готовности к сложной отладке.

#прерывания #isr #архитектурапрошивки #embedded #встраиваемыесистемы #realtime
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Планировщик на таймере - порядок во времени

Следующий шаг к структурированию - Планировщик на таймере (Timer-Based Scheduler).

Это эволюция суперлупа, где системный таймер периодически генерирует прерывание (тик), которое проверяет, каким задачам пора выполняться, и выставляет флаги.
Основной цикл проверяет эти флаги и запускает соответствующие задачи.

volatile uint32_t tick_count = 0;
#define TASK1_PERIOD 100 // Выполнять каждые 100 мс
#define TASK2_PERIOD 500 // Выполнять каждые 500 мс

void systick_isr(void) {
tick_count++;
}

int main(void) {
uint32_t last_task1 = 0, last_task2 = 0;
setup_timer();
enable_interrupts();

while(1) {
uint32_t now = tick_count;

// Задача 1: каждые 100 тиков
if((now - last_task1) >= TASK1_PERIOD) {
last_task1 = now;
read_sensors();
}

// Задача 2: каждые 500 тиков
if((now - last_task2) >= TASK2_PERIOD) {
last_task2 = now;
update_display();
}

idle_task();
}
}

Как это работает на практике?
🔴Системный таймер генерирует регулярные прерывания (например, каждую 1 мс - системный тик).
🔴Каждый тик инкрементирует счетчик времени.
🔴Основной цикл постоянно проверяет, не наступило ли время для выполнения очередной задачи, сравнивая текущее время с временем последнего запуска и заданным периодом.

Когда выбирать планировщик на таймере?
🟡Нужна ли предсказуемая периодичность? (Опрос датчиков раз в 100 мс)
🟡Требуется ли лучшее структурирование, чем у суперлупа?
🟡Можно ли допустить задержку реакции до следующего тика?
🟡Хватит ли ресурсов МК для системного таймера и флагов?

Идеальные кандидаты: Системы управления с четкими циклами (термостаты, регуляторы), устройства с периодическим опросом датчиков, информационные дисплеи, мигалки с разными периодами.

Сильные стороны:
Детерминированность. Задачи выполняются с фиксированной периодичностью
Лучшая модульность. Задачи изолированы, их проще добавлять и модифицировать
Контроль времени. Можно замерять длительность выполнения каждой задачи
Эффективность. Нет переключения контекста как в RTOS

Слабые стороны:
😀Кооперативная модель. Долгая задача блокирует всю систему
😀Задержка реакции. Событие обработается только в следующем "тике"
😀Статическое планирование. Периоды задач жестко заданы при компиляции
😀Накопление задержек. Если задача выполняется дольше, сдвигается весь график

Планировщик на таймере - это идеальный баланс между простотой суперлупа и сложностью RTOS. Он отлично подходит для циклических систем с четкими временными интервалами, где важна предсказуемость, но нет требований к мгновенной реакции или сложной многозадачности.

#планировщик #scheduler #таймер #архитектурапрошивки #embedded #встраиваемыесистемы #кооперативнаямногозадачность
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
RTOS - мощь многозадачности

Для сложных систем требуется полноценная многозадачность.

Здесь на сцену выходит RTOS (Real-Time Operating System), например, FreeRTOS, Zephyr или Azure RTOS.

Её ядро - это вытесняющий планировщик, который на основе приоритетов решает, какую задачу (поток) выполнять прямо сейчас.

// Поток 1: Высокоприоритетный, обрабатывает прерывания
void high_priority_task(void *params) {
while(1) {
xQueueReceive(irq_queue, &data, portMAX_DELAY);
process_critical_data(data);
}
}

// Поток 2: Средний приоритет, управление интерфейсом
void ui_task(void *params) {
while(1) {
update_display();
vTaskDelay(pdMS_TO_TICKS(100));
}
}

// Поток 3: Низкий приоритет, фоновая логика
void background_task(void *params) {
while(1) {
calculate_statistics();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

int main(void) {
xTaskCreate(high_priority_task, "IRQ", 512, NULL, 3, NULL);
xTaskCreate(ui_task, "UI", 256, NULL, 2, NULL);
xTaskCreate(background_task, "BG", 128, NULL, 1, NULL);
vTaskStartScheduler();
}

Как это работает на практике?
RTOS создает иллюзию параллельного выполнения нескольких задач (потоков). Планировщик на основе приоритетов решает, какой поток должен выполняться в каждый момент времени. Если поток с более высоким приоритетом готов к выполнению, он немедленно вытесняет текущий поток. Потоки могут общаться через механизмы синхронизации: очереди, семафоры, мьютексы.

Когда выбирать RTOS?
🔴Система требует настоящего параллелизма? (Независимые процессы)
🔴Нужна ли мгновенная реакция на события высокой важности?
🔴Проект достаточно сложен для команды разработчиков?
🔴Есть ли достаточно ресурсов МК (RAM, Flash)?
🔴Готовы ли вы изучать многопоточное программирование?

Идеальные кандидаты: Сложные устройства с графическим интерфейсом, сетевыми подключениями, множеством датчиков и исполнительных механизмов (умные термостаты, навигаторы, промышленные контроллеры).

Сильные стороны:
Вытесняющая многозадачность. Задача с высшим приоритетом получает CPU немедленно
Отличная модульность. Код делится на независимые потоки
Богатый набор инструментов. Очереди, семафоры, мьютексы "из коробки"
Эффективное ожидание. Поток может "уснуть" в ожидании события
Портативность. Код легче переносить между разными МК

Слабые стороны:
😀Накладные расходы. Переключение контекста, память под стеки каждого потока
😀Высокий порог входа. Нужно глубоко понимать многопоточность
😀Классические проблемы. Инверсия приоритетов, дедлоки, гонки данных
😀Требует ресурсов. Нужен более мощный МК с достаточным количеством RAM

RTOS - это промышленный стандарт для сложных встраиваемых систем, который даёт огромные возможности для создания модульных, отзывчивых и поддерживаемых приложений, но требует от разработчика высокой квалификации и понимания принципов многопоточного программирования.

#rtos #freertos #операционнаясистема #архитектурапрошивки #embedded #многозадачность #realtime
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Событийно-ориентированная архитектура - общение сообщениями

Самый высокоуровневый подход - Событийно-ориентированная архитектура (Event-Driven Architecture, EDA).

Здесь нет центрального цикла. Компоненты системы общаются через сообщения (события): одни модули их "издают" (publish), другие - "обрабатывают" (subscribe).

Часто строится поверх RTOS или как большая state-машина.

// Издатель события (например, драйвер кнопки)
void button_isr(void) {
event_t btn_event = { .type = EVENT_BUTTON_PRESSED, .data = 1 };
event_bus_publish(&btn_event); // Отправка в шину событий
}

// Подписчик события (обработчик интерфейса)
void ui_handler(const event_t *event) {
if(event->type == EVENT_BUTTON_PRESSED) {
update_display("Button pressed!");
}
}

// Инициализация системы событий
int main(void) {
event_bus_init();
event_bus_subscribe(EVENT_BUTTON_PRESSED, ui_handler);

while(1) {
event_bus_process(); // Обработка очереди событий
idle_task();
}
}

Как это работает на практике?
Система строится вокруг шины событий (event bus) или диспетчера сообщений.
Компоненты не вызывают функции друг друга напрямую, а отправляют события в шину.
Другие компоненты, подписанные на определенные типы событий, получают и обрабатывают их. Это создает полностью асинхронную и слабосвязанную архитектуру.

Когда выбирать событийно-ориентированную архитектуру?
🟢Система состоит из множества независимых компонентов?
🟢Требуется ли высокая гибкость и возможность расширения?
🟢Важна ли тестируемость и изоляция модулей?
🟢Система по природе реактивная (событие → реакция)?
🟢Готовы ли вы к сложному проектированию с самого начала?

Идеальные кандидаты: Сложные IoT-устройства, системы домашней автоматизации, продвинутые HMI-интерфейсы, сетевые шлюзы, приложения с множеством асинхронных источников данных.

Сильные стороны:
Слабая связанность. Модули ничего не знают друг о друге
Гибкость и масштабируемость. Новый функционал = новый подписчик
Идеально для реактивных систем. Отражает суть embedded
Упрощённое тестирование. Модули можно тестировать изолированно

Слабые стороны:
😀Высокий порог входа. Сложно правильно спроектировать
😀Непрозрачность потока. Трудно отлаживать цепочки событий
😀Накладные расходы. Буферизация и маршрутизация требуют ресурсов
😀Риск переполнения очереди. События генерируются быстрее обработки
😀Потенциальная латентность. Сообщение проходит дольше прямого вызова

Событийно-ориентированная архитектура - это выбор для больших, сложных и долгоживущих проектов, где критически важны чистота кода, слабая связанность компонентов и возможность эволюции системы без переписывания базовых модулей.

#событийнаяархитектура #eventdriven #архитектурапрошивки #embedded #шинасобытий #реактивноепрограммирование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Что куда ставить?

Итак, у нас есть 5 основных архитектур. Как выбрать?

Давайте сравним их по ключевым критериям.

По простоте и порогу входа
🟢Суперлуп - начать может любой новичок
🟢Прерывания - требуется понимание работы МК
🟢Планировщик - нужны знания таймеров и прерываний
🟢RTOS - серьезный скачок в сложности
🟢Событийно-ориентированная - самый сложный для проектирования

По отзывчивости и времени отклика
🟡Прерывания - абсолютный лидер (почти мгновенно)
🟡RTOS - отличная отзывчивость для приоритетных задач
🟡Событийно-ориентированная - зависит от реализации
🟡Планировщик - задержка до следующего "тика"
🟡Суперлуп - задачи ждут своей очереди

По детерминированности
🔴Суперлуп - абсолютно детерминирован
🔴Планировщик - все считается по формуле
🔴RTOS - детерминированна при правильном проектировании
🔴Прерывания - при высокой нагрузке непредсказуемы
🔴Событийно-ориентированная - наименее детерминирована

По масштабируемости и поддержке кода
🟢Событийно-ориентированная - создана для сложных систем
🟢RTOS - отличная модульность и сопровождение
🟢Планировщик - уже лучше суперлупа
🟢Прерывания - превращаются в "ад поддержки" при росте
🟢Суперлуп - катастрофически плохо масштабируется

По требованию к ресурсам МК
🟡Суперлуп - живет на самых скромных МК
🟡Прерывания - минимальные накладные расходы
🟡Планировщик - требует только таймер
🟡RTOS - нужен солидный объем RAM и Flash
🟡Событийно-ориентированная - добавляет накладные расходы

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

Выбирайте осознанно, оценивая сложность системы, требования к времени отклика, доступные ресурсы и опыт разработчиков.

Правильный выбор на этапе проектирования сэкономит месяцы работы в будущем.

#сравнение #архитектурапрошивки #embedded #встраиваемыесистемы #суперлуп #rtos #прерывания #планировщик #событийнаяархитектура
Please open Telegram to view this post
VIEW IN TELEGRAM
3🔥1💯1
Please open Telegram to view this post
VIEW IN TELEGRAM
Компиляция для embedded: почему это не просто «нажать F5»

Как рождается прошивка: обзор пути от C-кода до микроконтроллера

Вы пишете код на C, жмете "скомпилировать" и получате .hex файл, котрый заливаете в микроконтроллер. Кажется, что всё магически превращается в работающее устройство. Но за этой магией стоит строгий инженерный процесс, и для embedded-систем у него есть свои особенности.

Главное отличие от разработки под ПК - кросс-компиляция. Вы работаете на мощном x86-процессоре, а код выполняется на ARM, AVR или RISC-V. Компилятор должен не только перевести C в машинные команды, но и учесть, что память микроконтроллера разделена на флеш (ROM) и RAM, а ресурсы жёстко ограничены.

Современный стандарт де-факто в мире компиляторов - LLVM/Clang. Почему? Потому что это не просто компилятор, а модульный фреймворк. Clang отвечает за разбор языка C/C++, а LLVM - за генераию оптимального кода под любую архитектуу. Это даёт прозначность: на каждом этапе можно заглянуть внутрь и понять, что происходит.

Весь путь прошивки состоит из четырёх основных этапов:

🔴 Препроцессинг - текстовая подготовка.
🔴 Компиляция - перевод в промежуточное представление и оптимизация.
🔴 Ассемблирование - создание объектных файлов.
🔴 Компоновка (линковка) - сборка всего в единый образ.

И финальный шаг - упаковка в прошивку (.bin/.hex).

Далее пройдём каждый этап с примерами команд и объяснением внутренней кухни.
А пока лишь запомним: LLVM/Clang позволяет нам контролировать процесс на любом уровне, а не просто надеяться на чёрный ящик.

#embedded #компиляция #LLVM #Clang #микроконтроллеры #программирование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Препроцессинг и компиляция: от текста до промежуточного представления

Clang и LLVM IR: на каком языке думают компиляторы.

Первый этап - препроцессинг. Это самая простая часть: компилятор берёт ваш main.c и выполняет текстовые директивы. Он выкидывает коментарии, вставляет заголовочные файлы (#include), раскрывает макросы (#define) и обрабатываеи условную компиляцию (#ifdef). В Clang остановиться после препроцессора можно флагом -E:

clang -E main.c -o main.i

Теперь у нас есть один большой .i-файл с чистым кодом на C. На этом этапе часто кроются проблемы с макросами или неверными путями к заголовочным файлам.

Следующий шаг - собственно компиляция. Здесь Clang делает синтаксический и семантический анализ. Ошибки вроде забытой точки с запятой или несоответствия типов отлавливаются именно сейчас. Но Clang идёт дальше: вместо того чтобы сразу генерировать ассемблер, он создаёт промежуточное представление LLVM IR (Intermediate Representation).

LLVM IR - это нечто среднее между C и ассемблером, но не привязанное к конкретному процессору. Его можно представить как код для виртуальной машины с бесконечным числом регистров. Вот как выглдит простейшая функция main в IR:

define i32 @main() {
ret i32 0
}

Этот код уже оптимизирован, но ещё не превращён в машинные инструкции. Посмотреть его можно командой:

clang -S -emit-llvm main.c -o main.ll

Зачем это нужно? Потому что на уровне IR работают мощные оптимизаторы. LLVM может применить оптимизации, не думая о конкретной архитектуре: удалить мёртвый код, заменить константы, развернуть циклы. Причём оптимизации можно отложить до этапа компоновки (Link-Time Optimization, LTO), когда видна вся программа целиком.

После оптимизаций бэкенд LLVM транслиурет IR в ассемблер целевого процессру. Для этого ему нужно знать архитектуру: ARM, AVR, RISC-V. Это задаётся флагами вроде -target armv7m-none-eabi -mcpu=cortex-m3. Результат - файл .s с ассемблерными инструкциями.

Итог: этап компиляции превращает ваш C-код в машинно-зависимый ассемблер, но делает это через универсальное промежуточное звено, что даёт гибкость и мощные оптимизации. В следующем посте узнаем, что происходит с ассемблером дальше.

#компиляция #LLVM #Clang #IR #оптимизация #микроконтроллеры #программирование
👍1
Ассемблирование и линковка: собираем пазл из объектных файлов

Объектные файлы и линкер-скрипты: как микроконтроллер узнаёт, где что лежит

После того как у нас есть ассемблерный код (main.s), его нужно превратить в настоящий машинный код. Это делает ассемблер (в составе LLVM). Он просто транслируеь каждую инсрукцию в байты и упаковывает их в объектный файл (main.o).

Объектный файл - это контейнер с несколькими секциями:

🔴 .text - код программы.
🔴 .data - инициализированные глобальные переменные.
🔴 .bss - описание неинициализированных переменных (занимает место в памяти, но не в файле).
🔴 Таблица символов - имена функций и переменных, определённых в этом файле.
🔴 Записи релокаци - пометки о местах, куда позже нужно подставить реальные адреса.

На этом этапе адреса ещё не назначены. Например, вызов функции может быть записан как "вызвать функцию по адресу 0", потому что реальное расположение функции станет известно только после зборки всех объектных файлов.

Создать объектный файл в Clang можно так:

clang -c main.s -o main.o

Теперь у нас есть несколько объектных файлов (например, main.o, delay.o, uart.o). Их нужно объединить в одну программу. Это делает компоновщик (линкер) - в мире LLVM это lld.

Линковка решает две задачи:

🟡 Разрешение символов. Сопоставляет вызовы функций с их фактическими определениями. Если функция объявлена, но не определена - ошибка "undefined reference".
🟡 Релокация. Расставляет реальные адреса. В объектном файле вызов delay() был "вызови по адресу 0". Линкер знает, что функция delay будет лежать, скажем, по адресу 0x1000, и вписывает этот адрес в код.

Но для микроконтроллера этого недостаточно: нужно ещё сказать, куда именно класть код и данные. Ведь у микроконтроллера есть флеш-память (для кода) и RAM (для переменных). Для этого используется линкер-скрипт (.ld). Он описывает карту памяти: "флеш начинается с адреса 0x08000000, RAM - с 0x20000000; код клади во флеш, переменные - в RAM, а начальные значения переменых храни во флеше и копируй при старте".

Линковка с линкер-скриптом выглядит так:

clang main.o delay.o -T stm32f103.ld -o firmware.elf

На выходе получается ELF-файл - исполняемый образ, содержащий все байты кода и данных, а также отладочную информацию. Именно этот файл нужен отладчику (GDB, например), чтобы показывать вам исходный код во время отладки. Но микроконтроллеру ELF не подходит - ему нужен сырой дамп. Об этом - в заключительном посте.

#компоновка #линковка #ELF #объектныефайлы #LLVM #микроконтроллеры
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Финальная упаковка: как получить .bin и зачем это знать

От ELF до прошивки: последний шаг перед загрузкой в микроконтроллер

Мы получили firmware.elf - файл, который содержит всё: код, данные, адреса, отладочную информацию. Но программатору (ST-Link, J-Link) нужен простой бинарый дамп: байты, которые нужно записать во флеш-память по порядку. Именно здесь вступает в игру утилита objcopy (в LLVM - llvm-objcopy).

Команда:

llvm-objcopy -O binary firmware.elf firmware.bin

Флаг -O binary означает: "извлеки из ELF-файла только секци, которые должны быть во флеше (обычно .text, .data и т.д.), и запиши их подряд, как они расположены в памяти". Получается файл firmware.bin, готовый к прошивке.

Альтернативно можно создать Intel HEX (-O ihex) - текстовый формат, где каждая строка содержит адрес и данные. Многие старые программаторы любят именно HEX.

Теперь этот бинарик можно загрузить в микроконтроллер, и он запутсится.

Зачем разработчику знать все эти этапы?

Понимание процесса компиляции даёт вам суперсилы:

🔴 Ошибки линковки больше не будут загадкой. "Undefined reference" - значит, символ не найден среди объектных файлов; проверяйте, подключили ли вы нужную библиотеку.
🔴 Управление памятью. Если прошивка не влезает во флеш, вы можете посмотреть размер секций с помощью утилит вроде llvm-size и понять, что раздуло код. А линкер-скрипт позволяет вручную распределить данные по памяти.
🔴 Оптимизация. LTO (Link-Time Optimization) может выкинуть неиспользуемые функции из разных файлов, уменьшив размер прошивки. Это включается флагом -flto.
🔴 Отладка на низком уровне. Иногда полезно заглянуть в ассемблерный листинг или даже в LLVM IR, чтобы понять, во что компилятор превратил ваш хитрый алгоритм.

Итак, полный путь от main.c до firmware.bin выглядит так:

🟡 Препроцессор: main.c → main.i
🟡 Фронтенд Clang + оптимизатор LLVM: main.i → LLVM IR → main.s
🟡 Ассемблер: main.s → main.o
🟡 Линковка (с линкер-скриптом): main.o + другие .o → firmware.elf
🟡 objcopy: firmware.elf → firmware.bin

Каждый этап открыт для вас. LLVM/Clang даёт инструменты, чтобы заглянуть внутрь и понять, что происходит. Это знание превращает компиляию из магии в инженерную дисциплину.

Теперь вы готовы не только писать код, но и полностью контролировать его превращение в работающее устройство.

#прошивка #bin #hex #objcopy #ELF #микроконтроллеры #компиляция #LLVM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2