In my last post, I briefly mentioned that I chose the Nano Pi M3 because of it’s size and, more importantly, because it had an on-board LVDS header. In this post, I’ll be detailing how I managed to get this board talking to my Chimei Innolux EJ070NA-01J screen.
Pramble
If you’re here because you want to use the LVDS header, I’d encourage you to first read the Nano Pi M3 Wiki page, particularly the sections dealing with compiling your own kernel. Yes, you will be hacking up driver. Furthermore, you will need a serial board, such as the PSU-ONECOME as mentioned in the wiki, or a USB-to-Serial board (ie. FT232RL), like the one I have.
Hardware Setup
In between the NanoPi and the screen sits a PCB800182 signal converter board. Plug the parts together, including the serial board as shown below. Connect GND, Rx, and Tx from the NanoPi to GND, Tx and Rx pins on the serial board respectively. Do NOT connect the 5V line because you will be powering the NanoPi via USB cable.

You will need two USB cables connected to your computer; one for the serial board and one for the NanoPi. Since the screen is drawing power through the NanoPi, be aware that quite a bit of power will be drawn from your USB port. A standard USB port outputs ~500-900 mA, which may not enough. On some laptops, there is a USB charging port which can crank out up to 1,500 mA of power. If you have this port, use it. Otherwise, the NanoPi can shut down randomly if it’s not getting enough power. Alternatively, if you’re handy with a soldering iron, the PCB800182 can supply power to the screen and itself; it requires a bit of manual wiring. Another option would be to use a powered USB hub.
On your PC, boot into your favourite Ubuntu-based distro. You will require the following:
1 2 3 |
$> sudo add-apt-repository ppa:phablet-team/tools $> sudo apt-get update $> sudo apt-get install android-tools-adb android-tools-fastboot minicom |
Minicom is a serial terminal emulator that allows us to talk to the board via UART. Once you have the serial board plugged in, issue the following to start listening:
1 |
$> sudo minicom -b 115200 -o -D /dev/ttyUSB0 |
Flashing the SD Card
The NanoPi wiki contains instructions on how to flash your SD card with a preloaded linux or android distribution. For the purpose of this post, I’ll be using the Ubuntu Core QtE image. To flash the image into your card, issue the following (substitute file name and SD Card path):
1 2 |
$> sudo dd bs=4M if=NAME_OF_IMAGE_FILE.img of=/dev/SDCARD_PATH status=progress $> sudo sync |
Once that completes, throw the card into the NanoPi.
The Bootloader
When the NanoPi is booted up, the first thing that runs is the U-Boot bootloader. Minicom should spring to life and start displaying rather meaningless information to the screen.
1 2 3 4 5 6 7 8 9 10 |
U-Boot 2014.07-g66f140d-dirty (Mar 12 2017 - 13:34:16) PLL : [0] = 800000000, [1] = 800000000, [2] = 614394000, [3] = 800000000 (0) PLL1: CPU FCLK = 800000000, HCLK = 200000000 (G0) (7) PLL1: CPU FCLK = 800000000, HCLK = 200000000 (G1) (2) PLL3: MEM FCLK = 800000000, DCLK = 800000000, BCLK = 400000000, PCLK = 200000000 (1) PLL0: BUS BCLK = 400000000, PCLK = 200000000 (8) PLL0: CCI4 BCLK = 400000000, PCLK = 200000000 .... |
Before it continues booting the Linux kernel, it will ask to interrupt the autoboot sequence with a key stroke. Doing so will result in a user prompt.
1 2 |
Hit any key to stop autoboot: 0 s5p6818# |
If the LCD screen is connected to the LVDS header on the NanoPi, you may see a logo displayed. Or, at the very least, the screen being powered. It’s expected that the screen won’t work properly at this point.
You may continue booting by issuing a boot
command into the prompt.
Modifying U-Boot
The U-Boot source code can be obtained using the steps detailed on the NanoPi wiki. If everything is set up correctly, you should be able to compile the source and generate U-Boot binary.
The bootloader detects the video output device by probing both HDMI and the RGB port on the board. Then, it initialises the display and displays a FriendlyArm image before passing control to the kernel. Notice that I did not mention anything about detecting a display device connected to the LVDS header. The reason being, is that the display detection code lacks any notion of LVDS output; the header seems to be powered regardless. In it’s default configuration, it will display anything in the frame buffer regardless of where the video signal is destined for.
Side Note on U-Boot Initialisation
The U-Boot code initialises the board by calling into board_early_init_f()
first, then board_init()
in board/s5p6818/nanopi3/board.c
. The former function initialises the onewire library and probes the RGB port. The latter function calls into bd_lcd_init()
, where the result of the probe is used to initialise display info structures. And within this process is where the aforementioned problem lies. If the onewire probe did not find an LCD screen, the code defaults to using HDMI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static void bd_lcd_init(void) { /* ... */ myid = onewire_get_lcd_id(); /* -1: onewire probe failed * 0: bad * >0: identified */ if (myid <= 0 || /* ... */) { /* No LCD - Initialise HDMI */ } else { /* Found LCD - Initialise */ } /* ... */ } |
Since the code lacks any way of probing the LVDS header, the only avenue left is to hard-code the screen we’re using. For the sake of my project, this is not that big of an issue.
LVDS Initialisation Code
The ultimate goal is to initialise a struct nxp_lcd
with the correct timing values so that the screen can function properly. The easiest way is to use the framework developed by the FriedlyArm folks located in board/s5p6818/nanopi3
. It’s not the prettiest code, mind you, but it does the job. The contents of display.c
contain the code to initialise the video parameters with the correct display timings; we will start there. Add the following function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* Add this with the rest of the #defines. */ #define INIT_PARAM_LVDS(name) \ struct disp_lvds_param name = { \ .lcd_format = CFG_DISP_LVDS_LCD_FORMAT, \ }; /* Add this with the rest of the bd_disp_*() functions. */ static void bd_disp_lvds(void) { #if defined(CONFIG_DISPLAY_OUT_LVDS) struct nxp_lcd *lcd = nanopi3_get_lcd(); INIT_VIDEO_SYNC(vsync); INIT_PARAM_SYNCGEN(syncgen); INIT_PARAM_MULTILY(multily); INIT_PARAM_LVDS(lvds); nxp_platform_disp_init(lcd, &vsync, &syncgen, &multily); display_lvds(CFG_DISP_OUTPUT_MODOLE, CONFIG_FB_ADDR, &vsync, &syncgen, &multily, &lvds); mdelay(50); printf("Using LVDS!\n"); printf("DISP: W=%4d, H=%4d, Bpp=%d\n", lcd->width, lcd->height, CFG_DISP_PRI_SCREEN_PIXEL_BYTE*8); #endif } |
And modify the bd_display()
function as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
int bd_display(void) { const char *name = nanopi3_get_lcd_name(); if (strncmp(name, "HDMI", 4) == 0) { #if defined(CONFIG_DISPLAY_OUT_HDMI) bd_disp_hdmi(); #endif } else if (strncmp(name, "LVDS", 4) == 0) { #if defined(CONFIG_DISPLAY_OUT_LVDS) bd_disp_lvds(); #endif } else { #if defined(CONFIG_DISPLAY_OUT_RGB) bd_disp_rgb(); #endif } return 0; } |
LCD Timings
To use an LCD screen, the driver needs to know specific information about the screen; mainly timing values and resolution. Those values are published inside datasheets that can be downloaded from a manufacturer’s website. Unfortunately for me, I had to do a bit more digging to find the timing values for the EJ070NA-01J because not all values were published in their PDF. Luckily, I found someone that had the same screen. Moving on, the screen timings need to be added into the lcds.c
file as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/* Insert this into the LCD screen listings. */ static struct nxp_lcd wsvga_ej070na_01j = { .width= 1024, .height = 600, .p_width = 154, .p_height = 90, .bpp = 24, .freq = 60, /* page 11 */ .timing = { .h_fp = 35, .h_bp = 50, .h_sw = 30, .v_fp = 5, .v_fpe = 1, .v_bp = 10, .v_bpe = 1, .v_sw = 5, }, .polarity = { .rise_vclk = 1, .inv_hsync = 1, .inv_vsync = 1, .inv_vden = 0, }, }; |
Then add the structure into the LCD config list using the LVDS
name:
1 2 3 4 5 6 7 8 |
nanopi3_lcd_config[] = { /* ... */ /* Make sure to use the "LVDS" label. */ { -1, "LVDS", &wsvga_ej070na_01j, 170, 0 }, /* ... */ }; |
Enabling the Screen
Finally, the only step remaining is to enable the screen’s usage. As mentioned in the U-Boot side note, there is no way to detect the LVDS screen using the framework provided. So, in bd_lcd_init()
, the only thing left is to hard-code the screen’s usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static void bd_lcd_init(void) { struct nxp_lcd *cfg; int width, height; /* Don't detect anything, force LVDS usage. */ nanopi3_setup_lcd_by_name("LVDS"); cfg = nanopi3_get_lcd(); width = cfg->width; height = cfg->height; #if defined(CONFIG_DISPLAY_OUT) /* Clear framebuffer */ memset((void *)CONFIG_FB_ADDR, 0, width * height * 4); #endif } |
Then build the bootloader.
1 |
$> make CROSS_COMPILE=arm-linux- |
Flashing the Bootloader
To flash the newly created U-Boot binary, interrupt the autoboot process on the NanoPi and enter the bootloader into fastboot
mode. Then, flash the bootloader and reboot.
1 2 3 4 |
$> sudo fastboot flash bootloader u-boot.bin ... $> sudo fastboot reboot |
When you boot up, you should be greeted with a perfectly rendered boot logo!

Kernel Hacking
For this step, you will need to pull down the Linux kernel source code. The steps are detailed in the NanoPi wiki.
Once the bootloader finishes doing it’s work, it launches the Linux kernel and passes in the name of the LCD screen as a command line parameter. In our case, the name of the display will be LVDS
, as defined above. During boot, that parameter is looked up in the nanopi3_lcd_config
list, which is similar to the list from the U-Boot code, and the timings are subsequently used.
Add the following to arch/arm/plat-s5p6818/nanopi3/lcds.c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* Direct copy from U-Boot. */ static struct nxp_lcd wsvga_ej070na_01j = { .width= 1024, .height = 600, /* ... etc. */ }; /* And modify the list. */ static struct { char *name; struct nxp_lcd *lcd; int ctp; } nanopi3_lcd_config[] = { /* ... */ /* Name must be "LVDS". */ { "LVDS", &wsvga_ej070na_01j, 0 }, /* ... */ }; |
Compiling the Kernel
The wiki documents the steps needed to compile the kernel. In the menuconfig
step, enable the LVDS option buried deep within the menu:
1 2 3 4 5 |
Device Drivers ---> Graphics support ---> Nexell Graphics ---> [*] LVDS (0) Display In [0=Display 0, 1=Display 1] |
Finally, make the kernel and copy the generated uImage
onto the boot partition of the SD card.
1 2 |
$> sudo cp arch/arm/boot/uImage /PATH/TO/SD_CARD/boot $> sudo sync |
Once you boot the NanoPi up, the kernel boot text should be rendered perfectly on the screen.

The QtE-Demo should load up shortly after boot.
Happy User
That is an impressive tutorial. Wow!
I came across this while dealing with a similar mess with a Banana Pi-M2. I got that one for the exact same reason – the LVDS connector. What I am trying to do is hook up an automotive grade display. These are by far better than anything you can find on the consumer market. You can get one of these from eBay as a spare part for cars obviously.
Should have done my research before ordering the board. After reading this I am starting to think I should order one of the NanoPi-M3 boards and use this. It will definitely be worth the time saved.
Any advice? Should I invest into a NanoPi M3 or you’re thinking some other SBC?
Mark
The M3 is a decent board, I would recommend it. The downside is that you’re stuck with Linux 3.x, because Samsung (or Nexell) has not ported their drivers to a newer linux kernel. Another recommendation would be to power the screen independently of the LVDS connector; because it can draw quite a bit of juice.
Happy User
Thanks for the heads-up.
What I am dealing with now is virtually the same. I have to choose between tweaking a working solution with Linux 3.x kernel that uses FEX files for configuring *or* be the first one to push through a LVDS device tree configuration for a board with the now deprecated A31s SOC.
So, for now I’ll hold off purchasing a NanoPi M3 until your 4th post “NanoPi M3 and LVDS with Linux Kernel 4.4” is out 😉
Keep up the great work. It is definitely professional grade.
Mark
Nothing would make me happier than linux 4.x on this board :).
I asked on the FriendlyArm boards and, so far, no reply. http://www.friendlyarm.com/Forum/viewtopic.php?f=42&t=507&p=1684#p1684
Jackson
Do you happen to have if I can have Nano Pi M3 to drive a 14″ – 16″ laptop LCD panel (which also speak LVDS obviously)?
Mark
In theory, yes. You’d have to tailor the parameters to your screen, but it seems doable.