T3.6 + Arducam Mini2MP = recording 320x240 14 FPS on microSD :)

Status
Not open for further replies.

XFer

Well-known member
With the latest Arducam library, I've finally managed to have a working sketch with Teensy 3.6 recording video from the Arducam Mini2MP module and saving it to the onboard microSD.
Output format is MJPEG AVI.
Due probably to some bottleneck inside the Arducam module (OV2640->AL422->MAXX 2->SPI) I can't pull more than 14 FPS at 320x240: not top-notch video, but not bad after all.
Unfortunately, it drops to 3.7 FPS if switching to VGA capture (640x480).
The Arducam Mini2MP module is nice: cheap, easy to connect (SCCB which really is I2C for control, SPI for data).

Here's the sketch, adapted from a (then non-working) example from Arducam library (remember to pull the latest Arducam library and to configure its memorysaver.h):

Code:
// ArduCAM Mini demo (C)2017 Lee
// Web: http://www.ArduCAM.com
// This program is a demo of how to use most of the functions
// of the library with ArduCAM Mini camera, and can run on any Arduino platform.
// This demo was made for ArduCAM Mini Camera.
//This demo timed 5 seconds to record video.
// It can shoot video and store it into the SD card
// The demo sketch will do the following tasks
// 1. Set the camera to JPEG output mode.
// 2. Capture a JPEG photo and buffer the image to FIFO
// 3.Write AVI Header
// 4.Write the video data to the SD card
// 5.More updates AVI file header
// 6.close the file
//The file header introduction
//00-03 :RIFF
//04-07 :The size of the data
//08-0B :File identifier
//0C-0F :The first list of identification number
//10-13 :The size of the first list
//14-17 :The hdr1 of identification
//18-1B :Hdr1 contains avih piece of identification 
//1C-1F :The size of the avih
//20-23 :Maintain time per frame picture

// This program requires the ArduCAM V4.0.0 (or later) library and ArduCAM Mini camera
// and use Arduino IDE 1.6.8 compiler or above

// FC aka XFer: last edit 20181106
// 14 FPS sustained on Teensy 3.6 at 320x240, with subject movement (87 KB/s max data rate)
// Drops to 3.8 FPS at 640x480: we simply don't get more speed from the camera. FIFO bandwidth issue? CPLD bandwidth issue?
// Note: Mini2MP module has onboard 7K pullups on I2C. We can do without external pullups if wires are kept short

// TODO: 
// Preallocate output file (create at max length, fill with 0, rewind, then truncate to actual bytes written after capture finished)

#include <stdint.h>
#include <Wire.h>
#include <ArduCAM.h>
#include <SPI.h>
#include <SdFat.h>  // Use the excellent SdFat library https://github.com/greiman/SdFat
#include "memorysaver.h"

// THIS VERSION IS FOR TEENSY 3.5/3.6 https://www.pjrc.com/store/

// DEFINES
//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|| defined OV3640_MINI_3MP)
  #error Please select the hardware platform and camera module in the ../libraries/ArduCAM/memorysaver.h file
#endif
#define SERIAL_SPEED 115200
#define BUFFSIZE 4096   // Good compromise between latency and write speed on T3.5/3.6
#define FRAME_SIZE OV2640_320x240 // OV2640_640x480 
#define WIDTH_1 0x40    // 320 (Big Endian: 320 = 0x01 0x40 -> 0x40 0x01). For 640: 0x80 0x02
#define WIDTH_2 0x01 
#define HEIGHT_1 0xF0   // 240 (0x00 0xF0 -> 0xF0 0x00). For 480: 0xE0 0x01	
#define HEIGHT_2 0x00 
#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
#define AVIOFFSET 240 // AVI main header length

// GLOBALS
SdFatSdioEX SD; // Much more efficient than standard SD, expecially on Teensy 3.5-3.6 (up to 18 MB/s write speed!)
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)
  ArduCAM myCAM( OV2640, SPI_CS );
#elif defined (OV3640_MINI_3MP)
  ArduCAM myCAM( OV3640, SPI_CS );
#else
  ArduCAM myCAM( OV5642, SPI_CS );
