Главная / Без рубрики / Программирование микроконтроллеров на языке C: работа с прерываниями, прямой доступ к памяти (DMA)

Программирование микроконтроллеров на языке C: работа с прерываниями, прямой доступ к памяти (DMA)

Введение

Микроконтроллеры (МК) — «мозги» встраиваемых систем: от бытовой техники до промышленного оборудования. Их эффективность зависит от умения программиста:

  • управлять событиями в реальном времени (через прерывания);
  • оптимизировать обмен данными (с помощью DMA);
  • работать с периферией на низком уровне.

В статье разберём:

  • архитектуру прерываний в МК;
  • синтаксис и семантику обработчиков прерываний на C;
  • принципы работы DMA;
  • примеры кода для популярных платформ (STM32, AVR).

1. Прерывания: концепция и назначение

1.1. Что такое прерывание

Прерывание — сигнал, заставляющий процессор:

  1. Приостановить выполнение основного кода (фоновой задачи).
  2. Переключиться на обработчик прерывания (Interrupt Service Routine, ISR).
  3. После обработки — вернуться к фоновой задаче.

Цели:

  • реагировать на внешние события (нажатие кнопки, приём байта по UART);
  • обслуживать таймеры (счётчики, ШИМ);
  • обрабатывать ошибки (переполнение, сбой питания).

1.2. Типы прерываний

  • Аппаратные (от периферии):
    • GPIO (изменение уровня на пине);
    • UART/SPI/I²C (приход данных);
    • таймер (переполнение, сравнение);
    • АЦП (завершение преобразования).
  • Программные (вызываются инструкцией в коде).
  • Исключения (сбои: деление на ноль, неверный адрес).

1.3. Векторная таблица прерываний

Каждый тип прерывания имеет:

  • номер (IRQ number);
  • вектор — адрес обработчика в памяти.

При срабатывании:

  1. Процессор читает вектор из таблицы.
  2. Сохраняет контекст (регистры, PC, PSW).
  3. Переходит на ISR.
  4. После 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. Включение прерываний

Шаги:

  1. Разрешить прерывание в периферии:USART1->CR1 |= USART_CR1_RXNEIE; // Разрешить RXNE
  2. Установить приоритет (для Cortex‑M):NVIC_SetPriority(USART1_IRQn, 1);
  3. Включить прерывание в NVIC:NVIC_EnableIRQ(USART1_IRQn);
  4. Разрешить глобальные прерывания:__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

  1. Настройка:
    • источник/приёмник (адрес периферии, адрес в RAM);
    • количество элементов;
    • размер элемента (байт, полуслово, слово);
    • режим (циркулярный, однократный).
  2. Запуск:
    • периферия генерирует запрос DMA (например, АЦП завершил преобразование).
  3. Передача:
    • DMA‑контроллер перемещает данные;
    • CPU свободен.
  4. Завершение:
    • 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

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *