TFT display using NV3007 controller - Page 1
Forum section: electronics / projects

author: ataradov
author_rank: Super Contributor
timestamp: September 01, 2025, 04:08:07 am
content: It is a very long shot, but may be someone worked with TFT displays using NV3007 controller? It seems to be very new and it seems to have issues.I'm trying to bring up ER-TFTM1.65-2 (142x428) and overall it works, but it looks like adjacent memory locations are overwritten in some cases.A simple test case - start with a black background and draw a green 3x3 square with increasing level of G value (from 0 to 255). After that loop is done, there will be a few pixels of half-lit green around the 3x3 square. This also happens with blue component, but not red. It also does not happen if there is already non-black background around the square.And I don't think this is interface issue, since I tried both hardware SPI and bit-banging at a very slow speed. And it happens in both 16- and 24-bit mode.And it can't be interface issue, since writing the square involves a command to set the region and then just wiring 9 color values. It is impossible to miss that square.I have not figured out how to read the memory out of this chip yet to confirm that the values are present in the actual memory.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 01, 2025, 04:41:20 am
content: Just to clarify:You set a region using commands 0x2A and 0x2B, and write data to it using command 0x2C (and optionally followed by one or more 0x3C commands for more data). If the pixels immediately outside the region were white, you see some color data bleed-through?I also assume you are using theNV3007 datasheetfor the command details, and the power-on sequence on page 68?The only thing that immediately comes to mind — I've considered getting one of these, but haven't yet — is that the datasheet mentions on page 106 that if you use 0x2A+0x2B+0x3C, i.e. a memory write continue (instead of 0x2C memory write) after setting the region, the data is written to undefined address.

author: ataradov
author_rank: Super Contributor
timestamp: September 01, 2025, 04:57:57 am
content: That is correct. Set 3x3 region using 2A and 2B, then write 9 pixel values (2 or 3 bytes per pixel depending on the mode). Do this multiple times with varying G value. Writing static value the same number of times does not reproduce the issue. No continue is used.If the background was white (or any other color), then nothing bad happens. If the background was black, then a few surrounding pixels (on two sides of the square) become half-lit version of the fill color (but only G and B components).I can then write a black color to the 3x3 square and the half lit pixels remain. So, it is not ghosting of the shown data, it looks like it is like this in the memory. This really looks like a bug.

author: ataradov
author_rank: Super Contributor
timestamp: September 01, 2025, 05:10:07 am
content: And actually it is not any background color, low intensity colors still get damaged. And red component seems to get damage too. It is really bizarre and hard to tell exactly what combination of factors lead to the issue. But regardless, memory outside the write region gets affected.The minimum region that causes the issue is 2x1 and the pixel under the second one gets damaged.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 01, 2025, 05:56:43 am
content: Indeed, extremely bizarre.Their 8051 demo board has bog-standard "4-wire" SPI connection, but 0.1µF + 10µF bypass capacitors on 3.3V VDD, and the initialization function isCode://=============================================================//LCD Initialvoid lcd_Initial(void){CS=1;delayms(5);RES=0;delayms(10);RES=1;delayms(50);Write_Cmd(0xff);Write_Cmd_Data(0xa5);Write_Cmd(0x9a);Write_Cmd_Data(0x08);Write_Cmd(0x9b);Write_Cmd_Data(0x08);Write_Cmd(0x9c);Write_Cmd_Data(0xb0);Write_Cmd(0x9d);Write_Cmd_Data(0x16);Write_Cmd(0x9e);Write_Cmd_Data(0xc4);Write_Cmd(0x8f);Write_Cmd_Data(0x55);Write_Cmd_Data(0x04);Write_Cmd(0x84);Write_Cmd_Data(0x90);Write_Cmd(0x83);Write_Cmd_Data(0x7b);Write_Cmd(0x85);Write_Cmd_Data(0x33);Write_Cmd(0x60);Write_Cmd_Data(0x00);Write_Cmd(0x70);Write_Cmd_Data(0x00);Write_Cmd(0x61);Write_Cmd_Data(0x02);Write_Cmd(0x71);Write_Cmd_Data(0x02);Write_Cmd(0x62);Write_Cmd_Data(0x04);Write_Cmd(0x72);Write_Cmd_Data(0x04);Write_Cmd(0x6c);Write_Cmd_Data(0x29);Write_Cmd(0x7c);Write_Cmd_Data(0x29);Write_Cmd(0x6d);Write_Cmd_Data(0x31);Write_Cmd(0x7d);Write_Cmd_Data(0x31);Write_Cmd(0x6e);Write_Cmd_Data(0x0f);Write_Cmd(0x7e);Write_Cmd_Data(0x0f);Write_Cmd(0x66);Write_Cmd_Data(0x21);Write_Cmd(0x76);Write_Cmd_Data(0x21);Write_Cmd(0x68);Write_Cmd_Data(0x3A);Write_Cmd(0x78);Write_Cmd_Data(0x3A);Write_Cmd(0x63);Write_Cmd_Data(0x07);Write_Cmd(0x73);Write_Cmd_Data(0x07);Write_Cmd(0x64);Write_Cmd_Data(0x05);Write_Cmd(0x74);Write_Cmd_Data(0x05);Write_Cmd(0x65);Write_Cmd_Data(0x02);Write_Cmd(0x75);Write_Cmd_Data(0x02);Write_Cmd(0x67);Write_Cmd_Data(0x23);Write_Cmd(0x77);Write_Cmd_Data(0x23);Write_Cmd(0x69);Write_Cmd_Data(0x08);Write_Cmd(0x79);Write_Cmd_Data(0x08);Write_Cmd(0x6a);Write_Cmd_Data(0x13);Write_Cmd(0x7a);Write_Cmd_Data(0x13);Write_Cmd(0x6b);Write_Cmd_Data(0x13);Write_Cmd(0x7b);Write_Cmd_Data(0x13);Write_Cmd(0x6f);Write_Cmd_Data(0x00);Write_Cmd(0x7f);Write_Cmd_Data(0x00);Write_Cmd(0x50);Write_Cmd_Data(0x00);Write_Cmd(0x52);Write_Cmd_Data(0xd6);Write_Cmd(0x53);Write_Cmd_Data(0x08);Write_Cmd(0x54);Write_Cmd_Data(0x08);Write_Cmd(0x55);Write_Cmd_Data(0x1e);Write_Cmd(0x56);Write_Cmd_Data(0x1c);//goa map_selWrite_Cmd(0xa0);Write_Cmd_Data(0x2b);Write_Cmd_Data(0x24);Write_Cmd_Data(0x00);Write_Cmd(0xa1);Write_Cmd_Data(0x87);Write_Cmd(0xa2);Write_Cmd_Data(0x86);Write_Cmd(0xa5);Write_Cmd_Data(0x00);Write_Cmd(0xa6);Write_Cmd_Data(0x00);Write_Cmd(0xa7);Write_Cmd_Data(0x00);Write_Cmd(0xa8);Write_Cmd_Data(0x36);Write_Cmd(0xa9);Write_Cmd_Data(0x7e);Write_Cmd(0xaa);Write_Cmd_Data(0x7e);Write_Cmd(0xB9);Write_Cmd_Data(0x85);Write_Cmd(0xBA);Write_Cmd_Data(0x84);Write_Cmd(0xBB);Write_Cmd_Data(0x83);Write_Cmd(0xBC);Write_Cmd_Data(0x82);Write_Cmd(0xBD);Write_Cmd_Data(0x81);Write_Cmd(0xBE);Write_Cmd_Data(0x80);Write_Cmd(0xBF);Write_Cmd_Data(0x01);Write_Cmd(0xC0);Write_Cmd_Data(0x02);Write_Cmd(0xc1);Write_Cmd_Data(0x00);Write_Cmd(0xc2);Write_Cmd_Data(0x00);Write_Cmd(0xc3);Write_Cmd_Data(0x00);Write_Cmd(0xc4);Write_Cmd_Data(0x33);Write_Cmd(0xc5);Write_Cmd_Data(0x7e);Write_Cmd(0xc6);Write_Cmd_Data(0x7e);Write_Cmd(0xC8);Write_Cmd_Data(0x33);Write_Cmd_Data(0x33);Write_Cmd(0xC9);Write_Cmd_Data(0x68);Write_Cmd(0xCA);Write_Cmd_Data(0x69);Write_Cmd(0xCB);Write_Cmd_Data(0x6a);Write_Cmd(0xCC);Write_Cmd_Data(0x6b);Write_Cmd(0xCD);Write_Cmd_Data(0x33);Write_Cmd_Data(0x33);Write_Cmd(0xCE);Write_Cmd_Data(0x6c);Write_Cmd(0xCF);Write_Cmd_Data(0x6d);Write_Cmd(0xD0);Write_Cmd_Data(0x6e);Write_Cmd(0xD1);Write_Cmd_Data(0x6f);Write_Cmd(0xAB);Write_Cmd_Data(0x03);Write_Cmd_Data(0x67);Write_Cmd(0xAC);Write_Cmd_Data(0x03);Write_Cmd_Data(0x6b);Write_Cmd(0xAD);Write_Cmd_Data(0x03);Write_Cmd_Data(0x68);Write_Cmd(0xAE);Write_Cmd_Data(0x03);Write_Cmd_Data(0x6c);Write_Cmd(0xb3);Write_Cmd_Data(0x00);Write_Cmd(0xb4);Write_Cmd_Data(0x00);Write_Cmd(0xb5);Write_Cmd_Data(0x00);Write_Cmd(0xB6);Write_Cmd_Data(0x32);Write_Cmd(0xB7);Write_Cmd_Data(0x7e);Write_Cmd(0xB8);Write_Cmd_Data(0x7e);Write_Cmd(0xe0);Write_Cmd_Data(0x00);Write_Cmd(0xe1);Write_Cmd_Data(0x03);Write_Cmd_Data(0x0f);Write_Cmd(0xe2);Write_Cmd_Data(0x04);Write_Cmd(0xe3);Write_Cmd_Data(0x01);Write_Cmd(0xe4);Write_Cmd_Data(0x0e);Write_Cmd(0xe5);Write_Cmd_Data(0x01);Write_Cmd(0xe6);Write_Cmd_Data(0x19);Write_Cmd(0xe7);Write_Cmd_Data(0x10);Write_Cmd(0xe8);Write_Cmd_Data(0x10);Write_Cmd(0xea);Write_Cmd_Data(0x12);Write_Cmd(0xeb);Write_Cmd_Data(0xd0);Write_Cmd(0xec);Write_Cmd_Data(0x04);Write_Cmd(0xed);Write_Cmd_Data(0x07);Write_Cmd(0xee);Write_Cmd_Data(0x07);Write_Cmd(0xef);Write_Cmd_Data(0x09);Write_Cmd(0xf0);Write_Cmd_Data(0xd0);Write_Cmd(0xf1);Write_Cmd_Data(0x0e);//LCD_WR_REG(0xF9);//LCD_WR_Byte(0x17);//LCD_WR_REG(0xf2);//LCD_WR_Byte(0x2e);//LCD_WR_Byte(0x1b);//LCD_WR_Byte(0x0b);//LCD_WR_Byte(0x20);//LCD_WR_REG(0xF9);Write_Cmd_Data(0x17);Write_Cmd(0xf2);Write_Cmd_Data(0x2c);Write_Cmd_Data(0x1b);Write_Cmd_Data(0x0b);Write_Cmd_Data(0x20);////1 dotWrite_Cmd(0xe9);Write_Cmd_Data(0x29);Write_Cmd(0xec);Write_Cmd_Data(0x04);//TEWrite_Cmd(0x35);Write_Cmd_Data(0x00);Write_Cmd(0x44);Write_Cmd_Data(0x00);Write_Cmd_Data(0x10);Write_Cmd(0x46);Write_Cmd_Data(0x10);Write_Cmd(0xff);Write_Cmd_Data(0x00);Write_Cmd(0x3a);Write_Cmd_Data(0x05);Write_Cmd(0x36); /* Memory Access Control */if (DERECTION == 0){Write_Cmd_Data(0x00);}else if (DERECTION == 1){Write_Cmd_Data(0xC0);}else if (DERECTION == 2){Write_Cmd_Data(0x60);}else{Write_Cmd_Data(0xA0);}Write_Cmd(0x11);delayms(120);Write_Cmd(0x29);}with bit-banged SPI,Code://===============================================================//write parametervoid Write_Cmd_Data (unsigned char CMDP){ unsigned char i;CS=0;DC=1;for(i=0;i<8;i++){ SCL=0;if(CMDP&0x80) SDI=1;else SDI=0;SCL=1;CMDP=CMDP<<1;}CS=1;}//=============================================================//write commandvoid Write_Cmd(unsigned char CMD){ unsigned char i;CS=0;DC=0;for(i=0;i<8;i++){ SCL=0;if(CMD&0x80) SDI=1;else SDI=0;SCL=1;CMD=CMD<<1;}CS=1;}//==============================================================//write data wordvoid Write_Data_U16(unsigned int y){unsigned char m,n;m=y>>8;n=y;Write_Data(m,n);}//===================================================================//write data bytevoid Write_Data(unsigned char DH,unsigned char DL){unsigned char i;CS=0;DC=1;for(i=0;i<8;i++){ SCL=0;if(DH&0x80) SDI=1;else SDI=0;SCL=1;DH=DH<<1;}for(i=0;i<8;i++){ SCL=0;if(DL&0x80) SDI=1;else SDI=0;SCL=1;DL=DL<<1;}CS=1;}The code to set the update region has weird offsets, too:Code://===============================================================void LCD_SetPos(unsigned int Xstart,unsigned int Ystart,unsigned int Xend,unsigned int Yend){if(DERECTION==0){Write_Cmd(0x2a);Write_Cmd_Data((Xstart+0x0c)>>8);Write_Cmd_Data(Xstart+0x0c);Write_Cmd_Data((Xend+0x0c)>>8);Write_Cmd_Data(Xend+0x0c);Write_Cmd(0x2b);Write_Cmd_Data(Ystart>>8);Write_Cmd_Data(Ystart);Write_Cmd_Data(Yend>>8);Write_Cmd_Data(Yend);Write_Cmd(0x2c);}else if(DERECTION==1){Write_Cmd(0x2a);Write_Cmd_Data((Xstart+0x0e)>>8);Write_Cmd_Data(Xstart+0x0e);Write_Cmd_Data((Xend+0x0e)>>8);Write_Cmd_Data(Xend+0x0e);Write_Cmd(0x2b);Write_Cmd_Data(Ystart>>8);Write_Cmd_Data(Ystart);Write_Cmd_Data(Yend>>8);Write_Cmd_Data(Yend);Write_Cmd(0x2c);}else if(DERECTION==2){Write_Cmd(0x2a);Write_Cmd_Data(Xstart>>8);Write_Cmd_Data(Xstart);Write_Cmd_Data(Xend>>8);Write_Cmd_Data(Xend);Write_Cmd(0x2b);Write_Cmd_Data((Ystart+0x0e)>>8);Write_Cmd_Data(Ystart+0x0e);Write_Cmd_Data((Yend+0x0e)>>8);Write_Cmd_Data(Yend+0x0e);Write_Cmd(0x2c);}else{Write_Cmd(0x2a);Write_Cmd_Data(Xstart>>8);Write_Cmd_Data(Xstart);Write_Cmd_Data(Xend>>8);Write_Cmd_Data(Xend);Write_Cmd(0x2b);Write_Cmd_Data((Ystart+0x0c)>>8);Write_Cmd_Data(Ystart+0x0c);Write_Cmd_Data((Yend+0x0c)>>8);Write_Cmd_Data(Yend+0x0c);Write_Cmd(0x2c);}}(Apologies for not fixing whitespace issues in their code.)Perhaps the issue is that the controller defaults — 0x9A defaulting to 0x28, but the 8051 example code setting it to 0x08 — are not suitable for this display, and we need to copy the settings from the 8051 example code above at initialization? I've seen that before.

