Monday, 28 January 2013

Reading the OpenGL backbuffer to system memory

Sometimes you are in the need to read back the OpenGL backbuffer, or other framebuffers. In my case the question was how to read back a downsampled framebuffer resp. its texture to system memory. There are different methods for this and I wrote a small benchmark to test them on various systems.

UPDATE #1: A thing I've totally overlooked is that you can actually blit the backbuffer to the downsampled framebuffer directly, saving you the FBO overhead (memory, setup, rendering time). I've updated the page and the code reflecting that.
UPDATE #2: The code now works on Windows and Linux (via GLX). Maybe I'll port it to GLES2 for the Raspberry Pi too... :)
UPDATE #3: The code works on the Raspberry Pi now using OpenGL ES 2.0. Reading the framebuffer is quite slow though even with an overclocked device. It's not the actual reading that is slow, but that we have to render to another framebuffer first and back to the screen. Atm all buffers and textures use RGBA8888, which might slow things down. I'll try RGB565 in the future. Also there are no PBOs and no glGetTexImage(), so only the glReadPixels method is working... If you have any ideas on how to make the code faster, I'd love to hear them.
UPDATE #4: Tinkered around with color formats a bit and updated the Pi's firmware and MESA implementation. I've tried changing the FBOs color format to 16bit, but that didn't change much. The secret seems to lie in using a RGB565 EGL display/framebuffer format with no alpha, no depth, no stencil and to remove the glDiscardFramebuffer calls. The FBO backbuffer and the downsampled FBO still have RGBA8888. Together with using a screen size of 640x480 this nearly quadrupled the frame rate. Blitting the FBO to the screen now seems to be much, much faster... I updated the code accordingly and also did some cleanup and found a safer way to get function addresses on Linux systems.

Prequisites

I needed to downsample the framebuffer. This can be done by blitting to a smaller framebuffer that will be read back. For setting up framebuffers, see here. In your rendering loop, do:

// set viewport to window size
glViewport(0, 0, width, height);

// draw stuff here

// blit backbuffer to downsampled buffer
context->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
context->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, smallId);
context->glBlitFramebuffer(0, 0, width, height, 0, 0, smallWidth, smallHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);


// read back buffer content here (Method #1, #2, #3)
// unbind again
glBindFramebuffer(GL_FRAMEBUFFER, 0);


Blitting the framebuffer using glBlitFramebuffer isn't much faster than binding a texture and rendering a quad to another framebuffer, but it has much less setup needed and you don't find yourself setting up projection or modelview matrices and trashing the state.

Method #1 - glReadPixels

The first method is to use a standard glReadPixels. This is the common way to read the backbuffer since ancient OpenGL times. Do (depending on you framebuffer format):

// bind downsampled buffer for reading
glBindFramebuffer(GL_FRAMEBUFFER, smallId);

glReadPixels(0, 0, smallWidth, smallHeight, GL_RGBA, GL_UNSIGNED_BYTE, downsampleData);
// unbind buffer again
glBindFramebuffer(GL_FRAMEBUFFER, 0);

The data will end up in downsampleData in system memory. Note that you need to allocate space for that data before!

Method #2 - glGetTexImage

The second method does not read the actual buffer, but the texture attached to it. That can be done using glGetTexImage. Do (depending on you framebuffer format):

// bind downsampled texture
glBindTexture(GL_TEXTURE_2D, downsampleTextureId);

// read from bound texture to CPU
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, downsampleData);
// unbind texture again
glBindTexture(GL_TEXTURE_2D, 0);


The data will end up in downsampleData in system memory again. Note that you need to allocate space for that data before!

Method #3 - PixelBufferObjects

This is the most complicated, but a slightly faster method than the previous two. You read the downsampled framebuffer using glReadPixels again, but now you read it to a PixelBuffer object asynchronously. For that you set up two PBOs and alternate between them so the GPU can go on rendering to the other while you process the first. The download of the data to system memory happens using DMA and does not block the CPU. To do this, first start up creating two PBOs:

int readIndex = 0;
int writeIndex = 1;
GLuint pbo[2];


// create PBOs to hold the data. this allocates memory for them too
glGenBuffers(2, pbo);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[0]);
glBufferData(GL_PIXEL_PACK_BUFFER, smallWidth, smallHeight * smallDepth, 0, GL_STREAM_READ);

glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[1]);
glBufferData(GL_PIXEL_PACK_BUFFER, smallWidth, smallHeight * smallDepth, 0, GL_STREAM_READ);
// unbind buffers for now
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);


Now that you have created the PBOs, go to your rendering loop and there you read back data to the PBOs there.

// bind downsampled fbo
downsample->bind();
// swap PBOs each frame
writeIndex = (writeIndex + 1) % 2;
readIndex = (writeIndex + 1) % 2;
// bind PBO to read pixels. This buffer is being copied from GPU to CPU memory
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[writeIndex]);
// copy from framebuffer to PBO asynchronously. it will be ready in the NEXT frame
glReadPixels(0, 0, downsample->getWidth(), downsample->getHeight(), GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
// now read other PBO which should be already in CPU memory
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[readIndex]);
// map buffer so we can access it
downsampleData = (unsigned char *)glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
if (downsampleData) {
    // ok. we have the data now available and could process it or copy it somewhere
    // ...
    // unmap the buffer again
    downsampleData = nullptr;
    glUnmapBuffer(GL_PIXEL_PACK_BUFFER);

}
// back to conventional pixel operation
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
// unbind downsampled fbo
downsample->unbind();
// now in the next readIndex and writeIndex will be swapped and we'll read from what is currently writeIndex...

The data will end up in downsampleData in system memory. This method seems to be slightly faster on some systems, but gain is highly dependent on the frame rate.

Results

The results depend on the system, method used and read color format. Here's a table showing my benchmarking results. The first column is regular rendering to the backbuffer, no FBOs involved. Then follow the tests, every test first without actually reading the buffer, just rendering to the downsampled FBO. Then two tests reading the downsampled buffer to system memory using different formats:
SystemNo FBOs#1 no reading#1 BGRA#1 RGBA#2 no reading#2 BGRA#2 RGBA#3 no reading#3 BGRA#3 RGBA
Intel Core i7 960
Nvidia Quadro 600, Driver 310.90
OpenGL 4.3,
640x480
0.390.540.700.670.540.880.870.550.640.55
Intel Core i5 480M
AMD Radeon 5650M, Driver Catalyst 13.1
OpenGL 4.2,
640x480
0.300.410.800.770.410.810.810.420.510.49
Intel Core i5 480M
Integrated HD Graphics, Driver 8.15.10.2827
OpenGL 2.1,
640x480
0.730.761.751.520.785.262.560.791.771.70
Windows 7 x64, frame time in ms, vsync off, data size 160x90x32bit

SystemNo FBOs#1 no reading#1 BGRA#1 RGBA#2 no reading#2 BGRA#2 RGBA#3 no reading#3 BGRA#3 RGBA
Intel Core i5 480M
Integrated HD Graphics, Driver Mesa DRI Intel Ironlake Mobile
OpenGL 2.1, GLX 1.4, MESA 9.0.2,
640x480
1.131.463.2516.911.973.4016.771.894.2217.05
Intel Core i3-2120T
Integrated HD Graphics 2000, Driver Mesa DRI Intel Sandybridge Desktop x86/MMX/SSE2
OpenGL 3.0, GLX 1.4, MESA 9.0.2,
640x480
0.781.141.301.771.131.291.551.131.301.77
  Ubuntu 12.10 x64, frame time in ms, vsync off ("export vblank_mode=0"), data size 160x90x32bit

SystemNo FBOs#1 no reading#1 BGRA#1 RGBA#2 no reading#2 BGRA#2 RGBA#3 no reading#3 BGRA#3 RGBA
Intel Core i5 480M
Integrated HD Graphics, Driver Mesa DRI Intel Ironlake Mobile
OpenGL 2.1, GLX 1.4, MESA 11.2.0,
640x480
1.081.041.481.421.031.441.421.041.071.07
  Ubuntu 16.04 x64, frame time in ms, vsync off ("export vblank_mode=0"), data size 160x90x32bit

SystemNo FBOs#1 no reading#1 BGRA#1 RGBA#2 no reading#2 BGRA#2 RGBA#3 no reading#3 BGRA#3 RGBA
Raspberry Pi
OpenGL ES 2.0, EGL 1.4,
640x480
2.374.494.546.25------
  Raspian (Updates: March 25th, 2013), frame time in ms, vsync off (eglSwapInterval(0)), data size 160x90x32bit

Source code

The source code, a CMake build file and the benchmark results can be found on GitHub and building should work at least on Windows 7, Ubuntu 12.10 (via GLX on X11) and on a current Raspian for the Raspberry Pi (via EGL). The beef is in the Test_... classes. To do a benchmark, run the application from the command line and wait for it to finish. It'll spit out some frame time values for the different methods and color formats.
I'd love to get your feedback on how else to read to system memory or maybe speed up the methods. Just drop me a comment.

Sunday, 13 January 2013

Using an USB joystick or gamepad in XBMC on Ubuntu

Now I have those cheap USB gamepads, XBMC running, the ROM collection browser addon and MAME all set up. Awesome! But why can't I navigate XBMC with my joystick to select games and adjust the volume etc?! Here's how to solve that problem...

1. Prequisites

First of all you need joystick support and some tools. Go ahead and install the joystick package in Ubuntu with "sudo apt-get install joystick". That gives you joystick tools like jstest and jscal which we'll need later.
Run "cat /proc/bus/input/devices" which shows you a list of all input devices your system has. Locate the entry describing your joystick and write down or copy the exact string after "NAME=...", including all spaces etc.! We'll need that later. The last string of the line "Handlers=..." should read jsX with X corresponding to the number of the joystick.
Now start the tool jstest with the joystick you want to use, like so: "jstest /dev/input/js0". It will show you the states of all of the buttons and axes of js0 and you can see them change when you use the joystick. Find out which indexes correspond to which axis or button.

2. XBMC keymap files

XBMC uses keymap xml files to connect input device "keys" to XBMC functions. We need to create a keymap file for our joystick. Create a new keymap file in your users keymap folder using an editor of you choice and name it after your joysticks name, e.g. "nano ~/.xbmc/userdata/keymaps/joystick.SOMENAME.xml".
The file starts with <keymap>, then <global> and then you start a joystick entry:

<keymap>
   <global>
      <joystick name="NAME_OF_YOUR_JOYSTICK">

Then follows the button/axis configuration. For a button add an entry:
<button id="BUTTON_ID">NAME_OF_XBMC_ACTION</button>
for an axis add:
<axis id="AXIS_ID" limit="VALUE_LIMIT">NAME_OF_XBMC_ACTION</axis>
and for a D-PAD or hat add:
<had id="HAT_ID" position="DIRECTION">"NAME_OF_XBMC_ACTION</hat>

BUTTON_ID, AXIS_ID and HAT_ID correspond to the index you got from jstest above +1, as jstests indices start as 0, but XBMC seems to start from 1...
VALUE_LIMIT is the limit that needs to be reached so XBMC activates that function. The default value of an axis is usually 0, so use "+1" and "-1" for the different directions of the axis.
DIRECTION is the direction of the D-PAD or hat and takes strings like "left", "right", "up" or "down".
NAME_OF_XBMC_ACTION is the function that should be executed in XBMC. A list of possible actions can be found here. An example for that section could be:

         <button id="3">Select</button>
         <axis id="1" limit="-1">AnalogSeekBack</axis>
         <hat id="1" direction="left">Left</hat>

 Finish the file with closing all sections again:

      </joystick>
   </global>
</keymap>

