Dma stm32 что это
Перейти к содержимому

Dma stm32 что это

  • автор:

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

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

Функциональное описание 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

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

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

uint32_t Sourse[4] = < 0xB3B2B1B0, 0xB7B6B5B4, 0xBBBAB9B8, 0xBFBEBDBC >;

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

uint16_t Destination[4];

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

for(int i=0; i

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

uint16_t Destination[4] = < 0xB1B0, 0xB5B4, 0xB9B8, 0xBDBC >;

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

Особенности обращения к периферии 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 -> 0xABCDABCD

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

0xAB -> 0xABABABAB

Таким образом, в 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, а уже потом перейдем к практике. Продолжение следует. ��

Урок 33. Прямой доступ к памяти в STM32. Контроллер DMA.

Контроллер ПДП

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

Микроконтроллер обменивается данными с “внешним миром” только через периферийные устройства, а данные хранит в памяти. Поэтому в любой реальной программе существует задача обмена данными между оперативной памятью микроконтроллера и периферийными устройствами. Также бывает необходимость в обмене данными между блоками памяти (копирование блоков) и в передаче информации с одного периферийного устройства на другое.

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

  • В каких-то случаях управление периферийными устройствами с помощью программных операторов выглядело единственным логичным вариантом. Например, когда мы зажигали светодиод, подключенный к порту ввода/вывода. Странно было бы установить ячейку памяти в нужное состояние, а затем с помощью дополнительных аппаратных узлов переслать ее содержимое на порт ввода/вывода.
  • В других случаях передача данных из программы нас вполне устраивала, не вызывала проблем в реализации. Например, когда выводили сообщения из массива или текстовой строки в последовательный порт. Но уже тогда было заметно, что мощный процессор использует минимум своих вычислительных возможностей. Он просто ждет освобождения контроллера UART, считывает данное из памяти и записывает его в регистр данных контроллера UART. И так нужное количество циклов.

Использование прерываний позволяло только оперативно перераспределять работу процессора между задачами. Процессор все равно занимался примитивными операциями – пересылкой данных. Программа была простой, и мы могли позволить тратить почти все процессорное время на копирование данных.

  • А вот при реализации цифрового осциллографа в уроке 29 программное чтение данных АЦП значительно ограничивало возможности устройства. На время чтения развертки осциллографа программа работала в блокирующем режиме, зависала. Приходилось даже запрещать прерывания. Благодаря высокому быстродействию STM32 удалось считывать данные АЦП с дискретностью 1 мкс, но это был предел. С большей скоростью поток данных уже не обработать.

А ведь задача пересылки данных между памятью и периферийным устройством с аппаратной точки зрения очень простая. Достаточно аппаратного счетчика адреса памяти, счетчика количества данных и промежуточного регистра. Данное считывается, например, из АЦП и записывается в память по адресу из специального регистра-счетчика. Его содержимое увеличивается на 1, и новое данное из АЦП записывается в память по следующему адресу. Процесс продолжается пока счетчик количества данных не остановит работу устройства. При этом процессор продолжает выполнение основной программы.

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

Процесс обмена данными без участия процессора (за счет аппаратных узлов микроконтроллера)

  • между памятью и периферийными устройствами микроконтроллера,
  • или между периферийными устройствами,
  • или между блоками памяти

называется прямым доступом к памяти (ПДП). Английская аббревиатура DMA - Direct memory access.

Аппаратная часть микроконтроллера, реализующая процесс прямого доступа к памяти называется контроллер прямого доступа к памяти (контроллер DMA).

Использование ПДП (DMA) позволяет:

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

Контроллер ПДП

Устройство контроллеров DMA, как правило, простое, очевидное и понятное для разработчиков программ.

Общие принципы работы контроллера прямого доступа к памяти в STM32.

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

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

  • В микроконтроллере STM32F103c8 есть только один контроллер DMA c 7 каналами. В микроконтроллерах старших серий (high-density и XL-density) добавлено второе устройство DMA.
  • Как я писал выше, прямой доступ к памяти происходит по общей с процессором системной шине. Поэтому при одновременном обращении к памяти или периферийному устройству процессора и контроллера DMA могут возникать конфликты. В результате работа процессора может приостанавливаться, но как минимум половина пропускной способности системной шины отдается процессору. Программа будет выполняться даже при интенсивном обмене через прямой доступ к памяти. Не говоря о том, что не каждая команда программы работает с оперативной памятью или периферийным устройством.
  • Инициаторами обмена с помощью контроллера DMA являются аппаратные события микроконтроллера (event). Конечно, при наличии соответствующих разрешений в управляющих регистрах. В уроке 18 я писал о событиях, и чем они отличаются от прерываний.

В общем случае прямой доступ к памяти происходит следующим образом.

  • В периферийном устройстве возникает событие, требующее операции DMA. Соответствующий сигнал поступает на контроллер DMA.
  • Происходит обмен данными между памятью и периферийным устройством.
  • Сразу после обращения к периферийному устройству, контроллер посылает ему сигнал подтверждения, который сбрасывает событие.
  • Новое событие вызывает очередной обмен данными.

Сам обмен данными (транзакция) с использованием DMA состоит из трех операций.

  • Данное загружается в промежуточный регистр из памяти или из регистра данных периферийного устройства в зависимости от направления обмена. Адреса памяти и периферийного устройства содержатся в специальных регистрах.
  • Данное перегружается в память или регистр данных периферийного устройства.
  • Регистр количества транзакций (элементов обмена) уменьшается на 1. При достижении 0, обмен завершается.

Все просто и логично.

Каналы DMA.

Повторюсь. В микроконтроллере STM32F103c8 есть только один контроллер DMA c 7 каналами.

С точки зрения программиста каждый канал – это отдельный аппаратный узел микроконтроллера. Он осуществляет DMAпередачу между регистром данных периферийного устройства (расположенным по фиксированному адресу) и памятью.

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

В каждый момент времени обмен данными может происходить только по одному каналу. При нескольких активных каналах контроллер DMAразрешает работу канала с более высоким приоритетом.

Приоритет каждого канала устанавливается программно и может иметь четыре уровня:

  • Very high priority (очень высокий);
  • High priority (высокий);
  • Medium priority (средний);
  • Low priority (низкий).

При активных каналах с одинаковым уровнем приоритета преимущество имеют каналы с меньшим номером.

Регистры каналов.

Каждый канал для нас – отдельный аппаратный контроллер передачи данных.

Каждый канал имеет 4 собственных регистров, которые полностью определяют режим обмена данными по нему.

Каналы настолько независимы, что если вести речь обо всех регистрах контроллера DMA, то к регистрам каналов добавятся только регистры управления прерываниями.

Итак 4 регистра, с очевидным назначением.

  • DMA_CMARx — адрес памяти, по которому выполняется запись или чтение в зависимости от направления передачи данных.
  • DMA_CPARx — адрес регистра данных периферийного устройства, из которого выполняется чтение или в который производится запись. В режиме обмена данными между блоками памяти он также содержит адрес памяти.
  • DMA_CNDTRx — содержит количество элементов для передачи (количество транзакций). В качестве элемента транзакции могут быть выбраны байт, полуслово, слово.
  • DMA_CCRx — регистр конфигурации канала. Разрешает работу канала, определяет режимы передачи данных.

Полное описание регистров есть в справочнике.

Количество элементов данных для передачи устанавливается программно и может быть от 0 до 65535.

В общем случае обмен DMA происходит между памятью и периферийным устройством. Источником или приемником данных периферийного устройства является регистр данных этого устройства. Он имеет фиксированный адрес, который должен оставаться неизменным в ходе DMA обмена.

Что касается памяти, то как правило, необходимо записывать или считывать данные по последовательно расположенным адресам.

Для этого существует режим инкрементации адреса. Он доступен как для адреса памяти (бит MINC регистра DMA_CCRx), так и для адреса периферийного устройства (бит PINC регистра DMA_CCRx). Хотя в последнем случае не имеет смысла.

Таким образом, после каждого обращения к памяти указатель увеличивается на 1, 2 или 4, в зависимости от заданного размера элемента данных. Начальные адреса передачи данных хранятся в регистрах DMA_CMARx (для памяти) и DMA_CPARx (для периферии). В процессе обмена данными их значения не изменяются, а текущие значения указателей содержатся во внутренних регистрах микроконтроллера и программе недоступны.

Размер элементов данных транзакции может иметь значения:

  • 8 бит (байт);
  • 16 бит (полуслово);
  • 32 бита (слово).

Выбор размера данных осуществляется независимо не только для каждого канала, но и отдельно для приемника и передатчика данных каждого канала. Для этого в регистре DMA_CCRx существуют битовые поля PSIZE и MSIZE.

После выполнения последней передачи запросы по каналу перестают обслуживаться. Регистр количества данных передачи DMA_CNDTRx содержит 0. Чтобы инициировать новый DMA обмен необходимо загрузить в него новое значение. Это возможно только после отключения канала. При этом все остальные регистры канала (DMA_CMARx, DMA_CPARx и DMA_CCRx) сохраняют значения.

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

Конфигурация канала DMA.

Для использования канала необходимо конфигурировать его, выполнив следующую последовательность действий. Изначально канал должен быть запрещен (бит EN регистра DMA_CCRx сброшен).

  • Загрузить в DMA_CPARx адрес регистра данных периферийного устройства. Данные будут записываться/считываться по этому адресу после возникновения события периферийного устройства, формирующего запрос DMA.
  • Загрузить в DMA_CMARx начальный адрес блока памяти. В этот блок будут записываться данные из периферийного устройства, либо из него будут считываться данные для передачи в периферийное устройство.
  • Записать в DMA_CNDTRx общее количество элементов обмена. Каждая транзакция будет уменьшать значение этого регистра. При достижении 0, DMA передача будет остановлена.
  • Задать конфигурацию DMA передачи в регистре DMA_CCRx: направление передачи, приоритет канала, режим инкрементирования, размер элемента данных и т.д.
  • Включить канал битом EN регистра DMA_CCRx.

После этого начинают обрабатываться запросы от периферийных устройств, подключенных к каналу.

Чтением регистра DMA_CNDTRx можно узнать количество оставшихся для передачи элементов.

  • После завершения передачи половины от общего количества элементов, будет установлен флаг HTIFx в регистре DMA_ISR. Произойдет прерывание на это событие, если оно разрешено в регистре DMA_CCRx (бит HTIE).
  • После завершения передачи всех данных, будет установлен флаг TCIFx в регистре DMA_ISR. Произойдет прерывание на это событие, если оно разрешено в регистре DMA_CCRx (бит TCIE).
  • Если не задан циклический режим, то после пересылки заданного количества данных работа канала останавливается.
  • В циклическом режиме (бит CIRC регистра DMA_CCRx) после завершения передачи данных, в регистр DMA_CNDTRx автоматически перезагружается его первоначальное значение, и обмен продолжается бесконечно.

Режим “из памяти в память”.

Режим используется для копирования блоков данных в памяти.

  • Для его конфигурации необходимо установить бит MEM2MEM в регистре DMA_CCRx.
  • После включения канала битом EN регистра DMA_CCRx сразу начнется передача данных. Дополнительные запросы не требуются.

Режим запрещено использовать совместно с циклическим режимом.

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

При DMA передаче данных разрядности приемника и источника могут не совпадать. Действует простое и логичное правило. Всегда сохраняются младшие разряды.

  • Если разрядность источника превышает разрядность приемника, то лишние старшие разряды источника игнорируются. Например, 32х разрядное число 0x87654321 передаем в периферию с регистром данных 16 разрядов. В него запишется 0x4321.
  • Если разрядность источника меньше разрядности приемника, то недостающие старшие разряды в приемнике будут заполнены нулями. Например, 16ти разрядное число 0x4321 источника загружаем в 32х разрядный приемник. Запишется 0x00004321.

В принципе используется правило преобразования (приведения) типов данных языка C.

Специфика доступа к периферийным устройствам через шины AHB/APB.

Периферийные устройства STM32 подключены к процессору и памяти через шины AHB/APB. Это 32х разрядные шины. Они не поддерживают операций передачи меньшей разрядности. В результате в регистры периферийных устройств можно записывать или считывать из них только 32х разрядные слова.

Многие регистры периферии имеют меньшую разрядность, часто 16 разрядов. В оставшиеся биты при передаче данных DMA тоже что-то будет записываться. В большинстве случае это не важно, разряды не значащие. Но все-таки давайте внесем определенность в этот процесс.

При записи в периферийное устройство байта или полуслова через шину AHB данные на неиспользуемых линиях шины дублируются. Например.

  • Если мы записываем полуслово 0x4321, в регистр периферии загрузится 0x43214321.
  • Если мы записываем байт 0x21, в регистр периферии загрузится 0x21212121.
  • Но если мы укажем разрядность источника слово, т.е. запишем слово 0x87654321, а разрядность периферийного регистра укажем полуслово, то произойдет выравнивание разрядности, описанное в предыдущем абзаце. В результате в регистр загрузится 0x00004321.

Ошибки при DMA передаче.

Ошибка может возникнуть только в результате попытки доступа к зарезервированному адресному пространству. В этом случае канал, в котором произошла ошибка, отключается аппаратным сбросом бита EN. Устанавливается флаг ошибки TEIFx в регистре DMA_ISR. И формируется прерывание, если оно разрешено битом TEIE в регистре DMA_CCRx.

Прерывания DMA.

Для каждого канала могут генерироваться прерывания по следующим флагам регистра DMA_ISR.

  • HTIFx – завершена передача половины данных. Разрешается прерывание битом HTIE в регистре DMA_CCRx.
  • TCIFx - завершена передача всех данных. Разрешается прерывание битом TCIE в регистре DMA_CCRx.
  • TEIFx – ошибка передачи. Разрешается прерывание битом TEIE в регистре DMA_CCRx.

Соответствие каналов DMA периферийным устройствам.

Каналов DMA только 7. Периферийных устройств больше. Поэтому на каждый канал поступают сигналы запросов от нескольких периферийных устройств, объединенные логическим элементом ИЛИ. В каждый момент времени только одно устройство может использовать канал.

Остальные периферийные устройства должны отключить свои запросы DMA. Для этого у каждого устройства с поддержкой DMA есть специальный бит активации/деактивации формирования запроса.

Остается привести таблицу соответствия каналов DMA периферийным устройствам.

Соответствие каналов DMA периферийным устройствам STM32

На мой взгляд, контроллер DMA один из самых простых и понятных узлов STM32.

В следующем уроке будем разрабатывать программы, использующие контроллер DMA.

DMA вообще и в частности

О существовании DMA (Direct Memory Access) — русскоязычное ПДП (Прямой Доступ к Памяти) многие разработчики встроенных устройств слышали, но вот применяют его гораздо реже, чем он (ПДП) этого заслуживает. Кстати, я буду упоминать именно эту аббревиатуру, но не потому, что я такой упрямый патриот и противник англоязычных заимствований, а всего лишь от того, что мне лень лишний раз переключать раскладку клавиатуры.

Основных причин недостаточного использования ПДП в программах для МК три: 1) относительная сложность данного устройства, которая вместе с 2) непониманием выгод его применения приводит к нежеланию данное устройство изучать и осваивать (как говорят в таких случаях, старшая сестра не велит — для тех, кто в танке — это про лень, котороая раньше нас родилась), отягощенному 3) отсутствием хороших и понятных примеров применения ПДП в поставляемых с МК руководствами. И если первые две причины носят явно субъективный характер, то третья несомненно объективна и внутри меня просыпается параноик и настойчиво утверждает, что это сделано специально с целью не допустить отечественных разработчиков МК на продвинутые уровни, где-то выше 60 (то, что при этом страдают и остальные разработчики по всему миру, параноиком игнорируется, поскольку либо 1) за пределами России распространяются правильные примеры, либо 2) ради великой цели не допустить вставания, сами понимаете кого, с колен буржуины готовы пойти на любые жертвы).

