6
\$\begingroup\$

I would like to play musical notes using a PIC microcontroller 16F887. I want to store the frequency of 109 musical notes in an array of type float, then call the needed frequency to play.

The problem is that when I have created the array as follows using mikroC, the compiler says: not enough RAM:

float notes_freq[109] = 
{
16.35,17.32,18.35,19.45,20.60,21.83,23.12,24.50,25.96,27.50,29.14,30.87,32.70,34.65,36.71,38.89,                  
41.20,43.65,46.25,49.00,51.91,55.00,58.27,61.74,65.41,69.30,73.42,77.78,82.41,87.31,92.50,98.00,                  
103.83,110.00,116.54,123.47,130.81,138.59,146.83,155.56,164.81,174.61,185.00,196.00,207.65,220.00,                  
233.08,246.94,261.63,277.18,293.66,311.13,329.63,349.23,369.99,392.00,415.30,440.00,466.16,493.88,                   
523.25,554.37,587.33,622.25,659.25,698.46,739.99,783.99,830.61,880.00,932.33,987.77,1046.50,1108.73,                 
1174.66,1244.51,1318.51,1396.91,1479.98,1567.98,1661.22,1760.00,1864.66,1975.53,2093.00,2217.46,2349.32,            
2489.02,2637.02,2793.83,2959.96,3135.96,3322.44,3520.00,3729.31,3951.07,4186.01,4434.92,4698.63,4978.03,5274.04,5587.65,5919.91,6271.93,6644.8
8,7040.00,7458.62,7902.13
};

I understand that the PIC RAM is full. How can I overcome this issue? Can I store the array in EEPROM? If so, how can I do it at compile time?

\$\endgroup\$
12
  • 5
    \$\begingroup\$ What does your consultation of your compiler's documentation reveal, how to store constant data in flash memory? You might need the keyword const and perhaps some compiler specific "hint" in the source code. \$\endgroup\$ Aug 4 at 5:13
  • 14
    \$\begingroup\$ Note that the PIC16F887, as well as many other chips in this class, lacks a floating-point unit (FPU). Consequently any floating point operations must be emulated by the compiler (if the compiler supports this), which is extremely slow. Use integer / "fixed-point" techniques instead wherever possible. \$\endgroup\$
    – TypeIA
    Aug 4 at 5:26
  • 6
    \$\begingroup\$ I'm curious how these floats are going to be converted into frequencies anyway on a PIC. I'd have thought that having a timer controlled by a uint32_t or similar would be the way to go, in which case floats are not going to be helpful. \$\endgroup\$
    – danmcb
    Aug 4 at 7:32
  • 8
    \$\begingroup\$ @thebusybee No, avoiding "float" types is not a premature optimization when programming for an integer-only platform. This class of low-performance platform has tight requirements that can't usually be ignored the way they are in large scale (desktop, server, even mobile SoC) software development. \$\endgroup\$
    – TypeIA
    Aug 4 at 10:51
  • 4
    \$\begingroup\$ Forget floats on a small MCU, use Ints \$\endgroup\$
    – Neil_UK
    Aug 4 at 11:39

4 Answers 4

16
\$\begingroup\$

You need to do two things:

  1. Make the array constant with the modifier const, as you only need to read the values.

  2. Add the compiler specific keyword code to locate the array in (flash) ROM.

const float code notes_freq[] = { 16.35, /* ... */ };

Note: You don't need to specify the size of the array, which is error prone as you will count the values manually. Let the compiler count them.

\$\endgroup\$
15
  • 1
    \$\begingroup\$ I don't think you need 'code' for XC8 (or Hitech C). Just const. \$\endgroup\$ Aug 4 at 10:53
  • 1
    \$\begingroup\$ @SpehroPefhany The OP uses microC, as the question states. And I used "compiler specific" in my answer. ;-) \$\endgroup\$ Aug 4 at 11:09
  • 2
    \$\begingroup\$ "You don't need to specify the size of the array, which is error prone" You don't need to but doing so provides a useful cross-check that you provided the expected number of entries. Otherwise the compiler will silently build an array with an incorrect number of entries. Good luck finding that in debug. \$\endgroup\$
    – Graham Nye
    Aug 4 at 11:21
  • 6
    \$\begingroup\$ @GrahamNye This only works if the specified size is smaller than the number of provided elements. The other way around unspecified elements are silently zero-initialized. I see no gain. If you need an size check, do it explicitely by one of the usual methods. As a sidenote, even hobbyist's project should do testing. ;-) \$\endgroup\$ Aug 4 at 12:26
  • 2
    \$\begingroup\$ Agree with not explicitly setting the array size. Does microC support static asserts? You could use that as a compile-time check on the size of the array if it does. \$\endgroup\$
    – brhans
    Aug 4 at 13:00
12
\$\begingroup\$

Even if you say so, you actually don't want or need to store 109 floats.

First of all, each octave is 12 notes only and next and previous octaves are gotten by doubling or halving the frequencies.

And they don't need to be floats either, just pick 12 highest notes you want to support and use the frequencies as integers. If you want higher than 1 Hz precision then use the octave that scales between 32768 and 65535 Hz and shift it down to any octave you want as there will likely be more precision you can ever use to generate a tone. Depends on how you generate the tone though, with frequency divider or DDS.

If you want to avoid math with frequencies then it might be better to not store the frequencies at all but the precalculated values you actually need to produce those frequencies.

If you don't use floats you have more room storing intermediate notes for sliding and finetuning between the 12 notes of an octave.

