Fonts, and driving LCD controllers for drawing text - Page 1
Forum section: electronics / general

author: peter-h
author_rank: Super Contributor
timestamp: June 23, 2025, 08:40:32 am
content: I've been playing around with text. The context is herehttps://www.eevblog.com/forum/microcontrollers/anyone-here-using-the-ramtex-graphics-library/msg5946664/#msg5946664The first issue one comes up against is how to display an updating value (e.g. from an ADC) without leaving artefacts at the end of the line. It seems that all fonts are defined as a rectangular matrix i.e. all the pixels forming the character's rectangle, plus a margin on the right of it, are defined. So even with a proportional font you can rewrite a line with different numbers within it, and you don't get artefactswithinthe line.But you do get them at the end of the line, if the new content is shorter. One crude fix, sufficient if the length of the line is not varying much, is to output a space char at the end. That works if the line remains in the same place, but what if you need to move it?I played around with other methods which deal with this by properly deleting the previous text. One method is to rewrite it in the background colour, but that causes a lot of flicker (168MHz 32F417 and a 10.5MHz SPI interface to the completely dumb LCD controller). Next I did the same but rewriting each char to BG and immediately replacing with the new one; that produces a lot less flicker but still visible. Even if you rewrite only text which changed, and thereafter. Next, I calculated the size of the enclosing rectangle (Ramtex provide a function for this, otherwise it would be messy for a proportional font) and drew a BG-filled rectangle; that is optimised to use DMA (to write a whole horizontal line at 10.5MHz) and is a lot better, but still visible!I reckon there is something weird about the display management because the computational overhead of font rendering etc is tiny. Admittedly, the text-overwrite-to-BG method would not be great since one is writing quite a lot of SPI data for each pixel of the character. It is done pixel by pixel because you have viewport clipping. But the rectangle should be very fast. Maybe there is a display scanning sync issue, but over SPI you have no control over sync.I also noticed that font aliasing is done at the font definition stage, by defining "grey scale" pixels. For some fonts only.I wonder if somebody here has been up this learning curve.Windows stuff is obviously different. But you have many more times CPU power, and a memory-mapped display.EDIT: Just noticed the interface has a Tearing Effect output pinbut there is no spec on how long this lasts for. One can software select it to be only H sync or V sync. So perhaps one can guess. But it will be milliseconds at most.

author: MarkF
author_rank: Super Contributor
timestamp: June 23, 2025, 09:30:44 am
content: I don't know how to completely eliminate the flicker unless the display has double buffer capabilities, where-by you write to the background buffer at a leisurely pace and then swap it with the foreground buffer.As for proportional fonts, I avoid them on small displays. If I remember correctly in Windows, the proportional fonts have the rectangle size of each letter internally so you know how many pixels a string will use. In DirectX, you can ask it how many pixels a string will take up. Helps with justification. IMHO, you just don't have the time or transfer speed for this luxury. Forget proportional fonts, aliasing, font shading and any other fancy thing you can think of. You don't have a big fancy computer with a multiple buffered display. You have a dumb display with a slow SPI channel with no bells and whistles.For small displays (I mostly use small OLEDs with SSD1351 controller), the key is to erase the previous letters as fast a possible and then only write the pixels of the letter to be displayed. DO NOT write the background pixels in the letters rectangle. Transparent text unless you REALLY want a different background color. A few things to keep in mind to speed up writing text:- DO NOT clear the entire screen and then write your text.- When updating the display, only clear small areas of the screen at a time for new text. The smaller the areas you clear the less flicker.- DO NOT write the previous text in the background color to erase it. It will probably take too much time.-When clearing old text, use built-in display functions to do it. For example the OLED displays I've been using have a function (NOT DRAW LINE) that can modify a column or row very fast. It's a feature built into the OLED controller. Use it if your display has a similar function. I believe you can give it a color to erase with (just can't remember).The main goal is to only erase small sections of the display and write your new text.The graphics library from Adafruit shows their technique for drawing text. You only write the pixels that form each letter.

author: MarkF
author_rank: Super Contributor
timestamp: June 23, 2025, 09:38:07 am
content: In addition to looking for a hardware function to clear/write a full/partial row or column,the only thing I can suggest to minimize how often you update the display.Only update the areas of the display that have changed. Possible only the rectangle containing your new ADC value.You probable will never be able to eliminate all the flicker.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: June 23, 2025, 09:42:19 am
content: As long as your update takes significantly less than the frame period, then syncing the write to vertical sync should eliminate any artefacts. You probably don't want to have to wait doing nothing for the frame sync, so teh update would be done as an asynchronous task to the main code, e.g. new ADC result -> place in a buffer and on framesync any new data in the buffer gets written to the display

author: peter-h
author_rank: Super Contributor
timestamp: June 23, 2025, 12:04:44 pm
content: QuoteThere is a tradeoff there.If you write the whole row, say 14 pixels, the SPI transfer (for each row) is quite efficient for DMA, at about 50us per row at 10MHz, so about 1ms per char:4 bytes - command and address42 bytes - 14 pixels, 3 bytes per pixelIf you write just the pixels, most of the transfers will be too short for DMA to be efficient.Aliasing seems to be encoded within the font so, again, provided you are transferring whole rows (or non-zero pixels) it doesn't involve computing.I think Windows does aliasing in software, on the fly.QuoteDefinitely that's the biggest thingProblem with using the "tear effect" signal is that only some displays bring it out on the connector.

author: tooki
author_rank: Super Contributor
timestamp: June 23, 2025, 03:10:07 pm
content: Quote from: peter-h on June 23, 2025, 12:04:44 pm*antialiasing. (Aliasing is the distortion perceivable as jaggies.)All the major operating systems have done on-the-fly antialiasing for over 30 years. Windows and Mac can also do “subpixel antialiasing” where they introduce false color to take advantage of the known locations of the RGB subpixels within each pixel, to nearly triple the effective resolution in one axis. (Though this is disappearing again with high-DPI displays, which don’t need as heavy antialiasing.)

author: peter-h
author_rank: Super Contributor
timestamp: June 23, 2025, 03:51:43 pm
content: They are probably caching the anti aliased fonts.

author: tooki
author_rank: Super Contributor
timestamp: June 23, 2025, 04:02:28 pm
content: Quote from: peter-h on June 23, 2025, 03:51:43 pmEhhhh… depends on which rendering engine.Old-school ones (where on-screen text was positioned by integer pixel position) certainly did. But the later ones (2001 onward on Mac, 2006 onward on Windows, corresponding to the introductions of Mac OS X and Vista, respectively) allow text to be placed at arbitrary, noninteger (floating point) positions to allow for better kerning, etc., and those render each character in situ because its pixel values aren’t the same as the same character placed at some different noninteger position. It’s possible that they cache (some of?) them in case another identical position/offset instance occurs (which is probably not rare, given how many texts are left- or right-justified), but it might not be necessary, given how fast a modern CPU can just render it. The cache overhead might exceed the recalculation time…Note also that antialiased text needs to be either rendered using an alpha channel (transparency), or onto a specific background color, and/or subsequently blended (not just masked) onto a background. Windows XP and classic Mac OS didn’t really do alpha properly, so if they did cache, it was probably only when rendering onto solid backgrounds. Mac OS X and Vista both introduced modern graphics engines with true alpha support.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 23, 2025, 04:43:34 pm
content: For a specific embedded application, you can use bitmaps to store pre-rendered antialiased fonts, no need to generate them on the fly (unless storage was really at a premium).I do that and store fonts as grayscale bitmaps, then render them on display with any combination of text and background colors with a function which is just basically alpha blending. The bitmap fonts store the alpha channel.I use LCD Image Converter to generate those bitmap fonts, very flexible and handy:https://lcd-image-converter.riuson.com/enYou don't need a lot of grayscale levels for good antialising results. On smaller displays (up to ~ 4-5"), I generate fonts as 4-bit grayscale bitmaps (so blending using 4-bit alpha coefficients) and the result looks pretty good.

