├── .gitignore ├── CamToLCD ├── CAM_OV7670.h ├── CamToLCD.ino ├── DMABuffer.h ├── I2SCamera.cpp ├── I2SCamera.h ├── LCD.h └── Log.h ├── ESP32_Pin_Usage.pdf ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | 214 | # Pete's files 215 | *.bak 216 | 217 | -------------------------------------------------------------------------------- /CamToLCD/CAM_OV7670.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 5 | // Released under Apache License 2.0 6 | // Big chunks of the camera-side code were lifted from or inspired by 7 | // https://github.com/bitluni/ESP32CameraI2S by Bitluni, and 8 | // https://github.com/igrr/esp32-cam-demo by Ivan Grokhotkov (igrr) 9 | 10 | // The all-important reference manual for the camera: 11 | // [1] http://web.mit.edu/6.111/www/f2016/tools/OV7670_2006.pdf 12 | 13 | 14 | #include "Wire.h" 15 | #include "I2SCamera.h" 16 | #include "driver/ledc.h" 17 | 18 | bool ClockEnable(int pin, int Hz) 19 | { 20 | periph_module_enable(PERIPH_LEDC_MODULE); 21 | 22 | ledc_timer_config_t timer_conf; 23 | timer_conf.bit_num = (ledc_timer_bit_t)1; 24 | timer_conf.freq_hz = Hz; 25 | timer_conf.speed_mode = LEDC_HIGH_SPEED_MODE; 26 | timer_conf.timer_num = LEDC_TIMER_0; 27 | esp_err_t err = ledc_timer_config(&timer_conf); 28 | if (err != ESP_OK) { 29 | return false; 30 | } 31 | 32 | ledc_channel_config_t ch_conf; 33 | ch_conf.channel = LEDC_CHANNEL_0; 34 | ch_conf.timer_sel = LEDC_TIMER_0; 35 | ch_conf.intr_type = LEDC_INTR_DISABLE; 36 | ch_conf.duty = 1; 37 | ch_conf.speed_mode = LEDC_HIGH_SPEED_MODE; 38 | ch_conf.gpio_num = pin; 39 | err = ledc_channel_config(&ch_conf); 40 | if (err != ESP_OK) { 41 | return false; 42 | } 43 | return true; 44 | } 45 | 46 | void ClockDisable() 47 | { 48 | periph_module_disable(PERIPH_LEDC_MODULE); 49 | } 50 | 51 | // From igrr project 52 | 53 | //camera registers 54 | static const int REG_GAIN = 0x00; 55 | static const int REG_BLUE = 0x01; 56 | static const int REG_RED = 0x02; 57 | static const int REG_COM1 = 0x04; 58 | static const int REG_VREF = 0x03; 59 | static const int REG_COM2 = 0x09; 60 | static const int REG_COM4 = 0x0d; 61 | static const int REG_COM5 = 0x0e; 62 | static const int REG_COM6 = 0x0f; 63 | static const int REG_AECH = 0x10; 64 | static const int REG_CLKRC = 0x11; 65 | static const int REG_COM7 = 0x12; 66 | static const int COM7_RGB = 0x04; 67 | static const int REG_COM8 = 0x13; 68 | static const int COM8_FASTAEC = 0x80; // Enable fast AGC/AEC 69 | static const int COM8_AECSTEP = 0x40; // Unlimited AEC step size 70 | static const int COM8_BFILT = 0x20; // Band filter enable 71 | static const int COM8_AGC = 0x04; // Auto gain enable 72 | static const int COM8_AWB = 0x02; // White balance enable 73 | static const int COM8_AEC = 0x0; 74 | static const int REG_COM9 = 0x14; 75 | static const int REG_COM10 = 0x15; 76 | static const int REG_COM14 = 0x3E; 77 | static const int REG_COM11 = 0x3B; 78 | static const int COM11_NIGHT = 0x80; 79 | static const int COM11_NMFR = 0x60; 80 | static const int COM11_HZAUTO = 0x10; 81 | static const int COM11_50HZ = 0x08; 82 | static const int COM11_EXP = 0x0; 83 | static const int REG_TSLB = 0x3A; 84 | static const int REG_RGB444 = 0x8C; 85 | static const int REG_COM15 = 0x40; 86 | static const int COM15_RGB565 = 0x10; 87 | static const int COM15_R00FF = 0xc0; 88 | static const int REG_HSTART = 0x17; 89 | static const int REG_HSTOP = 0x18; 90 | static const int REG_HREF = 0x32; 91 | static const int REG_VSTART = 0x19; 92 | static const int REG_VSTOP = 0x1A; 93 | static const int REG_COM3 = 0x0C; 94 | static const int REG_MVFP = 0x1E; 95 | static const int REG_COM13 = 0x3d; 96 | static const int COM13_UVSAT = 0x40; 97 | static const int REG_SCALING_XSC = 0x70; 98 | static const int REG_SCALING_YSC = 0x71; 99 | static const int REG_SCALING_DCWCTR = 0x72; 100 | static const int REG_SCALING_PCLK_DIV = 0x73; 101 | static const int REG_SCALING_PCLK_DELAY = 0xa2; 102 | static const int REG_BD50MAX = 0xa5; 103 | static const int REG_BD60MAX = 0xab; 104 | static const int REG_AEW = 0x24; 105 | static const int REG_AEB = 0x25; 106 | static const int REG_VPT = 0x26; 107 | static const int REG_HAECC1 = 0x9f; 108 | static const int REG_HAECC2 = 0xa0; 109 | static const int REG_HAECC3 = 0xa6; 110 | static const int REG_HAECC4 = 0xa7; 111 | static const int REG_HAECC5 = 0xa8; 112 | static const int REG_HAECC6 = 0xa9; 113 | static const int REG_HAECC7 = 0xaa; 114 | static const int REG_COM12 = 0x3c; 115 | static const int REG_GFIX = 0x69; 116 | static const int REG_COM16 = 0x41; 117 | static const int COM16_AWBGAIN = 0x08; 118 | static const int REG_EDGE = 0x3f; 119 | static const int REG_REG76 = 0x76; 120 | static const int ADCCTR0 = 0x20; 121 | 122 | 123 | class pw_OV7670: public I2SCamera 124 | { 125 | 126 | enum Mode 127 | { 128 | QQQVGA_RGB565, 129 | QQVGA_RGB565, 130 | QVGA_RGB565 131 | }; 132 | 133 | public: 134 | 135 | int mode; 136 | 137 | int readRegister(unsigned char reg) { 138 | 139 | // Magic SCCB 8-bit write address hard-wired into OV7670 is 0x42. 140 | // ([1] pg 11). Wire wants a 7-bit base address, i.e. 0x21 141 | Wire.beginTransmission(0x21); 142 | Wire.write(reg); 143 | Wire.endTransmission(1); // the argument is critical 144 | Wire.requestFrom(0x21, 1, 1); // request 1 byte from slave device 145 | int timeout = 100000; 146 | while (--timeout > 0) { 147 | int val = Wire.read(); // returns -1 if no data yet available. 148 | if (val >= 0) return val; 149 | } 150 | return -1; // Give up 151 | } 152 | 153 | void writeRegister(unsigned char reg, unsigned char val) { 154 | Wire.beginTransmission(0x21); 155 | Wire.write(reg); 156 | Wire.write(val); 157 | Wire.endTransmission(1); 158 | } 159 | 160 | void testPattern(int kind) // 0-none, or 1,2,3 161 | { 162 | writeRegister(0x70, (readRegister(0x70) & 0x7F) | ((kind & 0x01) << 7)); 163 | writeRegister(0x71, (readRegister(0x71) & 0x7F) | ((kind & 0x02) << 6)); 164 | } 165 | 166 | void subsamplingControl(int com14, int downSample, int pclk_div) 167 | { 168 | writeRegister(REG_COM3, 0x04); //DCW enable 169 | 170 | writeRegister(REG_COM14, com14); //pixel clock divided by 4, manual scaling enable, DCW and PCLK controlled by register 171 | writeRegister(REG_SCALING_XSC, 0x3a); 172 | writeRegister(REG_SCALING_YSC, 0x35); 173 | 174 | writeRegister(REG_SCALING_DCWCTR, downSample); 175 | writeRegister(REG_SCALING_PCLK_DIV, pclk_div); //pixel clock divided by 4 176 | writeRegister(REG_SCALING_PCLK_DELAY, 0x02); 177 | } 178 | 179 | void frameControl(int hStart, int vStart) 180 | { 181 | int hStop = (hStart + 640) % 784; 182 | writeRegister(REG_HSTART, hStart >> 3); 183 | writeRegister(REG_HSTOP, hStop >> 3); 184 | writeRegister(REG_HREF, ((hStop & 0b111) << 3) | (hStart & 0b111)); 185 | 186 | int vStop = (vStart + 480); 187 | writeRegister(REG_VSTART, vStart >> 2); 188 | writeRegister(REG_VSTOP, vStop >> 2); 189 | writeRegister(REG_VREF, ((vStop & 0b11) << 2) | (vStart & 0b11)); 190 | } 191 | 192 | void saturation(int s) //-2 to 2 193 | { 194 | //color matrix values 195 | writeRegister(0x4f, 0x80 + 0x20 * s); 196 | writeRegister(0x50, 0x80 + 0x20 * s); 197 | writeRegister(0x51, 0x00); 198 | writeRegister(0x52, 0x22 + (0x11 * s) / 2); 199 | writeRegister(0x53, 0x5e + (0x2f * s) / 2); 200 | writeRegister(0x54, 0x80 + 0x20 * s); 201 | writeRegister(0x58, 0x9e); //matrix signs 202 | } 203 | 204 | void autoDeNoise(bool wanted) 205 | { 206 | if (wanted) { 207 | writeRegister(REG_COM16, readRegister(REG_COM16) | (1 << 4)); // set bit 4 208 | } 209 | else { 210 | writeRegister(REG_COM16, readRegister(REG_COM16) & (~(1 << 4))); // unset bit 4 211 | } 212 | } 213 | 214 | void softSleep(bool mustSleep) // false will wake up camera, true will make it sleep 215 | { 216 | if (mustSleep) { 217 | writeRegister(REG_COM2, (readRegister(REG_COM2) | (1 << 4))); 218 | } 219 | else { 220 | writeRegister(REG_COM2, (readRegister(REG_COM2) & (~(1 << 4)))); 221 | } 222 | } 223 | 224 | void setDriveStrength(int val) { // 0 to 3 225 | val = val % 4; 226 | writeRegister(REG_COM2, (readRegister(REG_COM2) & 0xFC) | val); 227 | } 228 | 229 | 230 | 231 | void setMode(int mode, bool autoStart) { // 0,1 or 2 for QQQ, QQ, or Q VGA. All are RGB565 232 | 233 | stop(); // In case the camera is already running 234 | 235 | if (mode < 0 || mode > 2) { // Sanity check on argument 236 | mode = 2; 237 | } 238 | 239 | this->mode = mode; 240 | 241 | writeRegister(REG_COM7, 0b10000000); // all registers default 242 | writeRegister(REG_CLKRC, 0b10000000); // double clock?? My spec sheet says Reserved 243 | writeRegister(REG_COM11, 0b1000 | 0b10); // enable auto 50/60Hz detect + exposure timing can be less... 244 | writeRegister(REG_COM7, 0b100); // RGB 245 | writeRegister(REG_COM15, 0b11000000 | 0b010000); //RGB565 246 | 247 | switch (mode) { 248 | case 0: 249 | xres = 80; 250 | yres = 60; 251 | subsamplingControl(0x1B, 0x33, 0xF3); 252 | frameControl(196, 14); 253 | break; 254 | case 1: 255 | xres = 160; 256 | yres = 120; 257 | subsamplingControl(0x1A, 0x22, 0xF2); 258 | frameControl(174, 14); 259 | break; 260 | case 2: 261 | xres = 320; 262 | yres = 240; 263 | subsamplingControl(0x19, 0x11, 0xF1); 264 | frameControl(154, 14); 265 | break; 266 | } 267 | 268 | //writeRegister(REG_COM10, 0x02); //VSYNC negative 269 | //writeRegister(REG_MVFP, 0x2b); //mirror flip 270 | 271 | writeRegister(0xb0, 0x84); // no clue what this is but it's most important for colors 272 | saturation(1); 273 | writeRegister(0x13, 0xe7); // AGC AWB AEC all on 274 | writeRegister(0x6f, 0x9f); // Simple AWB 275 | 276 | // What is this comment about? I tried but don't see any difference. 277 | // Line 1029 of https://github.com/yandex/smart/blob/master/drivers/media/i2c/ov7670.c 278 | writeRegister(REG_CLKRC, 0b10000000); 279 | 280 | 281 | if (autoStart) { 282 | start(); 283 | } 284 | debug_printf("Mode set to %d with xres=%d, autoStart=%d\n", mode, xres, autoStart); 285 | } 286 | 287 | pw_OV7670(const int SIOD, const int SIOC, const int VSYNC, const int HREF, const int XCLK, const int PCLK, const int databus[], 288 | void (*scanlineListener)(DMABuffer *buf), 289 | void (*vSyncListener)()) 290 | { 291 | Wire.begin(SIOD, SIOC); // join the i2c bus (address optional for master) 292 | Wire.reset(); // Without this, we cannot always get XCLK clock going. 293 | 294 | while (true) { 295 | bool succeeded = true; 296 | ClockEnable(XCLK, 20000000); // base is 80MHz, at 20MHz I get 25 VSYNCs per sec 297 | 298 | // Observation: this seems to work on the scope at 40Mhz, 20Mhz, etc. At fast rates, maybe 299 | // because of long leads and capacitance on my breadboard, the peak-to-peak voltage reduces 300 | // from 3.3v down to about 2V p-p at 20MHz. At 10Mhz the wave starts to show a flat top and 301 | // flat bottom, and it is no longer de-stabilized by flashing a torch into the camera. 302 | // http://embeddedprogrammer.blogspot.com/2012/07/hacking-ov7670-camera-module-sccb-cheat.html 303 | // says spec sheet limits for clock are 10MHz - 48MHz. Others report running it slower than 304 | // 10MHz with success. 305 | 306 | // Once the clock is running the camera will deliver frames, and a VSYNC at the 307 | // end of each frame. So this is really just a sanity check. 308 | pinMode(VSYNC, INPUT); 309 | int ttl = 1000000; 310 | while (!digitalRead(VSYNC) && --ttl > 0); // Wait, but fail if it takes too long. 311 | if (ttl <= 0) { 312 | succeeded = false; 313 | Serial.println(" VSYNC never went low"); 314 | } 315 | ttl == 100000; 316 | while (digitalRead(VSYNC) && --ttl > 0); 317 | if (ttl <= 0) { 318 | succeeded = false; 319 | Serial.println(" VSYNC never went high"); 320 | } 321 | if (succeeded) { 322 | //Serial.println(" VSYNC done"); 323 | break; // out of the retry loop 324 | } 325 | } 326 | delay(10); 327 | 328 | // Once we can talk to the camera using SCCB, set up the i2s data path and the interrupts, etc. 329 | I2SCamera::init(VSYNC, HREF, PCLK, databus, scanlineListener, vSyncListener); 330 | } 331 | 332 | void TestSuite(char *caller) 333 | { 334 | // The camera can auto adjust gain, exposure, etc. and as it does 335 | // so it writes current settings to some registers. 336 | // So we use -1 in the table below to mean "unpredictable" value. 337 | // This is just part of a test harness to ensure some sanity. 338 | // It may well be that other versions of the camera have some 339 | // different values in the registers. 340 | // Once we write setup data to the registers, some tests may fail. 341 | // So this test is probably only valid directly after a reset of the camera. 342 | const int NumRegs = 0xCA; 343 | int expectedInRegister[NumRegs] = 344 | { -1, -1, -1, -1, -1, -1, -1, -1, 345 | -1, 0x01, 0x76, 0x73, 0x00, 0x00, 0x01, 0x43, 346 | -1, 0x80, 0x00, 0x8F, 0x4A, 0x00, 0x00, 0x11, 347 | 0x61, 0x03, 0x7B, 0x00, 0x7F, 0xA2, 0x01, 0x00, 348 | 0x04, 0x02, 0x01, 0x00, 0x75, 0x63, 0xD4, 0x80, 349 | 0x80, -1, 0x00, 0x00, 0x80, 0x00, 0x00, -1, 350 | 0x08, 0x30, 0x80, 0x08, 0x11, -1, -1, 0x3F, 351 | 0x01, 0x00, 0x0D, 0x00, 0x68, 0x88, 0x00, 0x00, 352 | 0xC0, 0x08, 0x00, 0X14, 0xF0, 0x45, 0x61, 0x51, 353 | 0x79, -1, -1, 0X00, 0x00, -1, -1, 0x40, 354 | 0X34, 0X0C, 0X17, 0X29, 0X40, 0X00, 0X40, 0X80, 355 | 0X1E, 0X91, 0X94, 0XAA, 0X71, 0X8D, 0X0F, 0XF0, 356 | 0XF0, 0XF0, 0X00, 0X00, 0X50, 0X30, 0X00, 0X80, 357 | 0X80, 0X00, -1, 0X0A, 0X02, 0X55, 0XC0, 0X9A, 358 | 0X3A, 0X35, 0X11, 0X00, 0X00, 0X0F, 0X01, 0X10, 359 | 0X00, 0X00, 0X24, 0X04, 0X07, 0X10, 0X28, 0X36, 360 | 0X44, 0X52, 0X60, 0X6C, 0X78, 0X8C, 0X9E, 0XBB, 361 | 0XD2, 0XE5, 0X00, 0X00, 0X00, 0X0F, 0X00, 0X00, 362 | 0X00, 0X00, 0X00, 0X00, 0X50, 0X50, 0X01, 0X01, 363 | 0X10, 0X40, 0X40, 0X20, 0X00, 0X99, 0X7F, 0XC0, 364 | 0X90, 0X03, 0X02, -1, 0X00, 0X0F, 0XF0, 0XC1, 365 | 0XF0, 0XC1, 0X14, 0X0F, 0X00, 0X80, 0X80, 0X80, 366 | 0X00, 0X00, 0X00, 0X80, 0X00, 0X04, 0X00, 0X66, 367 | 0X00, 0X06, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 368 | 0X00, 0X00, -1, -1, -1, -1, -1, -1, 369 | -1, 0xC0 370 | }; 371 | 372 | Wire.reset(); 373 | 374 | Serial.printf("--- OV7670 sanity unit tests (called from %s) ---\n", caller); 375 | // Can we read correct expected values from registers? 376 | int passed = 0, failed = 0, skipped = 0; 377 | bool hadPasses = false; 378 | for (int i = 0; i < NumRegs; i++) { 379 | int expected = expectedInRegister[i]; 380 | if (expected < 0) { 381 | skipped++; 382 | } 383 | else { 384 | int val = readRegister(i); 385 | if (val == expected) { 386 | // Serial.printf("%02x=%02x ", i, val); 387 | passed++; 388 | hadPasses = true; 389 | } 390 | else { 391 | if (hadPasses) { 392 | printf("passed.\n"); 393 | hadPasses = false; 394 | } 395 | if (failed < 10) { 396 | Serial.printf("Reg %02x: %02x, fail - expected to get %02x.\n", i, val, expected); 397 | } 398 | failed++; 399 | } 400 | } 401 | } 402 | if (failed == 0) { 403 | Serial.printf("--- YAY! - readRegister() tests all passed!\n"); 404 | } 405 | else { 406 | Serial.printf("\n--- readRegister: Passed=%d, Failed=%d, Skipped=%d\n", passed, failed, skipped); 407 | } 408 | 409 | // Now write to a register and see that it works OK. 410 | int testReg = 0x1A; // this register looks innocuous enough... 411 | int val1 = readRegister(testReg); 412 | int val2 = 0x55; 413 | writeRegister(testReg, val2); 414 | int val3 = readRegister(testReg); 415 | // Put the original value back and see that it also gets there. 416 | writeRegister(testReg, val1); 417 | int val4 = readRegister(testReg); 418 | if (val1 == val4 && val2 == val3) { 419 | Serial.printf("--- YAY! - registerWrite() / readback passed!\n"); 420 | } 421 | else 422 | { 423 | Serial.printf("Register write: failed.\nStarted as %02X, wrote %02X, read back %02X, and finally had %02X .\n", val1, val2, val3, val4); 424 | } 425 | if (failed > 0) { 426 | Serial.printf("Register values are only relaible after a camera RESET.\n"); 427 | Serial.printf("If your camera RESET pin is wired high, you'll need to power everything off.\n"); 428 | } 429 | Serial.printf("--- OV7670 test suite completed. ---\n"); 430 | } 431 | 432 | }; 433 | 434 | -------------------------------------------------------------------------------- /CamToLCD/CamToLCD.ino: -------------------------------------------------------------------------------- 1 | 2 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 3 | 4 | // Get my OV7670 camera to deliver frames to my ILI9341 TFT LCD. 5 | // Big chunks of the camera-side code were lifted from or inspired by 6 | // https://github.com/bitluni/ESP32CameraI2S by Bitluni, and 7 | // https://github.com/igrr/esp32-cam-demo by Ivan Grokhotkov (igrr) 8 | // The LCD side of things started from some driver code on the Banggood site. 9 | 10 | // Released under Apache License 2.0 11 | 12 | // Uncomment this define to get some debugging, but perhaps slow down XCLK a bit first ... 13 | //#define DebuggingPort 14 | 15 | #include "Log.h" 16 | 17 | #ifdef DebuggingPort // if we are going to use debugging, create a debug sink. 18 | DebugPort debug(&Serial, false); 19 | #endif 20 | 21 | #include "LCD.h" 22 | LCD *theLCD; 23 | 24 | #include "CAM_OV7670.h" 25 | pw_OV7670 *theCam; 26 | 27 | int missedBlocks = 0, blockCount = 0, framesGrabbed = 0; 28 | 29 | // The control logic is a state machine: 30 | enum State { 31 | Lost, // We don't know where we are, and need a VSYNC to synchronize ourselves. 32 | Priming, // When we hit this state we'll prime the sink: e.g. send a frame header, open a file, set up LCD etc. 33 | Running, // Queueing blocks as they arrive in the interrupt handlier, and sinking the data in the main loop. 34 | Wrapup, // We got a VSYNC. We can wrap up the frame / close a file, restart the I2S engine, print stats, etc. 35 | Overrun // If either VSYNC or a scanline interrupts before we're finalized, we've lost the beat. 36 | }; 37 | char *stateNames[] = {"Lost", "Priming", "Running", "Wrapup", "Overrun"}; 38 | State theState; 39 | 40 | //******* The camera. SIOD and SIOC pins need pull-ups to Vcc(3.3v) (4.7k ohms seems OK) ********** 41 | 42 | // The "System" is a combination of the interrupt 43 | // callbacks that produces blocks, and the sink mechanism that consumes the blocks. 44 | // Here a block is always a full scanline. But the DMA buffers have limited capacity, 45 | // and the base i2sCamera code anticipates future cameras where a scanline might need 46 | // more than one DMA block. 47 | 48 | DMABuffer *queuedBlock = 0; 49 | // This listener gets a callback after every scan line. 50 | void IRAM_ATTR sinkOneScanline(DMABuffer *buf) 51 | { 52 | 53 | #ifdef DebuggingPort 54 | blockCount++; 55 | #endif 56 | 57 | switch (theState) { 58 | 59 | case Lost: // We just stay Lost. Only a VSYNC can rescue us. 60 | break; 61 | 62 | case Priming: 63 | #ifdef DebuggingPort 64 | if (queuedBlock != 0) { // problem, we've not fully dealt with the last buffer yet. 65 | debug.print('f'); 66 | } 67 | #endif 68 | queuedBlock = buf; // queue the first scanline for handling 69 | break; 70 | 71 | case Running: 72 | #ifdef DebuggingPort 73 | if (queuedBlock != 0) { // problem, cos we've not dealt with the last buffer yet. 74 | debug.print('F'); 75 | } 76 | #endif 77 | queuedBlock = buf; 78 | break; 79 | 80 | case Wrapup: 81 | missedBlocks++; 82 | theState = Overrun; 83 | break; 84 | 85 | case Overrun: 86 | missedBlocks++; 87 | break; 88 | } 89 | } 90 | 91 | // If registered, this listener gets a callback after every VSYNC. 92 | void IRAM_ATTR handleVSYNC() 93 | { 94 | #ifdef DebuggingPort 95 | if (blockCount == theCam->yres) { 96 | // debug_print("f"); 97 | } 98 | else { 99 | debug_printf("{%d} ", blockCount); // We did not get the expected numer of scanlines. 100 | } 101 | 102 | blockCount = 0; 103 | #endif 104 | 105 | 106 | switch (theState) { 107 | case Lost: 108 | theState = Priming; // The main loop can start preparing to send the next frame. 109 | break; 110 | 111 | case Priming: 112 | debug_print("."); 113 | // Serial.println("Unexpected VSYNC when we are in state Priming"); 114 | theState = Lost; 115 | break; 116 | 117 | case Running: 118 | theState = Wrapup; // Tell the main loop we're at the end of a frame. 119 | break; 120 | 121 | case Wrapup: 122 | debug_print("M"); 123 | theState = Overrun; 124 | break; 125 | 126 | case Overrun: 127 | debug_print("m"); 128 | break; 129 | } 130 | } 131 | 132 | void reclaim_JTAG_pins() 133 | { // https://www.esp32.com/viewtopic.php?t=2687 134 | // At reset, these pins (12,13,14,15) are configured for JTAG function. 135 | // You need to change function back to GPIO in the IO MUX to make the pins work as GPIOs. 136 | // If you use GPIO driver (include "driver/gpio.h", not "rom/gpio.h"), 137 | // it will configure the pin as GPIO for you, once you call gpio_config to configure the pin. 138 | PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[12], PIN_FUNC_GPIO); 139 | PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[13], PIN_FUNC_GPIO); 140 | PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[14], PIN_FUNC_GPIO); 141 | PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[15], PIN_FUNC_GPIO); 142 | } 143 | 144 | void setup() { 145 | Serial.begin(115200); 146 | Serial.printf("\nPete's CamToLCD, V1.0 (%s %s)\n%s\n", __DATE__, __TIME__, __FILE__); 147 | 148 | reclaim_JTAG_pins(); 149 | debug_enableOutput(false); 150 | 151 | theLCD = new LCD(); 152 | theLCD->Setup(); 153 | theLCD->TestSuite(); 154 | 155 | // Camera pin names D9 D8 D7 D6 D5 D4 D3 D2 156 | int cam_databus[] = { 13, 35, 12, 32, 14, 33, 27, 25}; // lsb on the right 157 | int VP = 36; 158 | int VN = 39; 159 | // SIOD, SIOC, VSYNC, HREF, XCLK, PCLK ... 160 | theCam = new pw_OV7670(05, 00, VN, 34, 26, VP, cam_databus, sinkOneScanline, handleVSYNC); 161 | theCam->TestSuite("Setup"); 162 | theCam->setMode(2, true); 163 | } 164 | 165 | // This is for interactively tweaking camera register settings, 166 | // providing a kind of camera playground. 167 | // None of this is central to the main ideas of the project. 168 | int saturation = 1; 169 | int frameStartX = 164; 170 | int frameStartY = 8; 171 | bool autoDeNoise = false; 172 | bool softSleep = false; 173 | int driveStrength = 1; 174 | int testPattern = 0; 175 | 176 | void handleUserInput() 177 | { 178 | 179 | int ch = Serial.read(); 180 | if (ch < 0) return; 181 | 182 | switch (ch) { 183 | 184 | case 'm' : 185 | { theCam->softSleep(false); // wake up camera before changing modes 186 | int nextMode = (theCam->mode + 1) % 3; 187 | theCam->setMode(nextMode, true); 188 | theLCD->ClearScreen(YELLOW); 189 | } 190 | break; 191 | 192 | case 'f': 193 | frameStartX--; 194 | theCam->frameControl(frameStartX, frameStartY); 195 | Serial.printf("frameStart %d, %d \n", frameStartX, frameStartY); 196 | break; 197 | 198 | case 'F': 199 | frameStartX++; 200 | theCam->frameControl(frameStartX, frameStartY); 201 | Serial.printf("frameStart %d, %d \n", frameStartX, frameStartY); 202 | break; 203 | 204 | case 'v': 205 | frameStartY--; 206 | theCam->frameControl(frameStartX, frameStartY); 207 | Serial.printf("frameStart %d, %d \n", frameStartX, frameStartY); 208 | break; 209 | 210 | case 'V': 211 | frameStartY++; 212 | theCam->frameControl(frameStartX, frameStartY); 213 | Serial.printf("frameStart %d, %d\n", frameStartX, frameStartY); 214 | break; 215 | 216 | case 's': 217 | saturation--; 218 | theCam->saturation(saturation); 219 | Serial.printf("Saturation %d\n", saturation); 220 | break; 221 | 222 | case 'S': 223 | saturation++; 224 | theCam->saturation(saturation); 225 | Serial.printf("Saturation %d\n", saturation); 226 | break; 227 | 228 | case 'd': 229 | autoDeNoise = !autoDeNoise; 230 | theCam->autoDeNoise(autoDeNoise); 231 | Serial.printf("Auto de-noise %d\n", autoDeNoise); 232 | break; 233 | 234 | case 'z': // This is nice to freeze a frame. 235 | softSleep = !softSleep; 236 | theCam->softSleep(softSleep); 237 | Serial.printf("Soft sleep %d\n", softSleep); 238 | break; 239 | 240 | case 'q': 241 | driveStrength = (driveStrength + 1) % 4; 242 | theCam->setDriveStrength(driveStrength); 243 | Serial.printf("Drive strength %d\n", driveStrength); 244 | break; 245 | 246 | case 't': 247 | testPattern = (testPattern + 1) % 4; 248 | theCam->testPattern(testPattern); 249 | Serial.printf("Test pattern %d\n", testPattern); 250 | break; 251 | } 252 | } 253 | 254 | 255 | long lastFpsTime = 0; 256 | int fpsReportAfterFrames = 100; 257 | long etHotspot; // Elapsed microsecs spent in recent calls to the hotspot code 258 | 259 | int regTry = 0; 260 | int testKind = 0; // what kind of testimage do we want? 261 | 262 | // Timing: In the OV7670 spec sheet, timings are defined in terms of tLine periods. 263 | // A VGA frame arrives in 510 x tLine, 480 of which are scanlines, and 30 x tLine between frames. 264 | // At 25fps, tLine = 78.4 microsecs (this depends on the 20MHz XCLK we feed to the camera). 265 | // At QVGA, time between scanlines is 2 x tLine, i.e. scanlines arrive 156.8 micros apart. 266 | // After the last scanline we have 10 x tLine periods before VSYNC, and then another 267 | // 20 x tLine periods to wrap up the frame. 268 | 269 | void loop(void) 270 | { 271 | 272 | // I normally only poll for user input at the end of a frame, but that fails if the camera is asleep... 273 | if (softSleep) 274 | { 275 | handleUserInput(); 276 | } 277 | 278 | switch (theState) 279 | { 280 | case Lost: break; // Just be patient and wait to synchronize with the start of the next frame. 281 | 282 | case Priming: // Set up to sink the next frame. 283 | { 284 | theCam->i2sRestart(); 285 | // Tell the LCD where to put the image on the screen. Center it. 286 | int x = (320 - theCam->xres) / 2; // this will need fixing when I buy a bigger screen. 287 | int y = (240 - theCam->yres) / 2; 288 | theLCD->Address_set(x, y, x + theCam->xres - 1 - 0, y + theCam->yres - 1); 289 | 290 | theState = Running; 291 | } 292 | break; 293 | 294 | case Running: // If there is a queued block, send it to the LCD 295 | { 296 | DMABuffer *buf = queuedBlock; 297 | 298 | if (buf) { 299 | long t0 = micros(); // Accumulate some diagnostic timing information 300 | theLCD->SinkDMABuf(theCam->xres, buf); 301 | etHotspot += (micros() - t0); 302 | queuedBlock = 0; // Indicate that we're done with this buffer 303 | } 304 | } 305 | break; 306 | 307 | case Wrapup: // Finalize things before the next frame begins. 308 | case Overrun: // Time after a VSYNC and before the next scanline is our biggest chunk of idle time, 309 | { // so we do a bit of other housekeeping here too, like polling for user input. 310 | 311 | // Housekeeping 312 | if (++framesGrabbed % fpsReportAfterFrames == 0) { 313 | long timeNow = millis(); 314 | double fps = (fpsReportAfterFrames * 1000.0) / (timeNow - lastFpsTime); 315 | Serial.printf("Mode:%dx%d; Last %d frames = %.1f FPS; Hotspot = %d micros;\n", 316 | theCam->xres, theCam->yres, fpsReportAfterFrames, fps, 317 | etHotspot / (fpsReportAfterFrames * theCam->yres)); 318 | etHotspot = 0; 319 | lastFpsTime = timeNow; 320 | } 321 | handleUserInput(); 322 | // If we got this all done before the next scanline arrived ... 323 | if (theState != Overrun) { // Yay! We got finalized before the deadline. 324 | theState = Priming; // Just go back and expect scanlines to appear for the next frame. 325 | } 326 | else { // Oops, we'll have to miss a frame and wait for the next vsync 327 | theState = Lost; 328 | } 329 | } 330 | break; 331 | } 332 | } 333 | 334 | -------------------------------------------------------------------------------- /CamToLCD/DMABuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 4 | // Released under Apache License 2.0 5 | // Big chunks of the camera-side code were lifted from or inspired by 6 | // https://github.com/bitluni/ESP32CameraI2S by Bitluni, and 7 | // https://github.com/igrr/esp32-cam-demo by Ivan Grokhotkov (igrr) 8 | 9 | #include "rom/lldesc.h" 10 | 11 | class DMABuffer 12 | { 13 | public: 14 | lldesc_t descriptor; 15 | unsigned char* buffer; 16 | DMABuffer(int bytes) 17 | { 18 | buffer = (unsigned char *)malloc(bytes); 19 | descriptor.length = bytes; 20 | descriptor.size = descriptor.length; 21 | descriptor.owner = 1; 22 | descriptor.sosf = 1; 23 | descriptor.buf = (uint8_t*) buffer; 24 | descriptor.offset = 0; 25 | descriptor.empty = 0; 26 | descriptor.eof = 1; 27 | descriptor.qe.stqe_next = 0; 28 | } 29 | 30 | void next(DMABuffer *next) 31 | { 32 | descriptor.qe.stqe_next = &(next->descriptor); 33 | } 34 | 35 | int sampleCount() const 36 | { 37 | return descriptor.length / 4; 38 | } 39 | 40 | ~DMABuffer() 41 | { 42 | if(buffer) 43 | delete(buffer); 44 | } 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /CamToLCD/I2SCamera.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 3 | // Released under Apache License 2.0 4 | // Big chunks of the camera-side code were lifted from or inspired by 5 | // https://github.com/bitluni/ESP32CameraI2S by Bitluni, and 6 | // https://github.com/igrr/esp32-cam-demo by Ivan Grokhotkov (igrr) 7 | // The LCD side of things started from some driver code on the Banggood site. 8 | 9 | #include "I2SCamera.h" 10 | #include "Log.h" 11 | 12 | int I2SCamera::framesReceived = 0; 13 | int I2SCamera::xres = 640; 14 | int I2SCamera::yres = 480; 15 | gpio_num_t I2SCamera::vSyncPin = (gpio_num_t)0; 16 | intr_handle_t I2SCamera::i2sInterruptHandle = 0; 17 | intr_handle_t I2SCamera::vSyncInterruptHandle = 0; 18 | int I2SCamera::dmaBufferCount = 0; 19 | int I2SCamera::dmaBufferActive = 0; 20 | DMABuffer **I2SCamera::dmaBuffer = 0; 21 | 22 | //unsigned char* I2SCamera::frame = 0; 23 | //int I2SCamera::framePointer = 0; 24 | 25 | int I2SCamera::frameBytes = 0; 26 | volatile bool I2SCamera::stopSignal = false; 27 | 28 | // The callbacks to a listener who is interested... 29 | void (*I2SCamera::BlockListener)(DMABuffer *buf); 30 | void (*I2SCamera::VSYNCListener)(); 31 | 32 | void IRAM_ATTR I2SCamera::i2sInterrupt(void* arg) 33 | { 34 | I2S0.int_clr.val = I2S0.int_raw.val; 35 | //debug_printf("low level SL"); 36 | // Grab a reference to the dma buffer with newest data, 37 | // and switch active DMA buffer to use the other one. 38 | DMABuffer *readyBlock = dmaBuffer[dmaBufferActive]; 39 | dmaBufferActive = (dmaBufferActive + 1) % dmaBufferCount; 40 | 41 | if (BlockListener) // If a listener is registered to sink blocks, do the callback. 42 | { 43 | BlockListener(readyBlock); 44 | } 45 | 46 | if (stopSignal) 47 | { 48 | i2sStop(); 49 | stopSignal = false; 50 | } 51 | } 52 | 53 | void IRAM_ATTR I2SCamera::vSyncInterrupt(void* arg) 54 | { 55 | // debug_printf("low level vsync interrupt\n"); 56 | GPIO.status1_w1tc.val = GPIO.status1.val; 57 | GPIO.status_w1tc = GPIO.status; 58 | if (VSYNCListener) // If a listener is registered to listen to VSYNC events, do the callback 59 | { 60 | VSYNCListener(); 61 | } 62 | } 63 | 64 | void I2SCamera::i2sStop() 65 | { 66 | debug_printf("i2sStop called isr=0x%08x\n", i2sInterruptHandle); 67 | // Make this safe to call even if camera has never been started. 68 | if (! i2sInterruptHandle) return; 69 | esp_intr_disable(i2sInterruptHandle); 70 | esp_intr_disable(vSyncInterruptHandle); 71 | i2sConfReset(); 72 | I2S0.conf.rx_start = 0; 73 | } 74 | 75 | void I2SCamera::i2sRun() 76 | { 77 | debug_print ("I2S Run "); 78 | 79 | // Whenever i2sRun is called, we might potentially discard the old DMA 80 | // buffer if it is the wrong size, and then allocate a new one. 81 | // This allows us to stop the camera, select a different mode, and 82 | // start it again. 83 | int requiredSize = xres * 2 * 2; 84 | if (dmaBuffer && dmaBuffer[0]->descriptor.length != requiredSize) { 85 | dmaBufferDeinit(); 86 | } 87 | if (!dmaBuffer) { 88 | dmaBufferInit(requiredSize); 89 | } 90 | 91 | while (gpio_get_level(vSyncPin) == 0); 92 | while (gpio_get_level(vSyncPin) != 0); 93 | 94 | esp_intr_disable(i2sInterruptHandle); 95 | i2sConfReset(); 96 | dmaBufferActive = 0; 97 | 98 | debug_print("Sample count "); 99 | debug_println(dmaBuffer[0]->sampleCount()); 100 | I2S0.rx_eof_num = dmaBuffer[0]->sampleCount(); 101 | 102 | I2S0.in_link.addr = (uint32_t) & (dmaBuffer[0]->descriptor); 103 | I2S0.in_link.start = 1; 104 | I2S0.int_clr.val = I2S0.int_raw.val; 105 | I2S0.int_ena.val = 0; 106 | I2S0.int_ena.in_done = 1; 107 | debug_printf("enabling i2sInterrupt %08x\n", i2sInterruptHandle); 108 | esp_intr_enable(i2sInterruptHandle); 109 | esp_intr_enable(vSyncInterruptHandle); 110 | I2S0.conf.rx_start = 1; 111 | debug_printf("IS2 started for camera.\n"); 112 | } 113 | 114 | bool I2SCamera::initVSyncInterrupt(int pin) 115 | { 116 | debug_print("Initializing VSYNC... "); 117 | vSyncPin = (gpio_num_t)pin; 118 | gpio_set_intr_type(vSyncPin, GPIO_INTR_POSEDGE); 119 | gpio_intr_enable(vSyncPin); 120 | // Bind the interrupt to our handler. 121 | if (gpio_isr_register(&vSyncInterrupt, (void*)"vSyncInterrupt", ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM, &vSyncInterruptHandle) != ESP_OK) 122 | { 123 | debug_println("failed!"); 124 | return false; 125 | } 126 | debug_println("done."); 127 | return true; 128 | } 129 | 130 | void I2SCamera::deinitVSyncInterrupt() 131 | { 132 | esp_intr_disable(vSyncInterruptHandle); 133 | } 134 | 135 | // This init should only ever be called once, after the camera is already generating VSYNCs 136 | bool I2SCamera::init(const int VSYNC, const int HREF, const int PCLK, const int databus[], 137 | void (*blockListener)(DMABuffer *buf), 138 | void (*vSyncListener)()) 139 | { 140 | // Setup the callbacks to the main program 141 | BlockListener = blockListener; 142 | VSYNCListener = vSyncListener; 143 | 144 | i2sInit(VSYNC, HREF, PCLK, databus[7], databus[6], databus[5], databus[4], databus[3], databus[2], databus[1], databus[0]); 145 | 146 | initVSyncInterrupt(VSYNC); 147 | return true; 148 | } 149 | 150 | bool I2SCamera::i2sInit(const int VSYNC, const int HREF, const int PCLK, const int D0, const int D1, const int D2, const int D3, const int D4, const int D5, const int D6, const int D7) 151 | { 152 | int pins[] = {VSYNC, HREF, PCLK, D0, D1, D2, D3, D4, D5, D6, D7}; 153 | gpio_config_t conf = { 154 | .pin_bit_mask = 0, 155 | .mode = GPIO_MODE_INPUT, 156 | .pull_up_en = GPIO_PULLUP_DISABLE, 157 | .pull_down_en = GPIO_PULLDOWN_DISABLE, 158 | .intr_type = GPIO_INTR_DISABLE 159 | }; 160 | for (int i = 0; i < sizeof(pins) / sizeof(gpio_num_t); ++i) { 161 | conf.pin_bit_mask = 1LL << pins[i]; 162 | gpio_config(&conf); 163 | } 164 | 165 | // Route input GPIOs to I2S peripheral using GPIO matrix, last parameter is invert 166 | gpio_matrix_in(D0, I2S0I_DATA_IN0_IDX, false); 167 | gpio_matrix_in(D1, I2S0I_DATA_IN1_IDX, false); 168 | gpio_matrix_in(D2, I2S0I_DATA_IN2_IDX, false); 169 | gpio_matrix_in(D3, I2S0I_DATA_IN3_IDX, false); 170 | gpio_matrix_in(D4, I2S0I_DATA_IN4_IDX, false); 171 | gpio_matrix_in(D5, I2S0I_DATA_IN5_IDX, false); 172 | gpio_matrix_in(D6, I2S0I_DATA_IN6_IDX, false); 173 | gpio_matrix_in(D7, I2S0I_DATA_IN7_IDX, false); 174 | gpio_matrix_in(0x30, I2S0I_DATA_IN8_IDX, false); 175 | gpio_matrix_in(0x30, I2S0I_DATA_IN9_IDX, false); 176 | gpio_matrix_in(0x30, I2S0I_DATA_IN10_IDX, false); 177 | gpio_matrix_in(0x30, I2S0I_DATA_IN11_IDX, false); 178 | gpio_matrix_in(0x30, I2S0I_DATA_IN12_IDX, false); 179 | gpio_matrix_in(0x30, I2S0I_DATA_IN13_IDX, false); 180 | gpio_matrix_in(0x30, I2S0I_DATA_IN14_IDX, false); 181 | gpio_matrix_in(0x30, I2S0I_DATA_IN15_IDX, false); 182 | 183 | gpio_matrix_in(VSYNC, I2S0I_V_SYNC_IDX, true); 184 | gpio_matrix_in(0x38, I2S0I_H_SYNC_IDX, false); //0x30 sends 0, 0x38 sends 1 185 | gpio_matrix_in(HREF, I2S0I_H_ENABLE_IDX, false); 186 | gpio_matrix_in(PCLK, I2S0I_WS_IN_IDX, false); 187 | 188 | // Enable and configure I2S peripheral 189 | periph_module_enable(PERIPH_I2S0_MODULE); 190 | 191 | // Toggle some reset bits in LC_CONF register 192 | // Toggle some reset bits in CONF register 193 | i2sConfReset(); 194 | // Enable slave mode (sampling clock is external) 195 | I2S0.conf.rx_slave_mod = 1; 196 | // Enable parallel mode 197 | I2S0.conf2.lcd_en = 1; 198 | // Use HSYNC/VSYNC/HREF to control sampling 199 | I2S0.conf2.camera_en = 1; 200 | // Configure clock divider 201 | I2S0.clkm_conf.clkm_div_a = 1; 202 | I2S0.clkm_conf.clkm_div_b = 0; 203 | I2S0.clkm_conf.clkm_div_num = 2; 204 | // FIFO will sink data to DMA 205 | I2S0.fifo_conf.dscr_en = 1; 206 | // FIFO configuration 207 | //two bytes per dword packing 208 | I2S0.fifo_conf.rx_fifo_mod = SM_0A0B_0C0D; //pack two bytes in one dword see :https://github.com/igrr/esp32-cam-demo/issues/29 209 | I2S0.fifo_conf.rx_fifo_mod_force_en = 1; 210 | I2S0.conf_chan.rx_chan_mod = 1; 211 | // Clear flags which are used in I2S serial mode 212 | I2S0.sample_rate_conf.rx_bits_mod = 0; 213 | I2S0.conf.rx_right_first = 0; 214 | I2S0.conf.rx_msb_right = 0; 215 | I2S0.conf.rx_msb_shift = 0; 216 | I2S0.conf.rx_mono = 0; 217 | I2S0.conf.rx_short_sync = 0; 218 | I2S0.timing.val = 0; 219 | 220 | debug_printf("i2sInit allocating interrupt, handle before = %08x.\n", i2sInterruptHandle); 221 | esp_intr_alloc(ETS_I2S0_INTR_SOURCE, ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_IRAM, &i2sInterrupt, NULL, &i2sInterruptHandle); 222 | debug_printf("i2sInit completed with handle = %08x.\n", i2sInterruptHandle); 223 | return true; 224 | } 225 | 226 | void I2SCamera::i2sRestart() { 227 | // Pete: At the end of every frame is is possible that the is2 logic and the camera logic might 228 | // have lost tight synchronization (I can deliberately provoke this by momentarily clamping PCLK 229 | // low, as if the signal is noisy). I want to recover synchronization on the next frame. But 230 | // I don't want to "waste" waiting for another VSYNC to ensure we start with a "clean slate". 231 | I2S0.conf.rx_start = 0; 232 | 233 | i2sConfReset(); 234 | dmaBufferActive = 0; 235 | I2S0.rx_eof_num = dmaBuffer[0]->sampleCount(); 236 | I2S0.in_link.addr = (uint32_t) & (dmaBuffer[0]->descriptor); 237 | I2S0.in_link.start = 1; 238 | I2S0.int_clr.val = I2S0.int_raw.val; 239 | I2S0.int_ena.val = 0; 240 | I2S0.int_ena.in_done = 1; 241 | I2S0.conf.rx_start = 1; 242 | } 243 | 244 | void I2SCamera::dmaBufferInit(int bytes) 245 | { 246 | dmaBufferCount = 2; 247 | dmaBuffer = (DMABuffer**) malloc(sizeof(DMABuffer*) * dmaBufferCount); 248 | for (int i = 0; i < dmaBufferCount; i++) 249 | { 250 | dmaBuffer[i] = new DMABuffer(bytes); 251 | if (i) 252 | dmaBuffer[i - 1]->next(dmaBuffer[i]); 253 | } 254 | dmaBuffer[dmaBufferCount - 1]->next(dmaBuffer[0]); 255 | debug_printf("DMA buffers (%d), each of size %d bytes were allocated.\n", dmaBufferCount, bytes); 256 | } 257 | 258 | void I2SCamera::dmaBufferDeinit() 259 | { 260 | if (!dmaBuffer) return; 261 | debug_println("Deleting DMA buffers."); 262 | for (int i = 0; i < dmaBufferCount; i++) 263 | delete(dmaBuffer[i]); 264 | delete(dmaBuffer); 265 | dmaBuffer = 0; 266 | dmaBufferCount = 0; 267 | } 268 | -------------------------------------------------------------------------------- /CamToLCD/I2SCamera.h: -------------------------------------------------------------------------------- 1 | 2 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 3 | // Released under Apache License 2.0 4 | // Big chunks of the camera-side code were lifted from or inspired by 5 | // https://github.com/bitluni/ESP32CameraI2S by Bitluni, and 6 | // https://github.com/igrr/esp32-cam-demo by Ivan Grokhotkov (igrr) 7 | // The LCD side of things started from some driver code on the Banggood site. 8 | 9 | #pragma once 10 | 11 | #include "soc/soc.h" 12 | #include "soc/gpio_sig_map.h" 13 | #include "soc/i2s_reg.h" 14 | #include "soc/i2s_struct.h" 15 | #include "soc/io_mux_reg.h" 16 | #include "driver/gpio.h" 17 | #include "driver/periph_ctrl.h" 18 | #include "DMABuffer.h" 19 | #include "Log.h" 20 | 21 | class I2SCamera 22 | { 23 | public: 24 | static gpio_num_t vSyncPin; 25 | static int framesReceived; 26 | static int xres; 27 | static int yres; 28 | static intr_handle_t i2sInterruptHandle; 29 | static intr_handle_t vSyncInterruptHandle; 30 | static int dmaBufferCount; 31 | static int dmaBufferActive; 32 | static DMABuffer **dmaBuffer; 33 | static int frameBytes; 34 | static volatile bool stopSignal; 35 | 36 | // Pete: Some type definitions for the callback functions 37 | static void (*BlockListener)(DMABuffer *buf); 38 | static void (*VSYNCListener)(); 39 | 40 | typedef enum { 41 | /* camera sends byte sequence: s1, s2, s3, s4, ... 42 | * fifo receives: 00 s1 00 s2, 00 s2 00 s3, 00 s3 00 s4, ... 43 | */ 44 | SM_0A0B_0B0C = 0, 45 | /* camera sends byte sequence: s1, s2, s3, s4, ... 46 | * fifo receives: 00 s1 00 s2, 00 s3 00 s4, ... 47 | */ 48 | SM_0A0B_0C0D = 1, 49 | /* camera sends byte sequence: s1, s2, s3, s4, ... 50 | * fifo receives: 00 s1 00 00, 00 s2 00 00, 00 s3 00 00, ... 51 | */ 52 | SM_0A00_0B00 = 3, 53 | } i2s_sampling_mode_t; 54 | 55 | 56 | static inline void i2sConfReset() 57 | {debug_println("i2sConfReset"); 58 | const uint32_t lc_conf_reset_flags = I2S_IN_RST_M | I2S_AHBM_RST_M | I2S_AHBM_FIFO_RST_M; 59 | I2S0.lc_conf.val |= lc_conf_reset_flags; 60 | I2S0.lc_conf.val &= ~lc_conf_reset_flags; 61 | 62 | const uint32_t conf_reset_flags = I2S_RX_RESET_M | I2S_RX_FIFO_RESET_M | I2S_TX_RESET_M | I2S_TX_FIFO_RESET_M; 63 | I2S0.conf.val |= conf_reset_flags; 64 | I2S0.conf.val &= ~conf_reset_flags; 65 | while (I2S0.state.rx_fifo_reset_back); 66 | } 67 | 68 | void start() 69 | { debug_printf("start camera is called\n"); 70 | i2sRun(); 71 | } 72 | 73 | void stop() 74 | { debug_printf("stop camera is called, but is it running?=%d\n", I2S0.conf.rx_start); 75 | // Make this safe to call even if camera has never been started. 76 | if (! I2S0.conf.rx_start) return; 77 | // if it is running, busy-wait till the end of the current frame. 78 | stopSignal = true; 79 | while(stopSignal); 80 | } 81 | 82 | void oneFrame() // Deprecated, because I don't like the busy loop. 83 | { 84 | start(); 85 | stop(); 86 | } 87 | 88 | void i2sRestart(); // Pete 89 | 90 | static void i2sStop(); 91 | static void i2sRun(); 92 | 93 | static void dmaBufferInit(int bytes); 94 | static void dmaBufferDeinit(); 95 | 96 | static bool initVSyncInterrupt(int pin); 97 | static void deinitVSyncInterrupt(); 98 | 99 | static void IRAM_ATTR i2sInterrupt(void* arg); 100 | static void IRAM_ATTR vSyncInterrupt(void* arg); 101 | 102 | 103 | static bool i2sInit(const int VSYNC, const int HREF, const int PCLK, const int D0, const int D1, const int D2, const int D3, const int D4, const int D5, const int D6, const int D7); 104 | static bool init(const int VSYNC, const int HREF, const int PCLK, const int databus[], 105 | void (*blockListener)(DMABuffer *buf), 106 | void (*vSyncListener)()); 107 | 108 | }; 109 | -------------------------------------------------------------------------------- /CamToLCD/LCD.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 5 | // Released under Apache License 2.0 6 | 7 | // Parts of this code (the LCD setup, drawing rectangles, clearing the screen) started from code at 8 | // https://www.banggood.com/2_4-Inch-TFT-LCD-Shield-240320-Touch-Board-Display-Module-With-Touch-Pen-For-Arduino-UNO-p-1171082.html 9 | // LCD_Touch\2.4inch_Arduino_HX8347G_V1.0\Arduino Demo_ArduinoUNO&Mega2560\Example01-Simple test\Simple test for UNO\_HX8347_uno 10 | // His "Simple test" for the UNO has no dependecies on any libraries (i.e. doesn't use AdaFruit, etc.) 11 | // It just clears the screen with some colours, and draws some rectangles. 12 | 13 | // My contribution was to clean it up, port it to the ESP32, wrap it into a class, and find some optimizations. 14 | // Pete Wentworth, September 2018. 15 | 16 | // This LCD is not a serial SPI LCD - it is a parallel, 8-bit-at-a-time model. 17 | 18 | #include "DMABuffer.h" // Because we want to unpack camera pixels from the DMA buffer ... 19 | 20 | 21 | //******** The LCD pins and mapping. ********** 22 | const int TX2 = 17; 23 | const int RX2 = 16; 24 | int lcdDataBus[] = {18, 4, 19, 21, 22, 23, TX2, RX2}; // D7, D6, D5, D4, D3, D2, D1, D0 25 | 26 | #define LCD_WR 2 // When it goes high, the LCD latches bits from the output pin 27 | #define LCD_WR_SETOUTPUT() pinMode(LCD_WR, OUTPUT) 28 | #define LCD_WR_HIGH() GPIO.out_w1ts=(1 << LCD_WR) 29 | #define LCD_WR_LOW() GPIO.out_w1tc=(1 << LCD_WR) 30 | 31 | 32 | #define LCD_DC 15 // HIGH means LCD interprets bits as data, LOW means the bits are a command. 33 | #define LCD_DC_SETOUTPUT() pinMode(LCD_DC, OUTPUT) 34 | #define LCD_DC_HIGH() GPIO.out_w1ts=(1 << LCD_DC) 35 | #define LCD_DC_LOW() GPIO.out_w1tc=(1 << LCD_DC) 36 | 37 | 38 | // Uncomment this line if you have a GPIO pin for LCD_RD 39 | // If you don't need to read from the device, strap it's LCD_RD pin high. 40 | // #define LCD_RD GPIO-pin-needs-to-be-assigned 41 | 42 | #ifdef LCD_RD 43 | #define LCD_RD_SETOUTPUT() pinMode(LCD_RD, OUTPUT) 44 | #define LCD_RD_HIGH() GPIO.out_w1ts=(1 << LCD_RD) 45 | #define LCD_RD_LOW() GPIO.out_w1tc=(1 << LCD_RD)) 46 | #else 47 | #define LCD_RD_SETOUTPUT() 48 | #define LCD_RD_HIGH() 49 | #define LCD_RD_LOW() 50 | #endif 51 | 52 | 53 | // Uncomment this line if you have a GPIO pin for LCD_CS 54 | // If you want the device permanently selected, strap this pin low. 55 | // #define LCD_CS GPIO-pin-needs-to-be-assigned 56 | 57 | #ifdef LCD_CS 58 | #define LCD_CS_SETOUTPUT() pinMode(LCD_CS, OUTPUT) 59 | #define LCD_CS_HIGH() GPIO.out_w1ts=(1 << LCD_CS) 60 | #define LCD_CS_LOW() GPIO.out_w1tc=(1 << LCD_CS) 61 | #else 62 | #define LCD_CS_SETOUTPUT() 63 | #define LCD_CS_HIGH() 64 | #define LCD_CS_LOW() 65 | #endif 66 | 67 | 68 | // Uncomment this line if you have a GPIO pin for LCD_RESET 69 | // Consider tying this pin to EN so that the LCD resets when the ESP32 resets. 70 | // #define LCD_RESET GPIO-pin-needs-to-be-assigned 71 | 72 | #ifdef LCD_RESET 73 | #define LCD_RESET_SETOUTPUT() pinMode(LCD_RESET, OUTPUT) 74 | #define LCD_RESET_HIGH(dly) { GPIO.out_w1ts=(1 << LCD_RESET); delay(dly); } 75 | #define LCD_RESET_LOW(dly) { GPIO.out_w1tc=(1 << LCD_RESET); delay(dly); } 76 | #else 77 | #define LCD_RESET_SETOUTPUT() 78 | #define LCD_RESET_HIGH(dly) 79 | #define LCD_RESET_LOW(dly) 80 | #endif 81 | 82 | 83 | 84 | const int RED = 0xf800; 85 | const int GREEN = 0x07E0; 86 | const int BLUE = 0x001F; 87 | const int YELLOW = 0xFFE0; 88 | const int MAGENTA = 0xF81F; 89 | const int CYAN = 0x07FF; 90 | 91 | int backGroundColours[] = {RED, GREEN, BLUE}; // Red, Green, Blue in RGB565 format 92 | 93 | class LCD 94 | { 95 | public: 96 | 97 | unsigned int outputMaskMap[256]; // Each 8-bit byte (used as an index) has a 32-bit mask to set the appropriate GPIO lines 98 | unsigned int busPinsLowMask; // This has clear bits for all the lcdDataBus pins and also for the LCD_WR pin, 99 | // which we set low at the same time. 100 | 101 | // Key optimization idea: Ahead of time, prepare output masks for all possible byte values. Store them in outputMaskMap. 102 | // To write a byte to the LCD, set all databus GPIOs low (also include LCD_WR) by writing a fixed mask to w1tc. 103 | // Then use the byte to be written as an index into outputMaskMap, and write that mask to w1ts to set the GPIOs. 104 | // Then latch that data onto the LCD device by making LCD_WR go high. 105 | #define Write_Byte(d) { GPIO.out_w1tc=busPinsLowMask; GPIO.out_w1ts=outputMaskMap[d]; LCD_WR_HIGH(); } 106 | 107 | 108 | void Write_Command(unsigned char d) 109 | { 110 | LCD_DC_LOW(); // Here comes a command 111 | Write_Byte(d); 112 | } 113 | 114 | void Write_Data(unsigned char d) 115 | { 116 | LCD_DC_HIGH(); // Here comes data 117 | Write_Byte(d); 118 | } 119 | 120 | 121 | // Where on the LCD do you want the data to go? 122 | void Address_set(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2) 123 | { 124 | Write_Command(0x2a); 125 | Write_Data(x1 >> 8); 126 | Write_Data(x1); 127 | Write_Data(x2 >> 8); 128 | Write_Data(x2); 129 | Write_Command(0x2b); 130 | Write_Data(y1 >> 8); 131 | Write_Data(y1); 132 | Write_Data(y2 >> 8); 133 | Write_Data(y2); 134 | Write_Command(0x2c); 135 | } 136 | 137 | void Init(void) 138 | { 139 | LCD_RESET_HIGH(5); 140 | LCD_RESET_LOW(15); 141 | LCD_RESET_HIGH(15); 142 | 143 | LCD_CS_LOW(); 144 | 145 | Write_Command(0xCB); 146 | Write_Data(0x39); 147 | Write_Data(0x2C); 148 | Write_Data(0x00); 149 | Write_Data(0x34); 150 | Write_Data(0x02); 151 | 152 | Write_Command(0xCF); 153 | Write_Data(0x00); 154 | Write_Data(0XC1); 155 | Write_Data(0X30); 156 | 157 | Write_Command(0xE8); 158 | Write_Data(0x85); 159 | Write_Data(0x00); 160 | Write_Data(0x78); 161 | 162 | Write_Command(0xEA); 163 | Write_Data(0x00); 164 | Write_Data(0x00); 165 | 166 | Write_Command(0xED); 167 | Write_Data(0x64); 168 | Write_Data(0x03); 169 | Write_Data(0X12); 170 | Write_Data(0X81); 171 | 172 | Write_Command(0xF7); 173 | Write_Data(0x20); 174 | 175 | Write_Command(0xC0); //Power control 176 | Write_Data(0x23); //VRH[5:0] 177 | 178 | Write_Command(0xC1); //Power control 179 | Write_Data(0x10); //SAP[2:0];BT[3:0] 180 | 181 | Write_Command(0xC5); //VCM control 182 | Write_Data(0x3e); //Contrast 183 | Write_Data(0x28); 184 | 185 | Write_Command(0xC7); //VCM control2 186 | Write_Data(0x86); //-- 187 | 188 | Write_Command(0x36); // Memory Access Control 189 | // I like the settings for a forward-facing camera, so text appears normally on the LCD. 190 | // Write_Data(0x28); // Exchange rows and cols for landscape 191 | Write_Data(0xE8); // Exchange rows and cols for landscape, and flip X (which is now Y) and flip Y 192 | 193 | 194 | Write_Command(0x3A); 195 | Write_Data(0x55); 196 | 197 | Write_Command(0xB1); 198 | Write_Data(0x00); 199 | Write_Data(0x18); 200 | 201 | Write_Command(0xB6); // Display Function Control 202 | Write_Data(0x08); 203 | 204 | Write_Data(0x82); 205 | Write_Data(0x27); 206 | 207 | Write_Command(0x11); //Exit Sleep 208 | delay(120); 209 | 210 | Write_Command(0x29); //Display on 211 | Write_Command(0x2c); 212 | } 213 | 214 | 215 | void H_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c) 216 | { 217 | unsigned int i, j; 218 | Write_Command(0x02c); //write_memory_start 219 | 220 | LCD_CS_LOW(); 221 | l = l + x; 222 | Address_set(x, y, l, y); 223 | j = l * 2; 224 | for (i = 1; i <= j; i++) 225 | { 226 | Write_Data(c); 227 | } 228 | LCD_CS_HIGH(); 229 | } 230 | 231 | void V_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c) 232 | { 233 | unsigned int i, j; 234 | Write_Command(0x02c); //write_memory_start 235 | 236 | LCD_CS_LOW(); 237 | l = l + y; 238 | Address_set(x, y, x, l); 239 | j = l * 2; 240 | for (i = 1; i <= j; i++) 241 | { 242 | Write_Data(c); 243 | } 244 | LCD_CS_HIGH(); 245 | } 246 | 247 | void Rect(unsigned int x, unsigned int y, unsigned int w, unsigned int h, unsigned int c) 248 | { 249 | H_line(x , y , w, c); 250 | H_line(x , y + h, w, c); 251 | V_line(x , y , h, c); 252 | V_line(x + w, y , h, c); 253 | } 254 | 255 | void ClearScreen(unsigned int backColour) 256 | { 257 | LCD_CS_LOW(); 258 | // Assume we're in landscape 259 | Address_set(0, 0, 320, 240); 260 | 261 | unsigned char u = backColour >> 8; 262 | unsigned char v = backColour; 263 | LCD_DC_HIGH(); 264 | for (int i = 0; i < 240 * 320; i++) 265 | { 266 | Write_Byte(u); 267 | Write_Byte(v); 268 | } 269 | LCD_CS_HIGH(); 270 | } 271 | 272 | void createOutputMasks() 273 | { 274 | busPinsLowMask = (1 << LCD_WR); 275 | for (int i = 0; i < 256; i++) { // possible byte values 276 | int v = i; 277 | for (int k = 7; k >= 0; k--) { // look at each bit in turn, starting at least significant 278 | int pinNum = lcdDataBus[k]; 279 | unsigned int pinBit = (0x0001 << pinNum); 280 | if (v & 0x01) { 281 | outputMaskMap[i] |= pinBit; 282 | } 283 | else { 284 | if (i == 0) { 285 | busPinsLowMask |= pinBit; 286 | // Serial.printf("Setting pinbit %08x so busPinsLowMask is %08x\n", pinBit, busPinsLowMask); 287 | } 288 | } 289 | v >>= 1; 290 | } 291 | } 292 | // Let's print the masks, in hex. 293 | /* 294 | Serial.println("Table of pins to make high for each byte value (in Hex):"); 295 | for (int row = 0; row < 8; row++) 296 | { 297 | char sep = ' '; 298 | for (int col = 0; col < 8; col++) { 299 | int indx = row * 8 + col; 300 | Serial.printf("%c %08x", sep, outputMaskMap[indx]); 301 | sep = ','; 302 | } 303 | Serial.println(); 304 | } 305 | Serial.println("-----------------"); 306 | */ 307 | } 308 | 309 | 310 | void SinkDMABuf(int xres, DMABuffer *buf) { 311 | // Here we unpack the DMA buffer to get at the bytes, 312 | // and send them to the LCD. 313 | // This is the "hotspot" code, so we optimize all we can. 314 | // My elapsed time call to this function with xres=320 315 | // shows just under 100 microsecs. 316 | // 320x240 frames at 25fps stablize when I can keep this 317 | // below 140 microsecs, so I have some room to play. 318 | // Unrolling the loop doesn't make it go any faster. 319 | 320 | unsigned char *p = buf->buffer; // Pointer to start of data 321 | 322 | LCD_DC_HIGH(); // Bytes are data, not LCD commands. 323 | for (int i = 0; i < xres * 4; i += 4) { 324 | Write_Byte(p[i]); // Every second byte of the DMA buffer has pixel data 325 | Write_Byte(p[i + 2]); 326 | } 327 | } 328 | 329 | void Setup() 330 | { 331 | Serial.println("Setting up LCD data and control pins"); 332 | for (int p = 0; p < 8; p++) 333 | { 334 | pinMode(lcdDataBus[p], OUTPUT); 335 | } 336 | 337 | createOutputMasks(); 338 | 339 | LCD_RD_SETOUTPUT(); 340 | LCD_RD_HIGH(); 341 | 342 | LCD_WR_SETOUTPUT(); 343 | LCD_WR_HIGH(); 344 | 345 | LCD_DC_SETOUTPUT(); 346 | LCD_DC_HIGH(); 347 | 348 | LCD_CS_SETOUTPUT(); 349 | LCD_CS_HIGH(); 350 | 351 | LCD_RESET_SETOUTPUT(); 352 | LCD_RESET_HIGH(15); 353 | 354 | Init(); 355 | } 356 | 357 | void TestSuite() 358 | { // It's not really a test suite, it just does some colourful things. 359 | int dly = 512; 360 | long t0, t1; 361 | for (int i = 0; i < 8; i++) 362 | { 363 | if (dly > 0 && i > 0) { 364 | delay(dly); 365 | dly >>= 1; 366 | } 367 | t0 = micros(); 368 | int backColour = backGroundColours[i % 3]; 369 | ClearScreen(backColour); 370 | t1 = micros(); 371 | if (i >= 3) 372 | { 373 | // Now draw some rectangles on the background. 374 | for (int i = 0; i < 100; i++) 375 | { 376 | Rect(random(300), random(300), random(300), random(300), random(65535)); 377 | // rectangle at x, y, width, height, color 378 | } 379 | } 380 | } 381 | Serial.printf("%d microsecs to set all screen pixels\n", t1 - t0); 382 | float fps = 1000000.0 / (t1 - t0); 383 | float dataRate = ((fps / (1024.0 * 1024.0)) * 240 * 320 * 2); 384 | Serial.printf("Theoretical max LCD FPS=%.2f.\nClearScreen() data rate=%.2f MB per sec.\n", fps, dataRate); 385 | } 386 | }; 387 | 388 | -------------------------------------------------------------------------------- /CamToLCD/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Pete Wentworth cspwcspw@gmail.com. Sept 2018 4 | // Released under Apache License 2.0 5 | 6 | #include "Arduino.h" 7 | #include 8 | 9 | #ifdef DebuggingPort 10 | 11 | class DebugPort 12 | { // This simple debugging output facility that can be dynamically 13 | // switched on or off as the program is running. 14 | // It sends output to Serial. 15 | private: 16 | bool enabled; 17 | HardwareSerial *serialDevice; 18 | 19 | public: 20 | // Set up to a debugger that sends stuff to this serial port 21 | DebugPort(HardwareSerial *device, bool enableOutput) { 22 | serialDevice = device; 23 | enabled = enableOutput; 24 | } 25 | 26 | void enableOutput(bool v) { 27 | enabled = v; 28 | } 29 | 30 | void print(int a) { 31 | if (enabled) serialDevice->print(a); 32 | } 33 | 34 | void println(int a) { 35 | if (enabled) serialDevice->println(a); 36 | } 37 | 38 | void print(const char* s) { 39 | if (enabled) serialDevice->print(s); 40 | } 41 | 42 | void println(const char* s) { 43 | if (enabled) serialDevice->println(s); 44 | } 45 | 46 | void println() { 47 | if (enabled) serialDevice->println(); 48 | } 49 | 50 | void printf(char *format, ...) { 51 | if (!enabled) return; 52 | char buf[512]; 53 | va_list arg; 54 | va_start (arg, format); 55 | vsprintf(buf, format, arg); 56 | va_end(arg); 57 | serialDevice->print(buf); 58 | } 59 | }; 60 | 61 | // And a declaration that somewhere, somebody will create the object called debug 62 | extern DebugPort debug; 63 | 64 | #define debug_println(a) debug.println(a) 65 | #define debug_print(a) debug.print(a) 66 | #define debug_printf(fmt, ...) debug.printf(fmt, ##__VA_ARGS__) 67 | #define debug_enableOutput(val) debug.enableOutput(val) 68 | 69 | #else 70 | #define debug_println(a) 71 | #define debug_print(a) 72 | #define debug_printf(fmt, ...) 73 | #define debug_enableOutput(enabled) 74 | #endif 75 | 76 | // Another way to debug is to directly toggle a GPIO pin and 77 | // watch it with a LED, a logic analyzer, or a scope. 78 | 79 | // #define debugPin 13 80 | #ifdef debugPin 81 | #define debugSetup() pinMode(debugPin, OUTPUT) 82 | #define debugHigh() digitalWrite(debugPin, HIGH) 83 | #define debugLow() digitalWrite(debugPin, LOW) 84 | #endif 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ESP32_Pin_Usage.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cspwcspw/ESP32_CamToLCD/9dd0f32906620febc75bc80d576d1b543979b3b0/ESP32_Pin_Usage.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ESP32_CamToLCD 3 | 4 | Pete Wentworth cspwcspw@gmail.com 5 | 6 | 7 | 8 | The project uses an ESP32 processor to 9 | stream video from my OV7670 camera, and to send 10 | it in real time to a 320x240 LCD screen. 11 | 12 | I am able to get 25 frames per second throughput at 13 | 320x240 pixels, each pixel is 16-bits, or two bytes, 14 | RGB565 colour format. This format is supported by both the 15 | camera the screen, so no format conversion is required. 16 | 17 | I discuss some key overview ideas here to help clarify what I did. 18 | 19 | The rest of this document is in sections: 20 | * The Hardware, 21 | * The Camera Side of Things, 22 | * The Main Program, 23 | * The LCD Side of Things, 24 | * A Summary. 25 | 26 | ## The Hardware 27 | 28 | All are cheap goodies from Banggood: 29 | 30 | * The [OV7670 Camera](https://www.banggood.com/VGA-OV7670-CMOS-Camera-Module-Lens-CMOS-640X480-SCCB-With-I2C-Interface-p-1320365.html) has no FIFO nor does it have its own clock. 31 | 32 | * The [320x240 LCD Arduino shield](https://www.banggood.com/2_4-Inch-TFT-LCD-Shield-240320-Touch-Board-Display-Module-With-Touch-Pen-For-Arduino-UNO-p-1171082.html) has an 8-bit parallel data bus - it is not a serial SPI device. 33 | 34 | * And an [ESP32 Devkit](https://www.banggood.com/ESP32-Development-Board-WiFiBluetooth-Ultra-Low-Power-Consumption-Dual-Cores-ESP-32-ESP-32S-Board-p-1109512.html). 35 | 36 | I didn't do a schematic. Instead, I wrote a more general document [ESP_32_Pin_Usage](https://github.com/cspwcspw/ESP32_CamToLCD/blob/master/ESP32_Pin_Usage.pdf) which summarizes some pin usage limitations I found helpful. The exact pin connections for this project at the end of that document. 37 | 38 | ## The Camera Side of Things 39 | 40 | My starting point was the [ESP32CameraI2S project](https://github.com/bitluni/ESP32CameraI2S) by Bitluni, in turn built on the [esp32-cam-demo](https://github.com/igrr/esp32-cam-demo) by Ivan Grokhotkov (igrr). 41 | 42 | They use one of the two ESP32's 43 | I2S engines in conjunction with 44 | Direct Memory Access (DMA). The camera is the master 45 | device controlling the I2S bus, and sends pixel and clocking 46 | information to the ESP32. With the correct setup, bytes are 47 | directly stored into a DMA buffer in memory. 48 | 49 | All this takes place in the background - it leaves 50 | the ESP32 free to do other things. (Thats what all these extra 51 | peripheral controllers in the ESP32 are meant for: to relieve 52 | the burden on the main processors.) 53 | 54 | When the IS2 controller has gathered a whole scan line into the 55 | buffer, it generates an interrupt. Now the main processor can deal 56 | with the latest scanline. 57 | 58 | There is a second interrupt in play too: at the end of each frame 59 | the camera generates a VSYNC signal, which is used to 60 | cause a VSYNC interrupt. This allows us to deal with the 61 | finalization of the current frame and set up for the start of the next frame. 62 | 63 | At 25 frames per second, 320x240, we get _**6000**_ scanlines 64 | arriving per second. Allowing some padding between frames, 65 | we have about _**156**_ microseconds between successive scanline interrupts. 66 | 67 | We can also expect 25 VSYNC interrupts per second. 68 | 69 | Compared to the two projects mentioned above, I do some things differently. 70 | 71 | Firstly, they allocate memory for the whole video frame, and re-pack new 72 | scanlines into the frame memory as they arrive. But the ESP32 doesn't have 73 | enough memory for a 320x240x2-byte frame (150KB), so this approach 74 | limits us to lower resolutions. 75 | 76 | I do not move the scanlines out of the DMA buffer. I consume the data in real time and free up the DMA buffer before it is needed again. There are two DMA buffers used alternately. So while the I2S hardware is filling one, we need to be emptying the other one. 77 | 78 | In the original camera code, when capturing a frame, 79 | the code waits for VSYNC to synchronize with the 80 | camera, starts the IS2 engine to collect the scan lines, 81 | and then sits in a busy loop waiting until the whole frame 82 | has arrived. 83 | 84 | Sitting in a busy loop somewhat defeats the purpose 85 | of having a specialized I2S engine running on its own silicon. 86 | 87 | So I keep the camera and the I2S capture engine running continuously. After each VSYNC I immediately stop and restart the I2S engine. More about that in a moment. 88 | 89 | I added two callback functions from the existing interrupt 90 | handlers into the main code, 91 | so when the I2S hardware gets to the end of a scanline and 92 | generates a "DMABuffer Ready" interrupt, it in turn calls 93 | back to the main program to deal with the scanline. 94 | 95 | So in summary, my camera code is a direct descendant of the camera 96 | projects with a few conceptual differences: 97 | * The idea is to consume or dispose of scanlines as they arrive. 98 | * I don't allocate memory or pack scanlines into a frame buffer. 99 | * I run the camera and I2S engine (almost) continuously, 100 | so I avoid the expensive wait for 101 | a fresh VSYNC to re-synchonize I2S and camera. 102 | * I get the interrupt handlers to call back to my main program so that 103 | the application can deal with them. 104 | * I don't wait in any busy loops in the camera code. My only busy loop is ``loop()`` in the main program. 105 | 106 | After each VSYNC I can momentarily stop and restart the I2S engine, but quickly enough so that I know the next scanline will be the first of the next frame. 107 | I do this in case we do lose scanlines or pixels somehow. (I tested this by momentarily grounding the PCLK signal from the camera to confuse the I2S engine. Of course I get bad-looking frames, but once I remove 108 | the ground, things correct themselves after the next VSYNC.) 109 | 110 | 111 | ## The Main Program 112 | 113 | It sets up the camera mode, sets up the LCD, and starts the camera. 114 | Interrupts start pouring in, each with a DMABuffer that holds 115 | a scanline of pixel data. After some scanlines we get a VSYNC. 116 | 117 | I like to organize complex logic like this as a state machine. 118 | So we consider the different states the system could be in, 119 | and what events or processes should move the state machine to another state. 120 | 121 | My design identified five different states, and the two interrupt events, 122 | plus some other things that cause a state change. 123 | 124 | ``` 125 | enum State { 126 | Lost, // We don't know where we are, and need a VSYNC to synchronize ourselves. 127 | Priming, // When we hit this state we'll prime the sink: e.g. send a frame header, open a file, set up LCD etc. 128 | Running, // Queueing blocks as they arrive in the interrupt handler, and sinking the data in the main loop. 129 | Wrapup, // We got a VSYNC. We can wrap up the frame / close a file, restart the I2S engine, print stats, etc. 130 | Overrun // If either VSYNC or a scanline interrupts before we're finalized, we've lost the beat. 131 | }; 132 | ``` 133 | 134 | When the system starts (or after an error or change of camera mode), we 135 | start off in the _**Lost**_ state. The main loop ignores any scanlines that arrive. We remain in this state until we get a VSYNC. 136 | 137 | The VSYNC arrival transfers us to _**Priming**_ state. 138 | Now the main loop primes the LCD: 139 | _"Expect scanlines, here is where I want you to put them on your screen"_. Once priming tasks have taken place, the main loop advances the state to _**Running**_. 140 | 141 | While we're running, when a scanline arrives (via the callback), 142 | we copy the DMABuffer address into a variable, and exit the interrupt. 143 | The main loop continuously tests the variable, and when it finds a 144 | buffer address it passes it to the LCD to consume the buffer contents, 145 | and then sets local variable back to NULL. 146 | So the variable is also acting as a flag for the main loop to know 147 | if a fresh DMAbuffer is waiting. 148 | 149 | If a new scanline arrives before the main loop has managed 150 | to clear the old one, we just overwrite the local variable. 151 | Our image will get corrupted, but it is a bit like a 152 | best effort audio stream - there is nothing we can do to recover 153 | the lost data. 154 | 155 | A VSYNC finally take us out of the _**Running**_ state to _**Wrapup**_ 156 | state. This is when we have the most time available in the main loop. 157 | We assume the final scanline has already been flushed (there is a 158 | considerable timing gap before VSYNC) 159 | we count the frame, reset the I2S engine, 160 | occasionally print diagnostic information, and even poll for user input 161 | to change the camera mode or tweak some camera register settings. 162 | 163 | If either callback occurs during wrap-up, we'll 164 | be put into the _**Overrun**_ state. Once we've done all the 165 | wrap-up tasks for the current frame, we've either beaten our 166 | deadline and can immediately go back to _**Priming**_ 167 | for the next frame. If we've overrun, we're lost 168 | we have no option but to skip a frame and wait for the next VSYNC. 169 | This we accomplish by transitioning back to state _**Lost**_. 170 | 171 | Besides some initialization, the only two ways our main 172 | program interacts with the LCD is to prime it to tell it 173 | where to put the next pixels, and then to pass it the DMABuffer 174 | addresses whenever a buffer needs to be consumed. 175 | 176 | 177 | ## The LCD Side of Things 178 | 179 | We've covered elsewhere how we drive the LCD at high speed. In summary, 180 | we send a byte of data at a time to the LCD. The fast way to do this is to have available 181 | a pre-arranged 32-bit mask. Suppose the byte I want to send is `0x52`. Its bit pattern is `01010010`. (i.e. three GPIO lines need to be high). 182 | 183 | Depending on which GPIOs are wired up to each LCD data bit, we can construct a 184 | 32-bit "set-these-bits-mask" with three one-bits in the correct places. Direct port writes allow us to also use a mask to clear all 8 data lines at once. 185 | 186 | Since there are only 256 possible bytes to send to the LCD, once we know 187 | GPIO pin numbers for the data bus we can pre-compute all the bit-setting masks. 188 | 189 | So at initialization we compute a 256-entry table. Sending a byte onto the bus requires 190 | clearing the bus, using the byte as an array index to find the bitmask 191 | that will set the appropriate bits. We can then use a direct port write 192 | to output the byte. Once the byte is on the bus we need to strobe high 193 | the LCD Write line. A trick of taking it low at the same time 194 | as we take other bus lines low means writing a byte becomes a really 195 | efficient macro: 196 | ``` 197 | #define Write_Byte(d) { GPIO.out_w1tc=busPinsLowMask; GPIO.out_w1ts=outputMaskMap[d]; LCD_WR_HIGH(); } 198 | ``` 199 | 200 | 201 | The other important method in the LCD code is the code for consuming the DMABuffer, and sending bytes as fast as we can to the LCD device. 202 | ``` 203 | void SinkDMABuf(int xres, DMABuffer *buf) { 204 | 205 | unsigned char *p = buf->buffer; // Pointer to start of data 206 | 207 | LCD_DC_HIGH(); // Bytes are data, not LCD commands. 208 | for (int i = 0; i < xres * 4; i += 4) { 209 | Write_Byte(p[i]); // Every second byte of the DMA buffer has pixel data 210 | Write_Byte(p[i + 2]); 211 | } 212 | } 213 | ``` 214 | 215 | We said earlier that we expect 6000 scanlines per second. 216 | So we need to handle the scanline in less than 156 microseconds. We 217 | put timing measurements around the call to this method, 218 | and measured just under 100 microseconds to send out 640 bytes 219 | (each pixel is two bytes). 220 | 221 | 6000 x 100 microsec is about 600ms. So this hotspot method accounts for 222 | about 60% of our processor time on the core that we're running on. 223 | 224 | ## Summary 225 | 226 | We kept the interrupt routines short, and we don't fall foul of the 227 | Watchdog Timer. We don't use intermediate buffer memory, 228 | we don't do format conversion of the data. This all helps speed things up. 229 | 230 | A fun project, I learned plenty, and I hope you do too. For what we get for 231 | about $7, the ESP32 is amazing! 232 | 233 | 234 | 235 | 236 | 237 | 238 | --------------------------------------------------------------------------------