STM Урок 180. HAL. Дисплей TFT 240×320. SPI. DMA



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

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

С DMA мы также постоянно работаем, знаем отлично, как он организован в контроллере STM32F1. STM32F4 не сильно в этом плане отличается.

Но некоторые различия есть. Пока мы сильно в них не будем вдаваться, так как применяем мы библиотеку HAL, которая берёт все вопросы аппаратной организации на себя.

Посмотрим лишь немного, чтобы было хотя бы какое-то представление аппаратной организации DMA в контроллере STM32F4.

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

 

 

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

Применять мы будем режим memory-to-peripheral, при котором данные передаются из памяти в периферию, так как передавать мы будем данные из массива в памяти в периферию SPI для дальнейшей передачи их в контроллер дисплея.

Вот блок-схема режима

 

 

И также применять мы будем не режим Normal, при котором по команде передачи данных передаётся только один буфер, а циклический (CIRCULAR), при котором после передачи буфера он передаётся снова и так пока не остановишь. Дело в том, что у нас дисплей при его полной заливке пикселями в 16-разрядном режиме цвета, требует передачи в него данных в количестве 153600 байтов, а через DMA максимум мы можем передать только 65535, поэтому мы и применим режим циклической передачи, чтобы обеспечить непрерывность передачи данных.

Ну и теперь, когда у нас есть хоть какое-то понимание, как передаются данные через периферию DMA, нам легче будет работать с кодом, имея представление того, чего мы вообще добиваемся.

Я расскажу, каким образом и какими порциями мы вообще будем передавать данные, в процессе написания кода.

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

Анализатор мы подключим только к ножкам GND, MOSI и SCK. Остальные нам не нужны. Если дисплей работает, значит и ножки RESET и DC тоже функционируют правильно и следить нам за ними ни к чему, а ножкой CS мы никак не управляем, она у нас всегда опущена

 

 

А теперь мы можем смело приступить к проекту, который был сделан из проекта прошлого урока с именем ILI9341_SPI и назван именем ILI9341_SPI_DMA.

Откроем наш проект в Cube MX, перейдём в Clock Configuration и переключим вот этот мультиплексор в режим HSE (простите, в прошлом уроке забыл, но, в принципе, и без этого работало)

 

 

Также после этого не забываем обновить частоту тактирования и выставить её 168 МГц.

Пока уменьшим немного скорость передачи данных по шине SPI для лучшего анализа логическим анализатором

 

 

Перейдём в настройки DMA в модуле SPI, добавим канал для передачи данных и включим ему режим Circular

 

 

 

Сгененрируем проект, откроем его в Keil, настроим программатор на авторезет, отключим оптимизацию, добавим файл spi_ili9341.c в дерево проектов, откроем файл main.c, закомментируем пока весь пользовательский код в бесконечном цикле функции main() и исправим заливку дисплея с чёрного на зелёный

 

TFT9341_FillScreen(TFT9341_GREEN);

 

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

 

 

Мы видим, что между 16-разрядными посылками у нас есть некоторые таймауты, причём если мы прибавим скорость SPI, то они пропорционально частоте тактирования SPI не уменьшатся а останутся такими же (порядка 1,5 микросекунды).

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

Один очень важный момент, надеюсь потом всё исправят.

В актуальной на данной момент версии Cube MX 5.4.0 существует проблема неправильной генерации кода, если мы в периферии SPI контроллера STM32F4 включаем DMA (на остальных сериях контроллеров и на остальных видах периферии не проверял).

Почему-то при генерации последовательность инициализации этих типов периферии генерируется с точностью до наоборот

 

 

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

Поэтому меняем местами данные функции

 

 

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

 

 

 

Перейдём в файл spi_ili9341.c, подключим наши переменные и добавим вот такой буфер

 

 

Я думаю, мы все понимаем, почему именно такой размер буфера.

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

 

 

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

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

Затем мы опускаем ножку данных, задаём количество буферов в счётчике и даём команду на передачу данных в SPI через DMA, передав в параметре указатель на буфер и размер буфера.

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

Если оставить так как есть, то у нас пойдёт бесконечный процесс отправки буферов в SPI. Его надо остановить, для этого нам надо будет обработать прерывание по окончании передачи буфера через DMA. Вернёмся в файл main.c и добавим такой обработчик

 

 

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

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

Посмотрим результат в программе логического анализа

 

 

Совсем другое дело. Закроем проект и вернём в Cube MX скорость шины

 

 

Снова сгенерируем проект, откроем его в Keil, не забываем снова поменять местами инициализацию DMA и SPI и вернём чёрный цвет

 

TFT9341_FillScreen(TFT9341_BLACK);

 

Логический анализатор теперь можно отключить и программу логического анализа закрыть.

Раскомментируем тест заливки всего экрана, соберём код, прошьём контроллер и посмотрим, как теперь заливается весь экран.

А заливается он теперь практически мгновенно (жаль что в текстовой версии показать не смогу, смотрите видеоурок).

Теперь раскомментируем тест заливки прямоугольных областей по углам экрана

 

 

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

 

 

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

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

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

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

Если вам удастся придумать алгоритм проще, то напишите в комментарии.

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

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

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

 

 

Здесь всё проще, здесь буфер никогда не превысит 65536, так как прямоугольники тут размера 120×160.

Поэтому раскомментируем следующий тест и посмотрим, как он работает

 

 

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

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

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

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

 

 

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

 

Исходный код

 

 

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

STM32F4-DISCOVERY

2,8 дюймов 240×320 SPI TFT LCD

Логический анализатор 16 каналов можно приобрести здесь

 

 

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

 

STM TFT 240×320. SPI. DMA