author: Pseudobyte
author_rank: Frequent Contributor
timestamp: June 23, 2025, 05:49:14 pm
content: If you have not tried LVGL i would recommend giving it a try. The license is permissible and it is very easy to port to any micro/display combo.https://docs.lvgl.io/master/intro/introduction.htmlThey provide scripts for converting true type fonts to c arrays. You can also limit the scope of the font to only be for certain characters.

author: peter-h
author_rank: Super Contributor
timestamp: June 23, 2025, 08:10:16 pm
content: LVGL came up in the other threads. I've also checked it out. There is no indication it does any of this any differently, and if it did I would like to know how.There is obviously endless incremental optimisation one can do. I've already optimised whole-frame fills and in-viewport fills to use DMA (the required buffer is only 480x3 bytes). One could optimise font rendering but only roughly 16 pixels (48 bytes) at a time (probably worth doing).LVGL has much more functionality than Ramtex which is roughly 15 years old, but the code size is also several times bigger.

author: Pseudobyte
author_rank: Frequent Contributor
timestamp: June 23, 2025, 08:50:13 pm
content: Understood. I should mention that I can't really picture in my head what sort of artifacts you are describing.Out of curiosity why are you running your bus at only 10.5MHz? You should be able to run that up to 42MHz no problem. **Edit just checked the datasheet, looks like SPI 1 can run that fast but the other SPI peripherals can only do 21**Also another option for you would be to use the FMC to memory map the display and drive it on a parallel bus.

author: peter-h
author_rank: Super Contributor
timestamp: June 23, 2025, 09:01:12 pm
content: Say you haveTemp: 24.55Cand then you haveTemp: 24.15CUnless the 1 is same raster width as the 5 (with monospaced it would be) the 2nd C will have a little fragment after it. See the video herehttps://www.eevblog.com/forum/microcontrollers/anyone-here-using-the-ramtex-graphics-library/msg5946664/#msg5946664at 0:30 and look carefully.Now that one is easy to deal with, by adding a trailing space (or two, if the variable string it longer). But that works only if the while line stays in the same x,y origin. And it works only if you are outputting the whole char's raster rectangle; if outputting just the char's pixels then you get a load more artefactswithinthe line.On the 32F417, 168MHz, SPI1 can do 42MHz, SPI2,3 can do 21MHz. But if you also want four UARTs then SPI1 is lost (QFP100). And the ILI9488 is max 15.5MHz. The 240x240 chips can do 60MHz... Actually I can run the 9488 at 21MHz too, just for testing, just fine. The problem is slightly less visible.I might need to do the frame sync, which is a PITA. I will look at font row rendering first. The font in the video is 14px wide which is 42 bytes per row!EDIT: just stepped through the code and it turns out they are outputting one pixel at a time, for each whole row (i.e. the whole rectangle). This is as expected since each pixel is checked against the viewport. So no optimisation is possible unless one does a substantial rewrite. 6 bytes are output for each pixel, with address autoincrement. And I can't use DMA even for those 6 bytes because of D/C manipulation. Only on horizontal lines can one just set D/C=1 and output the whole (up to) 480 pixels.

author: tooki
author_rank: Super Contributor
timestamp: June 23, 2025, 10:21:14 pm
content: Quote from: peter-h on June 23, 2025, 09:01:12 pmThis is why some proportional fonts offer optional monospaced numerals.

author: peter-h
author_rank: Super Contributor
timestamp: June 24, 2025, 05:49:05 am
content: Yes; I've had those too.What I wonder is how the frame sync is implemented in real libraries.For example a string(x,y,fontname,colour) function dives deep down and does a lot of work. Ultimately it ends up calling a couple of functions to output data: set address, and send data.The colour spec is in the send-data; 1 2 or 3 bytes per pixel.Do you put the frame sync waits in these low level functions? That would give you the best granularity. With an RTOS, you would test the sync signal and if the display scan is active, you call osDelay(1) or similar, to waste 1ms. Or you could have a callback from an interrupt triggered by the sync, to empty a buffer to the display.Another way would be to put the sync check into the higher level functions, but they would all need it.How does e.g. LVGL do it?I did a search and found that limiting display update to the sync periods does not avoid all of the "dynamic" artefacts. In the end you need a frame buffer, or some sort of dual porting of video RAM. But this is a different issue to artefacts which are static, due to previous stuff not being erased properly.Incidentally, Ramtex offers font anti aliasing. There are two functions: gputs() and gputsrot(). The latter will draw text and a float angle and performs fairly crude anti aliasing if the angle is nonzero so e.g. 0.001 of 2xPI is enough to trigger anti aliasing. The price is in the execution time e.g. 120ms becomes 470msBut I noticed some fonts appear to have anti aliasing designed into them and those would render much better.

author: Pseudobyte
author_rank: Frequent Contributor
timestamp: June 24, 2025, 04:13:48 pm
content: LVGL knows what area of the display was occupied by the text previously and marks it for re-rendering by invalidating it. So this means the entire area previously occupied is redrawn.I would assume ramtex does it the same way, but the ghosting would say otherwise.As far as ramtex goes, you may be able to mitigate the issue by setting the background of the text to opaque and the same color as the actual background.

author: peter-h
author_rank: Super Contributor
timestamp: June 24, 2025, 10:24:46 pm
content: Ramtex has a curious feature, not well described, gsetmode(GVPCLR), which draws the text string while clearing the viewport around the rectangle containing the text.With a fairly big font, I measure exec time at 130ms for just the string, and 500ms for the string plus the above clearing.Obviously, you want to make that viewport fairly tight around the text! They recommend using a separate viewport for the variable part of the string, to speed things up even more, and by doing that I can get the extra time down to some 10-30ms (it will be a rectangular fill, which I optimised to use DMA).It's an interesting technique and it does suppress flicker. Why it works so much better than the other methods is not obvious, but presumably it works because the "clearing around" operation is removing very littlepreviously visiblematerial. The clearing operation involves drawing four rectangles in BG colour, covering the entire space between the rectangle representing the text extents, and the viewport limits. Obviously this can be slow, and it is very slow if the viewport is big, but for a tightly sized viewport is works fine. Of course you could do this same thing manually; there is a function for returning the length and height of a string in pixels, so it is basically easy. The key seems to be to draw the new text and do the aforementioned clearingafterwards.But if you want to e.g. draw a simple analog clock, none of this is of any use to you. You have to, every second, drawall3 hands in the BG colour, and then drawall3 hands in the desired FG colour(s) in their new positions.QuoteHow is the memory management done? Does it save the area on the heap? Note that with most SPI displays you cannot read back data, so it must allocate a block to save it.QuoteIndeed; fewer visible pixels changing then.

