Diversi modi (e il più veloce) per calcolare seni (e coseni) in Arduino


9

Sto usando una scheda Arduino Uno per calcolare gli angoli del mio sistema (braccio robotico). Gli angoli sono in realtà valori di 10 bit (da 0 a 1023) dall'ADC, usando l'intera gamma dell'ADC. Opererò solo nel 1 ° quadrante (da 0 a 90 gradi), dove sia i seni che i coseni sono positivi, quindi non ci sono problemi con i numeri negativi. I miei dubbi possono essere espressi in 3 domande:

  1. Quali sono i diversi modi per calcolare queste funzioni trigonometriche su Arduino?

  2. Qual è il modo più veloce per fare lo stesso?

  3. Ci sono le funzioni sin () e cos () nell'IDE di Arduino, ma in che modo Arduino le calcola effettivamente (come usano le tabelle di ricerca, o approssimazioni ecc.)? Sembrano una soluzione ovvia, ma vorrei conoscere la loro effettiva implementazione prima di provarli.

PS: Sono aperto sia alla codifica standard sull'IDE Arduino sia alla codifica dell'assembly, nonché a qualsiasi altra opzione non menzionata. Inoltre non ho problemi con errori e approssimazioni, che sono inevitabili per un sistema digitale; tuttavia, se possibile, sarebbe bene menzionare l'entità dei possibili errori


Saresti d'accordo con valori approssimativi?
sa_leinad,

Sì, in realtà, ma vorrei conoscere l'entità dell'errore di diversi metodi. Questo non è un prodotto di precisione ma un mio progetto collaterale. In realtà le approssimazioni sono inevitabili per quasi tutti i sistemi digitali (se non nessuno) che implementano una funzione matematica
Transistor Overlord

Presumo che tu voglia lavorare in gradi. Vuoi inserire numeri interi o decimali per l'angolo?
sa_leinad,

Gradi si. Penso che sarebbe più facile scrivere codice e testare se usiamo numeri interi, quindi andrei con quello. Fornirò informazioni più chiare sulle modifiche
Transistor Overlord

1
Per soli 90 (interi) gradi una tabella di ricerca a 90 voci sarebbe più veloce ed efficiente. In effetti per tutti i 360 gradi è possibile utilizzare una tabella di ricerca a 90 voci. Basta leggerlo all'indietro per 90-179 e invertirlo per 180-269. Fai entrambe le cose per 270-359.
Majenko

Risposte:


11

I due metodi di base sono il calcolo matematico (con polinomi) e le tabelle di ricerca.

La libreria matematica di Arduino (libm, parte di avr-libc) usa la prima. È ottimizzato per l'AVR in quanto è scritto con un linguaggio assembly al 100% e come tale è quasi impossibile seguire ciò che sta facendo (non ci sono anche commenti). State tranquilli, anche se saranno i cervelli più ottimizzati per l'implementazione in puro float di gran lunga superiori ai nostri.

Tuttavia, la chiave è float . Qualsiasi cosa su Arduino che coinvolga il virgola mobile sarà pesante rispetto al numero intero puro, e poiché si richiedono solo numeri interi compresi tra 0 e 90 gradi, una semplice tabella di ricerca è di gran lunga il metodo più semplice ed efficiente.

Una tabella di 91 valori ti darà tutto da 0 a 90 inclusi. Tuttavia, se si considera che una tabella di valori in virgola mobile tra 0,0 e 1,0 si ha ancora l'inefficienza di lavorare con i float (garantito non inefficiente come il calcolo sincon float), quindi memorizzare un valore in virgola fissa invece sarebbe molto più efficiente.

Potrebbe essere semplice come memorizzare il valore moltiplicato per 1000, quindi hai tra 0 e 1000 anziché tra 0,0 e 1,0 (ad esempio sin (30) verrebbe memorizzato come 500 anziché 0,5). Più efficiente sarebbe memorizzare i valori come, ad esempio, un valore Q16 in cui ciascun valore (bit) rappresenta 1/65536 ° di 1.0. Questi valori Q16 (e i relativi Q15, Q1.15, ecc.) Sono più efficienti con cui lavorare poiché disponi di due poteri con cui i computer amano lavorare anziché di dieci con cui odiano lavorare.

Non dimenticare inoltre che la sin()funzione prevede radianti, quindi devi prima convertire i tuoi gradi interi in un valore in radianti a virgola mobile, rendendo l'utilizzo di un valore sin()ancora più inefficiente rispetto a una tabella di ricerca che può lavorare direttamente con il valore dei gradi interi.

Una combinazione dei due scenari, tuttavia, è possibile. L'interpolazione lineare consente di ottenere un'approssimazione di un angolo in virgola mobile tra due numeri interi. È semplice come calcolare la distanza tra due punti nella tabella di ricerca e creare una media ponderata in base a quella distanza dei due valori. Ad esempio, se sei a 23.6 gradi prendi (sintable[23] * (1-0.6)) + (sintable[24] * 0.6). Fondamentalmente la tua onda sinusoidale diventa una serie di punti discreti uniti da linee rette. Scambia la precisione con la velocità.


Ho scritto una libreria qualche tempo fa che utilizzava un polinomio di Taylor per sin / cos che era più veloce della libreria. Dato, stavo usando i radianti a virgola mobile come input per entrambi.
tuskiomi,

8

Ci sono alcune buone risposte qui, ma volevo aggiungere un metodo che non è stato ancora menzionato, molto adatto al calcolo di funzioni trigonometriche su sistemi embedded, e questa è la tecnica CORDIC Wiki Entry Here Può calcolare le funzioni di trigma usando solo i turni e aggiunge e una piccola tabella di ricerca.

Ecco un esempio approssimativo in C. In effetti, implementa la funzione atan2 () delle librerie C usando CORDIC (cioè trova un angolo dato due componenti ortogonali). Usa il virgola mobile, ma può essere adattato per l'uso con l'aritmetica a punto fisso.

/*
 * 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;
}

Ma prima prova le funzioni trig native di Arduino: potrebbero essere comunque abbastanza veloci.


1
Ho adottato un approccio simile in passato, su stm8. richiede due passaggi: 1) calcola sin (x) e cos (x) da sin (2x), quindi 2) calcola sin (x +/- x / 2) da sin (x), sin (x / 2) , cos (x) e cos (x / 2) -> tramite iterazione puoi avvicinarti al tuo obiettivo. nel mio caso, ho iniziato con 45 gradi (0,707) e mi sono avvicinato all'obiettivo. è considerevolmente più lento della funzione IAR sin () standard.
Dannyf,

7

Ho giocato un po 'con il calcolo di seno e coseno sull'Arduino usando approssimazioni polinomiali a punto fisso. Ecco le mie misurazioni del tempo medio di esecuzione e dell'errore nel caso peggiore, rispetto allo standard cos()e sin()da 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

Si basa su un polinomio di 6 ° grado calcolato con solo 4 moltiplicazioni. Le stesse moltiplicazioni vengono eseguite in assembly, poiché ho scoperto che gcc le ha implementate in modo inefficiente. Gli angoli sono espressi come uint16_tin unità di 1/65536 di una rivoluzione, il che fa sì che l'aritmetica degli angoli funzioni naturalmente modulo una rivoluzione.

Se ritieni che ciò possa adattarsi al tuo conto, ecco il codice: trigonometria a virgola fissa . Spiacenti, non ho ancora tradotto questa pagina, che è in francese, ma puoi capire le equazioni e il codice (nomi delle variabili, commenti ...) è in inglese.


Modifica : poiché il server sembra essere svanito, ecco alcune informazioni sulle approssimazioni che ho trovato.

Volevo scrivere angoli in virgola fissa binaria, in unità di quadranti (o, equivalentemente, a turno). E volevo anche usare un polinomio uniforme, poiché questi sono più efficienti da calcolare rispetto ai polinomi arbitrari. In altre parole, volevo un polinomio P () tale che

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

Ho anche richiesto che l'approssimazione fosse esatta ad entrambe le estremità dell'intervallo, per garantire che cos (0) = 1 e cos (π / 2) = 0. Questi vincoli hanno portato alla forma

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

dove Q () è un polinomio arbitrario.

Successivamente, ho cercato la soluzione migliore in funzione del grado di Q () e ho trovato questo:

        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 scelta tra le soluzioni sopra è un compromesso di velocità / precisione. La terza soluzione offre una precisione maggiore rispetto a quella ottenibile con 16 bit, ed è quella che ho scelto per l'implementazione a 16 bit.


2
È fantastico, @Edgar.
SDsolar,

Cosa hai fatto per trovare il polinomio?
TLW,

@TLW: ho richiesto che avesse alcune proprietà "belle" (es. Cos (0) = 1), che si limitava alla forma (1 − x²) (1 + x²Q (x²)), dove Q (u) è un arbitrario polinomio (è spiegato nella pagina). Ho preso una Q di primo grado (solo 2 coefficienti), ho trovato i coefficienti approssimativi per adattamento, quindi ho ottimizzato manualmente l'ottimizzazione per tentativi ed errori.
Edgar Bonet,

@EdgarBonet - interessante. Nota che quella pagina non viene caricata per me, sebbene funzioni nella cache. Potresti per favore aggiungere il polinomio usato a questa risposta?
TLW,

@TLW: aggiunto questo alla risposta.
Edgar Bonet,

4

È possibile creare un paio di funzioni che utilizzano l'approssimazione lineare per determinare sin () e cos () di un angolo particolare.

Sto pensando a qualcosa del genere: per ciascuno di essi ho suddiviso la rappresentazione grafica di sin () e cos () in 3 sezioni e ho fatto un'approssimazione lineare di quella sezione.
approssimazione lineare

Idealmente, la tua funzione verificherebbe innanzitutto che l'intervallo dell'angelo sia compreso tra 0 e 90.
Quindi utilizzerà ifelseun'istruzione per determinare a quale delle 3 sezioni appartiene e quindi esegue il calcolo lineare corrispondente (cioè output = mX + c)


Ciò non implica una moltiplicazione in virgola mobile?
Transistor Overlord

1
Non necessariamente. Potresti averlo in modo che l'output sia ridimensionato tra 0-100 anziché 0-1. In questo modo hai a che fare con numeri interi, non in virgola mobile. Nota: 100 era arbitrario. Non vi è alcun motivo per cui non è possibile ridimensionare l'output tra 0-128 o 0-512 o 0-1000 o 0-1024. Utilizzando un multiplo di 2, è sufficiente eseguire i turni giusti per ridimensionare il risultato.
sa_leinad,

Abbastanza intelligente, @sa_leinad. Upvote. Ricordo di averlo fatto lavorando con la polarizzazione dei transistor.
SDsolar,

4

Ho cercato altre persone che avevano approssimato cos () e sin () e ho trovato questa risposta:

La risposta di dtb a "Fast Sin / Cos usando un array di traduzione pre-calcolato"

Fondamentalmente ha calcolato che la funzione math.sin () dalla libreria matematica era più veloce dell'uso di una tabella di valori di ricerca. Ma da quello che posso dire, questo è stato calcolato su un PC.

Arduino ha una libreria matematica inclusa che può calcolare sin () e cos ().


1
Nei PC sono incorporate FPU che lo rendono veloce. Arduino no, e questo lo rende lento.
Majenko

La risposta è anche per C # che fa cose come il controllo dei limiti di array.
Michael,

3

Una tabella di ricerca sarà il modo più veloce per trovare i seni. E se sei a tuo agio nel calcolo con numeri a virgola fissa (numeri interi il cui punto binario si trova in un punto diverso da quello a destra del bit-0), anche i tuoi ulteriori calcoli con i seni saranno molto più veloci. Quella tabella può quindi essere una tabella di parole, possibilmente in Flash per risparmiare spazio RAM. Si noti che in matematica potrebbe essere necessario utilizzare longs per risultati intermedi di grandi dimensioni.


1

in generale, tabella di consultazione> approssimazione -> calcolo. ram> flash. numero intero> punto fisso> virgola mobile. pre-calcolo> calcolo in tempo reale. mirroring (da seno a coseno o da coseno a seno) rispetto al calcolo / ricerca effettivi ....

ognuno ha i suoi vantaggi e svantaggi.

puoi creare ogni tipo di combinazione per vedere quale funziona meglio per la tua applicazione.

modifica: ho fatto un controllo rapido. utilizzando l'output intero a 8 bit, il calcolo di 1024 valori sin con la tabella di ricerca richiede 0,6 ms e 133 ms con floater o 200 volte più lento.


1

Ho avuto una domanda simile a OP. Volevo creare una tabella LUT per calcolare il primo quadrante della funzione seno come numeri interi a 16 bit senza segno a partire da 0x8000 a 0xffff. E ho finito per scrivere questo per divertimento e profitto. Nota: questo funzionerebbe in modo più efficiente se usassi le istruzioni "if". Inoltre non è molto preciso, ma sarebbe abbastanza preciso per un'onda sinusoidale in un sintetizzatore audio

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)
}

Ora per recuperare i valori, utilizzare questa funzione. Accetta un valore compreso tra 0x0000 e 0x0800 e restituisce il valore appropriato da 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

Ricorda, questo non è l'approccio più efficiente a questo compito, non riuscivo proprio a capire come realizzare serie Taylor per fornire risultati nella gamma appropriata.


Il codice non viene compilato: Imm_UI_Aviene dichiarato due volte, ;mancano alcune dichiarazioni di variabili e uLut_0devono essere globali. Con le correzioni necessarie, lu_sin()è veloce (tra 27 e 42 cicli della CPU) ma molto impreciso (errore massimo ≈ 5,04e-2). Non riesco a capire il punto di questi "polinomi Arnadathian": sembra un calcolo abbastanza pesante, eppure il risultato è quasi negativo come una semplice approssimazione quadratica. Il metodo ha anche un enorme costo di memoria. Sarebbe molto meglio calcolare la tabella sul PC e inserirla nel codice sorgente come un PROGMEMarray.
Edgar Bonet,

1

Solo per divertirmi e per dimostrarlo, ho terminato una routine di assemblaggio AVR per calcolare i risultati sin (x) in 24 bit (3 byte) con un bit di errore. L'angolo di input è in gradi con una cifra decimale, da 000 a 900 (0 ~ 90,0) solo per il primo quadrante. Utilizza meno di 210 istruzioni AVR e funziona in media 212 microsecondi, variando da 211us (angolo = 001) a 213us (angolo = 899).

Ci sono voluti diversi giorni per fare tutto, più di 10 giorni (ore libere) solo pensando al miglior algoritmo per il calcolo, considerando il microcontrollore AVR, senza virgola mobile, eliminando tutte le possibili divisioni. Ciò che ha richiesto più tempo è stato quello di creare i giusti valori di incremento per gli interi, per avere una buona precisione è necessario aumentare i valori da 1e-8 a interi binari 2 ^ 28 o più. Una volta trovati tutti gli errori colpevoli di precisione e arrotondamento, aumentato la loro risoluzione di calcolo di 2 ^ 8 o 2 ^ 16 in più, i risultati migliori sono stati raggiunti. Ho prima simulato tutti i calcoli su Excel avendo cura di avere tutti i valori come Int (x) o Round (x, 0) per rappresentare esattamente l'elaborazione del core AVR.

Ad esempio, nell'algoritmo l'angolo deve essere in radianti, l'ingresso è in gradi per facilitare l'utente. Per convertire gradi in radianti la formula banale è rad = gradi * PI / 180, sembra piacevole e facile, ma non lo è, PI è un numero infinito - se usando poche cifre si creeranno errori in uscita, la divisione per 180 richiede Manipolazione di bit AVR poiché non ha istruzioni di divisione e, inoltre, il risultato richiederebbe un punto in virgola mobile poiché coinvolge numeri molto al di sotto dell'intero 1. Ad esempio, il radiante di 1 ° (gradi) è 0,017453293. Poiché PI e 180 sono costanti, perché non invertire questa cosa per una semplice moltiplicazione? PI / 180 = 0,017453293, moltiplicalo per 2 ^ 32 e risulta come una costante 74961320 (0x0477D1A8), moltiplica questo numero per il tuo angolo in gradi, diciamo 900 per 90 ° e spostatelo di 4 bit a destra (÷ 16) per ottenere 4216574250 (0xFB53D12A), ovvero i radianti del 90 ° con espansione 2 ^ 28, adattarsi in 4 byte, senza una singola divisione (tranne il 4 spostamento del bit verso destra). In un certo senso, l'errore incluso in tale trucco è inferiore a 2 ^ -27.

Quindi, tutti gli ulteriori calcoli devono ricordare che è 2 ^ 28 più alto e se ne è preso cura. È necessario dividere i risultati in movimento per 16, 256 o anche 65536 solo per evitarlo utilizzare byte di fame in crescita non necessari che non aiuterebbero la risoluzione. È stato un lavoro scrupoloso, trovare la quantità minima di bit in ogni risultato di calcolo, mantenendo la precisione dei risultati intorno a 24 bit. Ognuno dei numerosi calcoli è stato eseguito in tentativo / errore con bit più alti o più bassi nel conteggio di Excel, osservando la quantità complessiva di bit di errore sul risultato in un grafico che mostra 0-90 ° con una macro che esegue il codice 900 volte, una volta al decimo di laurea. Quell'approccio "visivo" di Excel è stato uno strumento che ho creato, aiutato molto a trovare la soluzione migliore per ogni singola parte del codice.

Ad esempio, arrotondando questo particolare risultato di calcolo da 13248737,51 a 13248738 o semplicemente perdendo i decimali "0,51", quanto influenzerà la precisione del risultato finale per tutti i 900 angoli di input (00,1 ~ 90,0) test?

Sono stato in grado di mantenere l'animale contenuto entro 32 bit (4 byte) su ogni calcolo e ho finito con la magia per ottenere la precisione entro 23 bit dal risultato. Quando si controllano tutti i 3 byte del risultato, l'errore è ± 1 LSB, in sospeso.

L'utente può prendere uno, due o tre byte dal risultato per i propri requisiti di precisione. Naturalmente, se è sufficiente un solo byte, consiglierei di utilizzare una singola tabella sin da 256 byte e di utilizzare l'istruzione AVR "LPM" per afferrarla.

Una volta che la sequenza di Excel è stata eseguita senza intoppi, la traduzione finale da Excel in assembly AVR ha richiesto meno di 2 ore, come al solito dovresti pensare di più prima, lavorare meno dopo.

A quel tempo sono stato in grado di spremere ancora di più e ridurre l'utilizzo dei registri. Il codice effettivo (non finale) utilizza circa 205 istruzioni (~ 410 byte), esegue un calcolo sin (x) in media di 212us, clock a 16MHz. A quella velocità può calcolare 4700+ sin (x) al secondo. Non è importante, ma può eseguire un'onda sinusoidale precisa fino a 4700Hz con 23 bit di precisione e risoluzione, senza alcuna tabella di ricerca.

L'algoritmo di base si basa sulla serie di Taylor per sin (x), ma modificato molto per adattarsi alle mie intenzioni con il microcontrollore AVR e la precisione in mente.

Anche se l'utilizzo di una tabella di 2700 byte (900 voci * 3 byte) sarebbe attraente per la velocità, qual è l'esperienza divertente o di apprendimento su questo? Naturalmente, è stato anche considerato l'approccio CORDIC, forse più tardi, il punto qui è quello di spingere Taylor nel nucleo dell'AVR e prendere l'acqua da una roccia asciutta.

Mi chiedo se Arduino "sin (78.9 °)" può eseguire Processing (C ++) con 23 bit di precisione in meno di 212us e il codice necessario più piccolo di 205 istruzioni. Può essere se C ++ utilizza CORDIC. Gli schizzi di Arduino possono importare il codice assembly.

Non ha senso pubblicare qui il codice, in seguito modificherò questo post per includervi un collegamento web, possibilmente sul mio blog in questo URL . Il blog è principalmente in portoghese.

Questa avventura senza soldi è stata interessante, spingendo i limiti del motore AVR di quasi 16MIPS a 16MHz, senza istruzione di divisione, moltiplicazione solo in 8x8 bit. Permette di calcolare sin (x), cos (x) [= sin (900-x)] e tan (x) [= sin (x) / sin (900-x)].

Soprattutto, questo mi ha aiutato a mantenere lucido e oliato il mio cervello di 63 anni. Quando gli adolescenti dicono che gli "anziani" non sanno niente della tecnologia, rispondo "ripensaci, chi pensi abbia creato le basi per tutto ciò che ti piace oggi?".

Saluti


Bello! Alcune osservazioni: 1. La sin()funzione standard ha circa la stessa precisione della tua ed è due volte più veloce. Si basa anche su un polinomio. 2. Se un angolo arbitrario deve essere arrotondato al multiplo più vicino di 0,1 °, ciò può portare a un errore di arrotondamento alto come 8.7e-4, che in qualche modo annulla il vantaggio della precisione a 23 bit. 3. Ti dispiacerebbe condividere il tuo polinomio?
Edgar Bonet,

1

Come altri hanno già detto, le tabelle di ricerca sono la strada da percorrere se si desidera la velocità. Recentemente ho studiato il calcolo delle funzioni di trigger su un ATtiny85 per l'uso di medie vettoriali veloci (vento nel mio caso). C'è sempre un compromesso ... per me avevo solo bisogno di una risoluzione angolare di 1 grado, quindi una tabella di ricerca di 360 int (ridimensionando da -32767 a 32767, lavorando solo con int) era il modo migliore per andare. Recuperare il seno è solo una questione di fornire un indice 0-359 ... molto veloce! Alcuni numeri dai miei test:

Tempo di ricerca FLASH (us): 0.99 (tabella memorizzata usando PROGMEM)

Tempo di ricerca RAM (us): 0.69 (tabella in RAM)

Tempo lib (us): 122.31 (usando Arduino Lib)

Si noti che si tratta di medie su un campione di 360 punti per ciascuna. Il test è stato eseguito su un nano.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.