Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2743
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать

Управление DMA

264

9.3. Использование CubeMX для конфигурации запросов к DMA

CubeMX может снизить до минимума усилия, необходимые для конфигурации запросов канала/потока. После включения периферийного устройства в разделе Pinout перейдите в раздел Configuration и нажмите кнопку DMA. Появится диалоговое окно DMA Configuration, как показано на рисунке 8.

Рисунок 8: Диалоговое окно DMA Configuration в CubeMX

Диалоговое окно содержит две или три вкладки (в зависимости от количества контроллеров DMA, предоставляемых вашим микроконтроллером). Первые два относятся к периферийным запросам. Например, если вы хотите разрешить запрос к DMA для USART2 в режиме передачи (для передачи типа память-в-периферию), нажмите кнопку Add и выберите запись USART2_TX. CubeMX автоматически заполнит оставшиеся поля для вас, выбрав правильный канал. Затем вы можете назначить приоритет запросу и установить другие параметры, такие как режим работы DMA, инкрементирование адресов периферийных устройств/памяти и т. д. После завершения нажмите кнопку OK. Таким же образом можно сконфигурировать каналы/потоки DMA для передач типа память-в-па-

мять.

CubeMX автоматически сгенерирует правильный код инициализации для используе-

мых каналов в файле stm32xxxx_hal_msp.c.

9.4. Правильное выделение памяти буферам

DMA

Если вы посмотрите на исходный код всех примеров, представленных в данной главе, вы увидите, что буферы DMA (то есть массивы как источника, так и пункта назначения, используемые для передач типа память-в-периферию и периферия-в-память) всегда выделяется в глобальной области. Почему мы так делаем?

Управление DMA

265

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

Рисунок 9: Разница между переменной, размещенной локальной и глобальной областях

На рисунке 9 четко показана разница между локальной переменной (lbuf) и глобальной (gbuf). lbuf будет активна, пока func1() находится в стеке.

Если вы хотите избежать глобальных переменных в вашем приложении, другое решение представляется ее декларированием в качестве static. Как мы увидим в Главе 20, статические переменные static автоматически размещаются в области .data (область глобальных данных на рисунке 9), несмотря на то что их «видимость» ограничена локальной областью.

9.5.Пример из практики: анализ производительности передачи типа

память-в-память модулем DMA

Контроллер DMA может также использоваться для передач типа память-в-память17. Например, его можно использовать для перемещения большого массива данных из Flash-памяти в SRAM, или для копирования массивов в SRAM, или для обнуления области памяти. Библиотека Си обычно предоставляет набор функций для выполнения этой

17 Помните, что в микроконтроллерах STM32F2/F4/F7 для этого типа передач может использоваться только

DMA2.

Управление DMA

266

задачи. memcpy() и memset() являются наиболее распространенными из них. Занимаясь серфингом в Интернете, вы можете найти несколько тестов, которые сравнивают производительность между процедурами memcpy()/memset() и передачами через DMA. Большинство этих тестов утверждают, что обычно DMA намного медленнее, чем ядро Cortex- M. Правда ли это? Ответ таков: зависит от обстоятельств. Тогда зачем вам использовать DMA, если у вас уже есть эти процедуры?

История этих тестов намного сложнее, и она включает в себя несколько факторов, таких как выравнивание памяти, используемая библиотека Си и правильные параметры DMA. Рассмотрим следующее тестовое приложение (код предназначен для работы на микроконтроллере STM32F4), разделенное на несколько этапов:

Имя файла: src/mem2mem.c

12 DMA_HandleTypeDef hdma_memtomem_dma2_stream0; 13

14const uint8_t flashData[] = {0xe7, 0x49, 0x9b, 0xdb, 0x30, 0x5a, ...};

15uint8_t sramData[1000];

16

