Arduino ArduCam – Motion Activated Video Spycam

This project uses a PIR to trigger the ArduCam to record a video file and store it as an .AVI on the SD Card. It names the video file based on a random number generation function. on non-windows systems you can use VLC to ply the video files.


  • ArduCam is quirky with writing to the SD card fro this project. In my experiments I had numerous failures in trying to create a corresponding text log files as I did in the jpeg version of this project.
  • For real world projects like this I would recommend something like the Raspberry Pi.

Prerequisite Classes:



Functional Parts in the Project:

PIR connects to Digital Pin 2, and VCC Splits to ArduCam and PIR using Breadboard
//This is a modification of ArduCAM_Mini_Video2SD Example
#include <stdint.h>
#include <Wire.h>
#include <ArduCAM.h>
#include <SPI.h>
#include <SD.h>
#include "memorysaver.h"

//ETCG Notes -- PIR Sensor
#define pirPin 2
int pirReading;

//This demo can only work on OV2640_MINI_2MP or OV5642_MINI_5MP or OV5642_MINI_5MP_BIT_ROTATION_FIXED platform.
#if !(defined OV5642_MINI_5MP || defined OV5642_MINI_5MP_BIT_ROTATION_FIXED|| defined OV2640_MINI_2MP_PLUS || defined OV2640_MINI_2MP|| defined OV3640_MINI_3MP)
#error Please select the hardware platform and camera module in the ../libraries/ArduCAM/memorysaver.h file

//#define FISHINO_UNO // Nice UNO board with integrated RTC, microSD, WiFi

#define SERIAL_SPEED 115200
#define BUFFSIZE 512  // 512 is a good buffer size for SD writing. 4096 would be better, on boards with enough RAM (not Arduino Uno of course)
#define FRAME_SIZE OV2640_320x240
#define WIDTH_1 0x40 // Video width in pixel, hex. Here we set 320 (Big Endian: 320 = 0x01 0x40 -> 0x40 0x01). For 640: 0x80
#define WIDTH_2 0x01 // For 640: 0x02  
#define HEIGHT_1 0xF0 // 240 pixels height (0x00 0xF0 -> 0xF0 0x00). For 480: 0xE0
#define HEIGHT_2 0x00 // For 480: 0x01 
#define FPS 0x0F // 15 FPS. Placeholder: will be overwritten at runtime based upon real FPS attained
#define TOTAL_FRAMES 200	// Number of frames to be recorded. If < 256, easier to recognize in header (for manual hex debug)
//set pin 7 as the slave select for SPI:
#define SPI_CS  7

//ETCG Notes -- SD Pin
#define SD_CS 10	// 9 on Arducam adapter Uno and SD shields

#define SD_AUX 10	// Needs to be Output on Fishino Uno for the integrated SD card to work
#define AVIOFFSET 240 // AVI main header length

//#define DISABLE_SD

