題:
在Arduino中以不同方式(最快)計算正弦(和余弦)
Transistor Overlord
2017-04-29 14:09:36 UTC
view on stackexchange narkive permalink

我正在使用Arduino Uno板來計算系統(機器人手臂)的角度。使用ADC的整個範圍,角度實際上是ADC的10位值(0至1023)。我只會在第一象限(0至90度)中操作,正弦和余弦均為正,因此負數沒有問題。我的疑問可以用三個問題來表達:

  1. 在Arduino上計算這些三角函數的不同方法是什麼?

  2. 什麼最快的方法是一樣的嗎?

  3. 在Arduino IDE中有sin()和cos()函數,但是Arduino實際如何計算它們(如它們使用查找表一樣,或者近似等)?它們似乎是一個顯而易見的解決方案,但是在嘗試之前,我想知道它們的實際實現。

  4. ol>

    PS:我對Arduino IDE上的兩種標準編碼都持開放態度和彙編代碼,以及未提及的任何其他選項。我也沒有錯誤和近似的問題,這對於數字系統是不可避免的。但是,如果可能的話,最好提一下可能的錯誤程度

您可以接受近似值嗎?
是的,實際上,但是我想知道不同方法的錯誤程度。這不是精密產品,而是我的附帶項目。實際上,幾乎所有實現數學功能的數字系統(如果沒有的話)都是不可避免的近似值
我以為你想學習學位。您是否要輸入角度的整數或十進制數字?
度是。我認為編寫代碼和測試是否使用整數會更容易,所以我會這樣做。我將提供有關編輯的更清晰的信息
對於僅90(整數)度,一個90條目的查找表將是最快和最有效的。實際上,對於完整的360度,您可以使用90項查找表。只需將其反向閱讀90-179,然後將其翻轉180-269。兩者都進行270-359。
您能否量化您的準確性要求?近似cos(π/ 2x)≈1-x²具有5.6e-2的最大誤差。並且(3−x²)(1−0.224x²)的乘積為3,乘積在9.20e-4之內。
-1
十 答案:
Majenko
2017-04-29 16:34:04 UTC
view on stackexchange narkive permalink

兩種基本方法是數學計算(帶多項式)和查找表。

Arduino的數學庫(libm,avr-libc的一部分)使用了前者。它針對AVR進行了優化,因為它是使用100%彙編語言編寫的,因此幾乎無法跟隨其工作(註釋也為零)。放心吧,儘管它將是最優化的純浮點實現方法,但我們的腦筋要遠遠勝過我們。

但是關鍵是 float 。與純整數相比,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.0的1 / 65536th。這些Q16值(以及相關的Q15,Q1.15等)使用起來效率更高,因為您擁有計算機喜歡使用的二次冪,而不是計算機討厭使用的二次冪。

也不要忘記 sin()函數期望弧度,因此首先必須將整數度轉換為浮點弧度值,並使用與可以直接使用整數度值的查找表相比,sin()的效率更低。

但這是兩種情況的結合。 線性插值可讓您獲得兩個整數之間的浮點角的近似值。這很簡單,只需計算出查找表中兩點之間的距離,然後根據兩個值的距離創建加權平均值。例如,如果您在23.6度,則使用(sintable [23] *(1-0.6))+(sintable [24] * 0.6)。基本上,您的正弦波變成了一系列由直線連接在一起的離散點。您以準確性為代價。

不久前,我寫了一個庫,它使用針對Taylor sin / cos的泰勒多項式比該庫快。給定的是,我使用浮點弧度作為兩者的輸入。
Halzephron
2017-04-29 23:07:15 UTC
view on stackexchange narkive permalink

這裡有一些很好的答案,但是我想添加一種尚未被提及的方法,一種非常適合在嵌入式系統上計算三角函數的方法,這就是CORDIC技術。 Wiki Entry Here它可以僅使用移位和加法以及小的查詢表來計算trig函數。

這是C語言中的一個粗略示例。實際上,它使用CORDIC實現了C庫的atan2()函數(即找到一個給定兩個正交分量的角度。)它使用浮點數,但可以用於定點算法。

  / * *使用CORDIC算法的簡單示例。 * /#include <stdio.h>#include <math.h>#define CORDIC_TABLE_SIZE 16double cordic_table [CORDIC_TABLE_SIZE]; void init_table(void); double angle(double I,double Q); / * *給出正弦值,使用CORIDC *算法計算角度。 * /雙角(雙I,雙Q){int L;雙K = 1; double angle_acc = 0;雙tmp_I; if(I < 0){/ *旋轉初始+/- 90度* / tmp_I = I; if(Q > 0.0){I = Q; / *減去90度* / Q = -tmp_I; angle_acc = -90; }其他{I = -Q; / *加90度* / Q = tmp_I; angle_acc = 90; }}其他{angle_acc = 0; } / *使用(1 + jK)因子* /旋轉(L = 0,K = 1; L < = CORDIC_TABLE_SIZE; L ++){tmp_I = I;如果(Q > = 0.0){/ *角度為正:做負旋轉* / I + = Q * K; Q-= tmp_I * K; angle_acc-= cordic_table [L]; } else {/ *角度為負:正向旋轉* / I-= Q * K; Q + = tmp_I * K; angle_acc + = cordic_table [L]; } K / = 2.0; } return -angle_acc;} void init_table(void){int i;雙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(“#角度,CORDIC角度,錯誤\ n”);對於(A = 0; < 90.0; A + = 0.5){Ar = A * M_PI / 180; / *轉換為C的正弦的弧度& cos fn * / R = 5; //任意半徑I = R * cos(Ar); Q = R * sin(Ar); Ac =角度(I,Q); printf(“%9f,%9f,%12.4e \ n”,A,Ac,Ac-A); } return 0;}  

但是請先嘗試使用本機Arduino trig函數-無論如何它們可能足夠快。

過去,我在stm8上採用了類似的方法。它需要兩個步驟:1)從sin(2x)計算sin(x)和cos(x),然後2)從sin(x),sin(x / 2)計算sin(x +/- x / 2) ,cos(x)和cos(x / 2)->通過迭代,您可以逼近目標。以我為例,我以45度(0.707)開始,然後向目標前進。它比標準IAR sin()函數要慢得多。
Edgar Bonet
2017-04-30 00:21:00 UTC
view on stackexchange narkive permalink

我一直在使用定點多項式逼近在Arduino上計算正弦和余弦。與來自avr-libc的標準 cos() sin()相比,這是我對平均執行時間和最壞情況錯誤的度量:

 函數最大錯誤循環時間----------------------------------------- cos_fix() 9.53e-5 108.25 6.77 µssin_fix()9.53e-5 110.25 6.89 µscos()2.98e-8 1720.8 107.5 µssin()2.98e-8 1725.1 107.8 µs  

它基於第六僅用4次乘法計算的度多項式。乘法本身是用彙編完成的,因為我發現gcc效率低下。角度以每轉1/65536的單位表示為 uint16_t ,這使得角度的運算自然可以以一轉為模。

如果您認為這樣可以滿足您的要求,請在這裡是代碼: Fixed-pointtrigonometry。很抱歉,我仍然沒有翻譯此頁面(法語),但是您可以理解方程式,並且代碼(變量名,註釋...)位於


編輯:由於服務器似乎已消失,因此以下是我找到的近似值的信息。

我想寫角度以二進制定點,以像限為單位(或等效地,依次)。我還想使用偶數多項式,因為它們比任意多項式更有效地計算。換句話說,我想要一個多項式P()使得對於x∈[0,1]

cos(π/ 2 x)≈P(x 2 sup>)

我還要求在區間的兩端都必須近似逼近,以確保cos(0)= 1和cos(π/ 2)= 0.這些約束導致形式為

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

其中Q()是任意多項式。

接下來,我搜索最佳解為Q()的度的函數,並得出以下結果:

  Q(u)│P(x²)的度│最大誤差
────────────────────┼──┼────────────┼ ────0│2│5.60e-2 −0.224│4│9.20e-4−0.2335216 + 0.0190963 u│6│9.20e-6  

在上述解決方案中進行選擇是速度/精度的權衡。第三種解決方案提供的精度比16位解決方案要高,這是我為16位實現選擇的解決方案。

太神奇了,@Edgar。
您如何找到多項式?
我要求@TLW:具有一些“不錯的”屬性(例如cos(0)= 1),並將其約束為(1-x²)(1 +x²Q(x²))形式,其中Q(u)是任意多項式(在頁面中進行了說明)。我取了一級Q(僅2個係數),通過擬合找到了近似係數,然後通過反複試驗手動優化了優化。
@EdgarBonet-有趣。請注意,儘管緩存有效,但該頁面不會為我加載。您能否將用於此答案的多項式相加?
@TLW:將此添加到了答案中。
sa_leinad
2017-04-29 16:44:59 UTC
view on stackexchange narkive permalink

您可以創建幾個函數,這些函數使用線性逼近來確定特定角度的sin()和cos()。

我在想這樣的事情:
linear approximation
對於每一個,我都將sin()和cos()的圖形表示分為三個部分,並對該部分進行了線性近似。

您的函數最好首先檢查天使的範圍在0到90之間。
然後它將使用 ifelse 語句確定它屬於3個部分中的哪一個,然後進行相應的線性計算(即 output = mX + c

這會不會涉及浮點乘法?
不必要。您可以使用它,以便將輸出縮放為0-100,而不是0-1。這樣,您正在處理整數,而不是浮點數。注意:100是任意的。沒有理由不能在0-128或0-512或0-1000或0-1024之間縮放輸出。通過使用2的倍數,您只需要進行右移即可將結果縮小。
非常聰明,@sa_leinad。贊成。我記得在使用晶體管偏置時會這樣做。
sa_leinad
2017-04-29 16:49:45 UTC
view on stackexchange narkive permalink

我尋找了近似於cos()和sin()的其他人,然後遇到了這個答案:

dtb對“使用預先計算的轉換數組的快速Sin / Cos”的答案

他基本上算出了數學庫中的math.sin()函數比使用值查找表要快。但是據我所知,這是在PC上計算的。

Arduino有一個數學庫,可以計算sin()和cos()。

PC內置有FPU,可以使其快速運行。 Arduino沒有,這使它變慢。
答案也適用於C#,它執行諸如數組邊界檢查之類的工作。
JRobert
2017-04-29 17:25:22 UTC
view on stackexchange narkive permalink

查找表將是查找異常的最快方法。而且,如果您願意使用定點數(整數位不在二進制位0右邊的整數)進行計算,則使用正弦的進一步計算也將更快。然後,該表可以是單詞表,可能在Flash中以節省RAM空間。請注意,在數學運算中,可能需要使用long以獲得較大的中間結果。

dannyf
2017-04-29 16:34:02 UTC
view on stackexchange narkive permalink

通常,查詢表>近似->計算。內存>閃光燈。整數>定點>浮點。預計算>實時計算。鏡像(正弦到餘弦或餘弦到正弦)與實際計算/查找...。

每個都有其優缺點。

您可以進行各種組合看看哪種方法最適合您的應用程序。

編輯:我進行了快速檢查。使用8位整數輸出,使用查找表計算1024個sin值需要0.6毫秒,使用浮點數需要133毫秒,或者慢200倍。

Arnadath
2017-10-30 02:50:49 UTC
view on stackexchange narkive permalink

我對OP有一個類似的問題。我想製作一個LUT表以將正弦函數的第一象限計算為從0x8000到0xffff的無符號16位整數。最後我為了娛樂和利潤而寫了這篇。注意:如果我使用'if'語句,這將更有效。同樣,它也不是很準確,但是對於聲音合成器中的正弦波而言,它的精確度足夠了

  void sin_lut_ctor(){//為正弦函數的511個項查詢表。/ /插入一些多項式來做一些魔術//,並且得到接近π/ 2的正弦的近似值。/////π/ 2以外的所有正弦都可以用mathconst uint16_t uLut_d = 0x0200導出; //π/ 2個項的最大LUT深度。 uint16_t uLut_0 [uLut_d]; // LUT本身。//將上面的2放在void setup()之前作為全局變量。//此係數僅適用於uLut_d = 511.uint16_t arna_poly_0 = 0x000a; // 11uint16_t arna_poly_1 = 0x0001; // 1uint16_t arna_poly_2 = 0x0007; // 7uint16_t arna_poly_3 = 0x0001; // 1個預先計算的Polynomialsuint16_t arna_poly_4 = 0x0001; // 1 uint16_t arna_poly_5 = 0x0007; // 7uint16_t arna_poly_6 = 0x0002; // 2uint16_t arna_poly_7 = 0x0001; // 1uint16_t Imm_UI_0 = 0x0001; // Itteratoruint16_t Imm_UI_1 = 0x007c; //時間減少的增量器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; //臨時變量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; // 510uLut_0 [0x0000] = 0x8000; //假設中間點是32768(十六進制0x8000),而(Imm_UI_0 < Imm_UI_A)//構造正弦表的四分之一{
Imm_UI_2 ++; //將臨時變量增加1Imm_UI_B = Imm_UI_2 / arna_coeff_0; //將其與第一個係數相除(注意:整數除法)Imm_UI_3 + = Imm_UI_B; //如果第一個臨時值增加到第一個係數,則增加下一個臨時值。Imm_UI_1-= Imm_UI_B; //如果是這種情況,則減少增量器Imm_UI_2 * = 0x001-Imm_UI_B; //將第一個臨時變量設置回0Imm_UI_B = Imm_UI_3 / arna_poly_1; //使用下一組臨時變量執行與以前相同的操作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; //再三...再三...你有了主意。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_5 * = 0×0001  -  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 * = 0×0001  -  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 * = 0×0001 -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上一個將要跳過的變量/下一個要遞增的變量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); //將當前值設置為上一個由我們的增量值增加的值Imm_UI_0 ++; //增加發射器} uLut_0 [Imm_UI_A] = 0xffff; //最後,將最後一個值設置為0xffff //就可以了。現在只有一個if語句(一個while循環)的正弦表}  

現在要獲取值,請使用此函數。它接受0x0000到0x0800的值,並從LUT

  uint16_t lu_sin(uint16_t lu_val0){//獲取從0x0000到0x0800的值。返回適當的sin(值)Imm_UI_0 = lu_val0 / 0x0200; //確定象限Imm_UI_1 = lu_val0%0x0200; //如果(Imm_UI_0 == 0x0000){返回uLut_0 [Imm_UI_1]; }如果(Imm_UI_0 == 0x0001){返回uLut_0 [0x01ff-Imm_UI_1]; }如果(Imm_UI_0 == 0x0002){返回0xffff-uLut_0 [Imm_UI_1]; }如果(Imm_UI_0 == 0x0003){返回0xffff-uLut_0 [0x01ff-Imm_UI_1]; }} //我在這裡使用了if語句,但與上面的代碼塊類似,//您可以不使用它。請記住,這不是整數除法和取模的方法 

,這並不是執行此任務的最有效方法,我只是想不出如何使taylor級數在適當範圍內給出結果

您的代碼無法編譯:Imm_UI_A被聲明兩次,`;和一些變量聲明丟失,並且uLut_0應該是全局的。通過必要的修復,`lu_sin()`很快(在27和42個CPU週期之間),但是_非常不准確(最大錯誤≈5.04e-2)。我無法理解這些“ Arnadathian多項式”:似乎是很繁重的計算,但是結果幾乎和簡單的二次逼近一樣差。該方法還具有巨大的存儲成本。最好在PC上計算表並將其作為PROGMEM數組放入源代碼中。
Wagner Lip
2019-03-18 21:29:49 UTC
view on stackexchange narkive permalink

僅出於樂趣,並證明它可以完成,我完成了一個AVR彙編例程,以24位(3字節)的格式計算sin(x)結果,但有1位錯誤。輸入角度以度為單位,以十進制數為單位,僅第一個像限為000到900(0〜90.0)。它使用不到210條AVR指令,平均運行時間為212微秒,從211us(角度= 001)到213us(角度= 899)不等。天(空閒時間),只考慮最佳計算算法,考慮到AVR微控制器,沒有浮點數,消除了所有可能的除法。花更多的時間來為整數設置正確的升壓值,要具有良好的精度,需要將1e-8的值升壓為2 ^ 28或更大的二進制整數。一旦找到所有精度和舍入誤差元,將其計算分辨率提高2 ^ 8或2 ^ 16,就可以達到最佳結果。我首先在Excel上模擬了所有計算,同時將所有值都設為Int(x)或Round(x,0)來表示AVR核心處理。

例如,在算法中,角度必須以弧度為單位,輸入以度為單位,以方便用戶使用。要將度數轉換為弧度,平凡的公式是rad = degrees * PI / 180,這看起來很好,很簡單,但不是,PI是一個無限數-如果使用很少的數字,它將在輸出中產生錯誤,除以180要求AVR位操作,因為它沒有除法指令,並且除此以外,由於涉及遠遠低於整數1的數字,結果將需要浮點運算。例如,1°(度)的Radian為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 LSB,非常明顯。

用戶可以根據自己的精度要求從結果中抓取一個,兩個或三個字節。當然,如果僅一個字節就足夠了,我建議使用一個256字節的sin表,並使用AVR'LPM'指令來抓取它。

一旦我使Excel序列運行流暢,整潔,從Excel到AVR程序集的最終翻譯就用了不到2個小時,像往常一樣,您應該首先考慮更多,然後再減少工作。

那時,我可以進行更多壓縮並減少寄存器使用量。實際的(不是最終的)代碼使用大約205條指令(〜410字節),平均以212us運行sin(x)計算,時鐘為16MHz。以該速度,它每秒可以計算4700+ sin(x)。並不重要,但是它可以運行高達4700Hz的精確正弦波,具有23位的精度和分辨率,而無需任何查找表。

基本算法基於針對sin(x)的泰勒級數,但是對其​​進行了很多修改,以適應AVR微控制器和精確度的要求。

即使使用2700字節表(900個條目* 3個字節)將具有極好的速度吸引力,在此方面的樂趣或學習經驗是什麼?當然,也考慮使用CORDIC方法,也許稍後,這裡的重點是將Taylor擠入AVR核心並從乾燥的岩石中取水。

我想知道Arduino的“ sin(78.9°)”是否可以以小於212us的精度運行23位處理(C ++),所需的代碼小於205條指令。可能是C ++使用CORDIC。 Arduino草圖可以導入彙編代碼。

沒有任何必要在此處發布代碼,稍後,我將對這篇文章進行編輯,以包括指向該文章的Web鏈接,可能會在我的博客中,此URL

這是一項業餘愛好,很有趣,它以16MHz的速度推動了幾乎16MIPS的AVR引擎的極限,無須除法指令,僅以8x8位進行乘法運算。它允許計算sin(x),cos(x)[= sin(900-x)]和tan(x)[= sin(x)/ sin(900-x)]。

最重要的是,這有助於使我63歲的大腦保持光彩照人。當青少年說“老年人”對技術一無所知時,我回答“再想一想,您認為誰為今天享受的一切奠定了基礎?”。

乾杯

真好!一些注意事項:1. [標準的`sin()`函數](http://svn.savannah.gnu.org/viewvc/avr-libc/trunk/avr-libc/libm/fplib/fp_sinus.S?revision = 2473&view = markup)的精度幾乎與您相同,並且速度快一倍。它也基於多項式。 2.如果必須將任意角度四捨五入到最接近的0.1°倍數,則可能導致舍入誤差高達8.7e-4,這抵消了23位精度的好處。 3.您介意共享多項式嗎?
acicuc
2020-02-06 06:08:56 UTC
view on stackexchange narkive permalink

就像其他人提到的那樣,查找表是提高速度的一種方法。我最近一直在研究ATtiny85上的三角函數的計算,以使用快速矢量平均值(在我的情況下為風)。總是存在折衷...對我來說,我只需要1度角分辨率,因此查找360 int(將-32767縮放為32767,只能與int一起使用)的查找表是最好的選擇。檢索正弦僅是提供索引0-359的問題...非常快!我測試中的一些數字:

閃存查找時間(us):0.99(使用PROGMEM存儲的表)

RAM查找時間(us):0.69(RAM中的表)

庫時間(us):122.31(使用Arduino庫)

請注意,這些是每個360點樣本的平均值。測試是在nano上完成的。



該問答將自動從英語翻譯而來。原始內容可在stackexchange上找到,我們感謝它分發的cc by-sa 3.0許可。
Loading...