17 комментариев на “STM Урок 180. HAL. Дисплей TFT 240×320. SPI. DMA
  1. megger380:

    «Один очень важный момент, надеюсь потом всё исправят»
    Блин, опять ST-шники косячат….

    • Кстати, если проект делать не из прошлого, а добавить SPI уже в новом кубе, а потом в нём включить DMA, то всё будет в нормальном порядке. Либо из прошлого, но удалить SPI, перегенерировать проект без него, затем добавить SPI, настроить его и включить в нем DMA, то тоже будет всё нормально. Также сейчас исправлю инициализацию. Там, где настройка гамма-коррекции Negative, кусок кода из старого проекта 37 урока, то есть там идёт не набор данных в массив, сразу их отправка, а потом ещё раз отправка, но не их, а массива, который набран в гамма-коррекции Negative, и также перезалью оба проекта уже с правильной инициализацией — и в уроке 179 и в этом. Было незаметно до тех пор, пока я не попытался с USB-Drive на этом дисплее выводить картинки, которые пошли в очень искаженных цветах. Когда поправил инициализацию, всё пошло отлично. Когда перескачаете оба проекта, то заметите, что с правильной настройкой гамма-коррекции цвета пойдут намного сочнее. Дисплей просто засияет.
      Спасибо за понимание!

  2. Евгений:

    Порядка 10fps можно получить на этом SPI дисплее используя DMA.
    Задача была вывести на дисплей ILI9341 через SPI картинки получаемые с тепловизионного сенсора Lepton 3.
    Жаль этого урока не было раньше. Сэкономил бы мне кучу времени. Автору спасибо! С интересом смотрю все уроки.

    • Спасибо за оценку!
      У меня такого дисплея просто не было раньше и не знал, что на F429-Discovery дисплей подключен также по SPI и всё отлично на нём можно было делать. А то давно бы сделал урок.
      Внешнюю ссылку, к сожалению, пришлось убрать. Себе скопировал. Просто всякие автоматы гугла и яндекса по-разному относятся к внешним ссылкам на сайтах и это может послужить причиной бана адреса сайта. А он мне очень дорог (надеюсь, не только мне).

  3. Maxim:

    Здравствуйте, мне нужна помощь в понимании работы EmWin. Точнее мне интересно как подключить физические кнопки для управления виджетами LTDC.

  4. Ollovein:

    Оказывается на F103C8T6 не хватает оперативной памяти для данного проекта. Не подскажете литературу по организации переменных в оперативной памяти?

  5. gord111:

    Пытаюсь запустить эту библиотеку на STM32F105RBT6. Но в нем 64к ОЗУ и буфер размером 65536 выдает ошибку при компиляции. Подскажите как это исправить.

      • DanilinS:

        И не получится. Памяти мало. Как вариант — вырисовывать интерфейс на экране небольшими фрагментами.

        И … делать буфер в 64К для заполнения экрана однородной заливкой?! А один байт ( слово) через DMA не судьба отправить?

    • W4d1m:

      Измените размер буфера frm_buf на 8192. В функции TFT9341_FillRect перепишите часть кода где if(n<=65535) на if(n<=8192) и где for(i = 3; i < n/3; i++) на for(i = 8; i < n/8; i++). В функции TFT9341_FillScreen for(i=0;i<25600;i++) замените на for(i=0;i<3200;i++) и n = 6400; на n = 51200; и dma_spi_cnt = 3; на dma_spi_cnt = 24; Вроде бы как всё. Уменя в CubeIDE проект занимает меньше 60kB.

  6. evorontsov:

    Чудеса в решете…..

    пока не заменил в

    while(!dma_spi_fl) {}
    dma_spi_fl=0;

    на if из цикла выйти не мог. При этом dma_spi_fl==1

    Как это может быть? Ума не приложу….

  7. Rustam:

    Как сделать вывод текста с помощью DMA?
    Чет не очень пойму как работает, все что старался сам сделать, не работает.
    Вот может кто подскажет.

    • azhig:

      Сначала вычисляешь размер заполняемой текстом области (длина * ширина), затем построчно заполняешь буфер как в примерах из урока. Если размер шрифтов такой, что один символ помещается в буфер (в зависимости от модели контроллера он будет разным, я использую размер 8192, например), то подойдет код , который я укажу ниже. В противном случае нужно будет буфер бить на несколько частей. Здесь в примере кода указана отрисовка одного символа, в принципе строка рисуется очень быстро, но если надо еще быстрее, то можно использовать такой же принцип и для строки, а не для отдельного символа.

      void TFT9341_DrawChar(uint16_t x, uint16_t y, uint8_t c)
      {
      uint32_t i = 0, j = 0;
      uint16_t height, width;
      uint8_t offset;
      uint8_t *c_t;
      uint8_t *pchar;
      uint32_t line=0;
      height = lcdprop.pFont->Height;
      width = lcdprop.pFont->Width;
      offset = 8 *((width + 7)/8) — width ;
      c_t = (uint8_t*) &(lcdprop.pFont->table[(c-' ') * lcdprop.pFont->Height * ((lcdprop.pFont->Width + 7) / 8)]);
      for(i = 0; i < height; i++)
      {
      pchar = ((uint8_t *)c_t + (width + 7)/8 * i);
      switch(((width + 7)/8))
      {
      case 1:
      line = pchar[0];
      break;
      case 2:
      line = (pchar[0]<< 8) | pchar[1];
      break;
      case 3:
      default:
      line = (pchar[0]<< 16) | (pchar[1]<< 8) | pchar[2];
      break;
      }
      for (j = 0; j < width; j++)
      {
      int buf_index = j + i*(width+1);
      if(line & (1 <> 8;
      frm_buf[buf_index*2+1] = lcdprop.TextColor & 0xFF;
      }
      else
      {
      frm_buf[buf_index*2] = lcdprop.BackColor >> 8;
      frm_buf[buf_index*2+1] = lcdprop.BackColor & 0xFF;
      }
      }
      y++;
      }
      TFT9341_SetAddrWindow(x, y, x+width, y+height);
      DC_DATA();
      dma_spi_cnt = 1;
      HAL_SPI_Transmit_DMA(&hspi1, frm_buf, (width+1)*(height+1)*2);
      while(!dma_spi_fl) {}
      dma_spi_fl=0;
      }

  8. Михаил:

    Пока не добавил команду 0х2С (запись в GRAM), в функцию TFT9341_FillScreen, заливки экрана не происходило.
    Может у меня другой дисплей.

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

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

*