Одна из самых удачных разновидностей отладочных плат семейства ESP32 — это ESP32CAM. Все дело в разъёме для видеокамеры, к которому обычно уже подключена OV2640.
Камера по современным меркам имеет скудные характеристики, но вполне подойдет для большинства DIY-идей. Матрица 2Мп выдаёт максимальное разрешение 1600 x 1200 точек. Поддерживаются функции автоматической выдержки, баланса белого, усиления сигнала.

Дисплей
Возникает вопрос: а можно ли изображение с камеры не сохранять на SD-карту и не передавать через Wi-Fi, а транслировать на дисплей? Справится ли esp32 с такой работой?
Попробуем это сделать. Для примера возьмём небольшой TFT модуль с матрицей 1,8 дюйма и контроллером ST7735.
Для эксперимента потребуется esp32cam, tft дисплей, макетная плата и немного проводов. Если чего-то не хватает, это можно купить в RobotClass:
Подключение
Если бы у нас был любой другой модуль 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 и наблюдаем результат:
Схема подключения:
IO2 — A0
IO14 — SCK
IO15 — CS
IO13 — SDA
IO16 — RESET
Почему сразу в Pixel_format не выбрать bmp вместо jpg, чтобы избежать лишней траты ресурсов на конвертирование? Bmp формат любые камеры для esp32 поддерживают, если верить даташитам.
Схему бы.. уже которую неделю бьюсь, пытаюсь и по видео и на стопкадрах понять, что куда подключено, сам код компилируется без проблем, схема из статьи неполная., так как даже на видео видно что проводов используется больше, при этом в комментах еще одна схема подключения. при этом по этим схемам подключить не получается.
Здравствуйте! Подскажите, схема будет работать на дисплее tft 1.77 дюйма? При попытках подключения виден только белый экран…
Надо знать модель контроллера дисплея.
17:48:08.708 -> Rebooting…
17:48:08.708 -> ets Jul 29 2019 12:21:46
17:48:08.748 ->
17:48:08.748 -> rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
17:48:08.748 -> configsip: 0, SPIWP:0xee
17:48:08.748 -> clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
17:48:08.748 -> mode:DIO, clock div:1
17:48:08.748 -> load:0x3fff0030,len:4980
17:48:08.748 -> load:0x40078000,len:16612
17:48:08.748 -> load:0x40080400,len:3480
17:48:08.748 -> entry 0x400805b4
17:48:10.282 -> E (2534) cam_hal: DMA overflow
17:48:10.327 -> E (2574) cam_hal: DMA overflow
17:48:10.327 -> E (2607) cam_hal: DMA overflow
17:48:10.391 -> E (2647) cam_hal: DMA overflow
17:48:10.437 -> E (2687) cam_hal: DMA overflow
17:48:10.484 -> E (2727) cam_hal: DMA overflow
17:48:10.527 -> E (2767) cam_hal: DMA overflow
17:48:10.527 -> E (2807) cam_hal: DMA overflow
17:48:10.607 -> E (2847) cam_hal: DMA overflow
17:48:10.647 -> E (2887) cam_hal: DMA overflow
Все в интернете пересмотрел