Close

A lot of tiny problems

A project log for Opensource HomeLink ecu for VAG

Open-source HomeLink module for VAG cars. Replaces the stock unit, working with standard LIN Bus and supporting various garage doors.

stepan-skopivskiyStepan Skopivskiy 04/25/2025 at 13:140 Comments

The next steps were tiny but important:

Platform

I have some experience in developing firmware for STM32F4 series processors and ESP processors. However, the expectation was to find something new, powerful, and well-known. My first thoughts are Arduino as a framework and Raspberry Pi as hardware. Raspberry Pi has great microcontrollers: 

Both are great and powerful devices, but neither has substantial support. The Pico 1 even offers Arduino support through PlatformIO, although that support is quite limited (very limited). So, unfortunately, they are not a good choice for the ECU proposal.

The next is STM32, and as I have experience with it, it sounds promising. The most popular MCUs are stm32f4 series. AliExpress sells a lot of variants of them, but the most interesting thing is that almost all of them have the same package and can be replaced easily with another one from the series. The most popular are:

Depending on xx, they can have varying amounts of RAM and ROM. However, for our purposes, even the simplest one is sufficient. They also provide good support for PlatformIO and various libraries, so we had a test project with PlatformIO, STM32F401, and the Arduino framework. After a few days of coding, the home link buttons started to blink. Here is the code snippet of the LIN frames structure from the LIN Description File.

#ifndef FRAMES_H
#define FRAMES_H

typedef enum
{
    UGDO_Door_Action_No_Request = 0,
    UGDO_Door_Action_Garage_Open_Or_Toggle = 1,
    UGDO_Door_Action_Garage_Close = 2,
    UGDO_Door_Action_Garage_Stop = 3,
    UGDO_Door_Action_Status_Request = 4
} UGDO_Door_Action_t;

typedef enum
{
    UGDO_Switchboard_Button_Function = 0,
    UGDO_Switchboard_Failure = 1
} UGDO_Switchboard_t;

typedef enum
{
    UGDO_Buttons_All_Released = 0,
    UGDO_Button_1_Pressed = 1,
    UGDO_Button_2_Pressed = 2,
    UGDO_Button_1_2_Pressed = 3,
    UGDO_Button_3_Pressed = 4,
    UGDO_Button_1_3_Pressed = 5,
    UGDO_Button_2_3_Pressed = 6,
    UGDO_Button_1_2_3_Pressed = 7,
    UGDO_Button_X_Pressed = 8
} UGDO_Buttons_t;

typedef enum
{
    UGDO_No_Request = 0,
    UGDO_Garage_1 = 1,
    UGDO_Garage_2 = 2,
    UGDO_Garage_3 = 3,
    UGDO_Garage_4 = 4,
    UGDO_Garage_5 = 5,
    UGDO_Garage_6 = 6,
    UGDO_Garage_7 = 7,
    UGDO_Garage_8 = 8,
    UGDO_Garage_9 = 9,
    UGDO_Garage_10 = 10,
    UGDO_Garage_11 = 11,
    UGDO_Garage_12 = 12,
    UGDO_Garage_13 = 13,
    UGDO_Garage_14 = 14,
    UGDO_Garage_15 = 15
} UGDO_Door_t;

typedef enum
{
    Kl_15_Off = 0,
    Kl_15_On = 1
} Kl_15_t;

typedef enum
{
    Not_Pressed = 0,
    Pressed = 1
} ZV_auf_Funk_t;

typedef enum
{
    LED_Init = 0,
    LED_Green_On = 1,
    LED_Yellow_On = 2,
    LED_Red_On = 3,
    LED_Green_Flashing_10Hz = 4,
    LED_Yellow_Flashing_1Hz = 5,
    LED_Flashing_Off = 16,
    LED_Green_Flashing_1x_1Hz = 17,
    LED_Green_Flashing_2x_1Hz = 18,
    LED_Green_Flashing_3x_1Hz = 19,
    LED_Green_Flashing_4x_1Hz = 20,
    LED_Green_Flashing_5x_1Hz = 21,
    LED_Green_Flashing_6x_1Hz = 22,
    LED_Green_Flashing_7x_1Hz = 23,
    LED_Green_Flashing_8x_1Hz = 24,
    LED_Green_Flashing_9x_1Hz = 25,
    LED_Green_Flashing_10x_1Hz = 26,
    LED_Green_Flashing_11x_1Hz = 27,
    LED_Green_Flashing_12x_1Hz = 28,
    LED_Green_Flashing_13x_1Hz = 29,
    LED_Green_Flashing_14x_1Hz = 30
} UGDO_LED_Code_t;

typedef enum
{
    UGDO_Function_LED = 0,
    UGDO_Function_Transceiver_Locked = 1,
    UGDO_Function_Reserved_2 = 2,
    UGDO_Function_Reserved_3 = 3,
    UGDO_Function_Successful_Learned = 4,
    UGDO_Function_Reserved_5 = 5,
    UGDO_Function_Timeout_Learning = 6,
    UGDO_Function_Error = 7
} UGDO_Function_t;

typedef enum
{
    UGDO_Antenna_Normal = 0,
    UGDO_Antenna_Error = 1
} UGDO_Failure_Antenna_t;

typedef enum
{
    UGDO_Coding_Normal = 0,
    UGDO_Coding_Error = 1
} UGDO_Failure_Coding_t;

typedef enum
{
    UGDO_Hardware_Normal = 0,
    UGDO_Hardware_Error = 1
} UGDO_Failure_Hardware_t;

typedef enum
{
    UGDO_Response_Normal = 0,
    UGDO_Response_Error = 1
} UGDO_ResponseError_t;

typedef enum
{
    UGDO_Channel_not_trained = 0,
    UGDO_Channel_trained = 1,
} UGDO_Channel_encoding_t;

typedef enum
{
    UGDO_Door_Idle = 0,
    UGDO_Door_Moving = 1,
    UGDO_Door_Open = 2,
    UGDO_Door_Closed = 3,
    UGDO_Door_Movement_Stopped = 4,
    UGDO_Door_No_Com_Out_Of_Reach = 5,
    UGDO_Door_Communication_Error = 6
} UGDO_DoorState_t;

/**
 * ESP_v_Signal is ESP speed, 0...253, 2.56 per step, Unit_KiloMeterPerHour
 */
typedef struct
{
    UGDO_Buttons_t UGDO_Buttons;
    Kl_15_t Kl_15;
    ZV_auf_Funk_t ZV_auf_Funk;
    uint8_t ESP_v_Signal;
    UGDO_Switchboard_t UGDO_Switchboard;
    UGDO_Door_t UGDO_Door;
    UGDO_Door_Action_t UGDO_Door_Action;
} ButtonRequest_t;

typedef struct
{
    UGDO_LED_Code_t UGDO_LED_Code;
    UGDO_Function_t UGDO_Function;
    UGDO_Failure_Antenna_t UGDO_Failure_Antenna;
    UGDO_Failure_Coding_t UGDO_Failure_Coding;
    UGDO_Failure_Hardware_t UGDO_Failure_Hardware;
    UGDO_ResponseError_t UGDO_ResponseError;
    UGDO_Channel_encoding_t UGDO_Channel_1; // Door 1 training status
    UGDO_Channel_encoding_t UGDO_Channel_2; // Door 2 training status
    UGDO_Channel_encoding_t UGDO_Channel_3; // Door 3 training status
    UGDO_Channel_encoding_t UGDO_Channel_4; // Door 4 training status
    UGDO_Channel_encoding_t UGDO_Channel_5; // Door 5 training status
    UGDO_Channel_encoding_t UGDO_Channel_6; // Door 6 training status
    UGDO_Channel_encoding_t UGDO_Channel_7; // Door 7 training status
    UGDO_Channel_encoding_t UGDO_Channel_8; // Door 8 training status
    UGDO_Channel_encoding_t UGDO_Channel_9; // Door 9 training status
    UGDO_Channel_encoding_t UGDO_Channel_10; // Door 10 training status
    UGDO_Channel_encoding_t UGDO_Channel_11; // Door 11 training status
    UGDO_Channel_encoding_t UGDO_Channel_12; // Door 12 training status
    UGDO_Channel_encoding_t UGDO_Channel_13; // Door 13 training status
    UGDO_Channel_encoding_t UGDO_Channel_14; // Door 14 training status
    UGDO_Channel_encoding_t UGDO_Channel_15; // Door 15 training status
    UGDO_DoorState_t UGDO_DoorState;
} ButtonResponse_t;

#endif //FRAMES_H

 And of course, a video with it. Based on the simple blinking mode rotation.

