STM Урок 209. LL. STM32F4. SPI. Дисплей TFT 240×320. DMA

 

 

 

Продолжим работу по программированию линейки контроллеров STM32F4 с использованием библиотеки LL.

Также продолжим работу с дисплеем TFT разрешением 240×320 на контроллере ILI9341, подключенному к контроллеру STM32F4 по шине SPI. Только на данном уроке мы будем передавать данные в дисплей с использованием DMA и убедимся на практике, насколько это ускорит процесс работы дисплея.

Мы уже с такой темой работали в уроке 180, только использовали мы при этом библиотеку HAL. Но с DMA мы также работали и с использованием библиотеки LL при работе с серией STM32F1.

В отличие от SPI, модуль DMA в контроллере семейства STM32F4 претерпел значительные изменения по сравнению с линейкой STM32F1. Поэтому нам перед тем, как мы начнём работать с проектом, следует вкратце познакомиться с данными изменениями.

Вот так выглядит блок-схема модуля DMA в контроллере STM32F4

 

 

Данную схему мы видели и в уроке 180. Мы видим здесь то, что теперь у нас по сравнению с линейкой F1 появились 8 потоков, каждый из которых может быть подключен к любому из 8 каналов (или запросов).

Также появились 8 буферов FIFO – по одному буферу на каждый поток. Буферы имеют глубину 32 ячейки, которые можно использовать не только в режиме FIFO, но и в режиме Direct (прямом). Также каждому буферу мы можем задать порог: 1/4, 1/2 и 3/4 от размера FIFO. Можем задавать приоритеты между запросами для разных потоков.

Также теперь появились целых 5 различных флагов событий (DMA Half Transfer, DMA Transfer complete, DMA Transfer Error, DMA FIFO Error, Direct Mode Error).

И это только главные отличия. Само собой, в регистрах также будут значительные отличия, причём названия регистров также изменились. Основная разница в появлении других имён в том, что теперь регистры распределяются не по каналам, а по потокам. То есть мы не поток для канала выбираем, а наоборот – канал для потока.

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

Начнём с регистра конфигурирования

 

 

Такой регистр есть у каждого потока.

Первое битовое поле CHSEL[2:0] отвечает за выбор канала для потока

000: выбран канал 0

001: выбран канал 1

010: выбран канал 2

011: выбран канал 3

100: выбран канал 4

101: выбран канал 5

110: выбран канал 6

111: выбран канал 7

Причём биты данного поля (как, впрочем, практически и всех остальных в данном регистре) защищены от записи, когда DMA включен, писать в них мы можем только, если бит EN будет сброшен.

Битовое поле MBRUST отвечает за настройку пакетной передачи данных памяти:

00: одиночная передача

01: INCR4 (инкрементальная передача 4 beat-пакетов)

10: INCR8 (инкрементальная передача 8 beat-пакетов)

11: INCR16 (инкрементальная передача 16 beat-пакетов).

 

Аналогично назначение следующего битового поля – PBRUST, только предназначено оно уже не для памяти, а для периферии.

Скорее всего, мы в будущем обязательно столкнёмся с пакетной передачей данных по DMA, в данном уроке она нам не потребуется, поэтому заострять своё внимание сильно на этом не будем.

Следующий бит CT (Current target) отвечает за выбор целевого буфера в режиме двойной буферизации

 

0: Текущая целевая память – 0 (адресуется указателем DMA_SxM0AR)

1: Текущая целевая память – 1 (адресуется указателем DMA_SxM1AR).

 

Следующий бит DBM как раз и включает или отключает режим двойной буферизации:

 

0: Нет переключения буфера в конце передачи (двойная буферизация отключена)

1: Переключение памяти по окончанию передачи (режим двойной буферизации).

 

Следующее поле PL отвечает за приоритет потока:

 

00: Low (низкий)

01: Medium (средний)

10: High (высокий)

11: Very high (очень высокий).

 

Если у нескольких потоков будет одинаковый приоритет, то большим приоритетом будет обладать тот поток, у которого номер меньше.

Следующий бит PINCOS отвечает за настройку инкрементирования (именно за настройку, а не за разрешение) адреса периферии. При включенном данном бите при инкрементировании адреса также происходит выравнивание размера смещения. Бит PINCOS влияет только на порт AHB. То есть произойдёт автоматическое выравнивание на 32-битный адрес не зависимо от размера PSIZE.

Следующие биты и битовые поля MSIZE, PSIZE, MINC, PINC, CIRC и DIR работают также как и в DMA контроллеров серии F1 и они нами уже рассмотрены в уроке 156.

Бит PFCTRL отвечает за контроль потока:

0: Потоком передачи управляет DMA

1: Потоком передачи управляет периферия.

 

Назначение битов TEIE, HTIE и TCIE, которые разрешают те или иные прерывания, такое же как и в линейке F1.

Бит DMEIE (Direct mode error interrupt enable) разрешает прерывание при возникновении события ошибки передачи данных в режиме Direct Mode:

0: прерывания запрещены

1: прерывания разрешены.

 

Ну а последний бит EN, как мы знаем из серии F1, включает или отключает DMA.

Также у серии F4 в DMA появился новый регистр, который управляет работой с буфером FIFO того или иного канала

 

 

Хотя в данном занятии работа с буфером FIFO не планируется, нам нужно будет его хотя бы отключить, поэтому познакомиться с данным регистром нам всё-таки следует.

Бит FEIE данного регистра отвечает за разрешение прерываний по реакции на событие ошибки буфера FIFO:

 

0: прерывания запрещены

1: прерывания разрешены.

 

Следующее поле FS позволяет узнать текущее состояние буфера

 

000: 0 < уровень FIFO < 1/4

001: 1/4 ? уровень FIFO < 1/2

010: 1/2 ? уровень FIFO < 3/4

011: 3/4 ? уровень FIFO < full

100: FIFO пуст

101: FIFO полон.

 

Следующий бит DMDIS позволяет отключить или включить режим Direct, тем самым разрешить или запретить использование FIFO

 

0: Режим Direct включен

1: Режим Direct отключен

 

Следующее битовое поле FTH задаёт величину порога FIFO в доле от полного размера

 

00: 1/4 полного размера FIFO

01: 1/2 полного размера FIFO

10: 3/4 полного размера FIFO

11: полный размер FIFO.

 

Регистры состояния и сброса флагов прерываний ISR и IFCR в DMA контроллера F4 превратились теперь в регистровые пары – LISR/HISR и LIFCR/HIFCR.

Скорее всего это деление было обусловлено увеличением разновидностей прерываний.

Начнём с младшей части регистра статуса прерываний

 

 

По сравнению с регистром ISR контроллера F4, здесь исчезли биты GIFx, отвечающие за глобальные прерывания.

Появился новый бит DMEIF, являющийся флагом прерывания по событию ошибки режима Direct.

Также появился бит FEIF, который служит флагом прерывания по событию ошибки FIFO.

Отдельно рассматривать старший регистр данной пары нет необходимости, так как там всё то же самое, только для других потоков.

Рассмотрим также младший регистр пары, отвечающей за сброс флагов прерываний

 

 

Здесь также исчез бит CGIF, а появились биты CDMEIFx и CFEIFx, с помощью которых мы, соответственно, можем сбросить флаги прерываний DMEIF и FEIF.

Регистр SxNDTR, отвечающий за количество передаваемых через DMA данных, подобен регистру CNDTRx контроллера STM32F1.

Также регистры SxPAR, SxM0AR и SxM1AR подобны регистрам CPARx и CMARx контроллера F1, только регистров для данных памяти здесь два для обеспечения режима двойной буферизации.

Вот, в принципе, и все регистры. Не хотел я вдаваться слишком плотно в их организацию, но пришлось.

Ну что ж, впору перейти и к проекту, для которого за основу мы, конечно же возьмём проект с именем LL_SPI_ILI9341 из прошлого урока, не писать же всё заново, и дадим ему, соответственно, имя LL_SPI_ILI9341_DMA.

Откроем наш проект в Cube MX и увеличим скорость SPI5 вдвое, уменьшив вдвое делитель

 

 

Включим DMA для периферии SPI5, выбрав соответствующий канал и поток для передачи данных (приём нам не нужен), а также включим в данном потоке непрерывный режим передачи данных Circular, остальное оставив по умолчанию

 

 

Прерывания, хоть мы их и будем использовать, включать отдельно не нужно, так как они включатся сами.

Задействуем для DMA библиотеку LL в настройках

 

 

Сгенерируем проект и откроем его в Cube IDE.

Как всегда, по традиции, проанализируем, что у нас происходит при инициализации DMA. Правда, основная инициализация, кроме настройки приоритета прерываний, DMA находится в функции инициализации модуля SPI в функции MX_SPI5_Init.

В теле данной функции мы видим вызов функции LL_DMA_SetChannelSelection, посредством которого у нас включится бит 1 в битовом поле CHSEL регистра S4CR периферии DMA2, который произведёт выбор канала 2 для нашего потока 4. остальные биты данного поля сбросятся.

Далее с помощью вызова функции LL_DMA_SetDataTransferDirection произойдёт установка бита 0 в битовом поле DIR того же регистра, посредством чего включится направление передачи данных из памяти в периферию.

С помощью вызова функции LL_DMA_SetStreamPriorityLevel полностью очистится битовое поле PFCTRL этого же регистра, посредством чего установится приоритет LOW, так как мы его оставили по умолчанию.

Далее при помощи функции LL_DMA_SetMode в этом же регистре установится бит CIRC, который включит нам режим циклической передачи данных.

Затем с помощью вызова функции LL_DMA_SetPeriphIncMode сбросится бит PINC того же регистра, так как адрес периферии у нас не меняется, поэтому мы его не инкрементируем.

 

 

Потом при помощи вызова функции LL_DMA_SetMemoryIncMode у нас в том же регистре установится бит MINC, так как адрес памяти мы инкрементируем.

Далее при помощи вызова функций LL_DMA_SetPeriphSize и LL_DMA_SetMemorySize у нас сбросятся все биты в битовых полях PSIZE и MSIZE того же регистра, так как ширина передаваемой информации в у нас 1 байт.

С помощью вызова следующей функции LL_DMA_DisableFifoMode у нас в регистре S4FCR сбросится бит FCR, тем самым выключится режим передачи без использования FIFO – Direct.

Ну вот и вся инициализация. Конечно, это не всё. Это только инициализация, которая сгенерировалась нам с помощью Cube MX. Дальше мы сами.

В функции main() перед тем, как вносить настройки, отключим наш DMA с помощью специальной функции, вследствие чего в регистре CR сбросится бит EN

 

 

Затем сбросим флаги прерываний по окончанию передачи данных и по ошибке с помощью вызова следующих двух функций, вследствие чего произойдёт запись единиц в биты CTCIF4 и CTEIF4 регистра HIFCR

 

 

В регистре периферии SPI5 CR2 включим бит TXDMAEN с помощью специальной функции для того, чтобы шина SPI5 могла формировать запросы к каналам DMA при установке флагов TXE

 

 

Затем мы включаем прерывания канала DMA с помощью установки TCIE и TEIE в регистре S4CR

 

 

Объявим глобальные пользовательские переменные, одна из которых будет служить флагом окончания передачи через DMA, а другая – счётчиком циклов при циклической передаче, чтобы вовремя эту передачу остановить

 

 

Добавим пользовательский обработчик прерывания по окончании передачи цикла через периферию DMA

 

 

Здесь мы для начала сбросим флаг прерывания, уменьшим счётчик, и если счётчик достиг нуля, то остановим DMA, установим счётчик в 1 и установим пользовательский флаг.

В файле stm32f4xx_it.c добавим прототип нашего обработчика

 

 

В обработчике прерываний DMA2_Stream4_IRQHandler, напишем код, который при установленном флаге TCIF4 в регистре HISR вызовет нашу пользовательскую функцию, а при установленном флаге TEIF4 того же регистра просто сбросит соответствующий флаг

 

 

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

Для начала в файле ll_spi_ili9341.h подключим библиотеку для DMA

 

 

Перейдём в файл ll_spi_ili9341.c и подключим наши флаги, а также объявим буфер для работы с DMA

 

 

