--- tags: - mcu - stm32 - HAL --- --- ### 1. Блок: Архитектура, ядро и память (Cortex-M) _Цель: оценить понимание работы процессорного ядра, памяти и загрузки._ **Вопрос 1:** Опишите полный путь программы от нажатия кнопки "питание" до выполнения первой инструкции в `main()`. Что происходит в Startup-файле? - **Ожидаемый ответ:** 1. После подачи питания и сброса, процессор забирает первое слово (32 бита) из таблицы векторов (начало памяти, обычно 0x08000000) — это начальное значение **основного указателя стека (MSP)**. 2. Процессор забирает второе слово из таблицы векторов — это адрес **обработчика сброса (Reset_Handler)**. 3. Выполняется код `Reset_Handler` (обычно написан на ассемблере или Си в startup-файле). Его задачи: - Копирование инициализированных данных из Flash в RAM (секция `.data`). - Обнуление секции неинициализированных переменных (секция `.bss`). 4. Вызывается функция `SystemInit()` (настройка базовой частоты, PLL). 5. Вызывается функция `main()`. **Вопрос 2:** В чем разница между стеком (Stack) и кучей (Heap)? Где хранятся локальные и глобальные переменные? - **Ожидаемый ответ:** - **Stack:** Растет вниз. Используется для хранения локальных переменных, адресов возврата функций, сохранения контекста при прерываниях. Память выделяется автоматически и очень быстро. - **Heap:** Используется для динамического выделения памяти (`malloc`/`new`). В bare-metal (голом металле) и критичных к времени системах на МК использование кучи часто ограничивают или запрещают из-за фрагментации и недетерминизма. - **Глобальные переменные:** Хранятся в RAM (секции `.data` и `.bss`). - **Локальные:** На стеке. - **Константы (`const`):** В Flash (секция `.rodata`). **Вопрос 3:** Для чего в программировании регистров используется ключевое слово **`volatile`**? Приведите два примера из практики STM32. - **Ожидаемый ответ:** Ключевое слово запрещает компилятору оптимизировать доступ к переменной, заставляя каждый раз читать её из памяти, а не использовать кэшированное значение в регистре CPU. 1. **Регистры периферии:** Например, флаг `RXNE` в регистре `USART_SR`. Если компилятор увидит цикл `while (!(USART_SR & RXNE_FLAG));`, без `volatile` он может оптимизировать чтение, вынеся его за пределы цикла. 2. **Переменные, разделяемые с прерываниями:** Глобальный флаг, который устанавливается в прерывании и проверяется в основном цикле `while(flag == 0) {}`. Без `volatile` компилятор может решить, что флаг никогда не меняется в основном потоке, и зациклить программу [](https://www.nowcoder.com/discuss/793216015415934976?sourceSSR=post)[](https://www.pvsm.ru/sobesedovanie/445258#begin). ### 2. Блок: Библиотеки (HAL, LL, SPL) и периферия _Цель: проверить практический опыт написания кода и конфигурации._ **Вопрос 4:** Чем отличается HAL от LL? В каком случае вы выберете LL, а в каком HAL? - **Ожидаемый ответ:** - **HAL (Hardware Abstraction Layer):** Высокоуровневые, переносимые функции с таймаутами. Удобен для быстрой разработки, сложных стеков (USB, ETH), когда скорость выполнения не критична. "Толстый" и может быть медленным. - **LL (Low-Layer):** Легковесные, inline-функции, работающие почти напрямую с регистрами. Нужен для high-speed периферии (SPI на максимальной частоте, тактирование таймеров) или для проектов с жесткими ограничениями по памяти/производительности. Требует глубокого понимания работы регистров. - _Доп. вопрос:_ Можно ли их миксовать? (Ответ: Можно для разных периферийных устройств, но для одного и того же (например, UART) лучше не стоит из-за конфликтов управления состояниями (handles) [](https://www.programmersought.com/article/79177505633/). **Вопрос 5:** Настройте таймер (например, TIM2) на генерацию прерывания с частотой 1 кГц при частоте шины APB1 = 80 МГц. - **Ожидаемый ответ (ход мыслей):** 1. Включить тактирование таймера (`__HAL_RCC_TIM2_CLK_ENABLE()`). 2. Настроить предделитель (**Prescaler**) и период (**AutoReload**). 3. Расчет: Частота таймера = 80 МГц / (Prescaler + 1). Желаемая частота срабатывания = Частота таймера / (AutoReload + 1) = 1 кГц. 4. Выбираем компромисс, например: `Prescaler = 8000 - 1` => частота таймера = 10 кГц. Тогда `AutoReload = 10 - 1` => получим 1 кГц. 5. Включить прерывание по обновлению (Update Interrupt) в NVIC. 6. Запустить таймер. **Вопрос 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 в круговом режиме: 1. Выделяем буфер в памяти (например, `uint8_t uart_rx_buffer[256]`). 2. Настраиваем DMA на запись данных из регистра `UART->DR` в этот буфер. 3. Включаем прерывание от DMA по достижению половины буфера (Half-Transfer) и по завершению (Transfer Complete). 4. В обработчике прерывания мы понимаем, какая половина буфера сейчас заполнена, и можем обрабатывать данные, пока DMA продолжает писать во вторую половину. Это классический механизм double-buffer (ping-pong). **Вопрос 8 (RTOS):** В чем разница между семафором (Semaphore) и мьютексом (Mutex)? Что такое **инверсия приоритета (Priority Inversion)** и как FreeRTOS с ней борется? - **Ожидаемый ответ:** - **Семафор:** Счетчик. Используется для уведомления о событиях или подсчета ресурсов (может быть дан несколько раз). - **Мьютекс:** Двоичный семафор, но с приоритетом. Обладает механизмом **наследования приоритета** (Priority Inheritance). Используется для защиты общего ресурса от одновременного доступа. - **Инверсия приоритета:** Ситуация, когда высокоприоритетная задача ждет ресурса, захваченного низкоприоритетной, а средняя задача (не работающая с ресурсом) вытесняет низкую и выполняется первой, косвенно "опуская" приоритет высокой задачи ниже средней. - **Решение в FreeRTOS:** На время, пока мьютекс захвачен низкоприоритетной задачей, ее приоритет временно повышается до уровня ожидающей высокоприоритетной задачи (наследование приоритета) [](https://www.nowcoder.com/discuss/793216015415934976?sourceSSR=post)[](https://www.pvsm.ru/sobesedovanie/445258#begin). **Вопрос 9:** Проектируем устройство сбора данных с 8 датчиков по SPI (макс. скорость 10 МГц). Данные должны обрабатываться и отправляться по USB. Предложите архитектуру задачи/прерывания/DMA. - **Ожидаемый ответ:** 1. Так как скорость высокая, используем **SPI + DMA** для опроса датчиков (по таймеру). DMA автоматически перекидывает данные в память. 2. По завершению DMA (Transfer Complete) генерируется прерывание, которое отправляет семафор задаче `DataProcessor`. 3. Задача `DataProcessor` (средний приоритет) просыпается, обрабатывает массив данных (усреднение, фильтрация, подсчет CRC). 4. Обработанные данные кладутся в **очередь (Queue)** для задачи `USB_Sender`. 5. Задача `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`). Ваши действия по отладке? - **Ожидаемый ответ:** 1. **Анализ стека:** Смотрю на значения регистров в момент остановки (особенно Program Counter, Link Register, Stack Pointer). 2. **Чтение регистров фault'а:** В STM32 есть специальные регистры (CFSR, HFSR, BFAR), которые точно указывают причину: обращение к памяти по неверному адресу (MemManage), выполнение недопустимой инструкции (UsageFault) или деление на ноль [](https://www.pvsm.ru/sobesedovanie/445258#begin). 3. **Использование отладчика:** Можно посмотреть дизассемблированный код по адресу из Program Counter. 4. **Вероятные причины:** - Выход за границы массива (испортили стек). - Размер стека задачи в RTOS слишком мал. - Использование указателя без проверки на NULL. - Обращение к уже освобожденной памяти.