#endif
// END GLOBALS


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;


	// Create a avi file. Try it to be unique, but short	
    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");
    Serial.print("\nFile name will be "); Serial.println(str);
  
	//Open the new file
	outFile = SD.open(str, O_WRITE | O_CREAT | O_TRUNC);
	if (! outFile)
	{
	  Serial.println(F("File open failed"));
	  while (1);
	  return;
	}


	//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;
	}
	outFile.write(buf, AVIOFFSET);
  
    Serial.print(F("\nRecording "));
    Serial.print(TOTAL_FRAMES); 
    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)
	  yield();
	  #endif
	  temp_last = 0;temp = 0;
	  //Capture a frame            
	  //Flush the FIFO
	  myCAM.flush_fifo();
	  //Clear the capture done flag
	  myCAM.clear_fifo_flag();
	  //Start capture
	  myCAM.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
#if defined(SPI_HAS_TRANSACTION)
      SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
    #endif

	  // Deassert camera Chip Select to start SPI transfer
	  myCAM.CS_LOW();

	// 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;
    // Set FIFO to burst read mode
	  myCAM.set_fifo_burst();
    // Transfer data, a byte at a time
	  while ( length-- )
	  {	// For every byte in the FIFO...
  		#if defined (ESP8266)
  		yield();
  		#endif
		// 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)
      #if defined(SPI_HAS_TRANSACTION)
        SPI.endTransaction();
      #endif     
		// 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
		  // 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;
  		  else
  		  { // Buffer is full: transfer to file
    			//Write BUFFSIZE bytes image data to file
    			myCAM.CS_HIGH();  // End SPI transfer
    			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
      if (remnant > 0)
      {
	  //  jpeg_size += remnant; // Wrong: jpeg_size and movi_size should not include padding:
        outFile.write(zero_buf, remnant); // see https://docs.microsoft.com/en-us/windows/desktop/directshow/avi-riff-file-reference
      }

	  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

	  fileposition = outFile.position();  // Here, we are at end of chunk (after padding)
	  outFile.seek(fileposition - 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)
      outFile.seek(fileposition - jpeg_size - remnant + 6); // 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
	  outFile.seek(fileposition);
	}	// End cycle for all frames
  // END CAPTURE
  
    // 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

	//Modify the MJPEG header from the beginning of the file, overwriting various placeholders
	outFile.seek(4);
	print_quartet(movi_size + 12*frame_cnt + 4, outFile); //    riff file size
	//overwrite hdrl
	//hdrl.avih.us_per_frame:
	outFile.seek(0x20);
	print_quartet(us_per_frame, outFile);
	unsigned long max_bytes_per_sec = movi_size * iAttainedFPS / frame_cnt; //hdrl.avih.max_bytes_per_sec
	outFile.seek(0x24);
	print_quartet(max_bytes_per_sec, outFile);
	//hdrl.avih.tot_frames
	outFile.seek(0x30);
	print_quartet(frame_cnt, outFile);
    outFile.seek(0x84);
    print_quartet((int)iAttainedFPS, outFile);
	//hdrl.strl.list_odml.frames
	outFile.seek(0xe0);
	print_quartet(frame_cnt, outFile);
	outFile.seek(0xe8);
	print_quartet(movi_size, outFile);// size again
	myCAM.CS_HIGH();
	//Close the file
	outFile.close();

	Serial.println(F("\n*** Video recorded and saved ***\n"));
    Serial.print(F("Recorded "));
	Serial.print(elapsedms / 1000);
	Serial.print(F("s in "));
	Serial.print(frame_cnt);
	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(max_bytes_per_sec);
	Serial.print(F(" byte/s\nFrame duration is "));
	Serial.print(us_per_frame);
	Serial.println(F(" us"));
    Serial.print(F("Average frame length is "));
    Serial.print(uVideoLen / TOTAL_FRAMES);
    Serial.println(F(" bytes"));
}

////////////
//
//  SETUP
//
////////////

void setup()
{
	uint8_t vid, pid;
	uint8_t temp;

	Wire.begin();
	Serial.begin(SERIAL_SPEED);
	while(!Serial);

	Serial.println(F("ArduCAM Start!\n"));
	// set the SPI_CS as an output:
	pinMode(SPI_CS, OUTPUT);
	digitalWrite(SPI_CS, HIGH);
	delay(1000);
	// initialize SPI:
	SPI.begin();
  
	//Reset the CPLD
	myCAM.write_reg(0x07, 0x80);
	delay(100);
	myCAM.write_reg(0x07, 0x00);
	delay(100);
  
	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;
	  }
	}
	//Initialize SD Card
	while(!SD.begin()){
	  Serial.println(F("SD Card Error!"));delay(1000);
	}
	Serial.println(F("SD Card detected."));
	SD.chvol();

	#if defined (OV2640_MINI_2MP)
	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;    
		}
	 } 
	#else
	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;
	  } 
	}
	#endif
	myCAM.set_format(JPEG);
	myCAM.InitCAM();
	#if defined (OV2640_MINI_2MP)
	  myCAM.OV2640_set_JPEG_size(FRAME_SIZE);
	#elif defined (OV3640_MINI_3MP)
	  myCAM.OV3640_set_JPEG_size(OV3640_320x240);
	#else
	  myCAM.write_reg(ARDUCHIP_TIM, VSYNC_LEVEL_MASK);   //VSYNC is active HIGH
	  myCAM.OV5642_set_JPEG_size(OV5642_320x240);
	#endif

	  delay(1000);

	Video2SD();  
}

void loop(){
delay(500000);  
}
 
Looks interesting - Will have to try it out as my earlier attempts to use this camera failed!

Looks like I need to look for where this library object is defined: SdFatSdioEX

It is not located in my Arduino Teensy install nor in my Arduino libraries... The only place I find it in those files is a test program for for ILI9341_t3DMA library...
 
Last edited:
Yep - I have SdFat installed... Turned out I had an older version as well, which was conflicting.

Still have a compile error, which I will look into:
It has to do with SPIBUSY and some other constants not not being defined:
Code:
C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp: In member function 'void ArduCAM::setDataBits(uint16_t)':

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3208:28: error: 'SPIMMOSI' was not declared in this scope

   const uint32_t mask = ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO));

                            ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3208:40: error: 'SPILMOSI' was not declared in this scope

   const uint32_t mask = ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO));

                                        ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3208:53: error: 'SPIMMISO' was not declared in this scope

   const uint32_t mask = ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO));

                                                     ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3208:65: error: 'SPILMISO' was not declared in this scope

   const uint32_t mask = ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO));

                                                                 ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3210:3: error: 'SPI1U1' was not declared in this scope

   SPI1U1 = ((SPI1U1 & mask) | ((bits << SPILMOSI) | (bits << SPILMISO)));

   ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp: In member function 'void ArduCAM::transferBytes_(uint8_t*, uint8_t*, uint8_t)':

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3214:10: error: 'SPI1CMD' was not declared in this scope

   while (SPI1CMD & SPIBUSY) {}

          ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3214:20: error: 'SPIBUSY' was not declared in this scope

   while (SPI1CMD & SPIBUSY) {}

                    ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3219:34: error: 'SPI1W0' was not declared in this scope

   volatile uint32_t * fifoPtr = &SPI1W0;

                                  ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3237:3: error: 'SPI1CMD' was not declared in this scope

   SPI1CMD |= SPIBUSY;

   ^

C:\Users\kurte\Documents\Arduino\libraries\ArduCAM\ArduCAM.cpp:3237:14: error: 'SPIBUSY' was not declared in this scope

   SPI1CMD |= SPIBUSY;

              ^

This is in their library code: https://github.com/ArduCAM/Arduino/blob/master/ArduCAM/ArduCAM.cpp#L3214

It appears like the Teensyduino builds are lumped with esp8266 in the code and all of these defines are in the esp8266 code chain...
Will create an issue up on github...
 
Actually now that I looking, it appears like you already have an issue opened up there... Also a text file of your changes... Will try applying them and see if it now builds.
 
Actually now that I looking, it appears like you already have an issue opened up there... Also a text file of your changes... Will try applying them and see if it now builds.

I think I've got that patch reversed! First time I've used that Windows diff tool :(
You could use the attachment to my previous message (it also supports SPI transactions, which they did not include in their latest version)
 
Status
Not open for further replies.
Back
Top