author: Pseudobyte
author_rank: Frequent Contributor
timestamp: June 25, 2025, 04:57:28 pm
content: Quote from: peter-h on June 24, 2025, 10:24:46 pmAFAIK, when the object is invalidated its bounding box is added to a list of areas that have been invalidated. That list is apart of the display struct that is created on the heap at the time of display initialization.https://github.com/lvgl/lvgl/blob/879ec2282750e7d317121b1c9b5d99330ad738da/src/display/lv_display_private.h#L105

author: PlainName
author_rank: Super Contributor
timestamp: June 25, 2025, 05:31:01 pm
content: Quote from: peter-h on June 24, 2025, 10:24:46 pmThe way I've done displays of various sorts previously is to use a frame buffer to which all writes are done, and then update the display from there. Sometimes there isn't enough memory so I buffer a smaller viewport instead, but the principle is the same. All the write functions mark each pixel as updated (only needs one bit) so the updater knows what needs to be updated on the real display. Usually it's sufficient to just go from start to finish updating only changed pixels and then doing it all again until there have been no changes. At that point the updater goes to idle, and any write operation will kick it off again. So no unnecessary background scanning.Depending on display you may be able to do block writes, which are lots faster. The updater can figure if some area is a single colour and block update that when appropriate. I recall one display (on a DVR) which was very slow if even just changed pixels were updated, but turned out to be fast enough when block writing, say, spaces and sometimes even just the blank row of pixels under a character.I think the takeaway is to have a separate process that updates the physical display from a buffer, and then it doesn't really matter how long it takes to write to it (other than having the display update fast enough that you don't see each character drawn, of course). And to only update changes (which may be the entire display but is typically only a small area).For the avoidance of doubt, I tend to steer away from reading the physical display. They don't all allow that and it's slow. Buffer so you know the logical state of the display and, worst case, just blast the entire thing over again if something is amiss. Can't be sure what you read back is what you wrote in the first place.

author: peter-h
author_rank: Super Contributor
timestamp: June 25, 2025, 07:16:04 pm
content: QuoteThat sounds rather non deterministic on memory usageI know there are heap strategies which avoid fragmentation (e.g. the power of two blocksize method) and maybe they do something like that.QuoteFor sure but the problem here is lack of RAM. A 240x240x8 display is about the smallest and that needs 58k RAM. The "buffer" mode of Ramtex needs slightly more than that. And a 320x480x24 needs 338k. A smaller buffer could be used for parts of the display... That then gets quite application-specialised.

author: PlainName
author_rank: Super Contributor
timestamp: June 25, 2025, 07:48:41 pm
content: QuoteSuch is life: no free lunches. You can't expect to run, say, Windows on an ATTiny, to pick an extreme.

author: peter-h
author_rank: Super Contributor
timestamp: June 26, 2025, 07:01:42 am
content: Of course there are no free lunches; the skill in "embedded" is doing this stuff with the resources available.For a mental exercise, think of say drawing an analog clock. On the LVGL site there are various sources for this. But they aren't saying how much RAM they used up. It is pretty easy on a 240x240 1bppYou can send up the whole frame buffer of 7.2k bytes in 3ms, at 21MHz.The Ramtex library comes with a really great font and icon editor. I've been playing with it. You can do font subsets, which is a major thing in embedded, since a say 32x32 font can exceed the 1MB CPU FLASH just by itself.

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 26, 2025, 07:58:35 am
content: Don't use transparent fonts, that way the entire field is replaced at once, greatly reducing the effect.Otherwise, it takes two operations: Drawing a box to erase the old text, then drawing the new one.This will cause a very noticiable flicker.The drawing algorithm is unknown, but probably it's a generic and inefficient function made to work on all archs...

author: paulca
author_rank: Super Contributor
timestamp: June 26, 2025, 08:13:34 am
content: Not sure if it helps, but in "big iron" GUI and windowsing toolkits there is usually a "FontMetrics" utility.FontMetrics utils take the same inputs as your text drawing functions and return you metrics. Like "What is the bounding box area containing the text?"You can give it a bounding box, some text objects and ask it for a font size to fit, for example.When It gives you the box and you "blank" that box, it will blank all places the font would have drawn.For for faster operations with texts it can be useful to not over-write, but to layer. It has memory considerations though and memory addressing hardware helps.So instead of mutating the buffer by "assigning" the new pixel colours to "the" image directly. You create a new sub-image with the text and layer it onto the background with a rasteriser. When you want to remove that text, you just delete the layer. If you want to redraw it without artifacts, you create a new one first. Then swap the pointers. When the refresh comes back around it copies the new layer into that section of the screen. "Sprites", Multi-buffering, composition are terms to consider. Basic stuff on basic backgrounds is easy. This stuff only gets horrifying when you introduce the "alpha channel" and transparency.This is basically how things like the Amiga did games.To do it performantly virtual memory addressing and a fast rasteriser is how to they did it. But your average MCU today with a TFT is way more powerful over all. I know some of those TFT screens themselves have block re-addressing, just never tried to use it.EDIT: I now recall where I explored this with an STM32. I had a themed background BMP image and I wanted to draw text on it. No matter what I did I got garbage like you did.I ended up doing memory swapping, not overwriting. Basically before I drew the text "box" I copied the background to another bit of screen memory outside the view port. When I wanted to change the text, I copied that little buffer back into the screen memory and then drew the text over it again.It worked, but it was fiddly as hell. Most of the reason for it being fiddly was fiddling around with the STM32 SPI routines the gfx driver was using for block copies. The concept was fairly intuitive. If I find the code I'll pop back in.

author: peter-h
author_rank: Super Contributor
timestamp: June 26, 2025, 01:09:31 pm
content: Can anyone explain to me where a palette is used?I can see two uses:1) The LCD controller can have one loaded, so you can send it say 4 bits per colour and obtain 16 different RGB colours. Neither of my two chips (ST7789 or ILI9488) supports this though.2) You can reduce data storage at the driver, in the same way as above. But for that to work you need to have a system where you define colours locally which then generate a palette lookup.

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 26, 2025, 01:12:25 pm
content: Layering, etc, nothing applies here, there's no local buffer, all operations are written to the display vram.

author: tooki
author_rank: Super Contributor
timestamp: June 26, 2025, 01:20:12 pm
content: Quote from: peter-h on June 26, 2025, 01:09:31 pmIf you mean the concept of having a fixed number of software-defined colors selected from a much larger potential color gamut (e.g. 256 colors, selected from the 16.7 million possible in 24-bit color):One use for this in the past was to do extremely CPU-light animations, by using palette swapping to change the rendered colors of things without having to re-render anything to the frame buffer. It was commonly used for background animations like waves on water, marquee lights, trees waving, etc. A great example of this is SimCity 2000, which used palette swapping extensively to animate all the water, lights, road traffic, etc. at virtually zero CPU cost.Another use for palettes is for displaying palette-optimized images, where the image creation software analyzes the image and generates an optimized palette for that specific image, so that none of the palette is wasted on colors that aren’t used in the image. This allows a 256-color image to look surprisingly good, especially with error-diffusion dithering applied. This, too, was widely used in games in the 80s and 90s, and survives to this day in GIFs. It’s become a lot less important, though, thanks to advances in compression algorithms, especially lossy compression.

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 26, 2025, 01:24:21 pm
content: I also researched this some time ago, most lcd controllers doesn't seem to support palettes or anything lower than 8bpp.

author: peter-h
author_rank: Super Contributor
timestamp: June 26, 2025, 02:06:48 pm
content: OK yes I remember the fast palette reprogramming method. But that requires the palette to beinthe controller, which doesn't seem to be the case.A software palette is no good for this because you have to transfer the pixel data.

author: Pseudobyte
author_rank: Frequent Contributor
timestamp: June 27, 2025, 01:23:54 pm
content: Quote from: peter-h on June 25, 2025, 07:16:04 pmI do believe the array of invalidated areas is pre-allocated and limited to 32 by default. You can also statically allocate the lv_display_t struct along with the draw buffers to eliminate the dynamic allocation.Along those same lines you can completely turn off LVGL's malloc usage and statically allocate everything. You do need to avoid certain features if you do this though.

author: ajb
author_rank: Super Contributor
timestamp: June 27, 2025, 02:36:56 pm
content: To the original problem, I solve this in a graphics system I wrote with a 'print area' function. It takes an additional width parameter for the total area that it should fill, which you can set to be the maximum expected text width, and if the text doesn't fill that far it fills the remainder with the background color. This is obviously much faster than blanking out the entire area first, and the total number of pixels written per update is fixed. If your graphics library doesn't provide something like that it should be easy to implement as a wrapping function, as long as you know the font height and can determine the cursor position after writing the text. If you'd rather the writes be minimal rather than constant size, you could store the width of the last written text and use that as the width to fill on the next update.

author: peter-h
author_rank: Super Contributor
timestamp: June 27, 2025, 03:08:10 pm
content: IF the variation in the text string is just due to the use of proportional fonts, i.e. the string length does not vary all that much, your method seems to be equivalent to outputting a trailing space char or two.For bigger variations, clearing from the end of the new text to some point to the right of it is exactly what that gsetmode(GVPCLR) does: it clears all around the text, up to the edges of the viewport. If you set the viewport tight above, below, and to the left of the text, then you will get exactly that. And indeed it doesn't produce flicker.LVGL has tons more features than Ramtex. For example I've just needed a rotated rectangle. It took me only minutes to get one from Claude AIBut a filled rectangle is way more complex. It does not need recursion or anything dodgy, but is non trivial because the fill lines have to be horizontal. Same for a circle of a thickness > 1 pixel; I am drawing that by drawing a filled one and then drawing a BG-filled one in the middle of it. Slow... I did all this on a Z80 in the 1980s but there I could read back the video RAM and do fills the primitive way.

