Вы еще не программируете микроконтроллеры? Тогда мы идем к вам!

Здравствуйте, уважаемые Хабражители!
В этой статье я хочу рассказать о том, как однажды решил начать программировать микроконтроллеры, что для этого понадобилось и что в итоге получилось.
Тема микроконтроллеров меня заинтересовала очень давно, году этак в 2001. Но тогда достать программатор по месту жительства оказалось проблематично, а о покупке через Интернет и речи не было. Пришлось отложить это дело до лучших времен. И вот, в один прекрасный день я обнаружил, что лучшие времена пришли не выходя из дома можно купить все, что мне было нужно. Решил попробовать. Итак, что нам понадобится:
1. Программатор
На рынке предлагается много вариантов — от самых дешевых ISP (In-System Programming) программаторов за несколько долларов, до мощных программаторов-отладчиков за пару сотен. Не имея большого опыта в этом деле, для начала я решил попробовать один из самых простых и дешевых — USBasp. Купил в свое время на eBay за $12, сейчас можно найти даже за $3-4. На самом деле это китайская версия программатора от Thomas Fischl. Что могу сказать про него? Только одно — он работает. К тому же поддерживает достаточно много AVR контроллеров серий ATmega и ATtiny. Под Linux не требует драйвера.

Для прошивки надо соединить выходы программатора VCC, GND, RESET, SCK, MOSI, MISO с соответствующими выходами микроконтроллера. Для простоты я собрал вспомогательную схему прямо на макетной плате:

Слева на плате — тот самый микроконтроллер, который мы собираемся прошивать.
2. Микроконтроллер
С выбором микроконтроллера я особо не заморачивался и взял ATmega8 от Atmel — 23 пина ввода/вывода, два 8-битных таймера, один 16-битный, частота — до 16 Мгц, маленькое потребление (1-3.6 мА), дешевый ($2). В общем, для начала — более чем достаточно.

Под Linux для компиляции и загрузки прошивки на контроллер отлично работает связка avr-gcc + avrdude. Установка тривиальная. Следуя инструкции, можно за несколько минут установить все необходимое ПО. Единственный ньюанс, на который следует обратить внимание — avrdude (ПО для записи на контроллер) может потребовать права супер-пользователя для доступа к программатору. Выход — запустить через sudo (не очень хорошая идея), либо прописать специальные udev права. Синтаксис может отличаться в разных версиях ОС, но в моем случае (Linux Mint 15) сработало добавление следующего правила в файл /etc/udev/rules.d/41-atmega.rules :
# USBasp programmer SUBSYSTEM=="usb", ATTR=="16c0", ATTR=="05dc", GROUP="plugdev", MODE="0666"
После этого, естественно, необходим перезапуск сервиса
service udev restart
Компилировать и прошивать без проблем можно прямо из командной строки (кто бы сомневался), но если проектов много, то удобнее поставить плагин AVR Eclipse и делать все прямо из среды Eclipse.
Под Windows придется поставить драйвер. В остальном проблем нет. Ради научного интереса попробовал связку AVR Studio + eXtreme Burner в Windows. Опять-таки, все работает на ура.
Начинаем программировать
Программировать AVR контроллеры можно как на ассемблере (AVR assembler), так и на Си. Тут, думаю, каждый должен сделать свой выбор сам в зависимости от конкретной задачи и своих предпочтений. Лично я в первую очередь начал ковырять ассемблер. При программировании на ассемблере архитектура устройства становится понятнее и появляется ощущение, что копаешься непосредственно во внутренностях контроллера. К тому же полагаю, что в особенно критических по размеру и производительности программах знание ассемблера может очень пригодиться. После ознакомления с AVR ассемблером я переполз на Си.
После знакомства с архитектурой и основными принципами, решил собрать что-то полезное и интересное. Тут мне помогла дочурка, она занимается шахматами и в один прекрасный вечер заявила, что хочет иметь часы-таймер для партий на время. БАЦ! Вот она — идея первого проекта! Можно было конечно заказать их на том же eBay, но захотелось сделать свои собственные часы, с блэк… эээ… с индикаторами и кнопочками. Сказано — сделано!
В качестве дисплея решено было использовать два 7-сегментных диодных индикатора. Для управления достаточно было 5 кнопок — “Игрок 1” , “Игрок 2” , “Сброс” , “Настройка” и “Пауза” . Ну и не забываем про звуковую индикацию окончания игры. Вроде все. На рисунке ниже представлена общая схема подключения микроконтроллера к индикаторам и кнопкам. Она понадобится нам при разборе исходного кода программы:

Разбор полета
Начнем, как и положено, с точки входа программы — функции main . На самом деле ничего примечательного в ней нет — настройка портов, инициализация данных и бесконечный цикл обработки нажатий кнопок. Ну и вызов sei() — разрешение обработки прерываний, о них немного позже.
int main(void) < init_io(); init_data(); sound_off(); sei(); while(1) < handle_buttons(); >return 0; >
Рассмотрим каждую функцию в отдельности.
void init_io() < // set output DDRB = 0xFF; DDRD = 0xFF; // set input DDRC = 0b11100000; // pull-up resistors PORTC |= 0b00011111; // timer interrupts TIMSK = (1<
Настройка портов ввода/вывода происходит очень просто — в регистр DDRx (где x — буква, обозначающая порт) записивается число, каждый бит которого означает, будет ли соответствующий пин устройством ввода (соответствует 0) либо вывода (соответствует 1). Таким образом, заслав в DDRB и DDRD число 0xFF, мы сделали B и D портами вывода. Соответственно, команда DDRC = 0b11100000; превращает первые 5 пинов порта C во входные пины, а оставшиеся — в выходные. Команда PORTC |= 0b00011111; включает внутренние подтягивающие резисторы на 5 входах контроллера. Согласно схеме, к этим входам подключены кнопки, которые при нажатии замкнут их на землю. Таким образом контроллер понимает, что кнопка нажата.
Далее следует настройка двух таймеров, Timer0 и Timer1. Первый мы используем для обновления индикаторов, а второй — для обратного отсчета времени, предварительно настроив его на срабатывание каждую секунду. Подробное описание всех констант и метода настройки таймера на определенноый интервал можно найти в документации к ATmega8.
Обработка прерываний
ISR (TIMER0_OVF_vect) < display(); if (_buzzer >0) < _buzzer--; if (_buzzer == 0) sound_off(); >> ISR(TIMER1_COMPA_vect) < if (ActiveTimer == 1 && Timer1 >0) < Timer1--; if (Timer1 == 0) process_timeoff(); >if (ActiveTimer == 2 && Timer2 > 0) < Timer2--; if (Timer2 == 0) process_timeoff(); >>
При срабатывании таймера управление передается соответствующему обработчику прерывания. В нашем случае это обработчик TIMER0_OVF_vect, который вызывает процедуру вывода времени на индикаторы, и TIMER1_COMPA_vect, который обрабатывает обратный отсчет.
Вывод на индикаторы
void display() < display_number((Timer1/60)/10, 0b00001000); _delay_ms(0.25); display_number((Timer1/60)%10, 0b00000100); _delay_ms(0.25); display_number((Timer1%60)/10, 0b00000010); _delay_ms(0.25); display_number((Timer1%60)%10, 0b00000001); _delay_ms(0.25); display_number((Timer2/60)/10, 0b10000000); _delay_ms(0.25); display_number((Timer2/60)%10, 0b01000000); _delay_ms(0.25); display_number((Timer2%60)/10, 0b00100000); _delay_ms(0.25); display_number((Timer2%60)%10, 0b00010000); _delay_ms(0.25); PORTD = 0; >void display_number(int number, int mask)
Функция display использует метод динамической индикации. Дело в том, что каждый отдельно взятый индикатор имеет 9 контактов (7 для управления сегментами, 1 для точки и 1 для питания). Для управления 4 цифрами понадобилось бы 36 контактов. Слишком расточительно. Поэтому вывод разрядов на индикатор с несколькими цифрами организован по следующему принципу:
Напряжение поочередно подается на каждый из общих контактов, что позволяет высветить на соответствующем индикаторе нужную цифру при помощи одних и тех же 8 управляющих контактов. При достаточно высокой частоте вывода это выглядит для глаза как статическая картинка. Именно поэтому все 8 питающих контактов обоих индикаторов на схеме подключены к 8 выходам порта D, а 16 управляющих сегментами контактов соединены попарно и подключены к 8 выходам порта B. Таким образом, функция display с задержкой в 0.25 мс попеременно выводит нужную цифру на каждый из индикаторов. Под конец отключаются все выходы, подающие напряжение на индикаторы (команда PORTD = 0; ). Если этого не сделать, то последняя выводимая цифра будет продолжать гореть до следующего вызова функции display, что приведет к ее более яркому свечению по сравнению с остальными.
Обработка нажатий
void handle_buttons() < handle_button(KEY_SETUP); handle_button(KEY_RESET); handle_button(KEY_PAUSE); handle_button(KEY_PLAYER1); handle_button(KEY_PLAYER2); >void handle_button(int key) < int bit; switch (key) < case KEY_SETUP: bit = SETUP_BIT; break; case KEY_RESET: bit = RESET_BIT; break; case KEY_PAUSE: bit = PAUSE_BIT; break; case KEY_PLAYER1: bit = PLAYER1_BIT; break; case KEY_PLAYER2: bit = PLAYER2_BIT; break; default: return; >if (bit_is_clear(BUTTON_PIN, bit)) < if (_pressed == 0) < _delay_ms(DEBOUNCE_TIME); if (bit_is_clear(BUTTON_PIN, bit)) < _pressed |= key; // key action switch (key) < case KEY_SETUP: process_setup(); break; case KEY_RESET: process_reset(); break; case KEY_PAUSE: process_pause(); break; case KEY_PLAYER1: process_player1(); break; case KEY_PLAYER2: process_player2(); break; >sound_on(15); > > > else < _pressed &= ~key; >>
Эта функция по очереди опрашивает все 5 кнопок и обрабатывает нажатие, если таковое случилось. Нажатие регистрируется проверкой bit_is_clear(BUTTON_PIN, bit) , т.е. кнопка нажата в том случае, если соответствующий ей вход соединен с землей, что и произойдет, согласно схеме, при нажатии кнопки. Задержка длительностью DEBOUNCE_TIME и повторная проверка нужна во избежание множественных лишних срабатываний из-за дребезга контактов. Сохранение статуса нажатия в соответствующих битах переменной _pressed используется для исключения повторного срабатывания при длительном нажатии на кнопку.
Функции обработки нажатий достаточно тривиальны и полагаю, что в дополнительных комментариях не нуждаются.
Полный текст программы
#define F_CPU 4000000UL #include #include #include #define DEBOUNCE_TIME 20 #define BUTTON_PIN PINC #define SETUP_BIT PC0 #define RESET_BIT PC1 #define PAUSE_BIT PC2 #define PLAYER1_BIT PC3 #define PLAYER2_BIT PC4 #define KEY_SETUP 0b00000001 #define KEY_RESET 0b00000010 #define KEY_PAUSE 0b00000100 #define KEY_PLAYER1 0b00001000 #define KEY_PLAYER2 0b00010000 volatile int ActiveTimer = 0; volatile int Timer1 = 0; volatile int Timer2 = 0; volatile int _buzzer = 0; volatile int _pressed = 0; // function declarations void init_io(); void init_data(); int number_mask(int num); void handle_buttons(); void handle_button(int key); void process_setup(); void process_reset(); void process_pause(); void process_timeoff(); void process_player1(); void process_player2(); void display(); void display_number(int mask, int number); void sound_on(int interval); void sound_off(); // interrupts ISR (TIMER0_OVF_vect) < display(); if (_buzzer >0) < _buzzer--; if (_buzzer == 0) sound_off(); >> ISR(TIMER1_COMPA_vect) < if (ActiveTimer == 1 && Timer1 >0) < Timer1--; if (Timer1 == 0) process_timeoff(); >if (ActiveTimer == 2 && Timer2 > 0) < Timer2--; if (Timer2 == 0) process_timeoff(); >> int main(void) < init_io(); init_data(); sound_off(); sei(); while(1) < handle_buttons(); >return 0; > void init_io() < // set output DDRB = 0xFF; DDRD = 0xFF; // set input DDRC = 0b11100000; // pull-up resistors PORTC |= 0b00011111; // timer interrupts TIMSK = (1<void init_data() < Timer1 = 0; Timer2 = 0; ActiveTimer = 0; >int number_mask(int num) < switch (num) < case 0 : return 0xC0; case 1 : return 0xF9; case 2 : return 0xA4; case 3 : return 0xB0; case 4 : return 0x99; case 5 : return 0x92; case 6 : return 0x82; case 7 : return 0xF8; case 8 : return 0x80; case 9 : return 0x90; >; return 0; > void process_setup() < Timer1 += 60; Timer2 += 60; // overflow check (5940 seconds == 99 minutes) if (Timer1 >5940 || Timer2 > 5940) < Timer1 = 0; Timer2 = 0; >> void process_reset() < init_data(); >void process_timeoff() < init_data(); sound_on(30); >void process_pause() < ActiveTimer = 0; >void process_player1() < ActiveTimer = 2; >void process_player2() < ActiveTimer = 1; >void handle_button(int key) < int bit; switch (key) < case KEY_SETUP: bit = SETUP_BIT; break; case KEY_RESET: bit = RESET_BIT; break; case KEY_PAUSE: bit = PAUSE_BIT; break; case KEY_PLAYER1: bit = PLAYER1_BIT; break; case KEY_PLAYER2: bit = PLAYER2_BIT; break; default: return; >if (bit_is_clear(BUTTON_PIN, bit)) < if (_pressed == 0) < _delay_ms(DEBOUNCE_TIME); if (bit_is_clear(BUTTON_PIN, bit)) < _pressed |= key; // key action switch (key) < case KEY_SETUP: process_setup(); break; case KEY_RESET: process_reset(); break; case KEY_PAUSE: process_pause(); break; case KEY_PLAYER1: process_player1(); break; case KEY_PLAYER2: process_player2(); break; >sound_on(15); > > > else < _pressed &= ~key; >> void handle_buttons() < handle_button(KEY_SETUP); handle_button(KEY_RESET); handle_button(KEY_PAUSE); handle_button(KEY_PLAYER1); handle_button(KEY_PLAYER2); >void display() < display_number((Timer1/60)/10, 0b00001000); _delay_ms(0.25); display_number((Timer1/60)%10, 0b00000100); _delay_ms(0.25); display_number((Timer1%60)/10, 0b00000010); _delay_ms(0.25); display_number((Timer1%60)%10, 0b00000001); _delay_ms(0.25); display_number((Timer2/60)/10, 0b10000000); _delay_ms(0.25); display_number((Timer2/60)%10, 0b01000000); _delay_ms(0.25); display_number((Timer2%60)/10, 0b00100000); _delay_ms(0.25); display_number((Timer2%60)%10, 0b00010000); _delay_ms(0.25); PORTD = 0; >void display_number(int number, int mask) < PORTB = number_mask(number); PORTD = mask; >void sound_on(int interval) < _buzzer = interval; // put buzzer pin high PORTC |= 0b00100000; >void sound_off() < // put buzzer pin low PORTC &= ~0b00100000; >
Прототип был собран на макетной плате:

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

