Turnigy ESC Programming Card Reverse Engineered

I replicated the functionality of a Turnigy ESC programming card. These programming cards are meant to configure electronic speed controllers (ESC). I always wanted to know how they work. Eventually I purchased one since I need one for my quadrotor helicopter’s ESC, and then I started playing with it.

It should be very simple to adapt the code to any microcontroller.

Most of the analysis was done using a logic analyzer. The communication protocol was determined to be a half-duplex protocol that used one wire. Bytes being transmitted looked very similar to a 10-bit UART bus. There were some acknowledgements and stuff that I was able to identify. In the end, I was able to come up with my own code after figuring out the patterns I found by comparing the traffic caused by different configurations.

Since the original card itself is only about $7, and a microcontroller costs around $5, it’s not actually economical for anybody to make one of these at home. But my code might be useful if you integrate it into another project where you want to load one fixed set of configuration settings, or automate the process for multiple ESCs.

This is the proof of concept code (compiled for a ATmega328P running at 12 MHz):

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
 
#define TPC_PIN_INPUT() DDRD &= ~_BV(6)
#define TPC_PIN_OUTPUT() DDRD |= _BV(6)
#define TPC_PIN_ON() PORTD |= _BV(6)
#define TPC_PIN_OFF() PORTD &= ~_BV(6)
#define TPC_PIN_IS_ON() bit_is_set(PIND, 6)
#define TPC_PIN_IS_OFF() bit_is_clear(PIND, 6)
#define TPC_delay_us(x) _delay_us(x)
#define TPC_READ_BIT_TIME_WIDTH 2437
#define TPC_READ_HALF_BIT_TIME_WIDTH (TPC_READ_BIT_TIME_WIDTH/2)
#define TPC_WRITE_BIT_TIME_WIDTH TPC_READ_BIT_TIME_WIDTH
#define TPC_WRITE_HALF_BIT_TIME_WIDTH (TPC_WRITE_BIT_TIME_WIDTH/2)
 
enum TPC_battType_t
{
    TPC_battType_Li = 0,
    TPC_battType_Ni = 1
};
 
enum TPC_cutoffType_t
{
    TPC_cutoffType_softCut = 0,
    TPC_cutoffType_cutoff = 1
};
 
enum TPC_cutoffVoltage_t
{
    TPC_cutoffVoltage_low = 0,
    TPC_cutoffVoltage_middle = 1,
    TPC_cutoffVoltage_high = 2
};
 
enum TPC_startMode_t
{
    TPC_startMode_normal = 0,
    TPC_startMode_soft = 1,
    TPC_startMode_verySoft = 2
};
 
enum TPC_timingMode_t
{
    TPC_timingMode_low = 0,
    TPC_timingMode_middle = 1,
    TPC_timingMode_high = 2
};
 
enum TPC_lipoCells_t
{
    TPC_lipoCells_autoDetect = 0,
    TPC_lipoCells_2 = 1,
    TPC_lipoCells_3 = 2,
    TPC_lipoCells_4 = 3,
    TPC_lipoCells_5 = 4,
    TPC_lipoCells_6 = 5,
    TPC_lipoCells_7 = 6,
    TPC_lipoCells_8 = 7,
    TPC_lipoCells_9 = 8,
    TPC_lipoCells_10 = 9,
    TPC_lipoCells_11 = 10,
    TPC_lipoCells_12 = 11
};
 
typedef struct TPC_settings_t_
{
    char brake;
    enum TPC_battType_t battType;
    enum TPC_cutoffType_t cutoffType;
    enum TPC_cutoffVoltage_t cutoffVoltage;
    enum TPC_startMode_t startMode;
    enum TPC_timingMode_t timingMode;
    enum TPC_lipoCells_t lipoCells;
    char governorMode;
     
} TPC_settings_t;
 
// initializes the settings to default values
void TPC_loadDefault(TPC_settings_t* x)
{
    x->brake = 0;
    x->battType = TPC_battType_Li;
    x->cutoffType = TPC_cutoffType_softCut;
    x->cutoffVoltage = TPC_cutoffVoltage_middle;
    x->startMode = TPC_startMode_normal;
    x->timingMode = TPC_timingMode_low;
    x->lipoCells = TPC_lipoCells_autoDetect;
    x->governorMode = 0;
}
 
// configs the settings struct with 2 bytes (which you should get from the ESC, but you can also store it in your own EEPROM or something)
void TPC_word_to_struct(TPC_settings_t* x, unsigned short y)
{
    // map the bits to the settings
    x->brake = (y & (1 << 0)) == 0 ? 0 : 1;
    x->battType = (y & (1 << 1)) == 0 ? TPC_battType_Li : TPC_battType_Ni;
    x->cutoffType = (y & (1 << 2)) == 0 ? TPC_cutoffType_softCut : TPC_cutoffType_cutoff;
    x->cutoffVoltage = ((y & (0x03 << 3)) >> 3);
    x->startMode = ((y & (0x03 << 5)) >> 5);
    x->timingMode = ((y & (0x03 << 8)) >> 8);
    x->lipoCells = ((y & (0x0F << 10)) >> 10);
    x->governorMode = (y & (1 << 7)) == 0 ? 0 : 1;
}
 
// translates the settings struct into 2 bytes (which you can send to the ESC, or store in your own EEPROM or something)
unsigned short TPC_struct_to_word(TPC_settings_t* x)
{
    return 0 | 
           ((x->brake ? 1 : 0) << 0) |
           ((x->battType ? 1 : 0) << 1) |
           ((x->cutoffType ? 1 : 0) << 2) |
           ((x->cutoffVoltage & 0x03) << 3) |
           ((x->startMode & 0x03) << 5) |
           ((x->timingMode & 0x03) << 8) |
           ((x->lipoCells & 0x0F) << 10) |
           ((x->governorMode ? 1 : 0) << 7);
}
 
// reads a byte from a psuedo 10-bit UART
unsigned char TPC_ser_read()
{
    unsigned char i, x = 0;
     
    TPC_PIN_ON(); // input
    TPC_PIN_INPUT();
     
    while (TPC_PIN_IS_OFF()); // wait for powerup if not already
    while (TPC_PIN_IS_ON()); // wait until start of frame
    while (TPC_PIN_IS_OFF()); // this period indicates start of frame
    while (TPC_PIN_IS_ON()); // the first bit always seems to be 1
     
    TPC_delay_us(TPC_READ_BIT_TIME_WIDTH + TPC_READ_HALF_BIT_TIME_WIDTH); // skip
     
    // read the 8 bits LSB first
    for (i = 0; i < 8; i++)
    {
        x |= (TPC_PIN_IS_ON() ? 1 : 0) << i;
        TPC_delay_us(TPC_READ_BIT_TIME_WIDTH);
    }
     
    return x;
}
 
// writes a byte to a psuedo 10-bit UART
void TPC_ser_write(unsigned char x)
{   
    TPC_PIN_ON(); // make sure
    TPC_PIN_OUTPUT();
    TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
     
    TPC_PIN_OFF(); // signal start
    TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
     
    TPC_PIN_ON(); // first bit always 1
    TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
     
    TPC_PIN_OFF(); // 2nd bit always 0
    TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
     
    // send the byte LSB first
    char i;
    for (i = 0; i < 8; i++)
    {
        if ((x & (1 << i)) == 0)
        {
            TPC_PIN_OFF();
        }
        else
        {
            TPC_PIN_ON();
        }
        TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
    }
    TPC_PIN_ON(); // leave as input
    TPC_PIN_INPUT();
}
 
// must be sent after receiving configuration from ESC upon initialization
void TPC_send_init_ack()
{
    TPC_PIN_ON();
    TPC_PIN_OUTPUT();
    TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
     
    // send pulses
    char i;
    for (i = 0; i < 6; i++)
    {
        TPC_PIN_OFF();
        TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
        TPC_PIN_ON();
        TPC_delay_us(TPC_WRITE_BIT_TIME_WIDTH);
    }
     
    TPC_PIN_INPUT(); // leave clean
}
 
// receive the ack from ESC after writing config to ESC
void TPC_wait_for_ack()
{
    TPC_ser_read();
}
 
// receive current config from ESC
void TPC_read_init(TPC_settings_t* x)
{
    // read in 2 bytes
    unsigned short y;
    y = TPC_ser_read();
    y |= TPC_ser_read() << 8;
     
    TPC_word_to_struct(x, y);
     
    TPC_delay_us(TPC_READ_BIT_TIME_WIDTH); // a small delay
    TPC_delay_us(TPC_READ_BIT_TIME_WIDTH); // a small delay
    TPC_delay_us(TPC_READ_BIT_TIME_WIDTH); // a small delay
    TPC_delay_us(TPC_READ_BIT_TIME_WIDTH); // a small delay
     
    TPC_send_init_ack(); // must be sent after receiving configuration from ESC upon initialization
}
 
// sends configuration to ESC
void TPC_send_config(TPC_settings_t* x)
{
    unsigned short y = TPC_struct_to_word(x);
     
    // start writing the config, LSB first
    TPC_ser_write(y & 0xFF);
    TPC_ser_write((y >> 8) & 0xFF);
    // just a small note, these two bytes match the received config during initialization
     
    // the settings are sent in this format
    TPC_ser_write((y & (0x01 << 0)) >> 0);
    TPC_ser_write((y & (0x01 << 1)) >> 1);
    TPC_ser_write((y & (0x01 << 2)) >> 2);
    TPC_ser_write((y & (0x03 << 3)) >> 3);
    TPC_ser_write((y & (0x03 << 5)) >> 5);
    TPC_ser_write((y & (0x03 << 8)) >> 8);
    TPC_ser_write((y & (0x01 << 7)) >> 7);
     
    // this is where the string of notes would be, but I don't have that implemented, so these two are just null
    TPC_ser_write(0); 
    TPC_ser_write(0);
     
    TPC_ser_write(11); // this is actually a byte count
     
    TPC_wait_for_ack(); // do not unpower ESCs until the ack has been received, since it's writing to EEPROM during this time
}
 
static int ser_tx(char c, FILE* f)
{
    loop_until_bit_is_set(UCSR0A, UDRE0);
    UDR0 = c;
    return 0;
}
 
static FILE mystdout = FDEV_SETUP_STREAM(ser_tx, NULL, _FDEV_SETUP_WRITE);
 
int main()
{
    // setup for 57600 baud
    UBRR0H = 0;
    UBRR0L = 12;
     
    UCSR0B = _BV(TXEN0) | _BV(RXEN0); // start serial port
    stdout = &mystdout; // setup stream
     
    DDRD |= _BV(4); // LED pin output
     
    TPC_PIN_ON();
    TPC_PIN_INPUT();
     
    printf("\r\nTesting Begin\r\n");
    // at this point, plug in the ESC to the battery
     
    static volatile TPC_settings_t mySettings;
     
    PORTD |= _BV(4); // LED on
    TPC_read_init(&mySettings);
    PORTD &= ~_BV(4); // LED off
     
    unsigned short x = TPC_struct_to_word(&mySettings);
    printf("Read from ESC: 0x%x\r\n", x);
     
    // here we change all the settings
    mySettings.brake = mySettings.brake ? 0 : 1;
    mySettings.battType = mySettings.battType ? 0 : 1;
    mySettings.cutoffType = mySettings.cutoffType ? 0 : 1;
    mySettings.cutoffVoltage = (mySettings.cutoffVoltage + 1) % 3;
    mySettings.startMode = (mySettings.startMode + 1) % 3;
    mySettings.timingMode = (mySettings.timingMode + 1) % 3;
    mySettings.lipoCells = (mySettings.lipoCells + 1) % (TPC_lipoCells_12 + 1);
    mySettings.governorMode = mySettings.governorMode ? 0 : 1;  
    x = TPC_struct_to_word(&mySettings);
    printf("Sending to ESC: 0x%x\r\n", x);
    // and send it back
     
    _delay_ms(1000);
     
    PORTD |= _BV(4); // LED on
    TPC_send_config(&mySettings);
    PORTD &= ~_BV(4); // LED off
     
    printf("Test Complete\r\n");
     
    // now the ESC must be unpowered
    // then reset the system
    // to confirm settings
     
    while (1);
    return 0;
}

22 thoughts on “Turnigy ESC Programming Card Reverse Engineered

  1. niraj

    I have prog card and i configured my turnigy plush 30a esc but when i am connecting it to arduino uno and power on the esc is beeping only.as in manual given this beep is for throttle value but i dont know how to give this?
    Code uploade in arduino is example-servo-knob
    Please help

    Reply
    1. EcoRick

      the example servo-knob is minum 0 degree to 180 degree maximum. but ESC looking for PWM of 1000 microsecond to 2000 microsecond so you have to edit the example code with servo.writeMicroseconds instead of servo.write is only write degree from 0 to 180 degrees, I don’t know conversion from degree to microsecond (1000microsecond). Most RC servos operating 1000microsecond to 2000microsecond which is only span of 60degree.

      Reply
  2. sputnik1892

    Great work! I used the code for my Arduino MiniPro board (16 MHz) and applied it on 4 different Hobbywing-Pentium30A ESCs. All I had to change was:
    – BaudRate Registers (UBRR0H / UBRR0L )
    -pin registers for ESC and LED
    -TPC_READ_BIT_TIME_WIDTH: this neede to be adapted for every ESC varying in my case from 2450 to 2750.

    Reply
  3. Duane Degn

    I notice the original setting read back from the card is zero. Have you successfully read back your modified settings?
    I just don’t like to trust zero data values nor do I trust values with 0xFFFF.
    Thank you for taking the time to share your work.

    Reply
    1. Admin Post author

      I’ve always had success reading back the values that are set by my code. It’s one of the basic tests to make sure the code actually works.

      Reply
  4. Terry

    I had to configure some ESCs (again). The thought of using the sticks to configure them again was too painful, so I was about to order the Programming Card from Hobby King. It was quicker to use your code ported for the Arduino over at rcgroups.com, than to deal with HK. It took 40 minutes to setup and find the correct timing. It took 2 minutes to configure 4 ESCs.

    TPC_READ_BIT_TIME_WIDTH from 2437 to 2000 for Turnigy Plush 30A ESCs.

    Excellent work. Thank you.

    Reply
  5. Duane Degn

    I ported this over to the Propeller. I found ESCs’ timing had a lot of variation. I added an “auto baud detect” of sorts so the Propeller’s bit intervals matched the ESC’s intervals.
    There are two versions of the Propeller code. One which automatically configures the ESC when first powered on and another version with a menu which one interacts with through a terminal window.
    http://forums.parallax.com/showthread.php/154854-Open-Propeller-Project-4-Program-our-ESCs-with-a-Propeller?p=1283727&viewfull=1#post1283727
    Hopefully the code linked to above will be useful others who use the Propeller microcontroller.
    Thank you Frank Zhao for sharing your work.

    Reply
  6. wayne

    Hi I was wondering if there is a way to change the startup music selection available to write to the esc. the selection of tunes offered on the turnigy programming card sucks and im getting pretty tired of “oh suzanna” any help or direction on where i can find the answer would be much appreciated

    Reply
  7. Gabry

    When i try compile on Arduino Nano 1.63 Ide i get this errors:
    101: error: invalid conversion from ‘unsigned int’ to ‘TPC_cutoffVoltage_t’ [-fpermissive]

    x->cutoffVoltage = ((y & (0x03 <> 3);

    102: error: invalid conversion from ‘unsigned int’ to ‘TPC_startMode_t’ [-fpermissive]

    x->startMode = ((y & (0x03 <> 5);

    and more….

    Reply
    1. Gabry

      I have solved in this way, with a VB.net program and arduino…is a different approach based on beep:

      s://www.youtube.com/watch?v=hyIqT0kAdVohttp

      Reply
  8. Thomas

    Hi Frank,
    very nice work! But I have some problems. I try to program my HobbyWing EZRun 60A-SL Brushless ESC with the ported arduino code from here: https://github.com/chatelao/esc_reader but it won’t work. I tried different values for TPC_READ_BIT_TIME_WIDTH form 2000 up to 2750. Any suggestions?

    Reply
    1. Admin Post author

      How do you know that HobbyWing uses a protocol that Turnigy uses? It is likely they do not use the same protocol at all.

      Reply
      1. Thomas

        I thought it because many ESC come from HobbyKing or Turnigy are only labled. So my thought was it’s the same. Sorry if I’m not right. In t he source also was written “// Sets up Turngy Plush/ Hobbywing ESCs”.

        Reply
        1. Admin Post author

          it is very likely that a 60A ESC requires a different firmware than a 20A ESC. So perhaps a 20A Turnigy ESC is the same as a 20A HobbyWing ESC, but the firmware for the 60A HobbyWing ESC might be different.

          Reply
    2. povlhp

      Well, it needs a different programming box (I ordered one).
      Not sure if I will have time to capture comms. Personally, I think they should just release specs.

      Reply
    1. Duane Degn

      NgoEC,
      Yes, the signal is on the servo wire. I found some ESCs required a pull-up on the signal line in order to monitor the signal from the ESC.

      Reply
  9. Matthias

    Just as info for the Turnigy program box “For 80A/120A Turbo ESC”
    This box uses a totally simple standard 8-bit USART protocol with 19200 baud. I tried to reverse engineering that thing, but I only can hack ESC’s that I own (call/response sniffing) like the Trackstar Turbo80A. For more infos and protocols and arduino sketches, please drop an e-mail.
    BTW: The MCU is an ATmega 168P

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *