Các cách khác nhau (và nhanh nhất) để tính toán các sin (và cosin) trong Arduino


9

Tôi đang sử dụng bảng Arduino Uno để tính toán các góc của hệ thống của tôi (cánh tay robot). Các góc thực sự là các giá trị 10 bit (0 đến 1023) từ ADC, sử dụng toàn bộ phạm vi của ADC. Tôi sẽ chỉ hoạt động trong góc phần tư thứ nhất (0 đến 90 độ), trong đó cả sin và cosin đều dương, vì vậy không có vấn đề gì với các số âm. Nghi ngờ của tôi có thể được thể hiện trong 3 câu hỏi:

  1. Các cách khác nhau để tính các hàm lượng giác này trên Arduino là gì?

  2. Cách nhanh nhất để làm như vậy là gì?

  3. Có các hàm sin () và cos () trong Arduino IDE, nhưng Arduino thực sự tính toán chúng như thế nào (như khi chúng sử dụng các bảng tra cứu, hoặc xấp xỉ, v.v.)? Chúng có vẻ như là một giải pháp rõ ràng, nhưng tôi muốn biết thực hiện thực tế của chúng trước khi tôi thử chúng.

PS: Tôi mở cho cả mã hóa tiêu chuẩn trên Arduino IDE và mã hóa lắp ráp, cũng như bất kỳ tùy chọn nào khác không được đề cập. Ngoài ra tôi không có vấn đề với lỗi và xấp xỉ, đó là điều không thể tránh khỏi đối với một hệ thống kỹ thuật số; tuy nhiên nếu có thể thì tốt hơn là đề cập đến mức độ lỗi có thể


Bạn sẽ ổn với giá trị gần đúng chứ?
sa_leinad

Có thực sự, nhưng tôi muốn biết mức độ lỗi của các phương pháp khác nhau. Đây không phải là một sản phẩm chính xác mà là một dự án phụ của tôi. Trên thực tế, gần đúng là không thể tránh khỏi đối với hầu hết mọi hệ thống kỹ thuật số (nếu không phải là bất kỳ) thực hiện chức năng toán học
Transitor Overlord

Tôi giả sử bạn đang muốn làm việc trong độ. Bạn sẽ muốn nhập số nguyên hoặc số thập phân cho góc?
sa_leinad

Độ có. Tôi nghĩ rằng sẽ dễ dàng hơn để viết mã và kiểm tra nếu chúng ta sử dụng số nguyên, vì vậy tôi sẽ đi với điều đó. Tôi sẽ cung cấp thêm thông tin rõ ràng về các chỉnh sửa
Transitor Overlord

1
Chỉ với 90 (số nguyên) độ, bảng tra cứu 90 mục sẽ nhanh nhất và hiệu quả nhất. Trong thực tế, đối với 360 độ đầy đủ, bạn có thể sử dụng bảng tra cứu 90 mục. Chỉ cần đọc ngược lại cho 90-179 và đảo ngược nó cho 180-269. Làm cả hai cho 270-359.
Majenko

Câu trả lời:


11

Hai phương pháp cơ bản là tính toán toán học (với đa thức) và bảng tra cứu.

Thư viện toán học của Arduino (libm, một phần của avr-libc) sử dụng cái trước. Nó được tối ưu hóa cho AVR ở chỗ nó được viết với 100% ngôn ngữ lắp ráp và vì vậy hầu như không thể làm theo những gì nó đang làm (cũng không có bình luận nào). Hãy yên tâm mặc dù nó sẽ là bộ não triển khai thuần túy được tối ưu hóa vượt trội nhất so với chúng ta có thể nghĩ ra.

Tuy nhiên, chìa khóa có float . Bất cứ điều gì trên Arduino liên quan đến dấu phẩy động sẽ trở nên nặng nề khi so sánh với số nguyên thuần và vì bạn chỉ yêu cầu các số nguyên trong khoảng từ 0 đến 90 độ, nên một bảng tra cứu đơn giản là phương pháp đơn giản và hiệu quả nhất.

Một bảng gồm 91 giá trị sẽ cung cấp cho bạn mọi thứ từ 0 đến 90. Tuy nhiên, nếu bạn tạo một bảng các giá trị dấu phẩy động giữa 0,0 và 1,0 thì bạn vẫn có hiệu quả làm việc với phao (được cấp không hiệu quả như tính toán sinvới phao), do đó, việc lưu trữ giá trị điểm cố định thay vào đó sẽ hiệu quả hơn nhiều.

Điều đó có thể đơn giản như việc lưu trữ giá trị nhân với 1000, vì vậy bạn có từ 0 đến 1000 thay vì giữa 0,0 và 1,0 (ví dụ sin (30) sẽ được lưu trữ là 500 thay vì 0,5). Hiệu quả hơn sẽ là lưu trữ các giá trị như, ví dụ, giá trị Q16 trong đó mỗi giá trị (bit) đại diện cho 1/65636 của 1.0. Các giá trị Q16 này (và Q15, Q1.15, v.v.) có hiệu quả hơn để làm việc vì bạn có quyền hạn mà hai máy tính thích làm việc thay vì quyền hạn mười mà chúng ghét làm việc.

Đừng quên rằng sin()hàm mong đợi radian, vì vậy trước tiên bạn phải chuyển đổi độ nguyên của bạn thành giá trị radian dấu phẩy động, khiến việc sử dụng sin()thậm chí không hiệu quả hơn so với bảng tra cứu có thể hoạt động trực tiếp với giá trị độ nguyên.

Một sự kết hợp của hai kịch bản, mặc dù, là có thể. Nội suy tuyến tính sẽ cho phép bạn lấy gần đúng một góc điểm nổi giữa hai số nguyên. Nó đơn giản như tìm ra khoảng cách giữa hai điểm trong bảng tra cứu và tạo trung bình có trọng số dựa trên khoảng cách của hai giá trị. Ví dụ, nếu bạn ở 23,6 độ bạn mất (sintable[23] * (1-0.6)) + (sintable[24] * 0.6). Về cơ bản sóng hình sin của bạn trở thành một chuỗi các điểm riêng biệt được nối với nhau bằng các đường thẳng. Bạn giao dịch chính xác cho tốc độ.


Tôi đã viết một thư viện một thời gian trước đó sử dụng đa thức Taylor cho sin / cos nhanh hơn thư viện. Cho, tôi đã sử dụng radian điểm nổi làm đầu vào cho cả hai.
tuskiomi

8

Có một số câu trả lời hay ở đây nhưng tôi muốn thêm một phương pháp chưa được đề cập, một phương pháp rất phù hợp để tính toán các hàm lượng giác trên các hệ thống nhúng, và đó là kỹ thuật CORDIC Wiki Entry Ở đây Nó có thể tính toán các hàm trig chỉ bằng cách dịch chuyển và thêm và một bảng tra cứu nhỏ.

Đây là một ví dụ thô trong C. Thực tế, nó thực hiện hàm atan2 () của thư viện C bằng cách sử dụng CORDIC (nghĩa là tìm một góc cho hai thành phần trực giao.) Nó sử dụng dấu phẩy động, nhưng có thể được điều chỉnh để sử dụng với số học điểm cố định.

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

Nhưng trước tiên hãy thử các chức năng Arduino trig - chúng có thể đủ nhanh.


1
Tôi đã thực hiện một cách tiếp cận tương tự trong quá khứ, trên stm8. phải mất hai bước: 1) tính sin (x) và cos (x) từ sin (2x), và sau đó 2) tính sin (x +/- x / 2) từ sin (x), sin (x / 2) , cos (x) và cos (x / 2) -> thông qua việc lặp lại, bạn có thể tiếp cận mục tiêu của mình. trong trường hợp của tôi, tôi bắt đầu với 45 độ (0,707), và tìm đường đến mục tiêu. nó chậm hơn đáng kể so với hàm IAR sin () tiêu chuẩn.
dannyf

7

Tôi đã chơi một chút với các phép tính và cosin trên Arduino bằng cách sử dụng các xấp xỉ đa thức điểm cố định. Dưới đây là các phép đo của tôi về thời gian thực hiện trung bình và lỗi trường hợp xấu nhất, so với tiêu chuẩn cos()sin()từ 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

Nó dựa trên đa thức bậc 6 được tính toán chỉ với 4 phép nhân. Các phép nhân được thực hiện trong lắp ráp, vì tôi thấy rằng gcc thực hiện chúng không hiệu quả. Các góc được biểu thị uint16_tbằng đơn vị 1/65536 của một cuộc cách mạng, điều này làm cho số học của các góc tự nhiên hoạt động theo cách mạng một cách tự nhiên.

Nếu bạn nghĩ rằng điều này có thể phù hợp với hóa đơn của bạn, thì đây là mã: Lượng giác điểm cố định . Xin lỗi, tôi vẫn không dịch trang này bằng tiếng Pháp, nhưng bạn có thể hiểu các phương trình và mã (tên biến, nhận xét ...) bằng tiếng Anh.


Chỉnh sửa : Vì máy chủ dường như đã biến mất, đây là một số thông tin về các xấp xỉ tôi tìm thấy.

Tôi muốn viết các góc theo điểm cố định nhị phân, theo đơn vị góc phần tư (hoặc, tương ứng, lần lượt). Và tôi cũng muốn sử dụng một đa thức chẵn, vì đây là những phép tính hiệu quả hơn so với đa thức tùy ý. Nói cách khác, tôi muốn một đa thức P () sao cho

cos (π / 2 x) ≈ P (x 2 ) cho x ∈ [0,1]

Tôi cũng yêu cầu xấp xỉ chính xác ở cả hai đầu của khoảng, để đảm bảo rằng cos (0) = 1 và cos (π / 2) = 0. Những ràng buộc này dẫn đến biểu mẫu

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

Trong đó Q () là một đa thức tùy ý.

Tiếp theo, tôi đã tìm kiếm giải pháp tốt nhất là hàm của mức độ Q () và tìm thấy điều này:

        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

Sự lựa chọn trong số các giải pháp trên là sự đánh đổi tốc độ / độ chính xác. Giải pháp thứ ba mang lại độ chính xác cao hơn mức có thể đạt được với 16 bit và đó là giải pháp tôi chọn để thực hiện 16 bit.


2
Điều đó thật tuyệt vời, @Edgar.
SDsolar

Bạn đã làm gì để tìm đa thức?
TLW

@TLW: Tôi yêu cầu nó phải có một số thuộc tính tốt đẹp (ví dụ cos (0) = 1), ràng buộc với dạng (1 − x²) (1 + x²Q (x²)), trong đó Q (u) là tùy ý đa thức (nó được giải thích trong trang). Tôi đã lấy Q cấp độ đầu tiên (chỉ có 2 hệ số), tìm thấy các hệ số gần đúng theo mức độ phù hợp, sau đó điều chỉnh tối ưu hóa bằng cách dùng thử và lỗi.
Edgar Bonet

@EdgarBonet - thú vị. Lưu ý rằng trang đó không tải cho tôi, mặc dù bộ nhớ cache hoạt động. Bạn có thể vui lòng thêm đa thức được sử dụng cho câu trả lời này?
TLW

@TLW: thêm vào câu trả lời.
Edgar Bonet

4

Bạn có thể tạo một vài hàm sử dụng xấp xỉ tuyến tính để xác định sin () và cos () của một góc cụ thể.

Tôi đang nghĩ một cái gì đó như thế này: Với mỗi tôi đã chia biểu diễn đồ họa của sin () và cos () thành 3 phần và đã thực hiện xấp xỉ tuyến tính của phần đó.
xấp xỉ tuyến tính

Trước tiên, chức năng của bạn sẽ kiểm tra xem phạm vi của thiên thần nằm trong khoảng từ 0 đến 90.
Sau đó, nó sẽ sử dụng một ifelsecâu lệnh để xác định phần nào trong 3 phần mà nó thuộc về và sau đó thực hiện phép tính tuyến tính tương ứng (nghĩa là output = mX + c)


Điều này sẽ không liên quan đến phép nhân dấu phẩy động?
Transitor Overlord

1
Không cần thiết. Bạn có thể có nó để đầu ra được thu nhỏ trong khoảng từ 0 đến 100 thay vì 0-1. Bằng cách này, bạn đang xử lý các số nguyên, không phải dấu phẩy động. Lưu ý: 100 là tùy ý. Không có lý do gì mà bạn không thể mở rộng quy mô đầu ra trong khoảng 0-128 hoặc 0-512 hoặc 0-1000 hoặc 0-1024. Bằng cách sử dụng bội số của 2, bạn chỉ cần thực hiện đúng ca để thu nhỏ kết quả xuống.
sa_leinad

Khá thông minh, @sa_leinad. Upvote. Tôi nhớ làm điều này khi làm việc với xu hướng của bóng bán dẫn.
SDsolar

4

Tôi đã tìm những người khác có xấp xỉ cos () và sin () và tôi đã bắt gặp câu trả lời này:

câu trả lời của dtb cho "Sin nhanh / Cos sử dụng mảng dịch được tính toán trước"