Тем не менее без шуток, действительно, в примерах в лучшем случае лежит модуль настройки отдельно взятого канала ПДП, а увязанную систему с ПДП драйвером Вы в примерах применений не найдете (даже в CMSIS не найдете, ну тут действительно есть объективная причина — напишу пост про него — упомяну). Почему так на самом деле — я не знаю, но разработчикам кристаллов виднее, единственное разумное обоснование, которое мне приходит в голову — это то, что ПДП довольно таки специфичны, поэтому «нельзя просто так взять и » перенести код из другого источника, а ввиду малой востребованности ПДП в реальных разработках отсутствие таких примеров не считается существенным недостатком. Восполнить указанный мной пробел в знаниях и предназначен настоящий пост (нескромное заявление, но если сам себя не похвалишь, весь день ходишь как оплеванный), поэтому те, кого я заинтриговал, могут нажать на кнопочку.

Тем не менее, должен предостеречь нетерпеливого читателя, что он не найдет тут серебряной пули, которую можно смело включать в свои разработки, а всего лишь (но и это немало) обнаружит некоторые мысли и подходы, которые облегчат ему построение своих собственных систем на МК с применением ПДП. То есть я поставлю флажки в тех местах, где точно лежат грабли, но не гарантирую, что непомеченных граблей не останется, что, впрочем, не мешает Вам повыкидывать флажки на фиг и пройтись по граблям самостоятельно. Вообще, те, кто читал мои посты, наверняка обратили внимание, что я делаю упор не на то, ЧТО следует сделать и КАК именно, а на то, ПОЧЕМУ я рекомендую сделать именно так.

