Reverse engineering the MQB Electric steering rack

Forum on Volkswagen related hardware, so VW, Audi, Seat, Skoda etc.
Post Reply
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

I'm looking for a bit of direction in trying to hack this MQB rack from a VW Transporter 6.1.
It works in fail safe but it'd be nicer if it ran with the speed mapping to the level of assist.
I've got some Canlogs from a MK7 Golf (I think) and played them back. I've identified the differences between the two, and there seems to be only one CAN ID required to wake up the rack... 0x0FD ESP_21

When powered up the unit sends out comms on...
086 - 4th and 5th bits change with torque input on the rack, whilt playing the CAN log back at the rack these bits change moreso, 4th bit indicates direction of turn (9F turned CCW and DF/5F when turned CW)
09F - 6th bit changes with torque applied to the rack, I need to map this out but this appears to be the angle of the steering wheel
7th bit changes with direction also (00 for CW and 80 for CCW)
5FC - is a static message when in failsafe mode (no playback, with playback it responds), I need to look into this further but changes occur on the 1st, 2nd, 3rd and 4th bits
32A - need to check this one more but no changes obseved
6C0 - changes at 7th bit from 19 to 11 when having the log played back.

Any pointers on how to narrow things down further?

0FD in the CAN log appears to contain some speed information but I'm sure someone with more experience will be able to shed some some light.

I've cut the CAN log down into sections where it appears there is no driving and parts with driving, as expected, playing back with driving provides more assistance than playing back parts with driving.
Attachments
Driving Log1 7-11-23-shortened with drive.csv
(20.34 MiB) Downloaded 438 times
Driving Log1 7-11-23-shortened no drive from 4286812-262989021.csv
(22.73 MiB) Downloaded 472 times
086-with drive.csv
(228.33 KiB) Downloaded 463 times
086-no drive.csv
(246.52 KiB) Downloaded 463 times
32a-with drive.csv
(66.75 KiB) Downloaded 470 times
32a-no drive.csv
(57.15 KiB) Downloaded 467 times
11d-with drive.csv
(91.4 KiB) Downloaded 455 times
11d-no drive.csv
(115.72 KiB) Downloaded 445 times
09f-with drive.csv
(171.83 KiB) Downloaded 451 times
09f-no drive.csv
(158.46 KiB) Downloaded 462 times
5fc and 6c0-with drive.csv
(79.06 KiB) Downloaded 458 times
5fc and 6c0-no drive.csv
(58.46 KiB) Downloaded 437 times
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

So I've had a little more time to mess with this.
I'm flying by the seat of my pants as I go here so may be making hard work of what you all would find pretty easy but playing the filtered CAN log back at the rack for 0FD EPS_21 I get assist and speed values showing on ODIS.
I played a log whilst taking a bunch of photos and then laid it out in a graph, removing outlier values of assist (likelihood due to extra torque from steering) and the averageing the results I've got an idea of the levels of assist at different speeds...
image.png
Following on from this, looking at EPS_21 I can see byte 2 starts at 211 and counts up to 223, so I guess that's a counter then that can be seen in green on this flow graph and the DBC ..
image.png

Code: Select all

BO_ 253 ESP_21: 8 Gateway_MQB
 [b]SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] ""  XXX
 SG_ COUNTER : 8|4@1+ (1,0) [0|15] ""  XXX[/b]
 SG_ BR_Eingriffsmoment : 12|10@1+ (1,-509) [-509|509] ""  XXX
 SG_ ESP_PLA_Bremseingriff : 22|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_Diagnose : 23|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESC_Reku_Freigabe : 24|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESC_v_Signal_Qualifier_High_Low : 25|3@1+ (1.0,0.0) [0.0|7] ""  XXX
 SG_ ESP_Vorsteuerung : 28|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_AWV3_Brems_aktiv : 29|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ OBD_Schlechtweg : 30|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ OBD_QBit_Schlechtweg : 31|1@1+ (1.0,0.0) [0.0|1] ""  XXX
[b] SG_ ESP_v_Signal : 32|16@1+ (0.01,0) [0.00|655.32] "Unit_KiloMeterPerHour"  XXX[/b]
 SG_ ASR_Tastung_passiv : 48|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_Tastung_passiv : 49|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_Systemstatus : 50|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ASR_Schalteingriff : 51|2@1+ (1.0,0.0) [0.0|3] ""  XXX
 SG_ ESP_Haltebestaetigung : 53|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_MKB_Abbruch_Geschw : 54|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_QBit_v_Signal : 55|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ABS_Bremsung : 56|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ASR_Anf : 57|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ MSR_Anf : 58|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ EBV_Eingriff : 59|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ EDS_Eingriff : 60|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_Eingriff : 61|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_ASP : 62|1@1+ (1.0,0.0) [0.0|1] ""  XXX
 SG_ ESP_Anhaltevorgang_ACC_aktiv : 63|1@1+ (1.0,0.0) [0.0|1] ""  XXX
The ESP V Signal is noted in "KiloMeterPerHour" so thats likely my speed, starts at byte 5 (bit 32) and is 16 bits long if I am interpreting the DBC correctly...
But I'm uncertain how the data is then cofigured to the speed shown below..
image.png
from the raw data flow graph...
image.png
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

OK, so the vehicle speed is made up from byte 5 and byte 6 and is calculated by (b5 + b6*253)*0.01.
image.png
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

So I made a little progress last night after not having done anything on it in a while...
I asked Gemini (googles AI) to help with the checksum for the CAN ID 0X0FD EPS_21...
It managed pretty quickly to crack it and comparing it to the CAN Log I've got appears to be correct which is good news. I've also got it to generate some code (below), but I need to check it over... whilst learning C++ 😄....

Then I'm hoping to merge it into BigPies Canfilter code so I can get it to translate from my VW transporter CAN data to the newer MQB format to satisfy the rack.

I'll bench test it all first but given the rack goes to failsafe mode if it receives invalid data or looses connection I'm confident it should work OK...

Code: Select all

#include <stdint.h>

uint8_t calculateCRC8(const uint8_t *data, size_t length) {
    uint8_t crc = 0x00;
    uint8_t polynomial = 0x07;

    for (size_t i = 0; i < length; i++) {
        uint8_t dataByte = data[i];
        for (uint8_t j = 0; j < 8; j++) {
            uint8_t msb = (crc & 0x80) ? 1 : 0; // Check if MSB of CRC is 1
            crc <<= 1;                           // Shift CRC left by one bit
            if (msb ^ ((dataByte >> (7 - j)) & 0x01)) { // If MSB of CRC XOR current data bit is 1
                crc ^= polynomial;                 // XOR CRC with the polynomial
            }
            crc &= 0xFF; // Ensure CRC remains an 8-bit value
        }
    }
    return crc;
}

// Example usage:
void setup() {
    Serial.begin(115200);
    Serial.println("CRC-8 Calculation Example");

    // Example CAN data (excluding the checksum byte itself)
    uint8_t canData[] = {0xD4, 0x1F, 0x80, 0xB4, 0x29, 0x00, 0x00};
    size_t dataLength = sizeof(canData) / sizeof(canData[0]);

    uint8_t calculatedChecksum = calculateCRC8(canData, dataLength);

    Serial.print("Data: ");
    for (size_t i = 0; i < dataLength; i++) {
        Serial.print(canData[i], HEX);
        Serial.print(" ");
    }
    Serial.println();

    Serial.print("Calculated CRC-8 Checksum: 0x");
    Serial.println(calculatedChecksum, HEX);

    // For the 5th frame in your log, the expected checksum (D1) was 0xCB
    Serial.print("Expected Checksum (from log): 0xCB");
}

void loop() {
    // In your real-time application, you would read CAN data here
    delay(1000);
}
User avatar
uhi22
Posts: 1143
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 231 times
Been thanked: 638 times

Re: Reverse engineering the MQB Electric steering rack

Post by uhi22 »

The shown code does not include the alive counter (which should increment with each message), and also does not contain the table with magic bytes. So it cannot produce valid E2E protected messages.
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

Thanks for looking over it.

If I'm understanding you, the code only calculates the 1st bit. This is the checksum, the counter is a latter bit (I cannot remember which). I gave the AI some data with the first checksum bit removed and asked it calculate the checksum based on the provided data and the checksum bit matched. But I may be misunderstanding how this all works by the sound of it.
User avatar
uhi22
Posts: 1143
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 231 times
Been thanked: 638 times

Re: Reverse engineering the MQB Electric steering rack

Post by uhi22 »

The snippet of the DBC above shows that the checksum is at bit 0 and has 8 bits, and the counter starts at bit 8 (means: in the second byte), and has four bits.
The CRC algorithm may be similar to one of the profiles described in the AUTOSAR E2E specification. I described a similar method which Tesla uses, https://github.com/uhi22/tesla-crc

If we have a lot of trace data of an original car, it is possible to find out the missing pieces: the 16-byte table with magic bytes and the CRC polynom.
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

I've got 2 can logs from a VW Mk7 Golf, both are quite long so theres plenty of data there of thats what you mean by trace data.
User avatar
uhi22
Posts: 1143
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 231 times
Been thanked: 638 times

Re: Reverse engineering the MQB Electric steering rack

Post by uhi22 »

Ahh, I just see the attached logs above, I will have a look into them in the next days.
If you have longer logs you could extract the lines with the esp_21 message (eg using linux grep command) and provide the extract.
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

Yes I can do this
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

I've filtered the log files for 0x0FD only and attached, thanks again for the help
Attachments
Driving Log1 7-17-23-0fd.csv
(2.28 MiB) Downloaded 461 times
Driving Log1 7-11-23-0fd.csv
(2.69 MiB) Downloaded 444 times
User avatar
uhi22
Posts: 1143
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 231 times
Been thanked: 638 times

Re: Reverse engineering the MQB Electric steering rack

Post by uhi22 »

Good news: The CRC algorithm is the same as used in Tesla. Created a program which reads the above two log files, calculates the CRC for each message and compares it with the transmitted CRC. Result: All pass. CRC ok: 91622, CRC fail: 0
Github: https://github.com/uhi22/tesla-crc/tree/main/vag
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

That's great thanks so much for the help!
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

Right im revisiting this. So what would be the recommended method for transmitting the checksum in a live translation from my VW T5?
User avatar
tom91
Posts: 2753
Joined: Fri Mar 01, 2019 9:15 pm
Location: Bristol
Has thanked: 264 times
Been thanked: 717 times

Re: Reverse engineering the MQB Electric steering rack

Post by tom91 »

Having a controller/micro do the math.
Creator of SimpBMS
Founder Volt Influx https://www.voltinflux.com/
Webstore: https://citini.com/
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

ok, so it looks like I've got the Can filter setup and running with the help of BigPies/RStevens CanBridge board and.... :o ChatGPT/ClaudeAI :o ...
https://github.com/jamiejones85/ESP32VWMITM
just bench tested it but playing a canlog from my VW T5 back at the Can filter hardware and then listening back on the second can network whilst recording on SavvyCAN shows a close match between the acrtual vehicle speed and the recorded vehicle speed, with the translated speed in blue dashed and the actual speed in black dashed on the graph below. I need to check the checksum calc is correct and then I'll try playing it back at the rack itself and see if it responds as expected. It's importrant to note the points where the vehicle speed doesnt align is where the can interface crashed and I had to restart it.
image.png
I'll add in an alive CANID on one of the networks (listening of translated) and also maybe add some other safety checks but if the rack doesnt accept the data as valid it should resort to its failsafe mode of 40mph assistance.
Jacobsmess
Posts: 836
Joined: Thu Mar 02, 2023 1:30 pm
Location: Uk
Has thanked: 480 times
Been thanked: 137 times

Re: Reverse engineering the MQB Electric steering rack

Post by Jacobsmess »

ok, after a lot of faffing I've managed to get the code to generate the CRC correctly.
I dont have access to my van or Can logs of my van moving at the moment but this translator appears to work to translate VW speed signals transmitted on 0x288 to MQB ESP 21 0x0FD and correctly calculate the checksum. It should run on an ESP32 with a second can interface added. I'll verify tomorrow. If there are any sense checks and/or safety checks I should implement also I'd be interested to know.

Code: Select all

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// ===== VAG CRC8 (POLY = 0x2F, ESP21 magic XOR) =====
// Based on the reference vag-crc-filereader.c implementation
uint8_t vag_crc_esp21(const uint8_t *data, int len, uint8_t counter) {
    
    // Magic bytes for ESP21 (pre-XORed with 0xBA in the reference)
    static const uint8_t magicBytes[16] = {
        0xA3, 0xA9, 0x66, 0x13, 0x85, 0x1F, 0x0D, 0x37,
        0xF0, 0xC2, 0x8E, 0xA1, 0x38, 0xB1, 0x4E, 0xD9
    };
    
    // CRITICAL FIX: Initial value must be 0x00, not 0xFF!
    uint8_t crc = 0x00; 

    // Process the payload bytes
    for (int i = 0; i < len; ++i) { 
        crc ^= data[i];
        for (int b = 0; b < 8; ++b) {
            if (crc & 0x80)
                crc = (crc << 1) ^ 0x2F;
            else
                crc = crc << 1;
        }
    }

    // Apply the magic byte XOR based on the alive counter (lower nibble)
    return crc ^ magicBytes[counter & 0x0F];
}

// Build the payload array exactly as the reference code does
void build_payload(uint8_t *payload, const uint8_t *frame) {
    // payload[0] = high nibble of D2 (counter byte), keeping lower nibble zero
    payload[0] = frame[1] & 0xF0;
    // payload[1..6] = D3 through D8
    for (int i = 0; i < 6; i++) {
        payload[i + 1] = frame[i + 2];
    }
    // payload[7] = virtual 0x00 byte
    payload[7] = 0x00;
}

// Global variables for CSV parsing
double timestamp;
uint32_t id;
char extended_str[8];
char dir_str[4];
uint8_t bus, len;
uint8_t data[8];

// ===== Process one CSV line =====
void processLine(char *line, FILE *out, uint8_t *counter) {
    int assigned = sscanf(line, "%lf,%x,%7[^,],%3[^,],%hhu,%hhu,%hhx,%hhx,%hhx,%hhx,%hhx,%hhx,%hhx,%hhx",
               &timestamp, &id, extended_str, dir_str, &bus, &len, 
               &data[0], &data[1], &data[2], &data[3],
               &data[4], &data[5], &data[6], &data[7]);

    if (assigned != 14) {
        return; // Skip header or malformed lines
    }

    if (id == 0x288) {
        // Extract engine speed from D4 (byte index 3)
        uint8_t src_byte3 = data[3];
        float speed_kph = (float)src_byte3 * 1.28f;
        uint16_t speed_eps = (uint16_t)roundf(speed_kph * 100.0f);

        // Build 0x0FD frame
        uint8_t frame[8] = {0};
        
        // D2 (index 1) = Counter in lower nibble, high nibble can be data (set to 0xD for now)
        frame[1] = 0xD0 | (*counter & 0x0F);
        
        // D3-D8 payload bytes
        frame[2] = 0x1F;
        frame[3] = 0x82;
        frame[4] = (uint8_t)(speed_eps & 0xFF);         // Speed Low
        frame[5] = (uint8_t)((speed_eps >> 8) & 0xFF);  // Speed High
        frame[6] = 0x00;
        frame[7] = 0x00;
        
        // Build payload array exactly as reference code does
        uint8_t payload[8];
        build_payload(payload, frame);
        
        // Calculate CRC over the 8-byte payload (includes virtual 0x00)
        frame[0] = vag_crc_esp21(payload, 8, *counter);

        // Output in SavvyCAN format
        fprintf(out, "%.6f,000000FD,false,Tx,0,8,"
                "%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X\n",
                timestamp,
                frame[0], frame[1], frame[2], frame[3],
                frame[4], frame[5], frame[6], frame[7]);

        // Increment counter (0-15 wraparound)
        (*counter)++;
        if (*counter > 0x0F) *counter = 0x00;
    }
}

// ===== Verification function =====
void verify_log(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) {
        printf("Could not open verification file: %s\n", filename);
        return;
    }
    
    printf("\n=== Verifying CRC against reference log ===\n");
    
    char line[1024];
    int line_num = 0;
    int crc_ok = 0;
    int crc_fail = 0;
    
    // Skip header
    fgets(line, sizeof(line), fp);
    
    while (fgets(line, sizeof(line), fp)) {
        line_num++;
        
        double ts;
        uint32_t msg_id;
        uint8_t msg_data[8];
        
        int assigned = sscanf(line, "%lf,%x,%*[^,],%*[^,],%*hhu,%*hhu,%hhx,%hhx,%hhx,%hhx,%hhx,%hhx,%hhx,%hhx",
                   &ts, &msg_id,
                   &msg_data[0], &msg_data[1], &msg_data[2], &msg_data[3],
                   &msg_data[4], &msg_data[5], &msg_data[6], &msg_data[7]);
        
        if (assigned != 10 || msg_id != 0x0FD) continue;
        
        // Extract CRC and counter from message
        uint8_t crc_from_can = msg_data[0];
        uint8_t counter = msg_data[1] & 0x0F;
        
        // Build payload
        uint8_t payload[8];
        payload[0] = msg_data[1] & 0xF0;
        for (int i = 0; i < 6; i++) {
            payload[i + 1] = msg_data[i + 2];
        }
        payload[7] = 0x00;
        
        // Calculate CRC
        uint8_t crc_calculated = vag_crc_esp21(payload, 8, counter);
        
        if (crc_calculated == crc_from_can) {
            crc_ok++;
        } else {
            crc_fail++;
            if (crc_fail <= 5) { // Show first 5 failures
                printf("Line %d: CRC mismatch - CAN: 0x%02X, Calc: 0x%02X, Counter: 0x%X\n",
                       line_num, crc_from_can, crc_calculated, counter);
            }
        }
    }
    
    fclose(fp);
    printf("Verification complete: %d OK, %d FAIL\n", crc_ok, crc_fail);
    if (crc_ok > 0 && crc_fail == 0) {
        printf("✓ SUCCESS: All CRCs match!\n");
    }
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <input_log.csv> [output_translated.csv] [--verify reference.csv]\n", argv[0]);
        printf("\nModes:\n");
        printf("  Translate: %s input.csv output.csv\n", argv[0]);
        printf("  Verify:    %s --verify reference.csv\n", argv[0]);
        return 1;
    }

    // Verification mode
    if (argc >= 3 && strcmp(argv[1], "--verify") == 0) {
        verify_log(argv[2]);
        return 0;
    }

    // Translation mode
    if (argc < 3) {
        printf("Error: output filename required for translation\n");
        return 1;
    }

    FILE *in = fopen(argv[1], "r");
    if (!in) {
        printf("Error: could not open input file %s\n", argv[1]);
        return 1;
    }

    FILE *out = fopen(argv[2], "w");
    if (!out) {
        printf("Error: could not create output file %s\n", argv[2]);
        fclose(in);
        return 1;
    }

    // Write header
    fprintf(out, "Time Stamp,ID,Extended,Dir,Bus,LEN,D1,D2,D3,D4,D5,D6,D7,D8\n");

    printf("Translating 0x288 -> 0x0FD...\n");
    uint8_t counter = 0x00; // Start at 0, not 0xD0
    char line[1024];
    
    // Skip header
    fgets(line, sizeof(line), in);

    while (fgets(line, sizeof(line), in)) {
        processLine(line, out, &counter);
    }

    fclose(in);
    fclose(out);
    printf("Translation complete. Output written to %s\n", argv[2]);
    
    // Auto-verify if verification file provided
    if (argc >= 4) {
        printf("\nAuto-verifying against: %s\n", argv[3]);
        verify_log(argv[3]);
    }
    
    return 0;
}
Post Reply