Diferentes formas (y la más rápida) de calcular senos (y cosenos) en Arduino

9

Estoy usando una placa Arduino Uno para calcular los ángulos de mi sistema (brazo robótico). Los ángulos son en realidad valores de 10 bits (0 a 1023) del ADC, utilizando el rango completo del ADC. Solo voy a estar operando en el primer cuadrante (0 a 90 grados), donde los senos y cosenos son positivos, por lo que no hay problema con los números negativos. Mis dudas se pueden expresar en 3 preguntas:

  1. ¿Cuáles son las diferentes formas de calcular estas funciones trigonométricas en Arduino?

  2. ¿Cuál es la forma más rápida de hacer lo mismo?

  3. Existen las funciones sin () y cos () en el IDE de Arduino, pero ¿cómo las calcula realmente Arduino (ya que usan tablas de búsqueda o aproximaciones, etc.)? Parecen una solución obvia, pero me gustaría saber su implementación real antes de probarlos.

PD: Estoy abierto tanto a la codificación estándar en Arduino IDE como a la codificación de ensamblaje, así como a cualquier otra opción no mencionada. Tampoco tengo problemas con errores y aproximaciones, que son inevitables para un sistema digital; sin embargo, si es posible, sería bueno mencionar el alcance de los posibles errores

Transistor Overlord
fuente
¿Estaría bien con valores aproximados?
sa_leinad
Sí, en realidad, pero me gustaría saber el alcance del error de diferentes métodos. Este no es un producto de precisión sino un proyecto paralelo mío. En realidad, las aproximaciones son inevitables para casi cualquier (si no alguno) sistema digital que implemente una función matemática
Transistor Overlord
Supongo que quieres trabajar en grados. ¿Vas a querer ingresar números enteros o números decimales para el ángulo?
sa_leinad
Grados si. Creo que sería más fácil escribir código y probar si usamos números enteros, así que iría con eso. Pondré información más clara sobre ediciones
Transistor Overlord
1
Por solo 90 grados (enteros), una tabla de búsqueda de 90 entradas sería más rápida y eficiente. De hecho, para los 360 grados completos, puede usar una tabla de búsqueda de 90 entradas. Simplemente léalo al revés para 90-179 e inviértalo para 180-269. Haz las dos cosas por 270-359.
Majenko

Respuestas:

11

Los dos métodos básicos son el cálculo matemático (con polinomios) y las tablas de búsqueda.

La biblioteca matemática de Arduino (libm, parte de avr-libc) usa la primera. Está optimizado para el AVR, ya que está escrito con un lenguaje de ensamblaje 100% y, como tal, es casi imposible seguir lo que está haciendo (también hay cero comentarios). Tenga la seguridad de que serán los cerebros de implementación de flotador puro más optimizados, muy superiores a los nuestros.

Sin embargo, la clave es flotar . Cualquier cosa en el Arduino que involucre punto flotante será pesada en comparación con un entero puro, y dado que solo solicita enteros entre 0 y 90 grados, una tabla de búsqueda simple es, con mucho, el método más simple y eficiente.

Una tabla de 91 valores le dará todo de 0 a 90 inclusive. Sin embargo, si hace que una tabla de valores de coma flotante entre 0.0 y 1.0 todavía tenga la ineficiencia de trabajar con flotadores (no es tan ineficiente como calcular sincon flotadores), por lo que almacenar un valor de punto fijo sería mucho más eficiente.

Eso puede ser tan simple como almacenar el valor multiplicado por 1000, por lo que tiene entre 0 y 1000 en lugar de entre 0.0 y 1.0 (por ejemplo, sin (30) se almacenaría como 500 en lugar de 0.5). Más eficiente sería almacenar los valores como, por ejemplo, un valor Q16 donde cada valor (bit) representa 1/65536 de 1.0. Es más eficiente trabajar con estos valores Q16 (y los Q15, Q1.15, etc.) relacionados, ya que tiene poderes de dos con los que las computadoras adoran trabajar en lugar de poderes de diez con los que odian trabajar.

No olvide también que la sin()función espera radianes, por lo que primero debe convertir sus grados enteros en un valor de radianes de coma flotante, lo que hace que el uso sea sin()aún más ineficiente en comparación con una tabla de búsqueda que puede funcionar directamente con el valor de grados enteros.

