Using the Bridgetek BT81x Controller Family
In this two-part series, Brian examines the features of Bridgetek’s BT815 graphics controller. In this second part, he details how to build a display system controlled by the BT815 that produces a waterfall display.
In my last article (Circuit Cellar 382, May 2022) [1], I introduced Bridgetek’s BT815 TFT display controller. This controller is used on Matrix Orbital’s EVE3x-50G-IPS display module. This module features a 5” TFT display with a capacitive touchscreen and an onboard 32MB of flash memory for images and other files. In that article, I described the overall functionality of the BT815 controller and gave code examples for various basic functions that you would need when designing a GUI using a touchscreen display, based upon a BT815 controller. Next, I’ll continue describing some more ways to load bitmap images.
LOADING BITMAPS FROM AN SD CARD
If you need many bitmap images in your application, then uploading them from an SD card is a viable option—particularly if you are using any of the higher-end Teensy modules, which contain an SD card socket using a fast SDIO interface to the Teensy MCU.
If you must interface an SD card via SPI (because your MCU has no SDIO port), you have to be sure that your SPI library supports transactions. The reason for this is that although an SPI port has discrete *CS lines to activate each attached peripheral, for any given data transfer, there is no guarantee that each SPI peripheral will require the same SPI Mode and Clock settings. However, an SPI library that handles transactions will configure the MCU’s SPI port correctly at the beginning of each data transfer, for the specific peripheral that is being accessed. This is generally done as part of the driver library for each specific peripheral and is usually transparent to the programmer.
If you use the “validated” JPG files from Bridgetek’s Asset Builder application, they will always have a long filename, since they have “_converted” appended to the original filename. If the SD library that you are using only recognizes files in the old “8.3” DOS format (8 characters for the file name and 3 characters for the file extension), you would have to rename these JPG files to conform to the “8.3” DOS convention.
The code I used to read JPG files from an SD card into the BT815’s G_RAM memory is given below. There are a few things of note about this code:
- I am using a large (50,000-byte) array to read-in the whole JPG file, before transferring it to the BT815’s G_RAM. This size might have to be increased if you are using large images that didn’t compress well.
- I am using the Teensy SDFAT library to handle the SD card operations. This newer, enhanced SD card library can handle long filenames. This is particularly useful here, since, as mentioned above, the “converted” JPG filenames provided by the EVE Asset Builder program are always long.
In Listing 1, BaseAdd refers to the location to load the image into EVE RAM. For the first image, this is normally set to RAM_G (which is defined as 0).
— ADVERTISMENT—
—Advertise Here—
Listing 1
This is a code snippet to load a JPG file from an SD card into the BT815's G_RAM memory
int ptr;
#define COPYBUFSIZE 50000
char DatBuf[COPYBUFSIZE];
char buffer[20];
// Open the file on SD card by name
FsFile f = SD.sdfs.open(filename);
if (f) {
Serial.println(“File opened OK”);
uint32_t Fsize = f.fileSize();
Serial.println(Fsize);
f.read(DatBuf, Fsize);
f.close();
Serial.println(“data read to buffer”);
Send_CMD(CMD_LOADIMAGE); // Tell the CoProcessor to prepare for compressed data
Send_CMD(BaseAdd); // This is the address where decompressed data will go
Send_CMD(Options); // Send options
CoProWrCmdBuf(DatBuf, Fsize); // write the block to FIFO - Does FIFO triggering
Wait4CoProFIFOEmpty(); // wait here until the coprocessor has read and
// executed every pending command.
}
While you might question my choice of reading the whole JPG file all at once into a large buffer, I did this out of necessity. When I tried to load the SD card’s JPG image into the BT815’s RAM in small packets (64 bytes or 512 bytes), an interaction between the SD card’s read routine and the BT815’s LOADIMAGE command caused erratic operation. This bad behavior didn’t show up when I was doing the same LOADIMAGE operation from either Teensy’s flash program memory (as described in Part 1, Circuit Cellar 382, May 2022), or from the BT815’s dedicated external flash memory.
The routine to determine the JPG file’s width, height, and expanded memory size is identical to that used by the “MCU program flash loading” routine found in Part 1. Also, the code for the rendering of the image is identical to the code described in Part 1.
USING THE BT815 FLASH MEMORY
My Matrix Orbital display module contains an onboard 32MB flash memory device (optionally up to 128MB). This is interfaced with the BT815 display controller using a dedicated QSPI port. This high-speed QSPI port can transfer images to the EVE RAM memory much more quickly than can be achieved using the methods mentioned earlier. Using this BT815-hosted flash memory for images will free up the host MCU Flash memory space if you were using the first technique (found in Part 1).
I found that using the Matrix Orbital display board’s flash memory was not that easy, however. There are a few reasons for this:
• When purchased, the Matrix Orbital BT815 board comes with the first 4,096 bytes of the onboard flash memory pre-programmed with a binary “blob.” Bridgetek doesn’t say what is contained in this “blob” in its “BRT_AN_033 BT81x Programming Guide.” In fact, it isn’t mention at all, apart from a single reference to the word “blob” in a code snippet in section 5.81 (CMD_FLASHUPDATE). Bridgetek provides a binary file containing this “blob” as a part of the EVE Asset Builder Program installation. I can only assume that the “blob” contains parameter data for the different flash devices that are supported—such as the ID # for the flash device, block sizes, and so on. The “blob” likely also includes a BT815-specific instruction code to allow it to read/write/erase the flash memory using the controller’s dedicated QPSI bus. In any case, if you mistakenly overwrite this 4,096-byte block with other data, you will no longer be able to access this flash from the BT815, itself. If this happens, it’s easiest to re-flash the “blob,” using the EVE2-USB2SPI-KIT-A interface board and the EVE Asset Builder program.
- While you may have successfully loaded images to the EVE display board’s flash using the EVE Asset Builder/EVE2-USB2SPI interface board, figuring out how to retrieve that flash image, using BT815 instructions, is not well-documented. More on this later.
- Facing the uncertainty arising from the flash issues previously described, I decided to play it safe and purchase the EVE2-USB2SPI-KIT-A board. This would allow me to:
- Verify that the on-board flash memory could be detected;
- Write a JPG image file to the flash (concatenated with the binary “blob” file available in the Asset Builder program);
- Verify that the flash had been written successfully.
Once that had been done, I could experiment with writing the code to retrieve that JPG image and display it, knowing that the file I was attempting to read was indeed present in the EVE board’s flash. I never like introducing two unknowns into the mix at once.
The Matrix Orbital EVE3x-50G-BLM-TPC-F32 board contains a 20-pin FFC flex connector acting as its MCU interface. This is the flex connector used by the EVE2-USB2SPI-KIT interface board. More important to me, however, the display also contains a 20-pin header (with 0.1” spacing) on the PCB. It contains all of the same MCU interface signals. I used a 6” ribbon cable to connect the display board to my host MCU board. This arrangement worked fine while running the display from the Teensy3.5 MCU. However, when I tried to use the USB2SPI board with the EVE Asset Builder program, those operations were extremely unreliable. The culprit was the ribbon cable that was still attached to the EVE display board (though it was disconnected at the Teensy host MCU end at this time). Once I unplugged the ribbon cable from the display socket while using the EVE2-USB2SPI adapter, everything worked fine. Figure 1 shows the cable arrangement between the display and my host MCU board.

