Программирование STM32. Часть 8: DMA

Direct memory access (DMA), или прямой доступ к памяти (ПДП) используется для быстрой передачи данных между памятью и периферийным устройством, памятью и памятью, или между двумя периферийными устройствами без участия процессора. В микроконтроллере STM32F103c8 доступен один контроллер DMA1 с 7-ю каналами. DMA2 присутствует только в микроконтроллерах high-density и XL-density. Предыдущая статья здесь, все статьи цикла можно посмотреть тут: http://dimoon.ru/category/obuchalka/stm32f1.

Содержание

  • Функциональное описание DMA
  • Приоритеты каналов DMA
  • Каналы DMA
  • Выравнивание данных разной разрядности
  • Особенности обращения к периферии AHB/APB
  • Кольцевой режим DMA (Circular mode)
  • Режим «Из памяти в память» (Memory-to-memory mode)
  • Прерывания DMA
  • Ошибки при передаче данных по DMA
  • Каналы DMA и периферия

Функциональное описание DMA

Контроллер DMA использует системную шину совместно с процессорным ядром. Если CPU и DMA обращаются к одной о той же области памяти или одной и той же периферии, то DMA может приостановить доступ CPU к системной шине, но при этом как минимум половина пропускной способности шины резервируется за CPU. Это означает, что даже при интенсивном обмене данными по DMA процессор не зависнет наглухо.

При возникновении определенного события, периферийное устройство отправляет сигнал запроса в контроллер DMA. После этого запускается процесс обмена данными, который состоит из 3-х шагов:

  1. Загрузка данных из регистра периферийного устройства (если направление передачи из периферии в память) или загрузка данных из памяти (если направление передачи из памяти в периферию);
  2. Сохранение загруженных данных в память (если из периферии в память) или в периферийное устройство (если из памяти в периферию);
  3. Уменьшение значения регистра DMA_CNDTRx на единицу. Как только этот регистр станет равен нулю, то передача данных завершится.

Приоритеты каналов DMA

DMA1 в микроконтроллерах STM32F103c8 имеет 7 каналов, причем в конкретный момент времени передача данных может осуществляться только по одному из них. Однако, если активно несколько каналов, то при одновременном возникновении запросов DMA передача будет запущена для того канала, приоритет которого выше. Приведу небольшой пример. Пусть у нас 3-й канал DMA1 настроен на передачу массива данных в SPI1, а 1-й канал на прием от ADC1. Установим приоритет канала 3 больше, чем у канала 1. В этом случае, если запросы от SPI1 и ADC1 возникнут одновременно, то сначала будет обработан запрос от SPI1 (3-й канал), а уже потом от ADC1 (1-й канал). То есть одновременно включать несколько каналов DMA можно, но одновременно вести передачу может только один из них.

Приоритеты можно настраивать программно, всего 4-е градации:

  • Very high priority (Очень высокий приоритет)
  • High priority (Высокий приоритет)
  • Medium priority (Средний приоритет)
  • Low priority (Низкий приоритет)

При одинаковом уровне приоритета (например, 1-й и 3-й канал настроили на Very high priority) канал с меньшим номером будет иметь приоритет над каналом с большим номером (канал 1 будет иметь бОльший приоритет)

Каналы DMA

Каждый канал DMA имеет следующие регистры:

  • DMA_CCRx — регистр конфигурации канала DMA. В нем содержатся биты конфигурации передачи данных, биты разрешения прерывания от канала и бит включения канала.
  • DMA_CNDTRx — сколько кадров данных подлежит передаче (0..65535). Я намеренно употребил слово «кадр», а не «байт». Дело в том, то есть возможность настроить количество байт данных, которые передаются за одну транзакцию (настраивается в регистре DMA_CCRx), об этом поговорим далее.
  • DMA_CMARx — адрес памяти. Если направление передачи из периферии в память, то сюда будем записывать данные, если обратно, то читать.
  • DMA_CPARx — адрес периферийного устройства. Если направление передачи из периферии в память, то отсюда будем читать данные, если обратно, то записывать. В режиме Memory-to-memory сюда так же записывается адрес памяти.

Так, вроде все понятно: имеем 4-е регистра, с помощью которых можно настроить пересылку данных туда-сюда.

Теперь самое время поговорить о такой вещи как инкремент адреса памяти и периферии. Все примеры буду приводить для SPI. Инкремент адреса периферии чаще всего не имеет смысла, а вот инкремент памяти очень полезен. Пусть в регистр DMA_CMARx занесен адрес нулевой ячейки массива, который мы ходим отправить в SPI (вспоминаем указатели Си). После каждой отправки данных в SPI внутренний указатель памяти канала DMA будет увеличиваться на 1 элемент массива. Тут стоит отметить один важный момент: инкремент производится внутреннего указателя, который недоступен программно для чтения или записи, регистр DMA_CMARx не меняет своего значения в процессе передачи данных. 

