STM Урок 156. LL. DMA. MEM2MEM. Часть 2
В предыдущей части нашего урока мы познакомились с реализацией передачи данных через DMA в контроллере STM32F1 в режиме MEM2MEM и познакомились с регистрами DMA и со всеми их битами.
Схема наша не изменилась, осталась с урока 155
Вернёмся к нашему проекту.
Давайте для начала посмотрим, как происходит процесс инициализации DMA и включения режима MEM2MEM.
Зайдём в тело функции MX_DMA_Init в файле main.c.
Сначала заводится тактирование периферии.
Затем задаётся направление передачи данных
|
1 |
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_MEMORY_TO_MEMORY); |
Здесь устанавливается бит 14 регистра CCR1 MEM2MEM, которым и задаётся режим передачи из памяти в память, а бит 4 DIR остаётся сброшенным.
Далее устанавливается приоритет канала
|
1 |
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_LOW); |
В нашем случае все биты поля PL сброшены, приоритет у нас низкий.
Затем устанавливается режим передачи данных. В нашем случае обычный, а не циклический
|
1 |
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL); |
Потом задаём режим автоинкрементирования адреса и источника и приёмника, включая тем самым биты PINC и MINC регистра CCR1
|
1 2 3 4 5 |
/* Set peripheral increment mode */ LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_INCREMENT); /* Set memory increment mode */ LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT); |
Далее задаём ширину данных или размерность источника при помощи установки определённых битов битового поля PSIZE, в нашем случае настраиваем на 16 бит
|
1 |
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_HALFWORD); |
Аналогичным образом задаётся ширина данных приёмника с помощью битового поля MSIZE
|
1 |
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_HALFWORD); |
Затем настраиваются приоритет и субприоритет прерываний DMA, но это уже другая история. О менеджере NVIC будет, я думаю, отдельный разговор.
Вот, в принципе, и вся инициализация DMA.
Добавим глобальные буферы и флаг
|
1 2 3 4 |
/* USER CODE BEGIN PV */ uint16_t src_dma_buf[1024] = {0}; uint16_t dst_dma_buf[1024] = {0}; uint8_t fl=0; |
В функции main() удалим объявление лишней локальной переменной, оставив только i.
uint16_t i, r;
Удалим также и её инициализацию
i=0; r=0;
Удалим установку высокого уровня ножки порта PA4
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4);
Удалим включение SPI1
LL_SPI_Enable(SPI1);
Удалим вот это
Number_7219(87654321);
LL_mDelay(2000);
Очистим табло индикатора
|
1 2 |
Init_7219(); Clear_7219(); |
Заполним приёмный буфер декрементирующимися от 1024 до 1 значениями
|
1 2 3 4 5 6 |
Clear_7219(); //fill src buffer for(i=0;i<1024;i++) { src_dma_buf[i] = 1024-i; } |
Подключим строковую библиотеку
|
1 2 |
#include "max7219.h" #include <string.h> |
Вернёмся в main() и скопируем из буфера-источника в буфер-приёмник наши данные пока с помощью обычного memcpy
|
1 2 3 |
src_dma_buf[i] = 1024-i; } memcpy(dst_dma_buf,src_dma_buf,2048); |
Так как у нас полуслова, то байтов будет вдвое больше (2048).
Отобразим скопированные данные из буфера-приёмника попеременно на индикаторе
|
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]); LL_mDelay(10); } |
Удалим весь пользовательский код из бесконечного цикла, соберём код, прошьём контроллер и посмотрим результат

Если немного подождать, то счётчик наш досчитает в обратную сторону до 1.
Можно будет, зайдя в отладку, посчитать примерно время, за которое наш буфер скопировался в приёмник.
Поставим одну точку останова на функции копирования, а другую — на следующей строке