Итак, ПДП — это часть аппаратуры МК, которая позволяет производить пересылку данных между различными составными частями данного МК (и системы с его участием) без привлечения ресурсов процессора (точнее говоря, с минимальным привлечением, поскольку делать что либо в МК системе вообще БЕЗ участия процессора — мысль смелая и далеко заводящая).

Основная идея данных устройств состоит в том, что при выполнении программы далеко не каждая команда вызывает обращение по системной шине к внешним устройствам или к оперативной памяти (обращение к памяти программ не рассматривается), поэтому у устройства, обслуживающего системную шину, имеются циклы простоя, которые было бы неплохо как нибудь использовать.

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

Встречалась картина (например в ДВК), когда одно ВУ работало по ПДП, а второе — по прерываниям или даже в цикле опроса (драйвер MX, если кто помнит). Когда появились первые МК, в которых память интегрирована в чип, создание внешних по отношению к кристаллу устройств с ПДП стало весьма нетривиальной задачей и, естественно, ПДП перекочевало внутрь МК и стало частью его архитектуры. Этот процесс не имел линейного поступательного характера и можно встретить как МК на основе 51й архитектуры с поддержкой ПДП, так и МК на основе Cortex-M3 с богатым набором периферии, но без поддержки ПДП (например, Stellaris). Тем не менее, в бОльшей части современных МК на основе ARM ПДП присутствует и можем перейти к их изучению и для начала остановиться на рассмотрению их особенностей.

