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:
¿Cuáles son las diferentes formas de calcular estas funciones trigonométricas en Arduino?
¿Cuál es la forma más rápida de hacer lo mismo?
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
fuente
Respuestas:
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
sin
con 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 seasin()
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.fuente
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.
Pero primero pruebe las funciones trigonométricas nativas de Arduino; de todos modos, podrían ser lo suficientemente rápidas.
fuente
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()
ysin()
de avr-libc: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_t
en 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:
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.
fuente
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.
Su función idealmente primero verificaría que el rango del ángel esté entre 0 y 90.
Luego usaría una
ifelse
declaración para determinar a cuál de las 3 secciones pertenece y luego realiza el cálculo lineal correspondiente (es deciroutput = mX + c
)fuente
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 ().
fuente
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.
fuente
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.
fuente
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
Ahora para recuperar los valores, use esta función. Acepta un valor de 0x0000 a 0x0800 y devuelve el valor apropiado de la LUT
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.
fuente
Imm_UI_A
se declara dos veces,;
faltan algunas declaraciones de variables yuLut_0
deben 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 unaPROGMEM
matriz.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
fuente
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?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.
fuente