第二十七章 FPU介绍及应用
1. FPU简介
FPU 即浮点运算单元(Float Point Unit)。浮点运算,对于定点 CPU(没有 FPU 的 CPU)来说必须要按照 IEEE-754 标准的算法来完成运算,是相当耗费时间的。而对于有 FPU 的 CPU来说,浮点运算则只是几条指令的事情,速度相当快。
STM32F407 属于 Cortex M4 架构,带有 32 位单精度硬件 FPU,支持浮点指令集,相对于Cortex M0 和 Cortex M3 等,高出数十倍甚至上百倍的运算性能。
STM32F407 硬件上要开启 FPU 是很简单的,通过一个叫:协处理器控制寄存器(CPACR)的寄存器设置即可开启 STM32F407 的硬件 FPU,该寄存器各位描述如图
这里我们就是要设置 CP11 和 CP10 这 4 个位,复位后,这 4 个位的值都为 0,此时禁止访问协处理器(禁止了硬件 FPU),我们将这 4 个位都设置为 1,即可完全访问协处理器(开启硬件 FPU),此时便可以使用 STM32F407 内置的硬件 FPU 了。 CPACR 寄存器这 4 个位的设置,我们在 system_stm32f4xx.c 文件里面开启,代码如下:
void SystemInit (void)
{
/* 省略部分代码 */
/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << (10*2))|(3UL << (11*2)));
/* set CP10 and CP11 Full Access */
#endif
/* 省略部分代码 */
}
此部分代码是系统初始化函数的部分内容,功能就是设置 CPACR 寄存器的 20~23 位为 1,以开启 STM32F407 的硬件 FPU 功能。从程序可以看出,只要我们定义了全局宏定义标识符__FPU_PRESENT 以及__FPU_USED 为 1,那么就可以开启硬件 FPU。其中宏定义标识符__FPU_PRESENT 用来确定处理器是否带 FPU 功能,标识符__FPU_USED 用来确定是否开启FPU 功能。
实际上,因为 STM32F407 是带 FPU 功能的,所以在我们的 stm32f407xx.h 头文件里面,我们默认是定义了__FPU_PRESENT 为 1。大家可以打开文件搜索即可找到下面一行代码:
#define __FPU_PRESENT 1U /* FPU present */
但是,仅仅只是说明处理器有 FPU 功能是不够的,我们还需要开启 FPU 功能。开启 FPU有两种方法,第一种是直接在头文件 stm32f407xx.h 中定义宏定义标识符__FPU_USED 的值为1。也可以直接在 MDK 编译器上面设置,我们在 MDK5 编译器里面,点击魔术棒按钮,然后在 Target选项卡里面,设置 Floating Point Hardware 为 Use Single Precision,
经过这个设置,编译器会自动加入标识符__FPU_USED 为 1。这样遇到浮点运算就会使用硬件 FPU 相关指令,执行浮点运算,从而大大减少计算时间。
2. Julia 分形简介
Julia 分形即 Julia 集,它最早由法国数学家 Gaston Julia 发现,因此命名为 Julia(朱利亚)集。 Julia 集合的生成算法非常简单:对于复平面的每个点,我们计算一个定义序列的发散速度。该序列的 Julia 集计算公式为:
zn+1 = zn2 + c
针对复平面的每个 x + i.y 点,我们用 c = cx + i.cy 计算该序列:
xn+1 + i.yn+1 = xn2 - yn2 + 2.i.xn.yn + cx + i.cy
xn+1 = xn2 - yn2 + cx 且 yn+1 = 2.xn.yn + cy
一旦计算出的复值超出给定圆的范围(数值大小大于圆半径),序列便会发散,达到此限值时完成的迭代次数与该点相关。随后将该值转换为颜色,以图形方式显示复平面上各个点的分散速度。
经过给定的迭代次数后,若产生的复值保持在圆范围内,则计算过程停止,并且序列也不发散,本例程生成 Julia 分形图片的代码如下:
#define ITERATION 128 /* 迭代次数 */
#define REAL_CONSTANT 0.285f /* 实部常量 */
#define IMG_CONSTANT 0.01f /* 虚部常量 */
/**
* @brief 产生 Julia 分形图形
* @param size_x : 屏幕 x 方向的尺寸
* @param size_y : 屏幕 y 方向的尺寸
* @param offset_x : 屏幕 x 方向的偏移
* @param offset_y : 屏幕 y 方向的偏移
* @param zoom : 缩放因子
* @retval 无
*/
void julia_generate_fpu(uint16_t size_x, uint16_t size_y, uint16_t offset_x,
uint16_t offset_y, uint16_t zoom)
{
uint8_t i;
uint16_t x, y;
float tmp1, tmp2;
float num_real, num_img;
float radius;
for (y = 0; y < size_y; y++)
{
for (x = 0; x < size_x; x++)
{
num_real = y - offset_y;
num_real = num_real / zoom;
num_img = x - offset_x;
num_img = num_img / zoom;
i = 0;
radius = 0;
while ((i < ITERATION - 1) && (radius < 4))
{
tmp1 = num_real * num_real;
tmp2 = num_img * num_img;
num_img = 2 * num_real * num_img + IMG_CONSTANT;
num_real = tmp1 - tmp2 + REAL_CONSTANT;
radius = tmp1 + tmp2;
i++;
}
LCD->LCD_RAM = g_color_map[i]; /* 绘制到屏幕 */
}
}
}
这种算法非常有效地展示了 FPU 的优势:无需修改代码,只需在编译阶段激活或禁止FPU(在 MDK Code Generation 的 Float Point Hardware 选项里面设置: Single Precision /Not Used),即可测试使用硬件 FPU 和不使用硬件 FPU 的差距。
3. FPU应用示例
#include "bsp_init.h"
uint8_t timeout;
/* FPU模式提示 */
#if __FPU_USED==1
#define SCORE_FPU_MODE "FPU On"
#else
#define SCORE_FPU_MODE "FPU Off"
#endif
#define ITERATION 128 /* 迭代次数 */
#define REAL_CONSTANT 0.285f /* 实部常量 */
#define IMG_CONSTANT 0.01f /* 虚部常量 */
/* 颜色表 */
uint16_t g_color_map[ITERATION];
/* 缩放因子列表 */
const uint16_t zoom_ratio[] =
{
120, 110, 100, 150, 200, 275, 350, 450,
600, 800, 1000, 1200, 1500, 2000, 1500,
1200, 1000, 800, 600, 450, 350, 275, 200,
150, 100, 110,
};
/**
* @brief 初始化颜色表
* @param clut : 颜色表指针
* @retval 无
*/
void julia_clut_init(uint16_t *clut)
{
uint32_t i = 0x00;
uint16_t red = 0, green = 0, blue = 0;
for (i = 0; i < ITERATION; i++) /* 产生颜色表 */
{
/* 产生RGB颜色值 */
red = (i * 8 * 256 / ITERATION) % 256;
green = (i * 6 * 256 / ITERATION) % 256;
blue = (i * 4 * 256 / ITERATION) % 256;
/* 将RGB888,转换为RGB565 */
red = red >> 3;
red = red << 11;
green = green >> 2;
green = green << 5;
blue = blue >> 3;
clut[i] = red + green + blue;
}
}
/* RGB LCD 缓存*/
uint16_t g_lcdbuf[800];
/**
* @brief 产生Julia分形图形
* @param size_x : 屏幕x方向的尺寸
* @param size_y : 屏幕y方向的尺寸
* @param offset_x : 屏幕x方向的偏移
* @param offset_y : 屏幕y方向的偏移
* @param zoom : 缩放因子
* @retval 无
*/
void julia_generate_fpu(uint16_t size_x, uint16_t size_y, uint16_t offset_x, uint16_t offset_y, uint16_t zoom)
{
uint8_t i;
uint16_t x, y;
float tmp1, tmp2;
float num_real, num_img;
float radius;
for (y = 0; y < size_y; y++)
{
for (x = 0; x < size_x; x++)
{
num_real = y - offset_y;
num_real = num_real / zoom;
num_img = x - offset_x;
num_img = num_img / zoom;
i = 0;
radius = 0;
while ((i < ITERATION - 1) && (radius < 4))
{
tmp1 = num_real * num_real;
tmp2 = num_img * num_img;
num_img = 2 * num_real * num_img + IMG_CONSTANT;
num_real = tmp1 - tmp2 + REAL_CONSTANT;
radius = tmp1 + tmp2;
i++;
}
LCD->LCD_RAM = g_color_map[i]; /* 绘制到屏幕 */
}
}
}
int main(void)
{
uint8_t key_value = 0;
uint8_t i = 0;
uint8_t autorun = 0;
float time;
char buf[50];
bsp_init();
LCD_ShowString(30,70,200,16,16,"FPU TEST");
LCD_ShowString(30,110,200,16,16,"KEY0:+ KEY1:-");
LCD_ShowString(30,130,200,16,16,"KEY_UP:AUTO/MANUL");
delay_ms(500);
julia_clut_init(g_color_map);
while(1)
{
key_value = key_scan(0);
switch(key_value)
{
case KEY0_Press:
i++;
if(i > sizeof(zoom_ratio)/2-1)
i = 0;
break;
case KEY1_Press:
if(i)
i--;
else
i = sizeof(zoom_ratio)/2-1;
break;
case WKUP_Press:
autorun = !autorun;
break;
default:break;
}
if(autorun)
{
i++;
LED_ON(LED1_GPIO_Pin);
if(i > sizeof(zoom_ratio)/2-1)
{
i = 0;
}
}
else
{
LED_OFF(LED1_GPIO_Pin);
}
LCD_Set_Window(0,0,lcddev.width, lcddev.height);
LCD_WriteRAM_Prepare();
TIM6->CNT = 0;
timeout = 0;
julia_generate_fpu(lcddev.width, lcddev.height, lcddev.width/2, lcddev.height/2, zoom_ratio[i]);
time = TIM6->CNT + (uint32_t)timeout * 65536;
sprintf(buf, "%s: Zoom:%d RunTime:%0.1fms", SCORE_FPU_MODE,zoom_ratio[i], time/10);
LCD_ShowString(5,lcddev.height-17,lcddev.width-5,12,12,buf);
printf("%s\n", buf);
LED_TOGGLE(LED0_GPIO_Pin);
}
}
4. FPU常见函数(HAL库)
4.1 FPU基础配置
4.1.1. 启用FPU支持
在STM32CubeIDE/IAR/Keil中启用FPU:
Keil MDK:
// Target -> Floating Point Hardware 选择 "Single Precision"
IAR EWARM:
// Project -> Options -> General Options -> Floating Point
// 选择 "FPv4-SP" (Single Precision)
STM32CubeIDE:
// Project Properties -> C/C++ Build -> Settings
// -> MCU Settings -> Floating-point unit = Hardware (-mfpu=fpv4-sp-d16)
4.1.2 启动文件配置
; startup_stm32f407xx.s
Reset_Handler:
; 启用FPU
LDR.W R0, =0xE000ED88 ; 加载CPACR地址
LDR R1, [R0] ; 读取CPACR
ORR R1, R1, #(0xF << 20) ; 设置CP10和CP11为全访问
STR R1, [R0] ; 写回CPACR
DSB ; 数据同步屏障
ISB ; 指令同步屏障
4.2 FPU相关寄存器
4.2.1 CPACR (协处理器访问控制寄存器)
地址: 0xE000ED88
// 启用FPU
SCB->CPACR |= (0xF << 20); // 设置CP10和CP11为全访问
// 检查FPU是否启用
if ((SCB->CPACR & (0xF << 20)) == (0xF << 20)) {
// FPU已启用
}
4.2.2 FPU状态寄存器
// 获取FPU状态
uint32_t fpscr = __get_FPSCR();
// 设置舍入模式
__set_FPSCR(fpscr & ~(0x3 << 22)); // 清除模式位
__set_FPSCR(fpscr | (0x1 << 22)); // 设置为向零舍入
// 启用/禁用异常
__set_FPSCR(fpscr | (1 << 9)); // 启用无效操作异常
4.3 FPU使用示例
4.3.1 基本浮点运算
float vector_add(float a, float b) {
return a + b; // 编译器自动生成FPU指令VADD.F32
}
float matrix_multiply(const float *matA, const float *matB, int size) {
float sum = 0.0f;
for (int i = 0; i < size; i++) {
// 使用FPU指令加速计算
sum += matA[i] * matB[i]; // VMLA.F32
}
return sum;
}
4.3.2 向量运算 (使用CMSIS-DSP)
#include "arm_math.h"
void fpu_vector_operations(void) {
float32_t vecA[4] = {1.0f, 2.0f, 3.0f, 4.0f};
float32_t vecB[4] = {0.5f, 1.5f, 2.5f, 3.5f};
float32_t result[4];
// 向量加法 (FPU优化)
arm_add_f32(vecA, vecB, result, 4);
// 点积运算
float32_t dotProduct;
arm_dot_prod_f32(vecA, vecB, 4, &dotProduct);
// 矩阵乘法
float32_t matA[9], matB[9], matC[9];
arm_mat_mult_f32(&matA, &matB, &matC);
}
4.4 性能优化技巧
4.4.1 循环向量化
// 启用自动向量化 (编译器选项)
// -O3 -ffast-math (GCC)
// --vectorize (IAR)
// 手动向量化示例
void vector_scale(float *data, float scale, int len) {
// 确保4字节对齐
if ((uint32_t)data & 0x3) {
// 处理非对齐数据
}
int i;
// 主循环 (每次处理4个元素)
for (i = 0; i < len - 3; i += 4) {
data[i] *= scale;
data[i+1] *= scale;
data[i+2] *= scale;
data[i+3] *= scale;
}
// 处理剩余元素
for (; i < len; i++) {
data[i] *= scale;
}
}
4.4.2 避免浮点转换
// 错误做法 - 导致整数到浮点的转换
for (int i = 0; i < 1000; i++) {
float x = i; // 隐式转换 (VSCVTF)
// ...
}
// 正确做法 - 使用浮点计数器
for (float f = 0.0f; f < 1000.0f; f += 1.0f) {
// 直接使用浮点值
}
4.4.3 使用FPU友好算法
// 使用FMA指令 (乘加融合)
float fma_example(float a, float b, float c) {
// 编译器可能生成 VFMA.F32
return a * b + c;
}
// 牛顿迭代法求平方根 (FPU优化)
float sqrt_newton(float x) {
float y = x * 0.5f; // 初始估计值
for (int i = 0; i < 4; i++) {
y = 0.5f * (y + x / y); // VSQRT.F32替代
}
return y;
}
4.5 FPU异常处理
4.5.1 异常类型
异常位
描述
IOC
无效操作
DZC
除零
OFC
溢出
UFC
下溢
IXC
不精确结果
4.5.2 异常处理配置
void configure_fpu_exceptions(void) {
// 启用FPU异常
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(MemoryManagement_IRQn, 0);
NVIC_SetPriority(BusFault_IRQn, 0);
NVIC_SetPriority(UsageFault_IRQn, 0);
NVIC_EnableIRQ(UsageFault_IRQn);
// 启用所有FPU异常
__set_FPSCR(__get_FPSCR() | 0x9F);
}
// 异常处理函数
void UsageFault_Handler(void) {
uint32_t cfsr = SCB->CFSR;
uint32_t fpscr = __get_FPSCR();
if (cfsr & (1 << 9)) { // FPU异常标志
printf("FPU Exception: ");
if (fpscr & (1 << 0)) printf("IOC "); // 无效操作
if (fpscr & (1 << 1)) printf("DZC "); // 除零
if (fpscr & (1 << 2)) printf("OFC "); // 溢出
if (fpscr & (1 << 3)) printf("UFC "); // 下溢
if (fpscr & (1 << 4)) printf("IXC "); // 不精确
// 清除异常标志
__set_FPSCR(fpscr & ~0x1F);
}
while(1);
}
4.6 FPU性能测试
4.6.1 性能测试函数
#include "arm_math.h"
void fpu_performance_test(void) {
const int ITERATIONS = 10000;
uint32_t start, end;
float result = 0.0f;
// 浮点加法测试
start = DWT->CYCCNT;
for (int i = 0; i < ITERATIONS; i++) {
result += 1.234f;
}
end = DWT->CYCCNT;
printf("Float Add: %lu cycles/op\n", (end - start) / ITERATIONS);
// 浮点乘法测试
start = DWT->CYCCNT;
for (int i = 0; i < ITERATIONS; i++) {
result *= 1.001f;
}
end = DWT->CYCCNT;
printf("Float Mul: %lu cycles/op\n", (end - start) / ITERATIONS);
// 浮点除法测试
start = DWT->CYCCNT;
for (int i = 0; i < ITERATIONS; i++) {
result /= 1.001f;
}
end = DWT->CYCCNT;
printf("Float Div: %lu cycles/op\n", (end - start) / ITERATIONS);
// 浮点平方根测试
start = DWT->CYCCNT;
for (int i = 0; i < ITERATIONS; i++) {
result = sqrtf(result + 1.0f);
}
end = DWT->CYCCNT;
printf("Float Sqrt: %lu cycles/op\n", (end - start) / ITERATIONS);
}