Как было сказано выше, основное назначение ПДП — передача данных между разными устройствами с минимальным привлечением процессора. Как правило, одним из этих устройств является основная память МК, хотя иногда возможны как передачи между двумя внешними устройствами, так из памяти в память. Некоторые ПДП поддерживают такие возможности, некоторые нет, и это следует учитывать при выборе МК.

Следующей важной характеристикой является количество одновременно обслуживаемых каналов передачи — в том смысле, конечно, что в каждый момент времени по шине может передаваться только одна порция данных, но может быть несколько запросов, которые будут обслуживаться поочередно, опять таки при минимальных затратах ресурсов процессора. Естественно, минимальное количество каналов ПДП -1 (меньшие количество каналов едва ли будут работоспособны), максимальное — практически неограничено, реальным ограничением может быть количество внешних устройств+1(для запросов типа память-память), но ничто (кроме стоимости) не может помешать сделать нам сколь угодно много каналов ПДП.

Следующей особенностью можно считать механизм обслуживания запросов в одном канале. Остановимся на этом моменте поподробнее и разберем в общих чертах работу одного запроса одного канала ПДП.

Для того, чтобы совершить передачу, нам следует сообщить ПДП о своих намерениях, то есть откуда мы хотим передавать данные, куда их следует направить, сколько именно данных следует передать, режимы передачи (об этом чуть позже), и, возможно, служебная информация. Поместить данную информацию мы можем либо в регистры, ответственные за работу ПДП (устаревший способ и дальше мы поймем, почему) либо в оперативную память системы, причем в последнем случае это должна быть либо специальная выделенная область памяти, чтобы ПДП знал, откуда надо взять информацию, либо в какой то регистр ПДП должна быть помещена информация о том, где именно в оперативной памяти лежат данные (говоря привычным языком, мы должны поместить ссылку). Именно последний метод и используется, поскольку он имеет следующие преимущества: оперативная память МК — ценный ресурс и сегментировать ее на детерминированные блоки не есть хорошая практика.

