ILI9341 problem with incomplete number of pixels being displayed - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 05, 2021, 10:52:11 pm
content: I'm experimenting with a TFT LCD driven by an ILI9341 controller that I have hooked up to an ESP32 via SPI. I've written some code to do something pretty basic, which is to draw a bunch of randomly sized and coloured rectangles on to the screen. But, I am encountering an odd problem, and I can't see where or what I've done wrong.The problem is that when a filled rectangle is drawn, the last (i.e. bottom) row of pixels is always incomplete, and doesn't run the full width of the rectangle. The amount varies - sometimes a few are not drawn, sometimes almost all. Here's an artist's illustration of what I'm talking about:I can't figure out why this is occurring. I've double-checked my code to make sure that I'm doing things correctly. I've even gone to the extent of breaking out the logic analyser to capture the SPI traffic so that I can confirm the ILI9341 is actually receiving the data I think I'm sending it. From this I have verified that the missing pixels aren't because not enough are being sent - I checked the number of bits being sent per rectangle (by counting the rising edges on SCK in the capture), and they are what they should be. For example, a rectangle with width and height of 90x10 has 900 pixels at 16bpp, so 14,400 bits should be transmitted, which the logic analyser capture does show. The pixel colour values are correct all the way through too, plus column and row (a.k.a. 'page') start and end addresses are being sent correctly.Here's my code:Code:void lcd_fill_colour(const uint16_t x1, const uint16_t y1, const uint16_t x2, const uint16_t y2, const uint16_t colour) {uint16_t pixel_buf; // Buffer size must be a multiple of 4 for DMAuint16_t col_addr_params, page_addr_params;uint16_t width, height;size_t pixel_count, buffer_count;if(x1 <= x2) {col_addr_params = __builtin_bswap16(x1);col_addr_params = __builtin_bswap16(x2);width = x2 - x1;} else {col_addr_params = __builtin_bswap16(x2);col_addr_params = __builtin_bswap16(x1);width = x1 - x2;}if(y1 <= y2) {page_addr_params = __builtin_bswap16(y1);page_addr_params = __builtin_bswap16(y2);height = y2 - y1;} else {page_addr_params = __builtin_bswap16(y2);page_addr_params = __builtin_bswap16(y1);height = y1 - y2;}if(width == 0 || height == 0) return;for(size_t i = 0; i < (sizeof(pixel_buf) / sizeof(pixel_buf)); i++) {pixel_buf = __builtin_bswap16(colour);}pixel_count = width * height;spi_device_acquire_bus(lcd_spi_dev, portMAX_DELAY);ili9341_write_command(lcd_spi_dev, ILI9341_COL_ADDR_SET);ili9341_write_data(lcd_spi_dev, col_addr_params, sizeof(col_addr_params));ili9341_write_command(lcd_spi_dev, ILI9341_PAGE_ADDR_SET);ili9341_write_data(lcd_spi_dev, page_addr_params, sizeof(page_addr_params));ili9341_write_command(lcd_spi_dev, ILI9341_MEM_WRITE);while(pixel_count > 0) {buffer_count = (pixel_count >= (sizeof(pixel_buf) / sizeof(pixel_buf)) ? (sizeof(pixel_buf) / sizeof(pixel_buf)) : pixel_count);ili9341_write_data(lcd_spi_dev, pixel_buf, buffer_count * sizeof(pixel_buf));pixel_count -= buffer_count;}spi_device_release_bus(lcd_spi_dev);}esp_err_t ili9341_write_command(const spi_device_handle_t dev, const uint8_t cmd) {esp_err_t err = ESP_FAIL;spi_transaction_t t;memset(&t, 0, sizeof(t));t.flags = SPI_TRANS_USE_TXDATA;t.user = (void *)0; // Set D/CX low, indicating commandt.length = 8 * sizeof(cmd);t.tx_data = cmd;err = spi_device_polling_transmit(dev, &t);return err;}esp_err_t ili9341_write_data(const spi_device_handle_t dev, const void *data, const size_t data_len) {esp_err_t err = ESP_FAIL;spi_transaction_t t;if(data_len > 0) {memset(&t, 0, sizeof(t));t.user = (void *)1; // Set D/CX high, indicating datat.length = 8 * data_len;t.tx_buffer = data;err = spi_device_polling_transmit(dev, &t);} else {err = ESP_OK;}return err;}// Callback function registered to be called just before SPI transaction starts.void lcd_spi_pre_transfer_callback(spi_transaction_t *t) {const uint32_t dc = (uint32_t)t->user;gpio_set_level((gpio_num_t)LCD_SPI_DCX_IO, dc);}One thing I thought of trying to do is read back the frame memory from the ILI9341 and see what it's contents is (i.e. does it match what I wrote), but I can't do that because this damn LCD screen seems to be configured either with the SPI MISO/SDO line not connected, or possibly running in half-duplex mode (so it's trying to output data back on the MOSI/SDA line).I can't even read out the device ID, it's just all zeros.

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 05, 2021, 11:10:20 pm
content: One thing I noted is that: for this controller, both the start column and end column parameters (same for row address) are inclusive, IIRC, but your code does as though the end parameter was not inclusive, which can be seen when you do this, for instance: "width = x2 - x1;".That should be "width = x2 - x1 + 1;". Same for the other combinations and for the row address. Check this out and report back.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: December 06, 2021, 06:40:14 am
content: Yes, that sounds likely.Diagnostics:- See if the left and right edges line up. Draw a rect at say (10, 10)-(20, 20) and another at (20, 13)-(30, 17). Does it overlap? Is there a gap? Do the same for vertical alignments, and around screen edges.- See if the number of pixels remaining (on the bottom row) is the one-off difference to the width. That is, a 10x10 rect needs 100 pixels, but drawn at 11 width, 100 pixels only fills 9 rows, coming up exactly one pixel short on the 8th row. Or at 9 width, one pixel more than 11 rows (just beginning a 12th). Note that if overdraw goes into vertical overflow, it probably wraps to the start.I haven't looked at ILI9341 exactly, but I've done a ILI9325 before, and actually have a ST7735 in front of me. The latter are... moderately similar, IIRC? Anyway, the ST does boundary-inclusive, so for example mydrawFillRectangle()has aCode:setScreenRegion(xStart, yStart, xStart + width - 1, yStart + height - 1);in it. (Or use x1, y1, x2, y2 instead of width, height if you like.)Tim

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 06, 2021, 03:25:04 pm
content: Quote from: SiliconWizard on December 05, 2021, 11:10:20 pmAaaah, that was the problem. What an elementary mistake to make!Yes, checking the datasheet again, the col/page end addresses do indeed appear to be inclusive.You know how the saying goes: the two hardest problems in computing are concurrency, naming things, and off-by-one errors.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 06, 2021, 03:42:37 pm
content: Now, if I can just figure out how to be able to read dataoutof the ILI9341...I thought maybe this LCD was configured for '4-line Serial Interface I', where there is a single SDA line that is half-duplex, because when issuing an 'Read ID' command (0x04) I was getting some data on the MOSI line. But it seems this is just junk from uninitialised memory that the ESP32 is clocking out, because the data bytes change on a power cycle.Is there any way of checking what interface mode the ILI9341 is using? I understand from the datasheet that it is set by the IM[3:0] pins. But of course the chip is internal to the LCD, so I can't check their state.However, I notice there are some passive components on the flat flex. Might these be what sets IM?Edit:no, they're all just capacitors.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 06, 2021, 04:07:19 pm
content: I checked with the scope directly at the SDO pin on the screen's flat flex and definitely nothing at all being output there when I issue a command to read data. So not a continuity problem. It definitely seems this LCD screen is not set up to use that pin, for some stupid reason.I just had a thought: if it is indeed set up to use a single half-duplex data line, if the ILI9341 is trying to drive the MOSI line at the same time as the ESP32, won't that lead to some brief short-circuit of current? For example, if the ILI9341 is trying to drive a low and the ESP32 a high?

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 06, 2021, 05:48:10 pm
content: I think those ILI controllers don't support data read when using SPI.

