RK3566 EBC Reverse-Engineering

From PINE64
Jump to navigation Jump to search

The RK3566 SoC, used in the Quartz64 SBC by PINE64, contains an eInk interface. This is referred to as ebc by Rockchip apparently.

Unfortunately, the driver published for this eInk interface within the BSP kernel is an assembly dump produced by gcc. Fortunately, it contains quite a bit of debug information, which we can use to reverse engineer it.



The ebc driver source is available from the quartz-bsp repository.

The files of interest are ebc_dev_v8.S, which implements a DRM (Direct Rendering Manager) driver for the eInk panel, and the waveform files in the epdlut subdirectory.

There's also a completely open-source simple driver for u-boot: drivers/video/rk_eink at JeffyCN/rockchip_mirrors

In-Development Driver

rk356x-ebc-dev is the branch on the pine64 linux-next repository used for developing an upstreamable driver based on mainline kernel sources.



TI TPS65185x

This is the PMIC used to drive the e-Ink panel, for which the downstream sources also implement a driver.


Assembly Syntax and Semantics

The Syntax is GNU Assembler (GAS) syntax. This modexp article provides a good introduction to the syntax, calling convention, semantics and some often used instructions.

The ARM Architecture Reference Manual for ARMv8 should be used as reference for any instructions.

At the very least, you should read up on the registers and calling convention used.

Various Findings

The driver isn't really something that can be mainlined as-is once reversed, as it makes a number of questionable design decisions.

  • It's technically a DRM subsystem driver, but doesn't really utilise what DRM provides at all.
  • It seems to register a new ioctl to set buffer attributes like width and height, despite DRM more than likely having a way for clients to tell a driver what size the framebuffer should be.
  • It directly interacts with the PMIC instead of going through regulators/hwmon.

However, reverse engineering to know how it works provides a good baseline from which we can rewrite it in a more sensible manner.

Debug Information

Quite a bit of debug info is left in the assembly dump, including function names, file names and line numbers. We can take this to our advantage.

.file file-number file-path

Specifies a number to reference a file by, and its path. All following code until the next .file or .loc statement are to be understood as originating from this file. This is particularly useful to understand which code has been inlined from other files, for which the source is available.

.loc file-number line-number 0

Specifies that the following code is generated from line-number stemming from file number file-number. See the .file directive for this file number to understand which source file it came from.

.type function-name, %function

This tells us that the following code belongs to function function-name. You'll usually see a .cfi_startproc, which signifies the start of the function code, until the matching .cfi_endproc.

A quick grep for %function shows that we are dealing with 30 functions in this file.

.type struct-name, %object

This seems to signify a definition of a C struct named struct-name.

A quick grep for %object shows that we are dealing with around 27 structs in this file.


TODO: This seems to contain the main bulk of the DWARF debug information, including enough info to reverse full structs and function signatures.

Finding Structs and Function Signatures

First, we'll need to assemble the file:

aarch64-linux-gnu-gcc -c -o ebc_dev_v8.o ebc_dev_v8.S

This gives us a ebc_dev_v8.o which we can feed into analysis tools.

For both of these, keep in mind that we're only interested in stuff from ebc_dev.c, or any other files for which we don't have the source. There's no point in getting the struct description or reverse-engineering a function that we have the source code for. A lot more than ebc_dev will be in the object file due to inlining and such.

Also make sure that if you are looking up known struct accesses, that you use struct definitions from the BSP kernel, not from mainline. The kernel has no internal ABI for drivers!

Faster and Easier - Ghidra

Import the file into Ghidra, open the code browser. After analysis, you should be able to find structs in the "Data Type Manager" marked with an S icon. You'll also find functions in the symbol tree.

Slow and Painful - readelf/objdump

Use this if you want to manually look up dwarf symbols for some reason.

readelf --debug-dump ebc_dev_v8.o

This will produce a lot of output, but we're mainly concerned with the start of the dump. We'll find things like:

 <2><101f8>: Abbrev Number: 0
 <1><101f9>: Abbrev Number: 79 (DW_TAG_subprogram)
    <101fa>   DW_AT_name        : (indirect string, offset: 0xa2b4): ebc_open
    <101fe>   DW_AT_decl_file   : 1
    <101ff>   DW_AT_decl_line   : 1377
    <10201>   DW_AT_prototyped  : 1
    <10201>   DW_AT_type        : <0xc6>
    <10205>   DW_AT_low_pc      : 0x0
    <1020d>   DW_AT_high_pc     : 0xc
    <10215>   DW_AT_frame_base  : 1 byte block: 9c 	(DW_OP_call_frame_cfa)
    <10217>   DW_AT_GNU_all_call_sites: 1
    <10217>   DW_AT_sibling     : <0x1023a>
 <2><1021b>: Abbrev Number: 88 (DW_TAG_formal_parameter)
    <1021c>   DW_AT_name        : (indirect string, offset: 0x1153): inode
    <10220>   DW_AT_decl_file   : 1
    <10221>   DW_AT_decl_line   : 1377
    <10223>   DW_AT_type        : <0x1c54>
    <10227>   DW_AT_location    : 0xd63 (location list)
 <2><1022b>: Abbrev Number: 106 (DW_TAG_formal_parameter)
    <1022c>   DW_AT_name        : (indirect string, offset: 0x8b06): file
    <10230>   DW_AT_decl_file   : 1
    <10231>   DW_AT_decl_line   : 1377
    <10233>   DW_AT_type        : <0x551f>
    <10237>   DW_AT_location    : 1 byte block: 51 	(DW_OP_reg1 (x1))