Так вот, после того, как заданная передача будет произведена, мы получим сигнал об окончании передачи (скорее всего в виде прерывания) и нам вполне может потребоваться как можно быстрее начать следующую передачу. Если мы задаем параметры передачи в регистрах, то либо мы не можем их модифицировать до окончания текущей передачи, либо делать теневые регистры. Если же в регистре хранится ссылка на данные, определяющие передачу (TCB — Transfer Control Block — Блок Управления Передачей -БУП), то мы можем во время выполнения одной передачи заранее настроить БУП для следующей и по окончанию текущей передачи изменить всего лишь ссылку в регистре ПДП.

На практике встречаются разнообразные комбинированные схемы, в основном с целью экономии аппаратуры МК — указатель на область памяти, в которой размещаются 2 или более БУП, циклически переключающиеся по мере исполнения, указатель на следующий БУП в служебном поле текущего БУП, иерархический доступ, когда БУП содержит указания на последовательность БУП, которые собственно и выполняются и так далее.

В вышеописанных случаях нам также потребуется специальный тег, говорящий о завершении всех к настоящему моменту запрошенных передач, который будет находиться среди служебной информации. Среди прочей служебной информации часто находятся теги, говорящие о режиме передачи (разрядность передаваемых данных, шаг и направление изменения адресов приемника и отправителя информация для модуля управления памятью . ).