На примере SPI работать это будет вот так. В регистр DMA_CMARx занесли адрес нулевого элемента массива, который хотим отправить, в DMA_CPARx адрес регистра данных DR модуля SPI. В DMA_CNDTRx записали количество байт для передачи. Включили инкремент адреса памяти, в модуле SPI разрешили запрос к DMA на передачу данных и запустили процесс, установкой бита EN в регистре DMA_CCRx. В начальном состоянии передатчик SPI пуст, флаг пустого передатчика устанавливается в единицу, это провоцирует зарос DMA. DMA получает запрос от SPI, после этого читает байт данных из массива и записывает его в регистр DR интерфейса SPI, увеличивает внутренний указатель памяти на один элемент массива (1 байт) и уменьшает значение регистра DMA_CNDTRx на единицу. После того, как SPI выплюнет байт данных, процесс повторится. Все это будет продолжаться до тех пор, пока значение DMA_CNDTRx не станет равно нулю. После этого канал DMA завершит передачу и больше не будет реагировать на запросы от SPI.

Но это для случая, если нам надо передавать данные в периферию по одному байту. А что делать, если у нас разрядность массива 2 байта, и периферия хочет на вход 2 байта тоже?

Для таких случаев в регистре DMA_CCRx есть конфигурационные биты разрядности периферийного регистра (PSIZE) и разрядности данных в памяти (MSIZE). Они могут принимать следующие значения:

  • 00: 8-bits
  • 01: 16-bits
  • 10: 32-bits
  • 11: Reserved (эта комбинация не используется)

То есть, если мы поставим MSIZE=16 бит (2 байта), то за раз мы будем отправлять уже 2 байта, и указатель на адрес памяти будет увеличиваться на 2. А вот регистр DMA_CNDTRx все так же будет уменьшаться на единицу, так как он содержит не количество байт для передачи, а количество самих передач (транзакций). Получается, что MSIZE нужен для того, чтобы сказать DMA, на сколько байт увеличивать внутренний указатель на адрес памяти. Все верно, но MSIZE  используется и еще для одной вещи.

Выравнивание данных разной разрядности

Очень часто бывают ситуации, когда разрядность приемника данных не совпадает с разрядностью источника. Например, в модуле SPI разрядность регистра данных DR равна 16 бит (2 байта, или полуслово). Однако, SPI у нас может быть настроен на передачу 8-и бит за раз и мы имеем массив данных для передачи, с разрядностью 8 бит. DMA позволяет настроить независимо разрядность передатчика и приемника данных. Как было сказано выше, с помощью битов MSIZE мы задаем разрядность данных в памяти. Но есть еще биты PSIZE, которыми надо указать разрядность регистра периферийного устройства (8, 16 или 32 бита). Если PSIZE не равен MSIZE, то DMA будет производить автоматическое выравнивание данных по следующим правилам.

Пусть разрядность источника данных 8 бит, а приемника 16. Тогда при пересылке DMA добавит 8 незначащих нулей к данным из источника и запишет их в приемник: из источника прочитали, например, 0x13, а в приемник записали 0x0013. В случае, если разрядность источника больше разрядности приемника, то DMA обрежет лишние старшие биты у данных из источника, и в приемник запишет только младшие биты: если разрядность источника 32 бита, а приемника 8 бит, то DMA прочтет из источника значение, например, 0xABCDEF12, а в приемник попадет 0x12. В принципе, все как при присвоении значений переменным в Си.

В Reference manual на микроконтроллеры STM32F1xxx в разделе про DMA есть вот такая таблица:

Programmable data width and endian behavior

Рис. 1. Правила выравнивания данных DMA

Таблица из Reference manual-а может показаться довольно замысловатой (на самом деле это так и есть 😉 ).Давайте разберемся в ней на одном из случае. Например, источник данных у нас 32 бита, а приемник 16 бит:

Рис. 2. Преобразование 32-х битных значений к 16-и битным

Пусть мы пересылаем данные из одного массива в памяти в другой посредством DMA. Причем эти массивы разной разрядности. Массив-источник имеет 32 разряда и содержит следующие данные:

  • элемент 0: B3B2B1B0, смещение 0x00
  • элемент 1: B7B6B5B4, смещение 0x04
  • элемент 2: BBBAB9B8, смещение 0x08
  • элемент 3: BFBEBDBC, смещение 0x0C

Если непонятно что такое смещение и почему оно принимает именно эти значения, то необходимо почитать про организацию данных в памяти микроконтроллера и указатели языка Си.

В качестве массива это будет выглядеть вот так:

Ну и приемник данных:

А DMA будет выполнять вот такую операцию:

Думаю, что для людей, знакомых с Си, будет понятен результат такой операции:

В принципе, все то же самое справедливо и для пересылки данных из памяти в регистр периферии, только в том случае не используется инкремент адреса периферии.

Особенности обращения к периферии AHB/APB

