Одна из самых удачных разновидностей отладочных плат семейства 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 поддерживают, если верить даташитам.
Схему бы.. уже которую неделю бьюсь, пытаюсь и по видео и на стопкадрах понять, что куда подключено, сам код компилируется без проблем, схема из статьи неполная., так как даже на видео видно что проводов используется больше, при этом в комментах еще одна схема подключения. при этом по этим схемам подключить не получается.