manual fixed-point conversion best practices
fixed-point designer™ software helps you design and convert your algorithms to fixed point. whether you are simply designing fixed-point algorithms in matlab® or using fixed-point designer in conjunction with mathworks® code generation products, these best practices help you get from generic matlab code to an efficient fixed-point implementation. these best practices are also covered in this webinar:
create a test file
a best practice for structuring your code is to separate your core algorithm from other code that you use to test and verify the results. create a test file to call your original matlab algorithm and fixed-point versions of the algorithm. for example, as shown in the following table, you might set up some input data to feed into your algorithm, and then, after you process that data, create some plots to verify the results. since you need to convert only the algorithmic portion to fixed-point, it is more efficient to structure your code so that you have a test file, in which you create your inputs, call your algorithm, and plot the results, and one (or more) algorithmic files, in which you do the core processing.
original code | best practice | modified code |
---|---|---|
% test input x = randn(100,1); % algorithm y = zeros(size(x)); y(1) = x(1); for n=2:length(x) y(n)=y(n-1) x(n); end % verify results yexpected=cumsum(x); plot(y-yexpected) title('error') | issue generation of test input and verification of results are intermingled with the algorithm code. fix create a test file that is separate from your algorithm. put the algorithm in its own function. | test file % test input x = randn(100,1); % algorithm y = cumulative_sum(x); % verify results yexpected = cumsum(x); plot(y-yexpected) title('error') algorithm in its own function function y = cumulative_sum(x) y = zeros(size(x)); y(1) = x(1); for n=2:length(x) y(n) = y(n-1) x(n); end end |
you can use the test file to:
verify that your floating-point algorithm behaves as you expect before you convert it to fixed point. the floating-point algorithm behavior is the baseline against which you compare the behavior of the fixed-point versions of your algorithm.
propose fixed-point data types.
compare the behavior of the fixed-point versions of your algorithm to the floating-point baseline.
your test files should exercise the algorithm over its full operating range so that the simulation ranges are accurate. for example, for a filter, realistic inputs are impulses, sums of sinusoids, and chirp signals. with these inputs, using linear theory, you can verify that the outputs are correct. signals that produce maximum output are useful for verifying that your system does not overflow. the quality of the proposed fixed-point data types depends on how well the test files cover the operating range of the algorithm with the accuracy that you want.
prepare your algorithm for code acceleration or code generation
using fixed-point designer, you can:
instrument your code and provide data type proposals to help you convert your algorithm to fixed point, using the following functions:
buildinstrumentedmex
, which generates compiled c code that includes logging instrumentation.showinstrumentationresults
, which shows the results logged by the instrumented, compiled c code.clearinstrumentationresults
, which clears the logged instrumentation results from memory.
accelerate your fixed-point algorithms by creating a mex file using the function.
any matlab algorithms that you want to instrument using
buildinstrumentedmex
and any fixed-point algorithms that you want to
accelerate using fiaccel
must comply with code generation requirements
and rules. to view the subset of the matlab language that is supported for code generation, see .
to help you identify unsupported functions or constructs in your matlab code, use one of the following tools.
add the
%#codegen
pragma to the top of your matlab file. the matlab code analyzer flags functions and constructs that are not available in the subset of the matlab language supported for code generation. this advice appears in real-time as you edit your code in the matlab editor.for more information, see .
use the code generation readiness tool to generate a static report on your code. the report identifies calls to functions and the use of data types that are not supported for code generation. to generate a report for a function,
myfunction1
, at the command line, entercoder.screener('myfunction1')
.for more information, see .
check for fixed-point support for functions used in your algorithm
before you start your fixed-point conversion, identify which functions used in your
algorithm are not supported for fixed point. consider how you might replace them or
otherwise modify your implementation to be more optimized for embedded targets. for example,
you might need to find (or write your own) replacements for functions like
log2
, fft
, and exp
. other
functions like sin
, cos
, and
sqrt
may support fixed point, but for better efficiency, you may want
to consider an alternative implementation like a lookup table or cordic-based
algorithm.
if you cannot find a replacement immediately, you can continue converting the rest of your algorithm to fixed point by simply insulating any functions that don’t support fixed-point with a cast to double at the input, and a cast back to a fixed-point type at the output.
original code | best practice | modified code |
---|---|---|
y = 1/exp(x); | issue the
fix cast the
input to double until you have a replacement. in this case,
|
y = 1/exp(double(x)); |
manage data types and control bit growth
the (:)= syntax is known as subscripted assignment. when you use this syntax, matlab overwrites the value of the left-hand side argument, but retains the existing data type and array size. this is particularly important in keeping fixed-point variables fixed point (as opposed to inadvertently turning them into doubles), and for preventing bit growth when you want to maintain a particular data type for the output.
original code | best practice | modified code |
---|---|---|
acc = 0; for n = 1:numel(x) acc = acc x(n); end | issue
fix to
preserve the original data type of |
acc = 0; for n = 1:numel(x) acc(:) = acc x(n); end |
for more information, see .
separate data type definitions from algorithm
for instrumentation and code generation, create an entry-point function that calls the function that you want to convert to fixed point. you can then cast the function inputs to different data types. you can add calls to different variations of the function for comparison. by using an entry-point function, you can run both fixed-point and floating-point variants of your algorithm. you can also run different variants of fixed-point. this approach allows you to iterate on your code more quickly to arrive at the optimal fixed-point design.
this method of fixed-point conversion makes it easier for you to compare several different fixed-point implementations, and also allows you to easily retarget your algorithm to a different device.
to separate data type definitions from your algorithm:
when a variable is first defined, use
cast(x,'like',y)
orzeros(m,n,'like',y)
to cast it to your desired data type.create a table of data type definitions, starting with original data types used in your code. before converting to fixed point, create a data type table that uses all single data types to find type mismatches and other problems.
run your code connected to each table and look at the results to verify the connection.
original code | best practice | modified code |
---|---|---|
% algorithm
n = 128;
y = zeros(size(n));
| issue the default data type in matlab is double-precision floating-point. fix
|
% algorithm t = mytypes('double'); n = cast(128,'like',t.n); y = zeros(size(n),'like',t.y); function t = mytypes(dt) switch(dt) case 'double' t.n = double([]); t.y = double([]); case 'single' t.n = single([]); t.y = single([]); end end |
separating data type specifications from algorithm code enables you to:
reuse your algorithm code with different data types.
keep your algorithm uncluttered with data type specifications and switch statements for different data types.
improve readability of your algorithm code.
switch between fixed-point and floating-point data types to compare baselines.
switch between variations of fixed-point settings without changing the algorithm code.
convert to fixed point
what are your goals for converting to fixed point?
before you start the conversion, consider your goals for converting to fixed point. are you implementing your algorithm in c or hdl? what are your target constraints? the answers to these questions determine many fixed-point properties such as the available word length, fraction length, and math modes, as well as available math libraries.
build and run an instrumented mex function
build and run an instrumented mex function to get fixed-point types proposals using
the buildinstrumentedmex
and showinstrumentationresults
functions. test files should exercise your
algorithm over its full operating range. the quality of the proposed fixed-point data
types depends on how well the test file covers the operating range of the algorithm with
the accuracy that you want. a simple set of test vectors may not exercise the full range
of types, so use the proposals as a guideline for choosing an initial set of fixed-point
types, and use your best judgement and experience in adjusting the types. if loop indices
are used only as index variables, they are automatically converted to integer types, so
you do not have to explicitly convert them to fixed point.
algorithm code | test file |
---|---|
function [y,z] = myfilter(b,x,z) y = zeros(size(x)); for n = 1:length(x) z(:) = [x(n); z(1:end-1)]; y(n) = b * z; end end |
% test inputs b = fir1(11,0.25); t = linspace(0,10*pi,256)'; x = sin((pi/16)*t.^2); % linear chirp z = zeros(size(b')); % build buildinstrumentedmex myfilter ... -args {b,x,z} -histogram % run [y,z] = myfilter_mex(b,x,z); % show showinstrumentationresults myfilter_mex ... -defaultdt numerictype(1,16) -proposefl |
create a types table
create a types table using a structure with prototypes for the variables. the proposed types are computed from the simulation runs. a long simulation run with a wide range of expected data produces better proposals. you can use the proposed types or use your knowledge of the algorithm and implementation constraints to improve the proposals.
because the data types, not the values, are used, specify the prototype values as
empty ([]
).
in some cases, it might be more efficient to leave some parts of the code in floating point. for example, when there is high dynamic range or that part of the code is sensitive to round-off errors.
algorithm code | types tables | test file |
---|---|---|
function [y,z]=myfilter(b,x,z,t) y = zeros(size(x),'like',t.y); for n = 1:length(x) z(:) = [x(n); z(1:end-1)]; y(n) = b * z; end end |
function t = mytypes(dt) switch dt case 'double' t.b = double([]); t.x = double([]); t.y = double([]); case 'fixed16' t.b = fi([],true,16,15); t.x = fi([],true,16,15); t.y = fi([],true,16,14); end end |
% test inputs b = fir1(11,0.25); t = linspace(0,10*pi,256)'; x = sin((pi/16)*t.^2); % linear chirp % cast inputs t=mytypes('fixed16'); b=cast(b,'like',t.b); x=cast(x,'like',t.x); z=zeros(size(b'),'like',t.x); % run [y,z] = myfilter(b,x,z,t); |
run with fixed-point types and compare results
create a test file to validate that the floating-point algorithm works as expected before converting it to fixed point. you can use the same test file to propose fixed-point data types, and to compare fixed-point results to the floating-point baseline after the conversion.
optimize data types
use scaled doubles
use scaled doubles to detect potential overflows. scaled doubles are a hybrid between
floating-point and fixed-point numbers. fixed-point designer stores them as doubles with the scaling, sign, and word length information
retained. to use scaled doubles, you can use the data type override (dto) property or you
can set the 'datatype'
property to 'scaleddouble'
in
the fi
or numerictype
constructor.
to... | use... | example |
---|---|---|
set data type override locally |
|
t.a = fi([],1,16,13,'datatype', 'scaleddouble'); a = cast(pi, 'like', t.a) a = 3.1416 datatypemode: scaled double: binary point scaling signedness: signed wordlength: 16 fractionlength: 13 |
set data type override globally |
|
fipref('datatypeoverride','scaleddoubles') t.a = fi([],1,16,13); a = 3.1416 datatypemode:scaled double: binary point scaling signedness: signed wordlength:16 fractionlength:13 |
for more information, see .
use the histogram to fine-tune data type settings
to fine-tune fixed-point type settings, run the buildinstrumentedmex
function with the –histogram
flag
and then run the generated mex function with your desired test inputs. when you use the
showinstrumentationresults
to display
the code generation report, the report displays a histogram icon. click the icon to open
the numerictypescope and view the distribution of values observed in your simulation for
the selected variable.
overflows indicated in red in the code generation report show in the "outside range" bin in the numerictypescope. launch the numerictypescope for an associated variable or expression by clicking on the histogram view icon .
explore design tradeoffs
once you have your first set of fixed-point data types, you can then add different variations of fixed-point values to your types table. you can modify and iterate to avoid overflows, adjust fraction lengths, and change rounding methods to eliminate bias.
algorithm code | types tables | test file |
---|---|---|
function [y,z] = myfilter(b,x,z,t) y = zeros(size(x),'like',t.y); for n = 1:length(x) z(:) = [x(n); z(1:end-1)]; y(n) = b * z; end end |
function t = mytypes(dt) switch dt case 'double' t.b = double([]); t.x = double([]); t.y = double([]); case 'fixed8' t.b = fi([],true,8,7); t.x = fi([],true,8,7); t.y = fi([],true,8,6); case 'fixed16' t.b = fi([],true,16,15); t.x = fi([],true,16,15); t.y = fi([],true,16,14); end end |
function mytest % test inputs b = fir1(11,0.25); t = linspace(0,10*pi,256)'; x = sin((pi/16)*t.^2); % linear chirp % run y0 = entrypoint('double',b,x); y8 = entrypoint('fixed8',b,x); y16 = entrypoint('fixed16',b,x); % plot subplot(3,1,1) plot(t,x,'c',t,y0,'k') legend('input','baseline output') title('baseline') subplot(3,2,3) plot(t,y8,'k') title('8-bit fixed-point output') subplot(3,2,4) plot(t,y0-double(y8),'r') title('8-bit fixed-point error') subplot(3,2,5) plot(t,y16,'k') title('16-bit fixed-point output') xlabel('time (s)') subplot(3,2,6) plot(t,y0-double(y16),'r') title('16-bit fixed-point error') xlabel('time (s)') end function [y,z] = entrypoint(dt,b,x) t = mytypes(dt); b = cast(b,'like',t.b); x = cast(x,'like',t.x); z = zeros(size(b'),'like',t.x); [y,z] = myfilter(b,x,z,t); end |
optimize your algorithm
use fimath to get natural types for c or hdl
fimath
properties define the rules for performing arithmetic
operations on fi
objects, including math, rounding, and overflow
properties. you can use the fimath
productmode
and summode
properties to retain natural
data types for c and hdl. the keeplsb
setting for
productmode
and summode
models the behavior of
integer operations in the c language, while keepmsb
models the behavior
of many dsp devices. different rounding methods require different amounts of overhead
code. setting the roundingmethod
property to floor
,
which is equivalent to two's complement truncation, provides the most efficient rounding
implementation. similarly, the standard method for handling overflows is to wrap using
modulo arithmetic. other overflow handling methods create costly logic. whenever possible,
set the overflowaction
to wrap
.
matlab code | best practice | generated c code |
---|---|---|
% code being compiled function y = adder(a,b) y = a b; end with types defined with default fimath settings: t.a = fi([],1,16,0); t.b = fi([],1,16,0); a = cast(0,'like',t.a); b = cast(0,'like',t.b); | issue additional code is generated to implement saturation overflow, nearest rounding, and full-precision arithmetic. |
int adder(short a, short b) { int y; int i; int i1; int i2; int i3; i = a; i1 = b; if ((i & 65536) != 0) { i2 = i | -65536; } else { i2 = i & 65535; } if ((i1 & 65536) != 0) { i3 = i1 | -65536; } else { i3 = i1 & 65535; } i = i2 i3; if ((i & 65536) != 0) { y = i | -65536; } else { y = i & 65535; } return y; } |
code being compiled function y = adder(a,b) y = a b; end with types defined with fimath settings that match your processor types: f = fimath(... 'roundingmethod','floor', ... 'overflowaction','wrap', ... 'productmode','keeplsb', ... 'productwordlength',32, ... 'summode','keeplsb', ... 'sumwordlength',32); t.a = fi([],1,16,0,f); t.b = fi([],1,16,0,f); a = cast(0,'like',t.a); b = cast(0,'like',t.b); | fix to make the generated code more efficient, choose fixed-point math settings that match your processor types. |
int adder(short a, short b) { return a b; } |
replace built-in functions with more efficient fixed-point implementations
some matlab built-in functions can be made more efficient for fixed-point implementation. for example, you can replace a built-in function with a lookup table implementation, or a cordic implementation, which requires only iterative shift-add operations.
re-implement division operations where possible
often, division is not fully supported by hardware and can result in slow processing. when your algorithm requires a division, consider replacing it with one of the following options:
use bit shifting when the denominator is a power of two. for example,
bitsra(x,3)
instead ofx/8
.multiply by the inverse when the denominator is constant. for example,
x*0.2
instead ofx/5
.
eliminate floating-point variables
for more efficient code, eliminate floating-point variables. the one exception to this is loop indices because they usually become integer types.