Тут есть одна очень важная и не очевидная особенность архитектуры микроконтроллеров STM32. CPU в STM32 является 32-х разрядным, и для записи в память 8, 16 или 32-х бит существуют разные команды и разные запросы на запись. Для ОЗУ ни каких проблем не существует: мы можем выполнять 8, 16 и 32-х разрядные запросы к памяти. А вот к периферии AHB/APB можно обращаться только 32-х битными запросами. А если нам надо выполнить запись в регистр, который имеет разрядность меньше, чем 32 бита? Объясню на примере все того же SPI. Регистр данных DR у него имеет разрядность 16 бит, и старшие 16 бит 32-х разрядной шины просто не используются:

SPI register map and reset values

Рис. 3. Карта регистров SPI

Если в DMA мы настроим разрядность периферии PSIZE = 16 бит, и разрядность памяти MSIZE = 16 бит, то DMA продублирует младшие 16 бит в старшие и произведет 32-х битный запрос к периферии:

Т.е. из 0xABCD DMA сделает 0xABCDABCD и это значение отправится в периферию. И так как старшие 16 бит регистра DR не используются (зарезервированы), то старшие 16 бит просто проигнорируются. Так же можно настроить PSIZE = 32 бита, и тогда в регистр DR будет занесено значение 0x0000ABCD. А вот если PSIZE установить 8 бит, то DMA сделает следующее преобразование:

Таким образом, в DR будет занесено 0xABAB а не 0x00AB, как можно подумать, если не знать этих особенностей. Вот как раз из-за того, что к периферии можно обращаться только 32-битными запросами, все регистры в периферии выравнены по границе 32 бита (см. рис. 3).

Кольцевой режим DMA (Circular mode)

Думаю, все знакомы с кольцевым буфером. Его очень удобно использовать при непрерывном приеме/передаче данных. В DMA микроконтроллеров STM32 такой режим работы реализован аппаратно, и включается он битом CIRC в регистре управления DMA_CCRx. Если этот режим активирован, то после передачи всех данных по DMA (после того, как DMA_CNDTRx станет равно нулю), регистр DMA_CNDTRx заново перезагружается исходным значением и передача продолжается.

Режим «Из памяти в память» (Memory-to-memory mode)

В «обычном» режиме канал DMA ждет запроса на передачу данных от какого-либо периферийного модуля (SPI, ADC, таймер, и т.д.) Однако, канал DMA может работать и без запроса от периферии, т.е. передача начнется сразу после установки бита EN в регистре DMA_CCRx. Этот режим может использоваться для копирования одной области памяти в другую. Для этого необходимо в регистры DMA_CPARx и DMA_CMARx занести адреса массивов, над которыми необходимо выполнить операцию копирования, и установить бит MEM2MEM в регистре DMA_CCRx. Получается, что и регистру адреса периферии, и регистру адреса памяти присваиваются адреса массивов в памяти. При пересылке MEM2MEM можно использовать любой свободный канал DMA. А как выбирается направление передачи? Точно так же, как и при обмене данными с периферией: битом DIR регистра DMA_CCRx. Пример передачи из памяти в память будет в одной из следующих статей, там, где мы перейдем к практике. Стоит отметить, что нельзя использовать режим MEM2MEM одновременно с Circular mode.

Прерывания DMA

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

  • Half-transferDMA передал половину данных, удобно при потоковой передаче данных совместно с кольцевым режимом: пока передаем одну половину массива, заполняем другую.
  • Transfer complete — прерывание о завершении передачи данных.
  • Transfer error — прерывание об ошибке передачи.

Ошибки при передаче данных по DMA

Ошибка DMA может возникнуть при чтении/записи в зарезервированное адресное пространство микроконтроллера STM32. При возникновении ошибки соответствующий канал DMA отключается (сбрасывается бит EN) и возникает прерывание Transfer error (если разрешено).

Каналы DMA и периферия

DMA1 в микроконтроллерах STM32F103C8 имеет 7 каналов передачи данных, причем на каждом канале висит своя периферия. Приведу таблицу из Reference manual, чтоб было понятнее:

Summary of DMA1 requests for each channel

Рис. 4. Каналы DMA и соответствующие им запросы от периферийных устройств

Например, 1-й канал может обслуживать запросы от ADC1, TIM2_CH3 и TIM4_CH1, а 2-й канал от SPI1_RX, USART3_TX, TIM1_CH1, TIM2_UP и TIM3_CH3. Стоит отметить, что сами запросы должны быть разрешены в регистрах периферийных устройств, при этом, если разрешить DMA-запрос от 2-х источников, то обмен данными будет запускаться от 2-х разных запросов. С ходу не смогу привести пример, где это может быть полезно, и скорее всего такая конфигурация не имеет смысла.

На этом пока все, в следующей статье будет описание регистров DMA, а уже потом перейдем к практике. Продолжение следует!!! 😉

Продолжение.

Метки: , , , . Закладка Постоянная ссылка.

Добавить комментарий

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