Введение
Микроконтроллеры (МК) — «мозги» встраиваемых систем: от бытовой техники до промышленного оборудования. Их эффективность зависит от умения программиста:
- управлять событиями в реальном времени (через прерывания);
- оптимизировать обмен данными (с помощью DMA);
- работать с периферией на низком уровне.
В статье разберём:
- архитектуру прерываний в МК;
- синтаксис и семантику обработчиков прерываний на C;
- принципы работы DMA;
- примеры кода для популярных платформ (STM32, AVR).
1. Прерывания: концепция и назначение
1.1. Что такое прерывание
Прерывание — сигнал, заставляющий процессор:
- Приостановить выполнение основного кода (фоновой задачи).
- Переключиться на обработчик прерывания (Interrupt Service Routine, ISR).
- После обработки — вернуться к фоновой задаче.
Цели:
- реагировать на внешние события (нажатие кнопки, приём байта по UART);
- обслуживать таймеры (счётчики, ШИМ);
- обрабатывать ошибки (переполнение, сбой питания).
1.2. Типы прерываний
- Аппаратные (от периферии):
- GPIO (изменение уровня на пине);
- UART/SPI/I²C (приход данных);
- таймер (переполнение, сравнение);
- АЦП (завершение преобразования).
- Программные (вызываются инструкцией в коде).
- Исключения (сбои: деление на ноль, неверный адрес).
1.3. Векторная таблица прерываний
Каждый тип прерывания имеет:
- номер (IRQ number);
- вектор — адрес обработчика в памяти.
При срабатывании:
- Процессор читает вектор из таблицы.
- Сохраняет контекст (регистры, PC, PSW).
- Переходит на ISR.
- После
reti(return from interrupt) — восстанавливает контекст.
1.4. Приоритеты и вложенность
- Приоритет определяет, какое прерывание обслуживается первым.
- Вложенность — возможность прервать ISR более приоритетным прерыванием.
- В ARM Cortex‑M: система NVIC (Nested Vectored Interrupt Controller) управляет приоритетами.
2. Программирование прерываний на C
2.1. Синтаксис обработчика (на примере GCC для AVR)
ISR(USART_RX_vect) {
char data = UDR0; // Читаем принятый байт
// Обработка данных
}
Особенности:
- Макрос
ISR(vector)генерирует правильный ассемблерный эпилог/пролог. - Имя вектора (
USART_RX_vect) зависит от МК (см. даташит).
2.2. Синтаксис для ARM Cortex‑M (STM32)
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) { // Приём байта
char data = USART1->DR;
// Обработка
}
if (USART1->SR & USART_SR_TXE) { // Буфер передатчика пуст
USART1->DR = 'A'; // Отправляем байт
}
}
Важно:
- Обработчики именуются по стандарту (см.
startup_stm32f10x.s). - Нужно явно проверять флаги (может сработать несколько условий).
2.3. Включение прерываний
Шаги:
- Разрешить прерывание в периферии:
USART1->CR1 |= USART_CR1_RXNEIE; // Разрешить RXNE - Установить приоритет (для Cortex‑M):
NVIC_SetPriority(USART1_IRQn, 1); - Включить прерывание в NVIC:
NVIC_EnableIRQ(USART1_IRQn); - Разрешить глобальные прерывания:
__enable_irq(); // или __set_PRIMASK(0)
2.4. Правила написания ISR
- Краткость: ISR должен выполняться за микросекунды.
- Атомарность: избегайте длинных операций (делений, плавающей точки).
- Защита данных:
- используйте
volatileдля переменных, изменяемых в ISR; - блокируйте прерывания на время критических секций:
__disable_irq(); // Критический код __enable_irq();
- используйте
- Отсутствие блокировок: не ждите событий внутри ISR.
- Экономия стека: локальные переменные в ISR расходуют стек.
2.5. Пример: обработка кнопки через прерывание (AVR)
volatile uint8_t button_pressed = 0;
ISR(INT0_vect) {
button_pressed = 1;
}
int main(void) {
DDRD &= ~(1 << PD2); // Пин как вход
PORTD |= (1 << PD2); // Подтяжка вверх
EICRA |= (1 << ISC01); // Прерывание по спаду
EIMSK |= (1 << INT0); // Включить INT0
sei(); // Глобальные прерывания
while (1) {
if (button_pressed) {
// Действие при нажатии
button_pressed = 0;
}
}
}
3. Прямой доступ к памяти (DMA): концепция
3.1. Зачем нужен DMA
Без DMA:
- Процессор читает/пишет каждый байт через регистры периферии.
- Нагрузка на CPU: 10–50% при интенсивном обмене.
С DMA:
- Периферия (АЦП, UART) передаёт данные в память без участия CPU.
- CPU лишь настраивает DMA и получает прерывание по завершении.
Выгоды:
- снижение нагрузки на процессор;
- стабильная пропускная способность;
- возможность фоновой передачи во время вычислений.
3.2. Как работает DMA
- Настройка:
- источник/приёмник (адрес периферии, адрес в RAM);
- количество элементов;
- размер элемента (байт, полуслово, слово);
- режим (циркулярный, однократный).
- Запуск:
- периферия генерирует запрос DMA (например, АЦП завершил преобразование).
- Передача:
- DMA‑контроллер перемещает данные;
- CPU свободен.
- Завершение:
- DMA генерирует прерывание;
- ISR обрабатывает результат.
3.3. Режимы DMA
- Однократный — передача N элементов, затем остановка.
- Циркулярный — буфер перезаписывается циклически (для аудио, потоковых данных).
- Двойной буфер — переключение между двумя буферами (минимизация задержек).
4. Программирование DMA на C (пример для STM32)
4.1. Настройка DMA для АЦП
void dma_adc_init(void) {
// 1. Включить часы для DMA1 и ADC1
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. Настроить АЦП
ADC1->CR2 |= ADC_CR2_ADON; // Включить АЦП
ADC1->SQR3 = 0; // Канал 0
ADC1->CR2 |= ADC_CR2_EXTTRIG; // Внешний триггер
// 3. Настроить DMA1 Channel1
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; // Источник: регистр данных АЦП
DMA1_Channel1->CMAR = (uint32_t)adc_buffer; // Приёмник: буфер в RAM
DMA1_Channel1->CNDTR = ADC_BUFFER_SIZE; // Количество элементов
DMA1_Channel1->CCR = DMA_CCR_EN // Включить DMA
| DMA_CCR_DIR // Чтение из периферии
| DMA_CCR_MINC // Инкремент памяти
| DMA_CCR_PSIZE_0 // Размер: байт
| DMA_CCR_MSIZE_0 // Размер: байт
| DMA_CC



