Anyone here using the Ramtex graphics library? - Page 1
Forum section: electronics / general

author: peter-h
author_rank: Super Contributor
timestamp: March 04, 2025, 04:32:33 pm
content: https://www.ramtex.dk/products/lcdd0129.htmI have just started with this; context:https://www.eevblog.com/forum/projects/small-tft-display-5x5cm-for-moving-graphics-over-spi/msg3976907/#msg3976907and started reading the voluminous manual. There is quite a lot to take in.There are just a couple of displays I am interested in driving, and just two different controllers. I bought some of these two displayshttps://www.buydisplay.com/catalog/product/view/id/1815/s/1-54-inch-tft-lcd-display-ips-panel-screen-240x240-for-smart-watch/category/208/That uses ST7789 which can be written at 60MHz.https://www.buydisplay.com/serial-spi-3-5-inch-tft-lcd-module-in-320x480-optl-touchscreen-ili9488That uses ILI9488 which can be written at 15MHz.My SPI can run at 21MHz, 10.5MHz, etc...CPU is 32F417.

author: peter-h
author_rank: Super Contributor
timestamp: June 08, 2025, 11:28:35 am
content: This is an update... I bought that 500 quid library.It took me a week full time to get graphics working on the ST7789 controller (240x240). It takes a long time to work out which of the many same-name files need to be used, and some of it is intended to be used under the MS VC++ devkit, which is fairly unusual for embedded work... It seems to work fine one you get it to hang together.Performance is just about OK for a 240x240 application, but is badly hit by two things: a) for graphics, the library transmits one byte at a time (not a string containing a command+data to an LCD controller) and b) I am using a shared SPI, which means it is mutexed, and after you transmit a byte you have to wait until it has been sent, lift -cs, and release the mutex. With a 21MHz SPI (and the ILI9488 is 15MHz max so has to be 10.5MHz) some 5us is spent on the mutex stuff (FreeRTOS standard functions) and 2us on the SPI stuff. If I remove the SPI sharing (which I can't do in the actual project) then the byte rate goes up to one every 1us! I am looking at two things: a) doing the mutex faster (the 32F4 must have a test-and-set instruction; even the 1985 Z280 had that) and b) getting Ramtex to give me a higher level interface (because internally they are obviously generating a string). This is on a 168MHz 32F417 and I can't change that.Code size is quite small and you can bring in as many .c files into your project as are needed for whatever graphics objects you need to draw.Then I moved to the ILI9488 controller (which is on the supported list) and it does nothing much. I quickly found that some of the initialisation commands used do not exist on the ILI9488, so it was probably never tested, despite being fairly popular for the 320x480 size which is ideal for so many embedded products. Putting in some values from projects found around the web I got it drawing what looks like the graphics but largely off the display and with wrong colours etc. Timing all checked, obviously.The feeling I get is that expecting all the controllers listed on their website to have been tested was optimistic. There is a huge number of nested #ifdefs etc for different controllers...Support is via email within a day or so but is of limited help on specifics.It's a funny area, where loads of people have reinvented the wheel and some got it working by accident. Some of the code I found online is clearly wrong... but then github is full of unfinished projectsI also reckon that most people buying a commercial library are going to use it for commercial projects and won't be feeding back what they found - either to the vendor, or (especially) posting it online. There is practically no trace online of anybody working with this library.It seems clear that you can draw graphics on a big range of controllers by using just a few commands which are similar or identical. That is how one can apparently support a lot of different controllers.

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 08, 2025, 01:08:49 pm
content: Controller support is trivial, just few low-level routines for the initialization, writing, addressing, often easy to modify.Even if you choose a supported controller, note that displays can made in different ways, what works for one might not in another, or get flipped image, swapped colors...To me, what really matters is the rest, the graphic lib itself.Have you seen uGUI? Doesn't have drop down menus, but supports windows, buttons, check lists, images, touch events...Just for fun, I ported it some time ago, adding better text support like full Unicode, single chars and/or ranges (If you wanted "0" and "9" you had to include all chars in the middle, wasting tons of space, but not anymore) and some drawing optimizations that really boosted the performance in repetitive pixel drawing.I also ported another STM32 demo, giving it DMA optimization and adding uGUI:https://github.com/deividAlfa/ST7789-STM32-uGUI

author: peter-h
author_rank: Super Contributor
timestamp: June 08, 2025, 01:55:05 pm
content: Not sure I saw that. There are lots of 7789 libs which do the basic ops but few that do stuff like rotated text.Anyway, I found a spot where this lib outputs 11 bytes in one go, and it does almost all operations using that. So I can stuff those into a buffer and use DMA...

author: krho
author_rank: Regular Contributor
timestamp: June 08, 2025, 04:11:21 pm
content: Why didn't you look at the open source LVGL?

author: peter-h
author_rank: Super Contributor
timestamp: June 08, 2025, 05:15:30 pm
content: I did and the reports were that it is big. For odd reasons I need something which is less than about 20k, and this is to include basic text.But maybe those reports were dud.From vague memory (from 2022 when I started on this bit) LVGL looked complicated to implement for different controllers. It is only now, having blown a solid week on this one, that I realise most of the work is in working out the initialisation (which everybody does differently and sometimes wrongly; not enough data bytes, etc) and then you use one or two basic functions to draw everything.I have just speeded up my output about 10x. I found that their lib calls a function called set_xyrange() whih emits 11 bytes in one go. It was trivial to put them into a buffer instead and at the end squirt them out, so the mutex handling is done only once.A slight complication with SPI is that you have to set up the controller's D/C line for each byte, which prevents the use of DMA unless you do some clever hack where the DMA is outputting 16 bit values, and one of the upper byte bits comes out on a GPIO... don't think that is actually possible with an internal SPI. Some controllers do have a 9 bit SPI mode though.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 08, 2025, 06:37:03 pm
content: I would have recommended LVGL as well. But I haven't used it yet. It's likely to take much more than 20 KB of code though.As to D/C, normally the bulk of the transfers should be data transfers and so should not require changing D/C for every byte.The commands should be much fewer. Unless the display is updated by very small areas repeatedly, which would be very suboptimal, but not knowing how this Ramtex lib operates, I don't know.

author: peter-h
author_rank: Super Contributor
timestamp: June 08, 2025, 07:14:11 pm
content: QuoteSo others said. It might also be pretty slow...Also, and many will regard this comment as dumb, but I went the commercial lib route because I've seen so much time spent fixing open source modules on this project, and I just don't have time for that anymore. LWIP took many man-weeks to fix, MbedTLS took much more (and that was done by a real X509 specialist). FreeRTOS and FatFS were pretty easy. The OS ex-ST USB code (MSC and CDC) was horrendous. So one sees this £500 library which advertises support for loads of controllers...QuoteNormally there is a command byte followed by at most four data bytes. Maybe for area fills it's different but that doesn't appear to have been optimised to splat out an entire horizontal line.But, think about it, if you are implementing viewports, you can't use those features anyway. You have to output just pixels (or little bits) and check if each one falls inside the viewport.But a software loop driven SPI is pretty fast. I am shifting 11 bytes in 14us, and that is without any optimisations like using 16 bit SPI mode (there is a nice thread on SPI performance).I think it is a tradeoff for a generic library. I did a ton of graphics in the 1980s with an 8MHz Z80 and the then fancy coprocessors like the SGS 9367 and the NEC UPD7220. The manual for the latter takes about a year to study... I built a nice graphics machine for a totally custom purpose. Fonts and all. Coding these controllers is a totally custom task. Whereas AFAICT most today's LCD controllers are dumb chips which don't even do clever stuff like circles, let alone polygon fills. I am sure the PC graphics chips do everything but that's another level... So nowadays you "can support" loads of controllers just by using two functions as the Ramtex lib does.I spent a day trying to optimise it further by getting it to send a few hundred bytes into a buffer and then sending that to SPI either when full or when a timeout (say 10ms) expires. This would be great with DMA, but then you can't do the D/C line. For whatever reason that did not work so I chucked it out. Then I discovered those two functions and optimised them to use a buffer, and I now have a roughly 5x speedup.

author: langwadt
author_rank: Super Contributor
timestamp: June 08, 2025, 07:19:22 pm
content: looked at this?https://github.com/Bodmer/TFT_eSPI

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 08, 2025, 07:19:29 pm
content: There's no point in using DMA for small transactions.Its is, for example, if you draw a large square. You send the window addressign command normally, then the RAMWR command... and leave CS low.Now you can pack all you want and send using DMA until the data is done.

author: peter-h
author_rank: Super Contributor
timestamp: June 08, 2025, 07:24:33 pm
content: QuoteNo, but thanks for the pointer. I will assume they actually tested the ILI9488and see if their config works.A little demo video

author: krho
author_rank: Regular Contributor
timestamp: June 09, 2025, 02:18:12 pm
content: We've replaced uGFX with LVGL in about a month. It's approximately 140kb with TTF support.

author: globoy
author_rank: Frequent Contributor
timestamp: June 09, 2025, 02:44:06 pm
content: FWIW I've used LVGL for many projects, commercial and private. I've even created devices that run the exact same GUI code on a local LCD and in a web browser. Over several major versions I've found it to be fairly bug-free, with an active online support community that's frequented by the authors who are very helpful. It can also be performant. Easy to reach 30 FPS on simple micro-controllers, 60 FPS on micro-controllers with some 2D graphics support (e.g. many STM32). An unlike this Ramtex library (upon seeing, my first thought was "Windows 95 is calling and wants its graphics back") it can generate modern, anti-aliased UIs, with dynamic (automatically re-adjusting) displays and large font/image type support.

author: peter-h
author_rank: Super Contributor
timestamp: June 09, 2025, 10:02:51 pm
content: QuoteBloody hell - a month?? Somebody made nice money there.QuoteThat's probably trueBut for most embedded GUIs that's fine. And one doesn't need 30fps or anything like that.QuoteThat's probably more important on large displays but those need to be fed via a 24 or 32 bit parallel bus. I need to run this via SPI. So it will always be speed limited by my 21MHz max SPI clock, and by the controller max clock (60MHz for the 7789 and 15MHz for the 9488).This stuff is very application dependent.The Ramtex library appears to have selectable anti aliasing.140k is a lot in this case.

author: JPortici
author_rank: Super Contributor
timestamp: June 10, 2025, 09:15:21 am
content: Quote from: peter-h on June 08, 2025, 05:15:30 pmROM or RAM?ROM, i wouldn't be surprised if it was five times that. LVGL has loooooots of code. RAM, it depends. You can almost omit the framebuffer if you want to, and just keep the layout objectsLVGL is heavy, contrary to its name, but it's really versatile. I'm happy to put up with that because it's much easier to create beautiful UIs than other libraries i tried.Also the fact they put the effor to make it multiplatform (i think their official GUI editor, squarestudio, is actually rendered using LVGL because the look and feel is exactly the same)Re: Antialias.Nope, it's very important on small screens as well. Yes, depends on the application. We are in the process of doing a gadget with those 2 inch 240x240 round displays, enabling antialiasing is night and day. Also being able to do the complete development on a PC, because of the multiplatform aspect, is a huge plus

author: peter-h
author_rank: Super Contributor
timestamp: June 10, 2025, 10:58:51 am
content: ROM. 100k would be a LOT given that a linker strips out uncalled functions, etc.The Ramtex lib has anti alias on fontsThe manual indicates it may have it on other things but doesn't actually say that.This lib also offers a PC interface, via USB, but I could not be bothered to spend time on it. 99% of the time is spent integrating the LCD controller...But yeah it is old - from about 2010. But that's not a bad thing. Most of the LCD controllers (the kind likely to be used in production embedded boxes) are also old. Post covid, only a fool will use something funky and newA few things I find pretty odd e.g. no origin definition (0,0 is always at top left of the screen), the colour setting options are weird ("The viewport color modes apply only for 2-color symbols and fonts, i.e. ‘black-and-white' symbols. For multicolor symbols the viewport color settings are ignored and the colors defined by the symbol are used directly instead") etc. I am not actually sure the above pic is antialiased font or not. You can disable it but it is enabled.

author: JPortici
author_rank: Super Contributor
timestamp: June 10, 2025, 11:32:38 am
content: Quote from: peter-h on June 10, 2025, 10:58:51 amThe LLVM linker, maybe. the GCC linker in my experience is not that intelligent, because gcc itself can't detect when that a non static function is never called, and -gc-section will wreak havoc(but the 100k also include fonts and whatnot)

author: peter-h
author_rank: Super Contributor
timestamp: June 10, 2025, 11:56:22 am
content: GCC linker is IME very good at stripping stuff out. It will strip out most of your product if you do not explicitly reference main()e.g. if you jump to it, via a fixed address, from a boot block.

author: peter-h
author_rank: Super Contributor
timestamp: June 11, 2025, 11:15:25 am
content: I wonder if anybody here might spot something obvious with this "wrong colour" problem.This test code (which is straightforward, and has been used before; it just draws some colour boxes, in the order of that "color" array)Code:/*Fill an area in a box with a named color*/static void colorbox(GXT xb, GYT yb, GXT xe, GYT ye, GCOLOR col){gsetcolorf(G_BLACK);grectangle(xb,yb,xe,ye);gsetcolorb(col);gfillvp(xb+1U,yb+1U,xe-1U,ye-1U,0U);}// Colour test functionstatic void colour_test(void){static const GCOLOR color = {G_RED,// upper lineG_ORANGE,G_YELLOW,G_GREEN,G_CYAN,G_BLUE,G_MAGENTA,G_WHITE,// lower lineG_LLIGHTGREY,G_LIGHTGREY,G_GREY,G_DARKGREY,G_DDARKGREY,G_BLACK};GXT x; GXT w; GXT wcnt;GYT y; GYT h; GYT hcnt;GCOLOR col;ginit();wcnt = 7U;hcnt = 2U;w = GDISPW / (wcnt + 2U);h = GDISPH / (hcnt + 2U);#if ((GDISPW <= 160U) || (GDISPH <= 160U))gselfont(&narrow10);#elsegselfont(&narrow20);#endif/* Top text */gsetmode(GALIGN_HCENTER|GLINECUT);gputs("Show Named Colors");/* Bottom text */gsetpos(0U,GDISPH-1U); /* Bottom pixel line */gputs("Bottom Text");col = 0U;for (y = 1U; y <= hcnt; y++){for (x = 1U; x <= wcnt; x++){/* colorbox() is located above in this module */colorbox(x*w, y*h, ((x+1U)*w)-2U, ((y+1U)*h)-2U, color);osDelay(50);// slow down drawing so colour order is visiblecol++;}}}produces thisTop left should be red, and top right should be magenta. The bottom all-greys line needs the gamma fixed (I did that; it is one of the LCD controller config bytes) but it is backwards i.e. bottom right should be black.And it isn't simply what one might think i.e. inverted colours.The colour defines seem clear for RGB so e.g. G_RED is 0xff 0x00 0x00:Code:/*Map "pure color names" to binary RGB color valuesThese definitions map a symbolic color value to the corresponding RGB valuematching the GDISPPIXW color type settings*/#define G_BLACK G_RGB_TO_COLOR(0x00U,0x00U,0x00U)#define G_RED G_RGB_TO_COLOR(0xffU,0x00U,0x00U)#define G_GREEN G_RGB_TO_COLOR(0x00U,0xffU,0x00U)#define G_YELLOW G_RGB_TO_COLOR(0xffU,0xffU,0x00U)#define G_BLUE G_RGB_TO_COLOR(0x00U,0x00U,0xffU)#define G_MAGENTA G_RGB_TO_COLOR(0xffU,0x00U,0xffU)#define G_CYAN G_RGB_TO_COLOR(0x00U,0xffU,0xffU)#define G_WHITE G_RGB_TO_COLOR(0xffU,0xffU,0xffU)/* Names for extended colors */#define G_LLIGHTGREY G_RGB_TO_COLOR(0xd0U,0xd0U,0xd0U)#define G_LIGHTGREY G_RGB_TO_COLOR(0xb8U,0xb8U,0xb8U)#define G_GREY G_RGB_TO_COLOR(0xa8U,0xa8U,0xa8U)#define G_DARKGREY G_RGB_TO_COLOR(0x90U,0x90U,0x90U)#define G_DDARKGREY G_RGB_TO_COLOR(0x78U,0x78U,0x78U)#define G_DARKGREEN G_RGB_TO_COLOR(0x00U,0xc0U,0x00U)#define G_DARKBLUE G_RGB_TO_COLOR(0x00U,0x00U,0xc0U)#define G_ORANGE G_RGB_TO_COLOR(0xffU,0x80U,0x00U)The graphics drawn are correct, so it isn't a case of wrong byte or wrong bit order on SPI.I am running the controller in 16 bit colour and this assembles the 16 bits from the three colour bytesCode:#define G_RGB_TO_COLOR(r,g,b) ((((GCOLOR)(r)&0xf8U)<<8)|(((GCOLOR)(g)&0xfcU)<<3)|(((GCOLOR)(b)&0xffU)>>3))which looks OK too. But I wonder if the 16 bit colour format is standard? The ST7789P3 uses 5,6,5.I think the above is wrong, because the controller wants this

author: dave j
author_rank: Regular Contributor
timestamp: June 11, 2025, 11:57:22 am
content: I note when you draw the white border for the boxes, you specify black as the colour. Does the display init code specify it as inverted? That would explains the colour differences.Under the MIPI standard, the display inversion on is command 21h and off is 20h.

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 11, 2025, 01:14:05 pm
content: Weird, it looks like#F800is getting inverted to#007FF.Did you debug the actual data sent to the display or SPI transfer function?Show G_RGB_TO_COLOR macro, maybe wrong endianess?

author: peter-h
author_rank: Super Contributor
timestamp: June 11, 2025, 01:32:23 pm
content: The G_RGB_TO_COLOR macro is above, and yes I am tracing the code all the way to SPI.Code:#define G_RGB_TO_COLOR(r,g,b) ((((GCOLOR)(r)&0xf8U)<<8)|(((GCOLOR)(g)&0xfcU)<<3)|(((GCOLOR)(b)&0xffU)>>3))It is bizarre. It is easy to use the wrong file (out of so many similar ones) but in the entire project directory there is no apparently correct G_RGB_TO_COLOR macro. The 16 bit RGB mode (RGB 5,6,5) is encoded with a 1 in the middle of the 6-bit G field, which is a bit of a giveawayThe R value seems right with a 0xf8 mask and then shift left 8. But bit 7 is supposed to be a 1 so you need an OR with 0x80.The rest is wrong.Dammit - just spotted that SPI diagram is for 9 bit SPI mode! You want thisSo:Code:#define G_RGB_TO_COLOR(r,g,b) ( (((GCOLOR)(r)&0x1fU)<<11) | (((GCOLOR)(g)&0x3fU)<<5) | ((GCOLOR)(b)&0x1fU) )

author: peter-h
author_rank: Super Contributor
timestamp: June 11, 2025, 02:14:01 pm
content: Bugger.The ER-TFT1.54-1 display does not bring out the controller's SPI mode selector pins, but it does bring out the D/C signal so the interface "must" be 4-line...If it wasn't. the graphical drawing would not work at all, would it?There is no colour spec command on this chip. Every pixel loaded has the colour implicitly specified via the 16 bits loaded.Something weird is going on. I wonder if there is a palette involved. The chip init doesn't enable or load anything like that, however. The graphics lib has palette options but I don't need them, with 16 bit colour.(replying to a post which has just disappeared)

author: DavidAlfa
author_rank: Super Contributor
timestamp: June 11, 2025, 03:44:50 pm
content: What's the problem?Toggling a GPIO doesn't add much overhead, it's only required before each cmd or data block.

author: peter-h
author_rank: Super Contributor
timestamp: June 11, 2025, 05:14:01 pm
content: That is all sorted and working.Currently I am finding something odd and I wonder if it is perhaps standard.Setting the display memory to zeroes and issuing the Display ON command shows an all-white screen. Is that some convention?The #defines in this lib define background as white and foreground as black. Is that normal? It seems to be. To get a black background:Now it works. It's a curious convention!All my graphics are now the correct colours. That RGB macro was actually right; they combine the 8 -> 5/6 truncation and shifting in one go.Every day is a school day!

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 11, 2025, 06:18:00 pm
content: Yes I've noticed too that some display *modules* (by modules I mean panel + driver IC) require inversion to get normal (not inverted) colors.I think it's not a convention, but has all to do with the panel itself, some are apparently driven in reverse.So you can't unfortunately expect the same behavior with the same driver IC but different panels. Some manufacturers of display modules do provide a specific initilization sequence, and you'll see that not two modules (well, almost) with the same driver IC require exactly the same parameters. Yes, that's a mess.

author: peter-h
author_rank: Super Contributor
timestamp: June 11, 2025, 07:03:56 pm
content: The 7789 controller clearly says you have to use the invert command to get white pixels on black background.I suppose some people want it backwards. I have a reflow oven on which the text is black...

author: langwadt
author_rank: Super Contributor
timestamp: June 11, 2025, 09:15:45 pm
content: Quote from: peter-h on June 11, 2025, 07:03:56 pmuntil darkmode became a thing, wasn't black text on white background the norm?

author: peter-h
author_rank: Super Contributor
timestamp: June 11, 2025, 09:33:12 pm
content: MaybeBTW the ILI9488 init code at the previously posted URLhttps://github.com/Bodmer/TFT_eSPI/blob/master/TFT_Drivers/ILI9488_Init.his probably never tested. It contains the same mistake as all other ILI9488 code I found, in that the B1 (frame rate control) command has two parameters, not one. Everybody is copying everybody else's open source codeI've been asking Claude for any pointers and it says "Color Format: The ILI9488 does not support RGB565 mode over SPI and only supports RGB666 mode (which is "stripped" RGB888 ") which is staggering. It found it here:https://forum.lvgl.io/t/driver-for-ili9488-display/2667. Bloody hell.The 9488 supports only 3 or 18 bits/pixel over SPI. But internally to your code you probably want to work as 24. The chip's interface then wastes 2 bits of each colourThisisin the DS but not explicitly. You find it by omission only.Also other settings were needed for the actual LCD, as suggested to me above. This one needs the RGB order changed. I eventually set it up for landscape since that is more useful for a 320x480 panel, arguably.It's pretty obvious this library never tested this controller and I bet whoever is coding LVGL would not have spotted it eitherAnd those who did probably didn't submit their solution to the devs. Basically your best bet is to run the lib in 24 bit mode (since 18 bits also needs 3 bytes) and then treat the controller as 24 bit, while configuring it for 18 bits. If you actually config it for 24 bits, via SPI, it is dead.

author: peter-h
author_rank: Super Contributor
timestamp: June 13, 2025, 06:55:51 am
content: I wonder if anyone has come across this before, on the ILI9488.The command {CMD, 0U,GCTRL_RESET} which everybody is using at the start of initialisation, causes the display to come up white. Not quite full brightness; about halfway up. This causes a brief flash.One could suppress it by turning off the backlight (a final product needs a provision for dimming that anyway) but I wonder if there is a smarter way. I've spent hours on it...Curiously the ILI9488 does not need the inversion mode on. Loading 0x00 into the frame buffer produces a black display.This is the relevant codeCode:{CMD, 0U,0x00U},// NOP{CMD, 0U,GCTRL_RESET},// Reset registers to default - this causes the flash on the display{CMD, 10U,GCTRL_SLPOUT},// Exit any sleep mode - this command is essential. >5ms wait required{CMD, 0, 0xB0},// Interface Mode Control, SDO not used{DAT, 0, 0x80},{CMD, 0, GCTRL_COLMOD}, // pixel interface format{DAT, 0, 0x66},// 18 bits/pixel (only 3 or 18 supported over SPI){CMD, 0, 0xE9},// Disable 24 bit parallel bus{DAT, 0, 0x00},// This is important since LCD controllers by convention map 0x00 in display RAM to white!// It goes together with the #defines for FB and BG colours being white and black// but the ILI9488 does not do this.{CMD, 0U,0x20U},// Display inversion OFF//{CMD, 0U,0x21U},// Display inversion ONThe issue arises because the Ramtex library calls the controller init function at various places involving e.g. screen clear, etc.BTW, as a code size data point, I am seeing 46k used, and that includes some fonts, drawing circles, etc. as in the video below. As for RAM, only a few hundred bytes but then I allocate a whole line buffer of 480x3 bytes for much faster block fills using DMA.But remember this library doesn't any any clever stuff like saving previous content around an object being redrawn (the "damaged area"). I believe LVGL has this option, but obviously it is complex, and probably involves the use of a heap. With this one, if say you want to draw an analog clock, you need to manage it yourself. Probably, for each of the three hands, you fill a rectangle (one that encloses the hand being redrawn) with the background colour, and redraw all three hands, every second. Or, draw the hand being redrawn wholly in the background colour, and redraw all three hands.Since the theoretical max speed of clearing the screen is 320x480x3 bytes at 10.5MHz which comes to about 300ms, which is very visible, I think turning off the backlight is the way to go.

author: peter-h
author_rank: Super Contributor
timestamp: July 23, 2025, 06:06:41 pm
content: Just an update in case somebody finds this in the future...Pluses:Yes the 500 cost is worth paying because it will save you a load of time writing even the most basic stuff.There is good font and text drawing support (which is very time consuming to do yourself). The font/icon editor is excellent.The code size is very small. Probably most of the FLASH usage will be fonts (and you can subset these using the icon editor, if needed).It works without a frame buffer of any kind; even a 320x480 LCD needs a LOT of RAM for a frame buffer otherwise.The author provides OK email support, for a while.Quite easy to load the library into Cube IDE.Minuses:It provides no support for maintaining the "damaged area" so you can restore objects, etc. LVGL does that (but is a lot bigger).Some of it is quite slow. This is partly due to viewport implementation which dictates pixel-at-a-time drawing in a lot of cases, but also to support many controllers.The manual is hard to get into. It is descriptive but fails to describe basic concepts, and some config gotchas (e.g. palette!=0 for some stuff to work at all).It seems clear that many of the "supported" LCD controllers were never tested (but that's to be expected).Graphics support could be a lot better e.g. thick lines, thick circles, filled rotated rectangles, filled polygons, etc. But you can find code for these easily enough.The code is a mess in a lot of places due to the many controller options. I chopped a load of it out because I decided to support just the two controllers.No anti aliasing on graphics. There is some anti aliasing on text if drawn in the rotated mode (quite slow though).You cannot pass on source code to customers etc.Running via SPI, not parallel, I ended up doing a lot of low level optimisation to write consecutive pixels (horizontal line) into a buffer (one LCD line size is enough) which is then sent out to SPI using DMA. This dramatically speeds up most graphics (but not text, because this is viewport-clipped pixel by pixel).Anyone using this should use a parallel controller bus, not SPI. Even 8-bit parallel will be much faster than any SPI.This clock example is via SPI at 10.5MHz and is not bad, but flickers ever so slightly and that was after spending a lot of time on the erasing/drawing order. But even something this simple, you could spend half your life on making it look good.

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 23, 2025, 07:03:42 pm
content: Very slow! Compiler optimization?

author: peter-h
author_rank: Super Contributor
timestamp: July 23, 2025, 07:28:11 pm
content: The code is not the bottleneck AFAICT. It is the SPI interface.This is SPI at 21MHzOptimisation is -Og. I've rebuilt with -O3 and see no change in graphics. And yes it all still runs - TLS and all, amazinglyBut wait... I will do some scope timing now for -O3. Times to draw all 3 hands, the four numbers 3 6 9 12 and the middle blob:-Og 10.5MHz 45ms code=590k-O3 10.5MHz 38ms code=644k-O3 21.0MHz 27msSo -O3 helps a little bit, but nothing visible. Also one could not use 21MHz in production - out of spec for ILI9488.Also I have about 25 tasks running under the RTOS and sometimes the drawing of a hand etc may get a gap in it. I have a web server and https client running at the same time as all this. The non regular flicker is probably due to these. One could play with the RTOS priority (currently this task is very low) but I am not going to do that right now.This is an example of one of my hacks to speed up (in this case) horizontal linesCode:ghw_set_xyrange(xb,yb,xe,yb);if ((xe-xb)<5)// single pixel case (xe=xb), or < 5 pixels to write{for (; xb <= xe; xb++ ){ghw_auto_wr(color);// This uses SPI loop}}else{// Fill line buffer with the colourint lfidx=0;for (int i=0; i<(xe-xb); i++){#ifdef LCD_CONTROLLER_ST7789linefillbuf[lfidx++] = (SGUCHAR)(c>>8);linefillbuf[lfidx++] = (SGUCHAR)(c);#endif#ifdef LCD_CONTROLLER_ILI9488linefillbuf[lfidx++] = (SGUCHAR)(color>>16);linefillbuf[lfidx++] = (SGUCHAR)(color>>8);linefillbuf[lfidx++] = (SGUCHAR)(color);#endif}// Send the buffer to the controller with DMA, D/C=1 for the whole transferLCD_Transmit_buf_DAT(linefillbuf, lfidx);}All the trig is precomputed before any drawing, whenever seconds change. I can't avoid the very slow double floats for the trig but it gets done in "spare time":Code:// 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 pointless because this is all// done before any drawing takes place (at 1Hz)// hours handhrs_angle = (((hrs*5.0f)+45.0f)+(mins/12.0f)) * TWOP / 60.0f;hrs_hand_x = (int)(centre_x + (hrshandl/2)*cos(hrs_angle) + 0.5f);hrs_hand_y = (int)(centre_y + (hrshandl/2)*sin(hrs_angle) + 0.5f);// minutes handmins_angle = ((mins+45.0f)+(secs/60.0f)) * TWOP / 60.0f;mins_hand_x = (int)(centre_x + (minshandl/2)*cos(mins_angle) + 0.5f);mins_hand_y = (int)(centre_y + (minshandl/2)*sin(mins_angle) + 0.5f);// seconds hand - has the short tail 180 degrees (PI radians) on the other sidesecs_angle = (secs+45.0f) * TWOP / 60.0f;secs_hand_start_x = (int)(centre_x + secshandt*cos(secs_angle+(TWOP/2.0f)) + 0.5f);secs_hand_start_y = (int)(centre_y + secshandt*sin(secs_angle+(TWOP/2.0f)) + 0.5f);secs_hand_end_x = (int)(centre_x + secshandl*cos(secs_angle) + 0.5f);secs_hand_end_y = (int)(centre_y + secshandl*sin(secs_angle) + 0.5f);FWIW the above maths takes 145us with -Og and same with -O3 because ST don't supply math.c in source form; it comes in libc.a I think.It is actually quite impressive that this chip does eight double sin/cos calculations in 145us! They are done totally in software; only single floats have hardware support on the 32F417.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 25, 2025, 06:49:44 pm
content: You took great care to use float, and f suffix for float literals, but then call sin()/cos(); try sinf()/cosf() to see if it improves the calculation performance. Especially important for both flash usage and speed if your MCU only has single precision floating point.It's a good idea to use objdump or nm to list functions (maybe sort by size) and see if software double precision library is being pulled in; that would mean you accidentally used double somewhere.

author: peter-h
author_rank: Super Contributor
timestamp: July 25, 2025, 08:41:40 pm
content: Jesus - I never knew of sinf/cosf.145us is down to 19usand that is despite me having added another four trigs:Code:hrs_angle = (((hrs*5.0f)+45.0f)+(mins/12.0f)) * TWOP / 60.0f;hrs_hand_x = (int)(centre_x + (hrshandl/2)*cosf(hrs_angle) + 0.5f);hrs_hand_y = (int)(centre_y + (hrshandl/2)*sinf(hrs_angle) + 0.5f);// minutes handmins_angle = ((mins+45.0f)+(secs/60.0f)) * TWOP / 60.0f;mins_hand_x = (int)(centre_x + (minshandl/2)*cosf(mins_angle) + 0.5f);mins_hand_y = (int)(centre_y + (minshandl/2)*sinf(mins_angle) + 0.5f);// seconds hand - has the short tail 180 degrees (PI radians) on the other sidesecs_angle = (secs+45.0f) * TWOP / 60.0f;secs_hand_start_x = (int)(centre_x + secshandt*cosf(secs_angle+(TWOP/2.0f)) + 0.5f);secs_hand_start_y = (int)(centre_y + secshandt*sinf(secs_angle+(TWOP/2.0f)) + 0.5f);secs_hand_end_x = (int)(centre_x + secshandl*cosf(secs_angle) + 0.5f);secs_hand_end_y = (int)(centre_y + secshandl*sinf(secs_angle) + 0.5f);secs_arrow_corner1_x = (int)(centre_x + (secshandl-secsarrowl)*cosf((secs_angle-secsarroww)) + 0.5f);secs_arrow_corner1_y = (int)(centre_y + (secshandl-secsarrowl)*sinf((secs_angle-secsarroww)) + 0.5f);secs_arrow_corner2_x = (int)(centre_x + (secshandl-secsarrowl)*cosf((secs_angle+secsarroww)) + 0.5f);secs_arrow_corner2_y = (int)(centre_y + (secshandl-secsarrowl)*sinf((secs_angle+secsarroww)) + 0.5f);Actually if I wanted a fast sin() I would use thisCode:// Compact sin(x) function which doesn't need math.h// Uses Horner's method// i is from 0 to SPC-1#define PI 3.14159265358979323846f#define SCALE 13107int sin2(int i){float rad = 2*PI*i/SPC, term=rad, sin=rad, old_sin=0.0f;for (int n=3; (sin != old_sin) && (n < 30); n+=2){old_sin = sin;term *= (rad*rad) / (-n * (n-1));sin += term;}return (sin * SCALE) + (sin >= 0 ? 0.5f : -0.5f);}which I use elsewhere in a synchro emulator... But with sinf() taking under 2us, I doubt this sin() is actually faster! It does a Taylor series over n terms.I do use double floats in some other places where I need to do stuff to 32 bits precision.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 26, 2025, 02:08:11 pm
content: Yes, base C math functions use double precision FP by default and if you use 'float', they'll still do double-precision and implicitely convert back and forth to float. Pretty wasteful. Double-precision FP when you only have a single-precision FPU is much slower than just twice the time, as you saw.The functions with the 'f' suffix are the pure single-precision versions.Note that if you want to make your code 'agnostic' to that and still use the corresponding math functions depending on type, you can include tgmath.h instead of math.h. That's all it takes. That's not just for the "lazy": it makes your code identical regardless of the FP type you use.For instance:Code:typedef float fp_t;fp_t f(fp_t x){return sin(x) * fabs(cos(x));}The compiler will use sinf(), fabsf() and cosf(). To convince yourself:https://godbolt.org/z/afPajGccvJust change fp_t to 'double' and it will use sin(), fabs() and cos().And yes, you can use an approximation of sin() if great accuracy is not required. Even just 3 terms should be enough for drawing a clock.In your case, note that if you're ok with say, a 1° resolution for angles (knowing that the 'second' only needs 6° of resolution, 1° should be plenty), you can just precompute an array of 360 (cos(a), sin(a)) with 'a' an integer in °, and you have a look-up table for 360*4*2 = 2880 bytes in flash. And of course, if 2° of resolution is enough, that'll halve the required size, etc. After that, you only need to handle your angles in degrees instead of radians, to avoid unnecessary conversions.Giving you a complete example of implementation below, that should be way more efficient (again if a resolution of 1° is enough, you can of course adapt it to a higher or lower discrete resolution):Code:#include <tgmath.h>typedef struct { float Cos; float Sin; } Cos_Sin_t;typedef struct { int x; int y; } Point_t;#define COS_SIN_TABLE_NPOINTS360static Cos_Sin_t Cos_Sin_Table;void Cos_Sin_Table_Init(void){const float Deg2Rad = 2.0f * M_PI / 360.0f;float theta = 0.0f;for (int a = 0; a < COS_SIN_TABLE_NPOINTS; a++){Cos_Sin_Table = (Cos_Sin_t){ .Cos = cos(theta), .Sin = sin(theta) };theta += Deg2Rad;}}Cos_Sin_t Cos_Sin(float angle_deg){int a = (int)(angle_deg + 0.5f) % 360;return Cos_Sin_Table;}Point_t PointOnCircle(Point_t C, float R, float angle_deg){Cos_Sin_t CS = Cos_Sin(angle_deg);return (Point_t){ .x = (int)(C.x + R*CS.Cos + 0.5f), .y = (int)(C.y + R*CS.Sin + 0.5f) };}Call Cos_Sin_Table_Init() once to initialize the LUT, you can of course alternatively create it via a script and put the values as an initilalzer for the array, but this function has to be called only once so that's probably not a big deal at all.Then, you have the PointOnCircle() function that can directly be called to get the coordinates of a point on a circle of center C and radius R, at an angle of angle_deg in degrees.Here's the compiled code. It should be pretty fast. Unless you're very short on Flash, that's Flash area well spent IMO:https://godbolt.org/z/GP1fxqoed

author: langwadt
author_rank: Super Contributor
timestamp: July 26, 2025, 03:09:35 pm
content: Quote from: SiliconWizard on July 26, 2025, 02:08:11 pmand the can easily be reduced since 3rd,4th quadrant of Sin is just 1st,2nd quadrant negated, and 2nd is just 1st backwards, and of course Cos is just in with an offset anglebinary angles makes the math a bit easier,https://en.wikipedia.org/wiki/Binary_angular_measurement

author: peter-h
author_rank: Super Contributor
timestamp: July 26, 2025, 04:58:20 pm
content: Thank you.The clock is just a fun thing, to test out graphics functions and such.The sin/sinf exec time is not a problem because I loop around, with a osDelay(100) in the loop, and if the seconds have not changed I yield to the RTOS (so with a 1kHz systick 99/100 of the time one yields to the RTOS) and if the secs have changed then I- compute the new hand positions (this is where all the trig is done)- undraw the hands in their previous positions (these are initialised to impossible values so initially one is not drawing background-colour stuff all over the place)- draw the new hands (the order of these matters; no perfect solution but I draw hrs mins secs, in that order, with some fudge on the seconds hand due to artefacts)- update old positions from new positionsSo even if the trig took say 200ms (which is about 10,000 times longer than it actually takes) you would not notice anything.Code:static void drawLine (int x1, int y1, int x2, int y2){gmoveto(x1,y1);// Ramtex library functionsglineto(x2,y2);}static void drawHorizontalLine (int x1, int x2, int y){gmoveto(x1,y);// Ramtex library functionsglineto(x2,y);}// 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)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 = cosf(angle);float sinAngle = sinf(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.5f); // +0.5 for roundingrotatedCorners = (int)(cy + rotatedY + 0.5f);}// 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);}// Function to draw a filled 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// This function is as fast as one can do. Each horizontal line is optimised (in glineto)// to use DMA.void drawFilledRotatedRectangle(int cx, int cy, int width, int height, float angle){float halfWidth = width / 2.0f;float halfHeight = height / 2.0f;float cosAngle = cosf(angle);float sinAngle = sinf(angle);// Calculate rotated corner pointsfloat corners ={{-halfWidth, -halfHeight}, // top-left{ halfWidth, -halfHeight}, // top-right{ halfWidth, halfHeight}, // bottom-right{-halfWidth, halfHeight} // bottom-left};float rotatedCorners;int i;for (i = 0; i < 4; i++){rotatedCorners = cx + corners * cosAngle - corners * sinAngle;rotatedCorners = cy + corners * sinAngle + corners * cosAngle;}// Find Y boundsfloat minY = rotatedCorners;float maxY = rotatedCorners;for (i = 1; i < 4; i++){if (rotatedCorners < minY) minY = rotatedCorners;if (rotatedCorners > maxY) maxY = rotatedCorners;}// For each scanline, find intersection points with rectangle edgesint y;for (y = (int)(minY - 1); y <= (int)(maxY + 1); y++) {int intersections;int intersectionCount = 0;// Check intersection with each edgefor (i = 0; i < 4; i++) {int next = (i + 1) % 4;float y1 = rotatedCorners;float y2 = rotatedCorners;float x1 = rotatedCorners;float x2 = rotatedCorners;// Check if scanline intersects this edgeif ((y1 <= y && y < y2) || (y2 <= y && y < y1)){if (y1 != y2) // Avoid division by zero{float t = (y - y1) / (y2 - y1);int x = (int)(x1 + t * (x2 - x1) + 0.5f);if (intersectionCount < 4){intersections[intersectionCount++] = x;}}}}// Sort intersections and draw spansif (intersectionCount >= 2){// Simple bubble sort for small arrayint j, k;for (j = 0; j < intersectionCount - 1; j++){for (k = 0; k < intersectionCount - j - 1; k++){if (intersections > intersections[k + 1]){int temp = intersections;intersections = intersections[k + 1];intersections[k + 1] = temp;}}}// Draw spans between pairs of intersectionsfor (j = 0; j < intersectionCount; j += 2){if (j + 1 < intersectionCount){drawHorizontalLine(intersections, intersections[j + 1], y);}}}}}// 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){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 mode//gsetcolorf(col);}// 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 but you could spend your whole life on it :)// A lot of the values below work better visually with odd numbers.// Filled hands thicker than 4px draw with DMA and much faster.void drawclock (void){#define TWOP (3.141592654f*2.0f)#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 9// fat mark width#define fatmarkl 19// fat mark length#define MIDBLOB 7// middle clock face blob radius#define secshandl (radius-24)// seconds hand length#define secshandt (radius/4)// seconds hand tail length#define secsarrowl 25// length of seconds hand arrow#define secsarroww (TWOP/120.0f)// half-width of secs hand arrow (radians)#define minshandl (secshandl-28)// mins hand length#define minshandw 5// mins hand width#define hrshandl (minshandl-20)// hrs hand length#define hrshandw 9// hrs hand width#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// 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))*cosf(angle) + 0.5);fatmark_y = (int)(centre_y + (radius-(fatmarkl/2))*sinf(angle) + 0.5);drawFilledRotatedRectangle(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)*cosf(angle) + 0.5);shortline_s_y = (int)(centre_y + (radius-shortmarkl)*sinf(angle) + 0.5);shortline_e_x = (int)(centre_x + radius*cosf(angle) + 0.5);shortline_e_y = (int)(centre_y + radius*sinf(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);gsetcolorf (COLSECS);gcircle(centre_x, centre_y, MIDBLOB, 1U);int lastsecs=-1;// -1 forces an initial updatefloat hrs,mins;int secs;float secs_angle;float mins_angle;float hrs_angle;float hrs_angle_l=100.0f;// init to impossible value (bigger than TWOPI)float mins_angle_l=100.0f;int secs_hand_start_x, secs_hand_start_y, secs_hand_end_x, secs_hand_end_y;int secs_arrow_corner1_x, secs_arrow_corner1_y, secs_arrow_corner2_x, secs_arrow_corner2_y;int mins_hand_x, mins_hand_y;// these are the *centre* of the rectangleint hrs_hand_x, hrs_hand_y;// "last" valuesint hrs_hand_x_l=0;int hrs_hand_y_l=0;int mins_hand_x_l=0;int mins_hand_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;int secs_arrow_corner1_x_l=0;int secs_arrow_corner1_y_l=0;int secs_arrow_corner2_x_l=0;int secs_arrow_corner2_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 initial garbage in the time valueif ( (secs!=lastsecs) && ((secs>=0) && (secs<=59)) ){lastsecs=secs;// Precalculate everything. We use single floats (sinf/cosf), because C uses double floats, and the 32F4// has only hardware single floats, but it is pointless because this is all done before any drawing takes// place (at 1Hz)// hours handhrs_angle = (((hrs*5.0f)+45.0f)+(mins/12.0f)) * TWOP / 60.0f;hrs_hand_x = (int)(centre_x + (hrshandl/2)*cosf(hrs_angle) + 0.5f);hrs_hand_y = (int)(centre_y + (hrshandl/2)*sinf(hrs_angle) + 0.5f);// minutes handmins_angle = ((mins+45.0f)+(secs/60.0f)) * TWOP / 60.0f;mins_hand_x = (int)(centre_x + (minshandl/2)*cosf(mins_angle) + 0.5f);mins_hand_y = (int)(centre_y + (minshandl/2)*sinf(mins_angle) + 0.5f);// seconds hand - has the short tail 180 degrees (PI radians) on the other sidesecs_angle = (secs+45.0f) * TWOP / 60.0f;secs_hand_start_x = (int)(centre_x + secshandt*cosf(secs_angle+(TWOP/2.0f)) + 0.5f);secs_hand_start_y = (int)(centre_y + secshandt*sinf(secs_angle+(TWOP/2.0f)) + 0.5f);secs_hand_end_x = (int)(centre_x + secshandl*cosf(secs_angle) + 0.5f);secs_hand_end_y = (int)(centre_y + secshandl*sinf(secs_angle) + 0.5f);secs_arrow_corner1_x = (int)(centre_x + (secshandl-secsarrowl)*cosf((secs_angle-secsarroww)) + 0.5f);secs_arrow_corner1_y = (int)(centre_y + (secshandl-secsarrowl)*sinf((secs_angle-secsarroww)) + 0.5f);secs_arrow_corner2_x = (int)(centre_x + (secshandl-secsarrowl)*cosf((secs_angle+secsarroww)) + 0.5f);secs_arrow_corner2_y = (int)(centre_y + (secshandl-secsarrowl)*sinf((secs_angle+secsarroww)) + 0.5f);// 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 ideal order would be: erase all three, then draw h-m-s, but this produces too much flicker.// This has been timed at ~45ms/35ms for 10.5MHz/21MHz SPI speed.// Erase old seconds hand ("last" values for secs are initialised to 0 so this writes a harmless BG pixel at 0,0)gsetcolorf(G_BLACK);drawLine (secs_hand_start_x_l,secs_hand_start_y_l,secs_hand_end_x_l,secs_hand_end_y_l);drawLine (secs_arrow_corner1_x_l,secs_arrow_corner1_y_l,secs_arrow_corner2_x_l,secs_arrow_corner2_y_l);drawLine (secs_arrow_corner1_x_l,secs_arrow_corner1_y_l,secs_hand_end_x_l,secs_hand_end_y_l);drawLine (secs_arrow_corner2_x_l,secs_arrow_corner2_y_l,secs_hand_end_x_l,secs_hand_end_y_l);// Erase old hrs hand and draw new oneif (hrs_angle_l<99.0f)// this prevents a spurious rectangle being drawn initially{gsetcolorf(G_BLACK);drawFilledRotatedRectangle(hrs_hand_x_l,hrs_hand_y_l,hrshandl,hrshandw,hrs_angle_l);}gsetcolorf(COLHRS);drawFilledRotatedRectangle(hrs_hand_x,hrs_hand_y,hrshandl,hrshandw,hrs_angle);// Erase old mins hand and draw new one// Curiously, filled is 11ms versus 12.5ms for unfilled - because the filled one// is done with hor. lines and DMA. Depends on hand angle and can be just 1.5 ms.if (mins_angle_l<99.0f)// this prevents a spurious rectangle being drawn initially{gsetcolorf(G_BLACK);drawFilledRotatedRectangle(mins_hand_x_l,mins_hand_y_l,minshandl,minshandw,mins_angle_l);}gsetcolorf(COLMINS);drawFilledRotatedRectangle(mins_hand_x,mins_hand_y,minshandl,minshandw,mins_angle);// Draw secs handgsetcolorf(COLSECS);drawLine (secs_hand_start_x,secs_hand_start_y,secs_hand_end_x,secs_hand_end_y);drawLine (secs_arrow_corner1_x,secs_arrow_corner1_y,secs_arrow_corner2_x,secs_arrow_corner2_y);drawLine (secs_arrow_corner1_x,secs_arrow_corner1_y,secs_hand_end_x,secs_hand_end_y);drawLine (secs_arrow_corner2_x,secs_arrow_corner2_y,secs_hand_end_x,secs_hand_end_y);// Refresh centre blob and clock face numbers; these get corrupted by the handsgsetcolorf (COLSECS);gcircle(centre_x, centre_y, MIDBLOB, 1U);gsetcolorf(COLFACE);clock_numbers(centre_x, centre_y, radius, COLFACE);// Update "last" hand datahrs_hand_x_l=hrs_hand_x;hrs_hand_y_l=hrs_hand_y;hrs_angle_l=hrs_angle;mins_hand_x_l=mins_hand_x;mins_hand_y_l=mins_hand_y;mins_angle_l=mins_angle;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;secs_arrow_corner1_x_l=secs_arrow_corner1_x;secs_arrow_corner1_y_l=secs_arrow_corner1_y;secs_arrow_corner2_x_l=secs_arrow_corner2_x;secs_arrow_corner2_y_l=secs_arrow_corner2_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);// Output GPS/NTP time source - a bit of a hack but we are just reading the RTC anywaygsetpos (timex+3,timey+25);extern bool g_block_ntp_client_rtc_update;if ( g_block_ntp_client_rtc_update )// this gets set if GPS time etc is valid{snprintf(timebuf,sizeof(timebuf),"GPS ");}else{snprintf(timebuf,sizeof(timebuf),"NTP ");}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...