This latest instalment of Adam Taylor's blog covers fixed-point math for implementation in the ARM-based Zynq SoC's programmable logic to improve performance.
In the last instalment of this blog—which has now been going weekly for six months now—we implemented a transfer function within the Zynq SoC’s Processor System (PS) side and crudely measured its calculation time. In this blog we start looking at what we need to understand and what we need to do to add VHDL code (or Verilog if you so desire) to the peripheral we created previously to implement and accelerate the same transfer function. We will be using a fixed-point number system to do this. Therefore, this blog looks at how fixed-point math works and how we can implement it correctly within the programmable logic (PL) side of the Zynq SoC.
There are two methods of representing numbers within a design: fixed point or floating point. Fixed-point representation maintains the decimal point at a fixed position, which greatly simplifies arithmetic operations. A fixed-point number consists of two parts called the integer and fractional parts as shown in this figure:
The integer part of the number is to the left of the implied decimal point and the fractional part is on the right. The above fixed-point number is capable of representing an unsigned number of between 0.0 and 255.9906375 or a signed number of between –128.9906375 and 127.9906375 using twos-complement representation.
Floating point numbers are divided into two parts the exponent and the mantissa. Floating-point representation allows the decimal point to float within the number depending upon the value’s magnitude.
The major drawback of fixed-point representation is that to represent larger numbers or to achieve a more accurate result with fractional numbers, you need more bits. Although FPGA’s can support both fixed and floating point numbers, most applications employ fixed-point number systems because they are simpler to implement than floating-point ones.
Within a design we can choose to use either unsigned or signed numbers. Typically, the choice is constrained by the algorithm being implemented. Unsigned numbers can represent a range of 0 to 2n – 1, and always represent positive numbers. Signed numbers use the twos-complement number system to represent both positive and negative numbers. The twos complement number system allows subtraction of one number from another simply by adding the two numbers. The range a twos-complement number can represent is:
- (2n-1) to + (2n-1 – 1)
The normal way of representing the split between integer and fractional bits within a fixed-point number is x,y where x represents the number of integer bits and y the number of fractional bits. For example 8,8 represents 8 integer bits and 8 fractional bits while 16,0 represents 16 integer and 0 fractional.
In many cases, the number of integer and fractional bits will be made at design time, normally following conversion of an algorithm from floating point. Thanks to the flexibility of FPGA’s, we can represent a fixed-point number of any bit length; we’re not limited to 32-, 64- or even 128-bit registers. FPGAs are equally happy with 15-, 37-, or 1024-bit registers. We can scale the hardware to fit the problem exactly.
The number of required integer bits depends upon the maximum integer value the number is required to store. The number of fractional bits depends upon the desired accuracy of the final result.
To determine the number of integer bits required, we can use the following equation:
For example the number of integer bits required to represent a value between 0.0 and 423.0 is:
We need 9 integer bits, allowing a range of 0 to 511 to be represented.
The decimal points of both fixed-point operands must be aligned to add, subtract, or divide the two numbers. That is, an x,8 number can only added to, subtracted from, or divided by a number that’s also in an x,8 representation. To perform arithmetic operations on numbers of different x,y formats, we must first align the decimal points. Note it is not strictly necessary to align the decimal points for division. However, implementing fixed-point division requires careful consideration to ensure that the result is scaled correctly in this case and not negative.
Similarly, the decimal points do not need to be aligned when multiplying two fixed-point numbers together. Multiplication provides a result that is X1 + X2, Y1 + Y2 wide. To clarify, multiplying two fixed-point numbers formatted as 14,2 and 10,6 produces a 24,8 result (formatted as 24 integer bits and 8 fractional bits).
For division by fixed constants, we can of course simplify the design by calculating the reciprocal of the constant and then using that constant result as a multiplier. Often, this technique results in a more efficient design implementation.
With this explanation of fixed-point math as a preface, we can progress to looking at implementing a function within the FPGA using fixed-point number systems in the next instalment of this blog series.
Incidentally I am speaking at EElive! this week in San Jose, California. If you are attending the event and see me, please stop and say “hello.” I’d love to meet you. Here’s my schedule.
Please see the previous entries in this MicroZed series by Adam Taylor: