STM Урок 15. HAL. USART. DMA



 

Урок 15

 

HAL. USART. DMA

 

Сегодня мы продолжим занятия по подключению микроконтроллера STM32 к ПК посредством интерфейса USART.

Только в отличие от прошлых уроков мы применим для этого технологию DMA.

Так как мы вообще впервые применяем данную технологию, то я коротко вас с ней познакомлю.

DMA (direct memory access) — прямой доступ к памяти. То есть мы из одной области памяти копируем данные в другую область памяти, либо из периферии в область памяти, либо из области памяти в периферию, но копируем не по одному байту, применяя при этом обязательно регистры АЛУ, а напрямую, без использования АЛУ. Этим самым достигается большее быстролействие, разгружается процессорное время. АЛУ лишь получает информацию о том, сколько определенных информационных единиц нужно переслать, а также адрес памяти источника и приемника, всё остальное уже происходит без его участия. Мы либо знаем приблизительно, сколько для этого требуется времени и исходя из этого уже строим свой алгоритм, либо пользуемся прерываниями и обрабатываем там событие окончания передачи через DMA.

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

Но у нас сегодня именно USART.

 

Проект был создан из USART_TRANSMIT, назван USART_TRANSMIT_DMA, схема также не изменилась.

Запускаем куб, в USART2 отключаем CTS и RTS, если они у вас были включены.

В настройках в Configuration включаем сначала общие прерывания. Затем заходим в DMA. В закладке DMA Settings добавляем DMA и настраиваем его следующим образом

 

image00

 

Выбираем там USART2_TX, так как мы будем заниматься передачей и технологию DMA мы применим именно в передаче данных. В данном случае все потоки и каналы, а также направление работы DMA, Cube MX выберет за нас.

В DMA и в первом и во втором существует насколько каналов и несколько потоков, предназначенных для того, чтобы распределить всё шинам и интерфейсам. С каналами и так всё ясно. С потоками немного по-другому. Чтобы предотвратить различные коллизии в буфере FIFO в DMA, было создано таких целых 8 буферов и для каждого буфера был назначен свой поток. Посмотрим организацию DMA в STM32 4 серии в Reference Manual

 

image05

 

 

Также в этой же технической документации отлично расписано, какой канал и поток для какого именно интерфейса работает

 

image07

 

Таблица данная продолжается и на данной странице, также за ней следует подобная таблица и для второго DMA, но нам это уже не так интересно, так как наша периферия именно на данной странице и поток с каналом для передачи данных по нашей периферии выделен синим квадратом. То есть Cube MX не ошибся, выставив нам именно 6 поток.

Внизу в настройках DMA мы ничего не трогаем.

Оставляем режим Normal. Также существует ещё режим Circular, который заставляет заниматься DMA передачей данных циклично без остановки.

Также оставляем инкрементирование адреса только памяти. Адрес периферии мы в каждом цикле не прибавляем, мы работаем только с одной периферией и у неё только один адрес памяти.

Буфер FIFO мы не используем, поэтому мы не вклчаем его в нашем случае. Он нужен в случае, чтобы за один цикл передачи мы могли передавать и принимать потоками за один цикл, ещё и используя при этом специальный буфер. Это ещё больше ускоряет скорость передачи данных. Единицу передачи данных оставляем байт, нам так будет удобнее, хотя мы вполне можем передавать и полусловами, и даже словами (по 4 байта).

 

 

Также включаем в закладке NVIC на всякий случай прерывания

 

image04

 

Генерируем проект.

В главной функции main() для красоты исправляем переменную

 

  /* USER CODE BEGIN 1 */

        uint8_t str[]=»USART Transmit DMA\r\n»;

  /* USER CODE END 1 */

 

Находим функцию для передачи с использованием DMA следующим образом:

 

image02

 