author: peter-h
author_rank: Super Contributor
timestamp: June 28, 2025, 05:48:27 pm
content: This was a bit of funThe #1 thing learnt was again to keep erasure of previous, and drawing of new, very close together in sequence. Erasing all 3 hands and then drawing new ones causes a lot of flicker.Doing fancy hands would take too much time. I don't have filled polygon etc functions. They are online but mostly not in a nice usable standalone form.Code:// Clock drawing support functions#include <math.h>static void drawLine (int x1, int y1, int x2, int y2){gmoveto(x1,y1);// Ramtex library functionsglineto(x2,y2);}// Function to draw a hollow rectangle rotated at an arbitrary angle// Parameters:// cx, cy: center coordinates of the rectangle// width, height: dimensions of the rectangle// angle: rotation angle in radians// drawLine: pointer to line drawing function that takes (x1, y1, x2, y2)/*// Example usage functionvoid exampleUsage() {// Assuming you have a line drawing function like:// void myDrawLine(int x1, int y1, int x2, int y2);// Draw a 100x60 rectangle centered at (200, 150), rotated 45 degrees// drawRotatedRectangle(200, 150, 100, 60, M_PI/4, myDrawLine);// Draw a 80x120 rectangle centered at (300, 200), rotated 30 degrees// drawRotatedRectangle(300, 200, 80, 120, M_PI/6, myDrawLine);}*/void drawRotatedRectangle(int cx, int cy, int width, int height, float angle){// Calculate half dimensionsfloat halfWidth = width / 2.0f;float halfHeight = height / 2.0f;// Calculate sine and cosine of rotation anglefloat cosAngle = cos(angle);float sinAngle = sin(angle);// Calculate the four corner points relative to center (before rotation)float corners = {{-halfWidth, -halfHeight}, // top-left{ halfWidth, -halfHeight}, // top-right{ halfWidth, halfHeight}, // bottom-right{-halfWidth, halfHeight} // bottom-left};// Rotate and translate corner pointsint rotatedCorners;int i;for (i = 0; i < 4; i++){// Apply rotation matrixfloat rotatedX = corners * cosAngle - corners * sinAngle;float rotatedY = corners * sinAngle + corners * cosAngle;// Translate to final positionrotatedCorners = (int)(cx + rotatedX + 0.5); // +0.5 for roundingrotatedCorners = (int)(cy + rotatedY + 0.5);}// Draw the four sides of the rectangle// Top side: top-left to top-rightdrawLine(rotatedCorners, rotatedCorners,rotatedCorners, rotatedCorners);// Right side: top-right to bottom-rightdrawLine(rotatedCorners, rotatedCorners,rotatedCorners, rotatedCorners);// Bottom side: bottom-right to bottom-leftdrawLine(rotatedCorners, rotatedCorners,rotatedCorners, rotatedCorners);// Left side: bottom-left to top-leftdrawLine(rotatedCorners, rotatedCorners,rotatedCorners, rotatedCorners);}// This just writes the four numbers 12 3 6 9// It is called during clock face init and also after each time change because the hand(s)// can overwrite the number(s).static void clock_numbers(int c_x, int c_y, int rad, uint32_t col, int blobrad){gselfont(&A_16x32_b_g2_num_sub);// number subset only plus degCGMODE mode = gsetmode(GTRANSPARENT);// select transparent to make the passage of the secs hand slickergsetpos(c_x+rad-40,c_y+16);gputch('3');gsetpos(c_x-8,c_y+rad-23);gputch('6');gsetpos(c_x-rad+26,c_y+16);gputch('9');gsetpos(c_x-17,c_y-rad+53);gputs("12");gsetmode(mode);// restore previous text modegsetcolorf(col);gcircle(c_x, c_y, blobrad, 1U);}// Draw clock.// This scales to the current LCD display, and has been tweaked to work on the two LCDs supported.// Can be much improved by drawing fancy hands etc.void drawclock (void){#ifdef LCD_CONTROLLER_ST7789// 240x240#define centre_x 120// use 120 for 240x240 LCD or 160 for 320x480 LCD#define centre_y 120// same as x unless on 320x480; then can use 480/2#define radius 119// clock radius#endif#ifdef LCD_CONTROLLER_ILI9488// 320x480#define centre_x 240#define centre_y 160#define radius 159#define timex 1// x,y position of time string#define timey 25#endif#define shortmarkl 12// short (seconds) mark length#define fatmarkw 8// fat mark width#define fatmarkl 20// fat mark length#define MIDBLOB 10// middle clock face blob radius#define secshandl (radius-24)// seconds hand length#define secshandt (radius/4)// seconds hand tail length#define minshandl (secshandl-28)// mins hand length#define hrshandl (minshandl-20)// hrs hand length#define COLFACE G_YELLOW// main face colour#define COLSECS G_RED// secs hand colour#define COLMINS G_GREEN// mins hand colour#define COLHRS G_BLUE// hrs hand colour#define TWOP 3.14159*2.0f// Draw the fixed clock portion - just once// Draw outline circle and the marksginit();// clear the screen, etcgselvp(1U);// create a viewport (may already exist)gsetvp (0,0,(centre_x*2)-1,(centre_x*2)-1);// VP size extends fully on either LCDgsetcolorf(COLFACE);gcircle(centre_x, centre_y, radius, 1U);// create a 3 pixel thick circle by drawing two filled circles...gsetcolorf(G_BLACK);gcircle(centre_x, centre_y, radius-3, 1U);gsetcolorf(COLFACE);// Draw the markings; every 5 secs it is a bigger rectangle// This stuff could also have been done with rotated font symbolsfor (int secs=0; secs<60; secs++){float angle = (secs+45) * TWOP / 60.0f;// 45: the trig functions have angle=0 lying on the 3 o'clock axisif ((secs % 5) == 0){// if secs is a multiple of 5, draw thicker 5-second markint fatmark_x, fatmark_y;// start and end of fat markingsfatmark_x = (int)(centre_x + (radius-(fatmarkl/2))*cos(angle) + 0.5);fatmark_y = (int)(centre_y + (radius-(fatmarkl/2))*sin(angle) + 0.5);drawRotatedRectangle(fatmark_x, fatmark_y, fatmarkl, fatmarkw-2, angle);}else{// for individual secs, except multiple of 5, draw short markint shortline_s_x, shortline_s_y;// start and end of short line markingsint shortline_e_x, shortline_e_y;shortline_s_x = (int)(centre_x + (radius-shortmarkl)*cos(angle) + 0.5);shortline_s_y = (int)(centre_y + (radius-shortmarkl)*sin(angle) + 0.5);shortline_e_x = (int)(centre_x + radius*cos(angle) + 0.5);shortline_e_y = (int)(centre_y + radius*sin(angle) + 0.5);drawLine (shortline_s_x,shortline_s_y,shortline_e_x,shortline_e_y);}}// Drop in the four numbers and the middle blobclock_numbers(centre_x, centre_y, radius, COLFACE, MIDBLOB);int lastsecs=-1;// -1 forces an initial updatefloat hrs,mins;int secs;float angle;int secs_hand_start_x, secs_hand_start_y, secs_hand_end_x, secs_hand_end_y;int mins_hand_start_x, mins_hand_start_y, mins_hand_end_x, mins_hand_end_y;int hrs_hand_start_x, hrs_hand_start_y, hrs_hand_end_x, hrs_hand_end_y;// "last" valuesint hrs_hand_start_x_l=0;int hrs_hand_start_y_l=0;int hrs_hand_end_x_l=0;int hrs_hand_end_y_l=0;int mins_hand_start_x_l=0;int mins_hand_start_y_l=0;int mins_hand_end_x_l=0;int mins_hand_end_y_l=0;int secs_hand_start_x_l=0;int secs_hand_start_y_l=0;int secs_hand_end_x_l=0;int secs_hand_end_y_l=0;// Main clock loop - runs for ever (currently)do{// Get RTC into mytimestruct tm mytime;getrtc (&mytime);hrs = mytime.tm_hour;hrs = (int)hrs % 12;// convert 24hrs to 12hrsmins = mytime.tm_min;secs = mytime.tm_sec;// update display only if secs have changed, and avoid garbage in the time valueif ( (secs!=lastsecs) && ((secs>=0) && (secs<=59)) ){lastsecs=secs;debug_thread_printf("hrs=%02d, mins=%02d, secs=%02d",hrs,mins,secs);// Precalculate everything. One could speed this up dramatically by using a fast sin/cos// algorithm which uses single floats, because C uses double floats, and the 32F4 has// only hardware single floats, but it would be completely pointless.// hours handangle = (((hrs*5.0f)+45.0f)+(mins/60.0f)) * TWOP / 60.0f;hrs_hand_start_x = centre_x;hrs_hand_start_y = centre_y;hrs_hand_end_x = (int)(centre_x + hrshandl*cos(angle) + 0.5);hrs_hand_end_y = (int)(centre_y + hrshandl*sin(angle) + 0.5);// minutes handangle = ((mins+45.0f)+(secs/60.0f)) * TWOP / 60.0f;mins_hand_start_x = centre_x;mins_hand_start_y = centre_y;mins_hand_end_x = (int)(centre_x + minshandl*cos(angle) + 0.5);mins_hand_end_y = (int)(centre_y + minshandl*sin(angle) + 0.5);// seconds hand - has the short tail 180 degrees (PI radians) on the other sideangle = (secs+45.0f) * TWOP / 60.0f;secs_hand_start_x = (int)(centre_x + secshandt*cos(angle+(TWOP/2.0f)) + 0.5);secs_hand_start_y = (int)(centre_y + secshandt*sin(angle+(TWOP/2.0f)) + 0.5);secs_hand_end_x = (int)(centre_x + secshandl*cos(angle) + 0.5);secs_hand_end_y = (int)(centre_y + secshandl*sin(angle) + 0.5);// Draw all three hands as fast after erasing each one as possible, in the order shown// because the secs hand sometimes leaves artefacts in the other two.// The "last" values are initialised to 0 so this writes a harmless BG pixel at 0,0.// secsgsetcolorf(G_BLACK);drawLine (secs_hand_start_x_l,secs_hand_start_y_l,secs_hand_end_x_l,secs_hand_end_y_l);gsetcolorf(COLSECS);drawLine (secs_hand_start_x,secs_hand_start_y,secs_hand_end_x,secs_hand_end_y);// minsgsetcolorf(G_BLACK);drawLine (mins_hand_start_x_l,mins_hand_start_y_l,mins_hand_end_x_l,mins_hand_end_y_l);gsetcolorf(COLMINS);drawLine (mins_hand_start_x,mins_hand_start_y,mins_hand_end_x,mins_hand_end_y);// hrsgsetcolorf(G_BLACK);drawLine (hrs_hand_start_x_l,hrs_hand_start_y_l,hrs_hand_end_x_l,hrs_hand_end_y_l);gsetcolorf(COLHRS);drawLine (hrs_hand_start_x,hrs_hand_start_y,hrs_hand_end_x,hrs_hand_end_y);// refresh clock face numbers and centre blob; these get corrupted by the handsgsetcolorf(COLFACE);clock_numbers(centre_x, centre_y, radius, COLFACE, MIDBLOB);// Update "last" hand datahrs_hand_start_x_l=hrs_hand_start_x;hrs_hand_start_y_l=hrs_hand_start_y;hrs_hand_end_x_l=hrs_hand_end_x;hrs_hand_end_y_l=hrs_hand_end_y;mins_hand_start_x_l=mins_hand_start_x;mins_hand_start_y_l=mins_hand_start_y;mins_hand_end_x_l=mins_hand_end_x;mins_hand_end_y_l=mins_hand_end_y;secs_hand_start_x_l=secs_hand_start_x;secs_hand_start_y_l=secs_hand_start_y;secs_hand_end_x_l=secs_hand_end_x;secs_hand_end_y_l=secs_hand_end_y;// On the bigger LCD, write the time#ifdef LCD_CONTROLLER_ILI9488gselfont(&narrow24_e_sub);char timebuf;snprintf(timebuf,sizeof(timebuf),"%02d.%02d.%02d UTC",mytime.tm_hour,mytime.tm_min,mytime.tm_sec);gsetpos (timex,timey);gsetcolorf(COLFACE);gputs(timebuf);gselfont(&A_16x32_b_g2_num_sub);#endif}else{osDelay(100);// yield to RTOS; nothing to do until secs change}} while (true);// for ever...}

author: Nominal Animal
author_rank: Super Contributor
timestamp: June 28, 2025, 07:42:01 pm
content: Quote from: peter-h on June 27, 2025, 03:08:10 pmI generally use the same polygon fill for such. If the polygon is convex, you only need to record two intersection points along one coordinate axis for the entire span. Concave and self-intersecting polygons may have more than one pair. Anti-aliased edges can be implemented by examining each consecutive pair of intersections on the same side, because that determines the span where the opacity/antialiasing varies linearly between 0% and 100%. Only the pixel strip containing a vertex differs; there you need to examine both intersections spanning the vertex, and the coordinates of the vertex.Quote from: peter-h on June 27, 2025, 03:08:10 pmThere are so many circle and arc-drawing routines! You can use the same method for polygon fills for a filled circle. For an antialiased thick circle, you need up to four intersections on each coordinate along one coordinate axis, but the variation between consecutive pairs on the same side is no longer linear; although most humans will not detect the difference.If you have a fast square root, you can use \$y = y_0 \pm \sqrt{ r^2 - (x - x_0)^2}\$, where \$(x_0, y_0)\$ is the center point, and \$r\$ is the radius.Alternatively, the signed distance between \$(x, y)\$ and the \$r\$-radius circle centered at \$(x_0, y_0)\$ is \$\sqrt{(x - x_0)^2 + (y - y_0)^2}-r\$ (for thick anti-aliased circles).Another approach is to usesinandcosapproximations, which don't need to be very precise. For example, if you have six decimal digits of precision, or 20 bits of precision, it is sufficient forantialiased1920×1080. And that means fast polynomial approximations. We've discussed those before here, I believe in the Programming sub-board.One extremely useful thing is to be able to draw filled cubic Bézier poly-curves. It gets a bit math-heavy, or kinda-sorta recursive via de Casteljau, but again, the same basic polygon fill approach will work, and generally treating the segments as straight lines gives sufficiently good results even when antialiasing is used. The trick is to decompose any Bézier curve after transformations but just before rasterization into two to five nondecreasing segments; the split islossless, proven via de Casteljau's algorithm, but then you know that the curve does not change sign along either axis. You can also simply subdivide the curve into line segments, until the distance between the new middle point to the line formed by the end points, or the angle between the two consecutive line segments produced by the subdivision is within some specified limit.Many toolkits (and even Inkscape, when converting circles and ellipses to paths) convert circles and circular arcs to cubic Bézier curves; typically four curves are used per full circle, as that gives you something like five decimal digits of precision approximation of a circle. There are formulas available for these.

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 28, 2025, 10:08:36 pm
content: A fill square is very very trivial, set LCD addressing window in the controller to the area of the square, then simply send x*y pixels.In my modded uGUI I use DMA for that, filling is extremely fast.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 28, 2025, 11:58:38 pm
content: Quote from: peter-h on June 28, 2025, 05:48:27 pmUsually, the better approach for animating graphics is to draw the item at the new position first, and erase it at its old position after that. It avoids any flicker.Now the potential pitfall is that depending on the item to draw and the respective old and new positions, the old and new may slightly overlap and thus erasing the old one last may erase part of the new one.To mitigate that, one can write a specific item erase function that will erase only the pixels that are not in the item at its new position.Now of course, the simplest approach by far is to use an "off" buffer when you can afford it in terms of available RAM. Draw the whole rectangle area that will get modified in an off buffer, and then copy this buffer to the display in one go. Many graphics libraries can do this and I'd be surprised if the one you use can not. But the issue may be with how much RAM you have left for this.Off buffers don't need to cover the whole screen (unless of course the whole screen must get updated, like if playing video. Otherwise, you can restrict an off buffer to the max area you'll want to animate at any time, and it's good enough.Say you have a 320x200 display but restrict any animated area to a 100x100 rectangle. In RGB888, that's 30 000 bytes. Can be a bit much depending on your system.

author: jpanhalt
author_rank: Super Contributor
timestamp: June 29, 2025, 12:27:12 am
content: I'm late to the party. When I do graphics and scroll, for example, I don't erase anything. I just redo the entire screen or screen segment. If I am writing values with variable lengths, I pad the new writes with enough to "erase" any vestiges of the previous write. The only time I really "erase" is when I clear screen at startup. An exception to that is when I can read the screen, and I have routines to clear rest of line. My current LCD's (e.g., ST7525) are driven with SPI, and I cannot read the screen with a serial interface.I tried to upload an MP4 (wrong format) and zipped version (file too big), so I can't illustrate here. Basically an erase is just writing, right? So, why "erase?"

author: peter-h
author_rank: Super Contributor
timestamp: June 29, 2025, 07:50:24 am
content: QuoteDoes the e.g. ILI9488 do that? AFAICT the Set Address command sets the starting address and the data that follows just fills pixels horizontally, wrapping at the end of each line to the start of the next one. So you can fill the whole LCD that way, and you even have the non incrementing source address option on the DMA to fill with all bytes the same value (the 16 bit DMA mode should also do 16 bit colour displays).I found Page Address Set (2Bh) which may be what you refer to. It seems to set the line to start and the line to end, but not allowing a square of a x a pixels to be written.QuoteThat is exactly what happens all the time on a clockBut I get your drift.QuoteIt does nothing clever like thatIt has a BUFFER mode where you have a RAM buffer for the whole frame (usually way too big for the RAM in embedded chips) and it does some RMW ops via that, but not that LVGL-like mode discussed above.QuoteVideos need to be hosted somewhere, AFAICT. The files are way too big to store them on the community site server.Sometimes one needs to clean up bits of the old stuff, but by careful design one can minimise that. For example, Ramtex provide a really slick font editor where e.g. with one button click one can monospace a selected range of chars, and monospacing the numerals avoids many artefactsSubsetting fonts saves a huge amount of FLASH usage too, and is trivial with this tool. AFAICT all they need is for the character codes to be monotonically located in the font. I could have drawn that clock using rotated characters, for which the Ramtex lib provides support and they also get anti aliased. In fact I am surprised at how much font support there is given how little graphics support there is.

author: Nominal Animal
author_rank: Super Contributor
timestamp: June 29, 2025, 08:04:17 pm
content: Quote from: peter-h on June 29, 2025, 07:50:24 amYes; you do need to send the pixel data, ILI9488 has no "fill" command.You use command0x2AColumn Address Setto set the coordinate range along the column axis, and command0x2BPage Address Setto set the coordinate range along the other axis, followed by one or more0x2CMemory Writeto set the pixels in this rectangular region, optionally followed by0x3CMemory Write Continueif you need to split the pixel data over multiple commands. Only this rectangular region is accessible to Memory Read and Write commands.ILI9341, NV3007, and ST7789 variants use the exact same commands for these. Only the display size and initialization sequence varies between these controllers, which is why a single "driver" can easily support all these with little code/data overhead.Quote from: peter-h on June 29, 2025, 07:50:24 amKeeping track of the rectangles used for various fields –– even text content rectangles when the display consists of lines of text and nothing else –– is my recommended solution.You first blit the new data on top of the old. Then, if the new data did not cover the entire old rectangle, you fill the 1-4 rectangular areas that were not covered by this latest writes to background. Then, you update the content rectangle to just the one covered by the new data.This means that if you wrote for example "Just a moment, please", the overwrote it with "Finishing...", and finally with "Done!", you'd only do background fills between each consecutive rectangle pair. That is, the background fills around "Done!" only covers the rectangle used by "Finishing...", not the rectangle used by "Just a moment, please". This way, you also limit the number of rectangles you need to keep track of. Do test your code computing the cutout between two rectangles; the result is always 0 to 4 non-overlapping rectangles.

author: peter-h
author_rank: Super Contributor
timestamp: June 30, 2025, 09:09:42 am
content: This is great stuff - thank you.I now "get" that the way to do moving graphics is to write all object drawing functions to also calculate an enclosing rectangle. It's a whole different way of doing stuff.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 30, 2025, 02:36:19 pm
content: Note that when cost is not a critical concern, and you don't want to bother with the low-level details of graphics, you may consider using an external controller, such as the FT800 series, which do a fantastic job with accelerated antialiased graphics for just a couple bucks.

author: peter-h
author_rank: Super Contributor
timestamp: June 30, 2025, 03:57:43 pm
content: Hmmm interesting. I looked it up. A weird 2013 job from FTDI - the company which shafted everybody (including me) during covid on the FT232No support; all moved from Scotland to Taiwan.The problem is that you need to build your own display module, using this chip, and an LCD panel. That's way more work than buying any number of panels from Buydisplay etc. It kinds of reminds me of the 1980s and the UPD7220, which did so much that the RM took a year to absorbIt wasn't fast though.But it is an interesting diversion. What I learnt doing this exercise is- using a parallel bus, even 8 bit, is much faster than any SPI speed you will get (other than on the 240x240 displays, 60MHz, which run super fast anyway, and using SPI1 I could get 42MHz)- if doing an "HMI" one is probably using a big panel, at least VGA, and they all have crap SPI speeds, so you must go parallel (which means a total system redesign, probably QFP176 package, to get the extra GPIO)- the path of least resistance is to port the product to a new high-end STM CPU, 480MHz, loads of RAM, and then you can have a frame buffer and do it properly- the totally other way, done by many, is to stick a "PC" in the box and then you get MS VC++ dev kit and an incredible speed. I've seen avionics which do the Windows BSODThe reason I did this LCD project is that the product I developed is likely to generate custom product enquiries and these will likely need an LCD, so I want to be ready. It's also fun for hobby projects.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 30, 2025, 04:44:36 pm
content: The EVE series of controllers (FT8xxx) were actually from a company called Bridgetek that FTDI bought a while ago.They work really well. Regarding panels, you need a RGB+HSYNC/VSYNC interface. There are a lot of available panels like this, and potentially cheaper in quantities than those with a higher-level controller, but yes, that may be a bit more work for mechanical integration. But yes, you then get into "vendor lock-in" as those EVE controllers have no direct equivalent, while LCD panels with a graphics controller pretty much all work in a similar way and porting code to a new controller is much easier.

author: PlainName
author_rank: Super Contributor
timestamp: July 01, 2025, 07:52:48 am
content: QuoteIs the intention to embed a panel into your product (that is, the product processor drives the panel) or to have an add-on which connects to the product? An example of the latter might beNextion.

author: peter-h
author_rank: Super Contributor
timestamp: July 01, 2025, 07:55:55 am
content: The former.Those modules are pricey, and single sourced so the vendor would have you over a barrel. And a few years later...?

author: PlainName
author_rank: Super Contributor
timestamp: July 01, 2025, 08:30:19 am
content: Sure, but if you were developing your own (I just used Nextion as an example of the decoupling) then there would be advantages: your product wouldn't need to be upgraded and would just need to know simple graphics commands. "box at this location, this size, border 1" kind of thing. Really unstressed. The hard work is then done on your display module, and if necessary you can switch hardware to suit whatever display without your product being any the wiser. Possibility to sell the display for others to utilise too - take on Nextion as a competitor!

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 04, 2025, 05:00:13 pm
content: With u8g2, you can get the pixel length of a string.This is not hard to implement, it's like a blank string draw, expect you do nothing, just process the string, adding each char width and returning the total length.So, you can store it, write the text, then fill a white rectangle for the remaining field size.Quote from: peter-h on June 29, 2025, 07:50:24 amI already linked my uGUI lib will all this, it seems to me you didn't bother to check anything, it easier to create 5 threads and wait... ?*Sigh* For sure, I've seen that in every color LCD controller from the last 20 years+, see CASET, RASET commands in the datasheet.Read ALL of it.After RAMWR cmd the display is ready to accept data.It'll start at [x0 y0], each write increase the column address, when x1 is reached the column will reset to x0 and the row will increase.After sending RAMWR command, just set DC=1, CS=0, write x*y pixels, set CS=1, then the operation will be done.Code:/*** @brief Set address of DisplayWindow* @param xi&yi -> coordinates of window* @return none*/static void LCD_SetAddressWindow(int16_t x0, int16_t y0, int16_t x1, int16_t y1){/* Column Address set */{uint8_t cmd[] = { CMD_CASET, x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF };LCD_WriteCommand(cmd, sizeof(cmd)-1);}/* Row Address set */{uint8_t cmd[] = { CMD_RASET, y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF };LCD_WriteCommand(cmd, sizeof(cmd)-1);}{/* Write to RAM */uint8_t cmd[] = { CMD_RAMWR };LCD_WriteCommand(cmd, sizeof(cmd)-1);}}

author: MarkF
author_rank: Super Contributor
timestamp: July 04, 2025, 06:32:34 pm
content: Quote from: DavidAlfa on July 04, 2025, 05:00:13 pmIt is also a fast way to draw a vertical line or a horizontal line.Either set the start and end column to the same column number orset the start and end row to the same row number, and it willincrement through all pixels without the need to re-send the row/column numbers.Any diagonal line will require sending both row and column for each pixel write.

author: dave j
author_rank: Regular Contributor
timestamp: July 04, 2025, 07:36:56 pm
content: Quote from: MarkF on July 04, 2025, 06:32:34 pmWhilst that's true for 45 degree lines, you can do a lot better for lines that approach either axis.The overhead for drawing any number of pixels in one block is 11 bytes (5 each for CASET and RASET and 1 for RAMWR). Each 16 bit pixel is 2 bytes after that. That means it's worth having a line drawing routine that draws as many pixels with the same column or row in one go as it can.1 pixel = 13 bytes, 2 pixels = 15 bytes, ..., 10 pixels = 31 bytes, etc.

author: peter-h
author_rank: Super Contributor
timestamp: July 11, 2025, 03:51:39 pm
content: QuoteSorry guys, I've been away on holiday and didn't put a watch on this thread.This is great stuff. I had a look at what Ramtex does and they do indeed use this for V and H lines. I optimised it for DMA without realising what it did

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 12, 2025, 02:56:09 pm
content: Basically what you said.Palettes are a 80-90s thing where resources where very limited.Instead storing and transferign the whole RGB value, it used an index, the display controller would have a look-up table and translate the index to actual colors.Of course this heavily limited the displayed colors, or required tricky or extremely timing-accurate hacks to overcome the limitations.You're not getting this in any common LCD driver, that's more a SoC or chipset thing.Maybe STM32 Chrom-Art peripheral supports this, but it completely defeats the purpose of a display with embedded controller, as it need local SRAM or external SDRAM.Likely, your code or that library is slow or not very optimized.

author: peter-h
author_rank: Super Contributor
timestamp: July 12, 2025, 03:50:08 pm
content: Well, sure, if you look at the range of controllers the Ramtex lib "supports", it cannot be optimised for most of them. I am realising what should have been obvious: it has not even been tested with most of them. Fortunately I decided to support only those two controllers, which pretty well directly follows from the choice of two display sizes.But I can also see how they can list as supported so many controllers. They are all very similar.A lot of the drawing routines they use are limited in optimisation by the need to clip to a viewport. This is a pretty big issue. Each drawing routine has to check the viewport rectangle and limit the size of the object accordingly. How useful viewports are is a different matter. I think in the embedded graphics context they are not that useful.Interestingly, the circular mode of the 32F4 DMA should work for filling blocks with 24 bit colour. 16 bit colour is more obviously doable by using a fixed source address and the 16 bit mode.

author: peter-h
author_rank: Super Contributor
timestamp: September 17, 2025, 10:17:08 pm
content: I have finally got around to playing with the TE (tearing effect) output from the LCD controller. This is what TE looks like on the ILI9488The TE=1 period, about 600us, is far too short to do anything useful on the display, even with a 21MHz SPI. Maybe with 24 bit parallel...If one does the TE=0 writing inhibit at a lower level e.g. when drawing individual horizontal lines, the idea works but the display update is far far too slow to be of any use. Unsurprising, given the very small TE=1 duty cycle.Maybe there is some controller config which would produce a far longer vertical interval...

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 18, 2025, 12:08:56 am
content: Quote from: peter-h on September 17, 2025, 10:17:08 pmWhy would you even want to?A crafty person waits for the TE period to end, or sets it to trigger on physical update of the 3rd or 4th scan line and waits for it to trigger, and then starts updating the framebuffer contents in the same order as the physical display update, essentially chasing the physical update. That way, you have almost two full physical updates (frames) to update the framebuffer on the controller, before the physical display update reaches your framebuffer updates and tearing becomes visible.In most displays, the vertical retrace (default TE) itself is way too short to complete updates anyway. Instead, you use it as an interrupt source for your MCU, to tell you when you can start the framebuffer update without risking tearing. If your MCU supports DMA to SPI, you can set up the update region, set up DMA for the payload pixel data, and even send the Memory Write command beforehand, if you keep /CS asserted. Then, in the TE-triggered interrupt, you just enable the DMA.Or, at least, this is how I do it using the various ILI94xx/ST7789 controllers. Even with 8-bit/9-bit/16-bit/18-bit parallel, the basic procedure is the same: you wait for the physical update to reach a point where you can start updating the pixel data without conflicting with the physical update, so no tearing occurs. Tearing being just the effect of displaying partially new and partially old data in the same physical update.

author: peter-h
author_rank: Super Contributor
timestamp: September 18, 2025, 07:58:29 am
content: I see that, if using a frame buffer and zapping it to the LCD controller in one go. But I am not using a frame buffer. I am drawing graphics all over the display.In the old Z80 days one used to use the vertical flyback for video RAM updates (or for flipping two video RAM banks). This should still work, except that 600us is not enough for a significant update (via 20MHz SPI).A possible way, for randomly drawn objects, is to limit it to TE=1, but split it up at theapplicationlevel. So to go back to my clock drawing example, on one TE=1 you do the hour hand (erase and redraw), on next TE=1 you do the minute hand, on the next TE=1 you draw the seconds hand, or something like that. Tacky but might work? Or one could pick the order of drawing stuff according to the position relative to the frame scan direction, which might not be hard if displaying a screen full of text.One can also config TE to go high during the frame update, as in the 2nd option herebut the pulses are too short to be useful. They correspond perhaps to the old CRT horizontal flyback time, which was of the order of 1-2us, and lots of people used that too.Am I right that drawing stuff during TE=1 will never produce artefacts?I experimented with commands like 0xB5 which appear to allow stretching of the vertical interval, but not enough because the vertical porch is max 32 lines (except that the value does not correspond to lines). Using the B1 command to halve the frame rate to Fosc/2 does do what one might expect but also produces much worse display artefacts with "unsynced" writing.I suspect that TE is intended to be used either with a frame buffer, or with random-position graphics which are written entirely within the blanking interval, which is during TE=1, plus a bit according to the vertical back porch size confighttps://www.rs-online.com/designspark/lcd-tft-displays-timing-parameters-explainedI am now using this config{CMD, 0, 0xB5}, // Porch config - set max VBP for max time for writing after TE->1{DAT, 0, 2}, // VFP{DAT, 0, 28}, // VBP (note VFP+VBP <= 30, and must be even) and 1 bit equals 7.7us{DAT, 0, 10}, // HFP - these two do nothing evident, so use DS defaults{DAT, 0, 4}, // HBPFirstly, the DS is probably wrong on the " VFP+VBP <= 32"; it is actually 30. Using e.g. 2+30 produces a dead display.Secondly, the TE=1 width is VFP+VBP, but there are constraints:VFP+VBP <= 30 so the max you can get is 1msMin TE=1 is 600us, so even if you set VFP+VBP to 2+2 you get this 600us.Putting other values into HFP and HBP has no effect on frame rate, which is weird.By using a "field probe" to probe the internal display signals, I see that the display drive commences almost immediately after TE goes low. The purple signal is the LCD driveI've got TE=1 up to 1ms at 58Hz frame rate, unless of course you set Fosc/2 and then it doubles, frame rate halves, etc. There is no way to get say 50ms, which would be great for random graphics.The VBP/VFP values make no obvious sense, with it being supposedly alinecount but actually 1 bit here is worth ~7us, which doesn't relate to anything obvious.The above raises various issues in the way I am doing this. With just 1msguaranteed tear-freewriting time, this can't be properly utilised from a 1ms-tick RTOS task. One would need to interrupt from TE rising edge but I can't do that because I have a shared SPI, mutexed... It would be doable with a parallel interfaced LCD; quite a lot of data could be pushed out in that 1ms, inside an ISR.OTOH, when doing random graphics (not pushing out a frame buffer) one is not getting much "tearing" to start with. One gets a bit of flicker here and there, which is minimised by using a faster SPI clock.Another approach is to drop the frame rate to ~30Hz (Fosc/2) and then you get a 2ms TE=1 and this can be polled from within the 1ms-systick RTOS "LCD task".Also discussed herehttps://www.eevblog.com/forum/microcontrollers/screen-tearing-on-lcd-displays/msg6043587/#msg6043587