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

Управление DMA

255

9.2.3. DMA_HandleTypeDef в HAL для L0/L4

Так как микроконтроллеры STM32L0/L4 принимают другую терминологию для указания пары поток/канал (они принимают терминологию канал/запрос), DMA_HandleTypeDef отражает эту разницу в своих HAL. Тем не менее, мы не будем повторять здесь полный рассказ. Нужно иметь в виду только три момента:

Поле DMA_HandleTypeDef.Instance – это номер канала, и он может принимать зна-

чения DMA1_Channel1..DMA1_Channel7;

Поле DMA_HandleTypeDef.Init.Request является линией запроса, и оно может при-

нимать значения DMA_REQUEST_0..DMA_REQUEST_11;

эта реализация DMA не поддерживает режим FIFO и режим пакетной передачи.

9.2.4. Как выполнять передачи в режиме опроса

После того, как мы сконфигурировали канал/поток DMA, нам нужно сделать несколько других манипуляций:

сконфигурировать адреса памяти и периферийного порта;

указать объем данных, которые мы собираемся передать;

подготовить контроллер DMA;

включить режим DMA в соответствующем периферийном устройстве.

HAL абстрагирует первые три пункта, используя

HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);

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

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

UART2 конфигурируется с использованием модуля HAL_UART, как мы видели в предыдущей главе.

Канал DMA1 (или пара канал/поток DMA1 для плат Nucleo на основе STM32F4) сконфигурирован на передачу типа память-в-периферию (см. таблицу 12).

Соответствующий канал готов к выполнению передачи, и UART включен в режиме DMA.

Управление DMA

256

Таблица 12: Как каналы DMA USART_TX/USART_RX отображаются в микроконтроллерах, оснащающих платы Nucleo

В следующем примере, который предназначен для работы на Nucleo-F030 (см. примеры книги для других плат Nucleo), показано, как легко это сделать.

Имя файла: src/main-ex1.c

43MX_DMA_Init();

44MX_USART2_UART_Init();

46hdma_usart2_tx.Instance = DMA1_Channel4;

47hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;

48hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;

49hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;

50hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;

51hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;

52hdma_usart2_tx.Init.Mode = DMA_NORMAL;

53hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;

54

HAL_DMA_Init(&hdma_usart2_tx);

55

 

56HAL_DMA_Start(&hdma_usart2_tx, (uint32_t)msg, (uint32_t)&huart2.Instance->TDR, \

57strlen(msg));

58// Включение UART в режиме DMA

59huart2.Instance->CR3 |= USART_CR3_DMAT;

60// Ожидание завершения передачи

61HAL_DMA_PollForTransfer(&hdma_usart2_tx, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

62// Отключение режима DMA в UART

63huart2.Instance->CR3 &= ~USART_CR3_DMAT;

64// Включение LD2

65HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);

Управление DMA

257

Переменная hdma_usart2_tx является экземпляром структуры DMA_HandleTypeDef, замеченной ранее. Здесь мы конфигурируем DMA1_Channel4 для выполнения передачи типа па- мять-в-периферию. Поскольку периферийное устройство USART имеет регистр передачи данных (Transmit Data Register, TDR) размером в один байт, мы конфигурируем DMA таким образом, чтобы периферийный адрес не увеличивался автоматически (DMA_PINC_DISABLE), в то же время мы хотим, чтобы адрес памяти источника автоматически увеличивался после каждой отправки байта (DMA_MINC_ENABLE). Как только конфигурация завершена, мы вызываем HAL_DMA_Init(), которая выполняет конфигурацию интерфейса DMA в соответствии с информацией, предоставленной в структуре hdma_usart2_tx.Init. Далее в строке 56 мы вызываем процедуру HAL_DMA_Start(), которая конфигурирует адрес памяти источника (то есть адрес массива msg), периферийный адрес пункта назначения (то есть адрес регистра USART2->TDR) и количество данных, которые мы собираемся передать. Теперь DMA готов к отправке, и мы начинаем передачу, устанавливая соответствующий бит периферийного устройства USART2, как показано в строке 59. Наконец, обратите внимание, что функция MX_DMA_Init() (вызывается в строке 43) использует макрос __HAL_RCC_DMA1_CLK_ENABLE() для включения контроллера DMA1 (помните, что почти каждый внутренний модуль STM32 должен быть включен с помо-

щью макроса __HAL_RCC_<ПЕРИФЕРИЙНОЕ_УСТРОЙСТВО>_CLK_ENABLE()).

