This information is intended for Advanced users ,a typical user does not need to have an understanding of the protocols used by the Interfaces in order to use Speeduino.
Speeduino can be interfaced to via several ways.
This is the primary interface and the way in which TunerStudio connects to Speeduino in order to program/configure its settings.
Only a single device can communicate with Speeduino via the USB at a time, this is usually a laptop or other computer running the TunerStudio Application software.
It is also possible to use this interface with other devices if the correct communication protocol is used. Great care must be taken as it is possible to corrupt the configuration of your Speeduino MCU such that it no longer functions correctly or at all!
It is highly recommended to connect Dashes,Dataloggers and other Third party devices via the Secondary Serial interface or Canbus(if available)
The Speeduino Primary serial protocol uses a request/response method, in that untill it recieves the correct set of commands it will not transmit data out.You must not send additional commands until the current one has been actioned.
All data is little-endian. (Low byte first.) Data is sent in binary format and there is no conversion to text.Commands are case sensitive.
This Command is for legacy use only. It returns the current realtime data.
The data value list speeduino replies with can be seen below , along with their function.ONLY the data value is sent NOT its order number or description.
The format to send is
'a' , '0' , '6'
Speeduino replies with
highByte(currentStatus.secl)
lowByte(currentStatus.secl)
highByte(currentStatus.PW1)
lowByte(currentStatus.PW1)
highByte(currentStatus.PW2)
lowByte(currentStatus.PW2)
highByte(currentStatus.RPM)
lowByte(currentStatus.RPM)
highByte(currentStatus.advance * 10)
lowByte(currentStatus.advance * 10)
currentStatus.nSquirts);
currentStatus.engine);
currentStatus.afrTarget);
currentStatus.afrTarget); // send twice so afrtgt1 == afrtgt2
(99)
// send dummy data as we don't have wbo2_en1
(99)
// send dummy data as we don't have wbo2_en2
highByte(currentStatus.baro * 10)
lowByte(currentStatus.baro * 10)
highByte(currentStatus.MAP * 10)
lowByte(currentStatus.MAP * 10)
highByte(currentStatus.IAT * 10)
lowByte(currentStatus.IAT * 10)
highByte(currentStatus.coolant * 10)
lowByte(currentStatus.coolant * 10)
highByte(currentStatus.TPS * 10)
lowByte(currentStatus.TPS * 10)
highByte(currentStatus.battery10)
lowByte(currentStatus.battery10)
highByte(currentStatus.O2)
lowByte(currentStatus.O2)
highByte(currentStatus.O2_2)
lowByte(currentStatus.O2_2)
(99)
// blank data for knock
(99)
// blank data for knock
highByte(currentStatus.egoCorrection * 10)
// egocor1
lowByte(currentStatus.egoCorrection * 10)
// egocor1
highByte(currentStatus.egoCorrection * 10)
// egocor2
lowByte(currentStatus.egoCorrection * 10)
// egocor2
highByte(currentStatus.iatCorrection * 10)
// aircor
lowByte(currentStatus.iatCorrection * 10)
// aircor
highByte(currentStatus.wueCorrection * 10)
// warmcor
lowByte(currentStatus.wueCorrection * 10)
// warmcor
(99)
// blank data for accelEnrich
(99)
// blank data for accelEnrich
(99)
// blank data for tpsFuelCut
(99)
// blank data for tpsFuelCut
(99)
// blank data for baroCorrection
(99)
// blank data for baroCorrection
highByte(currentStatus.corrections * 10)
// gammaEnrich
lowByte(currentStatus.corrections * 10)
// gammaEnrich
highByte(currentStatus.VE * 10)
// ve1
lowByte(currentStatus.VE * 10)
// ve1
highByte(currentStatus.VE2 * 10)
// ve2
lowByte(currentStatus.VE2 * 10)
// ve2
(99)
// blank data for iacstep
(99)
// blank data for iacstep
(99)
// blank data for cold_adv_deg
(99)
// blank data for cold_adv_deg
highByte(currentStatus.tpsDOT * 10)
// TPSdot
lowByte(currentStatus.tpsDOT * 10)
// TPSdot
highByte(currentStatus.mapDOT * 10)
// MAPdot
lowByte(currentStatus.mapDOT * 10)
// MAPdot
highByte(currentStatus.dwell * 10)
// dwell
lowByte(currentStatus.dwell * 10)
// dwell
(99)
// blank data for MAF
(99)
// blank data for MAF
(currentStatus.fuelLoad*10)
// fuelload
(99)
// blank data for fuelcor
(99)
// blank data for fuelcor
(99)
// blank data for portStatus
highByte(currentStatus.advance1 * 10)
lowByte(currentStatus.advance1 * 10)
highByte(currentStatus.advance2 * 10)
lowByte(currentStatus.advance2 * 10)
to 114. (99)
// bytes 75 to 114 blank data to fill buffer
This returns all the current realtime data(120 bytes 29/07/2021).
The data value list speeduino replies with can be seen below , along with their function.ONLY the data value is sent NOT its order number or description.
The Format to send is
'A'
Speeduino replies with
currentStatus.secl
//secl is simply a counter that increments each second. Used to track unexpected resets (Which will reset this count to 0)
currentStatus.status1
//status1 Bitfield
currentStatus.engine
//Engine Status Bitfield
currentStatus.syncLossCounter
lowByte(currentStatus.MAP)
highByte(currentStatus.MAP)
(byte)(currentStatus.IAT + CALIBRATION_TEMPERATURE_OFFSET)
//mat
(byte)(currentStatus.coolant + CALIBRATION_TEMPERATURE_OFFSET)
//Coolant ADC
currentStatus.batCorrection
//Battery voltage correction (%)
currentStatus.battery10
//battery voltage
currentStatus.O2
//O2
currentStatus.egoCorrection
//Exhaust gas correction (%)
currentStatus.iatCorrection
//Air temperature Correction (%)
currentStatus.wueCorrection
//Warmup enrichment (%)
lowByte(currentStatus.RPM)
//rpm HB
highByte(currentStatus.RPM)
//rpm LB
(byte)(currentStatus.AEamount >> 1)
//TPS acceleration enrichment (%) divided by 2 (Can exceed 255)
lowByte(currentStatus.corrections)
//Total GammaE (%)
highByte(currentStatus.corrections)
//Total GammaE (%)
currentStatus.VE1
//VE 1 (%)
currentStatus.VE2
//VE 2 (%)
currentStatus.afrTarget
currentStatus.tpsDOT
//TPS DOT
currentStatus.advance
currentStatus.TPS
// TPS (0% to 100%)
lowByte(currentStatus.loopsPerSecond)
highByte(currentStatus.loopsPerSecond)
lowByte(currentStatus.freeRAM)
highByte(currentStatus.freeRAM)
(byte)(currentStatus.boostTarget >> 1
//Divide boost target by 2 to fit in a byte
(byte)(currentStatus.boostDuty / 100)
currentStatus.spark
//Spark related bitfield
lowByte(currentStatus.rpmDOT)
// rpmDOT must be sent as a signed integer
highByte(currentStatus.rpmDOT)
currentStatus.ethanolPct
// Flex sensor value (or 0 if not used)
currentStatus.flexCorrection
// Flex fuel correction (% above or below 100)
currentStatus.flexIgnCorrection
//Ignition correction (Increased degrees of advance) for flex fuel
currentStatus.idleLoad
currentStatus.testOutputs
currentStatus.O2_2
//O2
currentStatus.baro
//Barometer value
lowByte(currentStatus.canin[0])
highByte(currentStatus.canin[0])
lowByte(currentStatus.canin[1])
highByte(currentStatus.canin[1])
lowByte(currentStatus.canin[2])
highByte(currentStatus.canin[2])
lowByte(currentStatus.canin[3])
highByte(currentStatus.canin[3])
lowByte(currentStatus.canin[4])
highByte(currentStatus.canin[4])
lowByte(currentStatus.canin[5])
highByte(currentStatus.canin[5])
lowByte(currentStatus.canin[6])
highByte(currentStatus.canin[6])
lowByte(currentStatus.canin[7])
highByte(currentStatus.canin[7])
lowByte(currentStatus.canin[8])
highByte(currentStatus.canin[8])
lowByte(currentStatus.canin[9])
highByte(currentStatus.canin[9])
lowByte(currentStatus.canin[10])
highByte(currentStatus.canin[10])
lowByte(currentStatus.canin[11])
highByte(currentStatus.canin[11])
lowByte(currentStatus.canin[12])
highByte(currentStatus.canin[12])
lowByte(currentStatus.canin[13])
highByte(currentStatus.canin[13])
lowByte(currentStatus.canin[14])
highByte(currentStatus.canin[14])
lowByte(currentStatus.canin[15])
highByte(currentStatus.canin[15])
currentStatus.tpsADC
getNextError()
lowByte(currentStatus.PW1)
//Pulsewidth 1 multiplied by 10 in ms. Have to convert from uS to mS.
highByte(currentStatus.PW1)
//Pulsewidth 1 multiplied by 10 in ms. Have to convert from uS to mS.
lowByte(currentStatus.PW2)
//Pulsewidth 2 multiplied by 10 in ms. Have to convert from uS to mS.
highByte(currentStatus.PW2)
//Pulsewidth 2 multiplied by 10 in ms. Have to convert from uS to mS.
lowByte(currentStatus.PW3)
//Pulsewidth 3 multiplied by 10 in ms. Have to convert from uS to mS.
highByte(currentStatus.PW3)
//Pulsewidth 3 multiplied by 10 in ms. Have to convert from uS to mS.
lowByte(currentStatus.PW4)
//Pulsewidth 4 multiplied by 10 in ms. Have to convert from uS to mS.
highByte(currentStatus.PW4)
//Pulsewidth 4 multiplied by 10 in ms. Have to convert from uS to mS.
currentStatus.status3
currentStatus.engineProtectStatus
lowByte(currentStatus.fuelLoad)
highByte(currentStatus.fuelLoad)
lowByte(currentStatus.ignLoad)
highByte(currentStatus.ignLoad)
lowByte(currentStatus.dwell)
highByte(currentStatus.dwell)
currentStatus.CLIdleTarget
currentStatus.mapDOT
lowByte(currentStatus.vvt1Angle)
//2 bytes for vvt1Angle
highByte(currentStatus.vvt1Angle)
currentStatus.vvt1TargetAngle
(byte)(currentStatus.vvt1Duty)
lowByte(currentStatus.flexBoostCorrection)
highByte(currentStatus.flexBoostCorrection)
currentStatus.baroCorrection
currentStatus.VE
//Current VE (%). Can be equal to VE1 or VE2 or a calculated value from both of them
currentStatus.ASEValue
//Current ASE (%)
lowByte(currentStatus.vss)
highByte(currentStatus.vss)
currentStatus.gear
currentStatus.fuelPressure
currentStatus.oilPressure
currentStatus.wmiPW
currentStatus.status4
lowByte(currentStatus.vvt2Angle)
highByte(currentStatus.vvt2Angle)
currentStatus.vvt2TargetAngle
(byte)(currentStatus.vvt2Duty)
currentStatus.outputsStatus
(byte)(currentStatus.fuelTemp + CALIBRATION_TEMPERATURE_OFFSET)
//Fuel temperature from flex sensor
currentStatus.fuelTempCorrection
//Fuel temperature Correction (%)
currentStatus.advance1
//advance 1 (%)
currentStatus.advance2
//advance 2 (%)
currentStatus.TS_SD_Status
//SD card status
lowByte(currentStatus.EMAP)
highByte(currentStatus.EMAP)
New EEPROM burn command to only burn a single page at a time
The Format to send is
'b' , '0' , '*'
Where * is the config page number
Speeduino response
(none)
This Burns the current configuration from RAM into EEPROM/non-volatile storage.
The Format to send is
'B'
Speeduino response
(none)
Send the current loops/sec value
The Format to send is
'c'
Speeduino response
lowByte(currentStatus.loopsPerSecond) , highByte(currentStatus.loopsPerSecond)
Test communications. This is used by Tunerstudio to see whether there is an ECU on a given serial port
The Format to send is
'B'
Speeduino response
Send a CRC32 hash of a given page
The Format to send is
'd' , '0' , '*'
where * is the value to calc the hash of.
The response is 3 bytes calculated as follows.
CRC32_val = calculateCRC32( * )
((CRC32_val >> 24) & 255) )
byte 1 = ( ((CRC32_val >> 16) & 255) )
byte 2 = ( ((CRC32_val >> 8) & 255) )
byte 3 = ( (CRC32_val & 255) )
Speeduino response
byte 1 , byte 2 , byte 3
Command button commands.
Commands are built as cmdCombined = word(cmdGroup, cmdValue).
this is the current(29/07/2021) list of valid cmdCombined command values.
TS_CMD_TEST_DSBL 256
TS_CMD_TEST_ENBL 257
TS_CMD_INJ1_ON 513
TS_CMD_INJ1_OFF 514
TS_CMD_INJ1_50PC 515
TS_CMD_INJ2_ON 516
TS_CMD_INJ2_OFF 517
TS_CMD_INJ2_50PC 518
TS_CMD_INJ3_ON 519
TS_CMD_INJ3_OFF 520
TS_CMD_INJ3_50PC 521
TS_CMD_INJ4_ON 522
TS_CMD_INJ4_OFF 523
TS_CMD_INJ4_50PC 524
TS_CMD_INJ5_ON 525
TS_CMD_INJ5_OFF 526
TS_CMD_INJ5_50PC 527
TS_CMD_INJ6_ON 528
TS_CMD_INJ6_OFF 529
TS_CMD_INJ6_50PC 530
TS_CMD_INJ7_ON 531
TS_CMD_INJ7_OFF 532
TS_CMD_INJ7_50PC 533
TS_CMD_INJ8_ON 534
TS_CMD_INJ8_OFF 535
TS_CMD_INJ8_50PC 536
TS_CMD_IGN1_ON 769
TS_CMD_IGN1_OFF 770
TS_CMD_IGN1_50PC 771
TS_CMD_IGN2_ON 772
TS_CMD_IGN2_OFF 773
TS_CMD_IGN2_50PC 774
TS_CMD_IGN3_ON 775
TS_CMD_IGN3_OFF 776
TS_CMD_IGN3_50PC 777
TS_CMD_IGN4_ON 778
TS_CMD_IGN4_OFF 779
TS_CMD_IGN4_50PC 780
TS_CMD_IGN5_ON 781
TS_CMD_IGN5_OFF 782
TS_CMD_IGN5_50PC 783
TS_CMD_IGN6_ON 784
TS_CMD_IGN6_OFF 785
TS_CMD_IGN6_50PC 786
TS_CMD_IGN7_ON 787
TS_CMD_IGN7_OFF 788
TS_CMD_IGN7_50PC 789
TS_CMD_IGN8_ON 790
TS_CMD_IGN8_OFF 791
TS_CMD_IGN8_50PC 792
TS_CMD_STM32_REBOOT 12800
TS_CMD_STM32_BOOTLOADER 12801
TS_CMD_VSS_60KMH 39168 //0x99x00
TS_CMD_VSS_RATIO1 39169
TS_CMD_VSS_RATIO2 39170
TS_CMD_VSS_RATIO3 39171
TS_CMD_VSS_RATIO4 39172
TS_CMD_VSS_RATIO5 39173
TS_CMD_VSS_RATIO6 39174
The Format to send is
'E' , cmdGroup , cmdValue
eg for cmdtestspk1on send 'E' , '0x03' , '0x01'
Speeduino response
(none , hardware action only)
send serial protocol version
The Format to send is
'F'
Speeduino response
'0' , '0' , '1'
NOTE these values are sent in ASCII.
Stop the tooth logger
This reconnects the crank and cam input interrupts back to the normal input trigger code.
The Format to send is
'h'
Speeduino response
(none)
Start the tooth logger
This disconnects the crank and cam input interrupts from their normal input trigger code and routes them to the tooth logger code.An acknowledge reply is made by speeduino.
The Format to send is
'H'
Speeduino response
'1'
Stop the composite logger
This reconnects the crank and cam input interrupts back to the normal input trigger code.
The Format to send is
'j'
Speeduino response
Start the composite logger
This disconnects the crank and cam input interrupts from their normal input trigger code and routes them to the composite logger code.An acknowledge reply is made by speeduino.
The Format to send is
'J'
Speeduino response
'1'
List the contents of current page in human readable form
You must set the current page prior to issuing this command to set the required page to be generated.
the data structure is as follows.
currentPage veMapPage:
Serial.println(F("\nVE Map"));
serial_print_3dtable(fuelTable);
currentPage veSetPage:
Serial.println(F("\nPg 2 Cfg"));
// The following loop displays in human readable form of all byte values in config page 1 up to but not including the first array.
serial_println_range((byte )&configPage2, configPage2.wueValues);
serial_print_space_delimited_array(configPage2.wueValues);
// This displays all the byte values between the last array up to but not including the first unsigned int on config page 1
serial_println_range(_end_range_byte_address(configPage2.wueValues), (byte)&configPage2.injAng);
// The following loop displays four unsigned ints
serial_println_range(configPage2.injAng, configPage2.injAng + _countof(configPage2.injAng));
// Following loop displays byte values between the unsigned ints
serial_println_range(_end_range_byte_address(configPage2.injAng), (byte*)&configPage2.mapMax);
Serial.println(configPage2.mapMax);
// Following loop displays remaining byte values of the page
serial_println_range(&configPage2.fpPrime, (byte *)&configPage2 + sizeof(configPage2));
break;
currentPage ignMapPage:
Serial.println(F("\nIgnition Map"));
serial_print_3dtable(ignitionTable);
currentPage ignSetPage:
Serial.println(F("\nPg 4 Cfg"));
Serial.println(configPage4.triggerAngle);// configPage4.triggerAngle is an int so just display it without complication
// Following loop displays byte values after that first int up to but not including the first array in config page 2
serial_println_range((byte*)&configPage4.FixAng, configPage4.taeBins);
serial_print_space_delimited_array(configPage4.taeBins);
serial_print_space_delimited_array(configPage4.taeValues);
serial_print_space_delimited_array(configPage4.wueBins);
Serial.println(configPage4.dwellLimit);// Little lonely byte stuck between two arrays. No complications just display it.
serial_print_space_delimited_array(configPage4.dwellCorrectionValues);
serial_println_range(_end_range_byte_address(configPage4.dwellCorrectionValues), (byte *)&configPage4 + sizeof(configPage4));
currentPage afrMapPage:
Serial.println(F("\nAFR Map"));
serial_print_3dtable(afrTable);
break;
currentPage afrSetPage:
Serial.println(F("\nPg 6 Config"));
serial_println_range((byte *)&configPage6, configPage6.voltageCorrectionBins);
serial_print_space_delimited_array(configPage6.voltageCorrectionBins);
serial_print_space_delimited_array(configPage6.injVoltageCorrectionValues);
serial_print_space_delimited_array(configPage6.airDenBins);
serial_print_space_delimited_array(configPage6.airDenRates);
serial_println_range(_end_range_byte_address(configPage6.airDenRates), configPage6.iacCLValues);
serial_print_space_delimited_array(configPage6.iacCLValues);
serial_print_space_delimited_array(configPage6.iacOLStepVal);
serial_print_space_delimited_array(configPage6.iacOLPWMVal);
serial_print_space_delimited_array(configPage6.iacBins);
serial_print_space_delimited_array(configPage6.iacCrankSteps);
serial_print_space_delimited_array(configPage6.iacCrankDuty);
serial_print_space_delimited_array(configPage6.iacCrankBins);
// Following loop is for remaining byte value of page
serial_println_range(_end_range_byte_address(configPage6.iacCrankBins), (byte *)&configPage6 + sizeof(configPage6));
break;
currentPage boostvvtPage:
Serial.println(F("\nBoost Map"));
serial_print_3dtable(boostTable);
Serial.println(F("\nVVT Map"));
serial_print_3dtable(vvtTable);
break;
currentPage seqFuelPage:
Serial.println(F("\nTrim 1 Table"));
serial_print_3dtable(trim1Table);
break;
currentPage canbusPage:
Serial.println(F("\nPage 9 Cfg"));
serial_println_range((byte *)&configPage9, (byte *)&configPage9 + sizeof(configPage9));
break;
currentPage fuelMap2Page:
Serial.println(F("\n2nd Fuel Map"));
serial_print_3dtable(fuelTable2);
break;
currentPage ignMap2Page:
Serial.println(F("\n2nd Ignition Map"));
serial_print_3dtable(ignitionTable2);
break;
currentPage warmupPage:
N/A
currentPage progOutsPage:
N/A
Send the current free memory
The Format to send is
'm'
Speeduino response
'lowByte(currentStatus.freeRAM)' , 'highByte(currentStatus.freeRAM)'
Displays a new line. Like pushing enter in a text editor
The Format to send is
'N'
Speeduino response
' '
NOTE this is sent as plain text NOT ASCII
Sets the current Page.This is the new foramt used by TunerStudio.
6 bytes are required:
2 byte - Page identifier
2 byte - offset
2 byte - Length
Sets the current page. This is a legacy function and is no longer used by TunerStudio. It is maintained for compatibility with other systems.
The Format to send is
'P' , '*'
Where * is the Page number to be selected. this MUST be sent in ASCII format
Speeduino response
(none)
Send the code version. The response is a 20 byte long ASCII converted string
The Format to send is
'Q'
Speeduino response
'speeduino 202104-dev'
Above is an example reply, the actual reply will depend on what firmware is installed.
This command has multiple functions, It requests specific data.This data may be realtime values or from RTC or SD card.
send the code version. The response is a 20 byte long ASCII converted string
The Format to send is
'S'
Speeduino response
'Speeduino 2021.04-dev'
Above is an example reply, the actual reply will depend on what firmware is installed.
receive new Calibration info.
Command structure: "t", tble_idx , data array.
Send 256 tooth log entries to Tuner Studios tooth logger
6 bytes required:
2 - Page identifier
2 - offset
2 - Length
User wants to reset the Arduino (probably for FW update)
send VE table and constants in binary
receive new VE or constant
'W' , offset , newbyte
Send the 256 tooth log entries to a terminal emulator
This will send out a human text readable string with details of the command characters and their functions.
The Format to send is
'?'
Speeduino response
===Command Help===
All commands are single character and are concatenated with their parameters without spaces.
Syntax: command , parameter1 , parameter2 , parameterN
===List of Commands===
A - Displays 31 bytes of currentStatus values in binary (live data)
B - Burn current map and configPage values to eeprom
C - Test COM port. Used by Tunerstudio to see whether an ECU is on a given serial port. Returns a binary number.
N - Print new line.
P - Set current page. Syntax: P , pageNumber
R - Same as A command
S - Display signature number
Q - Same as S command
V - Display map or configPage values in binary
W - Set one byte in map or configPage. Expects binary parameters.
Syntax: W+<offset , newbyte
t - Set calibration values. Expects binary parameters. Table index is either 0, 1, or 2.
Syntax: t+ , tble_idx , newValue1 , newValue2 , newValueN
Z - Display calibration values
T - Displays 256 tooth log entries in binary
r - Displays 256 tooth log entries
U - Prepare for firmware update. The next byte received will cause the Arduino to reset.
? - Displays this help page
The Secondary Serial interface enables an external device to access data from Speeduino or to expand the io of the Speeduino ECU.
A full explanation of the features and operation of secondary serial can be found here. Secondary_Serial_IO_interface
Canbus is only available directly on Teensy and STM32 MCU based Speeduino. Mega2560 based units need additional hardware such as DxControl GPIO .
A full explanation of the features and operation of secondary serial can be found here.
Canbus_Support