В предыдущей части нашего урока мы познакомились с реализацией передачи данных через 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.
Всем спасибо за внимание!
Предыдущая часть Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Индикатор светодиодный семиразрядный с драйвером MAX7219
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий