Hello, welcome!
In the video, we will talk about floating-point unit (FPU).
This figure shows all instructions supported by different ARM Cortex-M cores.
Here, the letter "M" stands for Micro-controller.
Cortex-M0 and M1 provide basic instructions for data processing and I/O control.
Cortex-M3 extends M0 and M1 by adding instructions for advanced data processing and bit field
manipulations.
Cortex-M4 extends M3 by adding instructions for digital signal processing and floating-point
operations.
The floating-point unit (FPU) is a math coprocessor designed to carry out operations on floating-point
numbers.
FPU is an optional hardware component.
Some Cortex-M4 microcontrollers don't have an FPU.
Therefore, check the datasheet carefully and see whether FPU is present.
A floating-point number can be single precision, which is declared as "float" in C, or double
precision, which is declared as "double" in C.
For a float, 32 bits are used to represent a floating-point number.
For a double, 64 bits are used.
The FPU on a Cortex-M4 can fully support single-precision operations, but not double-precision.
Let's use a simple example to compare the compiled programs under two different scenarios:
FPU is present, and FPU is not present.
This simple C program calculates the area of a rectangle.
When FPU is not present, the compiled program will be very similar to this one.
The floating-point multiplication is performed by this runtime library function.
Floating-point operations will be emulated by using integer operations.
As result, the input arguments are passed via Cortex core registers r0 and r1, and the
result is returned in register r0.
However, if FPU is available, the compiled program will look like this.
The floating-point multiplication is directly performed by the FPU, without using any software
library.
And the input arguments are passed via the floating-point register s0 and s1, and the
result is returned in s0.
On Cortex-M4, FPU can take as fast as 1 clock cycle to perform a floating-point multiplication,
which is much faster than the multiplication function in the runtime software library.
To ask the compiler to generate an executable that uses FPU, in the target setting in Keil,
we need to select single-precision.
For gcc users, you need to type some special command with some options to activate the
FPU.
If a C program performs double-precision calculations, the C compiler will use runtime library functions
to handle the calculation in software.
For most C compilers, a double-precision calculation will be emulated by using integer operations,
instead of single-precision floating-point operations.
To run floating-point instructions, software needs to enable FPU first.
After a system reset operation, the FPU is disabled.
Software must run this simple C statement to program the Coprocessor Access Control
Register (CPACR) before software uses any floating-point instructions.
Let's take a look at the CPACR register.
On Cortex-M4, both the CP10 and CP11 fields in CPACR are used for the FPU.
By default, the CP11 and CP10 fields are zeros after a system reset, and this setting disables
FPU to allow low power dissipations.
To enable FPU, software should set the CP11 and CP10 fields to 11 in binary.
This setting allows FPU to be fully accessed.
Here is the example code in C and assembly.
Note that CPACR can only be written if the processor is in the privileged mode.
Now let's look at how FPU operates.
This is Cortex-M core and it has 13 general-purpose registers named from r0 to r12, stack pointer,
link register, program counter, and a few special-purpose registers.
The processor core use LDR and POP instructions to read the memory, and STR and PUSH instructions
to write to the memory.
Similarly, the FPU coprocessor has its own general- and special-purpose registers.
Specifically, the FPU has 32 single-precision registers, named from s0 to s31.
Here the letter "S" stands for single-precision.
Each single-precision register has 32 bits, and each of them can hold either a single-precision
floating-point value, or a 32-bit integer.
FPU also has three special-purpose registers, as listed here.
The FPU uses VLDR or VPOP to load data from the memory, and VSTR or VPUSH to store data
into the memory.
These single-precision registers can also be accessed as a pair to form double-precision
registers, named from d0 to d15.
For example, the double-precision register d0 is composed of two single-precision registers
s1 and s0, with s1 holding the upper 32 bits, and s0 the lower 32 bits.
Let me show you a few assembly instructions.
We can use this instruction to load a single-precision floating-point number stored at the memory
address in r0.
We can use this to load a double-precision value from the memory.
FPU on Cortex-M4 doesn't support double-precision floating-point calculations.
But an assembly program can still load or store double-precision data.
These are two push instructions that push single- and double-precision registers onto
the stack.
This table compares some Cortex-M and FPU instructions, such as accessing memory, copying
registers, arithmetic operations, and comparison.
All FPU instructions start with the letter "V", which stands for vector floating-point.
FPU also supports some advanced operations, which have no corresponding instructions on
Cortex-M, such as the square root instruction vsqrt.
All assembly functions are required to follow the procedure call standard.
Registers s0 to s15, or d0 to d7, are caller saved registers.
If the caller wishes the values of these registers remain the same after the callee returns,
the caller should preserve them by using the stack.
Register s16 to s31, or d8 to d15 are callee saved registers.
The callee should recover their original values if the callee writes to them.
For example, suppose function A() calls function B().
If the caller A() expects the values of register s3 and s5 are not modified by the callee B(),
then the caller A() should push s3 and s5 onto the stack before calling B(), and also
pop them off the stack to restore their original values immediately after B() returns.
In addition, if the callee B() needs to write to register s17, B() should push s17 onto
the stack at the very beginning of the function, and pop it off the stack at the end.
The caller A() has such expectation that calling a function does not influence the value of
any register from s16 to s31.
How to pass floating-point arguments to a function if FPU is used?
When arguments are passed to a function, each argument is assigned in turn to the next free
register of the corresponding type.
Let's use this example to show how arguments are passed.
The first argument a1 is a double, and it is passed via register d0.
The second argument a2 is a float and it is passed in register s2.
The next argument a3 is a double and the next double-precision register available is d2.
Therefore, a3 is passed in register d2.
Following this process, the rest arguments are passed in this way: a4 in s3, a5 in s6,
a6 in d4, a7 in s7, and a8 in d5.
Let me give you an example to show how arguments are passed.
This C function calculates the area of a circle.
It takes one float argument named radius.
In the equivalent assembly implementation, the input argument radius is passed in the
single-precision register s0.
The result is also returned to the caller in s0.
The assembly program uses a pseudo-instruction to initialize s1, and then two multiplication
instructions to calculate the area.
The result is saved and returned in s0.
We can use the VMOV instruction to copy data between Cortex core registers and FPU general-purpose
registers.
However, we need to use two special instructions VMRS and VMSR to read or write to FPU special-purpose
registers.
For example, to change the FPU rounding mode to rounding toward zero, software needs to
use VMRS to read, and VMSR to write to the register FPSCR.
This assembly code sets bit 22 and 23 of FPSCR to 1.
Just for your reference, FPSCR stands for Floating-Point Status Control Register.
Bit 22 and 23 define the rounding mode.
The default is to round to the nearest.
When these two bits are 1, FPU rounds the result towards 0.
Now, let's look at the comparison instruction.
In this C code, the max function returns the larger one among float a and float b.
Pay attention here, the floating-point compare instruction VCMP updates the N, Z, C, V flags
of the FPU's Status Control Register (FPSCR), not the Cortex-M's Application Program Status
Register (APSR).
Therefore, in order to run the conditionally executed code or any conditional branch instructions,
software must copy the NZCV flags from the FPU's FPSCR to the Cortex-M's APSR.
FPU also supports fast data conversion between different data types.
This diagram shows instructions that convert a 32-bit signed or unsigned integer to a single-
or double-precision floating-point number.
This diagram shows the conversion in the opposite direction.
It converts a float or double to a 32-bit signed or unsigned integer.
FPU can also support the conversion between a float and a fixed-point.
This assembly instruction converts a 32-bit fixed-point to a float.
The immediate number 10 specifies that the fraction part of the fixed-point has 10 binary bits.
During an interrupt, the stacking and unstacking with FPU work differently than those without FPU.
Let's first review auto-stacking and unstacking.
When an interrupt request arrives, the processor automatically pushes some registers onto the
stack before it starts to execute the corresponding interrupt handler.
When an interrupt handler completes, the processor automatically popes these registers off the
stack and restores the processor state.
The automatic stacking and unstacking improve the performance of executing interrupt handlers.
Without FPU, hardware automatically pushes these eight registers onto the stack, and
later automatically pops them off the stack.
However, with FPU, 17 FPU registers are also pushed onto the stack, including s0 to s15,
and FPSCR.
The FPU special-purpose register called floating-point context address register (FPCAR) holds the
memory address where s0 is stored in the stack frame.
If lazy stacking is enabled, hardware does not push those FPU registers onto the stack
unless the interrupt handler uses FPU registers.
Thank you for watching.
Please visit the book website for more information.
See you next time.