Simple sine table - General Discussion - Netduino Forums
   
Netduino home hardware projects downloads community

Jump to content


The Netduino forums have been replaced by new forums at community.wildernesslabs.co. This site has been preserved for archival purposes only and the ability to make new accounts or posts has been turned off.
Photo

Simple sine table


  • Please log in to reply
22 replies to this topic

#1 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 25 June 2013 - 09:18 PM

[font="arial, sans-serif;"]Below is a simple class implementing a zero-biased sine table with values -255 to 255. I use it frequently and thought I'd share in hope of it being useful to some of you as well. [/font]

 

Values of the first quadrant ( 0 to pi / 2) are pre-calculated and stored in a byte array. By horizontal and vertical mirroring, values for a full period (2 * pi) is achieved using only a single 256 byte array which should keep memory footprint to a minimum.

 

[font="arial, sans-serif;"]An integer indexer [/font][font="arial, sans-serif;font-size:14px;"]0 to 1023 (wraps beyond 1023) [/font]is used to get the sine values for angles 0 to 2 * pi.

 

Hopefully there are none but please let me know should you find any errors.

 

[font="arial, sans-serif;font-size:14px;"]Note: The [/font][font="arial, sans-serif;font-size:14px;"]microcontroller of [/font][font="arial, sans-serif;font-size:14px;"]Netduino 2 and Netduino Plus 2 features FPU but I don't know if .NET MF takes advantage of that, either way the regular Netduino and Netduino mini does not have an FPU.[/font]

    // Simple 8 bit sine table    public class SineTable    {        private static byte[] _sin;        public static readonly SineTable Sin = new SineTable(0);        public static readonly SineTable Cos = new SineTable(256);        private int _offset;        private SineTable(int offset)        {            _offset = offset;            // 1st quadrant (0...pi/2) in 256 steps            if (_sin == null)            {                _sin = new byte[256];                double a = 0;                double da = System.Math.PI / (2.0 * _sin.Length);                for (int i = 0; i < _sin.Length; i++, a += da)                    _sin[i] = (byte)(256 * System.Math.Sin(a));            }        }        // 1024 = 2 * pi (one period)        public int this[int index]        {            get            {                index += _offset;                index &= 1023;                // 1st quadrant?                if (index < 0x100)                    return _sin[index];                // 2nd quadrant?                else if (index < 0x200)                    return _sin[0xff - (index & 0xff)];                // 3rd quadrant?                else if (index < 0x300)                    return -_sin[index & 0xff];                // 4th quadrant                else                     return -_sin[0xff - (index & 0xff)];            }        }    }

You can easily change the resolution by altering the length of the array and the modulo (1023).

 

[font="arial, sans-serif;font-size:10.5pt;"]UPDATE: The SineTable class now contains two static instances called "Sin" and "Cos" which are used like this:[/font]

// some arbitrary angleint alpha = 134; // get sine value for angleint sinval = SineTable.Sin[alpha];// get cosine value for angleint cosval = SineTable.Cos[alpha];


#2 cutlass

cutlass

    Advanced Member

  • Members
  • PipPipPip
  • 78 posts
  • LocationNew England. :)

Posted 26 June 2013 - 01:48 AM

Nice, good idea.

Thanks!



#3 Paul Newton

Paul Newton

    Advanced Member

  • Members
  • PipPipPip
  • 724 posts
  • LocationBerkshire, UK

Posted 26 June 2013 - 06:17 AM

Good idea to Share Hans!

 

I use tables like this all the time in signal processing applications, quite often they are built into the ROM of the DSP processors we use, other times we have to include a 'C' array created by a script.

 

Would be nice to rename the class into SineAndCosineTable, and add a Cosine function that shares the same array.

 

Thanks for sharing - Paul



#4 Paul Newton

Paul Newton

    Advanced Member

  • Members
  • PipPipPip
  • 724 posts
  • LocationBerkshire, UK

Posted 26 June 2013 - 06:27 AM

Hans - I think your mail box is full - can't send you a PM.



#5 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 26 June 2013 - 06:56 AM