В функции TFT9341_FillScreen мы полностью перепишем всё тело, так как изменений здесь будет много

 

 

Здесь мы организовали цикл, в котором заполнили буфер величиной 51200 байтов (25600?2) одинаковыми парами байтов, так как мы заливаем одним цветом.

Почему именно такое число? Потому что если разделить 153600 на 3, то получится такое число. Чтобы у нас был максимально возможный буфер, не превышающий 65535 и минимальное их количество. Также мы не можем передать часть буфера, только весь. DMA так не умеет (хотя вру, половинку может, но мы не будем сегодня обрабатывать такие прерывания).

Затем мы поднимаем ножку данных, задаём количество буферов в счётчике, отключаем сначала DMA, настраиваем количество передаваемых элементов с помощью внесения в регистр NDTR нужной величины, указываем адреса данных и периферии, занося эти адреса соответственно в регистры M0AR и PAR и включаем DMA, засчёт чего начнётся передача данных в SPI через DMA.

Потом мы ждём установки нашего пользовательского флага и сбрасываем его.

Можно теперь собрать код и прошить контроллер, экран теперь будет заливаться цветами мгновенно. Так как блоге я этого показать не могу, то смотрите видеоверсию урока, ссылка на которую внизу страницы.

В функции TFT9341_FillRect мы также полностью перепишем всё тело

 

 

Прежний код у нас остался только до активации ножки DC, и то не весь.

Дальше мы рассчитываем значение количества байтов по площади нашего прямоугольника.

Если количество байтов не превысит 65535, то сразу назначаем переменным количества буфера и размера буфера значение.

Если количество байтов превысило 65535, то в этом случае для того, чтобы получить наименьшее количество буферов, мы пытаемся найти наименьший делитель для количества байтов, на который данное количество разделится нацело, начиная с 3. Как только такой делитель найден, то мы этот делитель присваиваем переменной количества буфера, а размеру буфера – результат деления и выходим из цикла. И так до количества байтов, делённого на 3.

Затем аналогично функции TFT9341_FillScreen заполняем буфер значениями цвета заливки и таким же образом передаём наши буферы через DMA в SPI.

Соберём код, прошьём контроллер и посмотрим, как это работает.

Прямоугольники заливаются мгновенно (см. видеоверсию)

 

 

 

Также давайте ускорим вывод строк, вернее мы ускорим вывод отдельного символа, чем и обеспечим ускорение вывода строк.

Для этого в функции вывода символа TFT9341_DrawChar для начала объявим локальную переменную, которая будет хранить текущую горизонтальную координату, которую сначала мы приравняем к координате символа

 

 

Вместо вот этой строки

 

TFT9341_DrawPixel((x + j), y, lcdprop.TextColor);

 

будут две такие

 

frm_buf[(i*width + j) * 2] = lcdprop.TextColor >> 8;

frm_buf[(i*width + j)*2+1] = lcdprop.TextColor & 0xFF;

 

А вместо этой

 

TFT9341_DrawPixel((x + j), y, lcdprop.BackColor);

 

такие

 

frm_buf[(i*width + j)*2] = lcdprop.BackColor >> 8;

frm_buf[(i*width + j)*2+1] = lcdprop.BackColor & 0xFF;

 

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

Вместо

 

y++;

 

будет

 

y_cur++;

 

Так как переменную y нам теперь трогать нельзя.

Выходим из всех циклов, инициализируем счётчик буферов в 1, так как один символ у нас всегда влезет в максимум DMA, а затем устанавливаем окно для вывода значений буфера

 

 

Затем поднимаем ножку и передаём наш буфер в контроллер дисплея через DMA таким же образом, как передавали заливку

 

 

Соберём код, прошьём контроллер и увидим, что строки с текстом теперь выводятся практически мгновенно (см. видеоверсию урока).

Итак, на данном уроке нам удалось настроить передачу с использованием периферии DMA данных в контроллер дисплея по шине SPI. Также мы применили в DMA циклический режим (Circular).

Всем спасибо за внимание!

 

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

 

Исходный код

 

 

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

 

 

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

 

STM Name

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

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

*