This essentially tells us the full function signature of ebc_open:

DW_TAG_subprogram tells us of a function, with DW_AT_name letting us know that this is ebc_open. DW_AT_type of 0xc6 let's us know, once we jump to <c6>, that this function's return type is a signed 32-bit integer.

The DW_TAG_formal_parameter that follow tell us of each of the parameter the function takes. The first one is called inode and is of type 0x1c54. Referencing what this type is, we find:

 <1><1c54>: Abbrev Number: 7 (DW_TAG_pointer_type)
    <1c55>   DW_AT_byte_size   : 8
    <1c56>   DW_AT_type        : <0x1970>

which in of itself goes on to reference 0x1970, and looking this one up, we'll find a struct definition:

 <1><1970>: Abbrev Number: 26 (DW_TAG_structure_type)
    <1971>   DW_AT_name        : (indirect string, offset: 0x1153): inode
    <1975>   DW_AT_byte_size   : 672
    <1977>   DW_AT_decl_file   : 31
    <1978>   DW_AT_decl_line   : 611
    <197a>   DW_AT_sibling     : <0x1c4f>
 <2><197e>: Abbrev Number: 27 (DW_TAG_member)
    <197f>   DW_AT_name        : (indirect string, offset: 0x7d00): i_mode
[etc etc...]

Reverse-Engineered Stuff



struct ebc_info {
    long unsigned int ebc_buffer_phy;
    char* ebc_buffer_vir;
    int ebc_buffer_size;
    int ebc_buf_real_size;
    int direct_buf_real_size;
    int is_busy_now;
    int task_restart;
    int auto_refresh_done;
    char frame_total;
    char frame_bw_total;
    int auto_need_refresh0;
    int auto_need_refresh1;
    int frame_left;
    int part_mode_count;
    int full_mode_num;
    int height;
    int width;
    int* lut_addr;
    int buffer_need_check;
    int ebc_irq_status;
    int ebc_dsp_buf_status;
    struct device* dev;
    struct epd_lut_data lut_data;
    struct task_struct* ebc_task;
    int* auto_image_new;
    int* auto_image_old;
    int* auto_image_bg;
    int* auto_image_cur;
    u8* auto_frame_count;
    int* auto_image_fb;
    void* direct_buffer;
    int ebc_power_status;
    int ebc_last_display;
    char* lut_ddr_vir;
    struct ebc_buf_s* prev_dsp_buf;
    struct ebc_buf_s* curr_dsp_buf;
    struct wake_lock suspend_lock;
    int wake_lock_is_set;
    int first_in;
    struct timer_list vdd_timer;
    struct timer_list frame_timer;
    struct work_struct auto_buffer_work;
    int is_early_suspend;
    int is_deep_sleep;
    int is_power_off;
    int overlay_enable;
    int overlay_start;


struct ebc {
    struct device* dev;
    struct ebc_tcon* tcon;
    struct ebc_pmic* pmic;
    struct ebc_panel* panel;
    struct ebc_info* info;


struct rkf_waveform {
    int length,
    char[16] format,
    char[16] version,
    char[16] timeandday,
    char[16] panel_name,
    char[16] panel_info,
    char[64] full_version,
    char[64] reset_temp_list,
    char[64] gc16_temp_list,
    char[64] gl16_temp_list,
    char[64] glr16_temp_list,
    char[64] gld16_temp_list,
    char[64] du_temp_list,
    char[64] a2_temp_list,
    uint[64] reset_list,
    uint[64] gc16_list,
    uint[64] gl16_list,
    uint[64] glr16_list,
    uint[64] gld16_list,
    uint[64] du_list,
    uint[64] a2_list



enum rkf_waveform_type {
    RKF_WF_RESET = 0,
    RKF_WF_DU    = 1,
    RKF_WF_GC16  = 2,
    RKF_WF_GL16  = 3,
    RKF_WF_GLR16 = 4,
    RKF_WF_GLD16 = 5,
    RKF_WF_A2    = 6