Hi Paul, adding cosine is a good idea. I cleaned up my inbox just now so PM should work.



#6 ziggurat29

ziggurat29

    Advanced Member

  • Members
  • PipPipPip
  • 244 posts

Posted 26 June 2013 - 01:38 PM

sine and cosine are related by a phase shift, so you don't need a separate array (much like your 4-quadrant optimization)

 

Hmm, doesn't 'byte' only hold 0-255 (or is it -128 to 127?), but never +/- 255?  Oh never mind you get an extra bit of precision since you're in the first quadrant, which is always positive (and you get the negative values in your [] method).

 

I wonder if private static byte[] _sin; could be just be a constant array (i.e. not need initialization in the constructor, and stored in ROM).  Then again, I've found that netmf only seems to be clever like that (storing in ROM) for strings, but admittedly I haven't tried an array of integers...  If it's not, then perhaps on the upside you can claim that 'sine table' has a double meaning....

 

Looks like someones gonna be doing some Fourier transforms?



#7 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 26 June 2013 - 01:57 PM

sine and cosine are related by a phase shift, so you don't need a separate array (much like your 4-quadrant optimization) I wonder if private static byte[] _sin; could be simply be a constant array (i.e. not need initialization in the constructor, and stored in ROM). Then again, I've found that netmf only seems to be clever like that for strings, but admittedly I haven't tried an array of integers... If not, then perhaps on the upside 'sine table' has a double meaning....

Yes, they are the same wave form only cosine is 90 degrees ahead, e.g. cos(x) = sin(x + pi / 2) and consequently cos[x] = sin[x + 256] in code. As you said, the array does not have to be generated run-time but then you'd have to re-generate it outside .NETMF if/when you want to change the resolution. Also, I think arrays will end up in RAM anyway and that someone (was it you?) recently investigated the latter and came to that very conclusion. Also, generating the 256 byte array does not take long.

#8 ziggurat29

ziggurat29

    Advanced Member

  • Members
  • PipPipPip
  • 244 posts

Posted 26 June 2013 - 02:31 PM

.. Also, I think arrays will end up in RAM anyway and that someone (was it you?) recently investigated the latter and came to that very conclusion....

'twas indeed, alas.  I haven't tried it for ints, though, but I'm skeptical that they will avoid ram; only strings it seems  But your table is quite small, so no biggie!



#9 Dan T

Dan T

    Advanced Member

  • Members
  • PipPipPip
  • 91 posts
  • LocationBoston (Greater)

Posted 27 June 2013 - 03:29 AM

Looks like a sine wave to me. :) See attached.

(Though I only checked 1/8th of the samples -- the AGENT watchface resolution is 128x128).

 

Attached Files



#10 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 27 June 2013 - 06:59 AM

Great, that was the intention ;-) I take it you start at -90 degrees at xpixel=0 and so zero degrees is at xpixel=64, correct?

#11 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 27 June 2013 - 08:18 PM

Added cosine and updated code of initial post (see far above).

 

AFAIK, the microcontroller of Netduino 2 and Netduino Plus 2 features FPU but I don't know if .NETMF takes advantage of that. Either way, the regular Netduino and Netduino mini does not have an FPU and could probably benefit performance-wise.

 

However, I honestly don't really know how much performance gain there is to this or even if there is any at all - could even be performance is worse :wacko: than that of Math.Sine() but I haven't tested.

 

Perhaps somebody knows off heart or is willing to test it?



#12 Dan T

Dan T

    Advanced Member

  • Members
  • PipPipPip
  • 91 posts
  • LocationBoston (Greater)

Posted 28 June 2013 - 01:22 AM


 take it you start at -90 degrees at xpixel=0 and so zero degrees is at xpixel=64, correct?

 

Nope. xpixel=0 is 0[font="Calibri, 'sans-serif';font-size:11pt;"]°[/font].  The 180[font="Calibri, 'sans-serif';font-size:11pt;"]°[/font] phase change you see is because the AGENT screen's origin is upper left, and positive Y is DOWN the screen.