Sin embargo, es posible una combinación de los dos escenarios. La interpolación lineal le permitirá obtener una aproximación de un ángulo de coma flotante entre dos enteros. Es tan simple como calcular qué tan lejos se encuentra entre dos puntos en la tabla de búsqueda y crear un promedio ponderado basado en esa distancia de los dos valores. Por ejemplo, si está a 23.6 grados que toma (sintable[23] * (1-0.6)) + (sintable[24] * 0.6). Básicamente, su onda sinusoidal se convierte en una serie de puntos discretos unidos por líneas rectas. Cambias la precisión por la velocidad.

Majenko
fuente
Hace un tiempo escribí una biblioteca que usaba un polinomio de Taylor para sin / cos que era más rápido que la biblioteca. Dado, estaba usando radianes de coma flotante como entrada para ambos.
tuskiomi
8

Aquí hay algunas buenas respuestas, pero quería agregar un método que aún no se ha mencionado, uno muy adecuado para calcular funciones trigonométricas en sistemas embebidos, y esa es la técnica de CORDIC Wiki Entry Here Puede calcular funciones trigonométricas utilizando solo cambios y agrega y una pequeña tabla de consulta.

Aquí hay un ejemplo crudo en C. En efecto, implementa la función atan2 () de las bibliotecas C usando CORDIC (es decir, encuentra un ángulo dado dos componentes ortogonales). Utiliza punto flotante, pero se puede adaptar para usar con aritmética de punto fijo.

/*
 * Simple example of using the CORDIC algorithm.
 */

#include <stdio.h>
#include <math.h>

#define CORDIC_TABLE_SIZE  16

double cordic_table[CORDIC_TABLE_SIZE];

void init_table(void);
double angle(double I, double Q);

/*
 * Given a sine and cosine component of an
 * angle, compute the angle using the CORIDC
 * algoritm.
 */
double angle(double I, double Q)
{
    int L;
    double K = 1;
    double angle_acc = 0;
    double tmp_I;

    if (I < 0) {
        /* rotate by an initial +/- 90 degrees */
        tmp_I = I;
        if (Q > 0.0) {
            I = Q;           /* subtract 90 degrees */
            Q = -tmp_I;
            angle_acc = -90;
        } else {
            I = -Q;          /* add 90 degrees */
            Q = tmp_I;
            angle_acc = 90;
        }
    } else {
        angle_acc = 0;
    }

    /* rotate using "1 + jK" factors */
    for (L = 0, K = 1; L <= CORDIC_TABLE_SIZE; L++) {
        tmp_I = I;
        if (Q >= 0.0) {
            /* angle is positive: do negative roation */
            I += Q * K;
            Q -= tmp_I * K;
            angle_acc -= cordic_table[L];
        } else {
            /* angle is negative: do positive rotation */
            I -= Q * K;
            Q += tmp_I * K;
            angle_acc += cordic_table[L];
        }
        K /= 2.0;
    }
    return -angle_acc;
}

void init_table(void)
{
    int i;
    double K = 1;

    for (i = 0; i < CORDIC_TABLE_SIZE; i++) {
        cordic_table[i] = 180 * atan(K) / M_PI;
        K /= 2.0;
    }
}
int main(int argc, char **argv)
{
    double I, Q, A, Ar, R, Ac;

    init_table();

    printf("# Angle,    CORDIC Angle,  Error\n");
    for (A = 0; A < 90.0; A += 0.5) {

        Ar = A * M_PI / 180; /* convert to radians for C's sin & cos fn's */

        R = 5;  // Arbitrary radius

        I = R * cos(Ar);
        Q = R * sin(Ar);

        Ac = angle(I, Q);
        printf("%9f, %9f,   %12.4e\n", A, Ac, Ac-A);
    }
    return 0;
}

Pero primero pruebe las funciones trigonométricas nativas de Arduino; de todos modos, podrían ser lo suficientemente rápidas.

Halzephron
fuente
1
He tomado un enfoque similar en el pasado, en stm8. requiere dos pasos: 1) calcular sin (x) y cos (x) a partir de sin (2x), y luego 2) calcular sin (x +/- x / 2) a partir de sin (x), sin (x / 2) , cos (x) y cos (x / 2) -> a través de la iteración puedes acercarte a tu objetivo. en mi caso, comencé con 45 grados (0.707) y me abrí paso hacia el objetivo. es considerablemente más lento que la función estándar IAR sin ().
dannyf
7

He estado jugando un poco con la computación de senos y cosenos en el Arduino usando aproximaciones polinómicas de punto fijo. Aquí están mis medidas de tiempo de ejecución promedio y el peor de los casos, en comparación con el estándar cos()y sin()de avr-libc:

function    max error   cycles   time
-----------------------------------------
cos_fix()   9.53e-5     108.25    6.77 µs
sin_fix()   9.53e-5     110.25    6.89 µs
cos()       2.98e-8     1720.8   107.5 µs
sin()       2.98e-8     1725.1   107.8 µs

Se basa en un polinomio de sexto grado calculado con solo 4 multiplicaciones. Las multiplicaciones en sí mismas se realizan en conjunto, ya que descubrí que gcc las implementó de manera ineficiente. Los ángulos se expresan uint16_ten unidades de 1/65536 de una revolución, lo que hace que la aritmética de los ángulos funcione naturalmente módulo una revolución.

Si cree que esto puede adaptarse a su factura, aquí está el código: trigonometría de punto fijo . Lo siento, todavía no traduje esta página, que está en francés, pero puedes entender las ecuaciones, y el código (nombres de variables, comentarios ...) está en inglés.


Editar : dado que el servidor parece haberse desvanecido, aquí hay información sobre las aproximaciones que encontré.

Quería escribir ángulos en punto fijo binario, en unidades de cuadrantes (o, equivalentemente, en turnos). Y también quería usar un polinomio uniforme, ya que estos son más eficientes para calcular que los polinomios arbitrarios. En otras palabras, quería un polinomio P () tal que

cos (π / 2 x) ≈ P (x 2 ) para x ∈ [0,1]

También requería que la aproximación fuera exacta en ambos extremos del intervalo, para asegurar que cos (0) = 1 y cos (π / 2) = 0. Estas restricciones llevaron a la forma

P (u) = (1 - u) (1 + uQ (u))

donde Q () es un polinomio arbitrario.

A continuación, busqué la mejor solución en función del grado de Q () y encontré esto:

        Q(u)              degree of P(x²)  max error
─────────────────────────┼─────────────────┼──────────
          0                       2         5.60e-2
       0.224                     4         9.20e-4
0.2335216 + 0.0190963 u          6         9.20e-6

La elección entre las soluciones anteriores es una compensación de velocidad / precisión. La tercera solución ofrece más precisión de la que se puede lograr con 16 bits, y es la que elegí para la implementación de 16 bits.

Edgar Bonet
fuente
2
Eso es asombroso, @Edgar.
SDsolar
¿Qué hiciste para encontrar el polinomio?
TLW
@TLW: necesitaba que tuviera algunas propiedades "agradables" (por ejemplo, cos (0) = 1), que se limitaba a la forma (1 − x²) (1 + x²Q (x²)), donde Q (u) es un arbitrario polinomio (se explica en la página). Tomé un Q de primer grado (solo 2 coeficientes), encontré los coeficientes aproximados por ajuste, luego ajusté a mano la optimización por prueba y error.
Edgar Bonet
@EdgarBonet: interesante. Tenga en cuenta que esa página no se carga para mí, aunque el almacenamiento en caché funciona. ¿Podría agregar el polinomio utilizado a esta respuesta?
TLW
@TLW: agregó eso a la respuesta.
Edgar Bonet
4

Podría crear un par de funciones que usen aproximación lineal para determinar el sin () y el cos () de un ángulo particular.

Estoy pensando en algo como esto: para cada uno he dividido la representación gráfica de sin () y cos () en 3 secciones y he hecho una aproximación lineal de esa sección.
aproximación lineal

Su función idealmente primero verificaría que el rango del ángel esté entre 0 y 90.
Luego usaría una ifelsedeclaración para determinar a cuál de las 3 secciones pertenece y luego realiza el cálculo lineal correspondiente (es decir output = mX + c)

sa_leinad
fuente
¿No implicará esto la multiplicación de punto flotante?
Transistor Overlord
1
No necesariamente. Podría tenerlo para que la salida se ajuste entre 0-100 en lugar de 0-1. De esta manera, se trata de números enteros, no de punto flotante. Nota: 100 fue arbitrario. No hay razón para que no pueda escalar la salida entre 0-128 o 0-512 o 0-1000 o 0-1024. Al usar un múltiplo de 2, solo necesita hacer cambios correctos para escalar el resultado hacia abajo.
sa_leinad
Bastante inteligente, @sa_leinad. Voto a favor. Recuerdo haber hecho esto cuando trabajaba con polarización de transistores.
SDsolar
4

Busqué a otras personas que tenían aproximadamente cos () y sin () y encontré esta respuesta:

La respuesta de dtb a "Fast Sin / Cos usando una matriz de traducción precalculada"

Básicamente, calculó que la función math.sin () de la biblioteca matemática era más rápida que usar una tabla de valores de búsqueda. Pero por lo que puedo decir, esto se calculó en una PC.

Arduino tiene una biblioteca matemática incluida que puede calcular sin () y cos ().

sa_leinad
fuente
1
Las PC tienen FPU incorporadas que lo hacen rápido. Arduino no lo hace, y eso lo hace lento.
Majenko
La respuesta también es para C # que hace cosas como la comprobación de los límites de la matriz.
Michael
3

Una tabla de búsqueda será la forma más rápida de encontrar senos. Y si se siente cómodo computando con números de punto fijo (enteros cuyo punto binario está en algún lugar que no sea a la derecha del bit-0), sus cálculos adicionales con los senos también serán mucho más rápidos. Esa tabla puede ser una tabla de palabras, posiblemente en Flash para ahorrar espacio en la RAM. Tenga en cuenta que en sus matemáticas puede necesitar usar largos para obtener resultados intermedios grandes.

JRobert
fuente
1

generalmente, tabla de búsqueda> aproximación -> cálculo. ram> flash. entero> punto fijo> punto flotante. precálculo> cálculo en tiempo real. duplicación (seno a coseno o coseno a seno) versus cálculo / búsqueda real ...

cada uno tiene sus ventajas y desventajas.

puede hacer todo tipo de combinaciones para ver cuál funciona mejor para su aplicación.

editar: hice una comprobación rápida. usando una salida de enteros de 8 bits, calcular 1024 valores sin con la tabla de búsqueda toma 0.6ms, y 133ms con flotantes, o 200x más lento.

dannyf
fuente
1

Tenía una pregunta similar a OP. Quería hacer una tabla LUT para calcular el primer cuadrante de la función seno como enteros sin signo de 16 bits a partir de 0x8000 a 0xffff. Y terminé escribiendo esto por diversión y ganancias. Nota: Esto funcionaría de manera más eficiente si usara declaraciones 'if'. Además, no es muy preciso, pero sería lo suficientemente preciso para una onda sinusoidal en un sintetizador de sonido

void sin_lut_ctor(){

//Make a Look Up Table for 511 terms of the sine function.
//Plugin in some polynomials to do some magic
//and you get an aproximation for sines up to π/2.
//

//All sines yonder π/2 can be derived with math

const uint16_t uLut_d = 0x0200; //maximum LUT depth for π/2 terms. 
uint16_t uLut_0[uLut_d];        //The LUT itself.
//Put the 2 above before your void setup() as global variables.
//This coefficients will only work for uLut_d = 511.

uint16_t arna_poly_0 = 0x000a; // 11
uint16_t arna_poly_1 = 0x0001; // 1
uint16_t arna_poly_2 = 0x0007; // 7
uint16_t arna_poly_3 = 0x0001; // 1   Precalculated Polynomials
uint16_t arna_poly_4 = 0x0001; // 1   
uint16_t arna_poly_5 = 0x0007; // 7
uint16_t arna_poly_6 = 0x0002; // 2
uint16_t arna_poly_7 = 0x0001; // 1

uint16_t Imm_UI_0 = 0x0001;              //  Itterator
uint16_t Imm_UI_1 = 0x007c;              //  An incrementor that decreases in time

uint16_t Imm_UI_2 = 0x0000;              //  
uint16_t Imm_UI_3 = 0x0000;              //              
uint16_t Imm_UI_4 = 0x0000;              //
uint16_t Imm_UI_5 = 0x0000;              //
uint16_t Imm_UI_6 = 0x0000;              //  Temporary variables
uint16_t Imm_UI_7 = 0x0000;              //
uint16_t Imm_UI_8 = 0x0000;              //
uint16_t Imm_UI_9 = 0x0000;              //
uint16_t Imm_UI_A = 0x0000;
uint16_t Imm_UI_B = 0x0000;

uint16_t Imm_UI_A = uLut_d - 0x0001;     //  510

uLut_0[0x0000] = 0x8000;        //Assume that the middle point is 32768 (0x8000 hex)
while (Imm_UI_0 < Imm_UI_A) //Construct a quarter of the sine table
  {
Imm_UI_2++;                                   //Increase temporary variable by 1

Imm_UI_B = Imm_UI_2 / arna_coeff_0;           //Divide it with the first coefficient (note: integer division)
Imm_UI_3 += Imm_UI_B;                         //Increase the next temporary value if the first one has increased up to the 1st coefficient
Imm_UI_1 -= Imm_UI_B;                         //Decrease the incrementor if this is the case
Imm_UI_2 *= 0x001 - Imm_UI_B;                 //Set the first temporary variable back to 0

Imm_UI_B = Imm_UI_3 / arna_poly_1;           //Do the same thing as before with the next set of temporary variables
Imm_UI_4 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_3 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_4 / arna_poly_2;           //And again... and again... you get the idea.
Imm_UI_5 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_4 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_5 / arna_poly_3;
Imm_UI_6 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_5 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_6 / arna_poly_4;
Imm_UI_7 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_6 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_7 / arna_poly_5;
Imm_UI_8 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_7 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_8 / arna_poly_6;
Imm_UI_9 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_8 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_9 / arna_poly_7          //the last set won't need to increment a next variable so skip the step where you would increase it.
Imm_UI_1 -= Imm_UI_B;
Imm_UI_9 *= 1 - Imm_UI_B;

uLut_0[Imm_UI_0] = (uLut_0[Imm_UI_0 - 0x0001] + Imm_UI_1); //Set the current value as the previous one increased by our incrementor
Imm_UI_0++;              //Increase the itterator
  }   
  uLut_0[Imm_UI_A] = 0xffff; //Lastly, set the last value to 0xffff

  //And there you have it. A sine table with only one if statement (a while loop)
}

Ahora para recuperar los valores, use esta función. Acepta un valor de 0x0000 a 0x0800 y devuelve el valor apropiado de la LUT

uint16_t lu_sin(uint16_t lu_val0)
{
  //Get a value from 0x0000 to 0x0800. Return an appropriate sin(value)
  Imm_UI_0 = lu_val0/0x0200; //determine quadrant
  Imm_UI_1 = lu_val0%0x0200; //Get which value
  if (Imm_UI_0 == 0x0000)
  {
    return uLut_0[Imm_UI_1];
  }
  if (Imm_UI_0 == 0x0001)
  {
    return uLut_0[0x01ff - Imm_UI_1];
  }
  if (Imm_UI_0 == 0x0002)
  {
    return 0xffff - uLut_0[Imm_UI_1];
  }
  if (Imm_UI_0 == 0x0003)
  {
    return 0xffff - uLut_0[0x01ff - Imm_UI_1];
  }
}// I'm using if statements here but similarly to the above code block, 
 //you can do without. just with integer divisions and modulos

Recuerde, este no es el enfoque más eficiente para esta tarea, simplemente no podía entender cómo hacer series de Taylor para dar resultados en el rango apropiado.

Arnadath
fuente
Su código no se compila: Imm_UI_Ase declara dos veces, ;faltan algunas declaraciones de variables y uLut_0deben ser globales. Con las correcciones necesarias, lu_sin()es rápido (entre 27 y 42 ciclos de CPU) pero muy impreciso (error máximo ≈ 5.04e-2). No puedo entender el punto de estos "polinomios arnadathianos": parece un cálculo bastante pesado, pero el resultado es casi tan malo como una simple aproximación cuadrática. El método también tiene un gran costo de memoria. Sería mucho mejor calcular la tabla en su PC y ponerla en el código fuente como una PROGMEMmatriz.
Edgar Bonet
1

Solo por el gusto de hacerlo, y para demostrar que se puede hacer, terminé una rutina de ensamblaje AVR para calcular los resultados de sin (x) en 24 bits (3 bytes) con un bit de error. El ángulo de entrada está en grados con un dígito decimal, de 000 a 900 (0 ~ 90.0) solo para el primer cuadrante. Utiliza menos de 210 instrucciones AVR y funciona con un promedio de 212 microsegundos, que varía de 211us (ángulo = 001) a 213us (ángulo = 899).

Tomó varios días hacerlo todo, más de 10 días (horas libres) simplemente pensando en el mejor algoritmo para el cálculo, considerando el microcontrolador AVR, sin punto flotante, eliminando todas las divisiones posibles. Lo que llevó más tiempo fue hacer los valores de incremento correctos para los enteros, para tener una buena precisión necesita aumentar los valores de 1e-8 a enteros binarios 2 ^ 28 o más. Una vez que se encontraron todos los errores culpables de precisión y redondeo, aumentaron su resolución de cálculo en 2 ^ 8 o 2 ^ 16 adicionales, se obtuvieron los mejores resultados. Primero simulé todos los cálculos en Excel teniendo cuidado de tener todos los valores como Int (x) o Round (x, 0) para representar exactamente el procesamiento central de AVR.

Por ejemplo, en el algoritmo el ángulo debe estar en radianes, la entrada está en grados para facilitar al usuario. Para convertir grados a radianes, la fórmula trivial es rad = grados * PI / 180, parece agradable y fácil, pero no lo es, PI es un número infinito: si se usan pocos dígitos creará errores en la salida, la división por 180 requiere La manipulación de bits AVR ya que no tiene instrucción de división, y más que eso, el resultado requeriría coma flotante ya que involucra números muy por debajo del entero 1. Por ejemplo, Radián de 1 ° (grado) es 0.017453293. Dado que PI y 180 son constantes, ¿por qué no invertir esta cosa para una multiplicación simple? PI / 180 = 0.017453293, multiplíquelo por 2 ^ 32 y resulta como una constante 74961320 (0x0477D1A8), multiplique este número por su ángulo en grados, digamos 900 para 90 ° y desplazarlo 4 bits a la derecha (÷ 16) para obtener 4216574250 (0xFB53D12A), es decir, los radianes de 90 ° con 2 ^ 28 de expansión, caben en 4 bytes, sin una sola división (excepto los 4 poco desplazamiento a la derecha). En cierto modo, el error incluido en dicho truco es menor que 2 ^ -27.

Por lo tanto, todos los cálculos adicionales deben recordar que es 2 ^ 28 más alto y que se ha solucionado. Debe dividir los resultados sobre la marcha por 16, 256 o incluso 65536 solo para evitar que use bytes de hambre innecesarios que no ayudarían a la resolución. Ese fue un trabajo minucioso, solo encontrar la cantidad mínima de bits en cada resultado de cálculo, manteniendo la precisión de los resultados alrededor de 24 bits. Cada uno de los varios cálculos se realizó en prueba / error con bits más altos o más bajos en el flujo de Excel, observando la cantidad general de bits de error en el resultado en un gráfico que muestra 0-90 ° con una macro que ejecuta el código 900 veces, una vez por décima de grado Ese enfoque "visual" de Excel fue una herramienta que creé, ayudó mucho a encontrar la mejor solución para cada parte del código.

Por ejemplo, al redondear este resultado de cálculo en particular 13248737.51 a 13248738 o simplemente perder los decimales "0.51", ¿cuánto afectará la precisión del resultado final para todas las 900 pruebas de ángulos de entrada (00.1 ~ 90.0)?

Pude mantener el animal contenido dentro de 32 bits (4 bytes) en cada cálculo, y terminé con la magia para obtener precisión dentro de los 23 bits del resultado. Al verificar los 3 bytes completos del resultado, el error es ± 1 LSB, pendiente.

El usuario puede obtener uno, dos o tres bytes del resultado para sus propios requisitos de precisión. Por supuesto, si solo un byte es suficiente, recomendaría usar una sola tabla sin 256 bytes y usar la instrucción AVR 'LPM' para obtenerla.

Una vez que tuve la secuencia de Excel funcionando sin problemas y ordenada, la traducción final del ensamblaje de Excel a AVR tomó menos de 2 horas, como de costumbre, debería pensar más primero, trabajar menos después.

En ese momento pude exprimir aún más y reducir el uso de registros. El código real (no final) utiliza alrededor de 205 instrucciones (~ 410 bytes), ejecuta un cálculo sin (x) en promedio de 212us, reloj a 16MHz. A esa velocidad puede calcular más de 4700 sen (x) por segundo. No es importante, pero puede ejecutar una onda sinusoidal precisa de hasta 4700Hz con 23 bits de precisión y resolución, sin ninguna tabla de búsqueda.

El algoritmo base se basa en la serie Taylor para sin (x), pero modificó mucho para adaptarse a mis intenciones con el microcontrolador AVR y la precisión en mente.

Incluso si usar una tabla de 2700 bytes (900 entradas * 3 bytes) sería atractivo para la velocidad, ¿cuál es la experiencia divertida o de aprendizaje en eso? Por supuesto, también se consideró el enfoque CORDIC, tal vez más tarde, el punto aquí es presionar a Taylor en el núcleo AVR y tomar agua de una roca seca.

Me pregunto si Arduino "sin (78.9 °)" puede ejecutar Processing (C ++) con 23 bits de precisión en menos de 212us y el código necesario más pequeño que 205 instrucciones. Puede ser si C ++ usa CORDIC. Los bocetos de Arduino pueden importar código de ensamblaje.

No tiene sentido publicar el código aquí, más tarde editaré esta publicación para incluir un enlace web, posiblemente en mi blog en esta url . El blog está principalmente en portugués.

Esta aventura de hobby sin dinero fue interesante, superando los límites del motor AVR de casi 16MIPS a 16MHz, sin instrucción de división, multiplicación solo en 8x8 bits. Permite calcular sin (x), cos (x) [= sin (900-x)] y tan (x) [= sin (x) / sin (900-x)].

Por encima de todo, esto ayudó a mantener mi cerebro de 63 años pulido y engrasado. Cuando los adolescentes dicen que las 'personas mayores' no saben nada sobre tecnología, yo respondo "piensa de nuevo, ¿quién crees que creó las bases para todo lo que disfrutas hoy?".

Salud

Labio Wagner
fuente
¡Agradable! Algunas observaciones: 1. La sin()función estándar tiene aproximadamente la misma precisión que la suya y es dos veces más rápida. También se basa en un polinomio. 2. Si un ángulo arbitrario tiene que redondearse al múltiplo más cercano de 0.1 °, esto puede conducir a un error de redondeo tan alto como 8.7e-4, lo que niega el beneficio de la precisión de 23 bits. 3. ¿Te importaría compartir tu polinomio?
Edgar Bonet
1

Como otros han mencionado, las tablas de búsqueda son el camino a seguir si quieres velocidad. Recientemente he estado investigando el cálculo de las funciones trigonométricas en un ATtiny85 para el uso de promedios vectoriales rápidos (viento en mi caso). Siempre hay compensaciones ... para mí solo necesitaba una resolución angular de 1 grado, por lo que una tabla de búsqueda de 360 ​​int (escala -32767 a 32767, solo trabajando con int) era la mejor manera de hacerlo. Recuperar el seno es solo cuestión de proporcionar un índice 0-359 ... ¡muy rápido! Algunos números de mis pruebas:

Tiempo de búsqueda de FLASH (us): 0.99 (tabla almacenada usando PROGMEM)

Tiempo de búsqueda de RAM (us): 0.69 (tabla en RAM)

Tiempo de Lib (us): 122.31 (Usando Arduino Lib)

Tenga en cuenta que estos son promedios en una muestra de 360 ​​puntos para cada uno. Las pruebas se realizaron en un nano.

acicuc
fuente