\$\endgroup\$
7
  • \$\begingroup\$ "First of all, each octave is 12 notes only and next and previous octaves are gotten by doubling or halving the frequencies.", Can explaine it more, or give me a reference or example code to do that, because I did not grasp the point. \$\endgroup\$ Aug 5 at 4:17
  • 1
    \$\begingroup\$ @learndesign You need to learn basics of music theory to understand that but you don't need to learn it if you just want to code it. Just look at your frequency table. Take middle A note with 440 Hz, go up or down an octave, and you land up at note A with 880 Hz or 220 Hz which is double or half the 440 Hz. Therefore you only need a table of all notes within an octave, 12. \$\endgroup\$
    – Justme
    Aug 5 at 9:18
  • 1
    \$\begingroup\$ Using a frequency table that includes a 16-bit value for each distinct note is hardly unreasonable--it's what I usually do, though if one wanted to be maximally compact one should store note numbers as a 4-bit octave part and a 4-bit note-within-octave part, and have a table of 8-bit values from 30-60, 60-120, or 120 to 240 and scale things up from there, preferably playing music in the key represented by pitch values that are power-of-two multiples of 30. \$\endgroup\$
    – supercat
    Aug 5 at 16:54
  • 1
    \$\begingroup\$ @supercat separating note and octave to their own nybbles is neat as it avoids math or tables. Compare that to MIDI where a note number 0-127 must be divded by 12 to get octave and modulo 12 to get the note, before you can look up a frequency or other value and scale it to correct octave. \$\endgroup\$
    – Justme
    Aug 5 at 17:14
  • \$\begingroup\$ @Justme: On the flip side, separating note and octave complicates some operations such as transposing music up or down by some number of half steps, and also increases the size of frequency table needed to hold every frequency individually (on a properly-tuned acoustic piano or faithful imitation thereof, notes an octave apart will have a frequency ratio that is just a smidgen over 2:1). \$\endgroup\$
    – supercat
    Aug 5 at 17:28
2
\$\begingroup\$

This is just answering a comment from the OP.

Justme rightfully wrote:

each octave is 12 notes only and next and previous octaves are gotten by doubling or halving the frequencies.

learn design replied:

Can explaine it more, or give me a reference or example code to do that, because I did not grasp the point.

If you format your array with 12 frequencies per source line, it should look like this:

float notes_freq[109] = {
    16.35, 17.32, 18.35, 19.45, 20.60, 21.83, 23.12, 24.50, ...
    32.70, 34.65, 36.71, 38.89, 41.20, 43.65, 46.25, 49.00, ...
    65.41, 69.30, 73.42, 77.78, 82.41, 87.31, 92.50, 98.00, ...
    ...
};

Each row of 12 notes is called an “octave”. Note that from one octave to the next, the frequencies are multiplied by two. Then, you do not need to store the whole array: you can store just the first octave, and then compute any other frequency by multiplying the appropriate note (from the first octave) by a suitable power of two.

Example code:

float notes_freq[12] = { 16.351598, 17.323914, ... };

float note_frequency(int note)
{
    int multiplier = 1;
    while (note >= 12) {
        note -= 12;
        multiplier <<= 1;
    }
    return notes_freq[note] * multiplier;
}

You may want to add some bound checks.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ A few notes, the table is still floats and it is in ram and they both are unncessary on a small micro. The loop is nice on a small micro to reduce complex math, but division and modulo might demonstrate the idea better. \$\endgroup\$
    – Justme
    Aug 5 at 16:31
2
\$\begingroup\$

I think this answer actually offers the best solution to your specific problem.

However, for the more general case, you can often save a lot of space and a lot of CPU cycles in microcontroller programming by completely bypassing the need for float variables and instead storing your values in Q/Fixed Point format.

The long and short of it is that you take your fractional number and store it in an integer, left shifted by some amount (the amount is a trade off between space needed and precision). You then do your calculations with these integer values (you need to adjust the shift for certain operations) which is far faster than with floats. Then when you want your result, you right shift your integer right back out.

For reasonably low precisions, like yours, this saves on both the RAM requirements and CPU cycles. If you needed higher precision, the RAM requirements have less of a saving but the CPU saving of doing integer operations is almost always worth it on an MCU without an FPU even if you end up using huge integers.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ This, like other answers, misses a general important point: Why should the OP store constant values in RAM, if these values are only read and never written? The program needs to have the values in ROM anyway, to initialize the variables in RAM, and some code to do so. I would not waste precious RAM space for constant values. -- However, the point of reducing the size of such constants, is independent from the first point. In so far such answers are valuable, but answer another question. \$\endgroup\$ Aug 5 at 14:07
  • 1
    \$\begingroup\$ @thebusybee - I don't think I have missed the point here, the answer I said was ideal for this use case avoids storing these values at all, which I think is even better than storing them in ROM given how valuable ROM space can be and how easily these values can be calculated. Whilst, sure, my answer is somewhat tangential, I think it still adds value because it can be used in conjunction with any of the answers above to make them more effective \$\endgroup\$ Aug 5 at 14:33
  • 1
    \$\begingroup\$ The method where I said in my answer to use as large as integers as possible, is basically same as using fixed point without using the word fixed point. That's because you always divide down, or shift, the value to smaller, as frequencies in the table are absurdly high for human ears. It's up to the user to either use or discard the fractional bits. \$\endgroup\$
    – Justme
    Aug 5 at 16:25
  • \$\begingroup\$ @Justme - To be honest, I missed that... I saw the essence of your answer about being calculating them on the fly and not bothering with the fractional parts of the frequency rather than this being sort of pseudo-fixed-point \$\endgroup\$ Aug 6 at 16:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.