И в бесконечном цикле меняем также

 

  while (1)

  {

        HAL_UART_Transmit_DMA(&huart2, str, sizeof(str)-1);        

        HAL_Delay(100);

  /* USER CODE END WHILE */

 

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

Таймаут уже не нужен. В этом нам и помогают прерывания по окончанию передачи.

Собираем, открываем терминальную программу, жмём там Connect, прошиваем, смотрим

 

image08

 

Все у нас передается.

 

 

Дисплей я не отключал, так как он потребуется нам для отображения уже принятых данных.

Для приема данных был создан другой проект из проекта USART_RECEIVE.

Имя нового проекта USART_RECEIVE_DMA.

Запускаем вновь созданный проект в CUBE, аналогичным образом заходим в Configuration, убедимся что у нас включены прерывания в USART2, затем заходим в DMA и настраиваем всё вот так:

 

image01

 

У нас здесь уже выбрался поток 5, если мы заглянем в таблицу, которая дана была выше, то так оно и есть, По приёму у нас именно такой поток и используется. Также у нас, соответственно, изменилось и направление передачи — от периферии к памяти.

Применяем настройки, генерируем проект. Открываем его.

Добавляем файл lcd.c

Изменим немного размер переменной в main()

 

  /* USER CODE BEGIN 1 */

        char str[21]={0};

  /* USER CODE END 1 */

 

У нас же дисплей на 20 символов в строке, вот и будем заполнять символами всю строку.

Меняем функцию

 

        LCD_String(str);

        HAL_UART_Receive_DMA(&huart2, (uint8_t*) str, 20);

  /* USER CODE END 2 */

 

В бесконечном цикле также меняем функцию и размер принятых данных, а также нолик ставим по индексу 20

 

                LCD_SetPos(0, 3);

                str[20]=0;

                LCD_String(str);

                HAL_UART_Receive_DMA(&huart2, (uint8_t*) str, 20);

 

Давайте попробуем собрать дисплей и проверить работоспособность нашего кода

 

image09

 

Как мы видим, передавать с ПК мы можем уже не обязательно полностью по 20 символов сразу, то есть поток передачи уже, не взирая на то, что у нас стоит именно 20 байт в функции, напрямик передаёт в память дисплея байты в любом количестве, просто когда в совокупности будет передано именно 20, то тогда шина USART перейдёт в состояние, при котором она разрешит опять к ней обращаться снаружи, и ПК сможет дальше передавать данные. Передадим ещё такие же 10 байтов

 

image10

 

Как мы видим, строка у нас заполнилась до конца.

Попробуем передать какие-нибудь другие символы, всё передается.

Только теперь строка         if(huart2.RxXferCount==0)  теряет свой смысл, т.к. проверяемый параметр в случае использования DMA всегда будет в нуле. Что же нам тогда проверять, чтобы выводить строку, когда она будет полностью заполнена?

На помощь нам придет дебагер.

Соберем проект, запускаем дебаггер, ставим точку останова тут в файле stm32f4xx_it.c

 

image03

 

Стартуем прошивку.

В терминале передаем строку например «123456789» — в прерывание не попадаем, а как только мы передадим 10й символ, то попадем в прерывание. Шагаем дальше внутрь функции HAL_DMA_IRQHandler(&hdma_usart2_rx)

В окно переменных для отслеживания добавляем строку hdma_usart2_rx – это параметр нашей функции-обработчика, также можно добавить hdma, что в принципе одно и то же. И ждем когда у данной структуры изменится параметр State. Перед изменением запомним текущий статус – он будет

0x02 HAL_DMA_STATE_BUSY. Когда он изменится, он примет вид 0x31 HAL_DMA_STATE_READY_HALF_MEM0.

Это означает, что у нас прерывание вызвало заполнение половины памяти DMA. Запускаем дальше код.

В терминале опять сначала отправляем «123456789», затем вводим один символ – он получается будет уже для строки и для памяти DMA 20й – попадаем опять в обработчик прерывания от DMA.

Шагаем опять до изменения статуса. Теперь он у нас будет 0x11 HAL_DMA_STATE_READY_MEM0.

 

Вот это как раз то что нам и нужно. Его мы и будем отслеживать В нашем условии в бесконечном цикле меняем на

 

        if(hdma_usart2_rx.State==HAL_DMA_STATE_READY_MEM0)

        {

                HAL_Delay(100);

 

Также в конце обработки условия добавим сброс этого флага в вид по умолчанию HAL_DMA_STATE_BUSY

 

                LCD_String(str);

                HAL_UART_Receive_DMA(&huart2, (uint8_t*) str, 20);

                hdma_usart2_rx.State=HAL_DMA_STATE_BUSY;

        }

  /* USER CODE END WHILE */

 

Собираем, прошиваем, смотрим. Проверяем терминалом

 

image11

 

 

Предыдущий урок Программирование МК STM32 Следующий урок

 

Проект по передаче данных

Проект по приему данных

 

 

Терминальная программа

 

Отладочную плату можно приобрести здесь STM32F4-DISCOVERY

Переходник USB to TTL можно приобрести здесь ftdi ft232rl

 

 

Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)

STM32 HAL. USART. DMA

 

Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)

STM32 HAL. USART. DMA

