- Kernel Hacking: Connect an SSD1289 LCD to a Beaglebone
17 May 2012 04:00:00 am - 17 May 2012 10:49:04 am
- Last edited by KermMartian on 18 May 2012 03:39:26 pm; edited 2 times in total
As many of you may know, I recently purchased a Beaglebone, having gotten fed up waiting for the Raspberry Pi to become a reality. The Beaglebone is an embedded development board showcasing Texas Instruments' AM335x line of System-on-a-Chip (SoC) MPUs. For the $90 board, you get a USB slave and host, a 32-bit ARM processor, 256 MB of off-chip DDR RAM, about 60 3.3v GPIO pins, an Ethernet port, and all sorts of other fun things. I have eventual goals of building a complete device around the processor, so the Beaglebone is a great way to prototype as I go. Of course, almost any device I'd want to build needs a screen of some sort, so my first major challenge was interfacing an LCD. I chose a 320x240 pixel touchscreen with an SSD1289 controller, which I purchased for less than $20. I read enough to know that the AM335x has an on-board LCD controller, or LCDC, but the easy stuff ended there.
There's a lot of documentation on the internet about TI's LCDC Raster Engine, but very little documentation or code for the LCDC LIDD Engine, used to operate more intelligent LCD panels, so I hope to document here a lot of the lessons that I learned trying (painfully) to work with it. Since my current distro of choice for my Beaglebone is Angstrom, I'll also be discussing my lessons compiling kernel modules and patches for Angstrom. At the end of this article, you can download a patch to add SSD1289 support for 240x320 LCDs to your Beaglebone (or, with some work, another platform).
Building Angstrom Images
Angstrom Linux uses the OpenEmbedded build system, built around Bitbake. It makes it (supposedly) easy to use an abstraction called recipes to built different sorts of kernels and root file systems for embedded systems, simply by specifying the machine and type of image you want and waiting a bit. It will figure out and generate all of the cross-compilation tools that you need, but I found that if you want to customize the kernel yourself, it is very challenging. If you want to modify your kernel or add your own kernel modules, you should know several things:
- To perform a menuconfig to reconfigure your kernel, you must run a series of three commands:
Code:
If everything is working properly, you'll get the standard console menuconfig to set kernel options. When you have set everything to your preference, exit and save, and run "MACHINE=<system_type> ./oebb.sh bitbake virtual/kernel -c compile -f". Do not run the config (not to be confused with menuconfig) or update steps again, as those seem to wipe out everything you may have changed or configured.MACHINE=<system_type> ./oebb.sh config <system_type>
MACHINE=<system_type> ./oebb.sh update
MACHINE=<system_type> ./oebb.sh bitbake virtual/kernel -c menuconfig
- It's hard to find the actual kernel sources that are being built. For the Beaglebone (for which <system_type> above should be beaglebone), it can be found in ./build/tmp-angstrom_vYYYY_MM-eglibc/work/beaglebone-angstrom-linux-gnueabi/linux-ti33x-psp-X.X-rXXX-gitreNNN....NN/git/, where YYYY and MM are a datestamp, X.X rXXX is a Linux revision number, and NNN....NN is a long cryptographic hash. The exact path will be different depending on which kernel version and Angstrom version you're compiling against.
- While we're circling vaguely around the issue of Git, guides that suggest you use the Angstrom website to grab the setup-scripts/OEBB build system are wrong. The correct source is GitHub, so the git clone command is
Code:
git clone git://github.com/Angstrom-distribution/setup-scripts.git
- Building anything other than a console-image is shaky at best. You can specify several different sorts of images; the three I have been experimenting with are these:
Code:
I could only get the first of these three to reliably build.MACHINE=<system_type> ./oebb.sh bitbake console-image
MACHINE=<system_type> ./oebb.sh bitbake xfce-nm-image
MACHINE=<system_type> ./oebb.sh bitbake systemd-gnome-image
- When you're ready to deploy your built images, there's two places you'll want to look. If you want the full root filesystem plus the bootloader files, look in ./build/tmp-angstrom_vYYYY_MM-eglibc/beaglebone-angstrom-linux-gnueabi/deploy/images/beaglebone/. If all you're doing is repeatedly building a kernel with "MACHINE=<system_type> ./oebb.sh bitbake virtual/kernel -c compile -f", you can get the new uImage in the exhaustingly-long path ./build/tmp-angstrom_vYYYY_MM-eglibc/work/beaglebone-angstrom-linux-gnueabi/linux-ti33x-psp-X.X-rXXX-gitreNNN....NN/git/arch/arm/boot/. Of course, if you're not building an ARM Beaglebone image, the directory may be a bit different.
- It's hard to find the actual kernel sources that are being built. For the Beaglebone (for which <system_type> above should be beaglebone), it can be found in ./build/tmp-angstrom_vYYYY_MM-eglibc/work/beaglebone-angstrom-linux-gnueabi/linux-ti33x-psp-X.X-rXXX-gitreNNN....NN/git/, where YYYY and MM are a datestamp, X.X rXXX is a Linux revision number, and NNN....NN is a long cryptographic hash. The exact path will be different depending on which kernel version and Angstrom version you're compiling against.
Modifying the Angstrom Kernel
If you want to modify your kernel by adding extra drivers or modules, there are a few things that you need to do. First, you'll need the .c and possibly .h file(s) for your particular modification. Some of these will be stand-alone, and some will be additions to existing files. For the Beaglebone SSD1289 driver, I added git/drivers/video/ssd1289.c and modified git/arch/arm/mach-omap2/board-am335xevm.c, devices.c, and clock33xx_data.c. You also need to add the switches in the Kconfig relevant to your driver (for me, git/drivers/video/Kconfig) so that your module can be enabled and disabled. You must modify the requisite Makefile to be aware of your module; for me, this was git/drivers/video/Makefile. Finally, there may be additional data files you need to add; for the SSD1289 driver, I overwrote the custom giant Beaglebone drivers/logo/logo_linux_clut224.ppm with the original 80x80-pixel Tux image.
As you work, keep copies of each file that you modify, and ideally, also keep the originals. You'll eventually need to do a special sort of diff in order to create what's known as a patch, a semi-version-agnostic set of changes that can be applied to a kernel to add your changes. If you forget to keep an original of any file, don't worry; you'll be able to pull a clean version of the Angstrom build system once you finish your driver and diff against that. After each set of changes, you'll need to rebuild the kernel with the "-c compile -f" command I listed in the previous section. The updated uImage will show up in the folder discussed above (.../arch/arm/boot) if the build succeeds. To test out each new kernel to see if it would boot and properly load my driver, I copy the uImage file to the boot partition of the Beaglebone's microSD card as well as to the /boot directory of the Angstrom partition. I suspect that these aren't both necessary, but I did both for the sake of sanity. I set up a tiny bash script to copy the uImage to both partitions and eject both partitions, as I have probably moved the microSD card back and forth between the Beaglebone and my computer about 400 times since I started developing this driver. Make sure that your Beaglebone is powered off when you remove and insert the microSD card, and that you properly eject (umount) the microSD card from your computer where you're building the kernel before removing it! If you don't take such precautions, you're likely to damage or at least corrupt your microSD card.
Creating Angstrom Kernel Patches
Once you have a working driver, you'll need to create an Angstrom patch. Save the git directory, and either restore the original files, or re-create the original git directory so you have a fresh repository version, patched with all the existing Angstrom/Beaglebone patches. Then, you'll use the unified diff tool (via diff -uNr) to create your patch. Something like "diff -uNr old-git-dir/ new-git-dir/ >mypatch.patch" should do the trick. Once you are satisfied with your patch, and have checked it over to make sure it contains everything it should (and nothing extra), you'll have to insert it into the Angstrom recipes directories. At the time of writing, this would be:
Code:
sources/meta-ti/recipes-kernel/linux/linux-ti33x-psp-3.2/beaglebone/
Code:
file://beaglebone/0031-beaglebone-add-ss1289-support.patch \
Thus far I've given you the very basics of setting up a build environment, but there's more documentation out there on customizing kernels than the frustratingly-poorly-documented LCDC LIDD mode, so without further ado, let's move on to that.
The SSD1289 LCD and the AM335x's LCDC
The TI AM335x has an LCDC (LCD Controller) IP which has been used in one form or another in many of their previous SoCs and MPUs. It can support several different types of "dumb" Raster and "smart" LIDD displays. Raster displays must be used with Direct Memory Access, or DMA, meaning that you set up pointers and modes for a chunk of memory, and the AM335x takes care of repeatedly copying that memory to the screen. It worries about the LCD's timing needs without taking up actual CPU time. If you're using an LIDD display, where you speak to the display in address and data commands instead of a continuous stream of raw data, you have two options. You can write directly to memory-mapped I/O (MMIO) addresses, which the AM335x converts into the proper waveforms for sending and receiving addresses and data to and from the LCD's internal driver (such as the HX8347, ILI9325, and SSD1289). However, you still need to spend a lot of CPU time copying width*height*color_depth bits over to the LCD each time the screen image is updated. Once again, DMA can come to your rescue. In the next two sections, I'll discuss the DMA and non-DMA modes of the LCDC's LIDD functionality. For now, let me talk about the basics of interfacing an SSD1289-based LCD, and general lessons that I learned during development that I didn't really find documented anywhere else.
First, you need to have the physical connections in place. The Beaglebone has 3.3V I/O (and the AM335x can be permanently damaged if you accidentally try to interface 5V I/O without level-shifting), and luckily the SSD1289 is happy with a 3.3V supply and 3.3V I/O. Any 8080-style LCD driver (such as the SSD1289) should be interfaced to the Beaglebone/AM335x with the following wiring:
Code:
LCD D0-D15: 16 bits of data/address lines
LCD_VSYNC -> RS (also called Command/Data or CD or Address/Data; NOT reset)
LCD_HSYNC -> Write-bar
LCD_PCLK -> Read-bar
LCD_AC_ENB_CS -> Chip Select 0
LCD_MCLK -> Chip Select 1
The next step is to set up the multiplexers that choose what internal modules are connected to which external pins. The major file where the Beaglebone sets this up is ./<rest of the path>/git/arch/arm/mach-omap2/board-am335xevm.c. You'll see several examples of other modules setting up pins; I ended up doing it like this:
Code:
/* Module pin mux for SSD1289 */
static struct pinmux_config ssd1289_pin_mux[] = {
{"lcd_data0.lcd_data0", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT
| AM33XX_PULL_DISA},
{"lcd_data1.lcd_data1", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT
| AM33XX_PULL_DISA},
{"lcd_data2.lcd_data2", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT
| AM33XX_PULL_DISA},
[...lots more for lcd_data3-14...]
{"lcd_data15.lcd_data15", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT
| AM33XX_PULL_DISA},
{"gpmc_ad8.lcd_data16", OMAP_MUX_MODE7 | AM33XX_PIN_OUTPUT},
[...lots more for lcd_data17-22...]
{"gpmc_ad15.lcd_data23", OMAP_MUX_MODE7 | AM33XX_PIN_OUTPUT},
{"lcd_vsync.lcd_vsync", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT},
{"lcd_hsync.lcd_hsync", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT},
{"lcd_pclk.lcd_pclk", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT},
{"lcd_ac_bias_en.lcd_ac_bias_en", OMAP_MUX_MODE0 | AM33XX_PIN_OUTPUT},
{NULL, 0},
};
Notice that lcd_data0 through lcd_data15 are set to mux mode 0, which connects those pins to the LCDC module. lcd_data16 through lcd_data23 are used to create a 24-bit interface for 24-bit LCDs, but since the SSD1289 has a 16-bit data/address bus, I set those to mode7 so that they can be used as regular GPIO pins. The final four pins have different functions depending on whether you're using a Raster or LIDD LCD, but either way, you need to set the mux mode to connect them to the LCDC.
Of course, you also need to run some code to make that array do something useful. The function in question is setup_pin_mux(), and I use it like this:
Code:
#if defined(CONFIG_FB_SSD1289)
static void __init bbone_add_device_ssd1289(int evm_id, int profile)
{
pr_info("Initializing SSD1289 LCD driver...\n");
setup_pin_mux(ssd1289_pin_mux);
if (conf_disp_pll(300000000)) {
pr_info("Failed configure display PLL, not attempting to"
"register LCDC\n");
return;
}
if (am33xx_register_lcdc_lidd_dma(&ssd1289_device))
pr_info("Failed to register SSD1289 LCD device\n");
pr_info("Initialized SSD1289 LCD driver.\n");
pr_info("Registering PWM backlight for directly-connected LIDD LCD\n");
enable_ehrpwm1(0,0);
beaglebone_tsadcpins_free = 0;
}
#else
static void __init bbone_add_device_ssd1289(int evm_id, int profile) { while(0) {} }
#endif /* CONFIG_SSD1289 */
This is the code to initialize the LCD module, and it's a bit messy, but functional. If you're writing a non-DMA LCD driver, you can omit the am33xx_register_lcdc_lidd_dma() and instead directly call platform_register_device(). It seems that am33x_register_lcdc(), the raster version of the call, does some special device setup for DMA. It wasn't until I spent about 20 hours last weekend painfully debugging my DMA code that I discovered this was necessary. But more on this later.
You may note that the initialization function above references "ssd1289_device". If you're familiar with writing Linux device drivers, you're probably used to setting up the resources needed by each device. If you, like me, had never tried this before, then the following code might be new (but hopefully relatively understandable):
Code:
#if defined(CONFIG_FB_SSD1289)
#define LCD_BASE_ADDR 0x4830E000
static struct resource ssd1289_resources[] = {
[0] = {
.start = LCD_BASE_ADDR, //LCD BASE ADDR
.end = LCD_BASE_ADDR+SZ_4K-1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = AM33XX_IRQ_LCD, //LCD BASE ADDR
.end = AM33XX_IRQ_LCD,
.flags = IORESOURCE_IRQ,
}
};
static struct platform_device ssd1289_device = {
.name = "ssd1289",
.id = 0,
.num_resources = ARRAY_SIZE(ssd1289_resources),
.resource = ssd1289_resources,
};
#endif
Setting up the resources for a driver is, again, a relatively well-documented procedure, and if there's anything you don't understand, you (like me) can try to grok some of the extensive body of existing driver code that's out there. Let's look at initializing and using the AM335x LCDC in LIDD mode, with non-DMA commands.
Interfacing the TI's AM335x LCDC LIDD Mode in Non-DMA Mode
In non-DMA mode, the CPU is responsible for copying each frame of data over to the display when said frame is ready. The AM335x communicates with the LCD, generating the necessary waveforms and voltages on the LCD data/address bus pins to communicate with displays, while your driver need only feed in the 16-bit data and address words to be written (or to be read back out). In order to use the non-DMA LIDD mode of the LCDC module, I discovered through relatively painful trial-and-error that you must at least do the following steps:
- Fetch the MMIO address range from the device structure with the platform_get_resource() call.
- Request exclusive control over the MMIO address range with request_mem_region(); for the AM335x, this is 4KB starting at 0x4830E000. You will only use the lower 0x6C or so bytes.
- Map the memory into kernel virtual memory with ioremap().
- Enable the LCDC clock, which is actually more akin to flipping a power switch to turn on the LCDC module. You'll need to modify clock33xx_data.c to use <yourmodule>.c instead of da8xx-fb.c for &lcdc_fck, which is referenced on two lines of the file. If you modify clock33xx_data.c, you can use clk_get() to get the clock, and then clk_enable() to enable the clock (turn on the LCDC). Before you turn on the LCDC, if you try to access the MMIO registers, you'll get "Unhandled fault: external abort on non-linefetch (0x1028)".
- Now that the LCDC is on, you can check it's version by reading offset register offset 0x00. Value 0x4F200800 or 0x4F201000 means a Version 2 LCDC like the one in the AM335x, which has a more complex set of commands than the previous Version 1.
- Before you can send or receive any data or commands to and from the LCD itself, you have to enable a separate set of clocks, and set their multipliers. First turn just the LIDD clock on by writing LCD_V2_LIDD_CLK_EN to LCD_CLKC_ENABLE, offset 0x6C. You then set the desired divisor (I used x4) into LCD_CTRL, along with any other flags you want for that register (refer to the AM335x hardware manual for details). Finally, set the display type into LCD_LIDD_CTRL; for the SSD1289, it's an 8080-type, with no line inversion needed.
- At this point, you can properly communicate with the SSD1289 via the LCDC.
You will have to initialize the display's settings by alternately sending addresses (register offsets in the SSD's command memory) to LCD_LIDD_CS0_ADDR and data values to LCD_LIDD_CS0_DATA. You can see this done in ssd1289_setup() in my patch. When that is complete, you can create video memory for the display (which must be a multiple of the page size for your machine), then register a framebuffer. I was surprised to discover that even though the SSD1289 with a 16-bit display is used with FB_VISUAL_TRUECOLOR mode, you still have to create and reserve a pseudo_palette, otherwise your driver will inexplicably appear to hang the kernel deep within register_framebuffer().
Almost all of this initialization will take place in the ..._probe(struct platform_device *dev) function of your driver. Once you initialize the framebuffer, control will be turned over to the various framebuffer functions, which must at least including rectangle drawing (sys_fillrect), rectangle-copying (sys_copyarea), and 1-bit image blitting (sys_imageblit). The sys_ (instead of cfb_) versions of these functions work with a framebuffer stored in system RAM, rather than DMA'd to a device, so you may have to wrap those three functions in functions that call the system function and then manually copy the buffer to the screen. This is, needless to say, rather slow, which I discovered during my development. There's a method called deferred I/O that performs batching to minimize the overhead of re-copying the screen buffer to the screen itself, but I was unable to get it to work. I knew that there was an even better solution I could use, Direct Memory Access or DMA.
Interfacing the TI's AM335x LCDC LIDD Mode in DMA Mode
DMA mode means that the processor has separate hardware that can copy from RAM out to the display's driver and hence the LCD without the CPU itself having to mediate the copy. DMA is even worse-documented than the non-DMA LIDD mode. Although I already had a mostly-working framework for my driver from successfully writing it without DMA, it still took me two or three 12-hour days of work to get the DMA version functioning. The biggest problem turned out to be that with DMA, it becomes necessary to use omap_device_build() via am33xx_register_lcdc_lidd_dma(), a modified clone of am33xx_register_lcdc() that I wrote, which seems to enable one or more clocks necessary for proper DMA functionality. If you use a modified am33xx_register_lcdc() function that calls omap_device_build() (which will eventually reach your probe() method), then the following steps should be sufficient to get DMA running:
- Create an interrupt handler which will disable DMA, send the necessary commands to your video driver by writing to LCD_LIDD_CS0_ADDR and/or LCD_LIDD_CS0_DATA to reset the driver's internal memory cursor to the top-left corner, then re-enable DMA. This interrupt handler will be called each time the DMA module in the MPU completes a pass over the video buffer.
- Optionally, create a function that will read the LCD_LIDD_CTRL register in the LCDC, set or reset the DMA bit, and re-write the register. I called my version ssd1289_dma_setstatus().
- Perform the same initialization steps in ..._probe() as for non-DMA LIDD drivers. However, you'll need to turn on the DMA_CLK, the LIDD_CLK, and the CORE_CLK. You'll also have to request and register the IRQ (incidentally, it's IRQ #36) associated with the LCDC that will trigger your interrupt handler.
- Before you can register your framebuffer, you should set up DMA mode. Start by writing a 0 to LCD_DMA_CTRL_REG to make sure DMA is disabled. Then, configure the DMA burst size (which you might as well max out to 16) and FIFO threshold (0 will probably work). For the LCD_INT_ENABLE_SET_REG register, at least set LCD_V2_DONE_INT_ENA (bit 0). Finally, set LCD_DMA_FRM_BUF_BASE_ADDR_0_REG and LCD_DMA_FRM_BUF_CEILING_ADDR_0_REG.
- After that setup, you can request_irq(), enable DMA, and return from the probe function.
The key insight here is that if you don't go through omap_device_build(), then DMA will never truly begin. You'll be able to tell this is the case because about 10 seconds after power-on, you'll see another non-linefetch abort, this one with code 0x1018. You also cannot directly inject any data or address commands to the LCD driver while the DMA engine is active, so you must either put such commands between disabling and enabling DMA in the interrupt handler, or disable DMA and wait for the current frame to complete before trying to communicate directly with the display driver.
Concluding Remarks
I hope that you'll find this guide helpful in saving you from the many hours of frustrating testing, fruitless searching, and head-scratching confusion that I went through trying to write my driver. Needless to say, I've assumed that you've been able to find at least some information elsewhere, especially the TI Sitari AM3358 documentation. If there's anything that I omitted that you think I may be able to answer, feel free to post your question.
Download
SSD1289 Kernel Driver Patch for Beaglebone
Edit: Thanks to Hackaday for featuring this project!