Wednesday, June 24, 2020

PCMON (LED-display)

Did you know that the 32x16 dot Red/Green LED display from eBay is almost exactly the same size as two 5.25" half-height drive bays? Well now you know! One day I happened to look at this display that I've had on my table for maybe a year (not connected to anything) and thought that the size could be similar. The reason why I thought about that is that I do have a full-size PC next to my TV for playing games. This means that the PC is quite far away from the viewer and this kind of low resolution display would be quite perfect for PC monitoring system explained in the previous post. Especially when the display can be embedded into the PC case and will not be an external entity. To be honest the display is a little too low resolution and the text can be read from quite far away.. But it will do for this project.

Unfortunately they are not exactly the same size. The display is 152x76 mm (ish) and two 5.25" half-height drive bays (so that's the full-size 5.25" drive bay apparently, honestly didn't know that) are 146x82 mm. This means that the display is a few millimetres wider than the drive bay and somewhat lower. Filling the vertical gap is easy, but the width is a problem. It's fortunately not such a big problem, since the hole for the drive can be even bigger than this. I've measured that the display (LED matrix elements) are only 3 mm wider than the hole in my trusty Nexus Breeze case. So this requires a little bit of very accurate sanding...

The Display

The 32x16 dot Red/Green LED display module is quite simple. It contains 8 8x8 Red/Green LED matrices and some control logic. There are two decoder chips (74HC138D), 16 transistors (one for each line), 8 shift registers (74HC595D), ceramic capacitors (one for each chip) and one electrolytic capacitor. It's quite important to note, that there are no resistors on this board. So there is absolutely nothing limiting the current that goes to the LEDs. This causes some problems that I will explain later. The interface of the display contains 4-bit input for the line selection, two serial inputs for the red and green colours, one input clock for the serial line, output enable and "strobe" to move the serial data to the output of the shift register. The strobe pin allows the device to load the picture data while the previous one is still being displayed. It's also good to note that the output enable pin is active low. This means that this pin should be pulled high for example when the driver board is being reprogrammed.

From this explanation it should be clear that the display will require some kind of driver board and cannot be directly connected to the PC (well perhaps with a parallel port, but let's not go there). However the driver board is easy to make with an AVR microcontroller (an Arduino if you like). The advantage of this display is that it's quite inexpensive in my opinion. Another obvious advantage is that it fits this purpose very well. It has great contrast, it's not too complicated and it seems to be almost perfect in size.

Display driver board

This is a glorious subtitle. However I'm not going present here the driver board that I'm currently using, because it's one of my first designs and it's not very good. However driving the LED display is very easy. According to the previous paragraph, driving the display requires the following steps:
  1. Shift in data of line n (using SPI hardware for example)
  2. Disable output (pull OE high)
  3. Strobe the data in (pull STR high)
  4. Enable the line n (using ABCD inputs)
  5. Enable output (pull OE low)
  6. Wait for some time so that the user can actually see the output
  7. n++ and repeat
For best results, this "loop" should be run in a timer interrupt so that the update intervals are consistent and the driver can do other things rest of the time. The idea is that some line of the display is switched on most of the time and the update takes as little time as possible (for maximum brightness). Another advantage of using a timer is that the same timer could be used to control the brightness. Timers usually have multiple channels for interrupts. The second interrupt could be set to only disable the output of the display and this would happen to each and every line at the same time. This would effectively mean that the display would be less bright like using PWM. Obviously the updating should be done fast enough that the flickering could not be seen by a human eye.

Now one thing that I failed in my driver is having an external pull-up for the OE-pin. Since the driver is in reset while being reprogrammed, the display is on and it's constantly displaying the same line. Now since the ABCD inputs are all low, this means that the uppermost line is switched on. So this line is displaying whatever data is still stored in the shift registers (since it's not emptied or reset in any way). This actually caused the uppermost line to stop working.. I haven't really tried to debug this issue, but I would assume that the respective transistor failed.

Another thing to note in this implementation is that the Red and Green colours have separate inputs, but I did not mention that in this example. Luckily the LED display is designed so that you can daisy-chain multiple displays. So it has the above mentioned inputs and respective outputs. I didn't want to drive the two serial inputs separately, so I simply connected the output of the Green serial line to the input of the Red one. This means that the displays are kind of next to each other. This can be thought of having 64x16 dot display so that the left half of the display is green and the right half is red. The colours are of course not physically next to each other so the pixel at index 0 and the pixel at index 32 are in the same location.

Design

So first of all one has to have some vision of what one wants to do. Since I decided to use this 32x16 dot display, the result has to be quite low resolution. Using 5x8 pixel font the display could fit only around 10 characters. This won't do. Using a smaller 3x5 pixel font makes a little bit more sense. Using this font we can still fit only 2 lines of text since there has to be a gap between lines. Since I wanted to fit as much (readable) information on the display as possible, I've decided to make three lines of text without gaps between them. The text should however be interleaved so that it's still possible to read. The rest of the space could be used for bar graphs. The design that I've imagined is shown below. It's also adjusted so that the upper line is unused since it doesn't work in my display..


