Diferentes maneiras (e as mais rápidas) de calcular senos (e cossenos) no Arduino


9

Estou usando uma placa Arduino Uno para calcular os ângulos do meu sistema (braço robótico). Na verdade, os ângulos são valores de 10 bits (0 a 1023) do ADC, usando toda a faixa do ADC. Eu só operarei no 1º quadrante (0 a 90 graus), onde os senos e os cossenos são positivos, então não há problema com números negativos. Minhas dúvidas podem ser expressas em 3 perguntas:

  1. Quais são as diferentes maneiras de calcular essas funções trigonométricas no Arduino?

  2. Qual é a maneira mais rápida de fazer o mesmo?

  3. Existem as funções sin () e cos () no IDE do Arduino, mas como o Arduino realmente as calcula (como elas usam tabelas de consulta, aproximações etc.)? Eles parecem uma solução óbvia, mas eu gostaria de saber sua implementação real antes de testá-los.

PS: Estou aberto tanto à codificação padrão no IDE do Arduino quanto à codificação de montagem, além de outras opções não mencionadas. Também não tenho problemas com erros e aproximações, inevitáveis ​​para um sistema digital; no entanto, se possível, seria bom mencionar a extensão de possíveis erros


Você concorda com valores aproximados?
precisa saber é o seguinte

Sim, na verdade, mas gostaria de saber a extensão do erro de diferentes métodos. Este não é um produto de precisão, mas um projeto paralelo meu. Na verdade aproximações são inevitáveis para quase qualquer (se não houver) sistema digital implementação de uma função matemática
Transistor Overlord

Eu suponho que você está querendo trabalhar em graus. Você deseja inserir números inteiros ou decimais para o ângulo?
precisa saber é o seguinte

Graus sim. Eu acho que seria mais fácil escrever código e testar se usarmos números inteiros, então eu iria com isso. Vou colocar uma informação mais clara sobre as edições
Transistor Overlord

11
Por apenas 90 graus (inteiros), uma tabela de pesquisa de 90 entradas seria mais rápida e eficiente. De fato, para os 360 graus completos, você pode usar uma tabela de pesquisa de 90 entradas. Basta ler de trás para 90-179 e inverter para 180-269. Faça os dois para 270-359.
Majenko

Respostas:


11

Os dois métodos básicos são cálculo matemático (com polinômios) e tabelas de pesquisa.

A biblioteca de matemática do Arduino (libm, parte do avr-libc) usa o primeiro. Ele é otimizado para o AVR, pois é escrito com linguagem assembly 100% e, como tal, é quase impossível seguir o que está fazendo (também não há comentários). Tenha certeza de que ele será o mais otimizado que os cérebros de implementação de float puro, muito superiores aos nossos, poderiam oferecer.

No entanto, a chave existe flutuador . Qualquer coisa no Arduino que envolva ponto flutuante será pesada em comparação com número inteiro puro, e como você está solicitando números inteiros entre 0 e 90 graus, uma tabela de pesquisa simples é de longe o método mais simples e mais eficiente.

Uma tabela de 91 valores fornecerá tudo de 0 a 90, inclusive. No entanto, se você criar uma tabela de valores de ponto flutuante entre 0,0 e 1,0, ainda terá a ineficiência de trabalhar com flutuadores (concedido não tão ineficiente quanto o cálculo sincom flutuadores); portanto, armazenar um valor de ponto fixo seria muito mais eficiente.

Isso pode ser tão simples quanto armazenar o valor multiplicado por 1000, então você tem entre 0 e 1000 em vez de entre 0,0 e 1,0 (por exemplo, sin (30) seria armazenado como 500 em vez de 0,5). Mais eficiente seria armazenar os valores como, por exemplo, um valor Q16 em que cada valor (bit) representa 1 / 65536º de 1,0. Esses valores Q16 (e os Q15, Q1.15, etc) relacionados são mais eficientes de trabalhar, pois você possui poderes de dois com os quais os computadores adoram trabalhar, em vez de poderes com dez que eles odeiam trabalhar.

Não esqueça também que a sin()função espera radianos; primeiro, você deve converter seus graus inteiros em um valor de radianos de ponto flutuante, tornando o uso sin()ainda mais ineficiente em comparação com uma tabela de pesquisa que pode trabalhar diretamente com o valor de graus inteiros.

Uma combinação dos dois cenários, no entanto, é possível. A interpolação linear permitirá obter uma aproximação de um ângulo de ponto flutuante entre dois números inteiros. É tão simples quanto determinar a distância entre dois pontos na tabela de pesquisa e criar uma média ponderada com base nessa distância dos dois valores. Por exemplo, se você estiver em 23,6 graus você toma (sintable[23] * (1-0.6)) + (sintable[24] * 0.6). Basicamente, sua onda senoidal se torna uma série de pontos discretos unidos por linhas retas. Você troca precisão por velocidade.


Escrevi uma biblioteca um tempo atrás que usava um polinômio Taylor para sin / cos que era mais rápido que a biblioteca. Dado, eu estava usando radianos de ponto flutuante como entrada para ambos.
Tuskiomi

8

Há algumas boas respostas aqui, mas eu queria adicionar um método que ainda não foi mencionado, muito adequado para calcular funções trigonométricas em sistemas embarcados, e essa é a técnica CORDIC. Entrada da Wiki Aqui Ele pode calcular funções trigonométricas usando apenas turnos e adiciona e uma pequena tabela de consulta.

Aqui está um exemplo grosseiro em C. Na verdade, ele implementa a função atan2 () das bibliotecas C usando CORDIC (ou seja, encontre um ângulo com dois componentes ortogonais.) Ele usa ponto flutuante, mas pode ser adaptado para uso com aritmética de ponto fixo.

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

Mas tente primeiro as funções trigonométricas nativas do Arduino - elas podem ser rápidas o suficiente de qualquer maneira.


11
Eu adotei uma abordagem semelhante no passado, no stm8. são necessários dois passos: 1) calcular o pecado (x) e cos (x) do pecado (2x) e, em seguida, 2) calcular o pecado (x +/- x / 2) do pecado (x), pecado (x / 2) , cos (x) e cos (x / 2) -> através da iteração, você pode se aproximar do seu destino. no meu caso, comecei com 45 graus (0,707) e trabalhei até o alvo. é consideravelmente mais lento que a função IAR sin () padrão.
Dannyf 29/04

7

Eu tenho brincado um pouco com a computação de senos e cossenos no Arduino usando aproximações polinomiais de ponto fixo. Aqui estão minhas medições do tempo médio de execução e do pior erro, comparado com o padrão cos()e sin()com o 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

É baseado em um polinômio de 6º grau calculado com apenas 4 multiplicações. As multiplicações são feitas em montagem, como eu descobri que o gcc as implementou de maneira ineficiente. Os ângulos são expressos como uint16_tem unidades de 1/65536 de uma revolução, o que faz com que a aritmética dos ângulos trabalhe naturalmente no módulo uma revolução.

Se você acha que isso pode se adequar à sua conta, aqui está o código: Trigonometria de ponto fixo . Ainda não traduzi esta página, que está em francês, mas você pode entender as equações e o código (nomes de variáveis, comentários ...) está em inglês.


Edit : Como o servidor parece ter desaparecido, aqui estão algumas informações sobre as aproximações que encontrei.

Eu queria escrever ângulos em ponto fixo binário, em unidades de quadrantes (ou, equivalentemente, em turnos). E eu também queria usar um polinômio par, pois eles são mais eficientes em calcular do que polinômios arbitrários. Em outras palavras, eu queria um polinômio P () tal que

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

Também exigi que a aproximação fosse exata nas duas extremidades do intervalo, para garantir que cos (0) = 1 e cos (π / 2) = 0. Essas restrições levaram à forma

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

onde Q () é um polinômio arbitrário.

Em seguida, procurei a melhor solução em função do grau de Q () e encontrei o seguinte:

        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

A escolha entre as soluções acima é uma troca de velocidade / precisão. A terceira solução oferece mais precisão do que é possível com 16 bits e foi a escolhida para a implementação de 16 bits.


2
Isso é incrível, @ Edd.
precisa saber é o seguinte

O que você fez para encontrar o polinômio?
TLW

@TLW: Eu exigia que ele tivesse algumas propriedades "agradáveis" (por exemplo, cos (0) = 1), restritas à forma (1-x²) (1 + x²Q (x²)), onde Q (u) é arbitrário polinômio (é explicado na página). Tomei um Q de primeiro grau (apenas 2 coeficientes), encontrei os coeficientes aproximados por ajuste e depois ajustei manualmente a otimização por tentativa e erro.
Edgar Bonet

@EdgarBonet - interessante. Observe que essa página não carrega para mim, embora funcione em cache. Você poderia adicionar o polinômio usado para esta resposta?
TLW

@ TLW: acrescentou isso à resposta.
Edgar Bonet

4

Você pode criar algumas funções que usam aproximação linear para determinar o pecado () e cos () de um ângulo específico.

Estou pensando em algo assim: para cada uma, quebrei a representação gráfica de sin () e cos () em três seções e fiz uma aproximação linear dessa seção.
aproximação linear

Idealmente, sua função verifica primeiro se o intervalo do anjo está entre 0 e 90.
Em seguida, usaria uma ifelseinstrução para determinar qual das 3 seções a que pertence e depois faz o cálculo linear correspondente (por exemplo output = mX + c)


Isso não envolverá multiplicação de ponto flutuante?
Transistor Overlord

11
Não necessariamente. Você pode tê-lo para que a saída seja dimensionada entre 0-100 em vez de 0-1. Dessa forma, você está lidando com números inteiros, não com ponto flutuante. Nota: 100 foi arbitrário. Não há razão para que você não possa escalar a saída entre 0-128 ou 0-512 ou 0-1000 ou 0-1024. Ao usar um múltiplo de 2, você só precisa fazer os turnos certos para reduzir o resultado.
sa_leinad

Muito inteligente, @sa_leinad. Voto a favor. Lembro-me de fazer isso ao trabalhar com polarização de transistores.
precisa saber é o seguinte

4

Procurei outras pessoas que se aproximaram de cos () e sin () e me deparei com esta resposta:

resposta da dtb para "Fast Sin / Cos usando uma matriz de tradução pré-calculada"

Basicamente, ele calculou que a função math.sin () da biblioteca matemática era mais rápida do que usar uma tabela de valores de pesquisa. Mas, pelo que sei, isso foi calculado em um PC.

O Arduino possui uma biblioteca de matemática incluída que pode calcular sin () e cos ().


11
Os PCs possuem FPUs integradas que o tornam mais rápido. O Arduino não, e isso torna lento.
Majenko

A resposta também é para C #, que faz coisas como verificação de limites de matriz.
Michael

3

Uma tabela de pesquisa será a maneira mais rápida de encontrar senos. E se você se sente à vontade com computação com números de ponto fixo (números inteiros cujo ponto binário está em algum lugar que não seja à direita do bit 0), seus cálculos adicionais com os senos também serão muito mais rápidos. Essa tabela pode ser uma tabela de palavras, possivelmente no Flash, para economizar espaço na RAM. Observe que em sua matemática você pode precisar usar longos para obter grandes resultados intermediários.


1

geralmente, tabela de consulta> aproximação -> cálculo. ram> flash. inteiro> ponto fixo> ponto flutuante. pré-cálculo> cálculo em tempo real. espelhamento (seno para cosseno ou cosseno para seno) vs. cálculo real / consulta ....

cada um tem seus prós e contras.

você pode fazer todos os tipos de combinações para ver qual funciona melhor para seu aplicativo.

editar: eu fiz uma verificação rápida. usando uma saída inteira de 8 bits, o cálculo de 1024 valores de pecado com tabela de consulta leva 0,6ms e 133ms com moscas volantes, ou 200x mais lento.


1

Eu tinha uma pergunta semelhante ao OP. Eu queria criar uma tabela LUT para calcular o primeiro quadrante da função seno como números inteiros de 16 bits não assinados, começando de 0x8000 a 0xffff. E acabei escrevendo isso por diversão e lucro. Nota: Isso funcionaria com mais eficiência se eu usasse as instruções 'if'. Também não é muito preciso, mas seria preciso o suficiente para uma onda senoidal em um sintetizador de som

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

Agora, para recuperar os valores, use esta função. Ele aceita um valor de 0x0000 a 0x0800 e retorna o valor apropriado do 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

Lembre-se de que essa não é a abordagem mais eficiente para essa tarefa; eu simplesmente não conseguia descobrir como fazer séries de taylor para fornecer resultados no intervalo apropriado.


Seu código não é compilado: Imm_UI_Aé declarado duas vezes, ;faltam a e algumas declarações de variáveis ​​e uLut_0devem ser globais. Com as correções necessárias, lu_sin()é rápido (entre 27 e 42 ciclos da CPU), mas muito impreciso (erro máximo ≈ 5,04e-2). Não consigo entender o ponto desses "polinômios arnadáticos": parece uma computação bastante pesada, mas o resultado é quase tão ruim quanto uma simples aproximação quadrática. O método também tem um enorme custo de memória. Seria muito melhor calcular a tabela no seu PC e colocá-la no código-fonte como uma PROGMEMmatriz.
Edgar Bonet 30/10

1

Apenas por diversão, e para provar que isso pode ser feito, concluí uma rotina de montagem do AVR para calcular os resultados sin (x) em 24 bits (3 bytes) com um bit de erro. O ângulo de entrada está em graus com um dígito decimal, de 000 a 900 (0 ~ 90,0) apenas para o primeiro quadrante. Ele usa menos de 210 instruções AVR e executa em média 212 microssegundos, variando de 211us (ângulo = 001) a 213us (ângulo = 899).

Demorou vários dias para fazer tudo, mais de 10 dias (horas livres), apenas pensando no melhor algoritmo para o cálculo, considerando o microcontrolador AVR, sem ponto flutuante, eliminando todas as divisões possíveis. O que levou mais tempo foi criar os valores de aumento corretos para números inteiros; para ter uma boa precisão, é necessário aumentar os valores de 1e-8 para números inteiros binários 2 ^ 28 ou mais. Depois que todos os erros de precisão e arredondamento foram encontrados, aumentou sua resolução de cálculo em 2 ^ 8 ou 2 ^ 16 extras, os melhores resultados foram alcançados. Primeiro simulei todos os cálculos no Excel, tendo todos os valores como Int (x) ou Arredondado (x, 0) para representar exatamente o processamento do núcleo do AVR.

Por exemplo, no algoritmo, o ângulo deve estar em Radianos, a entrada está em Graus para facilitar o usuário. Para converter graus em radianos, a fórmula trivial é rad = degrees * PI / 180, parece fácil e agradável, mas não é, PI é um número infinito - se usar alguns dígitos, criará erros na saída, a divisão por 180 exige Manipulação de bits AVR, uma vez que não possui instrução de divisão e, além disso, o resultado exigiria ponto flutuante, pois envolve números muito abaixo do número inteiro 1. Por exemplo, Radiano de 1 ° (grau) é 0,017453293. Como PI e 180 são constantes, por que não reverter isso para uma multiplicação simples? PI / 180 = 0,017453293, multiplique por 2 ^ 32 e resulta como uma constante 74961320 (0x0477D1A8), multiplique esse número pelo seu ângulo em graus, digamos 900 para 90 ° e mova-o 4 bits para a direita (÷ 16) para obter 4216574250 (0xFB53D12A), que são os radianos do 90 ° com expansão 2 ^ 28, cabem em 4 bytes, sem uma única divisão (exceto os 4 mudança de bits para a direita). De certa forma, o erro incluído nesse truque é menor que 2 ^ -27.

Portanto, todos os cálculos adicionais precisam lembrar que é 2 ^ 28 maior e resolvidos. Você precisa dividir os resultados on-the-go por 16, 256 ou até 65536, apenas para evitar o uso desnecessário de bytes de fome crescentes que não ajudariam na resolução. Foi um trabalho minucioso, encontrar a quantidade mínima de bits em cada resultado de cálculo, mantendo a precisão dos resultados em torno de 24 bits. Cada um dos vários cálculos realizados em tentativa / erro com bits mais altos ou mais baixos conta no fluxo do Excel, observando a quantidade geral de bits de erro no resultado em um gráfico mostrando 0-90 ° com uma macro executando o código 900 vezes, uma vez por décimo de grau. Essa abordagem "visual" do Excel foi uma ferramenta que eu criei, que ajudou muito a encontrar a melhor solução para cada parte do código.

Por exemplo, arredondando esse resultado de cálculo específico 13248737,51 para 13248738 ou apenas perdendo os decimais "0,51", quanto isso afetará a precisão do resultado final para todos os 900 testes de ângulos de entrada (00,1 ~ 90,0)?

Consegui manter o animal contido em 32 bits (4 bytes) em todos os cálculos e terminei com a mágica para obter precisão em 23 bits do resultado. Ao verificar os 3 bytes inteiros do resultado, o erro é de ± 1 LSB, excelente.

O usuário pode obter um, dois ou três bytes do resultado para seus próprios requisitos de precisão. Obviamente, se apenas um byte for suficiente, eu recomendaria usar uma única tabela sin de 256 bytes e usar a instrução AVR 'LPM' para obtê-la.

Depois que a sequência do Excel foi executada sem problemas, a tradução final do assembly do AVR para o Excel levou menos de 2 horas; como de costume, você deve pensar mais primeiro, trabalhar menos depois.

Naquela época, eu era capaz de espremer ainda mais e reduzir o uso de registros. O código real (não final) usa cerca de 205 instruções (~ 410 bytes), executa um cálculo sin (x) em média de 212us, com clock de 16MHz. Nessa velocidade, ele pode calcular 4700+ sin (x) por segundo. Não sendo importante, mas pode executar uma onda senoidal precisa de até 4700Hz com 23 bits de precisão e resolução, sem nenhuma tabela de pesquisa.

O algoritmo base é baseado na série de Taylor para sin (x), mas modificou muito para atender às minhas intenções com o microcontrolador AVR e a precisão em mente.

Mesmo que o uso de uma tabela de 2700 bytes (900 entradas * 3 bytes) seja atraente em termos de velocidade, qual é a experiência divertida ou de aprendizado nisso? Obviamente, a abordagem CORDIC também foi considerada, talvez mais tarde, o ponto aqui é espremer Taylor no núcleo do AVR e tirar água de uma rocha seca.

Gostaria de saber se o Arduino "sin (78,9 °)" pode executar o Processing (C ++) com 23 bits de precisão em menos de 212us e o código necessário menor que 205 instruções. Pode ser que o C ++ use CORDIC. Os esboços do Arduino podem importar código de montagem.

Não faz sentido postar o código aqui, mais tarde editarei este post para incluir um link para ele, possivelmente no meu blog neste URL . O blog é principalmente em português.

Esse empreendimento de passatempo sem dinheiro foi interessante, elevando os limites do mecanismo AVR de quase 16MIPS a 16MHz, sem instrução de divisão, multiplicação apenas em 8x8 bits. Permite calcular sin (x), cos (x) [= sin (900-x)] e tan (x) [= sin (x) / sin (900-x)].

Acima de tudo, isso ajudou a manter meu cérebro de 63 anos polido e oleado. Quando os adolescentes dizem que os "idosos" não sabem nada sobre tecnologia, respondo "pense novamente, quem você acha que criou as bases para tudo o que você gosta hoje?".

Felicidades


Agradável! Algumas observações: 1. A sin()função padrão tem aproximadamente a mesma precisão que a sua e é duas vezes mais rápida. Também é baseado em um polinômio. 2. Se um ângulo arbitrário precisar ser arredondado para o múltiplo mais próximo de 0,1 °, isso pode levar a um erro de arredondamento tão alto quanto 8,7e-4, o que meio que anula o benefício da precisão de 23 bits. 3. Você se importaria de compartilhar seu polinômio?
Edgar Bonet

1

Como outros já mencionaram, as tabelas de pesquisa são o caminho a percorrer, se você deseja velocidade. Recentemente, estive investigando o cálculo de funções trigonométricas em um ATtiny85 para o uso de médias vetoriais rápidas (vento no meu caso). Sempre existem trocas ... para mim, eu só precisava de uma resolução angular de 1 deg, então uma tabela de pesquisa de 360 ​​int's (escala -32767 a 32767, trabalhando somente com int's) era o melhor caminho a percorrer. Recuperar o seno é apenas uma questão de fornecer um índice de 0 a 359 ... muito rápido! Alguns números dos meus testes:

Tempo de pesquisa do FLASH (us): 0,99 (tabela armazenada usando PROGMEM)

Tempo de pesquisa da RAM (us): 0,69 (tabela na RAM)

Lib time (us): 122.31 (Usando Arduino Lib)

Observe que essas são médias em uma amostra de 360 ​​pontos para cada uma. O teste foi feito em um nano.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.