Ниже показан окончательный вид устройства. Часы питаются от 9-вольтовой батарейки типа “Крона”. Потребление тока — 55 мА.

Заключение
Потратив $20-25 на оборудование и пару вечеров на начальное ознакомление с архитектурой микроконтроллера и основными принципами работы, можно начать делать интересные DIY проекты. Статья посвящается тем, кто, как и я в свое время, думает, что начать программировать микроконтроллеры — это сложно, долго или дорого. Поверьте, начать намного проще, чем может показаться. Если есть интерес и желание — пробуйте, не пожалете!
Удачного всем программирования!
P.S. Ну и напоследок, небольшая видео-демонстрация прототипа:
Диспетчер Задач для Микроконтроллера

Для этого конечно можно запустить FreeRTOS, однако тогда код не будет переносим на другие RTOS, например Zephyr RTOS/TI-RTOS/RTEMS/Keil RTX/Azure RTOS или SafeRTOS. Потом прошивку как код часто приходится частично отлаживать на PC а там никакой RTOS в помине нет.
Поэтому надо держать наготове какой-нибудь простенький универсальный переносимый кооперативный NoRTOS планировщик с минимальной диагностикой и возможностью в run-time отключать какие-то отдельные задачи для отладки оставшихся.
Проще говоря нужен диспетчер задач для микроконтроллера.
Определимся с терминологией
Кооперативный планировщик — это такой способ управления задачами, при котором задачи сами вручную передают управления другим задачам.
Супер-цикл — тело оператора бесконечного цикла в NoRTOS прошивках. Обычно бесконечный цикл в прошивках можно найти по таким операторам как for(;;)<> или while(1)<>
Bare-Bone сборка — это сборка прошивки на основе API какой-нибудь RTOS, где только один поток и этот поток прокручивает супер-цикл с кооперативным планировщиком. Эта сборка нужна главным образом только для отладки RTOS: настройки стека, очередей и прочего.
Ядром любого планировщика является генератор или источник стабильного тактирования. Желательно с высокой разрешающей способностью. Микросекундный таймер. Это может быть SysTick таймер с пересчетом в микросекунды или отдельный аппаратный таймер общего назначения. Обычно аппаратных таймеров от 3х до 14ти в зависимости от модели конкретного микроконтроллера. Также важно, чтобы таймер возрастал. Так проще и интуитивно понятнее писать код нам человекам, так как мы привыкли к тому что время оно всегда непрерывно идет вперед, а не назад.
Для определенности будем считать, что все задачи имеют одинаковых прототип
bool task_proc(void);
Сначала надо определить типы данных.
#ifndef TASK_GENERAL_TYPES_H #define TASK_GENERAL_TYPES_H /*Mainly for NoRtos builds but also for so-called RTOS Bare-Bone build*/ #include #include "task_const.h" #include "limiter.h" typedef struct < uint64_t period_us; bool init; #ifdef HAS_LIMITER Limiter_t limiter; #endif const char* const name; >TaskConfig_t; #endif /* TASK_GENERAL_TYPES_H */
Ключевым компонентом планировщика, его ядром является программный компонент называемый Limiter. Это такой программный компонент, который не позволит вызывать функцию function чаще чем установлено в конфиге. Например вызывать функцию не чаще чем раз в секунду или не чаще чем раз в 10 ms.
#ifndef LIMITER_TYPES_H #define LIMITER_TYPES_H #include #include #include "data_types.h" typedef bool (*TaskFunc_t)(void); typedef struct < bool init; bool on_off; uint32_t call_cnt; uint64_t start_time_next_us; U64Value_t duration_us; U64Value_t start_period_us; uint64_t run_time_total_us; uint64_t start_time_prev_us; TaskFunc_t function; >Limiter_t; #endif /* LIMITER_TYPES_H */
Вот API планировщика. Механизм очень прост. Limiter измеряет время с момента подачи питания up_time_us, смотрит на расписание следующего запуска start_time_next_us и, если текущее время (up_time_us) больше времени запуска, назначает следующее время запуска и запускает задачу (limiter_task_frame).
bool inline limiter(Limiter_t* const Node, uint32_t period_us, uint64_t up_time_us) < bool res = false; if(Node->on_off) < if(Node->start_time_next_us < up_time_us) < Node->start_time_next_us = up_time_us + period_us; res = limiter_task_frame(Node); > if(up_time_us < Node->start_time_prev_us) < LOG_DEBUG(LIMITER, "UpTimeOverflow %llu", up_time_us); Node->start_time_next_us = up_time_us + period_us; > Node->start_time_prev_us = up_time_us; > return res; >
Limiter также ведёт аналитику. Измеряет время старта и окончания задачи, вычисляет продолжительность исполнения задачи (duration), вычисляет минимум (run_time.min) и максимум (duration.max), суммирует общее время, которое данная задача исполнялась на процессоре (run_time_total).
static inline bool limiter_task_frame(Limiter_t* const Node) < bool res = false; if(Node) < uint64_t start_us = 0; uint64_t stop_us = 0; uint64_t duration_us = 0; uint64_t period_us = 0; start_us = limiter_get_time_us(); if(Node->start_time_prev_us < start_us) < period_us = start_us - Node->start_time_prev_us; res = true; > else < period_us = 0; /*(0x1000000U + start) - TASK_ITEM.start_time_prev; */ res = false; >Node->start_time_prev_us = start_us; if(res) < data_u64_update(&Node->start_period_us, period_us); > res = true; #ifdef HAS_FLASH res = is_flash_addr((uint32_t)Node->function); #endif /*HAS_FLASH*/ if(res) < Node->call_cnt++; res = Node->function(); > else < res = false; >stop_us = limiter_get_time_us(); if(start_us < stop_us) < duration_us = stop_us - start_us; res = true; data_u64_update(&Node->duration_us, duration_us); Node->run_time_total_us += duration_us; > else < duration_us = 0; res = false; >> return res; >
Стоит заметить, что перед непосредственным запуском конкретной задачи Limiter может проверить, что указатель на функцию в самом деле принадлежит Nor-Flash памяти микроконтроллера.
В основном супер цикле достаточно только перечислить те задачи, которые будут исполняться. Вот функция одной итерации супер-цикла.
bool inline tasks_proc(uint64_t loop_start_time_us) < bool res = false; uint32_t cnt = task_get_cnt(); uint32_t t = 0; for (t=0; t> return res; > bool super_cycle_iteration(void) < bool res = false; if(SuperCycle.init) < SuperCycle.spin_cnt++; res = true; SuperCycle.run = true; SuperCycle.start_time_us = time_get_us(); LOG_DEBUG(SUPER_CYCLE, "Proc %f Spin:%u", USEC_2_SEC(SuperCycle.start_time_us),SuperCycle.spin_cnt); if(SuperCycle.prev_start_time_us < SuperCycle.start_time_us) < SuperCycle.error++; >SuperCycle.duration_us.cur = (uint32_t)(SuperCycle.start_time_us - SuperCycle.prev_start_time_us); SuperCycle.duration_us.min = (uint32_t)MIN(SuperCycle.duration_us.min, SuperCycle.duration_us.cur); SuperCycle.duration_us.max = (uint32_t)MAX(SuperCycle.duration_us.max, SuperCycle.duration_us.cur); super_cycle_check_continuity(&SuperCycle, loop_start_time_us); tasks_proc(SuperCycle.start_time_us); SuperCycle.prev_start_time_us = SuperCycle.start_time_us; > return res; >
Вот код запуска супер цикла. Из функции super_cycle_start и будет исполняться вся прошивка, за исключением вызова обработчиков прерываний ISR.
_Noreturn void super_cycle_start(void) < LOG_INFO(SUPER_CYCLE, "Start"); super_cycle_init(); SuperCycle.start_time_ms = time_get_ms(); LOG_INFO(SUPER_CYCLE, "Started, UpTime: %u ms", SuperCycle.start_time_ms); for(;;) < super_cycle_iteration(); >>
Такая сформировалась зависимость между программными компонентами данного планировщика.

Отладка планировщика
Очевидно, что надо как-то наблюдать за работой планировщика. Для этого планировщик и были разработан, чтобы снимать метрики. Для этого можно воспользоваться интерфейсом командной строки CLI поверх UART.
В данном скриншоте можно замерить, что больше всего процессорного времени потребляет задача DASHBOARD (приборная панель). Тут же видно, что были такие итерации супер цикла, что задача DASHBOARD непрерывно исполнялась аж 0.33 сек!

Можно измерить период с которым вызывалась каждая из задач и сопоставить с конфигом для каждой задачи. Тут видно, что в среднем реже всего вызывается задача FLASH_FS (менеджер файловой системы). Одновременно драйвер светодиода LED_MONO отрабатывает c частотой (44 Hz). А чаще всего происходит опрос DecaDriver(а).

Тут заметно даже, что накладные расходы на данный планировщик составляют 47.9% по времени. Это лишь потому, что в обработчиках самих задач не происходит пока никакой обработки. Прошивка работает вхолостую. Не было прерываний и флаги не устанавливаются. Не приходят пакетов в интерфейсы. Ничего не происходит.
Также высокое процентное значение SchedulerOverhead это признак того, что периоды у всех задач большие и процессору нечего делать. А значит можно смело добавлять в супер цикл больше кооперативных задач или уменьшать периоды у нынешних задач.
Анализируя эти ценнейшие метрики данного импровизированного планировщика можно принимать решения по оптимизации кода всего проекта. Получился своеобразный Code Coverage.
Достоинства данного планировщика
1—Простота, очевидность, прозрачность, мало кода.
2—Можно вычистить процент загрузки процессора по каждой задаче.
3—Переносимость. Можно его прокручивать хоть на микроконтроллере, хоть на BareBone потоке в RTOS, хоть в консольном приложении на LapTop PC.
4—Приоритет задачи задается периодом её запуска. Чем ниже период, тем выше приоритет.
5—Легко масштабировать прошивку. Просто добавляем новые строчки в super цикл.
6—Можно весь этот планировщик вообще реализовать на функциях препроцессора. И тогда не будет накладных расходов на запуск функций. Однако так будет невозможно осуществлять пошаговую отладку программы.
7—Можно переназначать функции для узлов планировщика и таким образом перепрограммировать устройство далеко в Run-Time.
Недостатки данного планировщика
1—Если одна задача зависла, то считай что зависли все остальные задачи.
2—Надо проектировать задачи так, чтобы они что-то делали за один прогон и не тратили много времени внутри себя. Например переключили состояние конечного автомата и вышли. Совсем не здорово, если какая-то задача начнет расшифровывать 150kByte KeePass файл внутри общего супер цикла или вычислять обратную матрицу 100×100. У Вас перестанет мигать Heart Beat LED, перестанет отвечать CLI и пользователь будет с полной уверенностью считать, что прошивка просто взяла и зависла! А на самом деле программа через 57 секунд снова воспрянет.
3—Требуются накладные расходы (в виде процессорного времени) для вычисления метрик за которыми следит Limiter. Но это не такая и большая проблема, так как отладочные метрики можно включать или исключать на стадии препроцессора #ifdef(ами).
Вывод
Вот и Вы умеете делать кооперативный планировщик. Супер цикл это не такая уж и плохая вещь. Его можно отлично использовать и в RTOS прошивках. Есть код которому точно нужен RTOS. Это BLE/LwIP стек, однако всё остальное: LED, Button может отлично работать в пределах супер цикла в отдельном BareBone потоке. Благодаря супер циклу вы сэкономите на переключении контекста. Надеюсь, что этот текст поможет кому-нибудь писать прошивки и оценивать нагрузку на процессор.
Словарь
Акроним
Расшифровка
Инструкция
2.2 Приводы станков Привод — устройство, служащее для приведения в действие исполнительных звеньев станка. В привод входит также источник движения. Привод должен обеспечивать возможность регулирования… Подробнее » Что такое привод станка
Что такое программирование задачи жизнеобеспечения на микроконтроллере
- автор: admin
- 27.07.2023
Программирование микроконтроллеров: с чего лучше начать Что это? Программирование микроконтроллеров тесно связано с интернетом вещей. То есть вы пишите программу для компактного умного устройства, который… Подробнее » Что такое программирование задачи жизнеобеспечения на микроконтроллере
Что такое просадка напряжения в частном доме
- автор: admin
- 27.07.2023
Провалы напряжения, их характеристики и методы защиты Эффект «проседания» входного напряжения ниже установленной нормы довольно распространенная проблема. Она более характерна для электроснабжения в сельской местности,… Подробнее » Что такое просадка напряжения в частном доме
Что такое привод в механике
- автор: admin
- 27.07.2023
9. Назначение и структура механического привода Назначение и структура механического привода. Основные характеристики привода. Назначение и классификация передач (передачи зацеплением и трением, зубчатые, червячные, а… Подробнее » Что такое привод в механике
Что такое проводники полупроводники и диэлектрики
- автор: admin
- 27.07.2023
Проводники и диэлектрики Проводники и диэлектрики — это, в общем-то то, что нас и окружает. Но, обо всём по порядку. В веществе, помещённом в электрическое… Подробнее » Что такое проводники полупроводники и диэлектрики
Программирование современных микроконтроллеров: лекция 1
Конспект первой лекции по программированию современных микроконтроллеров на примере STM32 и операционной системы RIOT. Лекции читаются в Институте информационных технологий МИРЭА по субботам, с 12:50 в актовом зале на 4 этаже корпуса Д. В занятиях отводится 1,5 часа на саму лекцию и 3 часа на практические занятия в лаборатории IoT Академии Samsung по теме лекции.
Привет, Гиктаймс! Как мы и обещали, начинаем публикацию конспектов лекций, которые сейчас читаются в Институте ИТ МИРЭА. По результатам первой, вводной лекции мы решили немного изменить структуру курса — вместо планировавшихся двух потоков по 5 занятий будет один поток на 7 занятий. Это позволит в более спокойном темпе разобрать ряд вспомогательных вопросов, а также статьи с конспектом будут появляться на GT каждую неделю в течение всего марта и апреля, а не через неделю, как планировалось раньше.
Тем не менее, в семь лекций невозможно полностью уложить столь обширную тему, поэтому местами изложение будет тезисным — хотя для компенсации этого мы постараемся указывать, в какую сторону смотреть тем, кто хочет самостоятельно глубже разобраться в том или ином вопросе.
Курс рассчитан на студентов второго и третьего курсов, знакомых с языком C и базовыми понятиями электроники и электротехники. Предварительное знакомство с микроконтроллерами не требуется.
Цель курса — освоение навыков, позволяющих свободно работать с микроконтроллерами на ядре ARM Cortex-M на современном уровне и, при наличии такого желания, двигаться в сторону дальнейшего углубления своих знаний.

Сегодняшняя лекция — первая, поэтому на ней будут разбираться общие понятия: что такое вообще микроконтроллер и зачем он нужен, что такое прошивка и как она получается, зачем нам нужна операционная система, и наконец — как работать с git. Результат практического занятия — собственный репозитарий на GitHub с исходными кодами ОС, а также успешно настроенная среда сборки на локальном компьютере.
Микроконтроллер
Если говорить коротко, то микроконтроллер — это классический пример «системы на чипе», включающей в себя как процессорное ядро, так и набор вспомогательных и периферийных устройств, позволяющий микроконтроллеру во многих случаях быть полностью самодостаточным.

В типовом микропроцессоре, подобном тому, что стоит в любом ПК или смартфоне, практически все модули, которые можно отнести к вспомогательным (питание, тактирование, даже базовые периферийные устройства), вынесены за пределы самого чипа, несмотря на то, что работать без них микропроцессор не может.
В микроконтроллере же, наоборот, на одном кристалле с ядром реализованы не только необходимые для его работы подсистемы, но и масса периферийных устройств, которые могут потребоваться в различных практических задачах. Более того, многие производители микроконтроллеров соревнуются друг с другом не по производительности ядра или объёму памяти, а по обилию и функциям периферийных устройств.
Микроконтроллеры уже достаточно давно развиваются параллельно с микропроцессорами — так, до сих пор встречающаяся в промышленных изделиях архитектура Intel 8051 была разработана в 1980 году. В каких-то моментах линии их развития начинают пересекаться с микропроцессорами — так, старшие модели микроконтроллеров имеют интерфейсы для внешнего ОЗУ, а производители микропроцессоров интегрируют на кристалл всё больше периферийных устройств (достаточно вспомнить, что на заре «персоналок» даже кэш-память набиралась внешними микросхемами) — но в любом случае они остаются двумя существенно отличающимися ветвями развития.
Собственно, целью создания микроконтроллеров была возможность удешевления и миниатюризации различных устройств, требующих некоторой небольшой вычислительной мощности: использование одного чипа, на который для его работы достаточно просто подать питание, существенно упрощает разработку и производство печатной платы по сравнению с набором из 4-5 отдельных чипов.
Разумеется, у микроконтроллера есть свои ограничения — технически невозможно упаковать в один кристалл то, что в большом ПК занимает половину немаленькой платы.
- Рабочие частоты редко превышают 200 МГц, а чаще находятся в районе десятков мегагерц.
- Объём оперативной памяти — в пределах мегабайта, а чаще — в районе десятков килобайт.
- Объём памяти программ — в пределах мегабайта, а чаще — в районе десятков-сотен килобайт.
Память
В общем случае внутри микроконтроллера может быть четыре вида памяти:
- Постоянная память (флэш-память) используется для хранения пользовательских программ и, иногда, некоторых настроек самого микроконтроллера. Если при указании на характеристики микроконтроллера пишут объём памяти, не указывая, какой именно — как правило, это про флэш. Содержимое флэша не сбрасывается при пропадании питания, срок хранения информации в нём в нормальных условиях обычно не менее 10 лет.
- Оперативная память используется для выполнения пользовательской программы и хранения «сиюминутных» данных. ОЗУ всегда сбрасывается при перезагрузке или выключении питания, а также может не сохраняться при входе в некоторые режимы сна. В микроконтроллерах часто нет чёткого разделения на память программ и память данных — в результате можно встретить термин «выполнение из ОЗУ», означающий, что в ОЗУ находятся не только данные, но и сама программа; впрочем, это достаточно экзотические случаи.
- EEPROM. Тоже относится к постоянной памяти, но существенно отличается от флэш-памяти своими характеристиками. У флэша есть два больших недостатка, делающие его очень неудобным для сохранения из программы каких-то текущих данных — во-первых, у флэша ограниченное число перезаписей одной и той же ячейки, во-вторых, с флэшом часто можно работать только целыми страницами, которые имеют размер в сотни байт, даже если вам надо перезаписать всего один байт. EEPROM этих недостатков лишён — срок его службы обычно вдесятеро больше (от 100 тыс. до 1 млн. перезаписей), и работать в нём можно с каждым байтом по отдельности. По этой причине EEPROM используют для постоянного хранения данных, генерируемых самой программой (архивы измерений, настройки программы и т.п.), его типовой объём составляет единицы килобайт, но есть он не во всех контроллерах.
- Системная память. Области постоянной памяти, недоступные пользователю для записи, а записывающиеся при производстве микроконтроллера. Обычно в них находится исполняемый код загрузчика (о нём ниже), но могут также храниться какие-либо калибровочные константы, серийные номера или даже вспомогательные библиотеки для работы с периферийными устройствами
Нетрудно заметить, что все четыре вида памяти, о которых мы говорили, занимают очень небольшой кусочек карты — а на большей части картинки расположился перечень всех имеющихся в контроллере периферийных устройств.
Регистры
Дело в том, что всё — вообще всё — общение со всеми периферийными устройствами микроконтроллера и всеми его настройками осуществляется с помощью всего двух операций:
- прочитать значение по заданному адресу
- записать значение по заданному адресу
Так, например, если мы хотим, чтобы на третьей ножке порта А микроконтроллера (PA2, нумерация идёт с нуля) появилась «1», нам надо записать «1» в третий бит регистра, расположенного по адресу 0x4002014. А если эта ножка настроена как вход и мы, наоборот, хотим узнать, какое на ней значение — нам надо прочитать третий бит регистра по адресу 0x40020010.
Да, чтобы указать контроллеру, входом или выходом является эта ножка — надо записать соответствующие значения в соответствующие биты по адресу 0x40020000.
Это — важный момент в понимании работы микроконтроллера: абсолютно всё, что не является вычислительными операциями, за которые отвечает само ядро процессора, осуществляется с помощью записи или чтения того или иного регистра. Какие бы библиотеки не были наворочены в вашей программе сверху — в конечном итоге всё сводится к регистрам.
Разумеется, работать с числовыми адресами довольно неудобно, поэтому для каждого микроконтроллера на ядре Cortex-M существует библиотека CMSIS (Cortex Microcontroller Software Interface Standard), самый важный компонент которой для нас — заголовочный файл, описывающий имеющиеся в конкретном контроллере регистры и дающий им относительно человекочитаемые имена.
С использованием CMSIS описанные выше операции с ножкой PA будут выглядеть так:
int pin_num = 2; /* PA2*/ GPIOA->MODER &= ~(0b11 MODER |= 0b01 ODR |= 1 MODER &= ~(0b11 IDR & (1
Все названия регистров и значения полей в них описаны в документе, который можно считать Библией программиста микроконтроллеров — Reference Manual (он, разумеется, свой для каждого семейства контроллеров, ссылка дана на RM0038, соответствующий семейству STM32L1). Отмечу, что более чем 900 страниц RM0038 — это не очень большой объём информации, легко можно встретить контроллеры с руководствами по 1500-2000 страниц. Вряд ли есть кто-то, помнящий хотя бы треть такого руководства наизусть, но умение быстро в нём ориентироваться — обязательное качество для хорошего программиста.
Разумеется, этот код — лишь условно человекопонятный. Использование буквенных названий вместо адресов радикально снижает процент ошибок в коде и увеличивает его читаемость, но всё ещё крайне далеко от того, что большинство людей назовёт «нормальным» кодом.
Понимая это, производители контроллеров начали выпускать вспомогательные библиотеки, собирающие наборы обращений к регистрам в функции — например, если при работе с регистрами напрямую для включения какого-либо такового генератора вам надо сделать два действия (поставить в 1 бит, включающий генератор, и подождать, пока в 1 встанет флаг, индицирующий, что генератор вышел на режим), то в функции включения генератора в такой библиотеке они будут объединены.
В случае с STM32 основная библиотека называется Standard Peripherals Library, она же StdPeriphLib, она же SPL. Помимо неё, существует выпускаемая ST библиотека LL, и ряд сторонних библиотек — например, LibOpenCM3. Сторонние библиотеки часто поддерживают и контроллеры других производителей, но в силу распространённости STM32 они обычно оказываются на первом месте.
Так, при использовании SPL обращения к регистрам, которые мы совершали, чтобы зажечь светодиод, превратятся в обращения к функциям GPIO_Init и GPIO_Write.
Впрочем, нельзя не заметить, что среди профессиональных разработчиков отношение к SPL — двойственное.
С одной стороны, SPL позволяет значительно быстрее набросать «скелет» проекта, особенно при использовании графических средств конфигурирования контроллера, таких как STM32 CubeMX. При этом код будет довольно хорошо (настолько, насколько у них совпадает набор периферийных устройств и возможностей, которыми вы пользуетесь) переноситься между разными контроллерами семейства STM32.
С другой стороны, как показывает практика, в сложном проекте нет вопроса «что делать, если что-то будет работать не так» — в нём есть вопрос «что делать, когда всё будет работать не так». В SPL, как и в любой библиотеке, могут быть ошибки, кроме того, логика разработчиков SPL может не совпадать с вашим представлением о том, что должно происходить с контроллером при тех или иных действиях — в результате при попадании в такую ситуацию вам всё равно придётся открывать исходники SPL и смотреть, что конкретно там происходит на уровне регистров. На практике это может иногда занять времени не меньше, чем написание нужной вам функциональности с нуля.
Кроме того, библиотеки, выпущенные конкретным производителем чипов, хоть и позволяют в каких-то пределах мигрировать между чипами этого производителя, но перескочить, например, с STM32L1 на ATSAMD21 с кодом, написанным для SPL, у вас не получится при всём желании.
Не всегда помогает SPL и читаемости кода — в программах, написанных с её использованием, нередко можно увидеть простыни размером в полстраницы, состоящие из одних только вызовов SPL.
Наконец, SPL решает лишь одну проблему — абстрагирования от «железа» и работы с регистрами. Однако по мере развития проекта вы столкнётесь ещё с несколькими, например:
- Виртуализация периферийных устройств. Например, в вашем контроллере есть всего один таймер часов реального времени (RTC), на который можно установить два независимых события — и в то же время в серьёзной программе запросто может оказаться пять-шесть функций, который используют таймер регулярно (для периодического выполнения заданий) или разово (например, для отсчёта задержки), причём другие таймеры им не подходят. В этой ситуации вам потребуется функция-менеджер, которая будет организовывать одновременную работу всех этих процедур с единственным имеющимся таймером.
- Многозадачность. Любая достаточно сложная система быстро обрастает большим количеством всевозможных процедур, которым надо срабатывать с различной периодичностью или по различным событиями. Знакомый многим по Arduino цикл loop() уже на полудесятке утрамбованных в него разношёрстных процедур начинает выглядеть уродливым монстром, а попытка организовать в его рамках ещё и приоритизацию задач вселяет ужас в сердца людей. В этот момент вы захотите вынести все задачи из loop() в независимые функции, оставив в цикле только планировщик, который будет к указанным задачам обращаться. Это будет первыми зачатками многозадачности (о полной её реализации, типах планировщиков и общении между разными задачами мы поговорим на следующей лекции).
- Разделение труда. Как только разработка системы выйдет на уровень, на котором её ведут несколько человек, перед вами встанет задача разделения обязанностей — помимо оптимизации разработки, имеющая ещё и чисто практический смысл: в мире довольно мало программистов-универсалов, которые могут с одинаковой эффективностью отлаживать и работу с процессором, и сетевой стек, и пользовательский интерфейс. Со значительно большей вероятностью каждый из членов вашей команды будет лучше других разбираться в какой-то одной области — поэтому вам быстро захочется разделить эти области на уровне кода, чтобы, например, специалист по пользовательскому интерфейсу не был вынужден через строчку сталкиваться с обращением к регистрам контроллера, и наоборот. Это приведёт к разбиению вашего кода на отдельные модули, общающиеся друг с другом через стандартизированные API.
Несмотря на то, что ОС требует для своего существования дефицитных ресурсов контроллера (обычно 5-20 КБ постоянной памяти и ещё столько же оперативной), преимущества использования ОС настолько велики, что на данный момент в профессиональной разработке для встраиваемых систем около 70 % проектов используют ту или иную ОС.

Строго говоря, на нижнем уровне ОС может использовать вендорские библиотеки, подобные SPL. Однако в рамках нашего курса мы будем работать с RIOT OS, нижнеуровневый код которой для семейства STM32 написан на регистрах — работу с SPL же мы затрагивать не будем вообще.
Причина этого проста: хотя в целом мы будем изучать работу ОС и контроллера на верхнем уровне, но в тех случаях, когда мы захотим углубиться в детали их функционирования, нам всё равно придётся спускаться на уровень регистров, и прослойка в виде SPL довольно сильно бы этому мешала. Освоив же общие принципы работы с контроллерами, при желании с функционированием SPL вы сможете разобраться самостоятельно, тем более, что абсолютное большинство учебников по STM32, доступных онлайн, построены на её базе.
Операционная система
В виде максимально упрощённой схемы ОС можно представить как набор компонентов, выстроенных в определённую иерархию:

- нижний уровень — код, непосредственно работающий с микроконтроллером;
- средний уровень — компоненты, входящие в саму ОС, но уже не зависящие от конкретного контроллера: драйверы различных внешних устройств, планировщик задач, различные вспомогательные службы;
- верхний уровень — собственно пользовательское приложение.
Точнее, если вспоминать фразу Артура Кларка про технологии, неотличимые от магии, то это мышлении можно скорее назвать псевдомагическим, переформулировав афоризм как «любая технология, достаточно хорошо спрятанная от пользователя, становится неотличима от магии».
На самом деле, разумеется, никакой особенной функции меню в Arduino IDE, как и в любой другой IDE, не несёт — это лишь графическая оболочка для доступа к некоторым совершенно стандартным функциям и особенностям современных программных систем.

Если мы посмотрим на то, как выглядит RIOT OS в виде набора файлов на диске, то без труда опознаем разложенные по папочкам компоненты системы: HAL лежит в папке cpu (и если мы её откроем, то увидим описания для десятков различных микроконтроллеров, от AVR до PIC32), описания построенных на этих контроллерах плат — boards, драйверы внешних устройств — drivers, ядро ОС — core, системные и вспомогательные сервисы ОС — sys, пользовательские приложения — examples.
Один из важных моментов, который отличает микроконтроллеры от больших систем — то, что практически всегда (а в нашем случае совсем всегда) пользовательские приложения существуют не как отдельные файлы, загружаемые независимо от ОС, а компилируются вместе с ОС, всем набором драйверов и модулей в единый файл, загружаемый в память микроконтроллера. Причин этому несколько — начиная с отсутствия необходимости в отдельной загрузке приложений, что позволяет упростить всю систему, и заканчивая наличием необходимости собирать ОС и комплект драйверов и модулей под конкретное устройство ради экономии его отнюдь не бесконечной памяти.
Исходные коды ОС
Мы будем работать с исходными кодами в версии https://github.com/unwireddevices/RIOT/tree/mirea — это ответвление от основной разработки RIOT OS, в котором силами Unwired Devices улучшена поддержка микроконтроллеров STM32L1, а также добавлены некоторые полезные сервисы, например, таймеры на базе часов реального времени, включая миллисекундный таймер.
Исходные коды можно загрузить с Github, выбрав кнопку «Clone/Download» и «Download ZIP», но лучшим вариантом будет создание собственного репозитория. Для этого зарегистрируйтесь на GitHub, после чего вернитесь в указанный выше репозиторий и нажмите кнопку «Fork» — исходные коды будут скопированы в ваш аккаунт, откуда вы сможете уже без проблем работать с ними.
Я не буду описывать здесь детали работы с GitHub и Git — в интернете есть масса отличных пошаговых руководств, повторять которые нет смысла.
Компиляция простейшего приложения
Благодаря тому, что ОС берёт на себя всё взаимодействие с микроконтроллером, простейшее возможное приложение в общем-то ничуть не сложнее, чем традиционный «Hello world» на большом ПК:
#include int main(void)
В структуре нашей ОС это приложение располагается в папке example/hello-world в файле main.c (оно там уже есть).
Однако, очевидно, чтобы его скомпилировать, необходимо сначала настроить среду сборки. Это делается по-разному в разных ОС.
1. Windows 8 и старее. К сожалению, придётся использовать среду MinGW, медленную и неудобную. Процедура установки нужных компонентов достаточно подробно описана здесь. Отмечу, что для работы с Git/GitHub придётся также отдельно поставить Git for Windows, который притащит свою урезанную версию MinGW. При желании всё это можно свести в один терминал MinGW, но проблем с очень низкой скоростью работы MinGW и общим его неудобством это не решит.
В целом, Windows 7 и Windows 8, как можно понять, являются не самым удачным выбором для разработки.
2. Windows 10. Откройте магазин Microsoft Store, найдите в нём Ubuntu и установите. Если при первом запуске Ubuntu будет ругаться на выключенный компонент Windows, откройте системное приложение «Включение или отключение компонентов Windows», найдите там «Поддержка Windows для Linux» и включите.
Вся дальнейшая работа происходит в среде Ubuntu, значительно более комфортной и быстрой, нежели MinGW.
Скачайте компилятор и сопровождающие его утилиты отсюда (внимание: вам нужна 64-битная версия для Linux!), откройте Ubuntu, распакуйте архив и укажите системе пути к нему:
cd /opt sudo tar xf /mnt/c/Users/vasya/Downloads/gcc-arm-none-eabi-7-2017-q4-major-linux.tar.bz2 export PATH=/opt/gcc-arm-none-eabi-7-2017-q4-major/bin/:$PATH export PATH=/opt/gcc-arm-none-eabi-7-2017-q4-major/arm-none-eabi/bin/:$PATH echo "export PATH=/opt/gcc-arm-none-eabi-7-2017-q4-major/bin/:\$PATH" >> ~/.bashrc echo "export PATH=/opt/gcc-arm-none-eabi-7-2017-q4-major/arm-none-eabi/bin/:\$PATH" >> ~/.bashrc
Последние две команды пропишут установку путей к компилятору и утилитам в файл .bashrc, так что вам не придётся вручную их устанавливать при каждом запуске Ubuntu. Обратите внимание на обратную дробь перед $ и двойную стрелку >> в конце — без первого путь будет записан некорректно, без второго (с одиночной стрелкой) вы сотрёте всё предыдущее содержимое .bashrc.
После этого выполнение в консоли команды arm-none-eabi-gcc --version должно сообщать, что такой компилятор есть, а его версия — 7.2.1 (на текущий момент).
Для работы с Git вам потребуется сгенерировать пару из приватного и публичного ключей командой ssh-keygen, после чего скопировать публичный ключ (в Ubuntu под Windows это можно сделать командой cat ~/.ssh/id_rsa.pub, потом выделить выведенное мышкой и нажать Enter — оно скопируется в буфер обмена) и добавить его в ваш аккаунт GitHub. После этого можно будет работать с GitHub из командной строки командой git.
NB: если вы ранее не работали в командной строке Linux, то обратите внимание на два полезных момента: курсорные стрелки вверх и вниз позволяют листать историю введённых команд, а клавиша Tab дополняет набранный вами путь к файлу или папке до конца (то есть cd /opt/gcc- превратится в строку cd /opt/gcc-arm-none-eabi-7-2017-q4-major). Последнее служит также хорошей проверкой, правильно ли вы набираете путь — если неправильно, дополнен он по очевидной причине не будет. Если возможных вариантов дополнения несколько, то двойное нажатие Tab выведет их все.
NB: в Windows вам будет удобнее работать, если сами исходные коды ОС размещаются в папке, напрямую доступной из Windows, например Documents/git/RIOT. Из-под MinGW она будет доступна по пути /c/Users/vasya/Documents/git/RIOT, из-под Ubuntu — /mnt/c/Users/vasya/Documents/git/RIOT. В этом случае вы сможете свободно пользоваться для работы с кодом, например, текстовыми редакторами, написанными для Windows, такими как Notepad++.
3. Linux. Установка среды сборки ничем не отличается от инструкции для Windows 10, кроме того, что Microsoft Store вам не потребуется. Также не ищите gcc-arm-none-eabi в репозитории вашего дистрибутива — скачайте наиболее свежую версию с его официального сайта.
После установки среды сборки откройте консоль, перейдите в папку с RIOT и в подпапку examples/hello-world, после чего запустите команду make.
Скорее всего, она быстро прервётся ошибкой и сообщением, что у вас не хватает unzip (под Windows 10 по умолчанию он не устанавливается), make или других утилит. В Windows 10 их можно установить командой (перечень недостающего даётся простым списком через пробел):
sudo apt-get install unzip make
После установки попробуйте ещё раз запустить make — точнее, оптимальным вариантом является вызов команды «make clean && make»: первая очищает мусор, оставшийся от предыдущей попытки. Без ней компилятор может ошибочно решить, что какой-то из уже собранных модулей не менялся, и не пересобирать его — в результате вы получите прошивку, собранную из кусков старого и нового кода.
NB: в оригинальном RIOT приложение hello-world собирается для архитектуры native, то есть, в нашем случае ноутбука или десктопа, x86. Однако в нашем коде в параметрах сборки проекта уже указана плата unwd-range-l1-r3, использующая контроллер stm32, поэтому в начале процедуры вы должны увидеть строку
Building application "mirea" for "unwd-range-l1-r3" with MCU "stm32l1".
В случае успеха за ней вы увидите десяток-два строчек, начинающихся с команды make — это сборка отдельных компонентов операционной системы. Закончится всё сообщением об успешном создании файла прошивки mirea.elf с указанием размеров различных типов данных (объёмов используемой флэш-памяти и ОЗУ).

Итак, мы немного разобрались с тем, как выглядит микроконтроллер, скачали исходные коды нашей ОС, настроили среду сборки и убедились, что она работает.
На следующем занятии мы рассмотрим подробнее устройство микроконтроллера, начиная с портов GPIO, и загрузим в него первое приложение — оно, по старой традиции, будет мигать светодиодом — а далее вернёмся к операционной системе и внимательнее изучим, из каких компонентов она состоит и как настраивается её сборка.
P.S. И в качестве дополнения — живая 360-градусная запись лекции (запись семинаров не проводилась по понятной причине: «у доски» там говорится мало, значительная часть работы идёт уже с отдельными студентами, у которых что-то получается или не получается):