Пришло время познакомиться поближе с технологией DMA, её реализацией в контроллере STM32F1, пока поверхностно, без подробного рассмотрения регистров и их битов, которые мы рассмотрим в одном из следующих занятий, когда будем работать с DMA, применяя функционал библиотеки LL. Также в данном уроке мы остановимся пока на алгоритме передачи данных из одного участка памяти в другой или MEM2MEM без использования какой-либо периферии.
Использовать для изучения возможностей DMA мы будем на данном уроке библиотеку HAL.
С DMA мы уже частично знакомились, мало того, мы его уже применяли в ряде уроков и проектов, только мы глубоко не вдавались, как данная технология, аппаратно реализованная в нашем контроллере, работает.
Сначала давайте повторим, что же такое DMA, хотя я об этом говорил уже в уроке 15, когда мы применяли DMA совместно с шиной USART.
DMA (direct memory access) — прямой доступ к памяти. То есть мы из одной области памяти копируем данные в другую область памяти, либо из периферии в область памяти, либо из области памяти в периферию, но копируем не по одному байту, применяя при этом обязательно регистры АЛУ, а напрямую, без использования АЛУ. Этим самым достигается большее быстродействие (правда не всегда, да и смысл не в быстродействии), разгружается процессорное время. АЛУ лишь получает информацию о том, сколько определенных информационных единиц нужно переслать, а также адрес памяти источника и приемника, всё остальное уже происходит без его участия. Мы либо знаем приблизительно, сколько для этого требуется времени и исходя из этого уже строим свой алгоритм, либо пользуемся прерываниями и обрабатываем там событие окончания передачи через DMA.
В микроконтроллере STM32F1 модуль DMA имеет следующую структурную схему
Мы видим здесь, что блок-схема модуля DMA у контроллера STM32F1 содержит два контроллера DMA, в первом из которых присутствует семь независимых каналов, а во втором — пять, тем самым мы имеем 12 независимых каналов передачи данных.
Оба контроллера DMA связаны с шиной AHB, также имеют блоки Arbiter, которые отвечают за распределение приоритетов в случае возникновения одновременно двух запросов.
Приоритеты мы можем назначить каждому запросу следующие:
- Very high priority — очень высокий,
- High priority — высокий,
- Medium priority — средний,
- Low priority — низкий.
Каждый канал передачи данных может работать с определённой периферией контроллера STM32
Из данных схем мы видим, что для копирования из памяти в память (MEM2MEM) мы можем использовать любой канал
При инициализации DMA мы указываем кроме адресов ещё и размерность данных, которая может быть 8, 16 и 32 бита. Размерность данных отправителя и получателя может не совпадать. На этот случай контроллер всё выровняет и в Reference Manual существует таблица, как именно данные выравниваются.
Также в DMA контроллера STM32F1 имеется поддержка циклического управления буфером. Ещё мы можем использовать 3 флага прерываний, возникающих в случае передачи половины буфера, передачи всего буфера и возникновения ошибок.
В DMA нашего контроллера мы также можем программировать количество данных для передачи до 65536.
Давайте теперь приступим к проекту, в котором мы попробуем передать некоторое количество данных и одной области оперативной памяти в другую сначала без использования DMA, а затем с использованием и оценим плюсы и минусы этих вариантов копирования.
Для оценки и мониторинга скопированных данных мы будем использовать 8-разрядный индикатор на драйвере MAX7219, поэтому, чтобы нам не писать библиотеку для работы с данным драйвером, мы возьмём за основу проект, например, урока 153 с именем SPI_MASTER и создадим из него проект с именем DMA_MEM_TO_MEM.
Откроем наш проект в Cube MX и отключим там периферию SPI1
Также отключим и ножку PA4
Перейдём в раздел System view и откроем там периферию DMA
Добавим в список канал, выбрав тип передачи MEM2MEM, оставим канал 1, только размер передаваемых данных изменим на полуслова (Half Word), инкрементирование также оставляем и у приёмника, и у источника
Откроем теперь NVIC (модуль вложенных векторных прерываний)
И включим прерывания от DMA, приоритеты не трогая
Сгенерируем проект, откроем его в Keil, подключим файл библиотеки max7219.c, включим программатору атоперезагрузку, а также уберём оптимизацию.
В файле main.c удалим объявление буфера
uint8_t RxBuf[2] = {0};
Вместо него добавим два больших буфера, один из которых будет являться источником передаваемых данных, а другой — их приёмником
1 2 3 |
/* USER CODE BEGIN PV */ uint16_t src_dma_buf[1024] = {0}; uint16_t dst_dma_buf[1024] = {0}; |
В функции main() удалим инициализацию переменной и поднятие ножки
i=0;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
Наполним буфер-источник данными
1 2 3 4 5 6 |
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); //fill src buffer for(i=0;i<1024;i++) { src_dma_buf[i] = 1024-i; } |
Удалим вот это
Number_7219(87654321);
HAL_Delay(2000);
Подключим строчную библиотеку для работы функции memcpy
1 2 |
/* USER CODE BEGIN Includes */ #include <string.h> |
Вернёмся в main() и скопируем содержимое буфера-приёмника в буфер-источник после вызова функции инициализации индикатора пока с помощью стандартного копирования без использования DMA
1 2 |
Init_7219(); memcpy(dst_dma_buf,src_dma_buf,2048); |
2048, а не 1024 потому, что функция исчисляет объём в байтах, а у нас полуслова.
В бесконечном цикле удалим весь пользовательский код.
Покажем постепенно содержимое нашего приёмного буфера
1 2 3 4 5 6 |
memcpy(dst_dma_buf,src_dma_buf,2048); for(i=0;i<1024;i++) { NumberR_7219(dst_dma_buf[i]); HAL_Delay(100); } |
Подключим нашу схему. Схему возьмём ту же, как и в уроке 153 для ведомого устройства
Соберём код, прошьём контроллер, и мы увидим, как у нас появится число 1024 и начнёт постепенно декрементироваться
Если немного подождать, то счётчик наш досчитает в обратную сторону до 1.
Можно будет, зайдя в отладку посчитать примерно время, за которое наш буфер скопировался в приёмник.
Поставим одну точку останова на функции копирования, а другую — на следующей строке
Запустим отладку и запомним время
Возобновим выполнение проекта и посмотрим изменённое время
Посчитаем разницу — будет 0.0005667 или 566.7 микросекунд. Запомним эту цифру.
Теперь попробуем передать данные посредством DMA.
Закомментируем функцию копирования, скопируем данные с помощью DMA
1 2 |
//memcpy(dst_dma_buf,src_dma_buf,2048); HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)&src_dma_buf, (uint32_t)&dst_dma_buf, 1024); |
Если мы сейчас запустим код на выполнение, то у нас всё будет нормально работать, так как у нас буфер небольшой и пока мы выведем первое число на индикатор и в момент задержки 100 милисекунд у нас данные скопируются точно. Но вообще мы должны подождать, так как процесс копирования данных у нас теперь идёт независимо от выполнения нашего кода исходя из определения технологии DMA. Поэтому мы должны отследить окончание этого процесса. Для этого мы сначала объявим переменную для флага окончания передачи.
1 2 |
uint16_t dst_dma_buf[1024] = {0}; uint8_t fl=0; |
Добавим функцию-обработчик для прерывания по флагу окончания передачи данных, в которой установим наш флаг
1 2 3 4 5 |
/* USER CODE BEGIN 4 */ static void DMA1_Channel1_TransferComplete(DMA_HandleTypeDef *DmaHandle) { fl = 1; } |
Добавим прототип для этой функции
1 2 |
/* USER CODE BEGIN PFP */ static void DMA1_Channel1_TransferComplete(DMA_HandleTypeDef *DmaHandle); |
Затем мы вернёмся в функцию main() и зарегистрируем нашу функцию
1 2 |
/* USER CODE BEGIN 2 */ HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, DMA1_Channel1_TransferComplete); |
Теперь перед выводом на индикатор скопированных данных подождём установки нашего флага
1 2 |
HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)&src_dma_buf, (uint32_t)&dst_dma_buf, 1024); while (!fl) {} |
Вот теперь всё правильно. Соберём наш код, прошьём контроллер. Результат будет тот же самый, что и при копировании с помощью функции memcpy.
Оценим приблизительную скорость копирования. Для этого одну точку останова поставим на функцию копирования
А вторую точку поставим уже в обработчике, так как именно в этот момент у нас заканчивается процесс копирования
Теперь запустим отладку и запомним также время на первой точке
Возобновим процесс и запомнив время на точке в обработчике
Разница получится 0,0006974 или 697,4 микросекунды, что даже несколько медленнее, чем при использовании обычного копирования, но зато в эти 700 микросекунд у нас спокойно может выполняться любой код, будь то какая-то обработка или ещё что-то. Вот в этом-то и прелесть DMA.
Таким образом, на данном уроке мы немного подробнее познакомились с организацией технологии DMA в контроллере STM32F1, а также попрактиковались в копировании с помощью данной периферии данных из одного участка памяти в другой.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Индикатор светодиодный семиразрядный с драйвером MAX7219
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий