STM 32F4 FPU registers and main() gotcha - Page 1
Forum section: electronics / general

author: peter-h
author_rank: Super Contributor
timestamp: July 22, 2024, 01:50:47 pm
content: I wonder why thishttp://www.efton.sk/STM32/gotcha/g203.htmldoes not cause loads of trouble all over the place.AIUI, it relates to C compilers treating the function main() differently when it comes to FPU stack operations. This is pretty weird, to be generating different code for a function, based on its name!Maybe because I am using GCC (v11) and this version of GCC just happens to work i.e. does not emit the extra stack pushes/pops.To work around this, the FPU enable code would need to go into the startup.s code i.e. before main() is entered. I am doing that but purely by accident; my startupxxx.s code called b_main() and that starts the FPU withCode:// ========== This was in SystemInit() ============#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */#endifThat code is commonly used Cube MX ("HAL") stuff which you find all over the internet...FreeRTOS seems to do it again when it starts up (inside main() this time):Code:/* Ensure the VFP is enabled - it should be anyway. */vPortEnableVFP();/* Lazy save always. */*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;and vPortEnableVFP() contains/* This is a naked function. */static void vPortEnableVFP( void ){__asm volatile("ldr.w r0, =0xE000ED88\n" /* The FPU enable bits are in the CPACR. */"ldr r1, \n""\n""orr r1, r1, #( 0xf << 20 )\n" /* Enable CP10 and CP11 coprocessors, then save back. */"str r1, \n""bx r14");}>Does this make sense to anyone? It seems to be working by accident, but it is a really weird thing as it is C compiler dependent, and to be sure you want to enable to FPU in the startup.s code.

author: wek
author_rank: Frequent Contributor
timestamp: July 22, 2024, 02:06:10 pm
content: QuoteBecause main() usually does not contain FP operations, so usually the compiler does not need to stack FP registers.Usually, main() consists only from a bunch of function calls. And, usually, those functions - especially if they handle FP - are located in separate files, thus are not subject to inlining.Even with moderate FP usage within a function there's probably no stacking. I don't remember the details of the API, but are many FP registers, so probably some of them the callee don't need to preserve.The problem happened to me because I don't write programs in the usual way, so quite a significant portion of my programs tend to be either explicitly, or inlined, in main() (I love spaghetti, and have and use a spaghetti-making machine).Quoteb_main() is a C-function, and as such, it is vulnerable to the same problem, potential FP registers stacking - and it does not happen because of the same reason, you most probably have no FP operation in that function.QuoteIf it's naked indeed (i.e. there is somewhere a prototype with __attribute__((naked))), then there's no C prologue thus no registers stacking and no vulnerability of the kind described. However, the functions leading to calling that vPortEnableVFP() *are* vulnerable - but, again, FreeRTOS functions most probably have no FP operations in them.JW

author: brucehoult
author_rank: Super Contributor
timestamp: July 22, 2024, 02:50:52 pm
content: Quote from: peter-h on July 22, 2024, 01:50:47 pmNothing STM or even Arm-specific in that.If you're going to use an FPU (or vector unit, on ISAs / cores that have them) then you need to enable them before running a function that uses them, where "using" could involve arithmetic or, yes, storing or loading FPU registers.You can perfectly well do that inmain(), just as long asmain()is running in privileged mode and doesn't itself use the FPU (etc) before initialising it -- including using it by saving registers in the prologue.This will apply to anything that has an initially-disabled functional unit: It's certainly true on RISC-V (both FPU and Vector units, if present and used, need to be changed from "Off" to "Initial" or "Clean" in themstatus.FSandmstasus.VSfields) and I'd imagine it is similar on x86, MIPS, PowerPC, ... too.

author: peter-h
author_rank: Super Contributor
timestamp: July 22, 2024, 03:13:36 pm
content: Thank you both.I am certainly not using floats before enabling the FPU (which is done in b_main() which then does a long jump to main() which never returns) and I would hope that if I was, it would comprehensively not workIt is probably by accident that main() does not use floats currently. I do have some printf() debug calls in there (printf() being mapped to come out on the SWV ITM debug port) which output longs but not floats. If they were floats, would that matter? I am confused.

author: dietert1
author_rank: Super Contributor
timestamp: July 22, 2024, 03:14:43 pm
content: Today something similar happened when i worked on a small Win32 test app (network client).There were no FPU operations in main(), but some in a thread started with CreateThread(). The app failed with "FPU not initialized" error. I solved the problem using _beginthreadex() instead and it worked. I learned that _beginthreadex() includes necessary CRT initializations.Regards, Dieter

author: peter-h
author_rank: Super Contributor
timestamp: July 22, 2024, 03:41:14 pm
content: That however is not the same thing. It is obvious that float ops with an uninitialised FPU are not going to work.

author: dietert1
author_rank: Super Contributor
timestamp: July 22, 2024, 04:03:13 pm
content: I reported an actual incident and how the FPU remained uninitialized. Not on a STM32, but on Win32. Probably one can do something similar on a STM32.

author: wek
author_rank: Frequent Contributor
timestamp: July 22, 2024, 04:19:24 pm
content: QuoteThe gotcha is in the fact, that even if you don't use floats in a function before enabling FPU, the compiler can do so.If there are FP operations in a function - anywhere in that function - and those operations are so extensive that the compiler can't perform them using only the "callee-modifiable (*)" FP-registers, it then stacks the "callee-saves" FP-registers, and does so in the function's prologue, ie. before any C line is executed. Normally, main() is no exception in this regard (there is/are command-line flag/s which can make it an exception, though; but that might be topic for a different discussion).Now if you jump to main() after FP being enable, no part of main() executes before enabling FP thus your main() is safe. If you enable FP in a different C function, that function is not safe; but again, you are not likely to do any FP operations (enabling FP itself does not count, as it does not use FP registers and FP instructions) in that function.And no worry: would you be caught by this gotcha, it's an immediate 100% fault (I'm not sure which one, but pending individual treatment they normally all escalate to HardFault anyway).(*)Quote from: ARM ABI ("Procedure Call Standard for the Arm® Architecture, chapter6 The Base Procedure Call Standard, subchapter6.1.2.1 VFP register usage conventionsJW

author: peter-h
author_rank: Super Contributor
timestamp: July 22, 2024, 04:32:01 pm
content: This FPU stuff is above my pay gradebut is the problem that thestackingof the FPU registers fails if the FPU is not enabled? Then I can understand it. Those registers are unlikely to be accessible if the FPU is not enabled (same with SPI etc etc).So you will be stacking garbage, and then when this is popped, the FPU is loaded with garbage. Or will the CPU get a permanent "wait state" from the non-enabled FPU?

author: harerod
author_rank: Frequent Contributor
timestamp: July 22, 2024, 04:54:18 pm
content: Quote from: peter-h on July 22, 2024, 04:32:01 pmWhich is implied in the footnotes of the article you linked in your initial post:http://www.efton.sk/STM32/gotcha/g203.htmlThe FPU seems to be no different from any other peripheral on the STM32 - enable before first access. This may require a combination of power and clock.I haven't used the STM32F4 FPU in such a long time, although I designed heaps of devices based on that MCU. During the first tests I wrote setup routines based on the datasheet.This may have been before CooCox and Atollic became available. Where have the last ten years gone?

author: dietert1
author_rank: Super Contributor
timestamp: July 22, 2024, 05:04:06 pm
content: Other people experienced hard faults with STM32 FPU while using FreeRTOS. Apparently initialization of FPU isn't 100 % automatic. In my Win32 case i have to include some FPU usage in main() in order to make it work in the thread.

author: wek
author_rank: Frequent Contributor
timestamp: July 22, 2024, 06:03:29 pm
content: The FPU is part of the processor core, so it's not like other peripherals. This is ARM's rules, not ST's.So, if you don't enable it, and attempt to access its registers, the processor throws UsageFault (ARM® v7-M Architecture Reference ManualB1.6.3 Pseudocode details of FP operation). If you don't have UsageFault enabled - which is the default - then it escalates to HardFault.JW

author: wek
author_rank: Frequent Contributor
timestamp: July 22, 2024, 06:09:57 pm
content: QuoteThat is not necessarily consequence of *late* enabling the FPU (i.e. accessing FP registers or executing FP instructions before enabling FPU), as discussed in this thread.For example, if FPU is enabled, upon interrupt/exception, the processor stacks (or reserves stack for, if lazy stacking is enabled, which is the default) half of the FPU registers, plus one status word (plus alignment if set so). That's extra 17-20 words, or up to extra 80 bytes, and that may be the difference between stack overflow or not.JW

author: dietert1
author_rank: Super Contributor
timestamp: July 22, 2024, 06:59:32 pm
content: No, the person had enough stack space and fixed the problem by "manually" setting the FPU control register.

author: wek
author_rank: Frequent Contributor
timestamp: July 22, 2024, 07:26:12 pm
content: Quote from: dietert1 on July 22, 2024, 06:59:32 pmInteresting.Can you please give some links?Thanks,JW

author: dietert1
author_rank: Super Contributor
timestamp: July 22, 2024, 08:04:59 pm
content: https://forums.freertos.org/t/cortex-m4-hard-fault-when-using-floating-point-unit/10180Now that i read it once more, it isn't that clear whether increasing stack helped or using the CPAR register or both.

author: peter-h
author_rank: Super Contributor
timestamp: July 22, 2024, 08:50:56 pm
content: Now I see why FreeRTOS (or at least my port of it, which was done by the guy who started off my Cube IDE project) enables the FPU when it starts up...

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 22, 2024, 09:53:40 pm
content: Quote from: brucehoult on July 22, 2024, 02:50:52 pmI still recommend doing that kind of initialization in the startup code rather than in the main() function, unless your firmware does enable/disable the FPU (or other similar functionalities) on the fly during normal execution.One reason is separation of concerns. Another is what (from what I get) the OP describes: if the FPU (or vector, etc) is not enabled before entering main, then indeed any FP operation done inside main could cause a problem because the compiler may reorder some FP instructions before you actually enable the FPU.Similarly for the vector extension on RISC-V, if you enable the extension in the compiler flags, the compiler may use vector instructions even just for copying data (tested!), so that would raise an exception if the vector unit hasn't been enabled prior.

author: peter-h
author_rank: Super Contributor
timestamp: July 22, 2024, 09:58:55 pm
content: QuoteIf I get you right, when you say main() you actually mean any C function which enables the FPU for the first time and which might perform a floating operation later on.What I actually have isstartupxx.s -> b_main.c (enables the FPU but definitely does not use it) -> main.c (does not currently use floats, and starts FreeRTOS which enables the FPU again)So this should be OK but I need to comment in b_main.c to never do any floats there.Funny thing is one could do doublesOr is there a specific compiler treatment of the function name "main()" ? Example: GCC requires main() to be a type int - it cannot be a void.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 22, 2024, 10:12:53 pm
content: Quote from: peter-h on July 22, 2024, 09:58:55 pmYes.Is there a reason you don't enable the FPU in the startup assembly code? That would certainly make your life easier.Now if you have b_main.c (which as I get it contains a C function that enables the FPU among other things) and the main() function in another source file (another compilation unit, so don't include one source file into the other), then you don't run the risk to have functions inlined into one another and thus risk the reordering of some instructions. So, you should be fine.In a somewhat related matter, in a preemptive multitasker that I wrote (that I won't call "RTOS" quite yet) - at this point for RISC-V targets - in order to optimize context saving depending on the task, each task has a set of options, among which whether the FPU and/or vector unit is used in said task. If not, the corresponding registers are not saved/restored when switching from/to this task. Tasks are independent functions that are called by address (like ISRs) by the scheduler, so there is no risk of unintended code being run within one task as long as we don't explicitely do it.

author: brucehoult
author_rank: Super Contributor
timestamp: July 23, 2024, 12:33:09 am
content: Quote from: SiliconWizard on July 22, 2024, 09:53:40 pmSure, of course that is preferable, if it is under your control.

author: westfw
author_rank: Super Contributor
timestamp: July 23, 2024, 05:34:32 am
content: QuoteI've seen that a lot of the project-creation vendor packages (STM Cube, Atmel Start, etc) are set up to call assorted startup code before main. Clocks, memory wait states, floating point, RAM vector tables, privilege stuff, etc.I've always found this a bit annoying, since it results in the code I most often want to look at (to figure out how to do myself) is "hidden" and can be hard to find. (OK, trace upwards from reset_handler to systemInit to boardInit to blahEvalInit, etc, all in some "library" or "system" directory not considered part of your project, sort of.)I guess this is an example of why that is a good idea...I'll also point out that gcc has an __attribute__((main)) to label the top-level function that is never expected to return, and is therefore OK to leave out the initial context saving...

author: peter-h
author_rank: Super Contributor
timestamp: July 23, 2024, 06:16:19 am
content: QuoteYes; exactly. The startupxxx.s file called a C function called SystemInit(). That, among a load of other stuff, contained the above C code for FPU startup. It resided somewhere in ST's "HAL" function library. I had no idea why they did it that way (well, there was a different version for every CPU). It may have been random "luck" or they knew about this issue. However I found that a lot of stuff which was in SystemInit() was later duplicated in main() so I rationalised it and put the content of SystemInit() in b_main.c which is my "32k boot block" (discussed in other threads on overlays etc; my main.c is linked to be at base+32k). The end of startupxxx.s jumps to b_main().I am also not that good with arm32 asm (having done Z80 asm for 30+ years) so would need to learn how to put the FPU startup in startupxxx.s. But FreeRTOS gives me the code, I think??Code:static void vPortEnableVFP( void ){__asm volatile("ldr.w r0, =0xE000ED88\n" /* The FPU enable bits are in the CPACR. */"ldr r1, \n""\n""orr r1, r1, #( 0xf << 20 )\n" /* Enable CP10 and CP11 coprocessors, then save back. */"str r1, \n""bx r14");}And yes I know the .s file can also call a C function (the way it used to call SystemInit()) but that is more risk because you now have a C function which could one day contain float ops, so you have to document that... which is exactly what I have done for b_main.c.Would this be the right code to put in the startup file?Code:ldr.w r0, =0xE000ED88 /* The FPU enable bits are in the CPACR. */ldr r1, orr r1, r1, #( 0xf << 20 ) /* Enable CP10 and CP11 coprocessors, then save back. */str r1, The C version wasCode:#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */EDIT: above asm seems to work fine. This is the end of my startup.s fileCode:/* Initialise the 8k stack at the top of the 128k RAM. This is 32F417 only *//* For the 32F437, extra 64k, the fill is done just before jumping to user code */ldrr2, = 0x2001e000 /* = _estack - _Stack_Size */bLoopFillStackFillStack:movs r3, 0x73737373/* fill with 's' */str r3, adds r2, r2, #4LoopFillStack:ldrr3, = _estack /* = 0x20020000 */cmpr2, r3bccFillStack/* Call the clock system initialization function. Moved to b_main.c *//* bl B_SystemInit *//* Start FPU, to avoid this problem *//* https://www.eevblog.com/forum/microcontrollers/stm-32f4-fpu-registers-and-main()-gotcha/ */ldr.w r0, =0xE000ED88/* The FPU enable bits are in the CPACR. */ldr r1, orr r1, r1, #( 0xf << 20 )/* Enable CP10 and CP11 coprocessors, then save back. */str r1, /* Call the application's entry point */bl B_mainHowever some online stuff suggests using DSB after the FPU is enabled.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 23, 2024, 10:28:01 pm
content: Quote from: peter-h on July 23, 2024, 06:16:19 amAh, true. Nothing prevents you from writing your own startup assembly file though.But if you want to keep that as is, then it should be fine. If the startup assembly calls separate external functions for initializing various things, it should not cause any issue. Obviously just don't use any FP operations in any called function before the FPU has been enabled, and you'll be fine. Keep your code as simple as possible in those external functions and make sure they don't themselves call other external functions that you may not have full control over.If all you do in those init functions, as you showed here, is access some registers in C via their C definitions, there shouldn't be anything odd happening behind your back even with full optimization on.To be fair, ARM assembly is not the easiest to learn. I'm a bit biased, but I think RISC-V assembly is much easier to learn.

author: westfw
author_rank: Super Contributor
timestamp: July 24, 2024, 12:06:28 am
content: QuoteI believe that there is nothing stopping you from writing the startup file in C, to.Although starting from scratch and throwing out the vendor-provided code seems ... not worth it.

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 12:57:28 am
content: Quote from: westfw on July 24, 2024, 12:06:28 amYou fundamentally can not do things such as making the Stack Pointer point at the memory area reserved for the stack, in C.You can't even do that in evil inline asm because by the time you get to that you've already entered the C function and it might have tried to set up a stack frame and pushed registers to whatever random place the stack pointer points to. Mucking about with the scaffolding of how C works under the hood, from in inline asm inside a C function, is just dodgy.With sufficient care and magic incantations (pragmas) you might just make it work on the C compiler you're using right now. But it's fundamentally evil. It's much easier just to write it properly, in assembly language.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 02:29:33 am
content: I agree with brucehoult above.With GCC and Clang, for x86 and x86-64/AMD64 one can do__attribute__((__naked__)) void _start(void) { asm volatile ("..."); }and for ARM one can do__attribute__((__naked__, __general_regs_only__)) void _start(void) { asm volatile ("..."); }but since even this syntax supports only basic (and not extended) inline assembly, it is better to write the startup –– or, more properly,C runtime–– code in a separate assembly source file.It is typically very short, as it really only needs to set up the stack and call a C function; even for GNU libc when running under a fully-featured OS it is only a few lines (x86-64,aarch64,ARM,RISC-V).

author: cv007
author_rank: Frequent Contributor
timestamp: July 24, 2024, 03:22:40 am
content: QuoteIn the Cortex-M world, didn't they intend to make everything writable in the higher level language? That's why the stack pointer is loaded by hardware I assume, and why interrupts act as called functions with hardware taking care of the caller saved registers. The things where C cannot go (mrs,msr,etc) require inline asm and are provided as C functions by the cmsis headers. Other mcu's are not the same of course, so a blanket statement doesn't work without qualification.Simple example for M0+https://godbolt.org/z/4aGsP1q68The reset function will have a stack already setup, so it can be a normal function. If one is concerned about a few unnecessary bytes pushed on the stack by the reset function prologue, the function can be marked naked but then you are responsible for making sure a local stack is not used.Once you get to something such as an M33 you may already be at a point where you have given up the notion that you want to have control of all code, and will let the manufacturer do whatever it wants in initializing the mcu (and beyond), which will include turning on the fpu.

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 04:11:55 am
content: Quote from: cv007 on July 24, 2024, 03:22:40 amRight. It's certainly an interesting experiment to have the CPU load SP from address 0 and the initial PC from address 4. Many previous CPUs have of course had reset / interrupt / NMI vectors at fixed addresses, but loading SP from there is I think unique. And also as you say having interrupts automatically push the CPU registers that normal C functions won't.Has it really saved a lot of time for programmers? I really don't know. You still have to figure out how to get the correct initial SP loaded into the correct place by the linker. Is that really so much easier than writing aMOVW/MOVTpair at the start of a very short assembly language reset function? Are they expecting unskilled programmers to be doing that kind of thing themselves, from scratch, rather than using someone else's pre-canned linker script andstart.s(and adjusting them as needed, which is equally easy either way)?

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 24, 2024, 05:17:19 am
content: Yeah. Doesn't make any sense to me that it would be of any benefit.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 05:22:01 am
content: Indeed, SP gets loaded at reset from the table at VTOR but you may need to manipulate it afterwards e.g. if making a provision for a 32F417 or 437 and the latter has the general stack 64k higher up. Well, you could abandon that data-bss-heap-stack layout and do something differentFreeRTOS uses various bits of asm to do things they can't do in C. Although curiously they use asm to enable the FPU which is not necessary.

author: dietert1
author_rank: Super Contributor
timestamp: July 24, 2024, 06:03:52 am
content: The advantage of open source is that you can see everything with your own eyes. The disadvantage is nobody pays for a cleanup. A lot of dust there.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 08:13:45 am
content: Quote from: cv007 on July 24, 2024, 03:22:40 amTechnically speaking, ARM only provided a way to auto-load the Stack Pointer (and Instruction Pointer) from Flash on Cortex-M's; it is still not possible to set the Stack Pointer from freestanding C code (without helpers written in assembly).This does mean that one can write firmware for Cortex-M without writing any assembly, for example usingCMSIS coreheaders (which do require quite a few things from the linker script used), because essentially the Cortex-M hardware itself does all the necessary setup for typical freestanding C runtime environments. (As an example, the Teensy 3.0linker scriptdoes this, withhardware/teensy/avr/cores/teensy3/mk20dx128.c:_VectorsFlash[]array causing the Stack Pointer to be initialized to the address of the_estacksymbol, and execution to start at theResetHandler()function written in C.)I don't have an opinion whether this is better than having acrt0.Sor similar assembly file setting up the stack and callingmain()or not, but it could be because the tools I've used for this stuff makesbothsufficiently simple for me to manage. I've found that it is much more work to document and remember the details than actually implement it, either way.

author: Tation
author_rank: Regular Contributor
timestamp: July 24, 2024, 09:31:30 am
content: In spite of all these, the current approach advocated by ARM is to write all in C/C++. The reset handler (that can be written in C) must configure the hardware thru functionSystemInit()(that can be written in C) and then call__main()—ARM provided, that initializes .bss, .data, etc., the runtime library; calls static C++ constructors and callsmain()—cmsis-core provides mechanisms to access SP, special registers, etc. from C/C++, not needing inline asm. Well, yes, these functions are a simple C wrapper to an inline__asm()containing the desired instruction.Only in some cases (AFAIK: if you need to provide your own __user_setup_stackheap() function) you must use assembly. The interface for this function does not follow the AAPCS (it must return a value in register R2, where the AAPCS states that values must be returned in R0, R1), so it must be mandatorily written in ASM or in C with__attribute__((naked)) void __user_setup_stackheap (void)containing inline ASM.In my case I have projects using this approach where the only inline ASM is, precisely, inside__user_setup_stackheap()—4__asm()lines containing just 3 instructions—. On such projects, C/C++ code does things such as:configure the HW (power tree, clock tree, waitstates, DMA, SysTick …)paint the stack before initializing the runtime library or calling C++ static constructors or enteringmain()relocate the vector table to RAM, fiddle with vectorsprovide a heap extension function that checks for collisions against TOSprovide a periodic check for stack-heap collisions (I know, this approach is not 100 % bullet proof, but…)check the reset cause and, when it is one of a selected set of errors, signal it to the user, not even enteringmain()provide MUTEXes using the MCU atomic instructionsprovide a C++ class whose objects, when constructed, disable IRQs until destroyed (usually when exiting its scope), when they restore the previous IRQ status (so protecting a critical section is done putting it inside brackets and typingCritical lock;at its beggining)In case you need to access some linker defined symbols (e.g.: limits of any memory region), they can also be accessed from C/C++.There's a benefit from writting all this in C/C++ vs. assembly: it is easier. You still need to know the gory details of the MCU, but do not need to remember that, say, in the Cortex-M0, registers R8 and up cannot be the target of an LDR, for example.Quote from: peter-h on July 23, 2024, 06:16:19 amOne of such gory details is that, the proper way, as stated in ARM docs, is to insert a DSB followed by an ISB after enabling/disabling the FPU.NOTE: I'm using the ARM compiler and runtime, not gcc.

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 10:32:04 am
content: Quote from: Tation on July 24, 2024, 09:31:30 amBlimey. So that SystemInit() function written in "C" has to be very careful not to refer to any global or static data.QuoteI don't know why init code would need more than 8 registers. Yeah, sure, if you want the same code to run on three different instruction sets (T16, T32, A32) then writing in asm will mean sticking to what T16 can do. But at least with unified syntax that T16 code will assemble for the other ISA flavours no problems.I've seen RISC-V init.S code that works on both 32 bit and 64 bit CPUs. With just a little care, even that is not difficult.QuoteISB is only going to be important if there are FP instructions immediately following enabling it. But sure, better to be safe.

author: Tation
author_rank: Regular Contributor
timestamp: July 24, 2024, 12:29:39 pm
content: Quote from: brucehoult on July 24, 2024, 10:32:04 amIndeed. That's the case of cmsis global variable SystemCoreClock. ARM warns about this:https://www.keil.com/pack/doc/CMSIS_Dev/Core/html/group__system__init__gr.html.QuoteIt was an example. Put instead any other thing easier in C than in assembly... multiply two int32_t to get an int64_t in Cortex-M0?

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 01:19:36 pm
content: Quote from: Tation on July 24, 2024, 12:29:39 pmLibraries are there to use them ....Code:mv r0,amv r2,basrs r1, r0, #31 // sign extend to 64 bitsasrs r3, r2, #31bl __aeabi_lmul??

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 01:35:16 pm
content: SystemCoreClock is a funny one. The CPU has no way of knowing that. You have to #define it somewhere. Or you could read back the PLL ratios but then you still need a #define for the xtal.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 25, 2024, 03:26:47 am
content: Quote from: Tation on July 24, 2024, 12:29:39 pmOne of my pet peeves with Cortex-M0!On ARMv7-M (Cortex-M0+, M3, M4, M7) it is just a singlesmullorumullinstruction, including on STM32F4 (Cortex-M4). On RISC-V RV32M and some other architectures, it is two instructions: a multiply-high or multiply-high-unsigned and a multiply instruction (mulhuandmul, ormulhandmulinstructions). This makes fixed-point and bignum implementations efficient. I use it and need this quite often.I almost posted a 200-line post about how to deal with it on ARMv6-M Cortex-M0, but then I remembered this thread was about STM32F4/Cortex-M4, and even my Teensy LC is Cortex-M0+, so it is not really worth delving into. Suffice it to say that I consider it very annoying that the Cortex-M0 ABI (via__aeaebi_lmul,(uint64_t)a*(uint64_t)bor(int64_t)a*(int64_t)b) only supports a 64×64-bit multiplication with a 64-bit result, and wastes cycles compared to unsigned 32×32-bit multiplication with 64-bit result written in C or assembly (except possibly on those silly Cortex-M0 cores having a 32-cycle multiplication operation, where doing it bitwise can yield even better results).

author: brucehoult
author_rank: Super Contributor
timestamp: July 25, 2024, 04:56:16 am
content: Quote from: Nominal Animal on July 25, 2024, 03:26:47 amDo they exist? I know it's an option, but I've never come across an actual example. I always figured the (fairly silly) single cycle 32x32 multiply was the reason CM0 was never seen above 48 MHz. Then the RP2040 appeared at 133 MHz. Oh .. it's got the 32 cycle multiply? Nope ... single cycle. How the ... ?Oh, 40nm. OK then. Apple's A4 was on 40nm and 45nm (some of each) at 800 and 1000 MHz, and Intel's Nehalem was 45nm doing 2.8 to 3.5 GHz, with 3 cycle integer multiply. So, yeah, single cycle multiply at 133 MHz is nbd at all on 40nm. It would be tough on 180nm.QuoteThat would have to be with early-out on an operand with rather few bits!ARMv7 can do multiply without early-out using an unrolled series of 32 things like ...Code:tst r2, #(1<<30)it neaddne r0, r0, r1, lsl #30Original A32 can of course do the same but without the "it ne". So 64 cycles on A32 and 96 cycles on T32.But CM0? I don't think you can do better than ...Code:lsls r3, r2, #1bpl 1flsls r3, r1, #30adds r0, r0, r31:... where at each step the shifts in the first and secondlslsadd to 31. So it's 3-4 cycles per bit, 96 to 128 in total, depending on which bits are set.And CM0 with early out? Can you even do better than this?Code:movs r0, #0movs r4, #11:movs r3, r4ands r3, r1rsbs r3, r3, #0ands r3, r2adds r0, r0, r3lsls r2, r2, #1lsrs r1, r1, #1bne 1bThat's 8 instructions and 9 cycles in the loop. Maybe atstandbeqis better than theands/rsbs/andswith the CM0's low taken branch penalty.But anyway, by the time you get tor1somewhere between 512 and 4095 (depending on the number of set bits) you're better off just doing the unrolled version. i.e. on 99.9999% of random 32 bit operands.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 25, 2024, 05:22:17 am
content: Quote from: brucehoult on July 25, 2024, 04:56:16 amNeither have I, that's why the "can yield". Perhaps in an FPGA CM0 soft core? (Good thing we have RISC-V now!)Do note that this was about 32×32=64-bit multiplication, which for ARMv6-M CM0 using naïve__aeabi_lmulinvolves 16mulsinstructions (each for one 16×16-bit multiplication using 32-bit operands with a 32-bit result). In comparison, with the shift approach, the accumulator and adder are extended to 64 bits, but the number of iterations remains 32, with something like 8 cycles per iterations: 32×8 = 256 cycles (or less). Which is less than 16×32 = 512 cycles (plus the 64-bit shifts by 16 bits, and additions), for the worst-case slow-multiply CM0.

author: brucehoult
author_rank: Super Contributor
timestamp: July 25, 2024, 05:42:51 am
content: Quote from: Nominal Animal on July 25, 2024, 05:22:17 amRight. Extending to 64 bits is just one extra shift (right instead of left) and one extraadc.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 25, 2024, 06:05:58 am
content: The M0 is not supposed to be a performance beast and ARM defined their core series with marketing in mind.Yes RISC-V does have 32x32 -> 64 multiply, as long as you support the M extension, even on just a RV32E core. But RISC-V was designed for modularity and usefulness, not for marketing.And yes the multiplier is configurable. I have rarely seen, if ever, a Cortex-M0 MCU with a 32-cycle multiplier. But, out of fairness, if you compare a 32x32 -> 64 multiplier that requires 32 cycles (+ at least one cycle to get the upper 32 bits) to a 32x32 -> 32, single cycle multiplier, the latter will win in terms of performance for achieving a 32x32 -> 64 multiplication.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 25, 2024, 06:42:02 am
content: Quote from: SiliconWizard on July 25, 2024, 06:05:58 amOf course. The point is that 32×32→64-bit multiplication can be implemented in ARMv6-M Cortex-M0 (with only a 32×32→32-bit multiplication) quite efficiently viaCode:#include <stdint.h>uint64_t mul_u32x32_64(const uint32_t a, const uint32_t b){const uint32_t m = 0xFFFF;const uint32_t ah = a >> 16, al = a & m,bh = b >> 16, bl = b & m;return (uint64_t)(al * bl)+ ((uint64_t)(al * bh) << 16)+ ((uint64_t)(ah * bl) << 16)+ ((uint64_t)(ah * bh) << 32);}which can be implemented in unified assembly for Cortex-M0 (ARMv6-M) viaCode:mul_u32x32_64:push {r4, r5, r6, lr}mov r2, r1lsrs r1, r0, #16mov r3, r2muls r3, r1, r3lsrs r4, r2, #16mov r5, r0muls r5, r4, r5movs r6, #0adds r3, r5, r3adcs r6, r6lsrs r5, r3, #16lsls r6, r6, #16adds r5, r6, r5lsls r3, r3, #16muls r1, r4, r1muls r0, r2, r0adds r0, r0, r3adcs r1, r5pop {r4, r5, r6, pc}which ought to beat__aeabi_lmulthat you get via(uint64_t)a*(uint64_t)b, because that implements 64×64=64-bit multiplication and written in the same way would require 16mulsmultiplication instructions, 1 or 32 cycles each, depending on the core implementation.There are several different ways of implementing the signed version, with the simplest one using the magnitudes of the terms and the unsigned multiplication, and negating the result if exactly one of the terms was negative:Code:int64_t mul_i32x32_64(const int32_t a, const int32_t b){const uint32_t nr = (a < 0) ^ (b < 0),ua = (a < 0) ? -a : a,ub = (b < 0) ? -b : b;return nr ? -mul_u32x32_64(ua, ub): mul_u32x32_64(ua, ub);}Other options include reimplementing the unsigned multiplication for only the 31 low bits, and do the MSB products separately (since for two's complement 32-bit signed integers, the MSB or sign bit has logical value -2³¹) using unsigned math but subtraction.Because of this and similar effects on integer operations, I do prefer to "default" to using unsigned integer types, and only use signed integer types whenever I know I might need to support negative values. Which is basically the opposite of what the C standards imply/suggest.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 25, 2024, 07:01:58 am
content: Quote from: Nominal Animal on July 25, 2024, 06:42:02 amDitto, and it's not just for efficiency. Obviously with unsigned integers, you get twice the maximum magnitude for positive integers, and as a general rule, I use the types that match the use case as closely as possible. I like strong typing, which may explain it. So to me, using signed integers for variables that are meant to only ever be positive or zero is much too sloppy for my taste, and a very bad idea in general.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 25, 2024, 09:37:12 am
content: Quote from: SiliconWizard on July 25, 2024, 07:01:58 amThesize_ttype is a perfect example of this, for array indexing, in-memory object sizes and such.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 25, 2024, 10:35:26 am
content: Quote from: SiliconWizard on July 25, 2024, 07:01:58 amAdded benefit is easier bounds checking:Code:int func(int arg /*always >=0 by convention*/){if(arg < 0 /*said assumption needs to be checked*/ || arg > 42)return ERROR;}int func(unsigned int arg){if(arg > 42)return ERROR;}

author: brucehoult
author_rank: Super Contributor
timestamp: July 25, 2024, 12:06:01 pm
content: Quote from: Siwastaja on July 25, 2024, 10:35:26 amCompilers automatically turn that into a single unsigned comparison anyway:https://godbolt.org/z/j6vjTbhqKCode:func(int*, int):li a5,42bgtu a1,a5,.L6slli a1,a1,2add a0,a0,a1lw a0,0(a0)ret.L6:addi sp,sp,-16sw ra,12(sp)call error()Or, you can save typing while working with less good compilers by just castingargtounsignedin the actual bounds check.Code:if((unsigned)arg > 42)error();It's the same thing either way.You're never going to see a legitimate array index with the hi bit set anyway -- all the more so if the element size is greater than 1.Using an unsigned type as a counter / loop index can make it tricky getting the termination condition right when counting down, as you usually want to (for both simple code and efficiency) update the counter before the exit test, not after, so you want to stop not when the counter is zero before updating it, but when it is negative after updating it.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 25, 2024, 09:45:15 pm
content: Quote from: Nominal Animal on July 25, 2024, 09:37:12 amYes, and I also recommend using it for this in general, unless you have good reasons to restrict the width of your indices and sizes, in which case you should use a stdint (for portability reasons, otherwise you'd just be shooting in the dark).size_t on most platforms uses the native register width (32-bit on 32-bit CPUs, 64-bit on 64-bit CPUs, etc) and is thus usually also more efficient.Yes: try using a 32-bit index on a 64-bit CPU (for instance RISC-V) and see how that can degrade performance in some cases, as the underlying register will often need to be masked by the compiler (even when it's not strictly needed, but the compiler rarely knows that).

author: brucehoult
author_rank: Super Contributor
timestamp: July 26, 2024, 12:45:27 am
content: Quote from: SiliconWizard on July 25, 2024, 09:45:15 pmUsing int/int32_t on RV64 has always been fine as 32 bit results are sign-extended to 64 bits by all instructions. Casting to long/int64_t is a NOP.Using unsigned/uint32_t as an array index on RV64 used to be inefficient, as it needed to be explicitly zero-extended before taking part in 64 bit operations.This was especially galling in Coremark, which uses such 32 bit unsigned values as array indexes ("for efficiency"). Initially RISC-V people typedef them to more sensible types, until Arm-controlled EEMBC quite forcefully insisted that the Coremark source code be used unmodified, resulting in a drop in published RISC-V Coremark numbers.This of course would never happen with real open source software. You can do what you want locally, and almost certainly a patch with #ifdef would be accepted upstream.But that's all in the past with theZbaextension introducingadd.uw,sh1add.uw,sh2add.uw,sh3add.uw, andslli.uwin whichrs1is zero-extended from 32 to 64 bits before being shifted and/or added tors2.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 26, 2024, 06:37:48 am
content: Quote from: brucehoult on July 25, 2024, 12:06:01 pmForforloops, yes; but that's why I prefer the idiomsize_t i =number of elements;while (i-->0) {operate on element at indexi}instead. I find thei-->0expression easy to read and interpret and funny here.In many cases, however, when scanning through an array, the forward-scanning idiomtype*const end =array+number of elements;for (type*ptr =array; ptr < end; ptr++) {operate on element at*ptr;}can generate better code, although this does depend on the compiler and options used. Note that one can include the definition ofendin theforloop start condition, although it does make the line quite long.The corresponding backward-scanning idiomfor (type*ptr =array+number of elements- 1; ptr >=array; ptr--) {operate on element at*ptr;}suffers from a variant of the issue brucehoult mentoned; it requires the address ofarrayto be nonzero.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 26, 2024, 08:39:38 am
content: Yes. Alternatively, if you want to use a for loop, you can offset your index by 1. I guarantee you that it won't make a single difference in terms of performance on most targets. (Interestingly, for GCC on x86_64, the descending version looks faster than the ascending version - at least it has 1 fewer instruction in the compiled loop body - which admittedly may not necessarily mean that it's faster.)Like:Code:double Foo2(size_t n, double A){double S = 0.0;for (size_t i = n; i > 0; i--)S += A[i - 1];return S;}Of course you may find this version potentially more confusing to read, with this offset. Pick your favorite.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 26, 2024, 08:53:24 am
content: The loop idiom that I miss in C, especially for microcontrollers, isFORALLwith Fortran 95/2003/later semantics. Essentially, it works like aforloop, except that the compiler can choose the iteration order freely. (Thus, the parameter block should only define exactly one scalar or vector iterator variable, and the range of values and step sizes in each dimension.)

author: bson
author_rank: Unknown
timestamp: July 29, 2024, 08:24:41 pm
content: You could perhaps put your enable code in a function and give it the constructor attribute. This way it will run with the other static initializers from __main (in libgcc).Code:__attribute__((constructor))static void enable_fpu() {__asm volatile("ldr.w r0, =0xE000ED88\n" /* The FPU enable bits are in the CPACR. */"ldr r1, \n""\n""orr r1, r1, #( 0xf << 20 )\n" /* Enable CP10 and CP11 coprocessors, then save back. */"str r1, ");}https://gcc.gnu.org/onlinedocs/gccint/Initialization.htmlEdit: oh, and add r0 and r1 to the asm clobber list.

author: wek
author_rank: Frequent Contributor
timestamp: July 29, 2024, 10:14:47 pm
content: Quote from: bson on July 29, 2024, 08:24:41 pmUnless you've removed __libc_init_array() from the startup code, forsome of the good reasons... :-)JW

author: peter-h
author_rank: Super Contributor
timestamp: July 31, 2024, 12:30:33 pm
content: I was reading your article again,wek, and one could spend a week down that rabbit holeI just put this in the linkfileCode:/* this is for __libc_init_array() */.preinit_array :{PROVIDE_HIDDEN (__preinit_array_start = .);KEEP (*(.preinit_array*))PROVIDE_HIDDEN (__preinit_array_end = .);} >FLASH_APP.init_array :{PROVIDE_HIDDEN (__init_array_start = .);KEEP (*(SORT(.init_array.*)))KEEP (*(.init_array*))PROVIDE_HIDDEN (__init_array_end = .);} >FLASH_APP.fini_array :{PROVIDE_HIDDEN (__fini_array_start = .);KEEP (*(.fini_array*))KEEP (*(SORT(.fini_array.*)))PROVIDE_HIDDEN (__fini_array_end = .);} >FLASH_APPwith some comments e.g. malloc etc may not work otherwise...

author: bson
author_rank: Unknown
timestamp: August 02, 2024, 11:20:51 pm
content: Quote from: wek on July 29, 2024, 10:14:47 pmI wouldn't... unless you implementeverythingfrom scratch including, as peter-h mentioned, malloc. While somewhat rare, it IS a used feature in many libraries and it's easy to trip over a deeply buried constructor function somewhere.I also use C++, and while I don't rely on static initializers for non-POD types, it could still happen with composite POD types, especially with bloated debug builds with minimal compiler optimizations and lacking aggressive linker gc. I'd rather address such things as optimizations at some point, than crashes during init. Mostly, just copying .rodata to .data and zeroing the rest of memory should be enough though, but it's soo easy to trip over.However, I actually agree it's better to put it in a separate hardware _init function called separately prior to _main. The reason is to get a predictable initialization order. If the code is built with hard fp, then other constructor functions could very easily try to save/restore FP registers, although it is admittedly a bit of a stretch. Still, better to stay on the right side of the odds...

author: wek
author_rank: Frequent Contributor
timestamp: August 05, 2024, 08:53:56 am
content: Quote from: bson on August 02, 2024, 11:20:51 pmDefine *everything*Given __libc_init_array() and related is poorly defined (see rabbit hole), that's not a simple thing to do.But that's the overarching theme here. Would the toolmakers' attitude be "here's a description of each step of startup process, together with explanation and reasoning behind the individual steps" instead of "we, the almighty and powerful, have performed all the magic needed to make it work, don't you dare to question it", we wouldn't have this discussion at all.JW

author: westfw
author_rank: Super Contributor
timestamp: August 05, 2024, 08:04:47 pm
content: Quoteavr-gcc's documentation of the nine (!) different initialization steps that happen before main is pretty nice.https://www.nongnu.org/avr-libc/user-manual/mem_sections.html(and another 9 sections that happen after exit(), though most are unused.)

author: peter-h
author_rank: Super Contributor
timestamp: August 06, 2024, 07:59:22 am
content: The problem with __libc_init_array() etc is that we don't actually know the definitive sources for the newlib libc.a. There was a thread here a couple of years ago on this and the likely source code was located, and I confirmed it by looking at some functions, and disassembly, but nobody is 100% sure, and AFAIK ST are not telling. And then the actual library changes according to your float options selector in the Cube IDE config - something I spent months working out. ST built dozens of versions of libc.a and the one you get is based on that selector. The thing has loads of empty stubs for calling mutexes (because some of the code is not thread-safe) and I had to implement those, but the library was not built with weak symbols for these! Anyway I posted it all here; in the end I had to use objcopy to weaken the whole libc.a so it could be done properly. A massive amount of time wasted and I don't ever want to got into that again.

author: wek
author_rank: Frequent Contributor
timestamp: August 06, 2024, 09:08:02 am
content: Quote from: westfw on August 05, 2024, 08:04:47 pmIndeed it is. But that's the exception, result of enthusiasm and also a certain amount of luck.I believe you can name all major contributors to the avr-gcc/avr-libc project. This and the state of documentation are no coincidence: they put their names behind their work, and they were willing to publicly discuss it. That's not the norm in the world of "professionally" made tools (and make no mistake, gcc as such is *not* an enthusiasts-driven project).JW

author: Nominal Animal
author_rank: Super Contributor
timestamp: August 06, 2024, 10:01:57 am
content: Give me a specific version of newlibc and a gcc on a specific architecture compatible with those, and I can describe the entire C runtime and initialization scheme. Especially so if you use an officialARM GNU Toolchain. I've found that most work is mapping all the debugging options and their effects; for a single version, single set of compilation options, single target architecture, it isn't too painful..I fully understand wek's issues when trying to do that in a wholly version-agnostic way, because there is so muchconfigurable stuff in the target machine definitioncompiled to gcc, and all of it interacts with newlib – just see the macros controllinggcc/libgcc/crtstuff.c(which ends up compiled to most of the C runtime). I've considered sketching it out using Graphviz as a directed graph starting from the hardware reset vector, but it is alotof work; way too much for a generic directed graph!For a specific version of the ARM GNU toolchain on a specific architecture (say, ARMv7e-M for Cortex-M4 and -M7), it's not too bad. Thetoolchain release noteseven include the build instructions (usingLinaro ABEmanifest.txt files).What simply fucks all that up, is vendors like STMicroelectronics who package their own toolchains without documenting how to rebuild that toolchain (to distributed binaries differing only in timestamps and such). Users are told "it's gcc and newlib", but that's less precise than saying "Euler angles (there being only 12 different incompatible ways of defining Euler angles; or 24, if you consider intrinsic and extrinsic rotations separately).

author: langwadt
author_rank: Super Contributor
timestamp: August 06, 2024, 12:53:52 pm
content: Quote from: Nominal Animal on August 06, 2024, 10:01:57 amhttps://github.com/STMicroelectronics/gnu-tools-for-stm32?

author: Nominal Animal
author_rank: Super Contributor
timestamp: August 06, 2024, 02:03:45 pm
content: Quote from: langwadt on August 06, 2024, 12:53:52 pmHaving build scripts is nice, but not nearly as nice as a description of how to reproduce the distributed binaries (ignoring irrelevant differences like timestamps, function order, and so on).

author: peter-h
author_rank: Super Contributor
timestamp: September 07, 2024, 07:06:45 pm
content: Getting back to this FPU topic, and referring to this posthttps://www.eevblog.com/forum/microcontrollers/for-more-memory-check-this-out/msg5633683/#msg5633683I am a bit confused. That implies that you need to a float operation before interrupts are enabled.I am enabling the FPU in startup.sCode:/* Start FPU, to avoid this problem *//* https://www.eevblog.com/forum/microcontrollers/stm-32f4-fpu-registers-and-main()-gotcha/ */ldr.w r0, =0xE000ED88/* The FPU enable bits are in the CPACR. */ldr r1, orr r1, r1, #( 0xf << 20 )/* Enable CP10 and CP11 coprocessors, then save back. */str r1, but I am not doing a float operation before FreeRTOS is started near the end of main(). After that various RTOS tasks do float ops, but I wonder if I should do one early in main(). There is currently a printf there which goes to the SWV ITM console output (which is harmless if a debugger is not connected) and I could output a float via that, which would not get optimised-out.Or perhaps the other way is to enable the lazy stacking bit (somewhere??) right after the FPU is enabled. I read that FR handles the FPU issues internally when switching tasks and maybe it sets that bit.

author: brucehoult
author_rank: Super Contributor
timestamp: September 08, 2024, 01:37:14 am
content: Quote from: peter-h on September 07, 2024, 07:06:45 pmHow so?

author: ejeffrey
author_rank: Super Contributor
timestamp: September 08, 2024, 02:04:55 am
content: Quote from: peter-h on September 07, 2024, 07:06:45 pmWhat would that accomplish? As long as you have enabled the FPU before using it, isn't that all that matters?

author: peter-h
author_rank: Super Contributor
timestamp: September 08, 2024, 05:48:15 am
content: I would agree, but Siwastaja wrote:ARM Cortex-M implements lazy stacking and stacks the float registers when the first float operation is encountered. Therefore you can simply use floats everywhere your code, including most interrupts, and choose not to use them in those most timing-critical interrupts.The implication seems to be that you need to perform a float operation before interrupts are enabled.

author: brucehoult
author_rank: Super Contributor
timestamp: September 08, 2024, 06:04:19 am
content: Quote from: peter-h on September 08, 2024, 05:48:15 amThen you completely misunderstood what he wrote.ARM Cortex-M implements lazy stacking and stacks the float registers IN AN INTERRUPT HANDLER ONLY IF AND when the first float operation IN THAT INTERRUPT HANDLER is encountered, AND RESTORES THE FLOAT REGISTERS ON RETURN FROM THE INTERRUPT IF AND ONLY IF THEY WERE SAVED IN THAT INTERRUPT HANDLER.

author: gf
author_rank: Super Contributor
timestamp: September 08, 2024, 10:09:10 am
content: Quote from: brucehoult on September 08, 2024, 06:04:19 amAnd what happens if the interrupt handler function does not do any float operations itself, but calls functions which do float operations?

author: Siwastaja
author_rank: Super Contributor
timestamp: September 08, 2024, 11:19:22 am
content: Quote from: gf on September 08, 2024, 10:09:10 amIt just works, the Cortex-M CPU core is aware that it's executing in ISR mode, and the first floating point operation in ISR mode triggers stacking of float registers, and when the original handler function returns and thus ISR mode exits, then unstacking happens.The whole Cortex-M is designed such that handlers can be normal C functions and no special considerations of any kind are needed, this extends to being able to call arbitrarily functions from such functions. This only causes problems to people who have historical experience and assume things must be difficult when they aren't.Now if you do stuff like manipulate or analyze stack or registers in assembly (e.g. for debug logging),thenyou need to be aware of all these details.

author: peter-h
author_rank: Super Contributor
timestamp: September 08, 2024, 11:28:11 am
content: Quotehttps://www.eevblog.com/forum/microcontrollers/how-does-st-32f4-know-when-an-isr-has-finished/msg4164745/#msg4164745On an interrupt, the arm32 pushes magic numbers (which have high bits set and cannot be valid addresses) onto the stack and when it pops these, it knows the ISR has ended, regardless of how much stuff was in between. That is how it avoids the need for the classic "RETI" and how one can write ISRs in straight C without any keyword or attribute like "interrupt".On the earlier "debate", Siwastaja's words "when the first float operation is encountered" should have been "when the first float operation is encountered in the ISR"

author: wek
author_rank: Frequent Contributor
timestamp: September 08, 2024, 11:55:24 am
content: The painful detailshere, start with chapter 2.JW

author: gf
author_rank: Super Contributor
timestamp: September 08, 2024, 11:57:40 am
content: Quote from: wek on September 08, 2024, 11:55:24 amSeems to be pretty tricky

author: Siwastaja
author_rank: Super Contributor
timestamp: September 08, 2024, 12:30:15 pm
content: Quote from: peter-h on September 08, 2024, 11:28:11 amThis is similar to customer coming into a supermarket and to the fruit section, then asking "where are the apples". When given answer "on the left", customer runs out of the store and tries to look left out of the door, shouting "there are no apples here, you should have saidleft on where you currently stand in the fruit section".I thought it is completely obvious that in context of discussing ISRs, stacking the registers must happen after the interrupt is triggered. How could you even imagine somehow stacking the CPU state before the interrupt happens? If you know what stacking means and why it is done, that is. And if you don't, then Cortex-M really makes life easy for you, see below.You are a genius in making simple things sound complex and misunderstand anything.Quote from: gf on September 08, 2024, 11:57:40 amClassic issue.Mostpeople find itsimpleif you solve their problems and give them something whichjust works, even if it is complex behind the curtains. Then again, there is always someone who wants to understand the internals completely, will disagree with some details, and want to micromanage it. They will complain.I have programmed these CPUs for over a decade and never needed to think about these details at all. I have only read about them out of pure interest and learned about it on this very forum.

author: brucehoult
author_rank: Super Contributor
timestamp: September 08, 2024, 01:48:14 pm
content: Quote from: Siwastaja on September 08, 2024, 12:30:15 pmI'd have thought so too!Cortex-M really does an extraordinary amount of things in hardware to make using them easy for unskilled programmers. Far more than any other Arm CPU line -- and even on the (relatively) tiny Cortex-M0.Really the diametric opposite to RISC-V, which does the least amount in hardware that can possibly work efficiently, leaving the programmer (or your standard BSP library) to implement a lot of features.QuoteI reckon Simon has that prize.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 08, 2024, 02:44:50 pm
content: Quote from: gf on September 08, 2024, 11:57:40 amHow so? With lazy stacking, the Cortex-M core will reserve space for the floating-point registers in the stack, but not fill the values in until a floating-point instruction is first used. The core itself automatically tracks all this using registers named in the appnote, so there is nothing the user/programmer needs to do except enable this if desired. It is very clearly described, in detail, with simple diagrams included, in the linked-to appnote.When implementing an OS, the kernel must account for the unfilled floating-point registers in the stack until a floating-point instruction is used if lazy stacking is enabled, but that is it.To me, this is quite straightforward and easily explained and modeled. If anything, the only tricky bit is that when implementing an OS, one must understand the float registers values on stack are undefined until a float instruction is used; but among the tricks needed for OS development and userspace-kernel interfaces, this is insignificant.Quote from: brucehoult on September 08, 2024, 01:48:14 pmI've always associated AVRs as the little siblings of the Cortex-M series, as they both seem to be focused on this: easy to program, easy to generate code for.Although I've mostly used Cortex-M cores on 32-bit MCUs, I've always considered itnicehow the core handles so many details for me.Quote from: brucehoult on September 08, 2024, 01:48:14 pmYes. The RISC-V ISA seems easy to generate code for, although compilers haven't gotten the code optimization to the same level as on other ISAs (as observed at e.g.Compiler Explorer– edit: just a link to it, I don't have examples of this). It is the underlying "paradigm" that differs. Cortex-M is something like "make it easy", and RISC-V is something like "reduce the unnecessary complexity".While they have definitely different approaches, I like them both a lot: wider range of tools in my toolbox.

author: peter-h
author_rank: Super Contributor
timestamp: September 08, 2024, 02:54:37 pm
content: QuoteDepends on how much experience someone has. Some of us here have been programming micros since the 1970s when they first came out. So the way arm32 implements ISRs is not at all obvious.No need to treat others like idiots, notwithstanding that this is common on the internet.

author: brucehoult
author_rank: Super Contributor
timestamp: September 08, 2024, 03:04:40 pm
content: Quote from: Nominal Animal on September 08, 2024, 02:44:50 pmNot sure what you were trying to demonstrate there, as it brought up a default simplesquare()function with x86 code generation. Switching it to RISC-V, it couldn't possibly be improved upon...https://www.godbolt.org/z/4bsWeMY1d

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 08, 2024, 04:04:04 pm
content: Quote from: brucehoult on September 08, 2024, 03:04:40 pmNo specific code, just a link to the editor. You see, I do not have a good example.Perhaps I should have linked to saythis one, blending two RGB555 colors using a phase value between 0 (first color) and 32 (second color), inclusive. See how much the code generated by GCC for rv32gc differs between-O2and-Os, latter being 20% longer than the former?Using-O2 -Wall -march=armv7-m -mtune=cortex-m4 -mthumbon various compilers to compile to Cortex-M4, obviously the code is slightly denser –– that's what you should get with a complex instruction set, after all ––, but changing optimization to-Osdoes not change the code much, if at all.Granted, I haven't written a hand-optimized version of this in rv32gc assembly to say howcloseto optimal the RISC-V code generated by GCC (or clang) is, and am assuming that the large difference between-O2and-Osis an indicator that some things just aren't optimized well yet. (Code generated byclangseems to be quite stable across versions and between-O2and-Os, however.)

author: brucehoult
author_rank: Super Contributor
timestamp: September 08, 2024, 06:20:03 pm
content: Quote from: Nominal Animal on September 08, 2024, 04:04:04 pmHmm. I see 114 bytes vs 106, which is 7.5% more for-Os. Not a great thing, for sure, but it is after all just a heuristic not a guarantee. It seems it trips up on code generation strategies for the min/max adjustment. Also 7.5% is significantly smaller than the 20% you state.Turning onZbb(which has min/max directly) it changes to 94 bytes for-Osvs 102 bytes for-O2.Also, I'm confused why33 - phase, not 32?In fact I think your function is simply buggy.Here is a reference implementation, which I think is very probably correct:Code:uint_fast16_t blend555_ref(uint_fast16_t r5g5b5_0, uint_fast16_t r5g5b5_1, int_fast8_t phase) {if (phase < 0)phase = 0;if (phase > 32)phase = 32;const uint32_t src_r = (r5g5b5_0>>10) & 0x1f;const uint32_t src_g = (r5g5b5_0>>5) & 0x1f;const uint32_t src_b = (r5g5b5_0) & 0x1f;const uint32_t dst_r = (r5g5b5_1>>10) & 0x1f;const uint32_t dst_g = (r5g5b5_1>>5) & 0x1f;const uint32_t dst_b = (r5g5b5_1) & 0x1f;const uint32_t sum_r = (((uint32_t)phase * dst_r + (uint32_t)(32 - phase) * src_r) >> 5) & 0x1f;const uint32_t sum_g = (((uint32_t)phase * dst_g + (uint32_t)(32 - phase) * src_g) >> 5) & 0x1f;const uint32_t sum_b = (((uint32_t)phase * dst_b + (uint32_t)(32 - phase) * src_b) >> 5) & 0x1f;return (sum_r<<10) | (sum_g<<5) | sum_b;}Your function (with33-phasechanged to32-phase) produces different results in 35423187968 / 35433480192 test cases.Here is an alternative function which produces the same results as the reference function in all test cases:Code:uint_fast16_t blend555_2(uint_fast16_t r5g5b5_0, uint_fast16_t r5g5b5_1, int_fast8_t phase) {if (phase < 0)phase = 0;if (phase > 32)phase = 32;const uint32_t rb_mask = 0x7c1f;const uint32_t g_mask = 0x03e0;const uint32_t src1 = r5g5b5_0 & rb_mask;const uint32_t src2 = r5g5b5_0 & g_mask;const uint32_t dst1 = r5g5b5_1 & rb_mask;const uint32_t dst2 = r5g5b5_1 & g_mask;const uint32_t sum1 = (uint32_t)phase * dst1 + (uint32_t)(32 - phase) * src1;const uint32_t sum2 = (uint32_t)phase * dst2 + (uint32_t)(32 - phase) * src2;return ((sum1>>5) & rb_mask) | ((sum2>>5) & g_mask);}My function produces 74 bytes of code withZbbregardless of-O2or-Os, or withoutZbb92 bytes with-Osand 84 bytes with-O2.So yeah,-Osis a bit sad in the face of that min/max argument pinning without min/max instructions.

author: brucehoult
author_rank: Super Contributor
timestamp: September 08, 2024, 07:19:27 pm
content: Ahhh ... yourreturnexpression should have been ...Code:return ((sum >> 5) & 0x001F)| ((sum >> 10) & 0x03E0)| ((sum >> 15) & 0x7C00);... not ...Code:return ((sum >> 5) & 0x001F)| ((sum >> 15) & 0x03E0)| ((sum >> 25) & 0x7C00);Production code?

author: peter-h
author_rank: Super Contributor
timestamp: September 08, 2024, 07:46:40 pm
content: How does this relate to FPU?

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 08, 2024, 09:09:04 pm
content: Quote from: brucehoult on September 08, 2024, 07:19:27 pmNo, just wrote offhand and failed. I tried to think of a good enough test case without digging through my archives. Production code uses 8 or 10 bits per component and 64-bit arithmetic (or two pairs of 32-bit values). Correct code would beCode:uint_fast16_t blend555(uint_fast16_t r5g5b5_0, uint_fast16_t r5g5b5_1, int_fast8_t phase){const uint_fast32_t rgb0 = (r5g5b5_0 & 0x001F)| ((r5g5b5_0 & 0x03E0) << 5)| ((r5g5b5_0 & 0x7C00) << 10);const uint_fast32_t rgb1 = (r5g5b5_1 & 0x001F)| ((r5g5b5_1 & 0x03E0) << 5)| ((r5g5b5_1 & 0x7C00) << 10);if (phase < 0)phase = 0;if (phase > 32)phase = 32;const uint_fast32_t rgb = (32 - phase) * rgb0 + phase * rgb1 // Blending+ UINT32_C(0x01004010); // Roundingreturn ((rgb >> 5) & 0x001F)| ((rgb >> 10) & 0x03E0)| ((rgb >> 15) & 0x7C00);}as you surmised. (This one I actually verified, you see. It produces the same results as using floating-point arithmetic for red, green, and blue separately, then rounding the results in the normal fashion.)The 33 is actually a funky detail: \$(2^n + 1)(2^n - 1) = 2^{2 n} - 1\$. It means that scaling some value from \$n\$ bits to \$2n\$ bits is done via multiplication by a factor between \$0\$ and \$2^n+1\$, inclusive. Here, we do a weighted sum instead of that kind of scaling, so the addition of "0.5" per component is proper "rounding".Quote from: peter-h on September 08, 2024, 07:46:40 pmNot directly: it was a sidetrack about the fundamental difference in paradigm between Cortex-M and RISC-V architectures. It happened, because I mis-chose the link to Compiler Explorer, and then suggested examining pretty bad code in Compiler Explorer.The intent behind was to show that although RISC-V is easy to generate code for, compiler optimizations are not as mature as for e.g. Cortex-M. You can see if you simply compare the generated code between different versions of the same compiler; I suggest using-O2 -Wall -march=armv7e-m -mtune=cortex-m4 -mthumbfor Cortex-M4. Between GCC 10.2.1 and 14.2.0, the same assembly is produced for Cortex-M4 using none-eabi, with-O2and-Oshaving only trivial differences.However, therewasa tie. Let me explain (and apologies for the long post):I picked the blend function as an example because it is something that is often done with floating-point arithmetic instead. The corrected version produces exactly the same values as the standard RGB blending, including rounding. The underlying idea is SIMD: expanding the argument(s) to a single integer "vector", with room between components so that they will never overflow into each other. Indeed, only two multiplications are needed using this scheme, one of each coefficient ("phase" and "inverse phase"). In practice, most of the operation cost is in expanding the "vector" from the source values, and packing the result "vector" back to a returnable value.The technique can be used with up to 10 bits per color component using 64-bit arithmetic, but at some point depending on the architecture, saving on the number of multiplications just is not worth the effort of unpacking and re-packing the "vector". It also does not work optimally for e.g. RGB565 format, where the components have different sizes. For example, with 8 bits per color component, you can save multiplication operations using the method here, but the packing and unpacking operation has a fixed "cost" (depending only on the number of components, not the size of each component). It may be less costly to do at least some of the multiplications separately, and avoid some/most of the packing and unpacking work.This is analogous to FPU use in an IRQ with lazy stacking enabled. When lazy FPU stacking is enabled, the use of FPU in an IRQ will have a fixed cost compared to no FPU use at all. However, any additional FPU use will not incur an extra cost. So, one must consider the cost of implementing the calculation in fixed-point arithmetic or otherwise integer arithmetic, versus how much faster/easier to maintain the code would be if FPU were used – including all possible uses in the IRQ, not just the key part that triggered the consideration. The details of how the lazy stacking of FPU registers (or any other registers) does not matter at all, because we do not have a kernel-userspace type of interface at hand: it will just work.

author: brucehoult
author_rank: Super Contributor
timestamp: September 09, 2024, 12:29:52 am
content: Quote from: Nominal Animal on September 08, 2024, 09:09:04 pmThen you should be clamping phase to 33, not 32, otherwise you can never get the value ofdstas the result.I wanted to add rounding to my function, but that would make the results different to the (fixed version) of your function.I don't know what the performance characteristics of the M4's multiply are, but for the very first RISC-V microcontroller, the FE-310 in the HiFive1, or for any other Berkley Rocket based design (K210, the cores in Microsemi FPGAs etc) the multiplier is multi-cycle latency, but pipelined and can start/finish one multiply per cycle, so it's much faster to do twice the multiplies for even/odd elements and use simple masking to extract the even/odd, rather than futz about with a lot of operations to spread the elements apart and gather them again.

author: bson
author_rank: Unknown
timestamp: September 09, 2024, 01:22:43 am
content: Correct me if I'm wrong, but lazy stacking only works if there's one thread using FP with the possibility of an ISR. It doesn't work if there are two threads using FP and no ISR, as this will produce no stacking and the FP regs won't get saved across context switches. If there's no ISR FP usage and only a single thread using FP, then of course they don't need to be saved at all, they can be owned by that one thread. FP reg stacking can be disabled completely.

author: westfw
author_rank: Super Contributor
timestamp: September 09, 2024, 01:46:06 am
content: one thing to watch out for is that some chips (rp2040 for instance) have various "math accelerators" whose context will NOT be saved by the Cortex ISR mechanisms...

author: brucehoult
author_rank: Super Contributor
timestamp: September 09, 2024, 02:04:33 am
content: Quote from: bson on September 09, 2024, 01:22:43 amIf you're using threads then you're probably using an RTOS, which you didn't have to write, which deals with saving and restoring registers for you. The RTOS can also implement lazy FP save/restore , so threads that don't use FP in any given time-slice don't needlessly save/restore FP registers. In software, assuming it has FPU states such as off, clean, dirty like other ISAs have, which I haven't checked.

author: ejeffrey
author_rank: Super Contributor
timestamp: September 09, 2024, 02:18:44 am
content: Quote from: bson on September 09, 2024, 01:22:43 amMy understanding is that the auto lazy stacking of fpu registers only works for ISRs. It's part of the same mechanism that handles stacking of the caller saved integer registers. I think it is possible for an RTOS to implement lazy fpu stacking context switches in the traditional way: diable the FPU when switching threads. If the new thread uses the FPU that will fault and the fault handler can stack the old registers and enable the FPU before returning.In general I think most of the "automatic / just works with the standard C ABI" features are really only applicable to single tasking embedded applications plus interrupts. If you build a multitasking OS you need to go read all of that detailed hardware information you got to ignore for bare metal applications.

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 09, 2024, 05:45:40 am
content: Yes that's mainly for ISRs. Sure can be used for RTOS threading (will make things easier for the developer of the RTOS itself), but otherwise as you said, that can be handled "manually".I've written a small RTOS (for RISC-V for now) and this is how I implemented it. Each "task" has parameters, among which whether it uses the FPU (and soon will be also for vector instructions). If so, during context switch, FPU registers will be saved/restored; if not, only regular registers are. Simple and effective. (But requires determining in advance which task will use the FPU and which won't. So requires some discipline while maintaining the software.)

author: Siwastaja
author_rank: Super Contributor
timestamp: September 09, 2024, 05:56:32 am
content: Quote from: bson on September 09, 2024, 01:22:43 amARM Cortex-M provides two "threads" so to speak, or we could call themcontexts, "main" program and interrupt.Allof the details needed to manage this is in hardware, such that programmer can just write standard C and not think about it any further. This includes interrupts nesting (pre-empting each other.)But if you needmorethan those two levels of threads (for example, things not supported by the CPU hardware e.g. time-sharing multitasking), then you need software which implements this extra. This piece of software is commonly called an "operating system". And if you want to write your own, then you indeed need to understand all those CPU details. It is not black magic, but nevertheless takes some time and concentration to get right.But if bare metal and main "thread" + interrupt "thread" + nested interrupts is all that you need, then ARM Cortex-M is really super easy to use, even to beginners.

author: brucehoult
author_rank: Super Contributor
timestamp: September 09, 2024, 06:16:20 am
content: Quote from: SiliconWizard on September 09, 2024, 05:45:40 amWhy do it like that?OK, it's easier to get going at the start, I guess, but lazy save/restore is pretty easy.

author: peter-h
author_rank: Super Contributor
timestamp: September 09, 2024, 07:35:54 am
content: QuoteIIRC, FreeRTOS saves the FPU state across task switches, so multiple tasks (threads) can all use the FPU,withlazy stacking. It certainly seems to work fine.BUT I just had a look for the config for this in FR. Didn't find any! The following code appears on googleCode:/* Set configENABLE_FPU to 1 to enable the Floating Point Unit (FPU), or 0* to leave the Floating Point Unit disabled. */#define configENABLE_FPU 1but my port of FR does not make reference to configENABLE_FPU... it is not in the freertosconfig.h file or anywhere else.I seem to have the CM4F FR build, which does not have FPU support according to herehttps://forums.freertos.org/t/freertos-port-for-arm-m4-no-fpu-no-mpu/7089But think about it: is it even possible to get a task switch or an interruptduringan FPU operation? The 32F4 hardware single floats are 1 clock cycle, although I recall something about division being 11 clocks.

author: newbrain
author_rank: Super Contributor
timestamp: September 09, 2024, 08:59:25 am
content: Quote from: peter-h, heavily snipped by me on September 09, 2024, 07:35:54 amNo because that flag is only for ArmV8M - as the comments in later versions say.On the contrary, CM4Fdoesinclude FP support (as the name suggest). In the thread, they were looking for a non FP version, AFAICU.

author: peter-h
author_rank: Super Contributor
timestamp: September 09, 2024, 01:10:13 pm
content: Newbrain - you have saved me a ton of work - thank youIt looks like this FR port is one which isfor the FPUso they took out any FPU related #defines.This bit of code (with my mods) confirms it and interestingly talks about the lazy stacking bit (all /* */ quotes are original)Code:// This picks up the _estack value from vectab2. We have to add 64k for 32F437.static void prvPortStartFirstTask( void ){/* Start the first task. This also clears the bit that indicates the FPU isin use in case the FPU was used before the scheduler was started - whichwould otherwise result in the unnecessary leaving of space in the SVC stackfor lazy saving of FPU registers. */#ifdef EXTRA_64Kextern uint32_t g_dev_id;if (g_dev_id==437){__asm volatile(" ldr r0, =0xE000ED08\n" /* Use the NVIC offset register to locate the stack. */" ldr r0, \n"" ldr r0, \n"" adds r0, #65536\n" // adjust for stack being 64k higher up" msr msp, r0\n" /* Set the msp back to the start of the stack. */" mov r0, #0\n" /* Clear the bit that indicates the FPU is in use, see comment above. */" msr control, r0\n"" cpsie i\n" /* Globally enable interrupts. */" cpsie f\n"" dsb\n"" isb\n"" svc 0\n" /* System call to start first task. */" nop\n");}else// 32F417 - original code{__asm volatile(" ldr r0, =0xE000ED08\n" /* Use the NVIC offset register to locate the stack. */" ldr r0, \n"" ldr r0, \n"" msr msp, r0\n" /* Set the msp back to the start of the stack. */" mov r0, #0\n" /* Clear the bit that indicates the FPU is in use, see comment above. */" msr control, r0\n"" cpsie i\n" /* Globally enable interrupts. */" cpsie f\n"" dsb\n"" isb\n"" svc 0\n" /* System call to start first task. */" nop\n");}

author: wek
author_rank: Frequent Contributor
timestamp: September 09, 2024, 01:31:46 pm
content: Quote from: brucehoult on September 09, 2024, 02:04:33 amoff/on - SCB_CPACR.CP10 = 0/1clean/dirty - CONTROL.FPCA=0/1CONTROL is an internal register of the processor, i.e. it can be accessed only using MRS/MSR.If FPU is clean (i.e. there no FPU instruction has been executed since reset or last time this bit was cleared (by hardware, this bit is cleared upon interrupt entry and restored upon exit)), there is no stacking, i.e. not even stack reservation for the lazy stacking if that is enabled.Besides writing an OS (or context switching for any other purpose), activities where these painful details are needed are stack calculation, and stack unwinding during debugging (i.e. they are needed only if one writes tools like debugger or something similar).JW

author: ejeffrey
author_rank: Super Contributor
timestamp: September 09, 2024, 02:28:05 pm
content: Quote from: peter-h on September 09, 2024, 07:35:54 amThere is no such thing as a half completed instruction. The division (or other long operation) will either be complete or not started from an architectural view when the ISR fires. The CPU may throw away partially completed work to improve ISR latency (I don't know if the M4F actually does this), but that's a micro-architectural detail programmers don't really have to consider.The only partial exception to this that I am aware of is the LDM/STM instructions. Again, they are either completed or cancelled, but they may have generated visible results before being interrupted. That is they may have modified the target registers or memory. After the ISR they are still restarted from the beginning. I think this is only supported on certain core families and controlled with a mode bit to enable lower interrupt latency.

author: langwadt
author_rank: Super Contributor
timestamp: September 09, 2024, 02:29:22 pm
content: https://developer.arm.com/documentation/dai0298/latest/

author: peter-h
author_rank: Super Contributor
timestamp: September 09, 2024, 02:34:40 pm
content: I had a dig around the oldest fossilised bits of my project and have thisCode:// ========== This was in SystemInit() ============// FPU startup is now in startupxxx.s - see// https://www.eevblog.com/forum/microcontrollers/stm-32f4-fpu-registers-and-main()-gotcha// #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)// SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */// #endifbut now this is in asm in startup.sCode:/* Start FPU, to avoid this problem *//* https://www.eevblog.com/forum/microcontrollers/stm-32f4-fpu-registers-and-main()-gotcha/ */ldr.w r0, =0xE000ED88/* The FPU enable bits are in the CPACR. */ldr r1, orr r1, r1, #( 0xf << 20 )/* Enable CP10 and CP11 coprocessors, then save back. */str r1, Hopefully this is right... I have certainly used floats in various RTOS tasks and not seen any issues... not that is a 100% assurance because you can get accidental synchronicity.QuoteDoesn't that answer my Q about whether FPU reg stacking is required at all?It is what I would expect. The FPU must issue an internal WAIT while it is working, otherwise the compiled code would need to insert a loop waiting on some FPU status and that loopwouldthen be vulnerable to interrupts / RTOS task switching and the FPU regs would need saving.QuoteDoes GCC use these?They seem to be like the Z80 LDIR whichissubject to interrupts, etc.As an aside, aren't all "long" instructions subject to DMA cycle stealing? So DMA could pick up intermediate data from a large number of arm32 instructions used by a compiler. But relying on that not happening is dumb.

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 09, 2024, 02:44:49 pm
content: Quote from: brucehoult on September 09, 2024, 12:29:52 amYes, and that would look like the following (this time a real world example for RGB565, with phase = 0..32, inclusive), but it wouldn't tie to the lazy FPU stacking anymore. (Yes, Ireallydid try to think of an analogous example that hard, even though I failed!)Code:uint_fast16_t blend565(const uint_fast16_t rgb0, const uint_fast16_t rgb1, int_fast8_t p){if (p < 0)p = 0;if (p > 32)p = 32;const uint32_t rb = ( (uint32_t)(32 - p) * (uint32_t)(rgb0 & 0xF81F)+ (uint32_t)( p) * (uint32_t)(rgb1 & 0xF81F)+ UINT32_C(0x8010)) >> 5;const uint32_t g = ( (uint32_t)(32 - p) * (uint32_t)(rgb0 & 0x07E0)+ (uint32_t)( p) * (uint32_t)(rgb1 & 0x07E0)+ UINT32_C(0x0200)) >> 5;return (rb & 0xF81F) | (g & 0x07E0);}This does include rounding, and is strictly equivalent to floating-point calculation of said components with rounding usingroundf()(halfway cases away from zero).Some key notes when translating between floating-point and equivalent integer calculations:Linear interpolation with a real coefficient \$0 \le \lambda \le 1\$ is simply $$v = (1 - \lambda) v_0 + \lambda v_1$$which is often written in the mathematically equivalent form \$v = v_0 + \lambda (v_1 - v_0)\$, but the latter is not exact with floating-point numbers when the magnitudes of \$v_0\$ and \$v_1\$ differ enough: the error due to inexactness accumulates near \$\lambda=1\$, so \$v\$ may never reach \$v_1\$. The first expression, however, distributes most error in the middle of the range, and is exact at both ends, \$\lambda=0\$ and \$\lambda=1\$.When converted to integer or fixed-point arithmetic, we describe \$\lambda = w/W\$ where \$W \gt 0\$ determines the range of values and is usually a power of two, and \$0 \le w \le W\$. (Many programmers fail to notice that \$w\$ has \$W+1\$ possible values, because \$w = W\$ is allowed; if \$W = 2^n\$, \$w\$ needs \$n+1\$ bits.) With rounding, this ends up being $$v = \frac{(W - w) v_0 + w v_1 + \left\lfloor\frac{W}{2}\right\rfloor}{W}$$where adding half of \$W\$ (truncated, or rounded towards zero) applies rounding, halfway cases upwards (thus assuming \$v_0 \ge 0\$, \$v_1 \ge 0\$).This is useful to know if you wish to compare an IRQ with float computation to the equivalent one with integer or fixed-point computation. The one trick is to useroundf()/round()semantics, rounding exact halfway cases away from zero, because they're the easiest to implement.Another trick I often end up using is the fact that $$\frac{a}{b} = \frac{c}{d} \quad \iff \quad a d = b c \quad$$and when \$b \gt 0\$ and \$d \gt 0\$, you can replace the equality operator with any comparator operator (\$\lt\$, \$\le\$, \$=\$, \$\ne\$, \$\ge\$, \$\gt\$). It means that one can often use integer fractions to describe values, but instead of having to do two divisions when comparing them, one can do two multiplications instead. In some cases, like when implementing anatan2()operation, this can be more precise than the equivalent floating-point calculation. Of course, the same relation applies to floating-point numbers too, to within available precision (as multiplying with a reciprocal does not yield the exact same results as division; there is often a bit of rounding/quantization error due to finite precision).

author: DiTBho
author_rank: Super Contributor
timestamp: September 09, 2024, 07:00:53 pm
content: Quote from: SiliconWizard on September 09, 2024, 05:45:40 amis it like ucOS/2? or smaller?

author: bson
author_rank: Unknown
timestamp: September 09, 2024, 08:18:09 pm
content: Quote from: peter-h on September 09, 2024, 07:35:54 amYeah, actually, the scheduler when performing a context switch can do a dummy destructive FP operation, like zero an FP reg, to force the state to be stored if it hasn't already. I like this!It can be slightly inefficient though if the ISR uses FP and lazy stacks the state, then chains a SVC for a context switch, which then again stacks the state. The state can't change during the chaining so theoretically wouldn't need to be restacked, but I suppose it's a reasonable price to pay for using FP in an ISR.

author: bson
author_rank: Unknown
timestamp: September 09, 2024, 08:28:17 pm
content: Quote from: peter-h on September 09, 2024, 02:34:40 pmBy default FP operations complete out of order vs integer GPR operations, but this can be disabled.If disabled, the main pipeline will stall on long-running FP operations.Not sure what the interrupt behavior is in the first case, presumably if OOO the CPU has to stall waiting for the FP operation to finish before dispatching an interrupt. In the second case it can abort and discard a running FP operation, so maybe this is why the OOO can be disabled - to improve interrupt latency.

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 09, 2024, 08:58:13 pm
content: Quote from: brucehoult on September 09, 2024, 06:16:20 amWell, why not? It's simple, gives you full control and has negligible cost (just needs to check a flag in the task switching routine and branch accordingly), with zero potential for save/restore bugs as long as you don't use the FPU in tasks that are defined not to support it.I haven't really thought of doing this differently though so far - this isn't meant at this point to be a full general-purpose RTOS, widely used like FreeRTOS or such, so if it's slightly less developer-friendly, that's fine. But I admit I haven't thought about that lazy save/restore thing. How would you do this efficiently on RISC-V?

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 09, 2024, 09:12:13 pm
content: Quote from: DiTBho on September 09, 2024, 07:00:53 pmA bit in the same vein, but certainly smaller at this point. It's a total of about 2k LOCs (the task switching low-level routine being about 300 LOCs of assembly, the rest is C), including generic support for RV32/64 and a "port" for specifics of the CH32V307 (on which I've tested it so far, but I intend to test it on the CV1800B next, which I'm currently working on).It supports priorities, variable time slots, sleep/yield and events tasks can wait on and signal. There's a lot more that can be added next, but so far it's enough to be usable.

author: brucehoult
author_rank: Super Contributor
timestamp: September 10, 2024, 12:39:45 am
content: Quote from: SiliconWizard on September 09, 2024, 08:58:13 pmThere are many strategies possible (policy), depending on what you want to optimise for, but the mechanism is well described over four pages in section 3.1.6.6 "Extension Context Status in mstatus Register" on e.g. p25 and following of riscv-privileged-20211203.pdfRead that and ask if you have any remaining questions.Note that according to the V ABI the vector state is undefined after any function call, of which system calls are a subset, so on any syscall the OS is entitled to set the current VS to "off" (and future use of V after the syscall returns will trap, at which point the OS can clear the state to 0s) or alternatively clear all the registers right away and set the current VS to "initial", which means it won't need to be saved on the next task switch if no more V instructions are executed. So the (rather large) V state has to be saved and restored only on a timeslice expired task switch (if dirty), not on a synchronous system call task switch.So VS and FS differ in that way, by OS/ABI policy, not in hardware.> zero potential for save/restore bugs as long as you don't use the FPU in tasks that are defined not to support itThat means you have to know for sure what all the code in all the libraries you call does. Practical in small embedded software, not in desktop software. How do you know printf() doesn't run any FP instructions at all, even if there are no %f in the format string? Or any other library you didn't write.On the other hand, your scheme does allow the skilled developer to lie to the OS, say you don't use FP, but then actually do some small amount of FP computation, using a limited register set, and saving and restoring only the registers actually used (and the FP state: rounding mode, accrued exception flags, ...)That is, if the OS doesn't police it by turning the FPU off for threads that say they don't use it, and trapping and printing an error message if they do. That would also solve the "how do I know printf() doesn't use FP?" problem.Or I guess you could start with "uses FP" turned off for all threads, and turn it on the first time they use an FP instruction -- and then leave it on permanently, saving and restoring their FP registers on every task switch from then on.It might be ideal to have your "I use FP" / "I don't use FP" flag be instead "I use FP heavily, please eagerly restore my registers" / "I don't use FP, or rarely, please restore my FP registers lazily".

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 10, 2024, 06:24:49 am
content: Quote from: brucehoult on September 10, 2024, 12:39:45 amGoing to read that.Quote from: brucehoult on September 10, 2024, 12:39:45 amYes, as I said, it's not meant as a general-purpose OS, let alone anything desktop-like!But I may make it evolve in that direction. I'm certainly going to consider implementing lazy save/restore once I grabbed the concepts in those few pages.Quote from: brucehoult on September 10, 2024, 12:39:45 amWell, eventually I'll probably keep the "manual" approach as an option (so if you use that, you'll be supposed to know what you're doing), and otherwise make the lazy save/restore mechanism as the default.

author: brucehoult
author_rank: Super Contributor
timestamp: September 10, 2024, 06:38:00 am
content: Quote from: SiliconWizard on September 10, 2024, 06:24:49 amOne key thing to keep in mind right from the start:- modifyingmstatus.FSdoes not alter the the FP registers or FP status CSRs in any way. Setting it to "off" does not clear the registers or anything like that. If you later set the status to any of the other values you can then read and use the previous contents as if nothing happened. The same applies to settingFSto "initial". That doesn't clear the registers -- it's you recording the fact that you HAVE cleared them.The only other thing is the effect that FS has on normal FP instructions, and vice versa:- if FS is "off" then any FP instruction use will trap- if FS is "initial" or "clean" then any FP instruction that alters FP registers/CSRs will change FS to "dirty".