Поскольку мы не знаем, сколько времени потребуется, чтобы завершить процедуру передачи, мы используем функцию:

HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma,

uint32_t CompleteLevel, uint32_t Timeout);

которая автоматически ожидает полного завершения передачи. Данный способ отправки данных в режиме DMA называется «режим опроса» в официальной документации ST. Как только передача завершена, мы отключаем режим DMA у UART2 и включаем светодиод LD2.

9.2.5. Как выполнять передачи в режиме прерываний

С точки зрения производительности передача через DMA в режиме опроса не имеет смысла, если только нашему коду не нужно ждать завершения передачи. Если наша цель состоит в том, чтобы улучшить общую производительность, нет никаких причин использовать контроллер DMA, при этом затрачивая много тактовых циклов ЦПУ, ожидающих завершения передачи. Таким образом, лучший вариант – подготовить DMA и позволить ему уведомить нас о завершении передачи. DMA может генерировать прерывания, связанные с действиями канала (например, DMA1 в микроконтроллере STM32F030 имеет один IRQ для канала 1, один – для каналов 2 и 3 и один – для каналов 4 и 5). Кроме того, доступны три независимых бита разрешения для разрешения IRQ

при половинной передаче, полной передаче и ошибке передачи.

DMA можно включить в режиме прерываний, выполнив следующие действия:

определить три функции, выступающие в качестве процедур обратного вызова,

и передать их указателям на функции XferCpltCallback, XferHalfCpltCallback и XferErrorCallback в дескрипторе DMA_HandleTypeDef (это в порядке вещей – опре-

делять только те функции, которые нам интересны, при этом надо установить соответствующий указатель на NULL, в противном случае могут возникнуть странные ошибки);

Управление DMA

258

написать ISR для IRQ, связанного с каналом, который вы используете, и выполнить вызов HAL_DMA_IRQHandler(), передав указатель на дескриптор

DMA_HandleTypeDef;

разрешить соответствующий IRQ в контроллере NVIC;

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

HAL_DMA_Start().

«Пуристы производительности» будут разочарованы тем, как HAL управляет прерываниями DMA. Фактически, он разрешает по умолчанию все доступные IRQ для используемого канала, даже если мы не заинтересованы в некоторых из них (например, мы можем не интересоваться захватом прерывания при половинной передаче). Если производительность для вас принципиальна, взгляните на код HAL_DMA_Start_IT() и измените его в соответствии с вашими потребностями. К сожалению, ST решила спроектировать HAL таким образом, чтобы он абстрагировал многие детали от пользователя за счет скорости.

Важно отметить кое-что об обратных вызовах XferCpltCallback, XferHalfCpltCallback и XferErrorCallback: их нужно устанавливать, когда мы используем DMA без посредничества CubeHAL. Разъясним эту концепцию.

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

используем процедуры HAL_UART_Trasmit_DMA()/HAL_UART_Receive_DMA(), то HAL

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

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

Имя файла: src/main-ex2.c

47hdma_usart2_tx.Instance = DMA1_Channel4;

48hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;

49hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;

50hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;

51hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;

52hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;

53hdma_usart2_tx.Init.Mode = DMA_NORMAL;

54hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;

55hdma_usart2_tx.XferCpltCallback = &DMATransferComplete;

56HAL_DMA_Init(&hdma_usart2_tx);

57

58/* Инициализация прерываний DMA */

59HAL_NVIC_SetPriority(DMA1_Channel4_5_IRQn, 0, 0);

60HAL_NVIC_EnableIRQ(DMA1_Channel4_5_IRQn);

61

 

Управление DMA

259

62

HAL_DMA_Start_IT(&hdma_usart2_tx, (uint32_t)msg,

\

63

(uint32_t)&huart2.Instance->TDR,

strlen(msg));

64// Включение UART в режиме DMA

65huart2.Instance->CR3 |= USART_CR3_DMAT;

67/* Бесконечный цикл */

68while (1);

69}

70

71void DMATransferComplete(DMA_HandleTypeDef *hdma) {

72if(hdma->Instance == DMA1_Channel4) {

73// Отключение режима DMA в UART

74huart2.Instance->CR3 &= ~USART_CR3_DMAT;

75// Включение LD2

76HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);

77}

9.2.6.Как выполнять передачи типа периферия-в-периферию

Официальная документация ST подробно описывает передачи типа периферия-в-перифе- рию с использованием DMA в микроконтроллерах F0/F1/F3/L0/L1/L4. Но, глядя на справочные руководства и демонстрационные проекты, представленные в CubeHAL, невозможно найти какой-либо вразумительный пример того, как использовать эту функцию. Даже в Интернете (и на официальном форуме ST) нет никаких подсказок о том, как ее использовать. Во-первых, я подумал, что очевидным следствием является тот факт, что периферийные устройства – это память, отображаемая в адресном пространстве 4 ГБ. Тогда передача типа периферия-в-периферию будет просто частным случаем передачи типа периферия-в-память. Вместо этого, выполнив некоторые тесты, я пришел к выводу, что эта функция требует, чтобы DMA был специально спроектирован для обеспечения запуска передач между различными периферийными устройствами. Проводя некоторые эксперименты, я обнаружил, что в микроконтроллерах F2/F4/F7/L1/L4 только контроллер DMA2 имеет полный доступ к матричной шине, и он является единственным (вместе с ядром Cortex), который может выполнять передачи типа периферия-в-перифе-

рию.

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

Имя файла: src/main-ex3.c

45hdma_usart2_rx.Instance = DMA1_Channel5;

46hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;

47hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;

48hdma_usart2_rx.Init.MemInc = DMA_MINC_DISABLE;

49hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;

50hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;

51hdma_usart2_rx.Init.Mode = DMA_CIRCULAR;

14 Пример предназначен для запуска на Nucleo-F030. Для плат Nucleo, основанных на микроконтроллерах F2/F4/L1/L4, пример разработан для работы с UART1, чьи запросы к DMA связаны с DMA2.

Управление DMA

260

52hdma_usart2_rx.Init.Priority = DMA_PRIORITY_LOW;

53HAL_DMA_Init(&hdma_usart2_rx);

54

55 __HAL_RCC_DMA1_CLK_ENABLE(); 56

57HAL_DMA_Start(&hdma_usart2_rx, (uint32_t)&huart2.Instance->RDR, (uint32_t)&GPIOA->ODR, 1);

58// Включение UART в режиме DMA

59huart2.Instance->CR3 |= USART_CR3_DMAR;

На этот раз мы конфигурируем канал на передачу типа периферия-в-память, не увеличивая ни адрес источника – периферийного регистра (регистр данных UART), ни целевую ячейку памяти, которая в нашем случае является адресом регистра GPIOA->ODR. В конечном счете, канал сконфигурирован на работу в циклическом режиме: это приведет к тому, что все байты, передаваемые по UART, будут постоянно храниться в регистре

GPIOA->ODR.

Чтобы проверить пример, мы можем просто использовать следующий Python-скрипт:

Имя файла: src/uartsend.py

1#!/usr/bin/env python

2import serial, time

4SERIAL_PORT = "/dev/tty.usbmodem1a1213" #Пользователи Windows, замените на "COMx"

5ser = serial.Serial(SERIAL_PORT, 38400)

7while True:

8ser.write((0xff,))

9time.sleep(0.05)

10ser.write((0,))

11time.sleep(0.05)

13ser.close()

