For novel ideas about building embedded systems (both hardware and firmware), join the 40,000+ engineers who subscribe to The Embedded Muse, a free biweekly newsletter. The Muse has no hype and no vendor PR. Click here to subscribe. |
Thanks to Arturo Campos for doing this translation!
Guia a las aproximaciones trigonométricas
La mayoria de sistemas encastados no pueden calcular funciones trigonométricas u otras funciones complejas. En C normalmente llamariamos a una biblioteca que haria todo el trabajo por nosotros. Por desgracia esto no se puede hacer en sistemas en tiempo real donde el tamaño, la velocidad y la precisión son todos factores clave.
El run-time de un compilador es una solucion ya dada, con un buen balance entre velocidad y precision, pero cada sistema encastado es diferente y tiene diferentes requerimientos. En algunos casos vale la pena hacer nuestras propias funciones de aproximación. Porque?
Velocidad – los compiladores suelen tener run-times más bien lentos. Un ataque inteligente puede ahorrarnos usar una CPU más rápida.
Predictabilidad – el código de los compiladores puede variar mucho dependiendo de las entradas. Un sistema en tiempo real tiene que tener un comportamiento deterministico y predecible. Así tenemos que plantearnos el peor caso, que puede significar que la CPU sea demasiado lenta, o demasiado cargada para la aplicación
Precisión – Lee el manual del compilador atentamente! Algunos directamente no soportan el estandard ANSI de C (los compiladores 8051 son conocidos por esto). O incluso, porque pagar en tiempo por matemática en doble precisión cuando sólo necesitas 5 dígitos?
Este documento no es una enciclopedia de todas las aproximaciones posibles; más bien son las más prácticas destiladas de la biblia del tema: Computer Approximations by John Hart (ISBN 0-88275-642-7). Por desgracia esta obra ya no se imprime, y además es bastante difícil de entender sin una base matemática rigurosa.
Todas las aproximación mostradas son polinómios o ratios de polinómios. Todos usan coeficientes crípticos derivados de las series de Chebysehv y las funciones de Bessel. Daremos rangos mínimos de error para los rangos estudiados. Cada aproximación (excepto algunas) tiene un gráfico con el error para que puedas ver donde ocurre exactamente. En algunos casos tratamos con entradas limitadas a un rango, pues podrias tener mucha más precisión que la indicada. Por ejemplo, cos_73 es preciso a 7,3 dígitos decimales desde 0 a 360 grados, pero como se puede ver en el gráfico en el rando de 0 a 30 tienes una mejor de un orden de magnitud.
Presta atención a la precisión que te ofrece tu compilador. Algunos tratan los doubles como floats. Otros, especialmente para PICS, hacen tramaps y ofrecen floats con menos de 32 bits.
Todo el código ha sido compilado con Microsoft's Visual C++ 6.0. El código fuente esta disponible en www.ganssle.com/approx/sincos.cpp. Incluye pruebas con salida de consola, y ello importado en una hoja de calculo para que puedas ver como és de preciso.
Notas generales sobre funciones trigonométricas
Normalmente trabajaremos con radianes en vez de grados. 360 grados equivalen a 2? radianes. Por tanto un radian es 360/(2?) o más o menos 57.3 grados. Puede paracer un poco extraño hasta que piensas en una el perímetro de un círculo, que és 2?r; si r (el radio) és 1, entonces la circumferencia és de 2?.
Las conversiones entre radianes y angulos son:
Coseno y seno
Los siguientes ejemplos son para aproximar el coseno, el seno se deriva del coseno con la relación:
En otras palabras, el seno y el coseno són la misma función, solo que hay un desplazamiento de 90 grados en la fase. El código para el seno es (asumiendo que lo llamamos cos_32, a la más baja apromaxión del coseno):
Todas las aproximaciones del coseno en este capítulo asumen el rango entre 0 y ?/2 (de 0 a 90°). Esto nos dejaria sin el circulo completo! Pero las aproximaciones funcionan mejor con rangos limitados, és nuestra tarea reducir el rango de entrada a algo que nuestra aproximación pueda tratar con precisión.
Pues, antes de llamar al coseno, asumimos que el rango se ha reducido a [0, ?/2] con este código:
El código está preparado para llamar a cos_32s, que és una aproximación (que ahora veremos) para calcular el coseno con 3,2 dígitos. Usa este mismo código para todas las aproximaciones; cambia cos_32s a cos52_s, cos_73s o cos_121s, dependiendo del nivel de precisión que necesites.
Si puedes asegurar que la entrada siempre será superior a 0 y menor que 2?, borra las dos lineas en rojo, para ejecutar aun más rápido.
Sé inteligente cuando declares variables y constantes. Claramente, trabajando con cos_32 no necesitamos doubles, usa float. Leyendo el listado te darás cuenta de que cos_32 y cos52 usan siempre floats, las otrás – más precisas – usan doubles.
Un truco que accelera és calcular x2 mediante incrementos. Aunque tengas que saber exactamente cuantos números se guardan puedes ahorrarte microsegundos sobre la – más clara – operacion “x*x”.
para el primer cuadrante (de 0 a ?/2) no tenemos que hacer ya que és nuestro rango de entrada válido
en el cuadrante 1 el coseno es simetrico con el cuadrante 0, pues simplemente le restamos a ? el argumento. El coseno sin embargo es negativo en los cuadrantes 1 y 2 así: -cos(?-x)
el cuadrante 2 es similar al 1
Finalmente en el 3 el coseno es positivo de nuevo; si le restamos a 2? el argumento volvemos al rango [0,?/2].
Las aproximaciones conviernten los polinomios a una fomar más eficiente, como se describe en los comentarios. Todas las operaciones con floats tardan mucho, así que hay que optimizar el diseño.
Cos_32 calcula el coseno con 3,2 digitos de precisión. Usa el código de reducción de rango (que está más arriba) si la entrada está fuera de rango. Los errores representados son absolutos (no percentuales).
Cos_52 calcula el coseno con 5,2 digitos de precisión. Usa el código de reducción de rango (que está más arriba) si la entrada está fuera de rango. Los errores representados son absolutos (no percentuales).
Cos_73 calcula el coseno con 7,3 digitos de precisión. Usa el código de reducción de rango (que está más arriba) si la entrada está fuera de rango. Utiliza doubles en la reducción de rango para no perder precisión. Los errores representados son absolutos (no percentuales).
Cos_121 calcula el coseno con 12,1 digitos de precisión. Usa el código de reducción de rango (que está más arriba) si la entrada está fuera de rango. Utiliza doubles en la reducción de rango para no perder precisión. Los errores representados son absolutos (no percentuales).
Cosenos de precisión mayor
Dado un polinómio lo suficientemente grande no hay límite en la precisión. Veremos algunos algoritmos. Són válidos para el rango [0,?/2], como antes puedes usar el código de reducción. De nuevo trabajan con radianes.
No incluyo gráficos porque tienen más precisión que la función el de un compilador... así que no hay contra qué comparar.
Fíjate que los doubles en C normalmente tienen 15 dígitos. Así para estos algoritmos, especialmente 20,2 y 23,1, necesitarás un tipo de datos que ofrezca más bits. Algunos compiladores soportan el long double, mirate bien el manual! Microsoft's Visual C++ aunque acepta la keyword long double, la traduce a double.
Precisión de hasta 14,7 dígitos en el rango [0,?/2]
Precisión de hasta 20,2 dígitos en el rango [0,?/2]
Precisión de hasta 23,1 dígitos en el rango [0,?/2]
Tangente
La tangente de un ángulo se define como tan(x) = sin(x)/cos(x). Por desgracia esta no és la mejor elección de cara a aproximar, ya que mientras cos(x) se acerca a 0 los errores se propagan rápidamente. Además, en algunos puntos como ?/4 (mira los grafos anteriores sobre los errores del seno y coseno) los errores de uno y otro se complementan; ambos siendo grandes y del mismo signo.
Así que és mejor que usemos un algoritmo diferente para la tangente. Todas las aproximaciones que usaremos trabajan en el rango [0,?/4] (0 a 45 grados), así que de nuevo necesitaremos una funcion de traducción:
El código de arriba reduce el rango y luego llama a tan_32. Cuando necesites más precisión substituye el nombre tan_32 por otro.
La reducción funciona como con el coseno, excepto que divide el círculo en octantes. Una peculiaridad es que el argumento se multiplica por 4/?, eso es porque las aproximaciones están resolviendo tan((?/4)x). A continuación veremos los algoritmos.
Recuerda que tanto tan(90) como tan(270) van al infinito. Conforme el argumento se acerca a ellos, la tangente se dispara, como se puede ver en los gráficos de error. Nunca calcules una tangente en los entornos de 90 o 270 grados!
Tan_32 calcula la tangente de ?/4*x con 3,2 digitos de precisión. Usa el código de reducción a [0,?/4] y multiplica por el bias “?/4”. Los errores representados son absolutos (no percentuales).
Tan_56 calcula la tangente de ?/4*x con 5,6 digitos de precisión. Usa el código de reducción a [0,?/4] y multiplica por el bias “?/4”. Los errores representados son absolutos (no percentuales).
Tan_82 calcula la tangente de ?/4*x con 8,2 digitos de precisión. Usa el código de reducción a [0,?/4] y multiplica por el bias “?/4”. Las variables son doubles. Los errores representados son absolutos (no percentuales).
Tan_141 calcula la tangente de ?/4*x con 14,1 digitos de precisión. Usa el código de reducción a [0,?/4] y multiplica por el bias “?/4”. Las variables son doubles. Los errores representados son absolutos (no percentuales).
Tangentes de precisión mayor
Dado un polin
Dado un polinómio lo suficientemente grande no hay límite en la precisión. Veremos algunos algoritmos. Són válidos para el rango [0,?/4], como antes puedes usar el código de reducción. De nuevo trabajan con radianes y además necesitan del factor ?/4.
No incluyo gráficos porque tienen más precisión que la función el de un compilador... así que no hay contra qué comparar.
Fíjate que los doubles en C normalmente tienen 15 dígitos. Así para estos algoritmos, especialmente 20,2 y 23,1, necesitarás un tipo de datos que ofrezca más bits. Algunos compiladores soportan el long double, mirate bien el manual! Microsoft's Visual C++ aunque acepta la keyword long double, la traduce a double.
Arco-tangente, arco-seno y arco-coseno
El Arco-tangente es igual que el inverso de la tangente (pues arctan(tan(x))=x). La notación habitual es "atan(x)" o "tan-1(x)".
En la práctica las aproximaciones del arco-seno y arco-coseno no son muy útiles, sobretodo porque se derivan de la arco-tangente así:
Las aproximaciones són válidas en el rango [0,?/12]. El código está basado en el de Jack Crenshaw en su Math Toolkit for Real-Time Programming.
atan_66 calcula el arco-tangente 6,6 digitos de precisión usando polinómios racionales simples.Su rango es [0,?/12]. Usa el código de reducción visto.
atan_137 calcula el arco-tangente 13,7 digitos de precisión usando polinómios racionales simples.Su rango es [0,?/12]. Usa el código de reducción visto.
(por favor, usa un análisis fomal propio de un ingeniero cuando implementes estas aproximaciones, no damos ningún tipo de garantia).