Я использую плату Arduino Uno для вычисления углов моей системы (манипулятор). Углы на самом деле являются 10-битными значениями (от 0 до 1023) от АЦП, используя полный диапазон АЦП. Я собираюсь работать только в 1-м квадранте (от 0 до 90 градусов), где и синусы, и косинусы положительны, поэтому проблем с отрицательными числами нет. Мои сомнения можно выразить в 3 вопросах:
Каковы различные способы вычисления этих тригонометрических функций на Arduino?
Какой самый быстрый способ сделать то же самое?
В Arduino IDE есть функции sin () и cos (), но как Arduino фактически вычисляет их (как они используют справочные таблицы или приближения и т. Д.)? Они кажутся очевидным решением, но я хотел бы узнать их фактическую реализацию, прежде чем опробовать их.
PS: я открыт как для стандартного кодирования в Arduino IDE, так и для кодирования сборки, а также для любых других не упомянутых опций. Также у меня нет проблем с ошибками и аппроксимациями, которые неизбежны для цифровой системы; однако, если это возможно, было бы хорошо упомянуть степень возможных ошибок
источник
Ответы:
Два основных метода - математический расчет (с полиномами) и таблицы поиска.
Математическая библиотека 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)
. По сути, ваша синусоида становится серией дискретных точек, соединенных между собой прямыми линиями. Вы обмениваете точность на скорость.источник
Здесь есть несколько хороших ответов, но я хотел бы добавить метод, который еще не был упомянут, который очень хорошо подходит для вычисления тригонометрических функций во встроенных системах, и это метод CORDIC. Wiki Entry Here Здесь можно вычислять тригонометрические функции, используя только сдвиги и добавляет и небольшой справочный стол.
Вот грубый пример на C. В сущности, он реализует функцию atan2 () из библиотек C, используя CORDIC (т. Е. Находит угол с учетом двух ортогональных компонентов). Он использует плавающую точку, но может быть адаптирован для использования с арифметикой с фиксированной точкой.
Но сначала попробуйте родные функции триггера Arduino - они могут быть достаточно быстрыми в любом случае.
источник
Я немного поиграл с вычислением синусов и косинусов на Arduino, используя полиномиальные приближения с фиксированной точкой. Вот мои измерения среднего времени выполнения и наихудшей ошибки по сравнению со стандартом
cos()
иsin()
из avr-libc:Он основан на полиноме 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 () и нашел это:
Выбор из вышеупомянутых решений является компромиссом между скоростью и точностью. Третье решение дает больше точности, чем достижимое с 16-разрядными, и это то, что я выбрал для 16-разрядной реализации.
источник
Вы можете создать несколько функций, которые используют линейное приближение для определения sin () и cos () определенного угла.
Я думаю что-то вроде этого: для каждого я разбил графическое представление sin () и cos () на 3 раздела и сделал линейное приближение этого раздела.
Ваша функция в идеале сначала проверит, что диапазон ангела находится в диапазоне от 0 до 90.
Затем она будет использовать
ifelse
инструкцию, чтобы определить, к какому из 3 разделов она принадлежит, а затем выполнит соответствующий линейный расчет (то естьoutput = mX + c
).источник
Я искал других людей, которые приблизились к cos () и sin (), и я наткнулся на этот ответ:
Ответ dtb на «Fast Sin / Cos с использованием предварительно вычисленного массива перевода»
По сути, он вычислил, что функция math.sin () из математической библиотеки была быстрее, чем использование справочной таблицы значений. Но из того, что я могу сказать, это было вычислено на ПК.
В Arduino есть математическая библиотека, которая может вычислять sin () и cos ().
источник
Таблица поиска будет самым быстрым способом найти синусы. И если вам удобно работать с числами с фиксированной запятой (целыми числами, двоичная точка которых находится где-то иным, чем справа от бита-0), ваши дальнейшие вычисления с синусами также будут намного быстрее. Эта таблица может быть таблицей слов, возможно, во Flash для экономии места в оперативной памяти. Обратите внимание, что в вашей математике вам может потребоваться использование длинных для больших промежуточных результатов.
источник
В общем, справочная таблица> аппроксимация -> расчет. оперативная память> вспышка. целое число> фиксированная точка> с плавающей точкой. предварительный расчет> расчет в реальном времени. зеркальное отображение (синус к косинусу или косинус к синусу) против фактического вычисления / поиска ....
у каждого есть свои плюсы и минусы.
Вы можете создавать всевозможные комбинации, чтобы увидеть, какие из них лучше всего подходят для вашего приложения.
редактировать: я сделал быструю проверку. при использовании 8-разрядного целочисленного вывода вычисление значений 1024 sin с помощью справочной таблицы занимает 0,6 мс, а 133 мс - с плавающей запятой или в 200 раз медленнее.
источник
У меня был похожий вопрос к ОП. Я хотел создать таблицу LUT для вычисления первого квадранта функции синуса как 16-разрядных целых чисел без знака, начиная с 0x8000 до 0xffff. И я закончил тем, что написал это для удовольствия и выгоды. Примечание: это будет работать более эффективно, если я буду использовать операторы if. Также это не очень точно, но было бы достаточно точно для синусоиды в синтезаторе звука
Теперь, чтобы вернуть значения, используйте эту функцию. Она принимает значение от 0x0000 до 0x0800 и возвращает соответствующее значение из LUT
Помните, что это не самый эффективный подход к этой задаче, я просто не мог понять, как сделать ряд Тейлора, чтобы выдавать результаты в соответствующем диапазоне.
источник
Imm_UI_A
объявляется дважды, объявления a;
и некоторые переменные отсутствуют иuLut_0
должны быть глобальными. С необходимыми исправлениямиlu_sin()
это быстро (между 27 и 42 циклами ЦП), но очень неточно (максимальная ошибка ≈ 5.04e-2). Я не могу понять смысл этих «полиномов Арнадата»: это кажется довольно сложным вычислением, но результат почти такой же плохой, как и простое квадратичное приближение. Метод также имеет огромную стоимость памяти. Было бы лучше вычислить таблицу на вашем ПК и поместить ее в исходный код в видеPROGMEM
массива.Просто для удовольствия и чтобы доказать, что это возможно, я закончил процедуру сборки 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-летний мозг полированным и смазанным. Когда подростки говорят, что «старики» ничего не знают о технологиях, я отвечаю: «Подумайте еще раз, кто, по вашему мнению, создал основы для всего, чем вы наслаждаетесь сегодня?».
ура
источник
sin()
функция имеет примерно ту же точность, что и ваша, и в два раза быстрее. Он также основан на полиноме. 2. Если произвольный угол должен быть округлен до ближайшего кратного 0,1 °, это может привести к ошибке округления до 8,7e-4, что сводит на нет преимущество 23-битной точности. 3. Не могли бы вы поделиться своим полиномом?Как уже упоминали другие, таблицы поиска - это путь, если вам нужна скорость. Недавно я исследовал вычисление тригонометрических функций на ATtiny85 для использования быстрых средних векторов (ветер в моем случае). Всегда есть компромисс ... для меня мне нужно было только угловое разрешение в 1 градус, так что лучше всего было использовать таблицу соответствия 360 int (масштабирование от -32767 до 32767, работа только с int). Восстановление синуса - это просто предоставление индекса 0-359 ... очень быстро! Некоторые цифры из моих тестов:
Время просмотра FLASH (us): 0.99 (таблица хранится с использованием PROGMEM)
Время поиска ОЗУ (сша): 0,69 (таблица в ОЗУ)
Время освобождения (сша): 122,31 (с использованием Arduino Lib)
Обратите внимание, что это средние значения по выборке в 360 пунктов для каждого. Тестирование было сделано на нано.
источник