17int main(void) {

18HAL_Init();

19Nucleo_BSP_Init();

21hdma_memtomem_dma2_stream0.Instance = DMA2_Stream0;

22hdma_memtomem_dma2_stream0.Init.Channel = DMA_CHANNEL_0;

23hdma_memtomem_dma2_stream0.Init.Direction = DMA_MEMORY_TO_MEMORY;

24hdma_memtomem_dma2_stream0.Init.PeriphInc = DMA_PINC_ENABLE;

25hdma_memtomem_dma2_stream0.Init.MemInc = DMA_MINC_ENABLE;

26hdma_memtomem_dma2_stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;

27hdma_memtomem_dma2_stream0.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;

28hdma_memtomem_dma2_stream0.Init.Mode = DMA_NORMAL;

29hdma_memtomem_dma2_stream0.Init.Priority = DMA_PRIORITY_LOW;

30hdma_memtomem_dma2_stream0.Init.FIFOMode = DMA_FIFOMODE_ENABLE;

31hdma_memtomem_dma2_stream0.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;

32hdma_memtomem_dma2_stream0.Init.MemBurst = DMA_MBURST_SINGLE;

33hdma_memtomem_dma2_stream0.Init.PeriphBurst = DMA_MBURST_SINGLE;

34 HAL_DMA_Init(&hdma_memtomem_dma2_stream0); 35

36GPIOC->ODR = 0x100;

37HAL_DMA_Start(&hdma_memtomem_dma2_stream0, (uint32_t)&flashData, (uint32_t)sramData, 1000);

38HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

39GPIOC->ODR = 0x0;

40

 

41

while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin));

42

 

43hdma_memtomem_dma2_stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;

44hdma_memtomem_dma2_stream0.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;

45

46 HAL_DMA_Init(&hdma_memtomem_dma2_stream0); 47

48GPIOC->ODR = 0x100;

49HAL_DMA_Start(&hdma_memtomem_dma2_stream0, (uint32_t)&flashData, (uint32_t)sramData, 250);

Управление DMA

267

50HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

51GPIOC->ODR = 0x0;

52

53 HAL_Delay(1000); /* Это довольно примитивная форма борьбы с дребезгом */

54

55 while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin)); 56

57GPIOC->ODR = 0x100;

58memcpy(sramData, flashData, 1000);

59GPIOC->ODR = 0x0;

Здесь у нас есть два довольно больших массива. Один из них, flashData, размещается во Flash-памяти благодаря модификатору const18. Мы хотим скопировать его содержимое в массив sramData, который хранится в SRAM, как следует из названия, и мы хотим проверить, сколько времени это займет, используя DMA и функцию memcpy().

Сначала мы начнем тестирование DMA. Дескриптор hdma_memtomem_dma2_stream0 используется для конфигурации пары поток 0/канал 0 контроллера DMA2 для выполнения передачи типа память-в-память. На первом этапе мы конфигурируем поток DMA для выполнения передачи памяти с выравниванием по байтам. Как только конфигурация DMA завершена, мы начинаем передачу. Используя осциллограф, подключенный к выводу PC8 платы Nucleo, мы можем измерить, сколько времени займет передача. Нажатие кнопки USER платы Nucleo (подключенной к PC13) вызывает запуск другого этапа тестирования. На этот раз мы конфигурируем DMA так, чтобы выполнялась передача с выравниванием по словам. Наконец, в строке 58 мы проверяем, сколько времени занимает копирование массива с помощью memcpy().

В таблице 13 показаны результаты, полученные для каждой платы Nucleo. Давайте сосредоточимся на плате Nucleo-F401RE. Как видите, передача через DMA типа M2M (па- мять-в-память) с выравниванием по байтам занимает 42 мкс, а передача через DMA типа M2M с выравниванием по словам занимает 14 мкс. Это большой прирост скорости, который доказывает, что использование правильной конфигурации DMA может дать нам лучшую производительность передачи, так как мы перемещаем 4 байта одновременно при каждой транзакции через DMA. А что насчет memcpy()? Как видно из таблицы 13, скорость зависит от используемой библиотеки Си. Используемый нами инструментарий GCC предоставляет две библиотеки среды выполнения Си: одна называется newlib, а другая – newlib-nano. Первая из них является наиболее полной и оптимизированной по скорости, а вторая – облегченной версией. Функция memcpy() в библиотеке newlib предназначена для обеспечения максимальной скорости копирования за счет размера кода. Она автоматически обнаруживает передачи с выравниванием по словам, и ее передача равна передаче через DMA типа M2M с выравниванием по слову. Таким образом, она намного быстрее, чем передача через DMA типа M2M с выравниванием по байтам, и именно поэтому кто-то утверждает, что memcpy() всегда быстрее, чем DMA. С другой стороны, и ядро Cortex-M, и DMA должны получать доступ к Flash-памяти и к

