It would be great if you can show me the math I need to convert the integer values to Q15.
Like I said, if you use the signed fractional 16-bit format, the 16-bit ADC samples are in a suitable format.
If you want to convert values 0 to 1023, inclusive, to correspond to 0.0 to 0.9990234375=1023/1024 in Q15, just shift the value left by
6 5 bits.
Also from your explanation, can I assume that the example I have given is incorrect?
Yes.
Let's start from the beginning, then.
The ADC in your microcontroller has 10 bits of precision. It does not just yield values 0 to 2
10-1, but has 8 different modes in which the results can be obtained:
- Mode 0 = 0b000: Unsigned 16-bit integer. The 32-bit ADC result words (ADC1BUFx) reads in binary 0000 0000 0000 0000 0000 00dd dddd dddd, i.e. between 0 and 1023, inclusive.
- Mode 1 = 0b001: Signed 16-bit integer. The 32-bit ADC result words (ADC1BUFx) reads in binary 0000 0000 0000 0000 0000 sssd dddd dddd, i.e. between 0 and 511, or 57344 and 65535, inclusive. As a signed 16-bit integer, the value is between -512 and 511, inclusive.
- Mode 2 = 0b010: Unsigned 16-bit fractional. The 32-bit ADC result words (ADC1BUFx) reads in binary 0000 0000 0000 0000 dddd dddd dd00 0000, i.e. between 0 and 65472, inclusive, in steps of 64.
- Mode 3 = 0b011: Signed 16-bit fractional. The 32-bit ADC result words (ADC1BUFx) reads in binary 0000 0000 0000 0000 sddd dddd dd00 0000, i.e. between 0 and 32704, or 32768 and 65472, inclusive, in steps of 64. As a signed 16-bit integer, the value is between -32768 and 32704, inclusive, in steps of 64.
- Mode 4 = 0b100: Unsigned 32-bit integer. The 32-bit ADC result words (ADC1BUFx) reads in binary 0000 0000 0000 0000 0000 00dd dddd dddd, i.e. between 0 and 1023, inclusive.
- Mode 5 = 0b101: Signed 32-bit integer. The 32-bit ADC result words (ADC1BUFx) reads in binary ssss ssss ssss ssss ssss sssd dddd dddd, i.e. between 0 and 1023, inclusive.
- Mode 6 = 0b110: Unsigned 32-bit fractional. The 32-bit ADC result words (ADC1BUFx) reads in binary dddd dddd dd00 0000 0000 0000 0000 0000, i.e. between 0 and 4290772992, inclusive, in steps of 4194304.
- Mode 7 = 0b111: Signed 32-bit fractional. The 32-bit ADC result words (ADC1BUFx) reads in binary sddd dddd dd00 0000 0000 0000 0000 0000, i.e. between -2147483648 and 2143289344, inclusive, in steps of 4194304.
Above, d corresponds to value bits and s is sign bit.
Of these modes, 3 is compatible with Q15, and 7 is compatible with Q31. So, if you use these modes, your ADC data is in suitable form for the FFT.
Fixed point integers are perfectly normal integers, that
represent a different value. For example, if
x = 4251 = 0b0001000010011011, as a Q15 it represents value 4251/32768 ≃ 0.12973.
As the
Fixed-point aritmetic article at Wikipedia explains, addition and subtraction of fixed-point integer types is done exactly like for normal integers, but multiplication and division involves an extra scaling (bit shift) operation.
The FFT library you have only supports logical values from -1 to just under +1, represented by fixed-point integers.
It is up to you to decide how you map your ADC readings for the FFT, but if you use ADC mode 3 or 7, it provides the 16- or 32-bit ADC samples in a form that corresponds to -1 (smallest or most negative voltage the ADC can detect) to +1 (greatest or most positive voltage the ADC can detect) in Q15 or Q31 fixed-point format.
You only need to do math on the ADC readings if you need a custom range, so that e.g. an specific ADC reading corresponds to 0, and another to (just under) +1.
Mathematically, scaling \$x\$ from \$[x_{min}, x_{max}]\$ to \$[y_{min}, y_{max}]\$ is
$$y = (x - x_{min}) \frac{y_{max} - y_{min}}{x_{max} - x_{min}} + y_{min}$$
This only applies to integers if we do rounding correctly. Unfortunately, in C/C++, integer division truncates, i.e. rounds towards zero. Most people, including Arduino developers, do not care about such minor details, so their
map() function behaves differently for integer and floating-point values.
If we want to do rounding correctly, we need to use
$$y = \frac{(x - x_{min})(y_{max} - y_{min}) + r}{x_{max} - x_{min}} + y_{min}$$
where \$r\$ has magnitude just less than half of \$x_{max} - x_{min}\$, but has the same sign as \$y_{max} - y_{min}\$. This yields the same as calculating the value using floating-point numbers using the mathematically correct formula, and then rounding it to an integer. When programming, one needs to notice that the product is calculated first, and therefore the temporary variables need to be large enough (
int32_t if using
int16_t, for example).
For example, let's say we use ADC mode 0, but for say electrical reasons our readings are always between say 113 and 742, inclusive.
For the FFT, we wish to map these to say -0.25 to just under 0.75, which in Q15 correspond to integers -8192 and 24575.
Thus, we have \$x_{min} = 113\$, \$x_{max} = 742\$, \$y_{min} = -8192\$, \$y_{max} = 24575\$, and \$r = 314\$. To convert a buffer full of samples we could use
/* Maps buffer values 113..742, inclusive, to -8192..24575, with correct rounding. */
void prepare_adc(int16_t buffer[], uint32_t count)
{
for (uint32_t i = 0; i < count; i++)
if (buffer[i] <= 113)
buffer[i] = -8192;
else
if (buffer[i] < 742)
buffer[i] = ((int32_t)(buffer[i] - 113)*32767 + 314) / 629 - 8192;
else
buffer[i] = 24575;
}
which produces the same results if you used floating-point calculation,
buffer[i] = round( (buffer[i] - 113.0) * 32767.0 / 629.0 - 8192.0);
Mathematically, \$r\$ is the additive rounding factor needed to round towards nearest integer instead of towards zero; and the integer division (by 629, above) in C and C++ rounds towards zero.