author: ataradov
author_rank: Super Contributor
timestamp: September 01, 2025, 06:08:56 am
content: I've seen that code, it is impossible to do anything from just the datasheet, since there are a few undocumented registers that are necessary for operation. And unlike most other TFT controllers, this one needs a ton of random voltage settings for the panel. Most of those registers set by the code matter. But I did exact same initialization in exact same order.I really want to read back the RAM, but I'm not doing something right and the datasheet is entirely unclear on how to do it. I assume it should be similar to writing, but I'm not getting anything in response.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 01, 2025, 06:32:44 am
content: Do you have a second unit to verify you don't just happen to have a b0rked unit? I always get at least two just for that reason; my luck is so rotten that if I don't, the one I get will be b0rked.

author: amyk
author_rank: Super Contributor
timestamp: September 01, 2025, 07:34:27 am
content: This controller uses the old Philips-compatible instruction set that goes back to at least the PCF8833 (well known for being used in Nokia mobiles' screens) if not earlier. Yes it is possible to read back the RAM, this is command 2E.

author: ataradov
author_rank: Super Contributor
timestamp: September 01, 2025, 02:48:47 pm
content: All modern controllers use very similar instruction set, yet memory readback does not work in half of them via serial interfaces, only through parallel one.In this case, If I try to issue 2E, set DC=1 and start clocking out the data, I get all 0s.

author: ataradov
author_rank: Super Contributor
timestamp: September 01, 2025, 03:12:21 pm
content: I also contacted BuyDisplay and asked for their help, and they asked for a picture of the bug. So, I came up with a dead simple code to demo it.Code:for (int c = 0; c < 256; c++){display_set_color(0, c, 0);display_fill_rect(30, 30, 200, 1);}display_set_color(0, 0, 0);display_fill_rect(30, 30, 100, 1);This code shows 1 pixel line with all shades of a green color. After it reaches the maximum intensity, it erases half of the line. You can clearly see the place where pixels of the next line are half-lit in green color.This happens for all colors, not just green. But this does not happen if you do display_set_color(c, c, c); to get a white line in the end.

author: amyk
author_rank: Super Contributor
timestamp: September 02, 2025, 01:37:49 am
content: Quote from: ataradov on September 01, 2025, 02:48:47 pmThe NV3007 datasheet says that it supports readback; see section 4.1.1.2. It only has serial interfaces anyway. Do the Read ID (04) or Read Status (09h) commands work?

author: ataradov
author_rank: Super Contributor
timestamp: September 02, 2025, 01:57:15 am
content: Yes, read ID and read status work. The read diagrams (rather redundantly) show reading of 1, 2, 3 and 4 bytes, which I presume is done to show all possible register reads. But there are no diagrams for reading of SRAM. And I tried a bunch of stuff, and all I ever get in return is 0.

author: ataradov
author_rank: Super Contributor
timestamp: September 03, 2025, 12:39:17 am
content: I'm talking to the BuyDisplay support, and so far they have been less than useless. Not unexpected, but still disappointing.At the same time, I reproduced the issue with their drivers for Raspberry Pi. So, it is not my code.I sent that to the support, we'll see what they do. So far, they have not taken this issue seriously. I strongly suspect this is the controller issue. It seems to be very new and EastRising are the only users. I would not be surprised if the company making it is affiliated with EastRising.I think I can workaround this for my use case, but for real applications, I would avoid using displays with his controller.Also, I tried a second unit, same issue. So, it is not a single device defect.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 03, 2025, 01:22:32 am
content: Quote from: ataradov on September 03, 2025, 12:39:17 amVery much appreciated, Alex; thank you. Very sad to see EastRising/BuyDisplay not taking this seriously; I guess I better start looking at more reliable suppliers too.I had considered this display for my next hobby LTE router project, based onOrangePi RV2(I recently got one), orBananaPi BPI-F3(which has a miniPCIe connector with SIM, suitable for USB-based 4G/LTE modems), both having SPI and GPIO available on a pin header, to be running OpenWRT and the status display controlled by my own small native daemon. The size and resolution would have been a very good fit, but the lack of capacitive touch option made me look elsewhere. Nontechnical people often worry about "pushing the wrong button" or "did it fail because I sometimes pushed that button?", but not similarly associating tapping a display as affecting the device state.

author: ataradov
author_rank: Super Contributor
timestamp: September 03, 2025, 01:35:54 am
content: It is a really nice display and high resolution for its size. I've looked around and I don't see anything similar. Their RPi demo includes a few vibrant pictures and color rendering and viewing angles are excellent.They had to go with a different controller, since its resolution is not going to work with typical ST77xx/ILI9341, but it seems like that NV3007 was not the right choice.Unless they identify some issue that can be fixed by register settings, the best we can do is characterize the behavior and try to figure out ways to avoid it. For example, erase to black before writing the data avoids the issue. But results in flickering if large areas are updated. It would be application-specific workarounds. Updating the whole display area also works, but that's a lot of pixels to store and push over SPI.

author: amyk
author_rank: Super Contributor
timestamp: September 03, 2025, 01:52:50 am
content: Fill the entire RAM with pure white, then set column and row address to 0 before issuing 2E command; do you still read back all 0? Note that D/C must be set to Command (0) when clocking in the 2E, and should be irrelevant thereafter when reading out the data.If that doesn't work either, I'm not surprised - maybe they only implemented and tested the bare minimum and don't expect the host to do anything but treat it as a dumb framebuffer, while copy-pasting the datasheet from everyone else that claims to have a Philips-compatible controller (e.g. ST7732's datasheet has many paragraphs which are 100% exactly the same as the NV3007.)Nonetheless, the "bleeding" effect you get looks like a failure to combine pixel values correctly in the hardware.