Below, I've inverted the sine data to "correct" the phase.  Thanks.  I'll update the picture too.  <_<

?

?

static SineTable _sine;  ...  // initialize sine table  _sine = new SineTable();  ...  // Display single cycle of sine wave on 128 x 128 pixel screen  for (int i = 0; i < 128; i++)  {    _display.SetPixel(i, 64 - _sine[i * 8] / 4, Color.White); // Minus sign is 'cause screen Y is inverted  }


#13 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 28 June 2013 - 06:28 AM

Nope. xpixel=0 is 0. The phase change you see is because the AGENT screen's origin is upper left, and positive Y is DOWN.

Of course, how silly of me not to see that. Sorry.

#14 nakchak

nakchak

    Advanced Member

  • Members
  • PipPipPip
  • 404 posts
  • LocationBristol, UK

Posted 28 June 2013 - 12:19 PM

Nice, I am currently tinkering with something similar myself, using an ad9831 to create a basic arb sig gen although rather than storing the tables in codespace I am considering using an eeprom, that said I may end up using a CSV file on an SD card as it could be also used for storing arb waveforms... Nak.

#15 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 28 June 2013 - 12:50 PM

Cool, I've been thinking of that too at some point. It would be really nice to have a PC app where you could draw arb forms in free hand and then upload to the chip/board over USB. Also controlling freq and amplitude from the PC app.

 

Will the AD9831 retrieve output samples from EEPROM as it goes (like a RAMDAC) or will you be uploading a full "period" to the chip before starting it?



#16 Dan T

Dan T

    Advanced Member

  • Members
  • PipPipPip
  • 91 posts
  • LocationBoston (Greater)

Posted 29 June 2013 - 06:13 PM

I found an impurity that I could not let stand.

 

The mirroring duplicates samples that shouldn't be duplicated.  It is most egregious at the zero crossing -- a double sample of zero right where the sine should be most linear.

 

The fix is elegant.  I just extended the dataset by 1 sample to be endpoint INCLUSIVE, then handled the mirroring asymmetrically to avoid duplicates.  It actually cleans up the mirroring code, I think:

    // Below, endpoint asymmetry assures non-overlapping quadrants    if (index < quadLen) return _sin[index]; // 1st quadrant     index -= quadLen;    if (index < quadLen) return _sin[quadLen - index]; // 2nd quadrant    index -= quadLen;    if (index < quadLen) return -_sin[index]; // 3rd quadrant     index -= quadLen;    return -_sin[quadLen - index]; // 4th quadrant 

I also made these changes:

1) Added two public constructors, one for default sine wave and one for arbitrary phase (in radians).

2) Replaced your bitwise AND with more general modulo (%, aka "remainder"), required for next item.

3) Refactored the byte array length to be arbitrary. Just change variable quadLen to any integer and the sine data will be stored in quadLen+1 bytes.  I'm using quadLen=32 for my tiny AGENT screen.

using System;using Microsoft.SPOT;namespace AgentWatchSineWaves{    // Simple 8-bit sine table    // Adapted From http://forums.netduino.com/index.php?/topic/9115-simple-sine-table/    //    public class SineTable    {        const double Tau = 2.0 * System.Math.PI; // http://tauday.com/tau-manifesto 6.283185307179586476925286766559 ...        const int quadLen = 32; // A quandrant is one quarter length of a full period        const double amplitude = Byte.MaxValue; // Maximum byte value is 255 (byte is unsigned)        public static readonly SineTable Sin = new SineTable();        public static readonly SineTable Cos = new SineTable(Tau / 4.0);        private static byte[] _sin;        private int _offset;        public SineTable()        {            if (_sin == null) // one-time initialization            {                _sin = new byte[quadLen + 1]; // From 0 to quadLen INCLUSIVE                double a = 0; // Angle in radians                // Tau (one period) = 4 quadrants                double da = Tau / (4.0 * quadLen); // 1st quadrant (0...Tau/4)                 for (int i = 0; i < _sin.Length; i++, a += da)                    _sin[i] = (byte)(amplitude * System.Math.Sin(a));            }        }        public SineTable(double phaseInRadians) : this()        {            phaseInRadians %= Tau; // Normalize to ±1 cycle            if (phaseInRadians < 0.0) phaseInRadians += Tau; // Equivalent positive angle            _offset = (int)(phaseInRadians/Tau * 4 * quadLen);        }        public int this[int index]        {            get            {                index += _offset;                index %= 4 * quadLen; // Modulo limits index to 4 quadrants                // Below, endpoint asymmetry assures non-overlapping quadrants                if (index < quadLen) return _sin[index]; // 1st quadrant                 index -= quadLen;                if (index < quadLen) return _sin[quadLen - index]; // 2nd quadrant                index -= quadLen;                if (index < quadLen) return -_sin[index]; // 3rd quadrant                 index -= quadLen;                return -_sin[quadLen - index]; // 4th quadrant             }        }    }}


#17 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 29 June 2013 - 10:53 PM

Nice, it got better and I agree, the mirroring is more elegant now. The biwise AND was there for performance reasons, modulo requires a DIV but there might be no difference in execution time, it's just one of those autonomous things since my assembler days. When quadLen is an integer power of two, its the same thing and I was thinking of using quadBits but quadLen and modulo is more generic. You left quadLen=32, was that intentional?

#18 nakchak

nakchak

    Advanced Member

  • Members
  • PipPipPip
  • 404 posts
  • LocationBristol, UK

Posted 30 June 2013 - 10:14 AM

Cool, I've been thinking of that too at some point. It would be really nice to have a PC app where you could draw arb forms in free hand and then upload to the chip/board over USB. Also controlling freq and amplitude from the PC app. Will the AD9831 retrieve output samples from EEPROM as it goes (like a RAMDAC) or will you be uploading a full "period" to the chip before starting it?

Essentially you write to the phase register via spi, im considering making it as a shield and go module at this point in time im still reseraching but it should be doable with one of those cheap $10 dds modules you can find on eBay, when im in front of my pc shall post some links Nak.

#19 Dan T

Dan T

    Advanced Member

  • Members
  • PipPipPip
  • 91 posts
  • LocationBoston (Greater)

Posted 30 June 2013 - 04:31 PM

Nice, it got better and I agree, the mirroring is more elegant now. The biwise AND was there for performance reasons, modulo requires a DIV but there might be no difference in execution time, it's just one of those autonomous things since my assembler days. When quadLen is an integer power of two, its the same thing and I was thinking of using quadBits but quadLen and modulo is more generic. You left quadLen=32, was that intentional?

 

I'm using quadLen=32 for the tiny AGENT screen.  Converting to a different quadLen led me down the refactoring path.  And it was also how I discovered the sample overlap.  (With fewer samples, any mistake becomes more visible.)

Definitely change it back to 256 unless you're using AGENT.  32 is a bit coarse for general use.

 

I do understand how blazingly fast a single AND is, but I worry that "autonomous things since... assembler days" add code complexity when the benefit is negligible. I need to learn how to quantify such concerns and questions.  AGENT is size constrained AND severely power constrained -- so every detail might matter in the end on that platform.

 

In other words, how can I measure the difference between these two lines:

  index %= 4 * quadLen;

  index &= (4 * quadLen-1);

 

Anyone?



#20 hanzibal

hanzibal

    Advanced Member

  • Members
  • PipPipPip
  • 1287 posts
  • LocationSweden

Posted 30 June 2013 - 08:01 PM

Measure time spent performing, say 10k of each variant and then compare the end results. There are always trade-offs between, speed, complexity, size, ease of use, clarity, etc. In a triangle, you caanot be close to any single corner without getting farther away from the other two :-) @nakchak: I've got one of these laying around but I don't think it can do arbitrary wave forms, only sine and square: http://www.ebay.com/...=item5d3f85ca20




0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users

home    hardware    projects    downloads    community    where to buy    contact Copyright © 2016 Wilderness Labs Inc.  |  Legal   |   CC BY-SA
This webpage is licensed under a Creative Commons Attribution-ShareAlike License.