Различные способы (и самые быстрые) для вычисления синусов (и косинусов) в Arduino

9

Я использую плату Arduino Uno для вычисления углов моей системы (манипулятор). Углы на самом деле являются 10-битными значениями (от 0 до 1023) от АЦП, используя полный диапазон АЦП. Я собираюсь работать только в 1-м квадранте (от 0 до 90 градусов), где и синусы, и косинусы положительны, поэтому проблем с отрицательными числами нет. Мои сомнения можно выразить в 3 вопросах:

  1. Каковы различные способы вычисления этих тригонометрических функций на Arduino?

  2. Какой самый быстрый способ сделать то же самое?

  3. В Arduino IDE есть функции sin () и cos (), но как Arduino фактически вычисляет их (как они используют справочные таблицы или приближения и т. Д.)? Они кажутся очевидным решением, но я хотел бы узнать их фактическую реализацию, прежде чем опробовать их.

PS: я открыт как для стандартного кодирования в Arduino IDE, так и для кодирования сборки, а также для любых других не упомянутых опций. Также у меня нет проблем с ошибками и аппроксимациями, которые неизбежны для цифровой системы; однако, если это возможно, было бы хорошо упомянуть степень возможных ошибок

Транзистор Оверлорд
источник
Вы были бы в порядке с приблизительными значениями?
sa_leinad
Да, на самом деле, но я хотел бы знать степень ошибки различных методов. Это не прецизионный продукт, а побочный проект. На самом деле приближения неизбежны почти для любой (если не для любой) цифровой системы, реализующей математическую функцию
Overlord
Я предполагаю, что вы хотите работать в градусах. Вы хотите ввести целые или десятичные числа для угла?
sa_leinad
Степени да. Я думаю, что было бы легче написать код и протестировать, если бы мы использовали целые числа, поэтому я бы пошел с этим. Я буду уделять больше четкой информации о редактировании
Транзистор Overlord
1
Всего для 90 (целых) градусов таблица поиска с 90 записями будет самой быстрой и эффективной. Фактически для полных 360 градусов вы можете использовать справочную таблицу с 90 записями. Просто прочитайте это назад для 90-179 и инвертируйте для 180-269. Сделайте оба за 270-359.
Majenko

Ответы:

11

Два основных метода - математический расчет (с полиномами) и таблицы поиска.

Математическая библиотека Arduino (libm, часть avr-libc) использует первую. Он оптимизирован для AVR в том смысле, что он написан на 100% ассемблере, и поэтому практически невозможно следить за тем, что он делает (комментариев также нет). Будьте уверены, что это будет наиболее оптимизированная реализация, которая может превзойти наши мозги.

Однако ключ есть поплавок . Все в Arduino, включающее в себя число с плавающей запятой, будет тяжелым по сравнению с чистым целым числом, и, поскольку вы запрашиваете целые числа только в диапазоне от 0 до 90 градусов, простая таблица поиска - безусловно, самый простой и эффективный метод.

Таблица из 91 значения даст вам все от 0 до 90 включительно. Однако если вы создадите таблицу значений с плавающей запятой в диапазоне от 0,0 до 1,0, у вас все равно будет неэффективность работы с плавающими числами (предоставляется не так неэффективно, как вычисление sinс плавающими числами), поэтому вместо этого хранение значения с фиксированной точкой будет гораздо более эффективным.

Это может быть так же просто, как сохранить значение, умноженное на 1000, так что у вас будет от 0 до 1000 вместо 0,0–1,0 (например, sin (30) будет храниться как 500 вместо 0,5). Более эффективным было бы хранить значения как, например, значение Q16, где каждое значение (бит) представляет 1/65536-ую из 1,0. Эти значения Q16 (и связанные с ними Q15, Q1.15 и т. Д.) Более эффективны для работы, поскольку у вас есть полномочия двух, с которыми любят работать компьютеры, а не полномочия десяти, с которыми они ненавидят работать.

Не забывайте также, что sin()функция ожидает радианы, поэтому сначала нужно преобразовать целочисленные градусы в значение радиан с плавающей запятой, что делает использование sin()еще более неэффективным по сравнению с таблицей поиска, которая может работать непосредственно со значением целочисленных градусов.

Однако комбинация двух сценариев возможна. Линейная интерполяция позволит вам получить аппроксимацию угла с плавающей точкой между двумя целыми числами. Это так же просто, как определить, насколько далеко вы находитесь между двумя точками в таблице поиска, и создать средневзвешенное значение на основе этого расстояния двух значений. Например, если вы находитесь на 23,6 градусов, вы берете (sintable[23] * (1-0.6)) + (sintable[24] * 0.6). По сути, ваша синусоида становится серией дискретных точек, соединенных между собой прямыми линиями. Вы обмениваете точность на скорость.

Маженко
источник
Некоторое время назад я написал библиотеку, в которой использовался многочлен Тейлора для sin / cos, который был быстрее, чем библиотека. Учитывая, я использовал радианы с плавающей точкой в ​​качестве входных данных для обоих.
Tuskiomi
8

Здесь есть несколько хороших ответов, но я хотел бы добавить метод, который еще не был упомянут, который очень хорошо подходит для вычисления тригонометрических функций во встроенных системах, и это метод CORDIC. Wiki Entry Here Здесь можно вычислять тригонометрические функции, используя только сдвиги и добавляет и небольшой справочный стол.

Вот грубый пример на C. В сущности, он реализует функцию atan2 () из библиотек C, используя CORDIC (т. Е. Находит угол с учетом двух ортогональных компонентов). Он использует плавающую точку, но может быть адаптирован для использования с арифметикой с фиксированной точкой.

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

Но сначала попробуйте родные функции триггера Arduino - они могут быть достаточно быстрыми в любом случае.

Halzephron
источник
1
Я использовал подобный подход в прошлом, на stm8. требуется два шага: 1) вычислить sin (x) и cos (x) из sin (2x), а затем 2) вычислить sin (x +/- x / 2) из ​​sin (x), sin (x / 2) , cos (x) и cos (x / 2) -> посредством итерации вы можете приблизиться к своей цели. в моем случае я начал с 45 градусов (0,707) и пробился к цели. это значительно медленнее, чем стандартная функция IAR sin ().
Дэнниф
7

Я немного поиграл с вычислением синусов и косинусов на Arduino, используя полиномиальные приближения с фиксированной точкой. Вот мои измерения среднего времени выполнения и наихудшей ошибки по сравнению со стандартом cos()и sin()из 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

Он основан на полиноме 6-й степени, рассчитанном только с 4 умножениями. Сами умножения выполняются в сборке, так как я обнаружил, что gcc реализовал их неэффективно. Углы выражаются uint16_tв единицах 1/65536 оборота, что делает арифметику углов естественной работой по модулю одного оборота.

Если вы думаете, что это может удовлетворить ваш счет, вот код: тригонометрия с фиксированной точкой . Извините, я до сих пор не перевел эту страницу, которая на французском языке, но вы можете понять уравнения, а код (имена переменных, комментарии ...) на английском языке.


Изменить : так как сервер, кажется, исчез, вот некоторая информация о приближениях, которые я нашел.

Я хотел написать углы в двоичной фиксированной точке, в единицах квадрантов (или, что эквивалентно, в поворотах). И я также хотел использовать четный многочлен, так как он более эффективен для вычисления, чем произвольный многочлен. Другими словами, я хотел полином P () такой, что

cos (π / 2 x) ≈ P (x 2 ) для x ∈ [0,1]

Я также потребовал, чтобы приближение было точным на обоих концах интервала, чтобы гарантировать, что cos (0) = 1 и cos (π / 2) = 0. Эти ограничения привели к форме

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

где Q () - произвольный многочлен.

Затем я искал лучшее решение в зависимости от степени Q () и нашел это:

        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

Выбор из вышеупомянутых решений является компромиссом между скоростью и точностью. Третье решение дает больше точности, чем достижимое с 16-разрядными, и это то, что я выбрал для 16-разрядной реализации.

Эдгар Бонет
источник
2
Это удивительно, @ Эдгар.
SDsolar
Что вы сделали, чтобы найти полином?
TLW
@TLW: я требовал, чтобы он имел некоторые «хорошие» свойства (например, cos (0) = 1), которые ограничены формой (1 − x²) (1 + x²Q (x²)), где Q (u) - произвольный полином (это объясняется на странице). Я взял Q первой степени (только 2 коэффициента), нашел приблизительные коэффициенты по подгонке, затем вручную настроил оптимизацию методом проб и ошибок.
Эдгар Бонет
@EdgarBonet - интересно. Обратите внимание, что эта страница не загружается для меня, хотя кэширование работает. Не могли бы вы добавить полином, используемый в этом ответе?
TLW
@TLW: добавил это к ответу.
Эдгар Бонет
4

Вы можете создать несколько функций, которые используют линейное приближение для определения sin () и cos () определенного угла.

Я думаю что-то вроде этого: для каждого я разбил графическое представление sin () и cos () на 3 раздела и сделал линейное приближение этого раздела.
линейное приближение

Ваша функция в идеале сначала проверит, что диапазон ангела находится в диапазоне от 0 до 90.
Затем она будет использовать ifelseинструкцию, чтобы определить, к какому из 3 разделов она принадлежит, а затем выполнит соответствующий линейный расчет (то есть output = mX + c).

sa_leinad
источник
Не будет ли это включать умножение с плавающей запятой?
Повелитель транзисторов
1
Не обязательно. Вы можете иметь его, чтобы выходной сигнал масштабировался между 0-100 вместо 0-1. Таким образом, вы имеете дело с целыми числами, а не с плавающей точкой. Примечание: 100 было произвольным. Нет причины, по которой вы не могли бы масштабировать выходное значение между 0-128 или 0-512 или 0-1000 или 0-1024. Используя кратное 2, вам нужно только сделать правильные сдвиги, чтобы уменьшить результат.
sa_leinad
Довольно умно, @sa_leinad. Upvote. Я помню, как делал это при работе со смещением транзисторов.
SDsolar
4

Я искал других людей, которые приблизились к cos () и sin (), и я наткнулся на этот ответ:

Ответ dtb на «Fast Sin / Cos с использованием предварительно вычисленного массива перевода»

По сути, он вычислил, что функция math.sin () из математической библиотеки была быстрее, чем использование справочной таблицы значений. Но из того, что я могу сказать, это было вычислено на ПК.

В Arduino есть математическая библиотека, которая может вычислять sin () и cos ().

sa_leinad
источник
1
ПК имеют встроенные FPU, которые делают это быстро. Arduino нет, и это делает это медленно.
Majenko
Ответ также для C #, который выполняет такие вещи, как проверка границ массива.
Майкл
3

Таблица поиска будет самым быстрым способом найти синусы. И если вам удобно работать с числами с фиксированной запятой (целыми числами, двоичная точка которых находится где-то иным, чем справа от бита-0), ваши дальнейшие вычисления с синусами также будут намного быстрее. Эта таблица может быть таблицей слов, возможно, во Flash для экономии места в оперативной памяти. Обратите внимание, что в вашей математике вам может потребоваться использование длинных для больших промежуточных результатов.

JRobert
источник
1

В общем, справочная таблица> аппроксимация -> расчет. оперативная память> вспышка. целое число> фиксированная точка> с плавающей точкой. предварительный расчет> расчет в реальном времени. зеркальное отображение (синус к косинусу или косинус к синусу) против фактического вычисления / поиска ....

у каждого есть свои плюсы и минусы.

Вы можете создавать всевозможные комбинации, чтобы увидеть, какие из них лучше всего подходят для вашего приложения.

редактировать: я сделал быструю проверку. при использовании 8-разрядного целочисленного вывода вычисление значений 1024 sin с помощью справочной таблицы занимает 0,6 мс, а 133 мс - с плавающей запятой или в 200 раз медленнее.

dannyf
источник
1

У меня был похожий вопрос к ОП. Я хотел создать таблицу LUT для вычисления первого квадранта функции синуса как 16-разрядных целых чисел без знака, начиная с 0x8000 до 0xffff. И я закончил тем, что написал это для удовольствия и выгоды. Примечание: это будет работать более эффективно, если я буду использовать операторы if. Также это не очень точно, но было бы достаточно точно для синусоиды в синтезаторе звука

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

Теперь, чтобы вернуть значения, используйте эту функцию. Она принимает значение от 0x0000 до 0x0800 и возвращает соответствующее значение из 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

Помните, что это не самый эффективный подход к этой задаче, я просто не мог понять, как сделать ряд Тейлора, чтобы выдавать результаты в соответствующем диапазоне.

Arnadath
источник
Ваш код не компилируется: Imm_UI_Aобъявляется дважды, объявления a ;и некоторые переменные отсутствуют и uLut_0должны быть глобальными. С необходимыми исправлениями lu_sin()это быстро (между 27 и 42 циклами ЦП), но очень неточно (максимальная ошибка ≈ 5.04e-2). Я не могу понять смысл этих «полиномов Арнадата»: это кажется довольно сложным вычислением, но результат почти такой же плохой, как и простое квадратичное приближение. Метод также имеет огромную стоимость памяти. Было бы лучше вычислить таблицу на вашем ПК и поместить ее в исходный код в виде PROGMEMмассива.
Эдгар Бонет
1

Просто для удовольствия и чтобы доказать, что это возможно, я закончил процедуру сборки AVR для вычисления результатов sin (x) в 24 битах (3 байта) с одним битом ошибки. Угол ввода указывается в градусах с одной десятичной цифрой, от 000 до 900 (0 ~ 90,0) только для первого квадранта. Он использует менее 210 инструкций AVR и работает в среднем 212 микросекунд, варьируя от 211us (угол = 001) до 213us (угол = 899).

Все это заняло несколько дней, более 10 дней (свободные часы), чтобы подумать о лучшем алгоритме расчета, с учетом микроконтроллера AVR, без плавающей запятой, исключив все возможные деления. Что потребовало больше времени, чтобы сделать правильные повышающие значения для целых чисел, чтобы иметь хорошую точность, необходимо повысить значения от 1e-8 до двоичных целых чисел 2 ^ 28 или больше. Как только все виновники ошибок точности и округления были найдены, их разрешение вычислений увеличилось на дополнительные 2 ^ 8 или 2 ^ 16, наилучшие результаты были достигнуты. Сначала я смоделировал все вычисления в Excel, следя за тем, чтобы все значения, такие как Int (x) или Round (x, 0), точно представляли обработку ядра AVR.

Например, в алгоритме угол должен быть в радианах, а ввод в градусах для удобства пользователя. Чтобы преобразовать градусы в радианы, используется тривиальная формула: рад = градусы * PI / 180, это выглядит красиво и легко, но это не так, PI - это бесконечное число - если использование нескольких цифр приведет к ошибкам на выходе, для деления на 180 Манипулирование битами AVR, так как оно не имеет инструкции деления, и более того, в результате потребуется число с плавающей запятой, так как в нем используются числа, намного меньшие целого числа 1. Например, радиан 1 ° (градуса) равен 0,017453293. Поскольку PI и 180 являются константами, почему бы не изменить это на простое умножение? PI / 180 = 0,017453293, умножьте его на 2 ^ 32, и получится константа 74961320 (0x0477D1A8), умножьте это число на свой угол в градусах, скажем, 900 для 90 ° и сдвинем его на 4 бита вправо (÷ 16), чтобы получить 4216574250 (0xFB53D12A), то есть радианы 90 ° с расширением 2 ^ 28, помещаются в 4 байта, без единого деления (кроме 4 сдвиг вправо). В некотором смысле, ошибка, включенная в такой трюк, меньше, чем 2 ^ -27.

Таким образом, все дальнейшие вычисления должны помнить, что это на 2 ^ 28 выше и заботиться об этом. Вам нужно разделить результаты на ходу на 16, 256 или даже 65536, чтобы избежать использования ненужных растущих байтов голода, которые не помогут при разрешении. Это была кропотливая работа, просто найти минимальное количество бит в каждом результате вычислений, поддерживая точность результатов около 24 бит. Каждое из нескольких вычислений было выполнено в попытке / ошибке с большим или меньшим числом битов в потоке Excel, отслеживая общее количество битов ошибок в результате на графике, показывающем 0-90 ° с макросом, выполняющим код 900 раз, один раз за десятую часть степени. Этот «визуальный» подход Excel был инструментом, который я создал, очень помог найти лучшее решение для каждой части кода.

Например, округляя этот конкретный результат вычисления с 13248737.51 до 13248738 или просто теряя десятичные дроби "0.51", насколько это повлияет на точность конечного результата для всех 900 тестов входных углов (00.1 ~ 90.0)?

Я мог держать животное в пределах 32 бит (4 байта) при каждом расчете, и в результате получил магию для получения точности в пределах 23 бит от результата. При проверке целых 3 байтов результата ошибка составляет ± 1 младший бит, выдающийся.

Пользователь может получить один, два или три байта из результата для своих требований точности. Конечно, если достаточно одного байта, я бы рекомендовал использовать одну таблицу sin размером 256 байт и использовать инструкцию AVR 'LPM' для ее получения.

После того как последовательность Excel стала гладкой и аккуратной, окончательный перевод из Excel в сборку AVR занял менее 2 часов, как обычно вы должны думать в первую очередь, работать позже.

В то время я смог еще больше сжать и уменьшить использование регистров. Фактический (не окончательный) код использует около 205 инструкций (~ 410 байт), выполняет вычисление sin (x) в среднем 212us, тактовая частота 16 МГц. На этой скорости он может вычислять 4700+ sin (x) в секунду. Не важно, но он может работать с точной синусоидой до 4700 Гц с 23 битами точности и разрешения без каких-либо таблиц поиска.

Базовый алгоритм основан на рядах Тейлора для sin (x), но сильно изменен, чтобы соответствовать моим намерениям с микроконтроллером AVR и точностью.

Даже то, что использование таблицы 2700 байт (900 записей * 3 байта) было бы привлекательным для скорости, что это за удовольствие или опыт обучения на этом? Конечно, подход CORDIC также рассматривался, может быть, позже, дело здесь в том, чтобы втиснуть Тейлора в ядро ​​AVR и взять воду из сухой породы.

Интересно, может ли Arduino "sin (78.9 °)" запускать Processing (C ++) с точностью 23 бита менее чем за 212us и необходимым кодом, меньшим, чем 205 инструкций. Может быть, если C ++ использует CORDIC. Эскизы Arduino могут импортировать код сборки.

Не имеет смысла размещать код здесь, позже я отредактирую этот пост, добавив на него ссылку, возможно, в моем блоге по этому адресу . Блог в основном на португальском языке.

Это хобби без денег было интересным, раздвигая границы механизма AVR почти 16MIPS на 16 МГц, без инструкции деления, умножение только в 8x8 бит. Это позволяет рассчитать sin (x), cos (x) [= sin (900-x)] и tan (x) [= sin (x) / sin (900-x)].

Помимо всего прочего, это помогло сохранить мой 63-летний мозг полированным и смазанным. Когда подростки говорят, что «старики» ничего не знают о технологиях, я отвечаю: «Подумайте еще раз, кто, по вашему мнению, создал основы для всего, чем вы наслаждаетесь сегодня?».

ура

Вагнер Лип
источник
Ницца! Несколько замечаний: 1. Стандартная sin()функция имеет примерно ту же точность, что и ваша, и в два раза быстрее. Он также основан на полиноме. 2. Если произвольный угол должен быть округлен до ближайшего кратного 0,1 °, это может привести к ошибке округления до 8,7e-4, что сводит на нет преимущество 23-битной точности. 3. Не могли бы вы поделиться своим полиномом?
Эдгар Бонет
1

Как уже упоминали другие, таблицы поиска - это путь, если вам нужна скорость. Недавно я исследовал вычисление тригонометрических функций на ATtiny85 для использования быстрых средних векторов (ветер в моем случае). Всегда есть компромисс ... для меня мне нужно было только угловое разрешение в 1 градус, так что лучше всего было использовать таблицу соответствия 360 int (масштабирование от -32767 до 32767, работа только с int). Восстановление синуса - это просто предоставление индекса 0-359 ... очень быстро! Некоторые цифры из моих тестов:

Время просмотра FLASH (us): 0.99 (таблица хранится с использованием PROGMEM)

Время поиска ОЗУ (сша): 0,69 (таблица в ОЗУ)

Время освобождения (сша): 122,31 (с использованием Arduino Lib)

Обратите внимание, что это средние значения по выборке в 360 пунктов для каждого. Тестирование было сделано на нано.

acicuc
источник