To connect the Matrix Orbital EVE3-50G-IPS display to my Teensy3.5 board, I added a 0.1” header to the unpopulated footprint on the display board and used a ribbon cable for the interconnection. The tiny FFC socket for a 20-pin flex cable is at the front of the PCB.
While discussing problems, I should also mention that I had to make a small change to the SPI SCK signal coming from Teensy 3.5. When I was transferring large image files from the Teensy to the EVE display, intermittently the image would be rendered with lots of artifacts. I scoped each of the SPI lines, and when I was on the SCK line, the problem immediately disappeared. I didn’t see anything unusual with the Teensy’s SCK signal, apart from a bit of ringing on the transitions (which isn’t unusual). However, the fact that the oscilloscope probe cured the problem led me to add a 22pF capacitor from the Teensy3.5’s SCK line to the ground—essentially simulating the inherent load capacitance of the oscilloscope probe. This cured the problem.
Also, the first Matrix Orbital board I bought was an EVE3-50G (not the newer EVE3X-50G board discussed in this article). That model did not have the 20-pin ribbon cable header, just a PCB footprint containing all the necessary interface signals. I added header pins to the board and connected it up to my host MCU via a ribbon cable (Figure 1). I was disappointed to find that connecting it this way worked fine with a Teensy 3.5 host MCU but failed to work at all when using Teensy 3.2, Teensy 4.0, or Teensy 4.1 MCUs. I was using the same host MCU board and cable in all cases—just swapping-in the various pin-compatible Teensy modules. All were running the same code. I never solved this problem. I assume it had to do with the layout of the older EVE-3 module, combined with my using the PCB footprint to interface the signals, instead of the recommended FFC flex connector. The newer EVE-3X-50G display worked fine with all the above MCUs when connected up using a 6” ribbon cable.
— ADVERTISMENT—
—Advertise Here—
LOADING BITMAP IMAGES FROM BT815 FLASH MEMORY
Assuming that you have successfully loaded a JPG image to EVE-3X-50G’s flash (along with the “blob” block), as described in the last section, you can transfer that image to EVE RAM_G for rendering, Listing 2.
Listing 2
This is a code snippet to transfer a JPG image file contained in the EVE-3X-50G's Flash, to the BT815's RAM_G memory space.
Send_CMD(CMD_FLASHSOURCE); / Tell the next command where to get it’s data from
Send_CMD(RAM_FLASH + flashAddr); // This is the address in Eve attached flash
Send_CMD(CMD_LOADIMAGE);
Send_CMD(RAM_G); // This is the address where decompressed data will go
Send_CMD(OPT_FLASH); // Send options , load image from flash
UpdateFIFO(); // Trigger the CoProcessor to start processing commands out // of the FIFO
Wait4CoProFIFOEmpty();
The variable “flashAddr” is the offset into the flash memory space where the image is located. When you upload JPG files to EVE Flash, the Asset Builder program concatenates your JPG file(s) with the binary “blob” file and writes the resulting image to the EVE flash memory. The Asset Builder also creates a .map file, which lists the offset value and the size of any JPG file(s) that you’ve selected for uploading. The “blob” section/partition will always be at flash offset 0, with a size of 4,096 bytes (most of which are empty).
As in the previous examples, the code for rendering the image is the same as that which was listed in the “Load from MCU Program Flash” example. Determining the width, height, and final address in RAM_G is done using the GETPROPS command, as shown in Part 1 (“Loading Bitmaps from Program Flash Memory”).
BT815 ALTERNATE BITMAP MODES
The BT815’s bitmap handling hardware can operate in several modes that are not classically “bitmaps.” One mode is the bar graph mode, and another is a mode that lends itself to producing “waterfall” displays. In both cases, the BT815 has hardware that allows you to transfer an array of byte values into the BT815’s G_RAM memory, and then execute a few instructions to render that data as a graph. In the case of a bar graph, this hardware-accelerated rendering wouldn’t replace a whole lot of code running on the Host MCU, itself, but it does render the bar graph quickly, with no Host MCU load and very little SPI traffic.
In case you haven’t run into one, a waterfall display is commonly used by Software Defined Radios (SDR) to portray signal amplitudes within an RF spectrum of interest, graphed over time. An example is shown in Figure 2. Pardon the picture quality; the only samples I could find online were either screen captures or photos taken of the small TFT screens often present on SDR devices. The upper section is a plot representing instantaneous signal amplitudes over the chosen RF spectrum. The lower section is the same amplitude information, but mapped to the color property Hue, with the Y-axis representing elapsed time. This lower part is the “waterfall” display. Both of these graphs can be handled quickly using BT815’s coprocessor.

