Las FPU reducen el tiempo de ejecución de operaciones matemáticas, en esta entrada se cuantifican los tiempos de ejecución de las más elementales mediante un benchmark en la FPU de MicroBlazeTM. También se proponen varias optimizaciones y se mide su impacto.
Cuando el tiempo de ejecución es crítico se deben explotar al máximo las posibilidades de paralelización que ofrecen las FPGA. Sin embargo, por cuestiones de espacio podría no ser posible implementar todas las operaciones a nivel RTL, en ese momento hay que recurrir a filosofías de trabajo más tradicionales que utilicen procesadores de propósito general.

Al realizar la especificación de MicroBlazeTM, Xilinx® ofrece varias posibilidades a la hora de extender las instrucciones relacionadas con la FPU.
- FPU básica
- FPU extendida
Recurso | Básica | Extendida |
Área | +6% | +10% |
DSP48E | 2 | 2 |
Método del benchmark
Se medirá el tiempo empleado en realizar operaciones sobre un array de n elementos, el código será el siguiente
#include <math.h> int mult_bench(int n, float *a, float *b, float *c){ for(int i = 0; i<n; i++){ *(c+i) = *(a+i) * *(b+i); } return 0; } int div_bench(int n, float *a, float *b, float *c){ for(int i = 0; i<n; i++){ *(c+i) = *(a+i) / *(b+i); } return 0; } int sqrt_bench(int n, float *b, float *c){ for(int i = 0; i<n; i++){ *(c+i) = sqrtf(*(b+i)); } return 0; } int pow_bench(int n, float *a, float *b, float *c){ for(int i = 0; i<n; i++){ *(c+i) = powf(*(a+i), *(b+i)); } return 0; }
Para medir el tiempo de ejecución se utiliza un timer, el diagrama de bloques en VivadoTM queda de la siguiente manera

Se realizan varias pruebas utilizando distintos ‘n’ tamaños de array, con ello se hará una media para obtener un resultado intermedio entre llamadas a la FPU más y menos intensivas. El código con el que se lanzará el benchmark en la FPU de MicroBlazeTM es el siguiente
#include <stdio.h> #include <math.h> #include "platform.h" #include "xil_printf.h" #include "xtmrctr.h" #include "xparameters.h" #include "benchmark.h" #define TMR_DEVICE_ID XPAR_TMRCTR_0_DEVICE_ID #define TIMER_COUNTER_0 0 const int N_OPERATIONS = 200; int begin, end, status; XTmrCtr TMRInst; int printFloat(float f); int print_abc(); int startTable(float *a, float *b, float *c); int launchBenchmark(float *a, float *b, float *c){ print("Mul FPU "); begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); mult_bench(N_OPERATIONS, a, b, c); end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); //print_abc(a, b, c); xil_printf("ticks: %d \r\n", end-begin); print("Div FPU "); begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); div_bench(N_OPERATIONS, a, b, c); end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); //print_abc(a, b, c); xil_printf("ticks: %d \r\n", end-begin); print("Sqrt FPU "); begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); sqrt_bench(N_OPERATIONS, b, c); end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); //print_abc(a, b, c); xil_printf("ticks: %d \r\n", end-begin); print("Pow FPU "); begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); pow_bench(N_OPERATIONS, a, b, c); end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0); //print_abc(a, b, c); xil_printf("ticks: %d \r\n", end-begin); return 0; } int main() { float a[N_OPERATIONS+1]; float b[N_OPERATIONS+1]; float c[N_OPERATIONS+1]; init_platform(); XTmrCtr_Initialize(&TMRInst, TMR_DEVICE_ID); XTmrCtr_SetOptions(&TMRInst, TIMER_COUNTER_0, XTC_AUTO_RELOAD_OPTION); XTmrCtr_Start(&TMRInst, TIMER_COUNTER_0); startTable(&a[0], &b[0], &c[0]); launchBenchmark(&a[0], &b[0], &c[0]); cleanup_platform(); return 0; } int print_abc(float *a, float *b, float *c){ for(int i=0; i<=N_OPERATIONS; i++){ xil_printf("%d: ", i); printFloat(a[i]); printFloat(b[i]); printFloat(c[i]); print("\n\r"); } return 0; } int printFloat(float f){ int whole = f; int thousandths = (f - whole) * 1000; xil_printf("%d.%03d ", whole, thousandths); return 0; } int startTable(float *a, float *b, float *c){ print("\n\r**Benchmark FPU**\n\r"); xil_printf("N_op: %d\r\n", N_OPERATIONS); //Genera números negativos for(int i=0; i<=(N_OPERATIONS/2+1); i++){ a[i]=(float)(i*(-1)); b[N_OPERATIONS-i]=(float)i; c[i]=(float)0.0; } //Genera números positivos for(int i=N_OPERATIONS/2+1; i<=N_OPERATIONS; i++){ a[i]=(float)i; b[N_OPERATIONS-i]=(float)i; c[i]=(float)0.0; } return 0; }
Resultados del benchmark en la FPU MicroBlaze
Los resultados en bruto de este benchmark ejecutándolos en una tarjeta de desarrollo Basys 3 son los siguientes, donde cada unidad es un tick de reloj, en este caso 10ns.
N | MUL | DIV | SQRT | POW |
Sin FPU | ||||
10 | 18267 | 14111 | 17199 | 677285 |
50 | 90277 | 68911 | 81201 | 5522457 |
100 | 180298 | 137411 | 161197 | 9874587 |
200 | 360390 | 274411 | 321393 | 19565452 |
FPU Básica | ||||
10 | 652 | 920 | 17199 | 677285 |
50 | 2732 | 3980 | 81201 | 5522457 |
100 | 5332 | 7805 | 161197 | 9874587 |
200 | 10532 | 15455 | 321393 | 19565452 |
FPU Extendida | ||||
10 | 586 | 854 | 6181 | 114282 |
50 | 2426 | 3674 | 29707 | 928912 |
100 | 4726 | 7199 | 59106 | 1650205 |
200 | 9326 | 14249 | 118106 | 3257179 |
Al promediar y convertir estos datos a FLOPS (Floating point Operations Per Second)
MUL | DIV | SQRT | POW | |
NONE | 55272 | 72270 | 60996 | 1104 |
BASIC | 1784587 | 1229637 | 60996 | 1104 |
FULL | 2006997 | 1331141 | 167156 | 6583 |
Los resultados como mejoras porcentuales respecto a las implementaciones software de estas operaciones se muestran en la siguiente imagen, donde además se puede comparar la diferencia entre la FPU básica y extendida.

Optimizaciones usando la FPU de MicroBlazeTM
No solamente se trata de hardware dedicado cuando se quieren reducir tiempos de ejecución. Un diseño cuidado del código que utiliza dicho hardware es tan importante como el mismo. Se analiza el siguiente ejemplo, el cual es una implementación discretizada del SOGI-PLL del que se habló aquí.
typedef struct Sogi_pll{ //Parámetros SOGI_PLL float k; //SOGI k parameter float wn; //Frequency float ts; //Sample time float b[3]; //Numerador directa float c[3]; //Numerador cuadratura float a[3]; //Denominador directa y cuadratura float kp; //Ganancia proporcional PI float ki; //Ganancia integral PI //Señales SOGI_PLL float ug[3]; //Señal de entrada float yd[3]; //Salida SOGI directa float yq[3]; //Salida SOGI cuadratura float ud[2]; //Transformada Park (d)-q float pi[2]; //Salida PI float theta[2]; //Fase salida PLL float omega[2]; //Frecuencia salida PLL (rad/s) float fg; //Frecuencia señal }SOGI_PLL; void update_sogi_pll(SOGI_PLL *pll, float u_in){ pll->ug[0] = u_in; //Calcula salida del SOGI pll->yd[0] = pll->a[1]*pll->yd[1] + pll->a[2]*pll->yd[2] + pll->b[0]*(pll->ug[0] - pll->ug[2]); pll->yq[0] = pll->a[1]*pll->yq[1] + pll->a[2]*pll->yq[2] + pll->k*pll->c[0]*(pll->ug[0] + 2*pll->ug[1] + pll->ug[2]); //Transformada park pll->ud[0] = pll->yd[0]*cosf(pll->theta[0]) + pll->yq[0]*sinf(pll->theta[0]); //PI pll->pi[0] = pll->pi[1] + pll->kp_pll*pll->ud[0] + (pll->ki_pll*pll->ts-pll->kp_pll)*pll->ud[1]; //Frecuencia de red pll->omega[0] = pll->wn + pll->pi[0]; //Fase de red (integrador) pll->theta[0] = pll->theta[1] + pll->ts*pll->omega[1]; if(pll->theta[0] > (M_TWOPI)){ pll->theta[0] -= M_TWOPI; } pll->fg = pll->omega[0]/M_TWOPI; pll->omega[1] = pll->omega[0]; //Actualiza omega(n-1) pll->theta[1] = pll->theta[0]; //Actualiza theta(n-1) pll->pi[1] = pll->pi[0]; //Actualiza pi_out(n-1) pll->ud[1] = pll->ud[0]; //Actualiza ud(n-1) pll->yd[2] = pll->yd[1]; //Actualiza yd(n-2) pll->yd[1] = pll->yd[0]; //Actualiza yd(n-1) pll->yq[2] = pll->yq[1]; //Actualiza yq(n-2) pll->yq[1] = pll->yq[0]; //Actualiza yq(n-1) pll->ug[2] = pll->ug[1]; //Actualiza u(n-2) pll->ug[1] = pll->ug[0]; //Actualiza u(n-1) }
- Evitar a toda costa divisiones, aunque pueda parecer una operación sencilla tiene una carga computacional elevada, líneas como la 45 deben sustituirse por
- pll->fg = pll->omega[0]*(1/M_TWOPI);
- En la documentación de MicroBlaze se indica que por defecto las constantes con decimales se toman como double, esto es un serio inconveniente no solo por un exceso de precisión en principio no solicitado o innecesario, sino porque la FPU de MicroBlaze solo trabaja con variables en coma flotante de 32bits floats. Se deben castear o específicar todas las constantes que se utilicen, por ejemplo volviendo a la línea 45:
- pll->fg = pll->omega[0]*(float)(1.0/M_TWOPI);
- pll->fg = pll->omega[0]*(1.0F/3.1415F);
- El uso de struct, si en lugar de utilizar esta forma para almacenar y manipular los datos se usan variables «normales», se evita el direccionamiento indirecto, los tiempos de acceso a los mismos se aceleran reduciendo en consecuencia el tiempo de ejecución. Se debe tener en cuenta que esta forma de trabajar puede reducir la legibilidad del código, aumentando el coste de mantenimiento de este.
Resultado de las optimizaciones
El código optimizado aplicando los anteriores puntos quedaría así
//Parámetros SOGI_PLL float k_sogi; //SOGI k parameter float wn; //Frequency float ts; //Sample time float b_pll[3]; //Numerador directa float c_pll[3]; //Numerador cuadratura float a_pll[3]; //Denominator directa y cuadratura float kp_pll; //Ganancia proporcional PI float ki_pll; //Ganancia integral PI //Señales SOGI_PLL float ug[3]; //Señal de entrada float yd[3]; //Salida SOGI directa float yq[3]; //Salida SOGI cuadratura float ud[2]; //Transformada Park (d)-q float pi[2]; //Salida PI float theta[2]; //Fase salida PLL float omega[2]; //Frecuencia salida PLL (rad/s) float fg; //Frecuencia señal float update_sogi_pll(float u_in){ ug[0] = u_in; //Calcula salida del SOGI yd[0] = a_pll[1]*yd[1] + a_pll[2]*yd[2] + b_pll[0]*(ug[0] - ug[2]); yq[0] = a_pll[1]*yq[1] + a_pll[2]*yq[2] + k_sogi*c_pll[0]*(ug[0] + 2.0F*ug[1] + ug[2]); //Transformada park ud[0] = yd[0]*cosf(theta[0]) + yq[0]*sinf(theta[0]); //PI pi[0] = pi[1] + kp_pll*ud[0] + (ki_pll*ts-kp_pll)*ud[1]; //Frecuencia de red omega[0] = wn + pi[0]; //Fase de red (integrador) theta[0] = theta[1] + ts*omega[1]; if(theta[0] > ((float)M_TWOPI)){ theta[0] -= (float)M_TWOPI; } fg = omega[0]*(float)(1.0/M_TWOPI); omega[1] = omega[0]; //Actualiza omega(n-1) theta[1] = theta[0]; //Actualiza theta(n-1) pi[1] = pi[0]; //Actualiza pi_out(n-1) ud[1] = ud[0]; //Actualiza ud(n-1) yd[2] = yd[1]; //Actualiza yd(n-2) yd[1] = yd[0]; //Actualiza yd(n-1) yq[2] = yq[1]; //Actualiza yq(n-2) yq[1] = yq[0]; //Actualiza yq(n-1) ug[2] = ug[1]; //Actualiza u(n-2) ug[1] = ug[0]; //Actualiza u(n-1) return theta[0]; }
Al ejecutar este código en bucle, lo cual sería el funcionamiento normal de un PLL, se tendrían las siguientes mejoras en frecuencia de muestreo al ir aplicando optimizaciones

En la imagen anterior se han aplicado las optimizaciones de izquierda a derecha, por ejemplo al hacer el casting (float) también se evitó la división. La frecuencia de muestreo Final corresponde a realizar todas las optimizaciones sobre todas las líneas de código, antes de eso solo se habían realizado en una sola línea.
Aunque realizar un benchmark en la FPU de MicroBlazeTM no es una prueba real de funcionamiento, si da pistas sobre los puntos clave donde se debe incidir para realizar optimizaciones. Se debe tener en cuenta que un benchmark solo es útil si se conoce lo que mide, por eso, no son recomendables aquellos sin documentar o con el código fuente oculto, aunque esto último es más típico de benchmark para PC.