Видео с ESP32CAM на TFT-дисплей ST7735
Одна из самых удачных разновидностей отладочных плат семейства ESP32 — это ESP32CAM. Все дело в разъёме для видеокамеры, к которому обычно уже подключена OV2640.
Камера по современным меркам имеет скудные характеристики, но вполне подойдёт для большинства DIY-идей. Матрица 2 Мп выдаёт максимальное разрешение 1600 × 1200 точек. Поддерживаются функции автоматической выдержки, баланса белого, усиления сигнала.
Совет
Если возможностей камеры не хватает, её можно заменить, например, на OV5640.
Дисплей
Возникает вопрос: а можно ли изображение с камеры не сохранять на SD-карту и не передавать через Wi-Fi, а транслировать на дисплей? Справится ли ESP32 с такой работой?
Попробуем это сделать. Для примера возьмём небольшой TFT-модуль с матрицей 1,8 дюйма и контроллером ST7735.
Для эксперимента потребуется ESP32CAM, TFT-дисплей, макетная плата и немного проводов.
Примечание
Плата ESP32 с камерой OV2640 (ESP32-CAM)
Дисплей TFT, 128×160, 1.8»
Провода вилка-вилка, L=10 см
Подключение
Если бы у нас был любой другой модуль ESP32, данный раздел состоял бы из одного абзаца, но у ESP32CAM есть особенность. Дело в том, что камера подключается к ESP32 по интерфейсу SCCB и такое подключение «съедает» один из SPI-интерфейсов микроконтроллера, а именно VSPI.
Эта особенность негативно сказывается на работе многих библиотек для работы с дисплеями, они начинают использовать программный SPI и жутко тормозить, так как работают с этим интерфейсом, используя стандартные библиотеки ESP32-ядра для Arduino IDE.
Мы будем использовать самую лучшую на данный момент библиотеку для работы с дисплеями — TFT_eSPI (ссылка в конце статьи). Даже продвинутые разработчики Adafruit не смогли в своей Adafruit_GFX добиться таких скоростей для программного SPI.
Итак, подключаем дисплей к любым свободным контактам:
ESP32CAM |
2 |
12 |
13 |
14 |
15 |
|---|---|---|---|---|---|
TFT 1.8 ST7735 |
RS(DC) |
RST |
CS |
CLK |
SDA(MOSI) |
Программа
Начнём с проверки работоспособности дисплея. И первый шаг — настройка параметров библиотеки. Это необходимо сделать вручную, открыв в текстовом редакторе файл:
путь_к_библиотекам\Arduino\libraries\TFT_eSPI-master\User_Setup.h
Можно удалить всё содержимое этого файла и оставить только следующие строки:
#define USER_SETUP_INFO "User_Setup"
#define ST7735_DRIVER
#define TFT_WIDTH 128
#define TFT_HEIGHT 160
#define ST7735_BLACKTAB
#define TFT_MISO 16
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 12
#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#define SMOOTH_FONT
#define SPI_FREQUENCY 27000000
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000
Назначение параметров хорошо описаны в файле User_Setup. Не поленитесь перевести и прочитать, если планируете работать с другими дисплеями.
О модификациях ST7735
Указанные настройки подойдут только для дисплея 160×128 с контроллером ST7735. Но важно отметить, что в природе имеется несколько разновидностей таких модулей. DIY-энтузиастами замечена корреляция между цветом ярлычка на защитной плёнке дисплея и его особенностями.
Разработчики библиотеки выделили несколько разновидностей дисплеев и дали пользователю возможность выбрать. В нашем конкретном случае оказалось, что лучше всего подходит такая настройка:
#define ST7735_BLACKTAB
BlackTab означает, что на матрице была наклеена плёнка с ярлычком чёрного цвета. Вот только фактически на дисплее был зелёный ярлычок :)
Совет
Если у вас на дисплее появляются полосы шума по краям или ещё какие-то аномалии изображения, то стоит повыбирать разные модели в данном разделе настроек.
Сохраняем файл, идём дальше.
Тестовый скетч для дисплея
Открываем стандартный пример из библиотеки TFT_eSPI под названием Arduino_Life. Это модель клеточного автомата, описанная впервые английским математиком Джоном Конвеем в далёком 1970.
В коде примера ничего менять не нужно, всё уже настроено в User_Setup. Для правильной работы библиотеки в Arduino IDE необходимо выбрать целевую плату «ESP32 Wrover Kit (all versions)».
Примечание
Вероятно, можно подобрать и какие-то другие, но на родной для этого контроллера «AiThinker ESP32-CAM» дисплей не оживал.
Загружаем на ESP32CAM и наблюдаем.
Получение изображения с камеры
Всё, что нам нужно делать для достижения главной цели, это брать кадр из камеры и тут же выводить его на дисплей.
Инициализацию камеры можно подсмотреть в стандартном примере для ESP32-плат:
ESP32/Camera/CameraWebServer
В этих настройках нам важен параметр:
config.frame_size = FRAMESIZE_QQVGA;
Разрешение QQVGA соответствует 160×120 точкам, почти в размер нашего дисплея. Также можно поиграть со степенью сжатия jpeg_quality. Хотя на скорость работы системы это особо не повлияет.
Полный конфиг камеры для нашего эксперимента будет выглядеть так:
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 10000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_QQVGA;
config.jpeg_quality = 8;
config.fb_count = 1;
Инициализация камеры осуществляется вызовом функции:
esp_camera_init(&config);
Изображение хранится в буфере, который можно добыть так:
camera_fb_t* fb = esp_camera_fb_get()
А вот дальше интересно. Ведь камера нам возвращает кадр сжатый с помощью JPEG. Такой формат дисплей, разумеется, есть не захочет. Нужно декодировать JPEG в обычный BMP.
Преобразование JPEG в BMP
На этом шаге мы знакомимся с библиотекой TJpg_Decoder, которую можно скачать из репозитория Arduino IDE или по ссылке в конце статьи (установка библиотеки в Arduino IDE).
Первая функция, которая нам пригодится:
TJpgDec.setCallback(tft_output);
Она вызывается на этапе инициализации. В качестве аргумента — указатель на другую функцию, которая будет вызываться по завершении процедуры сжатия.
Другая функция — drawJpg, собственно, осуществляет преобразование:
TJpgDec.drawJpg(0, 0, (const uint8_t*)fb->buf, fb->len);
Таким образом, каждый раз, получив кадр из камеры, мы должны совершать преобразование. По завершении работы drawJpg будет вызвана другая функция — которая передаст результат в дисплей.
Итоговый код
#include "esp_camera.h"
#include <SPI.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
TFT_eSPI tft = TFT_eSPI();
void setup() {
displayInit();
cameraInit();
}
void loop() {
showingImage();
}
void cameraInit(){
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 10000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_QQVGA; //320x240
config.jpeg_quality = 8;
config.fb_count = 1;
esp_camera_init(&config);
}
void showingImage(){
camera_fb_t* fb = esp_camera_fb_get();
TJpgDec.drawJpg(0, 0, (const uint8_t*)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap){
if( y>= tft.height()) return 0;
tft.pushImage(x, y, w, h, bitmap);
return 1;
}
void displayInit(){
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_WHITE);
TJpgDec.setJpgScale(1);
TJpgDec.setSwapBytes(true);
TJpgDec.setCallback(tft_output);
}
Загружаем программу на ESP32CAM и наблюдаем результат.