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:
Quais são as diferentes maneiras de calcular essas funções trigonométricas no Arduino?
Qual é a maneira mais rápida de fazer o mesmo?
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
fonte
Respostas:
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
sin
com 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 usosin()
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.fonte
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.
Mas tente primeiro as funções trigonométricas nativas do Arduino - elas podem ser rápidas o suficiente de qualquer maneira.
fonte
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()
esin()
com o avr-libc:É 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_t
em 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:
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.
fonte
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.
Idealmente, sua função verifica primeiro se o intervalo do anjo está entre 0 e 90.
Em seguida, usaria uma
ifelse
instrução para determinar qual das 3 seções a que pertence e depois faz o cálculo linear correspondente (por exemplooutput = mX + c
)fonte
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 ().
fonte
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.
fonte
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.
fonte
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
Agora, para recuperar os valores, use esta função. Ele aceita um valor de 0x0000 a 0x0800 e retorna o valor apropriado do LUT
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.
fonte
Imm_UI_A
é declarado duas vezes,;
faltam a e algumas declarações de variáveis euLut_0
devem 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 umaPROGMEM
matriz.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
fonte
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?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.
fonte