unsigned long movi_size = 0;
unsigned long jpeg_size = 0;
const char zero_buf[4] = {0x00, 0x00, 0x00, 0x00};
const int avi_header[AVIOFFSET] PROGMEM = {
  0x52, 0x49, 0x46, 0x46, 0xD8, 0x01, 0x0E, 0x00, 0x41, 0x56, 0x49, 0x20, 0x4C, 0x49, 0x53, 0x54,
  0xD0, 0x00, 0x00, 0x00, 0x68, 0x64, 0x72, 0x6C, 0x61, 0x76, 0x69, 0x68, 0x38, 0x00, 0x00, 0x00,
  0xA0, 0x86, 0x01, 0x00, 0x80, 0x66, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
  0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  WIDTH_1, WIDTH_2, 0x00, 0x00, HEIGHT_1, HEIGHT_2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x84, 0x00, 0x00, 0x00,
  0x73, 0x74, 0x72, 0x6C, 0x73, 0x74, 0x72, 0x68, 0x30, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x73,
  0x4D, 0x4A, 0x50, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x01, 0x00, 0x00, 0x00, FPS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x66,
  0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, WIDTH_1, WIDTH_2, 0x00, 0x00, HEIGHT_1, HEIGHT_2, 0x00, 0x00,
  0x01, 0x00, 0x18, 0x00, 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54,
  0x10, 0x00, 0x00, 0x00, 0x6F, 0x64, 0x6D, 0x6C, 0x64, 0x6D, 0x6C, 0x68, 0x04, 0x00, 0x00, 0x00,
  0x64, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x00, 0x01, 0x0E, 0x00, 0x6D, 0x6F, 0x76, 0x69,
#if defined (OV2640_MINI_2MP)||defined (OV2640_MINI_2MP_PLUS)
ArduCAM myCAM( OV2640, SPI_CS );
#elif defined (OV3640_MINI_3MP)
ArduCAM myCAM( OV3640, SPI_CS );
ArduCAM myCAM( OV5642, SPI_CS );

static void inline print_quartet(unsigned long i, File fd)
{ // Writes an uint32_t in Big Endian at current file position
  fd.write(i % 0x100);  i = i >> 8;   //i /= 0x100;
  fd.write(i % 0x100);  i = i >> 8;   //i /= 0x100;
  fd.write(i % 0x100);  i = i >> 8;   //i /= 0x100;
  fd.write(i % 0x100);

static void Video2SD()
{ // We don't enforce FPS: we just record and save frames as fast as possible
  // Then we compute the attained FPS and update the AVI header accordingly
  char str[8];
  uint16_t n;
  File outFile;
  byte buf[BUFFSIZE];
  static int i = 0;
  uint8_t temp = 0, temp_last = 0;
  unsigned long fileposition = 0;
  uint16_t frame_cnt = 0;
  uint16_t remnant = 0;
  uint32_t length = 0;
  uint32_t startms;
  uint32_t elapsedms;
  uint32_t uVideoLen = 0;
  bool is_header = false;

#ifndef DISABLE_SD
  //ETCG Notes -- Create File Name
  // Create a avi file. Name should be unique-ish, but short
  digitalWrite(SD_CS, HIGH);
 randomSeed(analogRead(0) * millis());
  n = (random(2, 999));   // Don't use 1.avi: was the default in old code, we don't want to overwrite old recordings
  itoa(n, str, 10);
  strcat(str, ".avi");
  //Open the new file
  outFile =, O_WRITE | O_CREAT | O_TRUNC);
  if (! outFile)
    Serial.println(F("File open failed"));
    while (1);


  //Write AVI Main Header
  // Some entries will be overwritten later
  for ( i = 0; i < AVIOFFSET; i++)
    char ch = pgm_read_byte(&avi_header[i]);
    buf[i] = ch;
#ifndef DISABLE_SD
  outFile.write(buf, AVIOFFSET);

  Serial.print(F("\nRecording "));
  Serial.println(F(" video frames: please wait...\n"));

  startms = millis();

  //Write video data, frame by frame
  for ( frame_cnt = 0; frame_cnt < TOTAL_FRAMES; frame_cnt++)
#if defined (ESP8266)
    temp_last = 0; temp = 0;
    //Capture a frame
    //Flush the FIFO
    //Clear the capture done flag
    //Start capture
    // Wait for frame ready
    while (!myCAM.get_bit(ARDUCHIP_TRIG , CAP_DONE_MASK));
    length = myCAM.read_fifo_length();	// Length of FIFO buffer. In general, it contains more than 1 JPEG frame;
    // so we'll have to check JPEG markers to save a single JPEG frame
    SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));