Về cơ bản, ông đã tính toán rằng hàm math.sin () từ thư viện toán học nhanh hơn so với sử dụng bảng tra cứu các giá trị. Nhưng từ những gì tôi có thể nói, điều này đã được tính toán trên PC.

Arduino có một thư viện toán học bao gồm có thể tính sin () và cos ().


1
PC có FPU tích hợp sẵn giúp chúng hoạt động nhanh. Arduino không, và điều đó làm cho nó chậm.
Majenko

Câu trả lời cũng dành cho C #, công cụ kiểm tra giới hạn mảng.
Michael

3

Một bảng tra cứu sẽ là cách nhanh nhất để tìm sines. Và nếu bạn thoải mái tính toán với các số điểm cố định (số nguyên có điểm nhị phân ở đâu đó bên phải bit-0), thì các phép tính tiếp theo của bạn với các sin cũng sẽ nhanh hơn nhiều. Bảng đó sau đó có thể là một bảng từ, có thể trong Flash để tiết kiệm dung lượng RAM. Lưu ý rằng trong toán học của bạn, bạn có thể cần phải sử dụng lâu dài cho kết quả trung gian lớn.


1

nói chung, bảng tra cứu> xấp xỉ -> tính toán. ram> flash. số nguyên> điểm cố định> điểm nổi. tiền calci hóa> tính toán thời gian thực. phản chiếu (sin thành cosine hoặc cosine thành sin) so với tính toán / tra cứu thực tế ....

mỗi cái đều có điểm cộng và nhược điểm.

bạn có thể thực hiện tất cả các loại kết hợp để xem cái nào hoạt động tốt nhất cho ứng dụng của bạn.

chỉnh sửa: Tôi đã kiểm tra nhanh. sử dụng đầu ra số nguyên 8 bit, tính toán 1024 giá trị sin với bảng tra cứu mất 0,6ms và 133ms với số float hoặc chậm hơn 200 lần.


1

Tôi đã có một câu hỏi simillar cho OP. Tôi muốn tạo một bảng LUT để tính góc phần tư thứ nhất của hàm sin là số nguyên 16 bit không dấu bắt đầu từ 0x8000 đến 0xffff. Và tôi đã kết thúc việc viết này cho vui và lợi nhuận. Lưu ý: Điều này sẽ hoạt động hiệu quả hơn nếu tôi sử dụng câu lệnh 'if'. Ngoài ra, nó không chính xác lắm, nhưng sẽ đủ chính xác cho một sóng hình sin trong bộ tổng hợp âm thanh

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

Bây giờ để lấy lại các giá trị, hãy sử dụng hàm này. Nó chấp nhận một giá trị từ 0x0000 đến 0x0800 và trả về giá trị thích hợp từ 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

Hãy nhớ rằng, đây không phải là cách tiếp cận hiệu quả nhất cho nhiệm vụ này, tôi chỉ không thể tìm ra cách tạo ra chuỗi taylor để đưa ra kết quả trong phạm vi thích hợp.


Mã của bạn không biên dịch: Imm_UI_Ađược khai báo hai lần, một ;và một số khai báo biến bị thiếu và uLut_0phải là toàn cục. Với các sửa lỗi cần thiết, lu_sin()nhanh (trong khoảng từ 27 đến 42 chu kỳ CPU) nhưng rất không chính xác (lỗi tối đa ≈ 5.04e-2). Tôi không thể hiểu được các đa thức của Arn Arnadathian: nó có vẻ là một tính toán khá nặng nề, nhưng kết quả gần như tồi tệ như một xấp xỉ bậc hai đơn giản. Phương pháp này cũng có chi phí bộ nhớ rất lớn. Sẽ tốt hơn nếu tính toán bảng trên PC của bạn và đặt nó vào mã nguồn dưới dạng một PROGMEMmảng.
Edgar Bonet

1

Để giải trí, và để chứng minh điều đó có thể được thực hiện, tôi đã hoàn thành một quy trình lắp ráp AVR để tính kết quả sin (x) trong 24 bit (3 byte) với một bit lỗi. Góc đầu vào tính bằng độ với một chữ số thập phân, từ 000 đến 900 (0 ~ 90.0) chỉ cho góc phần tư thứ nhất. Nó sử dụng ít hơn 210 lệnh AVR và chạy trung bình là 212 micro giây, thay đổi từ 211us (angle = 001) đến 213us (angle = 899).

Phải mất vài ngày để làm tất cả, hơn 10 ngày (giờ rảnh) chỉ nghĩ về thuật toán tốt nhất để tính toán, xem xét bộ vi điều khiển AVR, không có điểm nổi, loại bỏ tất cả các phân chia có thể. Điều cần nhiều thời gian hơn là tạo ra các giá trị tăng dần phù hợp cho các số nguyên, để có độ chính xác tốt, nó cần phải tăng các giá trị từ 1e-8 lên các số nguyên nhị phân 2 ^ 28 trở lên. Khi tất cả các lỗi thủ phạm của độ chính xác và làm tròn đã được tìm thấy, tăng độ phân giải tính toán của chúng thêm 2 ^ 8 hoặc 2 ^ 16, kết quả tốt nhất đã được đáp ứng. Trước tiên tôi đã mô phỏng tất cả các tính toán trên Excel, đảm bảo có tất cả các giá trị là Int (x) hoặc Round (x, 0) để thể hiện chính xác quá trình xử lý lõi AVR.

Ví dụ, trong thuật toán, góc phải tính bằng Radian, đầu vào tính theo Độ để tạo điều kiện thuận lợi cho người dùng. Để chuyển đổi Độ sang Radian, công thức tầm thường là rad = độ * PI / 180, có vẻ hay và dễ dàng, nhưng thực tế không phải vậy, PI là một số vô hạn - nếu sử dụng vài chữ số, nó sẽ tạo ra lỗi ở đầu ra, chia 180 Thao tác bit AVR vì nó không có lệnh chia, và hơn thế nữa, kết quả sẽ yêu cầu dấu phẩy động vì liên quan đến các số ở dưới số nguyên 1. Ví dụ, Radian của 1 ° (độ) là 0,017453293. Vì PI và 180 là hằng số, tại sao không đảo ngược điều này để nhân đơn giản? PI / 180 = 0,017453293, nhân nó với 2 ^ 32 và kết quả là hằng số 74961320 (0x0477D1A8), nhân số này với góc của bạn theo độ, giả sử 900 cho 90 ° và dịch chuyển đúng 4 bit (÷ 16) để thu được 4216574250 (0xFB53D12A), đó là radian của 90 ° với độ mở rộng 2 ^ 28, vừa với 4 byte, không chia một byte (trừ 4 byte dịch chuyển bit phải). Theo một cách nào đó, lỗi bao gồm trong thủ thuật đó nhỏ hơn 2 ^ -27.

Vì vậy, tất cả các tính toán tiếp theo cần phải nhớ nó cao hơn 2 ^ 28 và chăm sóc nó. Bạn cần chia kết quả khi đang di chuyển cho 16, 256 hoặc thậm chí 65536 chỉ để tránh việc sử dụng các byte đói không cần thiết sẽ không giúp giải quyết. Đó là một công việc khó khăn, chỉ cần tìm số lượng bit tối thiểu trong mỗi kết quả tính toán, giữ cho kết quả chính xác khoảng 24 bit. Mỗi một trong số các phép tính được thực hiện trong thử / lỗi với số bit cao hơn hoặc thấp hơn trong luồng Excel, xem tổng số bit lỗi ở kết quả trong biểu đồ hiển thị 0-90 ° với macro chạy mã 900 lần, một lần một phần mười của một mức độ. Cách tiếp cận Excel "trực quan" đó là một công cụ tôi tạo ra, đã giúp rất nhiều để tìm ra giải pháp tốt nhất cho mọi phần của mã.

Ví dụ: làm tròn kết quả tính toán cụ thể này 13248737.51 đến 13248738 hoặc chỉ mất các số thập phân "0,51", nó sẽ ảnh hưởng đến độ chính xác kết quả cuối cùng của tất cả các phép thử 900 góc (00.1 ~ 90.0) như thế nào?

Tôi đã có thể giữ con vật chứa trong vòng 32 bit (4 byte) trên mỗi phép tính và kết thúc bằng phép thuật để có được độ chính xác trong vòng 23 bit của kết quả. Khi kiểm tra toàn bộ 3 byte kết quả, sai số là ± 1 LSB, chưa xử lý.

Người dùng có thể lấy một, hai hoặc ba byte từ kết quả cho các yêu cầu chính xác của chính nó. Tất nhiên, nếu chỉ cần một byte là đủ, tôi khuyên bạn nên sử dụng một bảng sin 256 byte duy nhất và sử dụng hướng dẫn 'LPM' của AVR để lấy nó.

Khi tôi đã có chuỗi Excel chạy trơn tru và gọn gàng, bản dịch cuối cùng từ lắp ráp Excel sang AVR mất chưa đầy 2 giờ, như thường lệ bạn nên suy nghĩ nhiều hơn trước, làm việc ít hơn sau.

Lúc đó tôi đã có thể siết chặt hơn nữa và giảm việc sử dụng thanh ghi. Mã thực tế (không phải cuối cùng) sử dụng khoảng 205 lệnh (~ 410 byte), chạy phép tính sin (x) trung bình là 212us, xung nhịp ở mức 16 MHz. Với tốc độ đó, nó có thể tính được 4700+ sin (x) mỗi giây. Không quan trọng, nhưng nó có thể chạy một chuỗi hình chính xác lên đến 4700Hz với độ chính xác và độ phân giải 23 bit, không có bất kỳ bảng tra cứu nào.

Thuật toán cơ sở dựa trên loạt Taylor cho sin (x), nhưng đã sửa đổi rất nhiều để phù hợp với ý định của tôi với bộ vi điều khiển AVR và độ chính xác trong tâm trí.

Ngay cả khi sử dụng bảng 2700 byte (900 mục * 3 byte) sẽ có tốc độ hấp dẫn, trải nghiệm thú vị hoặc học tập trên đó là gì? Tất nhiên, cách tiếp cận CORDIC cũng đã được xem xét, có thể sau đó, vấn đề ở đây là ép Taylor vào lõi AVR và lấy nước từ một tảng đá khô.

Tôi tự hỏi nếu Arduino "sin (78,9 °)" có thể chạy Xử lý (C ++) với độ chính xác 23 bit trong ít hơn 212us và mã cần thiết nhỏ hơn 205 lệnh. Có thể nếu C ++ sử dụng CORDIC. Bản phác thảo Arduino có thể nhập mã lắp ráp.

Không có ý nghĩa để gửi mã ở đây, sau này tôi sẽ chỉnh sửa bài đăng này để bao gồm một liên kết web đến nó, có thể trên blog của tôi tại url này . Blog chủ yếu bằng tiếng Bồ Đào Nha.

Liên doanh không có sở thích này rất thú vị, đẩy các giới hạn của động cơ AVR gần 16MIPS lên 16 MHz, không có hướng dẫn phân chia, nhân chỉ trong 8 x 8 bit. Nó cho phép tính sin (x), cos (x) [= sin (900-x)] và tan (x) [= sin (x) / sin (900-x)].

Trên tất cả, điều này đã giúp giữ cho bộ não 63 tuổi của tôi được đánh bóng và bôi dầu. Khi thanh thiếu niên nói rằng 'người già' không biết gì về công nghệ, tôi trả lời "hãy nghĩ lại, bạn nghĩ ai đã tạo ra cơ sở cho mọi thứ bạn thích ngày hôm nay?".

Chúc mừng


Đẹp! Một vài nhận xét: 1. Các tiêu chuẩn sin()chức năng có về độ chính xác tương tự như của bạn và là nhanh gấp hai lần. Nó cũng dựa trên một đa thức. 2. Nếu một góc tùy ý phải được làm tròn đến bội số gần nhất 0,1 °, điều này có thể dẫn đến sai số làm tròn cao tới 8,7e-4, điều này phủ nhận lợi ích của độ chính xác 23 bit. 3. Bạn có phiền khi chia sẻ đa thức của bạn?
Edgar Bonet

1

Như những người khác đã đề cập các bảng tra cứu là cách để đi nếu bạn muốn tốc độ. Gần đây tôi đã nghiên cứu tính toán các hàm lượng giác trên ATtiny85 để sử dụng trung bình vectơ nhanh (gió trong trường hợp của tôi). Luôn luôn phải đánh đổi ... đối với tôi, tôi chỉ cần độ phân giải góc 1 độ nên một bảng tra cứu 360 int (tỷ lệ -32767 đến 32767, chỉ hoạt động với int) là cách tốt nhất để đi. Lấy sin chỉ là vấn đề cung cấp chỉ số 0-359 ... rất nhanh! Một số số từ các bài kiểm tra của tôi:

Thời gian tra cứu FLASH (chúng tôi): 0,99 (bảng được lưu trữ bằng PROGMEM)

Thời gian tra cứu RAM (chúng tôi): 0,69 (bảng trong RAM)

Thời gian Lib (chúng tôi): 122,31 (Sử dụng Arduino Lib)

Lưu ý đây là các mức trung bình trên một mẫu 360 điểm cho mỗi mẫu. Thử nghiệm đã được thực hiện trên nano.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.