Drive a TFT LCD with Capacitive Touchscreen - Part 2
09/271/2016 | 03:56 PM
In the last section, we covered the basics of LCD screen architectures. In this section, we will connect the Adafruit display to the Wonder Gecko Starter Kit and light up some pixels.
Connecting the Adafruit Display to the Wonder Gecko Starter Kit
Use wire jumpers to attach all of the connections between the Wonder Gecko Starter Kit and the Adafruit display as shown in the table. Note that most of the connections should be made on the edge of the board that has the SPI labeling (i.e. MOSI/MISO, etc.) with the two I2C connections on the opposite board edge.
Signal
Adafruit LCD Display
Wonder Gecko GPIO Pin
LCD Chip Select (CS)
CS
PD3 – USART1 CS
MISO
MISO
PD1 – USART1 RX (MISO)
DC (Data/Command)
D/C
PD4 – Chosen at random
GND
GND
GND
MOSI
MOSI
PD0 – USART1 TX (MOSI)
Clock (CLK)
CLK
PD2 – USART1 CLK
Vcc
3-5V
V3V
Reset
RST
PD5 – Chosen at random
I2C SDA
SDA [ Capacitive Touchscreen]
PD7 – I2C0 SDA
I2C SCL
SCL [ Capacitive Touchscreen]
PD6 – I2C0 SCL
I2C Interrupt
IRQ [ Capacitive Touchscreen]
PD13 – Chosen at random
MicroSD Chip Select (CS)
Card CS
PD8 – Chosen at random
The application note for the display from Adafruit instructs us to close the IM1, IM2, and IM3 solder jumpers on the back of the display board in order to use the SPI interface, so warm up your soldering iron and make those connections. This will also allow us to communicate to the MicroSD card built into the display.
When you make these connections to the Starter Kit, the backlight to the display will automatically light up. This is because the Adafruit board is built to give full brightness by default. To turn off the backlight, you set the backlight pin to ground. You can dim the display by applying a PWM pulse (with open-drain output) to the backlight pin.
Configuring the Display
The Adafruit display is a board comprised of multiple devices bundled together on the same board. We have worked with the fundamental communication methods already in previous chapters, and this will just be an exercise of learning how to deal with several devices at once in the same firmware. We will communicate with the LCD and the MicroSD card over the same SPI bus (selecting each device independently with their own Chip Select (CS) signal.
We could make use of the Configurator tool in Simplicity Studio to find suitable pins for the SPI and I2C interfaces, and it would automatically generate code for our project. We will instead set things up manually to help you get practice in learning how it is done.
Remember, as we learned in chapter 11, the order in which to initialize the MCU to communicate with external devices follows a particular order:
Enable the GPIO clock
Enable the peripheral clocks (i.e. USART1 and SPI0)
Configure and enable the peripheral
Route the pins used by the peripheral through to the GPIO
Configure the pins used by the peripheral in the GPIO (push-pull, input, etc.)
If you do the above in the wrong order, you can see glitches on the output which could confuse your external devices. By following this order (and not forgetting a step!) we can ensure that the peripherals will be configured and external devices ready to be controlled.
By following these steps, I created a peripheral_setup() function to do all of the above:
void peripheral_setup()
{
// Set up the necessary peripheral clocks
CMU_ClockEnable(cmuClock_GPIO, true);
CMU_ClockEnable(cmuClock_USART1, true);
CMU_ClockEnable(cmuClock_I2C0, true);
// Initialize and enable the USART
USART_InitSync_TypeDef init = USART_INITSYNC_DEFAULT;
init.msbf = true;
USART_InitSync(USART1, &init);
USART1->CTRL |= USART_CTRL_AUTOCS;
// Connect the USART signals to the GPIO peripheral
USART1->ROUTE = USART_ROUTE_RXPEN | USART_ROUTE_TXPEN |
USART_ROUTE_CLKPEN | USART_ROUTE_CSPEN |
USART_ROUTE_LOCATION_LOC1;
// Set up i2c
I2C_Init_TypeDef i2c_init = I2C_INIT_DEFAULT;
I2C_Init(I2C0, &i2c_init);
/* Module I2C0 is configured to location 1 */
I2C0->ROUTE = (I2C0->ROUTE & ~_I2C_ROUTE_LOCATION_MASK) |
I2C_ROUTE_LOCATION_LOC1;
/* Enable signals SCL, SDA */
I2C0->ROUTE |= I2C_ROUTE_SCLPEN | I2C_ROUTE_SDAPEN;
// Enable the GPIO pins for the USART, starting with CS
// This is to avoid clocking the flash chip when we set CLK high
GPIO_PinModeSet(gpioPortD, 3, gpioModePushPull, 1); // CS
GPIO_PinModeSet(gpioPortD, 8, gpioModePushPull, 1); // MicroSD CS
GPIO_PinModeSet(gpioPortD, 0, gpioModePushPull, 0); // MOSI
GPIO_PinModeSet(gpioPortD, 1, gpioModeInput, 0); // MISO
GPIO_PinModeSet(gpioPortD, 2, gpioModePushPull, 1); // CLK
// Enable the GPIO pins for the misc signals, leave pulled high
GPIO_PinModeSet(gpioPortD, 4, gpioModePushPull, 1); // DC
GPIO_PinModeSet(gpioPortD, 5, gpioModePushPull, 1); // RST
// Enable the GPIO pins for the i2c signals, open drain, pulled up, with filter
GPIO_PinModeSet(gpioPortD, 6, gpioModeWiredAndPullUpFilter, 1); // SDA
GPIO_PinModeSet(gpioPortD, 7, gpioModeWiredAndPullUpFilter, 1); // SCL
GPIO_PinModeSet(gpioPortD, 13, gpioModeInput, 1); // IRQ
}
In this initialization code for the USART, we are going to let the USART control the chip select (CS) signal for the LCD. This requires that we set the AUTOCS flag in the USART peripheral with the code:
USART1->CTRL |= USART_CTRL_AUTOCS;
We also must route the CS pin through to the GPIO with the ROUTE statement that follows it. This will only work up until the point where we start to control the MicroSD card on the same SPI bus, since the USART cannot automatically control multiple chip selects at a time. But for our initial work with just the ILI9341 controller active on the SPI bus, we can let the USART control the CS signal.
Now, we are ready to start sending some cycles over SPI to configure the LCD for our first try at generating some graphics.
Activating Pixels
The LCD is controlled by the ILI9341 display driver chip built into the display panel. The ILI9341.pdf datasheet details the component package, pinout, electrical ratings, interface procedures, and provides a comprehensive guide of the different commands that the controller accepts. However, it is very frustrating that there is no “big picture” overview or even a guide detailing how to turn the display on from a power-off state, and no examples of how to use it. I had to scour the internet to find some examples, and even then, the examples didn’t always explain why they were doing what they were doing to initialize the display, as well as referencing registers that are not found in the datasheet. It is at times like these that working as an embedded developer feels more like a hacker and less like a software engineer!
Through my research, and some trial and error, I was able to initialize the display with just the following code.
When this code is executed, the display will go dark after it executes the ILI9341_DISPON command. This makes sense, because when the display is in the “off” state, the pixel grid allows the backlight to pass, but when the display driver chip turns on, all of the pixels are instructed to not allow any light to pass, causing the display to go dark, or mostly dark. If you look carefully at the screen, you can see that the backlight is still powered behind the blocking pixels. This confirms that we can communicate with the ILI9341 in the most basic of commands.
When you examine the writedata() and writecommand() functions, you will see the code implements the procedure that is detailed in the ILI9341 specification for SPI writes and reads:
The only detail that separates “command” packets from “data” packets is the D/CX signal, which we have connected to PD4. If it is low, the packet is seen as a command. If D/CX is high, the packet is seen as a data packet, or as any necessary parameter(s) that immediately follows a command packet.
The spec shows that pauses between commands and data/parameters (or successive data packets) are acceptable, so we don’t have to worry about keeping the CS signal low for the entire command/data operation. In order to make sure that there is some delay between multiple commands and command/data packets, a delay of 1ms is added before and after each USART_Tx() call in the writecommand() function. This will not degrade our screen display performance later on, because the command packet is only sent one time, which is then followed by thousands of data packets necessary to form an image on the display screen.
With the screen alive and ready to display some pixels, we can turn them all on to a specific color after a few more configuration commands, which are each accomplished with a command byte followed by a configuration data byte. After those are set, the memory write command is issued, followed by the color data for all of the pixels in the screen.
#define ILI9341_TFTWIDTH 240
#define ILI9341_TFTHEIGHT 320
#define ILI9341_RAMWR 0x2C
#define ILI9341_MADCTL 0x36
#define ILI9341_PIXFMT 0x3A
writecommand(ILI9341_PIXFMT); // Pixel format
writedata(0x55); // This sets up 16-byte color data, instead of default 18-bit color data
writecommand(ILI9341_MADCTL); // Memory Access Control
writedata(0x48); // This configures the screen orientation
uint16_t color = ILI9341_RED;
writecommand(ILI9341_RAMWR); //Memory write
for (int i = 0; i < (ILI9341_TFTWIDTH); i++)
{
for (int j = 0; j < ILI9341_TFTHEIGHT; j++)
{
writedata( color >> 8);
writedata( color );
}
}
With this code placed just before the while loop in main(), the screen will slowly be painted red. It should take about four seconds to send all of those 76,800 x 2 = 153,600 SPI cycles to be written at the default 1 MHz USART interface speed.
This works because the ILI9341 knows the width and height of the screen, so every Memory Write command advances the ILI9341’s pointer into the frame buffer by one pixel. When it gets to the end of a row, it starts on the next row, until the whole screen is painted red. At the present speed of the SPI bus, you can actually see that happening in real time.
Speeding up the SPI Bus Rate
If the fastest frame rate were critical to our application and we required several images refreshed in every second, we would have used the parallel interface, which allows up to 18 data bits to be sent to the ILI9371 at once instead of the single bit per clock with the SPI bus. However, we can speed up the rate of the SPI bus to improve performance by changing the MCU clock configuration.
If you add the following line of code to the peripheral_setup() function, just after the USART_InitSync() call, you will be able to read the clock rate of the SPI bus:
uint32_t baud = USART_BaudrateGet(USART1);
You should see that the clock is initially set at 1000000, or 1MHz. To get the fastest possible USART clock, we need to clock the MCU with the crystal oscillator with the following line of code:
CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);
This will increase the clock rate of the core to 48 MHz, and the High Frequency (HF) clock will increase to 24 MHz, which is the fastest that the USART can operate on the Wonder Gecko MCU. But the default SPI rate is still set to 1 MHz due to the use of USART_INITSYNC_DEFAULT during the USART initialization. You can change that to 24 MHz with the following line of code before you call USART_InitSync():
init.baudrate = 24000000;
Now, when you run this and check the baud rate variable again, you will see 24000000, or 24 MHz. Beware that you cannot always get the baud rate that you request. Always check the baud rate using the USART_BaudrateGet() call to see if you are requesting a valid and reachable clock divisor.
With the SPI bus set to 24 MHz, your screen should display all red pixels in about one second. This is still very slow and perhaps could be improved more through the use of DMA and a memory buffer, but this is good enough for the work of this chapter.
TIP: If you are wondering about all of the available commands for peripherals such as CMU and USART, for example the CMU_ClockSelectSet() function or the USART_BaudRateGet() function, you can use the Software Documentation reference available from the home screen of Simplicity Studio. Click on that tile, and a browser will open which you can use to explore all of the possible library calls in emlib, with hyperlinks to the types of parameters that each function expects as well.
It should be easier to find what you are looking for in the Software Documentation than in any number of chapters of this text or looking back through all of the examples that you have ever studied.
As a reminder, the final, complete source code for all examples in this book can be found on Github here.
In the next section, we will put a crude graphic library in place to handle displaying some standard graphic primitives like lines, rectangles, and text.
Drive a TFT LCD with Capacitive Touchscreen - Part 2
In the last section, we covered the basics of LCD screen architectures. In this section, we will connect the Adafruit display to the Wonder Gecko Starter Kit and light up some pixels.
Connecting the Adafruit Display to the Wonder Gecko Starter Kit
Use wire jumpers to attach all of the connections between the Wonder Gecko Starter Kit and the Adafruit display as shown in the table. Note that most of the connections should be made on the edge of the board that has the SPI labeling (i.e. MOSI/MISO, etc.) with the two I2C connections on the opposite board edge.
Signal
Adafruit LCD Display
Wonder Gecko GPIO Pin
LCD Chip Select (CS)
CS
PD3 – USART1 CS
MISO
MISO
PD1 – USART1 RX (MISO)
DC (Data/Command)
D/C
PD4 – Chosen at random
GND
GND
GND
MOSI
MOSI
PD0 – USART1 TX (MOSI)
Clock (CLK)
CLK
PD2 – USART1 CLK
Vcc
3-5V
V3V
Reset
RST
PD5 – Chosen at random
I2C SDA
SDA [ Capacitive Touchscreen]
PD7 – I2C0 SDA
I2C SCL
SCL [ Capacitive Touchscreen]
PD6 – I2C0 SCL
I2C Interrupt
IRQ [ Capacitive Touchscreen]
PD13 – Chosen at random
MicroSD Chip Select (CS)
Card CS
PD8 – Chosen at random
The application note for the display from Adafruit instructs us to close the IM1, IM2, and IM3 solder jumpers on the back of the display board in order to use the SPI interface, so warm up your soldering iron and make those connections. This will also allow us to communicate to the MicroSD card built into the display.
When you make these connections to the Starter Kit, the backlight to the display will automatically light up. This is because the Adafruit board is built to give full brightness by default. To turn off the backlight, you set the backlight pin to ground. You can dim the display by applying a PWM pulse (with open-drain output) to the backlight pin.
Configuring the Display
The Adafruit display is a board comprised of multiple devices bundled together on the same board. We have worked with the fundamental communication methods already in previous chapters, and this will just be an exercise of learning how to deal with several devices at once in the same firmware. We will communicate with the LCD and the MicroSD card over the same SPI bus (selecting each device independently with their own Chip Select (CS) signal.
We could make use of the Configurator tool in Simplicity Studio to find suitable pins for the SPI and I2C interfaces, and it would automatically generate code for our project. We will instead set things up manually to help you get practice in learning how it is done.
Remember, as we learned in chapter 11, the order in which to initialize the MCU to communicate with external devices follows a particular order:
If you do the above in the wrong order, you can see glitches on the output which could confuse your external devices. By following this order (and not forgetting a step!) we can ensure that the peripherals will be configured and external devices ready to be controlled.
By following these steps, I created a peripheral_setup() function to do all of the above:
In this initialization code for the USART, we are going to let the USART control the chip select (CS) signal for the LCD. This requires that we set the AUTOCS flag in the USART peripheral with the code:
We also must route the CS pin through to the GPIO with the ROUTE statement that follows it. This will only work up until the point where we start to control the MicroSD card on the same SPI bus, since the USART cannot automatically control multiple chip selects at a time. But for our initial work with just the ILI9341 controller active on the SPI bus, we can let the USART control the CS signal.
Now, we are ready to start sending some cycles over SPI to configure the LCD for our first try at generating some graphics.
Activating Pixels
The LCD is controlled by the ILI9341 display driver chip built into the display panel. The ILI9341.pdf datasheet details the component package, pinout, electrical ratings, interface procedures, and provides a comprehensive guide of the different commands that the controller accepts. However, it is very frustrating that there is no “big picture” overview or even a guide detailing how to turn the display on from a power-off state, and no examples of how to use it. I had to scour the internet to find some examples, and even then, the examples didn’t always explain why they were doing what they were doing to initialize the display, as well as referencing registers that are not found in the datasheet. It is at times like these that working as an embedded developer feels more like a hacker and less like a software engineer!
Through my research, and some trial and error, I was able to initialize the display with just the following code.
When this code is executed, the display will go dark after it executes the ILI9341_DISPON command. This makes sense, because when the display is in the “off” state, the pixel grid allows the backlight to pass, but when the display driver chip turns on, all of the pixels are instructed to not allow any light to pass, causing the display to go dark, or mostly dark. If you look carefully at the screen, you can see that the backlight is still powered behind the blocking pixels. This confirms that we can communicate with the ILI9341 in the most basic of commands.
When you examine the writedata() and writecommand() functions, you will see the code implements the procedure that is detailed in the ILI9341 specification for SPI writes and reads:
The only detail that separates “command” packets from “data” packets is the D/CX signal, which we have connected to PD4. If it is low, the packet is seen as a command. If D/CX is high, the packet is seen as a data packet, or as any necessary parameter(s) that immediately follows a command packet.
The spec shows that pauses between commands and data/parameters (or successive data packets) are acceptable, so we don’t have to worry about keeping the CS signal low for the entire command/data operation. In order to make sure that there is some delay between multiple commands and command/data packets, a delay of 1ms is added before and after each USART_Tx() call in the writecommand() function. This will not degrade our screen display performance later on, because the command packet is only sent one time, which is then followed by thousands of data packets necessary to form an image on the display screen.
With the screen alive and ready to display some pixels, we can turn them all on to a specific color after a few more configuration commands, which are each accomplished with a command byte followed by a configuration data byte. After those are set, the memory write command is issued, followed by the color data for all of the pixels in the screen.
With this code placed just before the while loop in main(), the screen will slowly be painted red. It should take about four seconds to send all of those 76,800 x 2 = 153,600 SPI cycles to be written at the default 1 MHz USART interface speed.
This works because the ILI9341 knows the width and height of the screen, so every Memory Write command advances the ILI9341’s pointer into the frame buffer by one pixel. When it gets to the end of a row, it starts on the next row, until the whole screen is painted red. At the present speed of the SPI bus, you can actually see that happening in real time.
Speeding up the SPI Bus Rate
If the fastest frame rate were critical to our application and we required several images refreshed in every second, we would have used the parallel interface, which allows up to 18 data bits to be sent to the ILI9371 at once instead of the single bit per clock with the SPI bus. However, we can speed up the rate of the SPI bus to improve performance by changing the MCU clock configuration.
If you add the following line of code to the peripheral_setup() function, just after the USART_InitSync() call, you will be able to read the clock rate of the SPI bus:
You should see that the clock is initially set at 1000000, or 1MHz. To get the fastest possible USART clock, we need to clock the MCU with the crystal oscillator with the following line of code:
This will increase the clock rate of the core to 48 MHz, and the High Frequency (HF) clock will increase to 24 MHz, which is the fastest that the USART can operate on the Wonder Gecko MCU. But the default SPI rate is still set to 1 MHz due to the use of USART_INITSYNC_DEFAULT during the USART initialization. You can change that to 24 MHz with the following line of code before you call USART_InitSync():
Now, when you run this and check the baud rate variable again, you will see 24000000, or 24 MHz. Beware that you cannot always get the baud rate that you request. Always check the baud rate using the USART_BaudrateGet() call to see if you are requesting a valid and reachable clock divisor.
With the SPI bus set to 24 MHz, your screen should display all red pixels in about one second. This is still very slow and perhaps could be improved more through the use of DMA and a memory buffer, but this is good enough for the work of this chapter.
TIP: If you are wondering about all of the available commands for peripherals such as CMU and USART, for example the CMU_ClockSelectSet() function or the USART_BaudRateGet() function, you can use the Software Documentation reference available from the home screen of Simplicity Studio. Click on that tile, and a browser will open which you can use to explore all of the possible library calls in emlib, with hyperlinks to the types of parameters that each function expects as well.
It should be easier to find what you are looking for in the Software Documentation than in any number of chapters of this text or looking back through all of the examples that you have ever studied.
As a reminder, the final, complete source code for all examples in this book can be found on Github here.
In the next section, we will put a crude graphic library in place to handle displaying some standard graphic primitives like lines, rectangles, and text.
PREVIOUS | NEXT