Best Practices for Converting MATLAB Code to Fixed Point
By Harshita Bhurat, Tom Bryan, and Julia Wall, MathWorks
When converting a floating-point implementation to fixed point, engineers must identify optimal fixed-point data types that meet the constraints of embedded hardware while satisfying system requirements for numerical accuracy. Fixed-Point Designer™ helps you develop fixed-point algorithms and convert floating-point algorithms to fixed point by automatically proposing data types and fixed-point attributes and enabling comparisons of bit-true fixed-point simulation results with floating-point baselines.
This article outlines best practices for preparing MATLAB® code for conversion, converting MATLAB code to fixed point, and optimizing your algorithm for efficiency and performance. Whether you are designing fixed-point algorithms in MATLAB in preparation for hand-coding or converting to fixed point for code generation, these best practices will help you turn your generic MATLAB code into an efficient fixed-point implementation.
Prepare Code for Fixed-Point Conversion
There are three steps that you can take to ensure a smooth conversion process:
- Separate your core algorithm from other code.
- Prepare your code for instrumentation and acceleration.
- Check the functions you use for fixed-point support.
Separate the Core Algorithm from Other MATLAB Code
Typically, an algorithm is accompanied by code that sets up input data and code that creates plots to verify the result. Since only the algorithmic code needs to be converted to fixed point, it is more efficient to structure the code so that a test file creates inputs, invokes the core algorithm, and plots the results, and algorithmic files perform the core processing (Table 1).
Original 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') |
Modified Code Test file. % TEST INPUT x = randn(100,1); % ALGORITHM y = cumulative_sum(x); % VERIFY RESULTS yExpected = cumsum(x); plot(y-yExpected) title('Error') Algorithm file. 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 |
Prepare Your Algorithmic Code for Instrumentation and Acceleration
Instrumentation and acceleration help streamline the conversion process. You can use Fixed-Point Designer to instrument your code and log minimum and maximum values of all named and intermediate variables. The tool can use these logged values to propose data types for use in the fixed-point code.
With Fixed-Point Designer you can also accelerate your fixed-point algorithms by creating a MEX file, and speed up the simulations needed to verify the fixed-point implementation against the original version.
Instrumentation and acceleration both rely on code generation technology, so before you can use them you must prepare your algorithms for code generation even if you do not plan to use MATLAB Coder™ or HDL Coder™ to generate the C or HDL code. First, identify functions or constructs in your MATLAB code not supported for code generation (see Language Support for a list of supported functions and objects).
There are two ways to automate this step:
- Add the
%#codegen
pragma to the top of the MATLAB file containing your core algorithm code. This pragma instructs Code Analyzer to flag functions and constructs that are not included in the subset of the MATLAB language supported for code generation.
- Use the Code Generation Readiness tool to produce a report that identifies calls to functions and the use of data types not supported for code generation.
Once you have prepared your algorithm for code generation, you can use Fixed-Point Designer to instrument and accelerate it. Use buildInstrumentedMex
to enable instrumentation for logging minimum and maximum values of all named and intermediate variables, and use showInstrumentationResults
to view a code generation report with proposed data types for use in the fixed-point code. Invoke fiaccel
to translate your MATLAB algorithm to a MEX file and accelerate your fixed-point simulations.
Check for Fixed-Point Support for Functions Used in Your Algorithmic Code
If you identify a function that is not supported for fixed point, you have three options:
- Replace the function with a fixed-point equivalent.
- Write your own equivalent function.
- Insulate the unsupported function with a cast to double at the input, and a cast back to a fixed-point type at the output.
You can then continue converting your code to fixed point, and return to the unsupported function when you have a suitable replacement (Table 2).
Original Codey = 1/exp(x); |
Modified Codey = 1/exp(double(x)); |
Manage Data Types and Control Bit Growth
In a fixed-point implementation, fixed-point variables must remain fixed point, and not be inadvertently turned into doubles. It is also important to prevent bit growth. For example, consider the following line of code:
y = y + x(n)
This statement overwrites y
with the value of y + x(n)
. When you introduce fixed-point data types into your code, y
may change data types when it is overwritten, potentially resulting in bit growth.
To preserve the data type of y
use (:) =
syntax (Table 3). This syntax, known as subscripted assignment, instructs MATLAB to retain the existing data type and array size of the overwritten variable. The statement y(:) = y + x(n)
will cast the right-hand-side value into y
's original data type and prevent bit growth.
Original Codey = 0; for n=1:length(x) y = y + x(n); end |
Modified Codey = 0; for n=1:length(x) y(:) = y + x(n); end |
Create a Types Table to Separate Data Type Definitions from Algorithmic Code
Separating data type definitions from algorithmic code makes it easier to compare fixed-point implementations and retarget your algorithm to a different device.
To apply this best practice, do the following:
- Use
cast(x,'like',y)
orzeros(m,n,'like',y)
to cast a variable to your desired data type when it is first defined. - Create a table of data type definitions, starting with the original data types used in your code—typically, double-precision floating-point, the default data type in MATLAB (Table 4a).
- Before converting to fixed point, add
single
data types to the types table to find type mismatches and other problems (Table 4b). - Verify the connection by running the code connected to each table with different data types and comparing the results.
Original Code% Algorithm
n = 128;
y = zeros(size(x));
|
Modified Code% Algorithm T = mytypes('double'); n = cast(128,'like',T.n); y = zeros(size(x),'like',T.y); % Types Table function T = mytypes(dt) switch(dt) case 'double' T.n = double([]); T.y = double([]); end end |
Original Code% Types Table function T = mytypes(dt) switch(dt) case 'double' T.n = double([]); T.y = double([]); end end |
Modified Codefunction T = mytypes(dt) switch(dt) case 'double' T.n = double([]); T.y = double([]); case 'single' T.n = single([]); T.y = single([]); end end |
Add Fixed-Point Entries to the Types Table
Once you have created a table of data type definitions, you can add fixed-point types based on your objectives for converting to fixed point. For example, if you plan to implement your algorithm in C, the word lengths for your fixed-point types will be constrained to a multiple of 16. On the other hand, if you plan to implement in HDL, the word length is not constrained.
To get a set of fixed-point type proposals for your code, use the Fixed-Point Designer commands buildInstrumentedMex
and showInstrumentationResults
(Table 5). You will need a set of test vectors that exercises the full range of types—the types proposed by Fixed-Point Designer are only as good as the test inputs. A long simulation run with a wide range of expected data will produce better proposals. Choose an initial set of fixed-point types from the proposals in the code generation report (Figure 1).
You can then adjust the proposed types as necessary (Tables 5 and 6).
Algorithm Codefunction [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 File% 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 |
Algorithm Codefunction [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 |
Test File% 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); |
Types Tablesfunction 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 |
Run your algorithm with the new fixed-point types and compare its output with the output of the baseline algorithm.
Optimize Data Types
Whether you select your own fixed-point data types or use those proposed by Fixed-Point Designer, look for opportunities to optimize the word lengths, fraction lengths, signedness, and possibly even the math modes (fimath
). You can do this by using scaled doubles, viewing a histogram of variable values, or testing different types in your data type table.
Use Scaled Doubles to Detect Potential Overflows
Scaled doubles are hybrids of floating-point and fixed-point numbers. Fixed-Point Designer stores scaled doubles as doubles with the scaling, sign, and word length information retained. To use scaled doubles, set the data type override (DTO) property (Table 7).
DTO Setting | Example |
DTO set locally using numerictype ‘DataType’ property |
>> 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 |
DTO set globally using fipref ‘DataTypeOverride’ property |
>> fipref('DataTypeOverride', 'ScaledDoubles'); >> T.a = fi([], 1, 16, 13); >> a = cast(pi, 'like', T.a) a = 3.1416 DataTypeMode: Scaled double: binary point scaling Signedness: Signed WordLength: 16 FractionLength: 13 |
Use buildInstrumentedMex
to run your code and showInstrumentationResults
to view the results. In the code generation report, values that would have overflowed are highlighted in red (Figure 2).
Check the Distribution of Variable Values
You can use a histogram to identify data types with values that are within range, outside range, or below precision. Click the Histogram icon to launch the NumericTypeScope and view the distribution of values observed in your simulation for the selected variable (Figure 3).
Test Different Types in Your Data Type Table
You can add your own variations of fixed-point types to your types table (Table 8).
Algorithm Codefunction [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 |
Test Filefunction 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 |
Types Tablesfunction 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 |
Compare results across iterations to verify the accuracy of your algorithm after each change (Figure 4).
Optimize Your Algorithm
There are three common ways to optimize your algorithm to improve performance and generate more efficient C code. You can:
- Use
fimath
properties to improve the efficiency of generated code - Replace built-in functions with more efficient fixed-point implementations
- Re-implement division operations
Use fimath
Properties to Improve the Efficiency of Generated Code
When you use default fimath
settings, extra code is generated to implement saturation overflow, nearest rounding, and full-precision arithmetic (Table 9a).
MATLAB CodeCode 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); |
Generated C Codeint adder(short a, short b) { int y; int i0; int i1; int i2; int i3; i0 = a; i1 = b; if ((i0 & 65536) != 0) { i2 = i0 | -65536; } else { i2 = i0 & 65535; } if ((i1 & 65536) != 0) { i3 = i1 | -65536; } else { i3 = i1 & 65535; } i0 = i2 + i3; if ((i0 & 65536) != 0) { y = i0 | -65536; } else { y = i0 & 65535; } return y; } |
To make the generated code more efficient, select fixed-point math settings that match your processor’s types. Choose fimath
properties for math, rounding, and overflow to define the rules for performing arithmetic operations on your fi
objects (Table 9b).
MATLAB CodeCode being compiled: function y = adder(a,b) y = a + b; end With types defined with fimath settings that match 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); |
Generated C Codeint adder(short a, short b)
{
return a + b;
}
|
Replace Built-In Functions with Fixed-Point Implementations
Some MATLAB functions can be replaced to achieve a more efficient fixed-point implementation. For example, you can replace a built-in function with a lookup table implementation or a CORDIC implementation, which only requires iterative shift-add operations.
Re-Implement Division Operations
Division operations are often not fully supported by hardware, and can result in slow processing. When your algorithm requires a division operation, consider replacing it with a faster alternative. If the denominator is power of two, use bit shifting; for example, use bitsra(x,3)
instead of x/8
. If the denominator is constant, multiply by the inverse; for example, use x*0.2
instead of x/5
.
Next Steps
After converting your floating-point code to fixed point by applying these best practices with Fixed-Point Designer, take the time to thoroughly test your fixed-point implementation using a realistic set of test inputs and compare the bit-true simulation results with your floating-point baseline.
Published 2014 - 92226v00