18 Причина, по которой это происходит, будет объяснена в Главе 20.

Управление DMA

268

памяти SRAM с использованием одной и той же шины. Так что нет причин, по которым ядро должно быть быстрее, чем DMA19.

Таблица 13: Результаты тестовой передачи типа М2М

Как видите, самая высокая скорость передачи достигается, когда поток/канал DMA отключает внутренний буфер FIFO ( 12 мкс). Важно отметить, что для микроконтроллеров STM32 с меньшим объемом Flash-памяти newlib-nano – почти неизбежный выбор, если код не помещается во Flash-память. Но опять же, используя правильные параметры DMA, мы можем достичь той же производительности, что и у библиотеки newlib – версии, оптимизированной по скорости.

Последнее, что мы должны проанализировать – это последний столбец в таблице 13. Он показывает, сколько времени занимает передача памяти с использованием простого цикла, подобного следующему:

...

GPIOC->ODR = 0x100;

for(int i = 0; i < 1000; i++) sramData[i] = flashData[i];

GPIOC->ODR = 0x0;

...

19 Здесь я явно исключаю некоторые «привилегированные пути» между ядром Cortex-M и SRAM. Эта роль принадлежит памяти, связанной с ядром (Core-Coupled Memory, CCM) – функции, доступной в некоторых микроконтроллерах STM32, которую мы рассмотрим подробнее в Главе 20.

Управление DMA

 

 

269

Как видите, с максимальным уровнем оптимизации (-O3) требуется точно такое же

время, как и у memcpy(). Почему так происходит?

 

...

 

 

 

 

GPIOC->ODR = 0x100;

 

 

 

8001968:

f44f 7380

mov.w

r3, #256

; 0x100

800196c:

6163

str

r3, [r4, #20]

 

800196e:

4807

ldr

r0, [pc, #28]

; (800198c <main+0x130>)

8001970:

4907

ldr

r1, [pc, #28]

; (8001990 <main+0x134>)

8001972:

f44f 727a

mov.w

r2, #1000

; 0x3e8

8001976:

f000 f92d

bl

8001bd4 <memcpy>

 

for(int

i = 0; i < 1000; i++)

 

 

 

 

sramData[i] = flashData[i];

 

 

GPIOC->ODR = 0x0;

 

 

 

800197a:

6165

str

r5, [r4, #20]

 

...

 

 

 

 

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

Таблица 13 показывает еще один интересный результат. Для микроконтроллера STM32F152RE функция memcpy() в newlib всегда в два раза быстрее, чем DMA M2M. Я не знаю, почему это происходит, но я выполнил несколько тестов, и могу подтвердить результат.

Наконец, другие тесты, о которых здесь не сообщается, показывают, что удобно использовать DMA для передачи типа M2M, когда массив содержит более 30-50 элементов, в противном случае затраты на настройку DMA перевешивают преимущества, связанные с его использованием. Тем не менее, важно отметить, что другое преимущество в использовании передачи через DMA типа M2M состоит в том, что ЦПУ свободен для выполнения других задач, пока DMA выполняет передачу, несмотря на то что его доступ к шине замедляет общую производительность DMA.

Как переключиться на библиотеку среды выполнения newlib? Это легко сделать в Eclipse, перейдя в настройки проекта (в меню Project → Properties), затем перейдя в раздел

C/C++ Build → Settings и выбрав пункт Miscellaneous в разделе Cross ARM C++ Linker. Снятие флажка с пункта Use newlib-nano (см. рисунок 10) автоматически приведет к тому, что последний бинарный файл будет связан с библиотекой newlib.

Управление DMA

270

Рисунок 10: Как выбрать библиотеку среды выполнения newlib/newlib-nano