Запустим отладку и запомним время
![]()
Возобновим выполнение проекта и посмотрим изменённое время
![]()
Посчитаем разницу — будет 0.0005671 или 567.1 микросекунд. Запомним эту цифру.
Теперь попробуем передать данные посредством DMA.
Закомментируем пока функцию копирования
//memcpy(dst_dma_buf,src_dma_buf,2048);
Сначала отключим канал с помощью специальной функции библиотеки LL
|
1 2 3 |
src_dma_buf[i] = 1024-i; } LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1); |
Данная функция сбросит бит EN в регистре CCR1
Сбросим флаги TE и TC
|
1 2 3 |
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1); LL_DMA_ClearFlag_TC1(DMA1); LL_DMA_ClearFlag_TE1(DMA1); |
Зададим количество передаваемых данных
|
1 2 |
LL_DMA_ClearFlag_TE1(DMA1); LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, 1024); |
При помощи функции LL_DMA_SetDataLength заносится значение количества передаваемых данных в регистр CNDTR канала.
С помощью следующей функции сконфигурируем адреса источника и приёмника, занеся соответствующие адреса буферов в регистры CPAR и CMAR канала
|
1 2 |
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, 1024); LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, (uint32_t)&src_dma_buf, (uint32_t)&dst_dma_buf, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1)); |
Включим прерывания по ошибке передаче и по полному окончанию передачи данных
|
1 2 3 |
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, (uint32_t)&src_dma_buf, (uint32_t)&dst_dma_buf, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1)); LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_1); |
Включим канал, тем самым дав команду DMA к началу копирования данных
|
1 2 |
LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); |
В данном случае у нас включится бит EN в регистре CCR1.
Дальнейшая задача — обработать прерывание окончании передачи и выставить флаг fl. Также при возникновении прерываний мы обязаны сбросить флаги прерываний.
Добавим функцию-обработчик прерывания окончания передачи, в которой сбросим все флаги прерываний с помощью функции LL_DMA_ClearFlag_GI1 и установим флаг fl
|
1 2 3 4 5 6 |
/* USER CODE BEGIN 4 */ void DMA1_Channel1_TransferComplete(void) { LL_DMA_ClearFlag_GI1(DMA1); fl = 1; } |
В файле stm32f1xx_it.c добавим на данную функцию прототип
|
1 2 |
/* USER CODE BEGIN PFP */ void DMA1_Channel1_TransferComplete(void); |
Вызовем её в соответствующем обработчике, узнав, что у нас установился нужный флаг
|
1 2 3 4 5 |
/* USER CODE BEGIN DMA1_Channel1_IRQn 0 */ if(LL_DMA_IsActiveFlag_TC1(DMA1) == 1) { DMA1_Channel1_TransferComplete(); } |
В случаем установки флага ошибки передачи сбросим нужный флаг прерывания
|
1 2 3 4 5 6 |
DMA1_Channel1_TransferComplete(); } else if(LL_DMA_IsActiveFlag_TE1(DMA1) == 1) { LL_DMA_IsActiveFlag_TE1(DMA1); } |
В функции main() файла main.c дождёмся установки нашего флага и сбросим его тоже
|
1 2 3 |
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); while (!fl) {} fl=0; |
Запустим код на выполнение, убедившись, что у нас также всё скопировалось. Для полноты эксперимента лучше отключить питание контроллера после прошивки и затем включив его снова.
Зайдя в отладку, удалим наши точки останова ибо они все передвинулись, и добавим новые.
Одну добавим на строчку с вызовом самой первой функции, где мы отключили канал. Считать надо время всей процедуры
Вторую точку ставим в обработчик прерывания. Именно там у нас закончится процесс копирования

Запустим отладку и посмотрим время первой остановки
![]()
Продолжим отладку и посмотрим время второй остановки
![]()
Посчитаем разницу. Она составит 744.3 микросекунды, что процентов на 30 больше, чем копирование с помощью memcpy.
Но, как мы знаем, основной смысл DMA не в скорости, а в том, что в то время, когда происходит процесс копирования данных, процессор свободен и может заниматься всем, чем угодно.
Итак, на данном занятии мы ещё глубже познакомились с аппаратной реализацией периферии DMA в контроллере STM32F1, а также на практике подтвердили свои знания, воспользовавшись при этом функционалом библиотеки LL.
Всем спасибо за внимание!
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Индикатор светодиодный семиразрядный с драйвером MAX7219
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)




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