void process(const ButtonRequest_t& request, ButtonResponse_t& response)
{
    static uint32_t count = 0;

    count =  (count + 1) % 100;
    
    switch (count / 10)
    {
    case UGDO_Button_1_Pressed:
        response.UGDO_LED_Code = LED_Yellow_Flashing_1Hz;
        break;
    case UGDO_Button_2_Pressed:
        response.UGDO_LED_Code = LED_Green_Flashing_10Hz;
        break;
    case UGDO_Button_3_Pressed:
        response.UGDO_LED_Code = LED_Green_Flashing_1x_1Hz;
        break;
    case UGDO_Button_1_2_Pressed:
        response.UGDO_LED_Code = LED_Red_On;
    default:
        response.UGDO_LED_Code = LED_Green_Flashing_10x_1Hz;
    }
} 

Almost the same way was realized for UDS, and the tiny MCU starts to respond to the ODIS diagnosis. But with one anomaly - at random times, some random characteristic: system designation, hardware part number, serial number, etc, was not responding and stayed in "NOT_AVAILABLE" mode. The main problem was that the characteristic not available was always different. Sometimes the part number was not available, sometimes the serial number, sometimes both. But anyway, the 4th item of the checklist was done too. The somehow working workbench was created: 

LIN bus <> TJA1021A <> STM32F401 <> Arduino 

The 3rd item of the checklist was done too, as the source code started to blink the buttons and respond to the ODIS diagnosis. 

But the problem with random NOT_AVAILABLE in the ODIS was new and unclear to investigate because of its randomness. The code was developed using the FreeRTOS library. It had 4 tasks:

Everything was okay with no exceptions, all delays were minimized, but the random NOT_AVAILABLE still appears. The logic analyzer shows that the MCU does not respond to the request frames, but unfortunately does not show why. It just shows that the BCM does request to read a frame, but the MCU responds with some delay in frames. E.g., when the BCM expects the first frame, the MCU does not respond. When BCM tries a second time, the MCU sends the first frame. 

To test this behavior, a lot of code was commented out. The idea was to test the LIN break or sync detection, to make it easy, the ping output pin was implemented. It should send a short impulse when the MCU detects the LIN break or sync. And after a few runs, the logic analyzer showed that at random times, the delay between the actual LIN break and the ping pin impulse, the delay was more than 5ms, when the 8-byte frame takes approximately 3ms.

This behavior starts the new investigation: what if the Arduino is too slow? The deep reading of the documentation of the Arduino core for STM32 shows that it uses the interrupt to save data from the UART to the Serial buffer. But we reading the buffer in the FreeRTOS task. This means that, depending on the situation, we can skip or, better to say, miss the frames. What if we use the IRQ? - Unfortunately, no. The Arduino function that is used as the UART IRQ handler cannot be overridden. Besides that, because of Arduino abstraction, it is impossible to use the STM built-in LIN functionality, which can easily detect and send the LIN break, for example. Theoretically, we can change the code compilation in a way that the interesting us function will be overridden with some tricks. But it adds a loooot of complication to the project then. So, the Arduino is not our way either.

In general, the PlatformIO tool supports the following frameworks for the STM32:

After a short discussion with ChatGPT, the next candidate was chosen - STM32Cube. This framework is the OEM one that the STM company proposes for their MCUs. Also, the internet has a lot of examples, and ChatGPT generates the working "led blinker" with the less tries. After choosing it, the next few days were wasted trying to make the UART run. All that stuff that Arduino does for you by Serial::begin() should be replicated with HAL now. And it takes a while of testing, setting up, and configuring the MCU with HAL...

But now we finally have full control of the hardware LIN (UART with LIN). We can attach the IRQ, configure it to react exactly on the LIN break signal, and it was cool! Now the MCU sends the ping signal in the microseconds after the LIN break happened! A few more movements of implementing freertos, adoption the Arduino style code for Buttons and UDS handlers, and here we go. Now the 3rd and 4th items from our checklist a 100% done! We have a workable and stable test workbench and environment.

Another interesting characteristic of ODIS was discovered, each data that it reads should have the minimum data length. All the data below the length will be ignored and shown as NOT_AVAILABLE. All data above - cutted. And it looks like that this length is controlled by the BCM. 

DID partDescription                                                         Length
0x6204VW/Audi part number11
0x6C04System designation13
0x6604Hardware part number11
0x6404Software version4
0x6804Hardware version3
0x6A04Serial number20
0x6004Coding3
0x6E04FAZIT Identification23

The left 2nd item was postponed to the future - investigate the remotes and commands sent by them.

Discussions