我希望在不使用标准函数sin()的情况下在C中生成正弦信号,以触发LED亮度的正弦形状变化。我的基本想法是使用一个包含40个点和插值的查找表。
这是我的第一个方法:
const int sine_table[40] = {0, 5125, 10125, 14876, 19260, 23170, 26509, 29196,
31163, 32364, 32767, 32364, 31163, 29196, 26509, 23170, 19260, 14876, 10125,
5125, 0, -5126, -10126,-14877, -19261, -23171, -26510, -29197, -31164, -32365,
-32768, -32365, -31164, -29197, -26510, -23171, -19261, -14877, -10126, -5126};
int i = 0;
int x1 = 0;
int x2 = 0;
float y = 0;
float sin1(float phase)
{
x1 = (int) phase % 41;
x2 = x1 + 1;
y = (sine_table[x2] - sine_table[x1])*((float) ((int) (40*0.001*i*100) % 4100)/100 - x1) + sine_table[x1];
return y;
}
int main()
{
while(1)
{
printf("%f ", sin1(40*0.001*i)/32768);
i = i + 1;
}
}
不幸的是,这个函数有时会返回远大于1的值。此外,插值看起来并不好(我用它来创建LED的正弦形亮度变化,但这些非常不合理)。
有人有更好的想法在C中实现正弦发生器吗?
答案 0 :(得分:21)
OP的主要问题是为表查找生成索引。
OP的代码尝试访问导致undefined behavior的外部数组sine_table[40]
。至少修正一下。
const int sine_table[40] = {0, 5125, 10125, ...
...
x1 = (int) phase % 41; // -40 <= x1 <= 40
x2 = x1 + 1; // -39 <= x2 <= 41
y = (sine_table[x2] - sine_table[x1])*... // bad code, consider x1 = 40 or x2 = 40,41
建议的更改
x1 = (int) phase % 40; // mod 40, not 41
if (x1 < 0) x1 += 40; // Handle negative values
x2 = (x1 + 1) % 40; // Handle wrap-around
y = (sine_table[x2] - sine_table[x1])*...
有更好的方法,但关注OP的方法见下文。
#include <math.h>
#include <stdio.h>
const int sine_table[40] = { 0, 5125, 10125, 14876, 19260, 23170, 26509, 29196,
31163, 32364, 32767, 32364, 31163, 29196, 26509, 23170, 19260, 14876, 10125,
5125, 0, -5126, -10126, -14877, -19261, -23171, -26510, -29197, -31164, -32365,
-32768, -32365, -31164, -29197, -26510, -23171, -19261, -14877, -10126, -5126 };
int i = 0;
int x1 = 0;
int x2 = 0;
float y = 0;
float sin1(float phase) {
x1 = (int) phase % 40;
if (x1 < 0) x1 += 40;
x2 = (x1 + 1) % 40;
y = (sine_table[x2] - sine_table[x1])
* ((float) ((int) (40 * 0.001 * i * 100) % 4100) / 100 - x1)
+ sine_table[x1];
return y;
}
int main(void) {
double pi = 3.1415926535897932384626433832795;
for (int j = 0; j < 1000; j++) {
float x = 40 * 0.001 * i;
float radians = x * 2 * pi / 40;
printf("%f %f %f\n", x, sin1(x) / 32768, sin(radians));
i = i + 1;
}
}
输出
OP's Reference sin()
0.000000 0.000000 0.000000
0.040000 0.006256 0.006283
0.080000 0.012512 0.012566
...
1.960000 0.301361 0.303035
2.000000 0.308990 0.309017
2.040000 0.314790 0.314987
...
39.880001 -0.020336 -0.018848
39.919998 -0.014079 -0.012567
39.959999 -0.006257 -0.006283
更好的代码不会将值i, x1, x2, y
作为全局变量传递,而是作为函数参数或函数变量传递。也许这是OP调试的工件。
有人有更好的想法在C中实现正弦发生器吗?
这是相当广泛的。速度,精度,代码空间,可移植性或可维护性更好? sine()
函数很容易实现。高质量的产品需要更多的努力。
虽然模糊,OP使用小型查找表是一个好的开始 - 虽然我看到它可以在没有任何浮点数学的情况下完成。我建议OP构建一个经过测试和运行的解决方案,并将其发布在Code Review中以获得改进的想法。
答案 1 :(得分:18)
...在C中实现正弦发生器更好吗?
编辑:建议先阅读 this article ,以了解OP的要求。
从您问题中提供的上下文中,我假设 更好 这个词可能与编译代码的大小和速度有关,因为可能需要运行一个小型微处理器。
CORDIC(协调旋转数据计算机)算法非常适合用于较小的uP和具有有限数学计算能力的FPGA实现,因为它计算值的正弦和余弦仅使用基本算术(加法,减法和移位)。有关CORDIC的更多信息,以及如何使用它来生成角度 are provided here 的正弦/余弦。
还有几个站点提供算法实现示例。 Simple CORDIC 包含有关如何生成表格的详细说明,然后可以对目标设备进行预编译,以及测试以下函数输出的代码(使用定点数学):
(参见以下文档和链接中的其他功能)
#define cordic_1K 0x26DD3B6A
#define half_pi 0x6487ED51
#define MUL 1073741824.000000
#define CORDIC_NTAB 32
int cordic_ctab [] = {0x3243F6A8, 0x1DAC6705, 0x0FADBAFC, 0x07F56EA6, 0x03FEAB76, 0x01FFD55B,
0x00FFFAAA, 0x007FFF55, 0x003FFFEA, 0x001FFFFD, 0x000FFFFF, 0x0007FFFF, 0x0003FFFF,
0x0001FFFF, 0x0000FFFF, 0x00007FFF, 0x00003FFF, 0x00001FFF, 0x00000FFF, 0x000007FF,
0x000003FF, 0x000001FF, 0x000000FF, 0x0000007F, 0x0000003F, 0x0000001F, 0x0000000F,
0x00000008, 0x00000004, 0x00000002, 0x00000001, 0x00000000 };
void cordic(int theta, int *s, int *c, int n)
{
int k, d, tx, ty, tz;
int x=cordic_1K,y=0,z=theta;
n = (n>CORDIC_NTAB) ? CORDIC_NTAB : n;
for (k=0; k<n; ++k)
{
d = z>>31;
//get sign. for other architectures, you might want to use the more portable version
//d = z>=0 ? 0 : -1;
tx = x - (((y>>k) ^ d) - d);
ty = y + (((x>>k) ^ d) - d);
tz = z - ((cordic_ctab[k] ^ d) - d);
x = tx; y = ty; z = tz;
}
*c = x; *s = y;
}
<强> 编辑: 强>
我发现在 Simple CORDIC 网站上使用示例的文档非常容易理解。但是,我遇到的一件小事是编译文件cordic-test.c
时出现错误:使用了未声明的标识符“M_PI”。在执行编译的gentable.c
文件(生成cordic-test.c
文件)时,似乎该行:
#define M_PI 3.1415926535897932384626
虽然包含在自己的声明中,但未包含在用于生成文件cordic-test.c
的printf语句中。一旦解决了这个问题,一切都按照宣传的方式进行。
如记载的那样,产生的数据范围产生完整正弦周期的1/4(-π/ 2-π/ 2)。下图包含浅蓝点之间产生的实际数据的表示。通过镜像和转置原始数据部分来制造正弦信号的其余部分。
答案 2 :(得分:14)
生成准确的正弦函数需要一定量的资源(CPU周期和内存),这在此应用程序中是没有根据的。您生成“平滑”正弦曲线的目的是未能考虑应用程序的要求。
当您绘制曲线时,您可能会观察到瑕疵 将该曲线应用于LED PWM驱动器,人眼无法察觉 那些不完美之处。
即使是40步曲线,人眼也不可能察觉到相邻值之间的亮度差异,因此不需要插值。
一般情况下,如果生成正弦函数直接生成没有浮点的PWM驱动值,效率会更高。事实上,不是正弦函数,缩放的升余弦会更合适,因此输入为零会导致输出为零,并且输入为循环中值的一半会导致PWM驱动器的最大值。
以下函数从16位(和16字节)查找生成8位FSD PWM的升余弦曲线,生成59步循环。因此,与40步浮点实现相比,它具有内存和性能效率。
#include <stdint.h>
#define LOOKUP_SIZE 16
#define PWM_THETA_MAX (LOOKUP_SIZE * 4 - 4)
uint8_t RaisedCosine8bit( unsigned n )
{
static const uint8_t lookup[LOOKUP_SIZE] = { 0, 1, 5, 9,
14, 21, 28, 36,
46, 56, 67, 78,
90, 102, 114, 127} ;
uint8_t s = 0 ;
n = n % PWM_THETA_MAX ;
if( n < LOOKUP_SIZE )
{
s = lookup[n] ;
}
else if( n < LOOKUP_SIZE * 2 - 1 )
{
s = 255 - lookup[LOOKUP_SIZE * 2 - n - 2] ;
}
else if( n < LOOKUP_SIZE * 3 - 2 )
{
s = 255 - lookup[n - LOOKUP_SIZE * 2 + 2] ;
}
else
{
s = lookup[LOOKUP_SIZE * 4 - n - 4] ;
}
return s ;
}
对于0 <= theta < PWM_THETA_MAX
的输入,曲线如下所示:
我建议使用足够光滑的照明。
在实践中你可以这样使用它:
for(;;)
{
for( unsigned i = 0; i < PWM_THETA_MAX; i++ )
{
LedPwmDrive( RaisedCosine8bit( i ) ) ;
Delay( LED_UPDATE_DLEAY ) ;
}
}
如果您的PWM范围不是0到255,只需缩放功能的输出; 8位分辨率足以完成任务。
答案 3 :(得分:6)
您是否考虑过将[0..PI]的正弦曲线部分建模为抛物线?如果LED的亮度仅供人眼观察,那么曲线的形状应该足够相似,以便检测到很小的差异。
您只需要找出适当的等式来描述它。
嗯,......顶点(PI / 2,1)
(0,0)和(PI,0)处的X轴交点
f(x) = 1 - K * (x - PI/2) * (x - PI/2)
K会是......
K = 4 / (PI * PI)
答案 4 :(得分:6)
绘制圆圈(因此也产生正弦波)的经典黑客是Hakmem #149 by Marvin Minsky。 。E.g,:
#include <stdio.h>
int main(void)
{
float x = 1, y = 0;
const float e = .04;
for (int i = 0; i < 100; ++i)
{
x -= e*y;
y += e*x;
printf("%g\n", y);
}
}
它会略微偏心,不是一个完美的圆圈,你可能会略微超过1,但你可以通过除以最大值或舍入来调整。此外,可以使用整数运算,并且可以通过对e
使用2的负幂来消除乘法/除法,因此可以使用shift。
答案 5 :(得分:6)
对于LED,您可能只需要进行16步左右而无需进行插值。也就是说,我可以在sin1()
函数中看到至少两个奇怪的东西:
1)sine_table
中有40个数据点,但您输入的索引x1
模数为41。这似乎不是处理周期性的正确方法,并让x1
指向数组的最后一个索引。
2)您然后添加+1,因此x2
甚至可以超出数组的限制。
3)您在函数中使用i
,但它仅在主程序中设置。我不能告诉它应该做什么,但在简单的计算函数中使用这样的全局似乎很脏。也许它应该为插值提供小数部分,但是你不应该使用phase
。
这是一个简单的插补器,似乎可行。调整味道。
#include <assert.h>
int A[4] = {100, 200, 400, 800};
int interpolate(float x)
{
if (x == 3.00) {
return A[3];
}
if (x > 3) {
return interpolate(6 - x);
}
assert(x >= 0 && x < 3);
int i = x;
float frac = x - i;
return A[i] + frac * (A[i+1] - A[i]);
}
一些任意的样本输出:
interpolate(0.000000) = 100
interpolate(0.250000) = 125
interpolate(0.500000) = 150
interpolate(1.000000) = 200
interpolate(1.500000) = 300
interpolate(2.250000) = 500
interpolate(2.999900) = 799
interpolate(3.000000) = 800
interpolate(3.750000) = 500
(我将把它留给感兴趣的读者用适当定义的符号常量替换所有出现的3
,以进一步推广该函数,并实现计算负相位。 )子>
答案 6 :(得分:5)
我会选择Bhaskara,我接近正弦函数。使用度数,从0到180,您可以像这样近似值
float Sine0to180(float phase)
{
return (4.0f * phase) * (180.0f - phase) / (40500.0f - phase * (180.0f - phase));
}
如果您想考虑任何角度,请添加
float sine(float phase)
{
float FactorFor180to360 = -1 * (((int) phase / 180) % 2 );
float AbsoluteSineValue = Sine0to180(phase - (float)(180 * (int)(phase/180)));
return AbsoluteSineValue * FactorFor180to360;
}
如果你想以弧度为单位,你可以添加
float SineRads(float phase)
{
return Sine(phase * 180.0f / 3.1416);
}
这是一个图表,显示了用这个近似值计算的点数,以及用正弦函数计算的点数。你几乎看不到从实际正弦点下面偷看的近似点。
答案 7 :(得分:5)
除非你的应用程序需要真正的精确度,否则不要自己想出一个40点正弦波或余弦波的算法。此外,表格中的值应与LED的pwm输入范围相匹配。
那就是说,我看了你的代码和它的输出,并认为你没有在点之间进行插值。经过一点修改,我修复了它,并且excel的标志功能和你之间的错误最多约为0.0032左右。这个变化非常容易实现,并且已经使用tcc进行了测试,tcc是我个人的C算法测试。
首先,我向你的正弦数组添加了一个点。最后一个点设置为与正弦数组中的第一个元素相同的值。这将修复正弦函数中的数学运算,特别是当您将x1设置为(int)phase%40,x2设置为x1 + 1时。添加额外的点是不必要的,因为你可以将x2设置为(x1 + 1)%40,但我选择了第一种方法。我只是指出了你可以通过不同的方式实现这一目标。我还添加了余数的计算(基本相 - (int)阶段)。我使用余数进行插值。我还添加了一个临时正弦值持有者和一个delta变量。
const int sine_table[41] =
{0, 5125, 10125, 14876, 19260, 23170, 26509, 29196,
31163, 32364, 32767, 32364, 31163, 29196, 26509, 23170,
19260, 14876, 10125, 5125, 0, -5126, -10126,-14877,
-19261, -23171, -26510, -29197, -31164, -32365, -32768, -32365,
-31164, -29197, -26510, -23171, -19261, -14877, -10126, -5126, 0};
int i = 0;
int x1 = 0;
int x2 = 0;
float y = 0;
float sin1(float phase)
{
int tsv,delta;
float rem;
rem = phase - (int)phase;
x1 = (int) phase % 40;
x2 = (x1 + 1);
tsv=sine_table[x1];
delta=sine_table[x2]-tsv;
y = tsv + (int)(rem*delta);
return y;
}
int main()
{
int i;
for(i=0;i<420;i++)
{
printf("%.2f, %f\n",0.1*i,sin1(0.1*i)/32768);
}
return 0;
}
结果看起来很不错。比较线性近似与系统的浮点正弦函数给出了如下所示的误差图。
答案 8 :(得分:2)
您可以使用sin
的{{3}}的前几个字词。您可以根据需要使用尽可能少的术语来达到预期的精度水平 - 比下面的示例更多的术语应该开始突破32位浮动的限制。
示例:
#include <stdio.h>
// Please use the built-in floor function if you can.
float my_floor(float f) {
return (float) (int) f;
}
// Please use the built-in fmod function if you can.
float my_fmod(float f, float n) {
return f - n * my_floor(f / n);
}
// t should be in given in radians.
float sin_t(float t) {
const float PI = 3.14159265359f;
// First we clamp t to the interval [0, 2*pi)
// because this approximation loses precision for
// values of t not close to 0. We do this by
// taking fmod(t, 2*pi) because sin is a periodic
// function with period 2*pi.
t = my_fmod(t, 2.0f * PI);
// Next we clamp to [-pi, pi] to get our t as
// close to 0 as possible. We "reflect" any values
// greater than pi by subtracting them from pi. This
// works because sin is an odd function and so
// sin(-t) = -sin(t), and the particular shape of sin
// combined with the choice of pi as the endpoint
// takes care of the negative.
if (t >= PI) {
t = PI - t;
}
// You can precompute these if you want, but
// the compiler will probably optimize them out.
// These are the reciprocals of odd factorials.
// (1/n! for odd n)
const float c0 = 1.0f;
const float c1 = c0 / (2.0f * 3.0f);
const float c2 = c1 / (4.0f * 5.0f);
const float c3 = c2 / (6.0f * 7.0f);
const float c4 = c3 / (8.0f * 9.0f);
const float c5 = c4 / (10.0f * 11.0f);
const float c6 = c5 / (12.0f * 13.0f);
const float c7 = c6 / (14.0f * 15.0f);
const float c8 = c7 / (16.0f * 17.0f);
// Increasing odd powers of t.
const float t3 = t * t * t;
const float t5 = t3 * t * t;
const float t7 = t5 * t * t;
const float t9 = t7 * t * t;
const float t11 = t9 * t * t;
const float t13 = t9 * t * t;
const float t15 = t9 * t * t;
const float t17 = t9 * t * t;
return c0 * t - c1 * t3 + c2 * t5 - c3 * t7 + c4 * t9 - c5 * t11 + c6 * t13 - c7 * t15 + c8 * t17;
}
// Test the output
int main() {
const float PI = 3.14159265359f;
float t;
for (t = 0.0f; t < 12.0f * PI; t += (PI * 0.25f)) {
printf("sin(%f) = %f\n", t, sin_t(t));
}
return 0;
}
示例输出:
sin(0.000000) = 0.000000
sin(0.785398) = 0.707107
sin(1.570796) = 1.000000
sin(2.356194) = 0.707098
sin(3.141593) = 0.000000
sin(3.926991) = -0.707107
sin(4.712389) = -1.000000
sin(5.497787) = -0.707098
sin(6.283185) = 0.000398
...
sin(31.415936) = 0.000008
sin(32.201332) = 0.707111
sin(32.986729) = 1.000000
sin(33.772125) = 0.707096
sin(34.557522) = -0.000001
sin(35.342918) = -0.707106
sin(36.128315) = -1.000000
sin(36.913712) = -0.707100
sin(37.699108) = 0.000393
正如您所看到的,精确度仍有提升空间。我不是浮点运算的天才,所以可能其中一些与floor
/ fmod
实现或执行数学运算的特定顺序有关。
答案 9 :(得分:2)
既然你试图生成一个信号,我认为使用微分方程应该不是一个坏主意!它给出了类似的东西
#include <stdlib.h>
#include <stdio.h>
#define DT (0.01f) //1/s
#define W0 (3) //rad/s
int main(void) {
float a = 0.0f;
float b = DT * W0;
float tmp;
for (int i = 0; i < 400; i++) {
tmp = (1 / (1 + (DT * DT * W0 * W0))) * (2 * a - b);
b = a;
a = tmp;
printf("%f\n", tmp);
}
}
仍设置信号的幅度和频率是颈部疼痛:/
答案 10 :(得分:1)
如果你解释为什么你不想要内置函数会有所帮助,但正如其他人所说,泰勒系列是估算价值的一种方法。然而,其他答案似乎实际上是使用Maclaurin系列,而不是泰勒。你应该有一个正弦和余弦的查找表。然后找到x 0 ,查找表中与您想要的x最接近的x值,找到d = x-x 0 。然后
sin(x)= sin(x 0 )+ cos(x 0 )* d-sin(x 0 )* d 2 / 2-cos(x 0 )* d 3 / 6 + ...
如果您的查找表是d <.01,那么每个术语的精度将超过两位数。
另一种方法是使用如果x = x 0 + d,那么
sin(x)= sin(x 0 )* cos(d)+ cos(x 0 )* sin(d)
你可以使用查找表来获得sin(x 0 )和cos(x 0 ),然后使用Maclaurin系列获得cos(d)和sin (d)。
答案 11 :(得分:0)
如果您需要浮点结果并接受整数度的分辨率,则可以使用此代码查找 sin。如果您需要 int(非浮点数)数字或更小的表,可以轻松修改代码:
/**
* Sinus lookup table
* The table only covers whole degrees lookup, and only in I. quarter of the circle. The remaining three quarters
* of the circle are figured out with logic.
*/
const float sinus_I_quarter[91] =
{
0.0000, 0.0175, 0.0349, 0.0523, 0.0698, 0.0872, 0.1045, 0.1219, 0.1392, 0.1564, // 00 .. 09
0.1736, 0.1908, 0.2079, 0.2250, 0.2419, 0.2588, 0.2756, 0.2924, 0.3090, 0.3256, // 10 .. 19
0.3420, 0.3584, 0.3746, 0.3907, 0.4067, 0.4226, 0.4384, 0.4540, 0.4695, 0.4848, // 20 .. 29
0.5000, 0.5150, 0.5299, 0.5446, 0.5592, 0.5736, 0.5878, 0.6018, 0.6157, 0.6293, // 30 .. 39
0.6428, 0.6561, 0.6691, 0.6820, 0.6947, 0.7071, 0.7193, 0.7314, 0.7431, 0.7547, // 40 .. 49
0.7660, 0.7771, 0.7880, 0.7986, 0.8090, 0.8192, 0.8290, 0.8387, 0.8480, 0.8572, // 50 .. 59
0.8660, 0.8746, 0.8829, 0.8910, 0.8988, 0.9063, 0.9135, 0.9205, 0.9272, 0.9336, // 60 .. 69
0.9397, 0.9455, 0.9511, 0.9563, 0.9613, 0.9659, 0.9703, 0.9744, 0.9781, 0.9816, // 70 .. 79
0.9848, 0.9877, 0.9903, 0.9925, 0.9945, 0.9962, 0.9976, 0.9986, 0.9994, 0.9998, // 80 .. 89
1.0000 // 90
};
#define CIRCLE_QUARTER_1 1
#define CIRCLE_QUARTER_2 2
#define CIRCLE_QUARTER_3 3
#define CIRCLE_QUARTER_4 4
/**
* The function converts passed angle into 0..89 degrees and looks up a corresponding sin() value.
* After lookup the return value is adjusted depending what was the original passed angle by figuring
* out belonging to circle quarter I., II, II. or IV.
* Any angle outside the accepted angle range of 0 .. 359 will be automatically corrected.
*
* @param angle Whole angle in degrees as used in mathematics: 0 degrees is EAST (in meteorology, 0 degrees wind is NORTH)
* @return The looked-up 4-decimal points accurate sinus
*/
float sinus_lookup (unsigned int angle)
{
float sin_value;
unsigned int circle_quarter;
// correct angles outside the accepted angle range into 0 .. 359
if (angle > 359u)
angle = angle % 360u;
circle_quarter = 1 + (angle / 90u);
switch (circle_quarter)
{
case CIRCLE_QUARTER_1: // 00 .. 89
sin_value = sinus_I_quarter[angle];
break;
case CIRCLE_QUARTER_2: // 90 .. 179
sin_value = sinus_I_quarter[180 - angle];
break;
case CIRCLE_QUARTER_3: // 180 .. 269
sin_value = -sinus_I_quarter[angle - 180];
break;
case CIRCLE_QUARTER_4: // 270 .. 359
sin_value = -sinus_I_quarter[360 - angle];
break;
}
return sin_value;
}