Теперь перейдем от рассмотрения ПДП вообще к рассмотрению конкретной реализации, а именно к МК фирмы «Миландр» 1986ВЕ1Т.

Почему именно к нему? Ну, во-первых, я с ним работаю, во-вторых, у него достаточно богатый по возможностям ПДП, а в-третьих, у него богато фич (это не баг это фича такая — да да именно из этого разряда), которые делают работу с МК увлекательным занятием, после которого работа с аналогами от известных производителей покажется легкой и простой.

Сначала о хорошем — ПДП поддерживает 32 независимых канала доступа — по одному на каждое внешнее устройство в составе МК и еще один для пересылок память-память. Кроме того, как вы уже поняли из предыдущего предложения, ПДП поддерживает все возможные режимы пересылки, а именно: регистры ВУ-память, регистры ВУ-регистры ВУ, память-память, и если вам кажется, что так и должно быть, то это совсем не так, и разные режимы функционируют слегка по-разному и не везде реализованы.

Далее, ПДП поддерживает разные форматы данных: 1 байт, 2 байта и 4 байта (слово в нашей архитектуре), а также разные виды инкремента адреса отдельно источника и приемника: на 1, на 2 и на 4 (декремент не поддерживается). ПДП имеет систему арбитража обслуживаемых каналов с возможностью назначения гибких приоритетов для каждого канала и настраиваемого размера элементарной транзакции (количества передач одного канала, по выполнению которых производится арбитраж). Кроме того, каждый канал может иметь до 2 БУП, которые могут сменяться по циклической системе, либо работать в иерархическом режиме, вместе с тем возможен и однократный режим.

Чтобы соблюсти баланс, скажем и о менее хорошем. Опять про документацию — если Вы на знаете, как работает ПДП, то из документации фирмы вы об этом точно не узнаете. Документация явно переводная, есть и ошибки перевода, которые сильно искажают смысл, есть и весьма невнятно описанные места, но в целом для подготовленного разработчика ее может и хватить, если вы привыкли домысливать за автора. Из более существенных недостатков (конечно по сравнению с идеальным устройством) — значительные затраты ресурса шины (6 доступов на 1 пересылку, хотя может я чего-то недопонял) и еще ряд фич, о которых чуть позже.

И здесь нас ждет:

ЗАСАДА №1 от разработчиков — прерывание от окончания передачи не проявляются в внешних устройствах. То есть у нас есть один вектор прерывания по окончанию транзакции от любого из запрограммированных каналов. Более того, нет никакого регистра, в котором хранился бы номер канала, завершившего транзакцию, либо хотя бы битовый регистр с флагами. То есть единственный способ определить номер завершившего передачу канала — перебирать все каналы и смотреть соответствующие поля TCB, и это нам придется делать в обработчике прерывания, который должен занимать минимальное время. Напрашивающееся решение — перенести поиск канала в нижнюю половину драйвера обработки не проходит, поскольку нас ожидает:

ЗАСАДА № 2 от разработчиков — прерывание является потенциальным и прекратить работу верхней половниы, не сбросив его явным образом, мы не можем. Более того, есть еще и

