15 KiB
tags
| tags | |||
|---|---|---|---|
|
1. Блок: Архитектура, ядро и память (Cortex-M)
Цель: оценить понимание работы процессорного ядра, памяти и загрузки.
Вопрос 1:
Опишите полный путь программы от нажатия кнопки "питание" до выполнения первой инструкции в main(). Что происходит в Startup-файле?
-
Ожидаемый ответ:
- После подачи питания и сброса, процессор забирает первое слово (32 бита) из таблицы векторов (начало памяти, обычно 0x08000000) — это начальное значение основного указателя стека (MSP).
- Процессор забирает второе слово из таблицы векторов — это адрес обработчика сброса (Reset_Handler).
- Выполняется код
Reset_Handler(обычно написан на ассемблере или Си в startup-файле). Его задачи:- Копирование инициализированных данных из Flash в RAM (секция
.data). - Обнуление секции неинициализированных переменных (секция
.bss).
- Копирование инициализированных данных из Flash в RAM (секция
- Вызывается функция
SystemInit()(настройка базовой частоты, PLL). - Вызывается функция
main().
Вопрос 2:
В чем разница между стеком (Stack) и кучей (Heap)? Где хранятся локальные и глобальные переменные?
-
Ожидаемый ответ:
-
Stack: Растет вниз. Используется для хранения локальных переменных, адресов возврата функций, сохранения контекста при прерываниях. Память выделяется автоматически и очень быстро.
-
Heap: Используется для динамического выделения памяти (
malloc/new). В bare-metal (голом металле) и критичных к времени системах на МК использование кучи часто ограничивают или запрещают из-за фрагментации и недетерминизма. -
Глобальные переменные: Хранятся в RAM (секции
.dataи.bss). -
Локальные: На стеке.
-
Константы (
const): В Flash (секция.rodata).
-
Вопрос 3:
Для чего в программировании регистров используется ключевое слово volatile? Приведите два примера из практики STM32.
-
Ожидаемый ответ:
Ключевое слово запрещает компилятору оптимизировать доступ к переменной, заставляя каждый раз читать её из памяти, а не использовать кэшированное значение в регистре CPU.-
Регистры периферии: Например, флаг
RXNEв регистреUSART_SR. Если компилятор увидит циклwhile (!(USART_SR & RXNE_FLAG));, безvolatileон может оптимизировать чтение, вынеся его за пределы цикла. -
Переменные, разделяемые с прерываниями: Глобальный флаг, который устанавливается в прерывании и проверяется в основном цикле
while(flag == 0) {}. Безvolatileкомпилятор может решить, что флаг никогда не меняется в основном потоке, и зациклить программу .
-
2. Блок: Библиотеки (HAL, LL, SPL) и периферия
Цель: проверить практический опыт написания кода и конфигурации.
Вопрос 4:
Чем отличается HAL от LL? В каком случае вы выберете LL, а в каком HAL?
-
Ожидаемый ответ:
-
HAL (Hardware Abstraction Layer): Высокоуровневые, переносимые функции с таймаутами. Удобен для быстрой разработки, сложных стеков (USB, ETH), когда скорость выполнения не критична. "Толстый" и может быть медленным.
-
LL (Low-Layer): Легковесные, inline-функции, работающие почти напрямую с регистрами. Нужен для high-speed периферии (SPI на максимальной частоте, тактирование таймеров) или для проектов с жесткими ограничениями по памяти/производительности. Требует глубокого понимания работы регистров.
-
Доп. вопрос: Можно ли их миксовать? (Ответ: Можно для разных периферийных устройств, но для одного и того же (например, UART) лучше не стоит из-за конфликтов управления состояниями (handles) .
-
Вопрос 5:
Настройте таймер (например, TIM2) на генерацию прерывания с частотой 1 кГц при частоте шины APB1 = 80 МГц.
-
Ожидаемый ответ (ход мыслей):
-
Включить тактирование таймера (
__HAL_RCC_TIM2_CLK_ENABLE()). -
Настроить предделитель (Prescaler) и период (AutoReload).
-
Расчет: Частота таймера = 80 МГц / (Prescaler + 1). Желаемая частота срабатывания = Частота таймера / (AutoReload + 1) = 1 кГц.
-
Выбираем компромисс, например:
Prescaler = 8000 - 1=> частота таймера = 10 кГц. ТогдаAutoReload = 10 - 1=> получим 1 кГц. -
Включить прерывание по обновлению (Update Interrupt) в NVIC.
-
Запустить таймер.
-
Вопрос 6:
В чем «подвох» работы с I2C в STM32 через HAL? Почему иногда "вешает" систему?
-
Ожидаемый ответ:
-
Функции типа
HAL_I2C_Master_Transmit()по умолчанию работают с бесконечным таймаутом (HAL_MAX_DELAY) или большим. Если устройство на шине "зависло" и не отвечает ACK, программа заблокируется в этом цикле. -
Необходимо либо использовать режимы с таймаутом (указать конечное значение в мс), либо переходить на прерывания (Interrupt) или DMA, чтобы освободить CPU.
-
Также нужно не забывать про программный сброс шины, если она зависла.
-
3. Блок: Продвинутая периферия (DMA, Interrupts, RTOS)
Цель: оценить способность писать эффективный многозадачный код.
Вопрос 7:
Объясните механизм работы DMA (Direct Memory Access). Как вы организуете прием потока данных по UART (RS-485) с помощью DMA в режиме "кругового буфера" (Circular mode)?
-
Ожидаемый ответ:
-
DMA — это интеллектуальный "пересыльщик" данных, который работает независимо от ядра CPU.
-
Настройка UART DMA Rx в круговом режиме:
-
Выделяем буфер в памяти (например,
uint8_t uart_rx_buffer[256]). -
Настраиваем DMA на запись данных из регистра
UART->DRв этот буфер. -
Включаем прерывание от DMA по достижению половины буфера (Half-Transfer) и по завершению (Transfer Complete).
-
В обработчике прерывания мы понимаем, какая половина буфера сейчас заполнена, и можем обрабатывать данные, пока DMA продолжает писать во вторую половину. Это классический механизм double-buffer (ping-pong).
-
-
Вопрос 8 (RTOS):
В чем разница между семафором (Semaphore) и мьютексом (Mutex)? Что такое инверсия приоритета (Priority Inversion) и как FreeRTOS с ней борется?
-
Ожидаемый ответ:
-
Семафор: Счетчик. Используется для уведомления о событиях или подсчета ресурсов (может быть дан несколько раз).
-
Мьютекс: Двоичный семафор, но с приоритетом. Обладает механизмом наследования приоритета (Priority Inheritance). Используется для защиты общего ресурса от одновременного доступа.
-
Инверсия приоритета: Ситуация, когда высокоприоритетная задача ждет ресурса, захваченного низкоприоритетной, а средняя задача (не работающая с ресурсом) вытесняет низкую и выполняется первой, косвенно "опуская" приоритет высокой задачи ниже средней.
-
Решение в FreeRTOS: На время, пока мьютекс захвачен низкоприоритетной задачей, ее приоритет временно повышается до уровня ожидающей высокоприоритетной задачи (наследование приоритета) .
-
Вопрос 9:
Проектируем устройство сбора данных с 8 датчиков по SPI (макс. скорость 10 МГц). Данные должны обрабатываться и отправляться по USB. Предложите архитектуру задачи/прерывания/DMA.
-
Ожидаемый ответ:
-
Так как скорость высокая, используем SPI + DMA для опроса датчиков (по таймеру). DMA автоматически перекидывает данные в память.
-
По завершению DMA (Transfer Complete) генерируется прерывание, которое отправляет семафор задаче
DataProcessor. -
Задача
DataProcessor(средний приоритет) просыпается, обрабатывает массив данных (усреднение, фильтрация, подсчет CRC). -
Обработанные данные кладутся в очередь (Queue) для задачи
USB_Sender. -
Задача
USB_Sender(высокий приоритет) забирает из очереди и отправляет через USB-стек (который внутри себя тоже использует DMA).
-
4. Блок: Низкий уровень (CMSIS, Регистры) и отладка
Цель: отделить инженеров, которые понимают "как это работает под капотом" от тех, кто просто копирует код из CubeMX.
Вопрос 10:
Покажите (или опишите), как на прямую через регистры (CMSIS) настроить ножку PA5 на выход и установить на ней высокий уровень. (Без HAL).
-
Ожидаемый ответ:
c
// Включаем тактирование порта A (RCC) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Настраиваем PA5 как общий выход (Push-pull) GPIOA->MODER &= ~GPIO_MODER_MODER5_Msk; // Очищаем биты GPIOA->MODER |= GPIO_MODER_MODER5_0; // 01 = General purpose output mode // Устанавливаем высокий уровень на выходе (Output Data Register) GPIOA->ODR |= GPIO_ODR_OD5;
Вопрос 11:
Программа на МК периодически "вылетает" (уходит в HardFault_Handler). Ваши действия по отладке?
-
Ожидаемый ответ:
-
Анализ стека: Смотрю на значения регистров в момент остановки (особенно Program Counter, Link Register, Stack Pointer).
-
Чтение регистров фault'а: В STM32 есть специальные регистры (CFSR, HFSR, BFAR), которые точно указывают причину: обращение к памяти по неверному адресу (MemManage), выполнение недопустимой инструкции (UsageFault) или деление на ноль .
-
Использование отладчика: Можно посмотреть дизассемблированный код по адресу из Program Counter.
-
Вероятные причины:
-
Выход за границы массива (испортили стек).
-
Размер стека задачи в RTOS слишком мал.
-
Использование указателя без проверки на NULL.
-
Обращение к уже освобожденной памяти.
-
-