programing

C에서의 무한 사인 생성

goodcopy 2022. 7. 23. 13:26
반응형

C에서의 무한 사인 생성

컨트롤 루프의 입력으로서 사인파 연산을 도입하는 프로젝트를 진행하고 있습니다.

사인파의 주파수는 280Hz이며 제어 루프는 30µs마다 실행되며 암 Cortex-M7의 경우 모든 것이 C로 기록됩니다.

현시점에서는, 다음의 작업을 간단하게 실시합니다.

double time;
void control_loop() {
    time += 30e-6;
    double sine = sin(2 * M_PI * 280 * time);
    ...
}

다음 두 가지 문제/질문이 발생합니다.

  1. 시 '''는 '''가 됩니다'''time더 커집니다.갑자기 사인 함수의 계산 시간이 급격히 증가하는 지점이 있습니다(이미지 참조).왜 이러한가?이러한 기능은 보통 어떻게 구현됩니까?속도가 매우 중요한 요소이기 때문에 (정밀도가 현저하게 저하되지 않고) 이를 회피할 수 있는 방법이 있습니까?math.h(Arm GCC)의 sin을 사용하고 있습니다.SIN 함수 프로파일링
  2. 일반적인 시간에는 어떻게 대처하면 좋을까요? 시 " " " " "time2배 정밀도의 한계에 도달할 수밖에 없습니다. 사용 time = counter++ * 30e-6;을 사용하다정확한 설명하겠습니다.빠르고 정확하게 구현할 수 있는 방법에 대해 설명합니다.

사인(sine)을 시간의 함수로 계산하는 대신 사인/코사인 쌍을 유지하고 복소수 곱셈을 통해 진행하십시오.여기에는 삼각 함수나 조회 테이블이 필요하지 않습니다. 4배만 곱하고 가끔 다시 정규화됩니다.

static const double a = 2 * M_PI * 280 * 30e-6;
static const double dx = cos(a);
static const double dy = sin(a);
double x = 1, y = 0; // complex x + iy
int counter = 0;

void control_loop() {
    double xx = dx*x - dy*y;
    double yy = dx*y + dy*x;
    x = xx, y = yy;

    // renormalize once in a while, based on
    // https://www.gamedev.net/forums/topic.asp?topic_id=278849
    if((counter++ & 0xff) == 0) {
        double d = 1 - (x*x + y*y - 1)/2;
        x *= d, y *= d;
    }

    double sine = y; // this is your sine
}

에 따라 할 수 .dx,dy.

또, 여기서의 모든 조작은, 어느 쪽인가 하면, 고정 포인트로 간단하게 실시할 수 있습니다.


합리성

@user3386109가 다음(+1)을 가리키고 있듯이280 * 30e-6 = 21 / 2500는 유리수이므로 2500개의 샘플 후에 사인파가 루프되어야 합니다.발전기를 리셋하여 이 방법을 그들의 방법과 결합할 수 있습니다.x=1,y=0 10000회 2500회 (5000회, 10000회 등).이렇게 하면 재규격화의 필요성이 없어질 뿐만 아니라 장기적인 단계의 부정확성을 없앨 수 있습니다.

(기술적으로는 임의의 부동소수점 숫자는 2차 유리입니다., <고객명>님280 * 30e-62년 전하다그러나 제너레이터를 권장대로 리셋하면 정확히 주기적인 사인파일을 얻을 수 있습니다.)


설명.

어떤 사람들은 왜 이것이 효과가 있는지에 대한 코멘트에 설명을 요구했습니다.가장 간단한 설명은 각도 합계 삼각법 식별 정보를 사용하는 것입니다.

xx = cos((n+1)*a) = cos(n*a)*cos(a) - sin(n*a)*sin(a) = x*dx - y*dy
yy = sin((n+1)*a) = sin(n*a)*cos(a) + cos(n*a)*sin(a) = y*dx + x*dy

그리고 정확한 것은 유도 뒤에 온다.

오일러의 공식에 따라 사인/코사인 쌍을 복소수로 본다면 이것은 본질적으로 드 무브르의 공식입니다.

더 통찰력 있는 방법은 그것을 기하학적으로 보는 것일지도 모른다.에 의한 complex complex:exp(ia) 같습니다.a따라서 반복 곱셈dx + idy = exp(ia) 우리는 1 + 0i유닛 서클을 따라서.y오일러의 공식에 따르면 좌표는 현재 위상의 사인입니다.

정규화

마다 계속 되지만, 노름)는 상 while while) 、 while while ( )) ) 。x + iy1반올림 오류로 인해.이 있습니다.1에 우리는 합니다.x + iy이치노물론 간단한 방법은 그 자체의 규범에 따라 나누는 것입니다.

double d = 1/sqrt(x*x + y*y);
x *= d, y *= d;

이를 위해서는 역제곱근을 계산해야 합니다.X번 반복마다 한 번만 정상화되더라도 피하는 것이 좋습니다. 다 fortun fortun fortun.|x + iy|에 이미 가까워지고 있다1따라서 약간의 수정이 필요합니다.「 」의 d위에 1), (1차 테일러 근사치), (1차 테일러 근사치), (1차 테일러 근사치), (1차 테일러 근사치), (1차 테일러 근사치

d = 1 - (x*x + y*y - 1)/2

TODO: 이 근사치의 유효성을 완전히 이해하려면 누적된 오류보다 더 빨리 반올림 오류를 보상한다는 것을 증명해야 합니다. 따라서 이 오차를 적용해야 하는 빈도에 대한 한계를 파악할 수 있습니다.

함수는 다음과 같이 고쳐 쓸 수 있습니다.

double n;
void control_loop() {
    n += 1;
    double sine = sin(2 * M_PI * 280 * 30e-6 * n);
    ...
}

이는 질문의 코드와 정확히 같은 기능을 하며, 정확히 같은 문제를 안고 있습니다.그러나 이제는 단순화할 수 있습니다.

280 * 30e-6 = 280 * 30 / 1000000 = 21 / 2500 = 8.4e-3

그 말은 즉,n2500에 도달하면 사인파의 정확히 21주기를 출력할 수 있습니다., 「 」, 「 」를 설정할 수 .n00으로 결과 코드는 다음과 같습니다.

int n;
void control_loop() {
    n += 1;
    if (n == 2500)
        n = 0;
    double sine = sin(2 * M_PI * 8.4e-3 * n);
    ...
}

코드가 문제없이 21 사이클 동안 실행된다면 문제없이 영원히 실행됩니다.

나는 기존의 답변에 다소 충격을 받았다.처음 발견한 문제는 쉽게 해결되고, 첫 번째 문제를 풀면 다음 문제는 마법처럼 사라집니다.

수학이 어떻게 작동하는지 보려면 수학에 대한 기본적인 이해가 필요하다. ★★★,sin(x+2pi) ★★★★★★sin(x)이 많이 되는 건 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'나 '우리'가sin(float)구현이 다른 알고리즘으로 전환됩니다.그러면 정말 회피해야 합니다.

하세요.float여섯 살 100000.0f*M_PI+x 숫자를 6자리 숫자로 합니다.100000.0f*M_PI 게 없어요.x.

때문에 쉬운 은 잘 있는 입니다.x★★★★★★★★★★★★★★★★★★★.t=0「」를 합니다.x로로 합니다.0.0f마다 30번씩 됩니다.x+= M_PI * 280 * 30e-06;이되지 않습니다 이이!!!!!!!!!!!!!!!!!! 만약의 경우x>2*M_PI,감소x-=2*M_PI;(sin(x)==sin(x-2*pi)이므로

으로, 「 」를 했습니다.x 범위 에 잘 0로로 합니다.6.2834서, snowledge.sin6월 6살

사랑스러운 사인(sine)을 생성하는 방법.

DAC는 12비트이므로 4096레벨밖에 없어요1주기에 4096개 이상의 샘플을 보내는 것은 의미가 없습니다.실제 환경에서는 좋은 품질의 파형을 생성하는 데 필요한 샘플이 훨씬 적습니다.

  1. (PC를 사용하여) 룩업테이블을 사용하여 C 파일을 만듭니다.출력을 파일(https://helpdeskgeek.com/how-to/redirect-output-from-command-line-to-text-file/)로 리다이렉트 합니다.
#define STEP   ((2*M_PI) / 4096.0)

int main(void)
{
    double alpha = 0;

    printf("#include <stdint.h>\nconst uint16_t sine[4096] = {\n");
    for(int x = 0; x < 4096 / 16; x++)
    {
        for(int y = 0; y < 16; y++)
        {
            printf("%d, ", (int)(4095 * (sin(alpha) + 1.0) / 2.0));
            alpha += STEP;
        }
        printf("\n");
    }
    printf("};\n");
}

https://godbolt.org/z/e899d98oW

  1. 초당 4096*280=1146880회 오버플로를 트리거하도록 타이머를 구성합니다.DAC 트리거 이벤트를 생성하도록 타이머를 설정합니다.180MHz 타이머 클럭의 경우 정확하지 않고 주파수는 279.906449045Hz가 됩니다.더 정확한 정밀도가 필요한 경우 타이머 주파수와 일치하도록 샘플 수를 변경하거나 타이머 클럭 주파수를 변경합니다(H7 타이머는 최대 480MHz까지 실행 가능).

  2. DMA를 사용하도록 DAC를 구성하고 스텝 1에서 작성한 조회 테이블에서 트리거 이벤트의 DAC로 값을 전송합니다.

  3. 오실로스코프를 사용하여 아름다운 사인파를 즐기십시오.마이크로 컨트롤러 코어는 전혀 로드되지 않습니다.다른 작업에 사용할 수 있습니다.주기를 변경하려면 타이머를 재설정하기만 하면 됩니다.초당 원하는 횟수만큼 할 수 있습니다.타이머를 재설정하려면 타이머 DMA 버스트 모드를 사용합니다. 그러면 업데이트 날짜 이벤트에서 PSC 및 ARR 레지스터가 자동으로 새로고침되며 생성된 파형에 영향을 주지 않습니다.

고급 STM32 프로그래밍이며 레지스터 레벨 프로그래밍이 필요할 것으로 알고 있습니다.장치에서 복잡한 파형을 생성하는 데 사용합니다.

그것은 올바른 방법이다.제어 루프도 계산도 코어 부하도 없습니다.

코드에 포함된 프로그래밍 문제에 직접 대처하고 싶습니다.@0_________의 답변은 마이크로컨트롤러로 이를 수행하는 올바른 방법이며 동일한 근거를 다시 읽지 않습니다.

  • 시간을 나타내는 변수는 부동 소수점일 수 없습니다.증분이 2의 거듭제곱이 아닐 경우 오류는 항상 누적됩니다.그 경우에도 최종적으로 증분이 가장 작은 증분보다 작아지고 타이머가 정지합니다.시간에는 항상 정수를 사용합니다.롤오버를 무시할 정도로 큰 정수 크기를 선택할 수 있습니다.밀리초를 나타내는 부호 없는 32비트 정수는 롤오버하는 데 50일이 걸리지만 부호 없는 64비트 정수는 5억 년 이상 걸립니다.
  • 신호의 위상에 관계없이 주기적인 신호를 생성하는 데 시간 변수가 필요하지 않습니다.대신 기간 종료 시 0으로 리셋되는 내부 카운터를 유지할 수 있습니다.(룩업 테이블과 함께 DMA를 사용하는 경우, 이것이 정확하게 실행됩니다.카운터는 DMA 컨트롤러의 다음 읽기 포인터입니다).
  • 마이크로컨트롤러에서 사인 등의 초월함수를 사용할 때는 "이러한 룩업테이블을 사용할 수 있을까요?"를 먼저 생각해야 합니다.4GHz+멀티코어 프로세서로 부하를 최적으로 분산시키는 최신 운영체제의 고급스러움을 이용할 수 없습니다.200MHz 마이크로컨트롤러가 FPU를 스탠바이에서 꺼내 근사 알고리즘을 실행할 때까지 기다리는 단일 스레드를 처리하는 경우가 많습니다.초월함수에는 상당한 비용이 든다.LUT에도 비용이 듭니다만, 계속 기능하고 있으면, LUT의 트레이드오프가 훨씬 더 마음에 들 가능성이 있습니다.

중 되었듯이, 「 」는, 「 」를 참조해 주세요.time시간이 지남에 따라 가치가 지속적으로 증가하고 있습니다.은 두 문제를 합니다: 이,,, 개개개개개 2 개개개 。

  1. sin함수는 내부 값을 지원되는 범위로 만들기 위해 내부적으로 계수를 실행해야 합니다.
  2. 더 높은 자릿수를 더하면 값이 증가할수록 시간 분해능은 점점 더 나빠집니다.

다음과 같이 변경하면 성능이 향상됩니다.

double time;
void control_loop() {
    time += 30.0e-6;
    if((1.0/280.0) < time)
    {
        time -= 1.0/280.0;
    }
    
    double sine = sin(2 * M_PI * 280 * time);
    ...
}

이 변경이 이루어지면 더 이상 시간 변수를 사용할 수 없습니다.

룩업 테이블을 사용합니다.Eugene Sh와의 논의에서 귀하의 의견은 다음과 같습니다.

사인 주파수(280.1Hz 등)에서 약간의 편차는 괜찮습니다.

이 경우 제어 간격이 30µs일 때 반복하는 119개 샘플의 테이블이 있으면 280.112Hz의 사인파가 발생합니다.DAC를 있기 DAC에 한 것은 119 * 2 =입니다.DAC는 DAC로 출력합니다.처럼 추가 할 때 하실 수 .float ★★★★★★★★★★★★★★★★★」double 스태틱에서는 메모리에서하는 데 .내장 스태틱 RAM을 탑재한 MCU에서는 메모리에서 로드하는 데 최대 몇 사이클밖에 걸리지 않습니다.

사용 가능한 메모리가 몇 킬로바이트인 경우 룩업테이블을 사용하여 이 문제를 완전히 해결할 수 있습니다.

샘플링 기간이 30µs일 경우 2500개의 샘플의 총 지속 시간은 75ms입니다.이는 280Hz에서 21사이클의 지속시간과 정확히 동일합니다.

다음 코드를 테스트하거나 컴파일한 적은 없지만 최소한 접근 방식을 보여줘야 합니다.

double sin2500() {
  static double *table = NULL;
  static int n = 2499;
  if (!table) {
    table = malloc(2500 * sizeof(double));
    for (int i=0; i<2500; i++) table[i] = sin(2 * M_PI * 280 * i * 30e-06);
  }
  n = (n+1) % 2500;
  return table[n];
}

아주 작은 양만큼 증가하는 각도에 대해 일련의 사인(및 코사인) 값을 계산하는 다른 방법이 있습니다.기본적으로 원의 X 및 Y 좌표를 계산한 다음 Y 값을 일정한 상수로 나누어 사인을 생성하고 X 값을 동일한 상수로 나누어 코사인(cosine)을 생성합니다.

"매우 둥근 타원"을 생성하는 데 만족하는 경우 다음과 같은 해크를 사용할 수 있습니다. 이는 1960년대 마빈 민스키의 소행으로 추정됩니다.이것은 sine과 cosine을 계산하는 것보다 훨씬 빠르지만, 시리즈에 매우 작은 오차를 발생시킵니다.이것은 Hakmem 문서, 항목 149에서 발췌한 것이다.민스키 서클 알고리즘의 개요를 나타냅니다.


항목 149(민스키):원 알고리즘 점묘도 디스플레이에 거의 원을 그리는 우아한 방법은 다음과 같습니다.

NEW X = OLD X - epsilon * OLD Y
NEW Y = OLD Y + epsilon * NEW(!) X

그러면 원점을 중심으로 한 매우 둥근 타원이 되고 초기 점에 의해 크기가 결정됩니다.엡실론은 순환점의 각 속도를 결정하며 이심률에 약간 영향을 미칩니다.엡실론이 2의 거듭제곱이라면 제곱근, 사인, 코사인은 말할 것도 없고 곱셈도 필요 없습니다!점들이 곧 주기적이 되기 때문에 "원"은 완벽하게 안정될 것입니다.

원 알고리즘은 디스플레이 해킹에 레지스터 하나를 저장하려고 할 때 실수로 발명되었습니다!Ben Gurley는 6~7개의 지시만으로 놀라운 디스플레이 해킹을 했고, 그것은 대단한 놀라움이었다.하지만 기본적으로 라인 지향적이었어요.커브가 있으면 신날 것 같아서 최소한의 명령으로 커브 디스플레이 해크를 얻으려고 했습니다.


다음은 hackem 링크입니다.http://inwap.com/pdp10/hbaker/hakmem/hacks.html

다른 사람의 모듈로 기반 개념의 변형은 어떻습니까?

int t = 0;
int divisor = 1000000;
void control_loop() {
    t += 30 * 280;
    if (t > divisor) t -= divisor;
    double sine = sin(2 * M_PI * t / (double)divisor));
    ...
}

정수로 모듈로를 계산하여 반올림 오류를 발생시키지 않습니다.

모듈로 사용이 가능할 것 같습니다.sin()정기적입니다.

그러면 당신은 그 문제에 대해 걱정할 필요가 없어요.

double time = 0;
long unsigned int timesteps = 0;
double sine;
void controll_loop()
{
  timesteps++;
  time += 30e-6;
  if( time > 1 )
  {
    time -= 1;
  }
  sine = sin( 2 * M_PI * 280 * time );
  ...
}

매혹적인 실.Walter Mitty의 답변에서 언급된 Minsky의 알고리즘은 Electronics & Wireless World에 게시된 원을 그리는 방법을 떠올리게 했고, 제가 유지했던 방법을 떠올리게 했습니다. (크레디트: https://www.electronicsworld.co.uk/magazines/))흥미를 위해 여기에 첨부합니다.

그러나 유사한 프로젝트(오디오 합성용)에서는 선형 보간이 충분히 정확할 정도로 충분한 포인트가 있는 룩업 테이블을 사용합니다(연산 실행).

원 그리기 알고리즘

언급URL : https://stackoverflow.com/questions/69729326/endless-sine-generation-in-c

반응형