author: DrG
author_rank: Super Contributor
timestamp: December 06, 2021, 06:01:04 pm
content: Quote from: SiliconWizard on December 06, 2021, 05:48:10 pmNot that I read it carefully, but it seems that they canhttps://www.avrfreaks.net/forum/reading-pixels-gram-memory-ili9341-and-ili9325

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 06, 2021, 08:25:03 pm
content: Yeah, I'm 99.9% sure the ILI9341 should support reading when using SPI. Section 7.1.10 of the datasheet is dedicated to reading with the serial interface.Quote from: DrG on December 06, 2021, 06:01:04 pmThe author of thelast postin that thread says:QuoteHmm, now this is something I'mnotdoing - essentially because I can't. Because the D/C line needs to be toggled between sending a command and reading the data, I need to split the thing into two SPI transactions (so D/C can be toggled in-between), and because the ESP32 hardware handles the CS automatically, it is raised between these two transactions.However, I'm not sure I believe this CS requirement. Section 7.1.12 ("Data Transfer Pause") of the datasheet seems to convey that releasing CS between a command byte and its parameters is okay, where the ILI9341 will effectively pause and retain its current state. However, I do note that the wording talks only in context of data beingreceived(i.e. writing to the display), so maybe this doesn't work for reading from the display?One thing I've discovered since my last post is that it may be the case that the device ID is just simply zero!I've found a couple of example pieces of code on GitHub that check the ID to decide what type of display they're dealing with (ILI vs. ST), and match an ID of zero to ILI9341. Who'd have thought?So, I decided to try reading some other info from the display - something that I know should have a non-zero value - for instance, display status (0x09). But I still read all zero for that too! However, interestingly, when capturing the SPI transaction on the logic analyser, I get something weird. The MISO line is pulled high immediately after the command byte is sent, but goes low again after about 1.5us, before the data is read out. See attached screenshot.Not sure what I can do to try and keep CS low during the entire exchange. Any ideas? Can I somehow tell the ESP-IDF SPI API not to handle CS in hardware?