author: ataradov
author_rank: Super Contributor
timestamp: September 03, 2025, 02:02:33 am
content: I tried filling the whole RAM with non-zero value (gray to see something change and not just get a solid value). I also tried different window positions and sizes. None of that helps.I also can't get out of that read mode mode. Once you enter, all you ever get is 0 regardless of any following commands and CS or D/C values.There should not be any combining even in theory. So, it is not even clear how it can break like this. The only thing I can imagine is weak SRAM and accessing cells also toggles some bits in the neighboring cells.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 03, 2025, 03:43:48 am
content: The OTP options mentioned in the NV3007 datasheet makes me feel this is not at all a well-tested practical controller, more like a prototype, or a soft core implemented on an OTP FPGA/ASIC. Me no like. Some others have their quirks and variances from datasheets, but this one just feels buggy, incomplete.Quote from: ataradov on September 03, 2025, 01:35:54 am121,552 bytes of pixel data if using RGB565; 182,328 bytes if using 24bpp with the possible unaligned access penalties that brings; 243,104 bytes if using 32bpp as is optimal on 32-bit microcontrollers and SBCs, and even some 64-bit ones.Pity no display controller I've seen supports 4-bit/8-bit LUT input, with 16×18-bit/256×18-bit programmable table applied at memory write time. If they did, then 60,776 bytes would suffice for 256 colors, and 30,388 for 16 colors. I did some EGA/VGA stuff back in the day, and that can be made to suffice for an UI, just not for images or video. It would make such small displays so much more useful for smaller microcontrollers. 4-bit LUT would be particularly useful for rendering antialiased text on a single-color background.NV3007 has no way to change the physical refresh rate that I can tell, or even reliably know the actual oscillator frequency it is using, which means one is likely stuck with the approximately 60 Hz refresh rate the datasheet hints at. A full display update within each physical refresh would require something like 86 MHz SPI clock for RGB666/RGB888, or 58 MHz for RGB565. The datasheet does claim the controller is capable of 100 MHz SPI clock, but I'm not so sure that is realistic at all, especially over an FPC cable.That said, as long as maximum update is less than half the physical refresh rate, one can almost halve the pixel data transfer rate and chase the physical refresh with new data, without visible tearing. As soon as physical refresh has reached say third or fourth scan line, you start updating the data. The physical refresh will be faster, so after the midpoint of the data update, the next physical refresh starts chasing the update. As long as you complete the data for the final scan line before the physical refresh for the next frame reaches that scan line, there will be no tearing. (So, you don't have two full physical frame updates, but one vertical blanking and refresh period less.)

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 03, 2025, 04:05:54 am
content: Quote from: ataradov on September 03, 2025, 02:02:33 amIt could be a design limitation due to internal graphics RAM organization into bytes, and not into 6-bit/18-bit units.Typically these controllers read a whole scan line into a buffer of some sort, because they drive the display transistors in parallel.When one uses a limited region, only a part of each scan line is modified. Most controllers cannot do a read-modify-write cycle, and instead only write to the modified 6-bit or 18-bit memory cells as data comes in, always expanding the written data to 3×6=18 bits first. (For RGB444 and RGB565, it is often implemented by copying the most significant bit(s) of the color component involved, and not a look-up table. Such conversion is mathematically as close to perfect as is possible to get, though.)It is rather obvious that the bug in NV3007 is in this one-by-one write part, with it somehow leaking data to the memory cell just past the first/last cell within the set update rectangle.It could be that to save IP costs, NV3007 uses 8-bit/24-bit memory structures, and just distributes the 2556 bits in each scan line to 320 bytes. For the physical refresh, that would be irrelevant, because it reads all 142×3×6 = 2556 bits in parallel. Their per-cell update would be designed to work for full display updates, but fail occasionally because pixels and the underlying memory storage units just do not match — thus writing data-dependent garbage to the "future" bits that it should keep unmodified because they are not part of the current pixel —, and correcting the bug would require a read-modify-write cycle, which is not feasible here. If the error pattern changes based on the update rectangle location (in the short axis) in a periodic manner, that would confirm this possibility. Similarly, if a full-screen update is terminated early, the pixel following the last written one should contain garbage (at least for some fill values/patterns). If they use 6-bit color components in 8-bit bytes, then we expect there to be three error patterns, repeating every four pixels, because 6:8 = 3:4.)I'm only speculating here, of course, but nothing foil-hattery here: these speculations could be tested and ruled out.