#ifndef DISABLE_SD
    // Write segment. We store 1 frame for each segment (video chunk)
    outFile.write("00dc");	// "start of video data chunk" (00 = data stream #0, d = video, c = "compressed")
    outFile.write(zero_buf, 4);	// Placeholder for actual JPEG frame size, to be overwritten later
    i = 0;
    jpeg_size = 0;

    // Deassert camera Chip Select to start SPI transfer
    // Set FIFO to burst read mode
    // Transfer data, a byte at a time
    while ( length-- )
    { // For every byte in the FIFO...
#if defined (ESP8266)
      // We always need the last 2 bytes, to check for JPEG begin/end markers
      temp_last = temp; // Save current temp value
      temp =  SPI.transfer(0x00); // Overwrite temp with 1 byte from FIFO (0x00 is dummy byte for the slave: we are reading, the slave will ignore it)
      // a JPEG ends with the two bytes 0xFF, 0xD9
      if ( (temp == 0xD9) && (temp_last == 0xFF) ) //	End of the image
        buf[i++] = temp;  // Add this last byte to the buffer
        myCAM.CS_HIGH();  // End of transfer: re-assert Slave Select
#ifndef DISABLE_SD
        // Write the buffer to file
        outFile.write(buf, i);
        is_header = false;	// We are at the last byte of the JPEG: sure is not the header :)
        jpeg_size += i;		// Update total jpeg size with this last buffer size
        i = 0;				// Reset byte counter (restart writing from the first element of the buffer)
      if (is_header == true)  // Not at end of JPEG, yet
        //Write image data to buffer if not full
        if (i < BUFFSIZE)
          buf[i++] = temp;
        { // Buffer is full: transfer to file
          myCAM.CS_HIGH();  // End SPI transfer

#ifndef DISABLE_SD
          //Write BUFFSIZE bytes image data to file
          outFile.write(buf, BUFFSIZE);
          i = 0;	// Restart writing from the first element
          buf[i++] = temp;	// Save current byte as first in "new" buffer
          myCAM.CS_LOW();   // Re-enable SPI transfer
          myCAM.set_fifo_burst();	// Set FIFO to burst read mode
          jpeg_size += BUFFSIZE;
      else if ((temp == 0xD8) & (temp_last == 0xFF))
      { // A JPEG starts with the two bytes 0xFF, 0XD8; so here we are at the beginning of the JPEG
        is_header = true;
        buf[i++] = temp_last;	// Save the first two bytes (off-cycle)
        buf[i++] = temp;
    }	// end loop over each byte in the FIFO: JPEG is complete

    // Padding
    remnant = jpeg_size & 0x00000001;	// Align to 16 bit: add 0 or 1 "0x00" bytes
#ifndef DISABLE_SD
    if (remnant > 0)
      outFile.write(zero_buf, remnant); // see
    movi_size += jpeg_size;	// Update totals
    uVideoLen += jpeg_size;   // <- This is for statistics only

    // Now we have the real frame size in bytes. Time to overwrite the placeholder

#ifndef DISABLE_SD
    fileposition = outFile.position();  // Here, we are at end of chunk (after padding) - jpeg_size - remnant - 4); // Here we are the the 4-bytes blank placeholder
    print_quartet(jpeg_size, outFile);    // Overwrite placeholder with actual frame size (without padding) - jpeg_size - remnant + 2); // Here is the FOURCC "JFIF" (JPEG header)
    outFile.write("AVI1", 4);         // Overwrite "JFIF" (still images) with more appropriate "AVI1"

    // Return to end of JPEG, ready for next chunk;
  }	// End cycle for all frames

  // Compute statistics
  elapsedms = millis() - startms;
  float fRealFPS = (1000.0f * (float)frame_cnt) / ((float)elapsedms);
  float fmicroseconds_per_frame = 1000000.0f / fRealFPS;
  uint8_t iAttainedFPS = round(fRealFPS); // Will overwrite AVI header placeholder
  uint32_t us_per_frame = round(fmicroseconds_per_frame); // Will overwrite AVI header placeholder

#ifndef DISABLE_SD
  //Modify the MJPEG header from the beginning of the file, overwriting various placeholders;
  print_quartet(movi_size + 12 * frame_cnt + 4, outFile); //    riff file size
  //overwrite hdrl
  print_quartet(us_per_frame, outFile);
  unsigned long max_bytes_per_sec = movi_size * iAttainedFPS / frame_cnt; //hdrl.avih.max_bytes_per_sec;
  print_quartet(max_bytes_per_sec, outFile);
  print_quartet(frame_cnt, outFile);;
  print_quartet((int)iAttainedFPS, outFile);
  print_quartet(frame_cnt, outFile);;
  print_quartet(movi_size, outFile);// size again
  //Close the file

  Serial.println(F("\n*** Video recorded and saved ***\n"));
  Serial.print(F("Recorded "));
  Serial.print(elapsedms / 1000);
  Serial.print(F("s in "));
  Serial.print(F(" frames\nFile size is "));
  Serial.print(movi_size + 12 * frame_cnt + 4);
  Serial.print(F(" bytes\nActual FPS is "));
  Serial.print(fRealFPS, 2);
  Serial.print(F("\nMax data rate is "));
  Serial.print(F(" byte/s\nFrame duration is "));
  Serial.println(F(" us"));
  Serial.print(F("Average frame length is "));
  Serial.print(uVideoLen / TOTAL_FRAMES);
  Serial.println(F(" bytes"));


void setup()
   //ETCG Note -- PIR Sensor
  pinMode(pirPin, INPUT);
  uint8_t vid, pid;
  uint8_t temp;

  while (!Serial);

  Serial.println(F("ArduCAM Start!\n"));
  delay(5000);	// Gain time to start logic analyzer

#ifndef DISABLE_SD
  // set the SPI_CS as an output:
  pinMode(SD_CS, OUTPUT);
  digitalWrite(SD_CS, HIGH);
  pinMode(SD_AUX, OUTPUT);


  // initialize SPI:

#ifndef DISABLE_SD
  //Initialize SD Card
  while (!SD.begin(SD_CS)) {
    Serial.println(F("SD Card Error!")); delay(1000);
  Serial.println(F("SD Card detected."));

  //Reset the CPLD
  myCAM.write_reg(0x07, 0x80);
  myCAM.write_reg(0x07, 0x00);

  while (1) {
    //Check if the ArduCAM SPI bus is OK
    myCAM.write_reg(ARDUCHIP_TEST1, 0x55);
    temp = myCAM.read_reg(ARDUCHIP_TEST1);
    if (temp != 0x55)
      Serial.println(F("SPI interface Error!"));
      delay(1000); continue;
    } else {
      Serial.println(F("SPI interface OK.")); break;
#if defined (OV2640_MINI_2MP)||defined (OV2640_MINI_2MP_PLUS)
  while (1)
    //Check if the camera module type is OV2640
    myCAM.wrSensorReg8_8(0xff, 0x01);
    myCAM.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid);
    myCAM.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid);
    if ((vid != 0x26 ) && (( pid != 0x41 ) || ( pid != 0x42 )))
      Serial.println(F("Can't find OV2640 module!"));
      delay(1000); continue;
    else {
      Serial.println(F("OV2640 detected.")); break;
#elif defined (OV3640_MINI_3MP)
  while (1) {
    //Check if the camera module type is OV3640
    myCAM.rdSensorReg16_8(OV3640_CHIPID_HIGH, &vid);
    myCAM.rdSensorReg16_8(OV3640_CHIPID_LOW, &pid);
    if ((vid != 0x36) || (pid != 0x4C)) {
      Serial.println(F("Can't find OV3640 module!"));
      delay(1000); continue;
    } else {
      Serial.println(F("OV3640 detected.")); break;
  while (1) {
    //Check if the camera module type is OV5642
    myCAM.wrSensorReg16_8(0xff, 0x01);
    myCAM.rdSensorReg16_8(OV5642_CHIPID_HIGH, &vid);
    myCAM.rdSensorReg16_8(OV5642_CHIPID_LOW, &pid);
    if ((vid != 0x56) || (pid != 0x42)) {
      Serial.println(F("Can't find OV5642 module!"));
      delay(1000); continue;
    else {
      Serial.println(F("OV5642 detected.")); break;
#if defined (OV2640_MINI_2MP)||defined (OV2640_MINI_2MP_PLUS)
#elif defined (OV3640_MINI_3MP)
  myCAM.write_reg(ARDUCHIP_TIM, VSYNC_LEVEL_MASK);   //VSYNC is active HIGH

void loop() {
  //ETCG Notes -- Motion Sensor Trigger
  pirReading = digitalRead(pirPin);
  if (pirReading == HIGH) {
    Serial.println("Motion Detected");
  } else {
    Serial.println("No Motion Detected");

Be the first to comment

Leave a Reply