17 комментариев на “STM Урок 15. HAL. USART. DMA
  1. Саболч:

    Здравствуй.

    Меня зовут Саболч из Венгрии.
    Я недавно начал используеться STM32. Но я всегда смотрю твои видео, мне очень нравятся, полезные.

    Я хотел бы спросить твой помощь, у меня есть проект в университете;

    Мне надо используеться ADC c DMA (это хорошо работает), и SPI или I2C TX c DMA. не могу новые данные отправлено из памяти. Я пробавал режим Circular,но не могу обновление данные в этом адресе.

    ADC_callback()
    {
    data = adc_buffer;
    }
    main()
    {
    HAL_i2c_MasterTransmit_DMA(&hi2c3,0x18,&data,1,100);
    while(1)
    {

    }
    }

    надеюсь ты можешь помочь.
    Я жду твоего ответа

    Саболч

    • Здравтсвуйте!
      Спасибо за интерес к моим ресурсам!
      А как объявлена переменная data?

      • Саболч:

        Спасибо за ответ,

        data глобальная (uint32_t)

        Я хочу независимая работа от CPU как ADC_DMA работает.
        CPU работает только, если отправлено значение.
        По твоему, это возможно?
        Мне нужно очень быстрая работа.

        (Извиный не так хорошо говорю по русски)

  2. Евгений:

    Здравствуйте, изучая данный урок, я столкнулся с тем, что если по какой-то причине данных пришло больше чем объявлено в функции HAL_UART_Receive_DMA(), то запись в массив str[] сбивается даже в том случае если следующий пакет данных правильного размера. Подскажите, пожалуйста, какие есть механизмы отлова и исправления такого рода ошибок?

  3. Евгений:

    Владимир, спасибо за ответ. Всю неделю я пытался разобраться с тем, что же происходит при такой работе UART + DMA. Вот как я понимаю, пожалуйста, поправьте меня если я где то не прав: Вызывая функцию HAL_UART_Receive_DMA() мы передаем ей ожидаемое кол-во байт для приема. Этот параметр устанавливает внутренний счетчик DMA, (регистр NDT). Для каждого байта, который он получает, счетчик уменьшается на единицу, пока не достигнет нуля. Когда счетчик становится равен нулю происходит передача данных. По всей видимости в этот момент мы попадаем в условие (hdma_usart2_rx.State==HAL_DMA_STATE_READY_MEM0), где мы опять передаем в функцию HAL_UART_Receive_DMA() ожидаемое кол-во байт для следующего приема. Пока все хорошо, однако если мы намеренно посылаем пакет большего размера чем у нас заявлено, например, вместо четырех байт отправим пять, то массив принятых данных сбивается, «лишние» эл-ты становятся на первые места в массиве, я отследил в отладчике что в этом случае происходит следующее: счетчик NDT достигает 0, вызывается прерывание, сразу же счетчик становится равен заданному значению и дочитываются оставшиеся байты, их значения заносятся в массив str[] на первые места (так как будто DMA работает в циклическом режиме!). А этого быть не должно т.к. у нас режим работы DMA – нормальный. Ситуацию спасает задержка + время на вывод строки, при их наличии все работает корректно.

    If (hdma_usart2_rx.State==HAL_DMA_STATE_READY_MEM0)
    {
    HAL_Delay(100);
    LCD_String(str);
    HAL_UART_Receive_DMA(&huart2, (uint8_t*) str, 20);
    hdma_usart2_rx.State=HAL_DMA_STATE_BUSY;
    }

    Но так быть не должно, функция должна отрабатывать и без таких таймаутов. Дальше пока не знаю что делать, в таком виде эта функция бесполезна, а хотелось бы её использовать.

  4. Александр:

    Здравствуйте, при компиляции примера Keil выдает одну ошибку (../Src/main.c(114): error: #20: identifier «HAL_DMA_STATE_READY_MEM0» is undefined) подскажите пожалуйста в чём может быть проблема.

  5. Алексей:

    Добрый день!
    При использовании HAL_UART_Transmit_DMA(&huart2, str, sizeof(str)-1); в порт ничего не выдается
    Стоит заменить на HAL_UART_Transmit(&huart2, str, sizeof(str)-1,0xFFFF); и сразу передача пошла.
    DMA в кубе включил и все настроил
    В чем может быть причина?

    • С 2019 года в кубе есть ошибка порядка инициализации периферии. Сначала надо настроить DMA, а потом USART. К примеру, в main() кубик сделает так:

      MX_USART2_UART_Init();
      MX_DMA_Init();

      а надо вот так:

      MX_DMA_Init();
      MX_USART2_UART_Init();

      тогда отправка работает.

  6. Михаил:

    Спасибо за примеры (:

  7. Николай:

    Алексей. Скорее всего не включено глобальное прерывание USART2.

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*