Save the file (here's my keymap file as reference) and start XBMC. You gamepad should work now. Enjoy.

Saturday, 12 January 2013

Calculate Catmull-Rom splines using forward differencing - UPDATE

I finally had the time to finish this post by whipping up a small JavaScript canvas example to show forward differencing in action - Here it is. If it doesn't seem to work try to view the page on its own, there's some issue with the onload() event...

Regular Forward differencing

Not much to see there, actually, other than that the regularly drawn spline and the one drawn with forward differencing look the same. Here's the part of the code that does the forward differencing calculation for one section of the spline:

ctx.moveTo(points[1][0], points[1][1]);
//calculate values at point a and b
var ax = points[3][0] - 3.0 * points[2][0] + 3.0 * points[1][0] - points[0][0];
var ay = points[3][1] - 3.0 * points[2][1] + 3.0 * points[1][1] - points[0][1];
var bx = 2.0 * points[0][0] - 5.0 * points[1][0] + 4.0 * points[2][0] - points[3][0];
var by = 2.0 * points[0][1] - 5.0 * points[1][1] + 4.0 * points[2][1] - points[3][1];
//calculate 1st, 2nd, 3rd derivative between a and b
var stepsize = 1.0 / subdivision;
var stepsize2 = stepsize * stepsize;
var stepsize3 = stepsize * stepsize2;
var dx =  0.5 * stepsize3 * ax + 0.5 * stepsize2 * bx + 0.5 * stepsize * (points[2][0] - points[0][0]);
var dy =  0.5 * stepsize3 * ay + 0.5 * stepsize2 * by + 0.5 * stepsize * (points[2][1] - points[0][1]);
var d2x = 3.0 * stepsize3 * ax +       stepsize2 * bx;
var d2y = 3.0 * stepsize3 * ay +       stepsize2 * by;
var d3x = 3.0 * stepsize3 * ax;
var d3y = 3.0 * stepsize3 * ay;
//calculate points while updating derivatives
var px = points[1][0];
var py = points[1][1];
for (var j = 0; j < subdivision; j++) {
    px += dx; dx += d2x; d2x += d3x;
    py += dy; dy += d2y; d2y += d3y;
    ctx.lineTo(px, py);
}


A nice addition would be to do adaptive subdivision of the spline, depending on curvature, but I haven't gotten that right yet...

Thursday, 10 January 2013

Setting up an Arduino + LPD8806 ambilight using boblight with XBMC on Ubuntu

An Ambilight is a great thing - if it works. I want to write about some of the hoops I had to jump through to make it work. This is very Linux-specific, but you might take away something for other configurations too... Here is the original adalight tutorial, which I used to get this working and it is really worth reading.
I have a bit of a different setup here though, consisting of a regular desktop PC with a COM port, an Arduino Pro (no USB) with an ATmega328 and 40 RGB-LEDs driven by the LPD8806 chipset.


1. LPD8806 + Arduino - Setup

The first thing is to set up the Arduino to use the LPD8806 LED strip (translated datasheet). Here's a great tutorial on how to use it. I'll concentrate on the setup I mentioned above.
The strips/chips run at 2.7-5.5V and use the SPI protocol. The Arduino is great for this task because it has SPI hardware so you don't have to bit-bang the GPIO pins, saving you lots of processor cycles.

Take a look at your LED strip. You'll need to solder wires to all the connections on the input end of the strip, namely the DI, CI, GND and 5V connectors. NOTE: The DO and CO connectors are for output and pass signals on to the next strip/segment, so leave them alone unless you want to connect additional strips/segments. Now connect the DI line to Arduino pin 11 and the CI line to Arduino pin 13 to use hardware SPI and connect GND to GND on the Arduino.

One segment of the LED strip (Picture from Adafruit, Attribution-ShareAlike Creative Commons)

It makes sense to use only a few strip segments first for a test. If you have only ~5-6 strip segments, connect 5V to the 5V line of your Arduino.
If you have more than that your Arduino can not power the strip directly from the 5V line (forget the 3.3V line btw), depending on how you've connected your Arduino. The LEDs can draw ~60mA each on full white, so a strip segment can draw ~120mA. 6*0,12A = 0,72A + driver chips + the Arduino running. The regulator on the Arduino is rated for 1A only! Thus you need to connect a 5V power supply to the Arduino power jack and connect 5V on the strip to Vin on the Arduino.

To supply power from the PC I used power connectors from the PC power supply. I wanted to be able to detach the whole thing easily, so I ripped appart an extension cable and "epoxied" it to a slot cover. There the serial cable (an old serial mouse cable) and the power cable to the Arduino can be connected.

2. Arduino + PC - COM port level shifter

If you have an Arduino that features an USB port you can skip this section. Else you might want to read on.

For my setup I needed a RS232 to TTL level converter. Regular PC/RS232 serial port voltage levels are +/-12V, but the Arduino runs at 3.3 or 5V and thus you'll fry it if you connect it directly. I had a MAX3222 (other datasheet here) level shifter IC lying around, so I used that on a piece of prototype board. Here's the basic schematic. The capacitor values should be ok for 3.0-5.5V applications. For similar ICs the pin numbers may change or there are no EN/ or SHDN/ pins, but that should be easy to figure out. I additionally connected the RST pin, but you could probably also omit that.


Here's what my finished prototype board looks like. The power supply for the Arduino and the LEDs is on the right side.


3. LPD8806 + Arduino - Software

Once you have your connections set up and power supply figured out, you'll need to program the Arduino to drive the LED strip. Code for this can be downloaded from github, including instructions on how to install it in the Arduino IDE. Once you've done that, connect your Arduino to the PC, start the IDE and load an example sketch, e.g. "strandtest". Make sure to comment the line "LPD8806 strip = LPD8806(nLEDs, dataPin, clockPin);" and uncomment the line "LPD8806 strip = LPD8806(nLEDs);" instead, so hardware SPI is used. Also adjust "int nLEDs = 32;" to your strip length. After uploading the sketch your strip should run some tests and you can see if it works ok.

4. Arduino + PC - LEDstream

Now you need code to receive the LED data from the PC. This is done via a sketch called LEDstream_LPD8806 that is available on the Adalight github page. Download it and program it onto your Arduino using the IDE.

5. Arduino + PC - boblight

Boblight is a software that grabs parts of your screen, processes them to your needs and then sends them to boblight clients over a COM port or the network in various formats. We need to download and compile boblight from the source:
  • Change to your home directory: "cd ~"
  • Download boblight using Subversion:"svn checkout http://boblight.googlecode.com/svn/trunk/ boblight-read-only"
  • Change to the directory: "cd boblight-read-only/"
  • Configure it: "./configure --without-portaudio --without-opengl --without-x11 --prefix=/usr"
  • Compile it: "make"
  • Install it: "sudo make install"
If there were no errors, boblight should be usable now.

6. Arduino + boblight - Communication setup

The LEDstream sketch running on the Arduino needs to receive the LED data in a specific format, so so we need to configure boblight to send data in that format. Locate your boblight.conf file. It should reside in "/etc/boblight.conf" on Ubuntu. Open it using an editor of your choice, e.g. nano or gedit: "sudo nano /etc/boblight.conf". Your [device] section should look like this:
  • Chose any name for your ambilight e.g. "name ambilight1".
  • The device type should be "type momo".
  • Output defines where the ambilight is connected. COM1 is /dev/ttyS0, an USB-COM interface could reside under /dev/ttyUSB0 or /dev/ttyACM0 instead. Use "ls /dev/tty*" to find out what serial ports you have.
  • Channels is the number of LEDs times the number of colors, 3*40 = 120.
  • The initialization sequence for your LEDstream program is "Ada" (416461h) this the first data sent. Then follow 2 bytes containing the number of LEDs and one byte of checksum for those bytes. The checksum is calculated by XORing the two bytes and 55h. In my case the three bytes are "00 28 7d" (40 LEDs = 0028h) 00^28^55 = 7d.
  • Interval specifies the update interval in microseconds. "interval 20000" means 50 updates per second and should be sufficient.
  • Then follows the baudrate of the com port. Use "rate 115200", the highest possible baud rate.
A good explanation of all boblight.conf parameters can be found here.

[device]
name ambilight1
type momo
output /dev/ttyS0
channels 120
prefix 41 64 61 00 28 7d
interval 20000
rate 115200
delayafteropen 1000000

7. Arduino + boblight - Color setup

I had a lot of problems with the "calibration" of the LEDs. Hue wasn't always correct and black was displayed as white, because everything was much, much to bright. I ended up with a rather strange color setup that is working for me(tm), but ymmv...

[color]
name red
rgb FF0000
gamma 2.1
adjust 0.12
blacklevel 0.01

[color]
namegreen
rgb 00FF00
gamma 2.1
adjust 0.12
blacklevel 0.01

[color]
name blue
rgb 0000FF
gamma 2.2
adjust 0.1
blacklevel 0.01

8. Arduino + boblight - Screen grabbing setup

The boblight deamon boblightd grabs regions of the screen, calculates the average of a region and attributes that data to a certain pixel. This needs to be set up properly so your LEDs reflect the correct parts of the screen. The settings depend on how you've laid out your backlight. Here's mine:


 Open your boblight.conf again. The logic for setting up an LED works like this:
  • Every single light/LED starts with "[light]"
  • Then follows its name e.g. "left1" "left2", "top1", ...
  • Then the colors (see section 7 above) sent to the device for it:
  • "color COLOR DEVICE INDEX" e.g. "color red ambilight1 1", "color green ambilight1 2", ...
  • Next the hscan parameters specifies where the screen capture region starts and ends. The value is in per cent of the screen, starting at the left border. A value of "hscan 75 100" would thus capture a region starting at 75% of the screen up to the end of the screen. It ist best not to make the regions extend into the screen too much (max. ~5-15%)
  • The vscan parameters works similar.
A complete section of light setup can be seen in the boblight wiki. I did my configuration by hand, but there's a configuration script and a windows tool available that can maybe save you some work.
Here's my boblight.conf as a reference.

Once you've set up boblight you can test it out. Open a terminal and start the boblight deamon with "boblightd" and watch the output. It should connect to the Arduino ok and display no errors. Open a second terminal and use the tool boblight-constant with a color you want to set, e.g. "boblight-constant FF0000" (red). There's also a tool called colorswirl in the adalight code base you could give a try. If you want to adjust settings in boblight.conf, kill the boblightd process in the first terminal, adjust the settings and restart it. Experiment until you like your colors, brightness etc.

9. boblight + XBMC - Startup

The boblight deamon needs to be available when the system resp. XBMC starts up. It makes sense to activate it upon system boot. You can do this by editing your /etc/rc.local as root, e.g. "sudo nano /etc/rc.local". Add the line "boblightd -f" (runs boblightd in the background) somewhere in the lower part.

10. boblight + XBMC - Addon

Make sure boblightd is running either by rebooting or running "boblight -f". To make boblight work in XBMC you need the addon, which you can download from within XBMC. Go to settings and enable it. Now your boblight should turn on whenever you play a video. Here's a good test video to see if all LEDs and regions work correctly, but I also use Darwin Deez - Radar detector as a test video a lot ;)
Should you need to adjust settings in boblight.conf you will have to restart boblightd.  It is running in the background though and you'll have to kill it first. Use "ps -A | grep boblightd" to find its process id, then kill the process with "sudo kill PROCESS_ID" using whatever process id the previous command told you. Then you can edit boblight.conf and afterwards restart boblightd with "boblightd -f".

11. Profit ;)

Enjoy your Ambilight!

Adding a "proper" keyboard backlight to an Acer Aspire 3820TG notebook

This thing started long ago when I got the laptop. I love it, but it really lacks a backlit keyboard. There was one excellent thread with people presenting solutions (add tiny switches, soldering to the bluetooth connector, to the USB breakout board etc.), but I found none of them really satisfying. I decided to void my warranty, build a circuit and connect it to the useless „P“-Button already available. After all: „If you can't open it, you don't own it!“ - MAKE: Magazine ;)



A friendly warning: This will void your warranty - all the way. You'll have to completely take apart your laptop, solder tiny SMD components to PCBs and hair-thin cables to your mainboard, cut traces and apply hot glue everywhere. Continue only if you know what you're doing! I take no responsibility whatsoever. There be dragons...

What you'll get:
  • A backlit keyboard
  • Brightness controlled by the „P“-button in 5 steps (from off->brighter...-> to off)
  • Backlight turns off when LCD backlight goes off
  • Last setting is restored when backlight goes on again
What you'll need:
  • The actual backlit keyboard (search for keyboards for the Acer Aspire 3810 model)
  • A custom PCB (EAGLE files available)
  • SMD components (PIC 10F202T, N-channel FET BSS138 or 2N7002, 3x10k resistors 1206 types)
  • A PIC programmer (code + binary available)
  • Really thin wires
  • Low-watt soldering iron with a tiny tip and SMD soldering experience
  • „Carpet“/exacto knife
  • Hot glue gun
The keyboard I got from Ebay was ~30€. You can see the backlight LED supply cables already attached. The backlight is not very bright, but totally sufficient for me.

The keyboard I got from Ebay

1. Build the circuit board

First step is to build the circuit board. It is controlled by a PIC10F202T, a tiny 6-pin MCU running at 4MHz. This is the schematic:


The PIC controls a FET that is able to switch the current needed for the backlight LEDs. The brightness is controlled by PWM . The FET should have a low RON-resistance. I used a BSS138, but a 2N7002 might actually be a better choice. The PIC input pins are connected to the „P“-Button (P_BUTTON) for switching through backlight brightness levels (switches to GND) and to the LCD display „backlight on“ pin (BL_ON). That one provides 3,3V while the LCD backlight is on.
You can see the PCB for the schematics at the right side. I provided solder pads for connecting a PIC programmer (Vpp, D, C, …). You can cut that part off after you've tested and are satisfied with the operation of the circuit and then install the thing into the laptop. You have to remove the excess parts of the circuit board to make it fit one of the openings below the keyboard.
You'll need to use a PIC programmer to write code onto the PIC. I used USBPICPROG, a cheap (~25€), open source programmer, with free software. There is a „3820_BL.production.hex“ file containing the binary under „/dist/default/production/“ in the ZIP. There's also source code provided in case you'd want to change functionality. You can compile it with the excellent, free PIC development system MPLAB X from Microchip (http://www.microchip.com/mplabx/).
This is more or less what it looks like when it is assembled and has wires connected (older revision btw.). You can see that it's pretty small...


2. Disassemble the laptop

After you've assembled, programmed and tested the circuit board you've got to solder wires to some points on the laptop mainboard.
First disassemble the laptop. Remove the keyboard using a screwdriver to carefully push the plastic pieces securing the keyboard in place back so you can lift the keyboard. There are 5 of them at the top part of the keyboard. Be careful with the ribbon cable below! Then remove all screws located below the keyboard, flip the laptop around and remove all screws at the bottom too. There are plenty. After opening the „hatch“ at the bottom remove the harddisk (left), the RAM (center), and carefully remove the WiFi and 3G card (right part). Be sure to take a photo of which antenna cable goes where (blue, yellow, black, white). Here's what mine looks like:


Use a thin piece of plastic to remove the front and back part of the case. When removing the front part of the case be careful to first detach all ribbon cables.

3. Modify the P-Button breakout board

The front part of the case has the breakout board with the RJ45 LAN-connector attached to it at the left side. Remove the screw, take it out, turn it around and peel the labels off. Notice switch SW3. That's the „P“-button. When pressed it connects the line to GND, but we want it to change its functionality, so you've got to cut the trace connecting it to the mainboard. That trace leads from the SW3 to the little solder spot just below the label „R11“ and then on to the connector that goes to the mainboard. That's where you want to cut it using a sharp „carpet“/exacto knife (see picture).
Solder a really thin wire to the top left connector of SW3 (see picture) and fix the wire to the board with some hot glue. After that you may put the breakout board back in place.



4. Power supply

We need a supply voltage for the whole thing which we can find at the pins of U28, located on the front of the mainboard at the lower left. That chip is a switch to toggle the USB ports at the left side of the laptop on and off.

We want permanent power when the laptop is on, so we connect the 5V pin of our circuit to pin 1 of U28 (pin 1, see picture). GND can be connected to the big solder pad between „RTC“ and „EC93“ (see picture) or anywhere else you find GND. Solder the wires and again fix them with some hot glue.


5. The BL_ON pin

Now it becomes a bit tricky. Take a look at the front side of mainboard and locate U17, the LPC controller (square, 128 pins, located at lower center). This chip controls the LCD backlight through pin 27 (BLON_OUT) which is connected to pin 5 of the LCD connector.


But both places are not solderable without bridging pins. Just above pin 27 is a via (connection to another layer of the mainboard). It is the left one of two vias close to each other (one over pin 25, one over pin 27, see picture). Use a „carpet“/exacto knife to CAREFULLY scratch off the lacquer until you see a copper „ring“. Dont scratch off too much, otherwise you might break the via and your LCD backlight will be gone... Once you've got a solderable spot, solder a wire that will be connected to BL_ON on our circuit board. Again, fix it with some hot glue, best directly where you've soldered it to the mainboard.



6. Putting it back together

That was the hard part. Now re-assemble the laptop again while making sure all wires end up coming out of the lower left opening in the front case below the keyboard (see picture below). There is space for the circuit board too.

7. Install the keyboard

You might need to prepare your keyboard so it fits in place of the old one. It will probably be a bit too thick, so there's two things you need to do. First bend down the „noses“ at the lower part of the key board ~10° using some universal pliers or whatever. Bend them until the keyboard fits nicely...
Then you'll have to file down (or use a „carpet“/exacto knife) the 7 „dents“ at the left, right and at the top of the keyboard by a good amount (~1-2mm) so the plastic securing pins can snap in place. Again you'll have to try what works in your case...
After the keyboard fits solder all the wires from the mainboard to the circuit board. Also solder the wires from the keyboard backlight to the circuit board. Try if the keyboard backlight control works. You don't have to boot up. Just enter the BIOS or something. Try pressing „Fn“+“F6“ to turn the LCD backlight off.
When all's fine wrap the circuit board in some tape and place it in the lower left opening below the keyboard, then put the keyboard back in place.


Hooray! You're done. Enjoy your keyboard backlight.

References:

Here are some docs for the Acer 3820T(G), which is based on the JM31-CP mainboard, that you might find useful:
Processor upgrade directions
JM31-CP schematics
Acer Aspire 3820T(G) service guide