author: DrG
author_rank: Super Contributor
timestamp: December 06, 2021, 09:01:48 pm
content: Quote from: HwAoRrDk on December 06, 2021, 08:25:03 pmYou are using Espressif's IDF program interface - right? I have almost no experience with that and am too lazy to use anything other than that Arduino stuff....and yet, I am respondingWould using GPIO Matrix rather than IO_MUX give you the flexibility to directly control the CS line? It seems like that is what they are getting at in that sectionhttps://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.htmlbut bear in mind my previous statement.The link that I posted is clearly NOT for ESP32. Can you test out the code suggestions on an AVR before assuming new difficulties with Espressif's driver?I am going to follow this thread just to find out if it's worth it (for you) to try and, of course, I will probably learn something. Sorry, I couldn't be of more help.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 06, 2021, 09:37:04 pm
content: Quote from: DrG on December 06, 2021, 09:01:48 pmYes, but via the Arduino environment. Despite that, I'm programming stuff mostly using ESP-IDF functions directly and not Arduino library functions. I couldn't be bothered to install and setup the entire Espressif toolchain.Quote from: DrG on December 06, 2021, 09:01:48 pmI have no idea.The IO_MUX and GPIO matrix stuff is a bit over my head at present. But my impression of that section in the docs is that it is only to do with maximum usable SPI clock frequency, and how it is affected by how the I/O signals are routed inside the ESP32.Quote from: DrG on December 06, 2021, 09:01:48 pmHmm, not really. The LCD screen I'm using is just the bare screen, and has it's flat flex soldered down to the board containing the ESP32, so I can't disconnect it and use it elsewhere. I have another ILI9341 screen module board (one of those common break-out modules with SD card slot), but there's no telling if that screen is configured the exact same way by its manufacturer. I hope so, because one of the header pins is labelled "MISO (SDO)". Maybe I'll try that one with an Arduino board.I have actually now realised there is a way to get manual control over the CS line. I simply have to set thespics_io_numfield in thespi_device_interface_config_tstruct to -1 (meaning "not used"). Then I can toggle CS whenever I like.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 07, 2021, 02:51:24 pm
content: I changed my code to toggle CS manually, rather than letting the ESP32 hardware do it automatically, so CS stays low between command and data on a read. Now it works, and I get some data!I don't understand what the datasheet is on about when it talks about dummy reads, though. It seems to imply that you need to supply 8 dummy clocks after the command before the data is output by the ILI9341. So, essentially, the first byte read should be ignored. But I'm not seeing that behaviour - the first byte read appears to be the actual data.

author: DrG
author_rank: Super Contributor
timestamp: December 07, 2021, 03:54:36 pm
content: Quote from: HwAoRrDk on December 07, 2021, 02:51:24 pmGood deal!RE: Dummy reads; from that previously posted link, last post by David Prentice..." I use SPI, and you need to read 4 bytes. i.e. 1 dummy, then R, G, B. Then I convert the 3 RGB bytes to a 5-6-5 uint16_t."So, I would be inclined to think that there is something to it because the datasheet includes it and someone with experience concurs. In my experience, when I am in that position ("I don't know what the datasheet is going on about"), I usually find out that I was wrong

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 07, 2021, 04:35:34 pm
content: I've actually figured out a way to have CS be kept asserted between SPI transactions and still have CS managed by hardware.You can set theSPI_TRANS_CS_KEEP_ACTIVEflag in thespi_transaction_tstruct, and it will not take CS back high after that transaction is finished. However, you have to ensure to callspi_device_acquire_bus()before making your transactions, andspi_device_release_bus()after. Otherwise, you will getESP_ERR_INVALID_ARGerrors fromspi_device_polling_transmit(),spi_device_transmit(), etc.It doesn't help that this flag is very poorly documented. All mentions of this flag other than the macro definition itself actually refer to it using the wrong name,SPI_DEVICE_CS_KEEP_LOW.

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 07, 2021, 04:59:05 pm
content: Quote from: HwAoRrDk on December 07, 2021, 04:35:34 pmI somehow fail to see how it's better than toggling it manually, as you still do the same manual control of CS, just through different functions.