Урок 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 и настраиваем его следующим образом
Выбираем там USART2_TX, так как мы будем заниматься передачей и технологию DMA мы применим именно в передаче данных. В данном случае все потоки и каналы, а также направление работы DMA, Cube MX выберет за нас.
В DMA и в первом и во втором существует насколько каналов и несколько потоков, предназначенных для того, чтобы распределить всё шинам и интерфейсам. С каналами и так всё ясно. С потоками немного по-другому. Чтобы предотвратить различные коллизии в буфере FIFO в DMA, было создано таких целых 8 буферов и для каждого буфера был назначен свой поток. Посмотрим организацию DMA в STM32 4 серии в Reference Manual
Также в этой же технической документации отлично расписано, какой канал и поток для какого именно интерфейса работает
Таблица данная продолжается и на данной странице, также за ней следует подобная таблица и для второго DMA, но нам это уже не так интересно, так как наша периферия именно на данной странице и поток с каналом для передачи данных по нашей периферии выделен синим квадратом. То есть Cube MX не ошибся, выставив нам именно 6 поток.
Внизу в настройках DMA мы ничего не трогаем.
Оставляем режим Normal. Также существует ещё режим Circular, который заставляет заниматься DMA передачей данных циклично без остановки.
Также оставляем инкрементирование адреса только памяти. Адрес периферии мы в каждом цикле не прибавляем, мы работаем только с одной периферией и у неё только один адрес памяти.
Буфер FIFO мы не используем, поэтому мы не вклчаем его в нашем случае. Он нужен в случае, чтобы за один цикл передачи мы могли передавать и принимать потоками за один цикл, ещё и используя при этом специальный буфер. Это ещё больше ускоряет скорость передачи данных. Единицу передачи данных оставляем байт, нам так будет удобнее, хотя мы вполне можем передавать и полусловами, и даже словами (по 4 байта).
Также включаем в закладке NVIC на всякий случай прерывания
Генерируем проект.
В главной функции main() для красоты исправляем переменную
/* USER CODE BEGIN 1 */
uint8_t str[]=»USART Transmit DMA\r\n»;
/* USER CODE END 1 */
Находим функцию для передачи с использованием DMA следующим образом:
И в бесконечном цикле меняем также
while (1)
{
HAL_UART_Transmit_DMA(&huart2, str, sizeof(str)-1);
HAL_Delay(100);
/* USER CODE END WHILE */
Как, я думаю, все заметили, применена уже другая функция, предназначенная именно для передачи с использованием DMA.
Таймаут уже не нужен. В этом нам и помогают прерывания по окончанию передачи.
Собираем, открываем терминальную программу, жмём там Connect, прошиваем, смотрим
Все у нас передается.
Дисплей я не отключал, так как он потребуется нам для отображения уже принятых данных.
Для приема данных был создан другой проект из проекта USART_RECEIVE.
Имя нового проекта USART_RECEIVE_DMA.
Запускаем вновь созданный проект в CUBE, аналогичным образом заходим в Configuration, убедимся что у нас включены прерывания в USART2, затем заходим в DMA и настраиваем всё вот так:
У нас здесь уже выбрался поток 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);
Давайте попробуем собрать дисплей и проверить работоспособность нашего кода
Как мы видим, передавать с ПК мы можем уже не обязательно полностью по 20 символов сразу, то есть поток передачи уже, не взирая на то, что у нас стоит именно 20 байт в функции, напрямик передаёт в память дисплея байты в любом количестве, просто когда в совокупности будет передано именно 20, то тогда шина USART перейдёт в состояние, при котором она разрешит опять к ней обращаться снаружи, и ПК сможет дальше передавать данные. Передадим ещё такие же 10 байтов
Как мы видим, строка у нас заполнилась до конца.
Попробуем передать какие-нибудь другие символы, всё передается.
Только теперь строка if(huart2.RxXferCount==0) теряет свой смысл, т.к. проверяемый параметр в случае использования DMA всегда будет в нуле. Что же нам тогда проверять, чтобы выводить строку, когда она будет полностью заполнена?
На помощь нам придет дебагер.
Соберем проект, запускаем дебаггер, ставим точку останова тут в файле stm32f4xx_it.c
Стартуем прошивку.
В терминале передаем строку например «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 */
Собираем, прошиваем, смотрим. Проверяем терминалом
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь STM32F4-DISCOVERY
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Здравствуй.
Меня зовут Саболч из Венгрии.
Я недавно начал используеться 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 работает только, если отправлено значение.
По твоему, это возможно?
Мне нужно очень быстрая работа.
(Извиный не так хорошо говорю по русски)
Здравствуйте, изучая данный урок, я столкнулся с тем, что если по какой-то причине данных пришло больше чем объявлено в функции HAL_UART_Receive_DMA(), то запись в массив str[] сбивается даже в том случае если следующий пакет данных правильного размера. Подскажите, пожалуйста, какие есть механизмы отлова и исправления такого рода ошибок?
Евгений, начните с логического анализа приходящего потока.
Владимир, спасибо за ответ. Всю неделю я пытался разобраться с тем, что же происходит при такой работе 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;
}
Но так быть не должно, функция должна отрабатывать и без таких таймаутов. Дальше пока не знаю что делать, в таком виде эта функция бесполезна, а хотелось бы её использовать.
Здравствуйте, при компиляции примера Keil выдает одну ошибку (../Src/main.c(114): error: #20: identifier «HAL_DMA_STATE_READY_MEM0» is undefined) подскажите пожалуйста в чём может быть проблема.
Здравствуйте!
Скорей всего со времён урока у текло много воды и в библиотеках HAL что-то изменилось.
я нашел такой ответ в интернете (и попробовал):
volatile bool full_received = false;
…
int main()
{
…
while(1)
{
HAL_UART_Receive_DMA(&huart2, (uint8_t *) rx_data, 20);
while(!full_data); // wait all 20 bytes to be received
full_data = false;
…
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
full_data = true;
return;
}
…
Работает как надо. Последняя функция вызывается когда все байты уже заполнены, то есть, код не работает, пока все байты не получены.
Да, да, спасибо!
Мы уже так и сделали на F103 в одном из последних уроков.
а где можно посмотреть этот урок?
На канале Narod Stream
Здравствуйте! У меня работает при HAL_DMA_STATE_READY вместо HAL_DMA_READY_MEM0. В новой версии так.
Добрый день!
При использовании 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();
тогда отправка работает.
Спасибо за примеры (:
Алексей. Скорее всего не включено глобальное прерывание USART2.