Files
obsidian-vault-/Wikipedia/MCU.md
T
2026-03-18 20:37:34 +03:00

15 KiB
Raw Blame History

tags
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 компилятор может решить, что флаг никогда не меняется в основном потоке, и зациклить программу .

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 МГц.

  • Ожидаемый ответ (ход мыслей):

    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: На время, пока мьютекс захвачен низкоприоритетной задачей, ее приоритет временно повышается до уровня ожидающей высокоприоритетной задачи (наследование приоритета) .

Вопрос 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) или деление на ноль .

    3. Использование отладчика: Можно посмотреть дизассемблированный код по адресу из Program Counter.

    4. Вероятные причины:

      • Выход за границы массива (испортили стек).

      • Размер стека задачи в RTOS слишком мал.

      • Использование указателя без проверки на NULL.

      • Обращение к уже освобожденной памяти.