Этот код действительно говорит сам за себя. Мы используем модуль pyserial, чтобы открыть новое последовательное соединение в VCP Nucleo. Затем мы запускаем бесконечный цикл, который отправляет байты 0xFF и 0x0 поочередно. Это приведет к тому, что GPIOA->ODR примет одно и то же значение (то есть, первые восемь I/O будут поочередно становиться ВЫСОКИМИ и НИЗКИМИ), а светодиод LD2 платы Nucleo будет мигать. Как видите, ядро Cortex-M ничего не знает о том, что происходит между UART2 и перифе-

рией GPIOA.

9.2.7.Использование модуля HAL_UART для передачи в режиме

DMA

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

Модуль HAL_UART разработан для абстрагирования от всех базовых аппаратных подробностей. Необходимые для его использования шаги, следующие:

Управление DMA

261

сконфигурировать канал/поток DMA, подключенный к UART, который вы собираетесь использовать, как было показано в данной главе;

связать UART_HandleTypeDef с DMA_HandleTypeDef, используя __HAL_LINKDMA();

разрешить прерывание DMA, связанное с используемым вами каналом/потоком, и вызвать процедуру HAL_DMA_IRQHandler() из соответствующей ISR;

разрешить прерывание, связанное с UART, и вызвать процедуру

HAL_UART_IRQHandler() из соответствующей ISR (это действительно важно, не пропустите этот шаг15);

Использовать функции HAL_UART_Transmit_DMA() и HAL_UART_Receive_DMA() для об-

мена данными по UART и быть готовым получить уведомление о завершении

передачи с помощью HAL_UART_RxCpltCallback().

Следующий код показывает, как получить три байта от UART2 в режиме DMA в микроконтроллере STM32F03016:

uint8_t dataArrived = 0;

int main(void) { HAL_Init();

Nucleo_BSP_Init(); //Конфигурация UART2

//Конфигурация канала 5 DMA1, который подключен к линии запроса UART2_RX hdma_usart2_rx.Instance = DMA1_Channel5;

hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart2_rx.Init.Mode = DMA_NORMAL; hdma_usart2_rx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_usart2_rx);

// Связывание дескриптора DMA с дескриптором UART2

__HAL_LINKDMA(&huart, hdmarx, hdma_usart2_rx);

/* Инициализация прерываний DMA */

HAL_NVIC_SetPriority(DMA1_Channel4_5_IRQn, 0, 0);

HAL_NVIC_EnableIRQ(DMA1_Channel4_5_IRQn);

/* Инициализация прерываний периферии */

HAL_NVIC_SetPriority(USART2_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART2_IRQn);

// Получение трех байт от UART2 в режиме DMA

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

16Организация кода инициализации DMA для других микроконтроллеров STM32 оставлена читателю в качестве упражнения.

Управление DMA

262

uint8_t data[3]; HAL_UART_Receive_DMA(&huart2, &data, 3);

while(!dataArrived); //Ожидание прибытия данных от UART

/* Бесконечный цикл */ while (1);

}

//Этот обратный вызов автоматически вызывается HAL,когда передача через DMA завершена void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {

dataArrived = 1;

}

void DMA1_Channel4_5_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart2_rx); //Это автоматически вызовет \

HAL_UART_RxCpltCallback()

}

Где именно вызывается HAL_UART_RxCpltCallback()? В предыдущих параграфах мы видели, что DMA_HandleTypeDef содержит указатель (названный XferCpltCallback) на функцию, которая вызывается процедурой HAL_DMA_IRQHandler(), когда передача через DMA была завершена. Однако, когда мы используем модуль HAL для выбранного периферийного устройства (в данном случае HAL_UART), нам не нужно предоставлять наши собственные обратные вызовы: они определяются внутри HAL, который использует их для выполнения своих действий. HAL предоставляет нам возможность определить наши соответствующие функции обратного вызова (HAL_UART_RxCpltCallback() для передач UART_RX в режиме DMA), которые будут автоматически вызываться HAL, как показано на рисунке 7. Это правило применяется ко всем модулям HAL.

Рисунок 7: Последовательность вызовов, сгенерированная HAL_DMA_IRQHandler()

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