STM32 - TFT дисплей часть первая
![](http://istarik.ru/uploads/images/00/00/01/2020/04/08/8924dd.jpg)
Подключение дисплеев построенных на базе драйвера ILI9341 к микроконтроллерам stm32 по шине SPI. Пример как всегда сделан для платы BluePill.
Замечание для тех у кого камень серии F3. В папке с проектом есть папка For_STM32F3, в ней лежит один файл —
На ILI9341 делаются дисплеи разной диагонали, но у всех одно и то же разрешение 320 на 240 пикселей. Размеры пикселя зависят от размера диагонали. Цвет 16-ти битный — два байта на каждый пиксель (RGB565). Существуют дисплеи у которых данные выводятся по 8-ми или 16-ти битной параллельной шине, но это другое, здесь будет рассматриваться работа по SPI.
про 8-ми битную параллельную шину
По SPI данные передаются байтами бит за битом, это обычный способ передачи данных, такой же как у USART'а или I2C. То есть для передачи одного бита нужен один условный такт, соответственно чтоб передать байт нужно сделать восемь тактов. В отличии от SPI, параллельная шина может «закидавать» в дисплей все восемь бит за один условный такт, где каждый бит «летит» по своему проводу. Такой способ подразумевает более высокую скорость передачи данных, но требует значительно большее количество пинов микроконтроллера.
Обычно эти дисплеи оснащаются ещё и тач-панелью, которая тоже работает по SPI. За работу «тача» отвечает отдельный чип, чаще всего это что-то вроде TSC2046 (буквы могут быть другие). И вот тут есть интересный момент.
Суть заключается в том, что в дисплей нам нужно передавать данные как можно быстрее
Другой вариант, это подключить дисплей и «тач» на разные SPI. С одной стороны кажется что так проще, но с другой, вам может понадобится флешка (подразумевается отдельный чип флеш-памяти) или SD карта для хранения изображений, и обе эти штуки тоже работают по SPI, а у BLuePill всего два таких интерфейса.
Чтоб было понятно, картинка в 16-ти битном цвете, размером во весь экран (320х240) весит 153600 байта. То есть ни о каком хранении её внутри простенького микроконтроллера и речи быть не может.
Следуя из выше сказанного, лучше всего подключить дисплей и «тач» на один SPI, так как они прекрасно уживаются вместе и не мешают друг другу при правильной организации программы, а второй SPI оставить для хранилища (флешка или SD карта), или ещё для чего-то.
Теперь к вопросу почему не стоит вешать на один SPI дисплей и хранилище. Дело в том, что процесс вычитывания с флешки/карты и вывод на дисплей занимают определённое время. Считывать данные из хранилища нужно по кусочкам, создав промежуточный буфер, например 10Кб
![](http://istarik.ru/uploads/images/00/00/01/2020/04/06/fa9a2a.png)
Исходя из выше сказанного, чтоб вывести данные хранящиеся в SPI-флешке на экран как можно быстрее, нужно во-первых, подключать хранилище и дисплей к разным интерфейсам, и во-вторых использовать DMA таким образом чтоб данные одновременно читались из флешки и отправлялись в дисплей. То есть заполняем половину буфера и начинаем её отправлять, пока первая половина отправляется, вторая заполняется, и т.д. В результате мы сможем получить максимально возможную скорость — картинка на весь экран будет выводится за ~70 мс, что равно 14 FPS. Это относится к SPI-флешке, работа с SD-картой будет происходить дольше так как там используется FATFS, которая требует дополнительного времени.
Расчёты для работы с SPI-флешкой: при скорости SPI 18Мбит/с, за 1 мс можно передать 2250 байт, соответственно 153600 байт передастся за 68 мс (153600 / 2250 = 68), плюс 2 мс на накладные расходы. Ну и 1000мс / 70мс = 14 FPS.
Если картинка не на весь экран, то естественно она будет считываться и выводится быстрее, а значит мы получим больше FPS. Вот пример…
Зацикленный вывод тринадцати картинок размером 200х214. Каждая картинка выводится за 41 мс, то есть 24 FPS. Размер экрана 2.4".
Подключение
У всех дисплеи разные поэтому перечислю те контакты дисплея, к которым будем подключаться. SPI (MOSI, MISO, CLK). CS (Chip Select) подача низкого уровня на этот контакт говорит о том, что мы собираемся работать с дисплеем
Помимо этого нужно конечно же подать питание, и включить подсветку. Подсветку можно сделать постоянную, а можно организовать регулируемую, через транзистор с помощью таймера в режиме ШИМ. Питание и подсветку я подавал от отдельного источника питания, то есть не от самой платы.
Тачскрин будем подключать к тому же SPI что и дисплей. У него есть свой CS, и контакт
Конфигурация
Настраиваем SPI1…
![](http://istarik.ru/uploads/images/00/00/01/2020/04/08/7e683a.jpg)
Устанавливаем максимальную скорость SPI (18.0 MBits/s). Это для системной частоты 72Мгц. Если ваш камень допускает большую частоту, то можно попробовать увеличить скорость SPI, но сначала протестируйте на этой.
Настраиваем пины…
![](http://istarik.ru/uploads/images/00/00/01/2020/04/08/f3acd5.jpg)
Если у вас нет тачскрина, тогда TOUCH_CS и IRQ не нужны. Пин IRQ настраиваем как
Названия вводятся в разделе GPIO…
![](http://istarik.ru/uploads/images/00/00/01/2020/04/08/1ce3cd.jpg)
User Label
И настраиваем USART для вывода инфы.
Подключаем всё, и если подсветка работает то переходим к кодингу.
Программа
Скачиваем библиотеку и добавляем в проект файлы:
xpt2046_touch.c
fonts.c
ILI9341_GFX.h
xpt2046_touch.h
fonts.h
img.h
Если тача нет, то
Номер SPI и пины задефайнены в файлах
В
#include "ILI9341_GFX.h"
#include "fonts.h"
#include "img.h"
#include "xpt2046_touch.h"
Далее перед бесконечным циклом добавляем следующий код…
/* USER CODE BEGIN 2 */
HAL_UART_Transmit(&huart1, (uint8_t*)"Start\n", 6, 1000);
__HAL_SPI_ENABLE(DISP_SPI_PTR); // включаем SPI
DISP_CS_UNSELECT;
TOUCH_CS_UNSELECT; // это нужно только если есть тач
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Init(); // инициализация дисплея
/////////////////////////// далее демонстрируются различные пользовательские функции ////////////////////////////
ILI9341_Set_Rotation(SCREEN_HORIZONTAL_2); // установка ориентации экрана (варианты в файле ILI9341_GFX.h)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Fill_Screen(MYFON); // заливка всего экрана цветом (цвета в файле ILI9341_GFX.h)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* вывод строк разными шрифтами (шрифты определены в файле fonts.h, а массивы шрифтов в файле fonts.c)
первый и второй аргумент это начало координат (справа, сверху), четвёртый аргумент шрифт
два последних аргумента это цвет шрифта и цвет фона шрифта */
ILI9341_WriteString(10, 10, "Hello World", Font_7x10, WHITE, MYFON); // можно передавать непосредственно текст
ILI9341_WriteString(20, 30, "Hello World", Font_11x18, WHITE, MYFON);
ILI9341_WriteString(30, 60, "Hello World", Font_16x26, BLUE, DARKGREEN);
char txt_buf[] = "Hello World";
ILI9341_WriteString(40, 96, txt_buf, Font_16x26, RED, GREEN); // можно передавать массив
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Pixel(100, 100, WHITE); // рисует пиксель (координаты и цвет)
HAL_Delay(300);
ILI9341_Draw_Pixel(102, 100, MAROON);
HAL_Delay(300);
ILI9341_Draw_Pixel(100, 102, BLUE);
HAL_Delay(300);
ILI9341_Draw_Pixel(102, 102, RED);
HAL_Delay(300);
for(uint8_t i = 0; i < 100; i++)
{
ILI9341_Draw_Pixel(i, 20, WHITE);
HAL_Delay(10);
}
for(uint8_t i = 0; i < 100; i++)
{
ILI9341_Draw_Pixel(40, i, BLUE);
HAL_Delay(10);
}
for(uint8_t i = 0; i < 100; i++)
{
ILI9341_Draw_Pixel(i, i, RED);
HAL_Delay(10);
}
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Rectangle(10, 10, 50, 70, WHITE); // рисует закрашеный прямоугольник (первые два аргумента это начальные координаты, а следующие два это ширина и высота)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Horizontal_Line(10, 10, 200, WHITE); // рисует горизонтальную линию (первые два аргумента это начальные координаты, а третий длина)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Vertical_Line(10, 10, 200, WHITE); // рисует вертикальную линию (первые два аргумента это начальные координаты, а третий длина)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Random_line(160, 120, 50, 175, WHITE); // рисует произвольную линию (первые два аргумента это начальные координаты, а третий и четвёртый - конечные)
HAL_Delay(1000);
ILI9341_Random_line(123, 180, 150, 75, WHITE);
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Hollow_Circle(100, 100, 50, WHITE); // рисует прозрачный круг (первые два аргумента это координаты центра, а третий радиус)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Filled_Circle(150, 100, 40, WHITE); // рисует закрашеный круг (первые два аргумента это координаты центра, а третий радиус)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Hollow_Rectangle_Coord(10, 10, 50, 70, WHITE); // рисует прозрачный прямоугольник (первые два аргумента это начальные координаты, а вторые два конечные)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ILI9341_Draw_Filled_Rectangle_Coord(20, 20, 70, 60, WHITE); // рисует закрашеный прямоугольник (первые два аргумента это начальные координаты, а вторые два конечные)
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
////////////////////////////////// ВЫВОД КАРТИНКИ ИЗ МАССИВА //////////////////////////////////////////
uint16_t size_img = sizeof(img_logo); // размер картинки в байтах (картинка лежит в файле img.h)
ILI9341_Draw_Image(img_logo, 30, 30, 80, 80, size_img); // подробности см. в статье
HAL_Delay(1000);
//////////////////////////////////// смена ориентации экрана ///////////////////////////////////////
ILI9341_Fill_Screen(MYFON);
ILI9341_Set_Rotation(SCREEN_VERTICAL_1);
ILI9341_Draw_Image(img_logo, 30, 30, 80, 80, size_img);
ILI9341_WriteString(30, 120, "SCREEN_VERTICAL_1", Font_11x18, WHITE, MYFON);
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
ILI9341_Set_Rotation(SCREEN_HORIZONTAL_1);
ILI9341_Draw_Image(img_logo, 30, 30, 80, 80, size_img);
ILI9341_WriteString(30, 120, "SCREEN_HORIZONTAL_1", Font_11x18, WHITE, MYFON);
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
ILI9341_Set_Rotation(SCREEN_VERTICAL_2);
ILI9341_Draw_Image(img_logo, 30, 30, 80, 80, size_img);
ILI9341_WriteString(30, 120, "SCREEN_VERTICAL_2", Font_11x18, WHITE, MYFON);
HAL_Delay(1000);
ILI9341_Fill_Screen(MYFON);
ILI9341_Set_Rotation(SCREEN_HORIZONTAL_2);
ILI9341_Draw_Image(img_logo, 30, 30, 80, 80, size_img);
ILI9341_WriteString(30, 120, "SCREEN_HORIZONTAL_2", Font_11x18, WHITE, MYFON);
/* USER CODE END 2 */
Всё это есть в примере, но тем не менее продублирую.
Здесь перечислены и прокомментированы все пользовательские функции, которые нужны для вывода текста, различных фигур, и картинок из внутренней памяти микроконтроллера.
Условимся что сейчас будем пользоваться экраном в горизонтальном положении. Если будете использовать экран в вертикальном положении, тогда нужно поменять местами значения ширины и высоты в файле
///////////////// ширина высота ///////////////////
#define ILI9341_SCREEN_WIDTH 320
#define ILI9341_SCREEN_HEIGHT 240
В этом же файле задефайнены другие цвета, и указан ресурс для создания своих.
В самом начале программы мы задали ориентацию зкрана…
ILI9341_Set_Rotation(SCREEN_HORIZONTAL_2);
Аргумент этой функции указывает от какого угла ведётся отсчёт начальных координат. В примере, в самом конце, это демонстрируется — начальные координаты у картинки и текста везде одинаковые, а угол от которого они начинаются меняется.
Собственно можно прошивать и смотреть что получилось. Если дошло до моего логотипа значит всё
![](http://istarik.ru/uploads/images/00/00/01/2020/04/09/20f6ae.png)
Теперь по поводу функции вывода картинок из массива…
ILI9341_Draw_Image(img_logo, 30, 30, 80, 80, size_img);
Она используется для вывода картинок хранящихся во внутренней памяти МК в виде массива. В частности мой логотип лежит в файле
Первый аргумент функции, это указатель на массив, второй и третий это начальные координаты, четвёртый и пятый это ширина и высота картинки, а последний это её размер в байтах.
Конвертер
Чтобы из ваших картинок создавать массивы правильного формата, можно воспользоваться специальным онлайн-ресурсом…
![](http://istarik.ru/uploads/images/00/00/01/2020/04/10/f350b0.jpg)
Выберите файл, укажите
Чтоб прикинуть размер будущей картинки, умножьте ширину на высоту, и умножьте это на 2 (картинка 16-ти битная, то есть на каждый пиксель два байта). На примере моего логотипа это выглядит так: 80 * 80 * 2 = 12800 байт.
Другие изображения можно так же добавлять в этот файл, только имена массивов меняйте.
Там же внизу можно скачать файл в бинарном виде —
На тот случай, если ресурс окажется не рабочим, я создал свой онлайн-конвертер. Работает очень просто — выбираете файл, загружаете, и если всё хорошо, через несколько секунд
Так же в библиотеке есть папка Converter, в которой лежит PHP-скрипт для использования на своём компе (понадобиться установленный php7+). Пример использования…
php img.php myfile.jpg
После выполнения появятся описанные выше файлы.
Тачскрин
Тачскрин будем опрашивать в бесконечном цикле, точнее не тачскрин, а пин IRQ, который сигнализирует о нажатии. Перед бесконечным циклом добавьте несколько переменных…
char buf[64] = {0,};
uint8_t flag_press = 1;
uint32_t time_press = 0;
uint8_t flag_hold = 1;
uint32_t timme_hold = 0;
uint16_t x = 0;
uint16_t y = 0;
А в цикл добавляем вот такой код…
while (1)
{
if(HAL_GPIO_ReadPin(IRQ_GPIO_Port, IRQ_Pin) == GPIO_PIN_RESET && flag_press) // если нажат тачскрин
{
x = 0;
y = 0;
TOUCH_CS_UNSELECT;
DISP_CS_UNSELECT;
HAL_SPI_DeInit(DISP_SPI_PTR);
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64;
HAL_SPI_Init(DISP_SPI_PTR);
if(ILI9341_TouchGetCoordinates(&x, &y))
{
flag_press = 0;
//////// вывод координат в уарт для отладки ////////
snprintf(buf, 64, "X = %d, Y = %d\n", x, y);
HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
buf[strlen(buf) - 1] = '\0';
////////////////////////////////////////////////////
}
HAL_SPI_DeInit(DISP_SPI_PTR);
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
HAL_SPI_Init(DISP_SPI_PTR);
__HAL_SPI_ENABLE(DISP_SPI_PTR);
DISP_CS_SELECT;
//////// вывод координат на экран для отладки ////////
ILI9341_Fill_Screen(MYFON);
ILI9341_WriteString(10, 120, buf, Font_11x18, WHITE, MYFON);
//////////////////////////////////////////////////////
if(x > 250 && x < 285 && y > 65 && y < 96) // если нажатие происходит в области этих координат
{
// что-то делаем
}
else if(x > 120 && x < 160 && y > 50 && y < 90) // если нажатие происходит в области этих координат
{
// что-то делаем
}
///////////////// если нажатие происходит c удержанием кнопки ////////////////
else if(x > 5 && x < 90 && y > 160 && y < 230 && flag_hold) // первая кнопка
{
flag_hold = 0;
timme_hold = HAL_GetTick();
// здесь ничего не делаем
}
else if(x > 100 && x < 200 && y > 160 && y < 230 && flag_hold) // вторая кнопка
{
flag_hold = 0;
timme_hold = HAL_GetTick();
// здесь ничего не делаем
}
///////////////////////////
time_press = HAL_GetTick();
}
if(!flag_press && (HAL_GetTick() - time_press) > 200) // задержка до следующего нажатия
{
flag_press = 1;
}
//////////////////////////// удержание кнопки //////////////////////////////
if(!flag_hold && HAL_GPIO_ReadPin(IRQ_GPIO_Port, IRQ_Pin) != GPIO_PIN_RESET)
{
flag_hold = 1;
}
if(!flag_hold && (HAL_GetTick() - timme_hold) > 2000) // 2 sek удержание кнопки
{
if(HAL_GPIO_ReadPin(IRQ_GPIO_Port, IRQ_Pin) == GPIO_PIN_RESET)
{
if(x > 5 && x < 90 && y > 160 && y < 230) // первая кнопка
{
HAL_UART_Transmit(&huart1, (uint8_t*)"LONG PRESS_1\n", 13, 100); // отладка
ILI9341_WriteString(10, 150, "LONG PRESS_1", Font_11x18, WHITE, MYFON); // отладка
// что-то делаем
}
else if(x > 100 && x < 200 && y > 160 && y < 230) // вторая кнопка
{
HAL_UART_Transmit(&huart1, (uint8_t*)"LONG PRESS_2\n", 13, 100); // отладка
ILI9341_WriteString(10, 150, "LONG PRESS_1", Font_11x18, WHITE, MYFON); // отладка
// что-то делаем
}
}
flag_hold = 1;
}
/////////////////////////////////////////////////////////////////////////////
// далее какой-то ваш код, который не должен сильно тормозить цикл
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
Когда условие «если нажат тачскрин» сработает (пин IRQ выдаст низкий уровень), SPI переключится на меньшую скорость, функция
Важно! Если у вас какой-то другой камень, то посмотрите в Кубе какие нужно прописать делители —
Теперь можно прошить и потыкать в экран.
В USART и на экран будут выведены координаты нажатия. Это отладочная инфа, в дальнейшем её можно закомментировать. Там же приведён пример как пользоваться этими координатами. То есть, допустим вы рисуете какую-то кнопку на экране, тыкаете по углам этой кнопки, получаете координаты области, и прописываете их в условии — «если больше и меньше по Х, и больше и меньше по Y, тогда что-то делаем».
Условие «задержка до следующего нажатия» нужно чтоб данные не сыпались как сумасшедшие, типа защита от типа «дребезга».
Ну и «удержание кнопки» говорит само за себя, время удержания можно установить какое нужно. При этом ничего в цикле не тормозится. Короткое нажатие на эту кнопку ничего не делает. То есть это сделано не для того чтоб было два варианта действий на одной кнопке, а для защиты от случайного нажатия, вдруг у вас эта кнопка что-то типа "АЗ-5".
Сам по себе тачскрин в связке с чипом XPT2046 является банальным АЦП, и при нажатии выдаёт два 16-ти битных значения (по горизонтали и по вертикали), которые преобразовываются в функции
#define TOUCH_MIN_RAW_X 1500
#define TOUCH_MAX_RAW_X 30000
#define TOUCH_MIN_RAW_Y 2500
#define TOUCH_MAX_RAW_Y 30000
Опытным путём разберётесь какое значение к какому краю относится. У себя я их округлил. Можно было бы сделать калибровку с крестиками по углам, но мне это делать не охота, да и необходимости особой нет.
На этом наверно пока всё, как говорилось выше, в следующей части речь пойдёт о хранении больших картинок на внешнем носителе и выводе их с помощью DMA.
Библиотека собрана из двух, взятых отсюда и отсюда.
Всем спасибо
![](https://istarik.ru/uploads/images/00/00/01/2019/10/17/1db9cc.jpg)
Телеграм-чат istarik
Телеграм-чат STM32
![](http://istarik.ru/uploads/images/thnd.png)
- 0
- stD
58662
Поддержать автора
Комментарии (0)