The display is designed to represent CPU, memory and network utilisation with graph bars. Since the display has two colours, it can naturally display more colours by mixing these two. It is used here to represent orange and red at absolutely imaginary values of 60% and 80% (to have a cool effect). The orange colour is created simply by switching on both the green and red LEDs. It happens quite nicely so that the bars are exactly 20 pixels long which means that every pixel represents 5%. This simplifies the maths quite a lot..

Now there is at least one problem with this design. It is caused by the lack of resistors in the display as mentioned above. This causes the lines of the display with lots of active pixels to appear dimmer than the ones with less active pixels. When the CPU bar is full, the upper pixels of the CPU text are very noticeably brighter than the ones in the middle of the text. The first solution to this problem was to use a less solid bar. I've decided to show only every second pixel which can be seen from the picture later on. Unfortunately this is not quite enough and I will present a solution to this problem later on.

Programming

The program in this project is quite simple:
  1. Wait for the data from the UART block
  2. Update the picture buffer according to the input
  3. Repeat
Basically there are three interrupt handlers in this program. One is to receive the input data one character at a time and two timer interrupts to control the display. One timer interrupt is to output one line of data at a time and another to switch off the display for brightness control. Receiving the data is quite simple. All that is needed is an endless loop to receive one character at a time. The characters are stored in a temporary value until the '\n' is received. Then the '\n' is replaced with an '\0' and an atoi() function is called on the value. The value is then stored to a corresponding variable according to the first character. I've decided to implement it so, that the display is updated when a character U is sent to the device (with the termination character '\n' of course), and that's when the above mentioned endless loop should end until the display is updated and the loop starts over. This relies on the fact that no new data will be sent before the display routine has finished. Here's an example:

while(1) {
if(uart_rx_read_ptr != uart_rx_write_ptr) {
data_buf[data_ind++] = uart_rx_buf[uart_rx_read_ptr++];

if(data_buf[data_ind-1] == '\n'){
data_buf[data_ind-1] = '\0'; // Terminate properly
data_ind = 0; // Reset incoming data index

if(data_buf[0] == 'C')
cpu = atoi(data_buf+1)/5;

if(data_buf[0] == 'U')
break;
}
}
}

Updating of the display is also quite easy. First the picture buffer should be cleared for simplicity sake and then the new picture should be formed. First the text and then the bars. So first clean the buffer:

uint8_t update_buffer[SCREEN_SIZE/8];
memset(update_buffer, 0, SCREEN_SIZE/8);

Then add texts to respective locations (first two letters as an example, starting from line one):

update_buffer[1*8]  = 0b01101100;
update_buffer[2*8]  = 0b10001010;
update_buffer[3*8]  = 0b10001100;
update_buffer[4*8]  = 0b10001000;
update_buffer[5*8]  = 0b01101000;

Then add the bars to respective locations (there are two loops for two colours, note that they overlap for 4 pixels == 20% as explained before):

void draw_bar(uint8_t x, uint8_t y, uint8_t val) {
for(uint8_t i = 0; i < (val < 16 ? val : 16); i++) {
pset(x+i, y+0, 0);
pset(x+i, y+1, 0);
pset(x+i, y+2, 0);
}

for(uint8_t i = 12; i < val; i++) {
pset(x+i, y+0, 1);
pset(x+i, y+1, 1);
pset(x+i, y+2, 1);
}
}

And lastly the function to add pixels to the buffer (notice complicated maths which should include constants instead of random numbers):

void pset(uint8_t x, uint8_t y, uint8_t color) {  // color: 0->green, 1->red
update_buffer[y * 8 + color * 4 + x / 8] |= (1 << (7 - (x & 7)));
}

And that's it actually.

Optimisation

The first problem with this display is that the picture buffer is potentially updated at the same time as the display is updated (from the buffer). This appears as "tearing" on the display, some kind of glitches that are very fast but still noticeable to the human eye. I could try to take a slow motion video of this, but just trust me on this one. The solution is of course to use double buffering. It works in such a way that there are two picture buffers and two pointers respectively. One pointer points to one buffer and another to the second buffer. The display is always updated from one pointer and the picture is updated using the second one. When the update of the picture is ready, the pointers are swapped so that the update process is not seen on the display. For example clearing of the picture shown above cannot be seen on the display after this modification. Updating of the buffers can be done very easily (notice the same variable names as above):

void update_display() {
uint8_t *temp = show_buffer;
show_buffer = update_buffer;
update_buffer = temp;
}

As I've already mentioned, there is another problem with this display. The problem is that brightness of the lines differ depending on the number of active pixels. Using non-solid bar graphs helps this a little, but in my opinion it's not enough. The solution to this problem is to compensate this by adjusting the brightness of each line while the display is being updated. This can be done very easily by counting the number of active pixels in each line, converting this number to some kind of compensation value and storing it in an array. This array can then be used for each line when the display is being updated. Calculation of the compensation values should obviously be done before hand, so that the display update routine is as fast as possible. This calculation could be done for example in the "update_display" function shown above. The only problem here is that this compensation will limit the maximum brightness, because the lines with lots of active pixels need to be brighter than the rest.

When the user does not want the display to be on, the Python script could be simply stopped. This however will leave the display stuck with the last status that it was displaying. The Python script could obviously have some kind of command to disable the display, but this is needlessly complicated in my opinion. Also this wouldn't work if the Python script were to crash. Instead the display could simply have an idle counter, which is repeatedly decreased after some time interval and reset every time the display receives some data from the PC. When the PC no longer sends any data to the display, the idle counter will go to zero and at this point the display should clear itself.

The picture quality of the display could be improved further. One problem with the display is that the Green and Red LEDs have somewhat different brightness. This can be resolved by displaying Green and Red pixels separately with different brightness. This complicates things quite a bit and reduces the maximum brightness. Another problem is that the Orange colour (Red and Green LEDs on together) is significantly brighter than the other colours. This is obviously because there are two LEDs switched on instead of one. This could of course be resolved by splitting the Red, Green and Orange to three different phases.. This will of course reduce the maximum brightness even further.. I suppose one solution to all this is to make three separate picture buffers for Red, Green and Orange colours. Then have respective brightness values with the above mentioned compensation. Then switch on all the LEDs that should be active and only disable them one by one depending on the brightness. This however might make it very complicated, still reduce maximum brightness.

Installation

Installation of the device was not as simple as I would have hoped. Sanding the display down to fit into the slot did not really work. It would require sanding so much that it could damage either the LED modules or the PCB that is the "module". I've noticed that there are actually some tracks that are quite on the edge of the PCB. However the Nexus Breeze case has an outer groove in the slot so that a cover could be installed for each slot. This groove is only few millimetres in size but it was enough so that I didn't need to sand down the display much more. This means that the display is not really in the case but kind of attached to it. Additionally it was such a nice fit that I only needed to push the display into the groove and it just stays there without any other means.

Installation required also a special cable. Since I wanted to make this display as integrated as possible (the text above states otherwise..) I had to make a special cable for this device. My driver board has a USB-serial adaptor integrated and also a micro USB port. This means that I needed a cable from PC motherboard to micro USB. Luckily I had a USB cable that I wasn't very fond of, so I simply cut the USB-A connector and soldered such a connector that could be connected to the PC motherboard header instead. I also cut the cable to a proper length so I don't have an extra meter of USB cable inside of the case. The motherboard side of the cable is shown in the picture below. It's not properly insulated but not much is properly insulated on the motherboard in any case.


Result

As I tried to explain above, the display is not actually in line with the front panel. This however is not such a big deal. One option would be to cut the front panel of the PC case a little, but I didn't want to do that. Also the display could be attached in a sturdier manner with the holes that are in the LED module. The holes however should be populated before soldering the LED displays in place.. I didn't do that and I don't fancy desoldering four LED modules, solder wick is not that cheap. In any case this LED module is broken as I also explained above, so I've already ordered a new one just in case. Regardless of that, the display looks quite good when you think about how much it took to build it. The display is not very visible in bright conditions, but I don't play with the lights on anyway. Some coloured filter could improve the visibility, but I don't have anything suitable and don't consider it necessary in this case.


The other problem here is that the display takes up to 8 seconds to power off after it stops receiving input through the serial line. This is because of how the code was implemented on my driver board and I was too lazy to change that. This is not really a problem, but even after some time it's still quite confusing since the PC itself shuts down quite quickly. Overall I would still say that the gain/effort ratio is quite good for this project..

ps. What I forgot to discuss here is the current consumption of the display. As already mentioned, the module does not include current limiting resistors. This however doesn't mean that unlimited current flows through the LEDs. The current is instead limited by other components, meaning the used logic chips and transistors. There are no specs for the current consumption of the module, but I have tested that it uses 500 mA when all the LEDs are switched on. This was measured with the refresh rate of around 122 Hz and without the feature to control the brightness. If the interrupt is enabled for the brightness control, the maximum current consumption drops to around 400 mA because of the overhead. This value is well within the spec of the USB so there is absolutely no problem regarding the current consumption when using the display from a USB port.

No comments:

Post a Comment

Internet of Crosstrainers, part 2

Introduction As mentioned in the original post here , there were some issues in the described implementation. I had some difficulties to fin...