author: ataradov
author_rank: Super Contributor
timestamp: September 03, 2025, 04:12:43 am
content: I think OTP are a bonus that in theory will let display vendors configure all the registers and avoid 100 proprietary register writes of values that are meaningless to regular users and are fixed for a display. But who knows.There is no way to tell when refresh happens. There is configuration for the H-Sync and V-Sync outputs, but they are not routed out to the connectors on the displays.But for small displays like this, I doubt tearing is going to be a huge issue. You are not going to play movies on it.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 03, 2025, 05:10:21 am
content: Not movies, but animation yes. Tearing is very noticeable with any kind of animation or movement. I also use current-based backlight control (rather than PWM'ing at full current), because I hate flicker and tearing. Now playing withTI TPS92201Afor parallel-LED backlighting, buck mode from 5V supply.

author: SYOM
author_rank: Newbie
timestamp: September 03, 2025, 08:23:52 am
content: I’ve seen similar behaviour on some newer display controllers — it can look like adjacent memory is being “smeared” even though you’re only writing the intended pixels. In my case it turned out to be related to the address window not resetting properly after the write. You might try explicitly re-sending the column/row set commands (0x2A/0x2B) before every small region write, even if you think the window hasn’t changed. Some controllers seem to need that “refresh” to avoid spilling data into neighbouring addresses.Also worth checking whether you’re using the right pixel format initialization sequence — a mismatch between 16-bit and 18-bit mode can produce ghost pixels in green/blue channels but not red. If you can, try reading back GRAM contents after your draw to confirm whether the overwrite is actually happening in memory or just on the display pipeline.

author: ataradov
author_rank: Super Contributor
timestamp: September 03, 2025, 01:21:36 pm
content: I do reset the window before each write. And I do use the right format. I tried both formats and they both behave the same way.This is really a bug in the display controller. I don't see why it is hard to believe. This controller is less than two years old and used in only 2 displays. And I'm having a really hard time convincing the vendor to run a trivial test on their side. They are coming back with trivial modifications to the test code that do absolutely nothing.

author: amyk
author_rank: Super Contributor
timestamp: September 04, 2025, 02:46:28 am
content: If the readback command is that broken, I'm not surprised at how buggy and incomplete the rest of the controller is.Quote from: Nominal Animal on September 03, 2025, 03:43:48 amYou mean like this? (See attached.)

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 04, 2025, 03:31:32 am
content: Quote from: amyk on September 04, 2025, 02:46:28 amNot exactly. ST7637 uses three separate LUTs, one each for red, green, and blue. In 8-bit mode, you have 8 red values, 8 green values, and four blue values (RGB332). In C terms, it implements something likeR = RLUT[byte>> 5]G = GLUT[(byte>> 2) & 7]B = BLUT[byte& 3]whereas what I want in 8-bit mode is simplyR = RLUTG = GLUTB = BLUTThe entries inRLUT,GLUT, andBLUTabove are 6-bit values, for an 18-bit RGB666 combined result.Similarly for 4-bit mode, except thatbyteis within 0..15, inclusive.The 8-bit mode would be useful for MCUs with enough RAM for a byte-per-pixel framebuffer, setting the three LUTs to match the "palette". Photoshop, GIMP, et al. call that "indexed color" mode, where you have 256 palette entries (each defined as some 24-bit RGB color).The 4-bit mode would be extremely useful for high-fidelity text and single-color symbols, because it would allow antialiasing and storing the symbols at half a byte per pixel. To draw a character, you'd set up the LUTs as a perceptively linear gradient from background to foreground color, and the target rectange for that glyph, and then just stream the binary data from Flash to the display. Repeat for the next character, optionally changing the LUTs in between to change colors. The 4-bit data in memory is simply opacity/transparency/alpha, with 0 being transparent and 15 fully opaque. If you don't use DMA for transferring the data, there are many techniques to decompress the glyph data on the fly, to reduce Flash size needed even further. (Then again, 128 glyphs at 16×16 pixel size only takes 16k of Flash, uncompressed. And that is with full antialiasing.)Of course, the nibble data does not have to be opacity, it could be color indexes to some 16-color palette as well. Then, instead of setting the LUTs based on foreground and background colors, it is simply set to the appropriate palette before transferring the data. The limitation is that the symbol is always rectangular, because these controllers typically cannot "skip" writing a random pixel within the region, due to hardware implementation details and tight timing requirements.

author: ataradov
author_rank: Super Contributor
timestamp: September 04, 2025, 01:50:03 pm
content: I'm still trying to convince them that there is an issue, but the response from last night is too hilarious not to share.Here is a picture with a "fix" they provided. It literally comments out the code that reproduces the issue and replaces it with a code that just shows the final expected result.So, I would assume that this controller is bugged, and there are no competent people reachable by email that can actually look into that issue, so I doubt it will ever get fixed.

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 04, 2025, 05:34:58 pm
content: Good to know. Haven't run into this controller yet.

author: ataradov
author_rank: Super Contributor
timestamp: September 04, 2025, 06:15:56 pm
content: I doubt you will any time soon. There seem to be only two panels using that controller. And I would certainly avoid it from now on.But at least after a bunch of emails I convinced them to run the code. Before that they would come up with irrelevant code changes and ask "see if that works".

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 04, 2025, 09:48:15 pm
content: Because of how I usually test and use these controllers — I compose the visuals from a full-color background layer and one or more LUT-based 8-bit layers on top — I usually update full display contents, so probably wouldn't have even caught this, until I actually needed a region update. Now I know what to test and why. Thanks.

author: ataradov
author_rank: Super Contributor
timestamp: September 05, 2025, 01:59:14 pm
content: We finally have a confirmation of the bug:QuoteThe only solution is pre-erase the updated area. If updating from any color other than black, there is a chance that neighboring pixels will be affected. Note that it does not always happen, I will try to figure out what are the exact conditions for this to happen. There may be there are some specific bit patterns to avoid, and it may be possible to automatically pick the closest best working color.I will go ahead with my application and we'll see how annoying it gets.

author: amyk
author_rank: Super Contributor
timestamp: September 06, 2025, 01:35:27 am
content: That's a good development, as I was going to suggest contacting the controller manufacturer directly:http://www.newvisionu.com.cn/They claim to have been around since 2005, but given that the datasheet I have is version 1.0 from June 2024, and this is by no means a new style of controller, I'm rather doubtful. In any case, this thread serves as a good warning to others, and "negative marketing" for the manufacturer --- if you can't even bother to implement and test the basic functionality (like readback) that you claim in your datasheet, you deserve to be ridiculed and avoided.

author: ataradov
author_rank: Super Contributor
timestamp: September 06, 2025, 02:41:32 am
content: So, I think I found a somewhat universal solution - mask two lower bit of each R,G,B component. This obviously limits the dynamic range, but for UI-type applications it likely does not matter. This is just a change that need to happen in one place - where color is combined into a bitmask for the display.It looks like transition though low values causes the problem. So, if you iterate though values 0,1,2,3,4,5,6,7... you will get the issue. If you do 0,4,5,6,7..., it works fine. So, if you need full dynamic range, you can apply other workarounds that avoid that specific pattern.You can also check for minimum value and filter out low values that are not 0. This will keep the resolution high on the high end. But this extra comparisons, and in most cases you will likely not see a difference.For my use it will be fine, but generally it would be nice to avoid buggy hardware. But I don't see any other displays with similar size and resolution. None of the really wide spread controllers can handle 428 pixel displays. So, they had to go to something else, and that did not work out too well.At the same time, I really don't understand how this did not come up in testing. Filling a square with all the colors is literally the first test you naturally do.

author: ataradov
author_rank: Super Contributor
timestamp: September 06, 2025, 02:53:56 am
content: Well, nevermind. It also depends on the fill color of the surrounding pixels. What I wrote above is true for the black color. As soon as you have any other color in the background, it all breaks again.Or, it looks like it works if you limit all colors to the top 3 bits. So, R and B get 2 lower bits masked and G gets 3 lower bits masked. This seems to do the trick regardless of the background color.This does get pretty rough on the palette, so just clamping the colors may be a way to go:Code:if (r > 0 && r < 32)r = 32;if (g > 0 && g < 32)g = 32;if (b > 0 && b < 32)b = 32;It does looks like some sort of memory update that they messed up. Still puzzles me how it passed any QC at tow different vendors.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 06, 2025, 04:04:08 am
content: Quote from: ataradov on September 06, 2025, 02:53:56 amDo you mean it works if in every used color each component is either zero, or at least one of the top 3 bits is nonzero?Or that all least significant bits in color components must be zero, except for the 3 top bits?For RGB565 mode, the first one leaves (32-7)×(64-15)×(32-7) = 30625 colors out of 65536; the second one just 8×8×8 = 512 colors.If the underlying cause is non-pixel-aligned memory access (i.e., storing 6 bits per color component in 8/9/16/18/32/36-bit words), then one would expect least significant bits to leak in one direction, and most significant bits in the opposite direction, depending on whether you issue command 0x36 0x00 (default), or 0x36 0x80 (which mirrors the memory write direction along the short axis).You could also check if changing the physical update order (bits 2 and 4 for command 0x36) makes any difference. Using 0x36 0x04 mirrors the contents along the long axis, 0x36 0x10 along the short axis, and 0x36 0x14 along both axes rotating the display 180°. The parameter is a single byte bit mask described on page 100, with the two least significant bits always zero.

author: ataradov
author_rank: Super Contributor
timestamp: September 06, 2025, 04:10:45 am
content: Yes, the fastest way is to limit yourself to 512 colors. This is achieved by masking, so it is just a matter of changing the constants in the existing code.What really matters is non-zero color values below 32 (in a 0-255 scale). So, if you just eliminate those colors, you will get almost full dynamic range, but at the expense of doing those checks for each color change. Or if you are picking all the colors (as may happen in UIs), then just don't pick ones where RGB components are below 32. Round them down to 0 or up to 32.None of the reflections or update order change anything. It is not memory read issue when displaying, it is an issue with writes.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 06, 2025, 06:51:52 am
content: Quote from: ataradov on September 06, 2025, 04:10:45 amI fully agree.My theory is that the implementation of commands 0x2C (Write Memory) and 0x3C (Write Memory Continue) is buggy by hardware, due to memory storage units not being pixel-aligned.If we look at the hardware of the controller, it is pretty obvious that the physical refresh uses three buffers of 168×6 bits, one buffer for each color component. It is read and cached as a full scanline (in three parts, one part for each color component), because each buffer drives a set of 168 transistors in parallel. How it is split into storage units does not matter at all. It is likely the read port is optimized for this, and the 0x2E (Read Memory) not implemented at all because it would require reading the memory for pairs of pixels and extracting the target pixel from unaligned bits: too complicated. In order to not raise any questions, they didn't document these deficiencies in the datasheet, and just copied ILI9341/ST7789/etc. commands.If you implement this storage as 8-bit units using cheap existing IP — and I am assuming that 6-bit "byte" SRAM is less common/cheap/available than 8-bit "byte" SRAM —, you can implement the SRAM using 3×1008×428 bits, say 3×126×428 bytes, and still support both memory write commands. Then, three out of four pixels are split across two consecutive memory bytes (but depends on the underlying memory access unit size in bits).If the Memory Write commands keep track of both the last written color and the address of the next pixel to be updated, this is not a problem for full screen updates: when the memory for the pixel is split over two consecutive bytes, the earlier part of the bytes is filled from the cached last-written color value, and the later part by the current pixel. The bug occurs here: the part of the written bytes that are part of thenextpixel get written too. When the short axis latter edge is on a split pixel, the following pixel will be damaged. There is no workaround, except making sure those damaged bits are always zero (or always one, I guess).If this is the case here, changing the order of memory writes, and/or changing the start and end coordinates along the short axis, should affect the error pattern. In particular, if both the start coordinate and the length along the short axis is a multiple of 6, you should not see any bleed-out, because both the first and the last pixel on each scan line should be at a non-split pixel.(Command 0x36 controls both the physical refresh direction, and the direction in which the memory write fills the pixels in the controller framebuffer. This might change how the Memory Write commands behave. In all situations, the color component would be damaged on the next pixel to be written if there was no region set, if this description of the cause of the bug is correct.)Of course, if they do have 6-bit/18-bit SRAM as the framebuffer, then their bug is ridiculous, having a simple write to one SRAM "word" bleed to nearby other words. Could be a low-level memory access bug, a bug in the Verilog/VHDL/whatever they use, or even a high-level state machine bug. I'm just slightly addicted to finding out the causes of bugs.