This is a sample of a waterfall display as used on an SDR (Software-Defined Radio). The meaning of the two sections of the display is explained in the text.
BT815 BAR GRAPH DISPLAY
The BT815 display controller, using its co-processor, can render a bar graph of up to 256 data points at 8-bit resolution, using one of its Bitmap modes. To do so, you load up to 256 8-bit data points into the BT815 RAM_G. You then define a few bitmap parameters and render the image. Figure 3 shows such a bar graph. All this takes very little code on the Host MCU end. To make the bar graph function more versatile, I’ve added a bit of code to allow customization of the graph’s height and width (in pixels), as well as the number of data points to plot, and the full-scale Y value. Together, these customizations allow the use of datasets of various sizes and amplitudes, along with a user-defined bar graph size on the screen.

The BT81x hardware-implemented bar graph. It is limited to 256 8-bit data points, but is rendered very quickly.
The following code defines some local variables and does the Y scaling:
float YScaleFactor = 256.0 / fullScale;
uint8_t a1[256];
for (int i = 0; i < numPoints; i++) {
a1[i] = BarGraphArray[i] * YScaleFactor;
}
The BarGraphArray is the raw data array and can be up to 256 data points. Once the data has been scaled in the Y-axis, it must be transferred into the BT815’s G-RAM:
Send_CMD(CMD_MEMWRITE);
Send_CMD(RAM_G);
Send_CMD(numPoints);
for (int i = 0; i < numPoints; i = i + 4) {
union
{
uint32_t BarGraphData;
uint8_t s[4];
};
s[0] = 255 - a1[i];
s[1] = 255 - a1[i + 1];
s[2] = 255 - a1[i + 2];
s[3] = 255 - a1[i + 3];
Send_CMD(BarGraphData);
}
Wait4CoProFIFOEmpty();
When we send the BT815 controller the CMD_MEMWRITE command, it expects two 32-bit parameters to follow. They are: starting location in the BT815’s G_RAM and the number of bytes (data points) to be sent.
The RAM_G value can point to anywhere in the 1MB RAM_G space that is free for use. Unless other elements in your display list have already used some of the RAM_G memory, you would normally leave RAM_G at its default value of zero.
Next, you perform a loop for each of the data points in your data array, divided by 4. Each loop iteration has to combine four 8-bit data points into a 32-bit word, because the BT815 expects 32-bit data for most of its commands, including the “Send_CMD” routine. This is most efficiently done using the “union” structure, which superimposes a 32-bit word onto a 4-byte array in memory.
The way that the BT815 renders the bar graph data is a bit odd. If a data point has a value of zero, it draws a full-scale bar, and a value of 255 will produce no bar at all. Therefore, when defining the byte values going to the union structure mentioned above, I negate each value by subtracting it from 255. I could have used the ‘~’ operator, but the small ‘~’ symbol is easy to overlook in the code. After sending the data to the BT815, I perform the “Wait4CoProFIFOEmpty” function to allow the coprocessor to process all the data.
To render the bar graph, the code in Listing 3 is needed. The Listing 3 code assumes that you are drawing this bar graph and nothing else, so it also includes the few mandatory display list commands, at the beginning and at the end, to render the bar graph. I use the FT81x_drawfilledRect routine from my library to provide a custom background color for the bar graph, but this is optional.
Listing 3
This code snippet draws a Bar Graph, assuming that you have loaded RAM_G with the data points (up to 256).
Send_CMD(CMD_DLSTART); // Start a new display list
Send_CMD(CLEAR_COLOR_RGB(255, 255, 255)); // Set the clearing color to white
Send_CMD(CLEAR(1, 1, 1)); // clear screen
Send_CMD(COLOR_RGB(0, 0, 0)); // define the graph background color as black
FT81x_drawFilledRect(xoffset, yoffset, xoffset + XSize, yoffset + YSize);
Send_CMD(COLOR_RGB(0xDE, 0x00, 0x08)); // define the bargraph color
// Scale and then define the bitmap
Cmd_Scale(XSize / numPoints * 65536, YSize / 256.0 * 65536);
Send_CMD(CMD_SETMATRIX);
Send_CMD(BITMAP_HANDLE(0)); // handle for this bitmap
Cmd_SetBitmap(RAM_G, BARGRAPH, XSize, YSize); // bitmap format is BARGRAPH
Send_CMD(BEGIN(BITMAPS));
Send_CMD(BITMAP_HANDLE(0));
Send_CMD(VERTEX2II(xoffset, yoffset, 0, 0));
Send_CMD(END()); // end placing bitmaps
Send_CMD(DISPLAYB()); // End display list
Send_CMD(CMD_SWAP); // Activate this display list
UpdateFIFO();
The BT815 can perform various “transforms” on any bitmap arrays that it is given. Internally, it uses six 32-bit Transform registers—TRANSFORM_A through TRANSFORM_F, which allow for scaling, translating, and rotating bitmaps. In this case, I just use the higher-level routine called Cmd_Scale, which can scale the bitmap representing the bar graph, in both the X and Y axes:
Cmd_Scale(XSize / numPoints * 65536, YSize / 256.0 * 65536);
Send_CMD(CMD_SETMATRIX);
The scaling values are 32-bit fixed-point values, with 16 bits for the integer part and 16-bits for the fractional part (thus the multiplication by 65,536). The remainder of the code in Listing 3 defines four things:
— ADVERTISMENT—
—Advertise Here—
- The bitmap type is BARGRAPH
- The RAM_G address where the bitmap resides
- The Xsize and Ysize of the bitmap
- The X and Y offsets of the bar graph on the display screen
The actual rendering is done by the VERTEX2II command. The end of the display list is normally signaled by the BT815 DISPLAY command. However, in the Arduino compiler, the constant DISPLAY is already defined, so I have modified my BT81x driver code to use the word DISPLAYB in its place. As with all BT815 display lists, the CMD_SWAP command is what actually loads the previously-defined display list into the graphics rendering engine (thus swapping out the previous display list).
One could eliminate the X and Y scaling commands if the number of data points and their typical values provided a bar graph that was appropriately sized, without any scaling. However, there is no noticeable rendering speed degradation when using the scaling command.
BT815 WATERFALL DISPLAYS
A waterfall display normally requires a lot of data to be sent to the display board, where it is then processed and rendered. There could be 200-800 amplitude data points across the spectrum, sent out as many times per second as needed to give a good resolution in the time domain. Although you can’t see it in Figure 2, a static photo, the waterfall display scrolls vertically, showing data for the last 10-30 seconds, as desired. If you had to send a complete bitmap image of the entire waterfall display area, several times per second, it would involve a lot of SPI traffic and a lot of processing on the part of the Host MCU and the graphics controller. However, the BT815 controller allows you to send just the data set for the current instant in time. It stores the past data in a large circular buffer (in RAM_G). By manipulating the pointer into that circular buffer (for both display and new data insertion), the BT815 will produce a waterfall display.
To get a high-resolution mapping of the signal amplitude into color Hue, I use the Bitmap PALETTED8 mode, which maps the 8-bit signal amplitudes into 256 different Hues. Each Hue value is made up of 8-bit Red, Green, and Blue (RGB) components. Therefore, each signal amplitude is represented by a unique 24-bit RGB color. You can tailor this mapping to produce Hues that the human eye is good at distinguishing. Figure 2 is one example of this mapping, but there are other popular choices as well.
If you depended upon your Host MCU to render such a waterfall display on its own, you would be transferring a large, 2D bit-map array via SPI to the display controller, at whatever image update rate you required. Most of this Host MCU processing is eliminated when you let the BT815’s coprocessor handle the rendering. Let’s examine the code needed to produce such waterfall graphs.
The first section of the following code is similar to the bar graph code. In this case, however, it sends the array of amplitude values to the RAM_G memory using a pointer (WaterfallGofs) that starts off at an offset of 0x5A00 in the RAM_G memory space. The 0x5A00 value is merely 256 x 90, where 90 is the chosen Y dimension of the waterfall display. At each subsequent invocation, this pointer is decremented by 256, so it gradually fills up the RAM_G memory space from 0x5B00 downward (that is, the original 0x5A00 starting pointer plus the initial 256 bytes of data).
Send_CMD(CMD_MEMWRITE);
Send_CMD(WaterfallGofs);
Send_CMD(256);
for (int i = 0; i < 256; i = i + 4) {
union
{
uint32_t WaterfallData;
uint8_t s[4];
};
s[0] = Spectrum[i];
s[1] = Spectrum[i + 1];
s[2] = Spectrum[i + 2];
s[3] = Spectrum[i + 3];
Send_CMD(WaterfallData);
}
Once the WaterfallGofs pointer reaches zero, you have to move the old block of data to make room for the new and adjust the WaterfallGofs pointer back to 0x5A00. This is done by using the BT815’s block memory move command:
if (WaterfallGofs == 0) {
WaterfallGofs = 0x5A00;
Send_CMD(CMD_MEMCPY);
Send_CMD(0x5A00);
Send_CMD(0);
Send_CMD(0x5A00); // move old data block into new buffer area
}
This MEMCPY command moves 0x5A00 bytes from a source location of 0 to a destination location of 0x5A00.
To render this bitmap, it’s a bit more complicated than the bar graph example. To begin, we must define a palette array. This is a 256 x 4 array of bytes. The 256 corresponds to the 8-bit resolution of the incoming signal amplitude. For each of these 256 signal amplitudes, we need to define three 8-bit values for red, green, and blue plus a single 8-bit value for alpha. Alpha is the transparency of the bitmap, and for this purpose, alpha is set to 255, which is 0% transparency.
I place this 1,024-byte palette array in the uppermost 1,024-byte section of the RAM_G which is 0xFFC00. There are some other BT81x commands that use this uppermost area, so if you use such commands, you would have to re-locate my palette array to some lower spot in RAM_G memory. The pointer to this palette array is FT_RAM_PAL. In this article, I don’t have space to describe the various ways you can fill this palette, but if you examine the example program which I provide on the Article Materials page of the Circuit Cellar website, you will find the routine FT81X_FillPalette. In that routine, I include a few different Hue mapping palettes that I tried out. The only thing I’ll add here is that a color at full saturation would have a value of 0, and the absence of color is represented by 255. So, I use the negation operator (~) to make this adjustment.
With the signal amplitude array loaded into RAM_G at the correct offset, and a proper palette defined, all that is left to do is render the bitmap image. Here, too, there is a little complication. The bitmap rendering engine, in this mode, can only handle one 8-bit color at a time. So, you actually have to invoke four separate bitmap rendering commands—one for each color and one for the alpha setting.
I won’t repeat code common to the bar graph function, Listing 4 is just the section of the code that performs the four bitmap renderings.
Listing 4
This code snippet renders the waterfall display as four bitmaps- Red, Green, Blue, and Alpha. The rest of the code needed to draw a waterfall display can be found in the demo program found in the article-materials section of the Circuit Cellar website.
uint32_t CMD_PALETTE_SOURCE = (0x2A << 24) + FT_RAM_PAL;
uint32_t CMD_COLOR_MASK = 0x20 << 24;
Cmd_SetBitmap(WaterfallGofs, PALETTED8,width,height);
// bitmap format is PALLETED8
Send_CMD(BITMAP_HANDLE(0));
Send_CMD(BEGIN(BITMAPS));
Send_CMD(CMD_COLOR_MASK + 1);
Send_CMD(CMD_PALETTE_SOURCE + 3);
Send_CMD(VERTEX2II(xoffset, yoffset, 0, 0)); // alpha
Send_CMD(CMD_COLOR_MASK + 8);
Send_CMD(CMD_PALETTE_SOURCE + 2);
Send_CMD(VERTEX2II(xoffset, yoffset, 0, 0)); // red
Send_CMD(CMD_COLOR_MASK + 4);
Send_CMD(CMD_PALETTE_SOURCE + 1);
Send_CMD(VERTEX2II(xoffset, yoffset, 0, 0)); // green
Send_CMD(CMD_COLOR_MASK + 2);
Send_CMD(CMD_PALETTE_SOURCE);
Send_CMD(VERTEX2II(xoffset, yoffset, 0, 0)); // blue
The PALLETTE_SOURCE and COLOR_MASK commands were not defined in the Matrix Orbital library upon which I based my library. The first two lines in the above code provide this definition. Note that in both cases these are base values; constants between 0 and 3 must be added to distinguish which color (or alpha) is being referenced, in the code that follows.
The maximum width of the waterfall display is limited to 256 pixels. This is only about one-third of the width of my 5” Matrix Orbital EVE display. Having discovered that I could enlarge bar graphs using the Bitmap Scale command, I hoped the same capability would apply to the bitmap mode used by the waterfall display. Unfortunately, I wasn’t able to get this to work. That’s not to say it’s impossible, but I haven’t been able to figure it out.
Figure 4 is an example of the waterfall display. I defined a palette that mapped signal amplitudes near zero as black, with increasing amplitudes mapped to blue, through red, and finally white for the highest values. To prove that the routine was working properly, the amplitude array that I sent to the routine was a simple ramp from 0 to 255. Each time I called the waterfall routine, I incremented each ramp value by 1 and used the % 256 operator to limit the values to the 8-bit range. That incrementing over time accounts for the diagonal pattern of the display. I didn’t have any SDR hardware available to connect to the Teensy3.5 board that I was using to test out the Matrix Orbital 5” EVE display. So, I couldn’t show a real-world example of a waterfall display.