ЗАСАДА № 3 от них же — мы не можем сбросить прерывание путем манипуляции реистрами ПДП, а должны проводить сброс разрешения выработки запросов в регистрах внешнего устройства. Да да, именно так, драйвер ПДП должен что то знать о составе регистров обслуживаемых устройств, более чудовищного нарущения принципа инкапсуляции (а он справедлив и для проектирования аппаратуры) трудно себе представить. Я не знаю, что курили разработчики ПДП, но, как написано у Гайдука, «что то очень интересное». То есть, вы можете не верить, но если мы запретим прохождение запросов соостветствующего канала и запретим его обработку, то прерывание мы все равно НЕ СБРОСИМ.
Что-то получилось больше букв, чем было запланировано, поэтому оставим читателей размышлять о сложной ситуации, в которой оказался главный герой истории, что особенно актуально в пятницу вечером, а сам напишу Продолжение следует…

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

Итак, мы рассмотрели обмен с периферийными устройствами посредством DMA на примере передачи данных из памяти в интерфейс SPI и обратно, а так же с помощью DMA копировали один массив данных в другой. В этой части мы рассмотрим прерывания, которые может генерировать контроллер DMA. Предыдущая статья здесь, все статьи цикла можно посмотреть тут: https://dimoon.ru/category/obuchalka/stm32f1.

Прерывания DMA

В stm32f103c8 каждый канал DMA может генерировать 3 типа прерываний:

  1. Передача данных завершена
  2. Завершена передача половины данных
  3. Ошибка передачи данных (возникает при обращении к зарезервированной области данных)

Для каждого канала нужные прерывания можно включить индивидуально в регистре конфигурации канала DMA_CCRx. За это отвечают биты TCIE (завершение передачи), HTIE ( передана половина буфера) и TEIE (ошибка передачи):

DMA channel x configuration register (DMA_CCRx)

Кроме того, есть еще 2 специальных регистра, один из которых содержит информацию об активных флагах прерываний всех каналов DMA (регистр DMA_ISR), а с помощью другого можно сбросить нужные флаги прерываний выбранных каналов (регистр DMA_IFCR):

Регистр статуса прерываний DMA_ISR

DMA interrupt flag clear register (DMA_IFCR)

Регистр сброса флагов прерываний DMA_IFCR

Более подробное описание всех регистров DMA можно посмотреть тут.

Использование прерываний DMA

Переходим к практике. В качестве основы возьмем код из статьи Программирование STM32. Часть 10: SPI + DMA, где мы через DMA отправляли массив данных в SPI. Инициализация SPI производится точно так же, только добавим в конец 3 строчки:

void SPIInit(void) < RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; //Включаем тактирование SPI1 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //включаем тактирование порта GPIOA RCC->AHBENR |= RCC_AHBENR_DMA1EN; //Включаем тактирование DMA1 //Настройка GPIO //PA7 - MOSI //PA6 - MISO //PA5 - SCK //Для начала сбрасываем все конфигурационные биты в нули GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk); //Настраиваем //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b) GPIOA->CRL |= (0x02<CRL |= (0x01<CRL |= (0x02<CR1 = 0CR2 |= 1CR2 |= 1CR1 |= 1IFCR = 1

С помощью строки

DMA1->IFCR = 1

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

NVIC_EnableIRQ(DMA1_Channel3_IRQn); //разрешаем прерывания //от канала 3 DMA1

Далее, рассмотрим функцию отправки данных в SPI через DMA:

void SPI_Send(uint8_t *data, uint16_t len) < //отключаем канал DMA после предыдущей передачи данных DMA1_Channel3->CCR &= ~(1 CPAR = (uint32_t)(&SPI1->DR); //заносим адрес регистра DR в CPAR DMA1_Channel3->CMAR = (uint32_t)data; //заносим адрес данных в регистр CMAR DMA1_Channel3->CNDTR = len; //количество передаваемых данных //Настройка канала DMA DMA1_Channel3->CCR = 0 CCR |= 1 

Здесь мы добавили строчку

с помощью которой включаем прерывание в 3-м канале DMA после завершения передачи.

Остался обработчик прерывания. Он будет выглядеть вот так:

void DMA1_Channel3_IRQHandler(void) < DMA1->IFCR = 1

Обращаю внимание на DMA1->IFCR = 1. С помощью этой строки мы сбрасываем флаг прерывания о завершении передачи данных в 3-м канале DMA. Без этой строки прерывание DMA1_Channel3_IRQHandler() будет вызываться бесконечное число раз, что застопорит выполнение основной программы МК.

Для проверки вот такой main():

uint8_t data[10]; void main() < for(int i=0; iSPIInit(); SPI_Send(data, sizeof(data)); for(;;) < >>

На этом пока все, спасибо за внимание. ��

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

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