The Waterfall display was produced by the BT81x using my code routine. It corresponds to the lower section of Figure 2. Its “barber-pole” appearance is due to the nature of the array values I sent it. I had no SDR circuit to connect to the BT81x display to show a “real-world” example.
There is no limitation on the Y size (time axis), apart from the vertical pixel size of the TFT screen in use. I measured the time it took to render the waterfall graph. With the dimensions shown, it was 6ms. When I built an SDR earlier, using the older FT800 display controller, I recall that the waterfall rendering time was quicker. However, the FT800 controller didn’t support the PALETTED8 mode—it only rendered the bitmaps using an 8-bit color resolution. As a result of this color resolution limitation, the FT800 only had to render the bitmap image once, not the four times that the BT815 requires. That likely explains the speed penalty.
LARGER FONTS
The original FT800 controller came equipped with a number of built-in fonts in various sizes. The font names do not reflect the point size of the font, but they were all proportionally spaced, and the largest of them, Font 31, had a height of 49 pixels. This value is 18% of the 480 x 272 resolution of the 4.3” TFT screens that were commonly used with this controller.
In the case of the 5” Matrix Orbital display, however, the screen resolution has increased to 800 x 480. The BT815 controller contained the same 31 fonts found in the FT800, but even Font31 was too small for viewing at any distance. Fortunately, the BT815 contained three additional fonts (Font 32-34) which were significantly larger. Font 34, for example, has a height of 108 pixels, large enough for viewing on an 800×480 TFT screen from a distance. Unfortunately, the same text commands that I had used with the smaller fonts did not work with the additional fonts. In fact, using them crashed the controller, leaving a blank display.
I speculated that there was an error in the documentation regarding the larger fonts. To make a long story short, I spent many hours writing my own library that would allow you to specify a magnification factor for Font 25. This was a lot of work because scaling a proportional font was not trivial. It was only after I had spent a significant amount of time perusing the BT815 datasheet that I came across the CMD_ROMFONT command. This command did not exist in the earlier FT800 controller, so I wasn’t looking for it.
It turns out that both the older FT800 and BT81x controllers only allow for bitmap/font handles up to 31 (a 5-bit field). Fonts 16-31 are directly mapped into this space, and the handles for other forms of bitmaps use slots 0-15. To make use of Fonts 32-34, you must use the CMD_ROMFONT command to assign one of the lower slots (0-15) to whatever font(s) in the 32-34 range you wish to use. In practice, this is done as follows:
Send_CMD(CMD_ROMFONT);
Send_CMD(5); // assign handle 5
Send_CMD(34); // to font 34
Cmd_Text(0, 200, 5, 0, "Font 34");
The larger, high-resolution fonts numbered 32-34 are much better than those of the scaled-font idea that I had spent so much effort creating. While I had looked over the Programmer’s Guide quite carefully, it’s human nature to overlook or forget a piece of information that is not of immediate use, and that is what happened to me in this case.
CONCLUSIONS
I wouldn’t be honest with you if I claimed that the learning curve for the BT81x EVE display controller is easy. The concept of a controller responding to a display list of both primitive operations and complex coprocessor-based widgets, loaded into a FIFO, and rendered in real-time for each screen update, is not the way that most TFT display controllers work. Similarly, the way that the BT81x controllers integrate the touchscreen functionality is unique. That is, you just “tag” a widget with an ID, and the BT81x reports back whenever that widget is touched. In the case of sliders, for example, it also reports what adjustment has been made to that control.
Once you has gotten used to the unique concept of the BT81x display controllers, though, it becomes fairly easy to program complicated GUIs with it. I’ll admit that I found Bridgetek’s EVE Screen Designer application overly complicated, and I didn’t spend enough time trying to master its use. However, the lower-level EVE Screen Editor is easier to use and will give you a text file containing a list of commands needed to implement the screen that you have laid out. Actually, that list of commands is merely a list of BT81x commands and macros. It’s not actually the C code needed to implement such a screen layout. For that, you have to add the C code that calls the appropriate functions in whatever BT81x library you are using. However, it does take care of a lot of the “detail” work, when you are placing and moving widgets on your GUI layout, and it also helps specify RGB color codes for user-specified color choices. Figure 5 shows this application.
Similarly, the EVE Asset Builder is a handy program to use if you need to generate your own fonts or convert bitmap images to a format usable by the BT8xx controller. Also, it is the only easy way to get such content loaded into the flash memory device present on the Matrix Orbital display board. Figure 6 shows the startup screen for this application.
Of the various display boards that use the BT81x display controller, I found the Matrix Orbital boards to be the easiest to interface with my host MCU—at least at the development/prototyping stage. While other manufacturers’ boards contain only the tiny FFC socket, as used on the Matrix Orbital board, the Matrix Orbital board also contains a standard 20-pin socket suitable for a ribbon cable. Alternately, the company also sell the EVE-SPI2BBC breakout board, which converts the flex cable into a standard 0.1” 20-pin header socket. This is shown in Figure 7.

I decided to connect a ribbon cable to the EVE display board by adding an 0.1” spaced header socket to the board. I could have used the flex cable that came with the display and converted it to a standard ribbon cable by using the SPI2BBC module sold by Matrix Orbital for use with their displays.
RESOURCES
BT815 Display controller: Bridgetek
https://brtchip.com/bridgeteks-eve-technology-now-supports-larger-higher-resolution-displays-than-ever-before-3/
BT81X IC datasheet:
https://brtchip.com/ic-module/wp-content/uploads/sites/3/2022/04/DS_BT817_8.pdf
BT81x Programmer’s Guide (BRT-AN_033_BT81x Series Programming Guide):
https://brtchip.com/wp-content/uploads/Support/Documentation/Programming_Guides/ICs/EVE/BRT_AN_033_BT81X_Series_Programming_Guide.pdf
Matrix Orbital EVE3x-50G-IPS 5” Display module with capacitive touch screen:
https://www.matrixorbital.com/ftdi-eve/eve-bt815-bt816/eve3x-50g-ips
Matrix Orbital EVE2-USB2SPI-KIT-A:
https://www.matrixorbital.com/eve2-usb2spi-kit-a
Matrix Orbital EVE-SPI2BBC breakout module:
https://www.matrixorbital.com/eve-spi2bbc
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JULY 2022 #384 – Get a PDF of the issue
Sponsor this ArticleBrian Millier runs Computer Interface Consultants. He was an instrumentation engineer in the Department of Chemistry at Dalhousie University (Halifax, NS, Canada) for 29 years.