Section 5 Functional Description - Combination Logic
5.1 Program Statements
5.1.1 assign statement
The assign statement is a continuous assignment statement. Generally, the value of one variable is assigned to another variable without interruption . The two variables are similar to being connected by a wire, which is used as a connection. The basic format of an assign statement is:
assign a = b (logical operator) c ...;
The function of the assign statement belongs to the category of combinational logic , and its scope of application can be summarized as follows:
(1) continuous assignment ;
(2) Connection ;
(3) Assign value to the wire type variable . Wire is a wire network, which is equivalent to the actual connection line. If you want to use assign to connect directly, use the wire type variable. The value of the wire type variable changes at any time.
It should be noted that multiple assign continuous assignment statements are executed independently and in parallel.
5.1.2 The always statement
The always statement is a conditional loop statement, and the execution mechanism is realized by driving an event called a sensitive variable table , which will be described in detail below. The basic format of the always statement is:
always @(sensitive event)begin
program statement
end
always means "always, always", @ is followed by events. The whole always means: when the condition of the sensitive event is met, execute the "program statement" once. Every time a sensitive event is satisfied, the "program statement" is executed once. (When the sensitive condition in the sensitive event changes, execute the content in the always conditional loop statement)
The meaning of this program is: When the signal a, signal b or signal d changes, execute the following statement once. When executing this statement, first judge whether the signal sel is 0, and if it is 0, execute the third line of code. If sel is not 0, execute the fifth line of code. It should be emphasized that if any one of a, b, and c changes once, lines 2 to 5 will only be executed once, and will not be executed for the second time.
It should be noted here that only the change of the sel signal will not execute the codes from line 2 to line 5, which is usually not in line with the designer's idea. For example, the general designer's idea is: when sel is 0, the result of c is a+b; when sel is not 0, the result of c is a+d. But if the trigger condition does not change, although sel changes from 0 to 1, the result of c is still a+b. So, this is not a canonical design thinking.
Therefore, redesign the code according to the designer's idea: when the signal a or the signal b or the signal d or the signal sel changes, execute lines 2 to 5. In this way, it can be ensured that when the signal value of sel is 0, the result of c must be a+b, and when sel is not 0, the result of c must be a+d. Therefore, to add sel to the sensitive list, the code is as follows.
When there are many sensitive signals, it is easy to miss the sensitive signals. To avoid this situation, you can use "*" instead. This "*" refers to all the conditional signals in the "program statement", that is, a, b, d, sel (not including c), and this writing method is also recommended, and the specific code is as follows.
This kind of always statement in which the result of a conditional signal change changes immediately is called "combinatorial logic".
The above code sensitivity list is **"posedge clk", where posedge means rising edge**. That is to say, when clk changes from 0 to 1, the program code is executed once, that is, lines 2 to 5, and the value of c remains unchanged at other times. It should be emphasized that if clk does not change from 0 to 1, then even if a, b, d, sel change, the value of c remains unchanged.
It can be seen that the sensitive list of the above code is "negedge clk", where negedg represents the falling edge . That is to say, when clk changes from 1 to 0, the program code is executed once, that is, lines 2 to 5, and the value of c remains unchanged at other times. It should be emphasized that if clk does not change from 1 to 0, even if a, b, d, sel change, the value of c remains unchanged.
The sensitive list of the above code is "posedge clk or negedge rst_n", that is to say, when clk changes from 0 to 1, or when rst_n changes from 1 to 0, the program code is executed once, that is, lines 2 to 8, others The value of c remains unchanged at time instant. This kind of signal edge trigger, that is, the signal always changes only on the rising or falling edge, is called "sequential logic" , and the signal clk is the clock at this time. Note: To identify whether a signal is a clock is not to look at the name, but to look at where the signal is placed. Only those that are placed in the sensitive list and are edge-triggered are clocks. The signal rst_n is a reset signal, and it is not judged by the name, but placed in the sensitive list and triggered by the same edge. The more important thing is that the "program statement" first judges the value of rst_n, which means that rst_n has the highest priority. are used for reset.
The following points should be paid attention to when designing :
1. * Sensitive variables in the always statement of combinatorial logic must be written in full, or replaced with " " .
2. The assignment of combinational logic devices adopts blocking assignment "=", the assignment statement of sequential logic devices adopts non-blocking assignment "<=" ,
See the section "Blocking and non-blocking assignments" for specific reasons.
5.2 Number system
5.2.1 Digital representation
The most commonly used format for digital representation in Verilog is: <bit width>'<radix><value> , such as 4'b1011. Bit Width: A decimal integer describing the number of bits contained in the constant, which is optional. For example, the 4 in 4'b1011 is the bit width, which is 4 wires in popular understanding. If there is no such item, it can be inferred from the value of the constant. For example 'b1011 infers a bit width of 4, while 'b10010 infers a bit width of 5.
Radix : Indicates how many bases the value is. Can be b, B, d, D, o, O, h or H for binary, decimal, octal and hexadecimal, respectively . Without this entry, the default defaults to a decimal number. For example, 4'b1011 in binary can be written as 4'd11 in decimal, 4'hb in hexadecimal or 4'o13 in octal, or 11 without the base. To sum up, as long as the binary number is the same, it is the same number regardless of whether it is written in decimal, octal or hexadecimal.
Value : A string of ASCII codes representing the real value of a constant determined by the base. If the base is defined as b or B, the value can be 0, 1, x, X, z or Z. If the base is defined as o or O, the values can be 2, 3, 4, 5, 6, 7. If the base is defined as h or H, the values can be 8, 9, a, b, c, d, e, f, A, B, C, D, E, F. For radix d or D, the value sign can be any decimal number: 0 to 9, but not x or z. For example, 4'b12 is wrong, because b means binary, and the value can only be 0, 1, x or z, not including 2. 32'h12 is equal to 32'h00000012, that is, when the value is not written completely, the high bit is filled with 0.
5.2.2 Binary is the basis
In a digital circuit, if chip A transmits data to chip B, such as transmitting 0 or 1 information, chip A and chip B can be connected through a pin, and then chip A controls the output of the pin to be high or low. Level, 0 and 1 are represented by high and low levels. When chip B detects that the pin is at low level, it means it has received 0, and when chip B detects that this pin is at high level, it means it has received 1.
Conversely, if a low level is used to indicate that 1 is received, and a high level is used to indicate that 0 is received, is it possible? Of course, as long as chip A and chip B agree in advance, when chip A wants to send a digital 1, it will set the pin to low level. Chip B detects that the pin is at low level, indicating that the digital 1 is received, and the communication is completed.
A pin has two states of high and low, which can represent two situations of digital 0 and 1 respectively. What if chip A wants to send numbers 0, 1, 2, 3 to chip B?
You can connect chip A and chip B with two pins, that is, two lines: a and b. When both lines are low level, it means sending digital 0; when a is high level and b is low level, it means sending digital 1; when a is low level and b is high level, it means sending digital 2; when When both lines are high, it means sending the number 3.
According to the same principle, when chip A wants to send data 4, 5, 6, 7 to chip B, just add another line. The three wires have a total of 8 states, which can represent 8 numbers. To sum up, different level states of the line can represent different meanings, and as many different states as there are can represent as many numbers.
Let's think about it if chip A wants to send +1, -1, 0, +2 and other numbers to chip B, how to express the positive and negative here? Referring to the previous idea, the meaning of the high and low levels of the line is agreed in advance by the two sides of the chip. In this case, a single line can be used to represent the symbol, for example, low level means positive number, and high level means negative number.
Among the three lines shown in the figure above, line c is used to represent positive and negative, where 0 represents a positive number and 1 represents a negative number. Use line a and line b to represent the value, taking 3'b111 as an example, it can be interpreted as the decimal number 7, it can also be interpreted as the signed number original code "-3", and it can also be interpreted as the signed number complement "-1 , how it is interpreted depends on the engineer's definition of a binary number. As long as this definition does not affect the communication between circuits no problem will occur. Therefore, "0" and "1" in numbers can not only represent literal numerical meanings, but also represent other meanings, such as positive and negative symbols, etc. In the same way, in digital circuits, binary numbers are the foundation of other number systems such as octal, decimal, hexadecimal, signed numbers, unsigned numbers, and decimals. In FPGA design, the most fundamental reason for not knowing the calculation methods of decimal and signed numbers is not knowing the binary values corresponding to these data. As long as you understand the corresponding binary values, many problems can be solved.
Let the students understand this concept better through examples below. Many beginners often ask, how to realize decimal calculation in FPGA? Take "0.5+0.25" as an example. It is well known that the result of 0.5+0.25 is 0.75. Consider how to express 0.5, 0.25 and 0.75 in binary? The specific representation method depends on the engineer's practice, because there are many such representation methods, such as fixed-point decimals, floating-point decimals, and even as discussed above, using a few lines to define by yourself. As long as the communication can be normal, there is no problem. Suppose an engineer uses three wires to define the decimal value represented by the binary value, as shown in the table below.
binary value | definition | binary value | definition |
---|---|---|---|
3'b000 | 0.1 | 3’b100 | 0.25 |
3’b001 | 0.5 | 3'b101 | 0.3 |
3'b010 | 0.75 | 3'b110 | 0.8 |
3'b011 | 0.2 | 3'b111 | 0 |
In order to illustrate that the meaning of binary values can be freely defined, the order of numbers is out of order. Then why only these kinds of decimals? This is because the assumed system only has these kinds of numbers, and if you want to represent more numbers, you can increase the number of lines. After completing the above definition, it is very easy to realize "0.5+0.25", which is actually the "addition" of 3'b001 and 3'b100, expecting to get 3'b010. But directly using 3'b001 + 3'b100 in the table, the result is "101", which is not the desired result, and the code can be written as:
Of course, this is just one way of writing, as long as the corresponding function can be realized and the result is correct, any way of writing is fine.
There may be doubts here, 0.1+0.8 should be 0.9, but there is no representation of 0.9 in the above table. This is actually a defect in the table defined by the designer, or the designer thinks that this situation will not occur. What I want to express here is: As long as the corresponding binary numbers are defined, many functions are easy to design.
Of course, in actual engineering, the agreed and customary practices are usually followed, and there is no need to find another way. For example, the following table is the definition of commonly used fixed-point decimals:
binary value | definition | binary value | definition |
---|---|---|---|
3'b000 | 0.0 | 3’b100 | 0.5 |
3’b001 | 0.125 | 3'b101 | 0.625 |
3'b010 | 0.25 | 3'b110 | 0.75 |
3'b011 | 0.3725 | 3'b111 | 0.8725 |
At this time, if you want to realize 0+0.5=0.5, that is, add 3'b000 and 3'b100, expect to get 3'b100. It can be found that 3'b100 can be obtained by directly using binary 3'b000+3'b100. Similarly, to achieve 0.125+0.75=0.8725, that is, add 3'b001 and 3'b110, expect to get 3'b111. It can be found that 3'b111 can be obtained by directly using binary 3'b001+3'b110.
If the calculation of 0.5+0.75=1.25 is to be realized, it can be seen that 1.25 has exceeded the range of representation at this time, and this problem can be solved by increasing the signal bit width or only representing decimal places. If only decimal places are represented, the result is 0.25, that is, adding 3'b100 and 3'b110, expecting to get 3'b010. It is not difficult to find that 3'b100 + 3'b110 = 4'b1010, expressed in 3 bits is 3'b010, which is 0.25. From the above, it can be seen that the calculation of fixed-point decimals is not complicated, and the calculation can be performed directly after defining the relationship between fixed-point decimals and binary values.
5.2.3 Indeterminate states
As mentioned above, digital circuits only have high level and low level, which represent 1 and 0 respectively. But x and z can often be seen in the code, such as 1'bx, 1'bz. So what are the levels of x and z? The answer is that there is no actual level corresponding to the two. x and z are more of a designer's intent or for simulation purposes, to tell simulators and synthesizers how to interpret this code.
The X state is called an indeterminate state, which is often used to judge the condition, so as to tell the synthesis tool designer that it does not care about its level, whether it is 0 or 1 is fine.
From the above example, it can be seen that the judgment condition is din== 4'b10x0, which is equivalent to din== 4'b1000||din==4'b1010, where "||" is an "or" symbol.
However, it is better to directly write din== 4'b1000||din == 4'b1010 than "din == 4'b10x0" in the design, because this way of writing is more direct and simple.
In the process of simulation, some signals have an indeterminate state, so the designer must carefully analyze whether the indefinite state is reasonable. If you really don't care whether it's 0 or 1, then you can leave it alone. But it is recommended that all signals should not be in an indeterminate state, write clearly whether it is 0 or 1, and do not add "thinking" troubles to the design.
5.2.4 High-impedance state
Z state, generally called a high-impedance state, means that the designer does not drive this signal (neither 0 nor 1), and is usually used in the tri-state gate interface.
The above figure is an application case of the tri-state bus. The connection bus in the figure is both input and output for CPU and FPGA, and it is a bidirectional interface. In general hardware circuits, a pull-up resistor (weak pull-up) or pull-down resistor (weak pull-down) will be connected to this line.
Point A remains high when neither the CPU nor the FPGA is driving the bus. When the FPGA does not drive the bus and the CPU drives the bus, the value of point A is determined by the CPU. When the CPU does not drive the bus and the FPGA drives the bus, the value of point A is determined by the FPGA. But FPGA and CPU can't drive the bus at the same time, otherwise the level of A will be uncertain. Usually, when FPGA and CPU drive the bus, they work according to the agreement negotiated in advance.
The figure above is a typical I2C timing. The bus SDA of I2C is a three-state signal. The I2C protocol has stipulated which time is driven by the master device and which time is driven by the slave device in the above time. Both parties must abide by the agreement and cannot be driven at the same time. So how does the FPGA achieve the behavior of "not driving" in the design? This is because there are three-state gates inside the FPGA.
The tri-state gate is a piece of hardware, and the figure above is its typical structure. The tri-state gate has four interfaces, such as the write enable wr_en, write data wr_data, read data rd_data and the tri-state signal data connected to external devices as shown in the figure above.
It should be noted that the write enable signal, when the signal is valid, the tri-state gate will assign the value of wr_data to the tri-state line data, at this time the value of data is determined by wr_data, when wr_data is 0, the value of data is 0; when wr_data When it is 1, the value of data is 1. When the write enable signal is invalid, no matter what the wr_data value is, it will not affect the external data value, that is, it will not be driven.
In Verilog, the above functions are realized by the following code:
When the synthesizer sees these two lines of code, it knows that it will be synthesized into a three-state gate, and this is the role of the high-impedance z. In addition, it can be noticed that the use of tri-state lines on the hardware is to reduce pins, and there is no need to reduce wiring in the FPGA, so it is meaningless to use tri-state signals. Therefore, it is recommended that you do not use the high-impedance state "z" inside the FPGA when designing, because there is no need to add "thinking" troubles to yourself. Of course, if the high-impedance state is used in the design, no error will be reported, and the function can also be realized.
Generally speaking, the high-impedance state "z" means the behavior of "not driving the bus". In fact, digital circuits are high or low, and there are no other levels.
5.3 Arithmetic operators
Arithmetic operators include addition "+", subtraction "-", multiplication "*", division "/" and remainder "%". The commonly used arithmetic operators mainly include: addition "+", subtraction "-" and multiplication "*".
Note that the commonly used operations do not include division and remainder operators. This is because division and remainder operators are not built with simple gate logic, and the corresponding hardware circuits are relatively large. Addition and subtraction are the simplest operations, and multiplication can be disassembled into multiple addition operations, so the circuits corresponding to addition, subtraction and multiplication are relatively small. The division is different. Students can recall the steps of the division, which involves multiple multiplications, shifts, addition and subtraction, so the circuit corresponding to the division is complicated, which also requires the designer to be careful when designing Verilog. Use division with caution.
5.3.1 Addition operator
First learn the addition operator, the symbol "+" can be used directly in Verilog code:
Its circuit diagram is as follows:
A synthesizer can recognize the addition operator and turn it into a circuit like the one shown above. The binary addition operation is similar to the decimal addition operation, the decimal system is every ten, and the binary is every two. The basic operation of binary addition is as follows:
5.3.2 Subtraction operator
Subtraction operator, the symbol "-" can be used directly in Verilog code:
Its circuit diagram is as follows:
A synthesizer can recognize the subtraction operator and turn it directly into the circuit shown above.
The binary subtraction operation is similar to the decimal subtraction operation, and there is also the concept of borrowing. In decimal system, one is borrowed as ten, and in binary, one is borrowed as two. The basic operation of 1-bit subtraction is as follows:
5.3.3 Multiplication operator
Multiplication operator, the symbol "*" can be used directly in Verilog code:
Its circuit diagram is as follows:
The synthesizer can recognize the multiplication operator and turn it directly into the circuit shown above. The binary multiplication operation is similar to the decimal multiplication operation, and the calculation process is the same. The basic operation of 1-bit multiplication is as follows:
The multiplication between multiple digits is the same as the decimal calculation process. For example, the calculation process of 2'b11 * 3'b101 is as follows:
5.3.4 Division and remainder operators
The division operator can use the symbol "/" directly in Verilog code, while the remainder operator is "%":
The schematic diagram of the division circuit is as follows:
The schematic diagram of the remainder circuit is as follows:
The synthesizer can recognize the division operator and the remainder operator, but these two operators include a large number of multiplication, addition and subtraction operations, so the circuit of the divider in the FPGA is very large, and the synthesizer may not be directly converted into circuit shown in Fig.
There may be doubts here: Why is division and remainder taking a lot of resources? Let's analyze the process of decimal division and remainder, taking 122 divided by 11 as an example.
In the process of doing the above operations, multiple shifts, multiplications, subtractions and other operations are involved. That is to say, multiple multipliers and subtractors are used to perform a division operation, which requires relatively large hardware resources. The same is true for binary operations.
Therefore, in design code, division and remainder are generally not used. There are various ways to avoid division and remainder operations in the algorithm. Therefore, in digital signal processing, communication, and image processing, you will find a lot of multiplication, addition and subtraction, etc., but rarely see division and remainder operations. But in the simulation test, division and remainder can be used, because it is only used for simulation test and does not need to be synthesized into a circuit, so naturally there is no need to care about how many resources are occupied.
5.3.5 Experience Summary
bit width problem
When writing code, you need to pay attention to the bit width of the signal. The final result depends on the bit width of the signal to the left of the "=" sign, save the low bit and discard the high bit. For example:
The bit width of the signal c is 1 bit, so the result of the operation finally reserves the lowest 1 bit, so the value of c is 1'b0. Since the bit width of d is 2 bits, the lower 2 bits of the operation result can be reserved, so the value of d is 2'b10. Since the bit width of e is 3 bits, the lower 3 bits of the operation result can be reserved, so the value of e is 3'b010. "1" is 32 bits by default, and the result of 1+1 is also 32 bits, but since the bit width of f is only 3 bits, the lower 3 bits of the operation result can be reserved, so the value of f is 3'b010.
The same is true for subtraction operations. Take the following code as an example:
The binary value obtained from "0-1" is "1111111111….", but the saved result depends on the bit width of the signal to the left of the "=" sign. The bit width of c is 1, and the lowest 1 bit is reserved, so the value of c is 1'b1. Since the bit width of d is 2 bits, the lower 2 bits are reserved in the result, so the value of d is 2'b11. Since the bit width of e is 3 bits, the lower 3 bits are reserved in the result, so the value of e is 3'b111. The bit width of f is 4 bits, so the lower 4 bits of the operation result can be reserved, so the value of f is 4'b1111.
When writing the multiplication code, you also need to pay attention to the bit width of the signal. The final result depends on the bit width of the signal to the left of the "*" sign. Save the low bit and discard the high bit:
The binary value obtained by "2'b11 * 3'b101" is "4'b1111", but the saved result depends on the bit width of the signal to the left of the "*". The bit width of c is 1, and the lowest 1 bit is reserved, so the value of c is 1'b1. Since the bit width of d is 2 bits, the lower 2 bits are reserved in the result, so the value of d is 2'b11. Since the bit width of e is 3 bits, the lower 3 bits are reserved in the result, so the value of e is 3'b111. The bit width of f is 4 bits, so the lower 4 bits of the operation result can be reserved, so the value of f is 4'b1111. It should be noted that h, the signal has 5 bits, 4'b1111 is assigned to the 5-bit signal, and the result is that the high bits are filled with 0, so the result is 5'b01111.
The origin of complement
When FPGA implements various algorithms, the most important thing is to ensure the correctness of the calculation results, otherwise everything is meaningless. When analyzing the addition and subtraction operators, it can be found that whether the signal bit width for saving the result is reasonable has a great influence on the correctness.
For example the following addition operation:
It can be seen from the above table that if the carry is not preserved, the calculation result is incorrect when the addition occurs, and the calculation result is correct only if the carry is preserved. From this, we can draw a conclusion: when using addition, in order to ensure the correctness of the result, the carry must be saved, that is, the result must expand the bit width.
For example, when adding two 8-bit numbers, the result needs to be extended by one bit, and the bit width is set to 9 bits.
Next, let's analyze the subtraction operation, as shown in the following table:
Note that in the table and 2'b00-2'b01, the result is 2'b11, the corresponding decimal value is 3, but the expected result is "-1". In the same way, 2'b01 - 2'b11, the result is 2'b10, the corresponding decimal value is 2, and the expected result is "-2", so the above result is incorrect.
When the expected result is positive or negative, a sign bit can be added to distinguish the positive or negative result. The representation method agreed in the industry is that when the highest bit is 0, it means a positive number, and when the highest bit is 1, it means a negative number. The value after the sign bit is represented by the lower 2 bits, and the result is as follows:
It can be seen from the above table that after adding the sign bit, there will still be some problems that the operation results do not meet the expectations. For example, 2'b00-2'b01 in the table, the result is 3'b111, the corresponding decimal value is -3, but the expected result is "-1". So the above result is still incorrect.
Now, re-convert the binary number "000~111" as follows :
a. Positive number: remain unchanged
b. Negative number: the sign bit remains unchanged, and the value is inverted and 1 is added .
That is to say, if it is a positive number "+1", it was represented by "001" before, but it is still represented by "001" now. If it is a negative number "-1", it was represented by "101" before, but now it is represented by "111". The negative number "-3" was previously represented by "111", but now it is represented by "101". This representation is the complement representation .
After expressing it in complement code, let’s analyze the result:
It can be seen that the results in the above table are all correct and consistent with expectations. This process did not make any changes to the code at all, but the correct result was achieved by changing the definition of the data.
In the previous discussion, the operations of addend, summand, subtrahend, and minuend did not use signed numbers. Now re-represent it using the two's complement of the signed number. Assuming that the addend, the summand, the subtrahend and the minuend are all 2 bits (the range is -2~1), considering the reason of carry and borrow, the result is represented by 3 bits (the range is -4~3). Because the bit width of the result becomes 3 bits, both the subtrahend and the minuend are expanded to be represented by 3 bits, as shown in the following table:
The summary operation steps are as follows:
- According to "human common sense", the maximum and minimum values of the result are expected to determine the signal bit width of the result .
- Extend the bit width of addend, subtrahend and other data to make the bit width of the result consistent.
- Perform calculations in binary addition and subtraction.
Through the above method, what is obtained is the result of complement code. In fact, in FPGAs and even computer systems, all data is stored in complement form . If you want to know more about complement code, you can refer to related materials.
5.4 Logical operators
There are 3 logical operators in Verilog HDL language, they are:
(1) &&: logic and ;
(2) | | : logical or ;
(3) !: Logical NOT .
5.4.1 Logical AND
"&&" is a binary operator, which requires two operands, such as a && b.
(1) 1-bit logical AND
When both A and B are 1, C is 1, otherwise C is 0.
The corresponding hardware circuit diagram is as follows:
(2) Multi-bit logical AND
C is 1 when neither A nor B is 0, otherwise it is 0.
5.4.2 Logical OR
"||" is a binary operator, which requires two operands , such as a||b.
(1) 1-bit logical OR
One of A and B is 1, C is 1, otherwise C is 0.
The corresponding hardware circuit diagram is shown in the figure below:
(2) Multi-bit logical OR
If one of A and B is non-zero, C is 1, otherwise C is 0.
The corresponding hardware circuit diagram is shown in the figure below:
5.4.3 Logical NOT
"!" is a unary operator, which requires only one operand, such as! (a>b).
For the operand a, it is necessary to judge whether not a is true. If it is true, execute the operation inside {}, and if it is false, end the operation.
The following table is the truth table of logic operations, which indicates the values obtained by various logic operations when the values of a and b are in different combinations.
The final result of logical operators is only logically true or logically false, that is, 1 or 0. In general, when logical operators are used as judgment conditions, the logical AND operation can only be two 1-bit-wide numbers. Only when two expressions are true at the same time can it be true, and if one of them is false, it can be false.
If the operand is multi-bit, the operand can be regarded as a whole. If each bit in the operand is 0, it is a logic 0 value;
if there is a 1 in the operand, it is a logic 1 value.
Since neither 4'b0111 nor 4'b1000 is 0, it is considered logically true if it is not 0, so the above code is equivalent to the following code.
That is, the result is that a is logically true, b is logically true, and c is logically false.
5.4.4 Experience Summary
(1) Priority of logical operators
Among the logical operators - "&&" and - "||" have lower priority than arithmetic operators; "!" has higher priority than binocular logical operators。
Examples are as follows:
(2) Both sides of the logical operator correspond to 1-bit signals
Experience :Logical operators on both sides correspond to 1-bit signals
Pay attention to the above code, where a and b are both multi-bit signals, indicating that two multi-bit signals are logically ANDed. The correct understanding of this code is: when a is not equal to 0 and b is not equal to 0, the value of d is 1. However, even engineers with many years of work experience can hardly intuitively understand the meaning implied by the above code. The concept that not equal to 0 means logically true and equal to 0 means logically false is easily overlooked.
Therefore, although there is no error in the above code, the designer should not write the code for the purpose of showing off the technology in the design, and this way of writing is prone to misdesign, for example, it may originally express assign d = a & b, but in the end, the above code was written because the design was not intuitive enough, which led to design problems. Therefore, it is very important to write intuitively and understandably in the design, so that you and others can immediately understand the meaning of the code when they see the code, so it is recommended to write the above code in the following form.
(3) Multi-use parentheses to distinguish priorities
Experience 2 :Don't try to remember precedence, use parentheses more
In fact, engineers do not remember all the prioritization in their work, and remembering all the priorities will not greatly improve the work efficiency of engineers. In the design, you may encounter the following code:
(1) a < b && c > d ;
(2) a = = b | | c = = d ;
(3)! a | | a > b 。
If you don't remember the precedence of operators, when you encounter situations like these three examples, you will definitely spend a certain amount of time to sort out your thoughts and think about which part to judge first. If the engineer can remember the priority, it also needs to communicate and check the work of these codes, and human beings are prone to make mistakes, and these mistakes are often overlooked and difficult to be checked.
Therefore, in order to improve the readability of the program and clearly express the priority relationship between operators, it is recommended to use more brackets in the design. The three examples above can be written as:
1)( a < b) &&( c > d);
2)( a = = b) | |( c = = d);
3)(!a) | |( a > b)。
(4) Use less logical NOT
Experience 3 :Use "logic and" and "logic or" more, and use less logic not
"Logical and" translated into Chinese is "and", and "logical or" translated into Chinese is "or". Suppose there is a signal flag, 0 means idle, 1 means busy. "(!(flag== 0) && a== 4' b1000)", which reads as "take the opposite state when idle, and the condition is true when a is equal to 4' b1000". This is very difficult to read, and when reading this code, you need to turn your head a little bit more and think about it a little bit more. In order to make the code more intuitive, it is suggested that the above example be written as "flag== 1 && a==4' b1000", which reads as "when busy and a is equal to 4' b1000" the condition is true.
5.5 Bitwise Logical Operators
Note: ~ ^, ^ ~ (binary XOR, NOR): (equivalent to NOR operation).
There are the following bitwise operators in Verilog HDL language:
~ (unary NOT): (equivalent to NOT operation)
& (binary AND): (equivalent to AND operation)
| (binary or): (equivalent to OR operation)
^ (binary XOR): (equivalent to XOR operation)
These operators operate bitwise on corresponding bits of the input operands and produce a vector result. Each truth table in the figure below shows the result of bitwise operation for different bitwise logical operators:
5.5.1 Monocular bitwise AND
The unary bitwise AND operator &, after the operator is the signal that needs to be logically operated, which means the AND operation between each bit of the signal. For example
Reg[3:0] A,C;
assign C=&A;
The above code is equivalent to C = A[3] & A[2] & A[1] & A[0]; if A=4'b0110, the result of C is 0.
5.5.2 Monocular bitwise OR
Monocular bitwise OR operator |, after the operator is the signal that needs to be logically operated, indicating that the signal is ORed between bits. For example
reg[3:0] A,C;
assign C=|A;
The above code is equivalent to C = A[3] | A[2] | A[1] | A[0]; if A=4'b0110, the result of C is 1.
5.5.4 Binocular bitwise AND
Binocular bitwise AND operator &, the signal is located on the left and right sides of the operator, which means that the corresponding phase AND operation is performed on the two signals. For example
reg[3:0] A,B,C;
assign C = A & B;
The above code is equivalent to: C[0] = A[0] & B[0], C[1] = A[1] & B[1], C[2] = A[2] & B[2 ], C[3] = A[3] & B[3]. If A=4'b0110, B=4'b1010, the result of C is 4'b0010.
If the operand lengths are not equal, the operand with the smaller length is padded with 0 on the leftmost side. For example,
reg[1:0] A;
reg[2:0] B;
reg[3:0] C;
assign C = A & B;
The above code is equivalent to: C[0] = A[0] & B[0], C[1] = A[1] & B[1], C[2] = 0& B[2], C[ 3] = 0 &0.
5.5.5 Binocular bitwise OR
Binocular bitwise OR operator |, the signal is located on the left and right sides of the operator, indicating that the corresponding phase OR operation is performed on the two signals. For example
reg[3:0] A, B, C;
assign C = A | B;
The above code is equivalent to: C[0] = A[0] | B[0], C[1] = A[1] | B[1], C[2] = A[2] | B[2 ], C[3] = A[3] | B[3]. If A=4'b0110, B=4'b1010, the result of C is 4'b1110.
If the operand lengths are not equal, the operand with the smaller length is padded with 0 on the leftmost side. For example,
reg[1:0] A;
reg[2:0] B;
reg[3:0] C;
assign C = A | B;
The above code is equivalent to: C[0] = A[0] | B[0], C[1] = A[1] | B[1], C[2] = 0 | B[2], C [3] = 0|0.
5.5.6 Binocular bitwise XOR
The binocular bitwise XOR operator ^, the signals are located on the left and right sides of the operator, which means to perform the corresponding phase XOR operation on the two signals. XOR refers to 0 0=0,1 1=0,0^1=1, that is, the same is 0, and the difference is 1. For example
reg[3:0] A, B, C;
assign C = A ^ B;
The above code is equivalent to: C[0] = A[0] ^ B[0], C[1] = A[1] ^ B[1], C[2] = A[2] ^ B[2 ], C[3] = A[3]
^ B[3]. If A=4'b0110, B=4'b1010, the result of C is 4'b1100.
If the operand lengths are not equal, the operand with the smaller length is padded with 0 on the leftmost side. For example,
reg[1:0] A;
reg[2:0] B;
reg[3:0] C;
assign C = A | B;
The above code is equivalent to: C[0] = A[0] ^ B[0], C[1] = A[1] ^ B[1], C[2] = 0 ^ B[2], C [3] = 0^0.
5.5.7 Experience Summary
Difference Between Logical Operators and Bitwise Operators
Logical operators include &&, ||, !, and bitwise operators include &, |, ~ . So what is the difference between logical operators and bitwise operators? Comparing the logical AND "&&" and the bitwise AND "&", it can be seen that the operation of the logical AND operatorThere are only two outcomes: logically true or logically false, that is, 1 or 0; and "&" is a bitwise operator,for two multi-bit wide data operations. For bitwise operators, two numbers are ANDed, ORed, or NOTed bitwise.
The result of the above operation is: a=1'b1, b=1'b1, c=1'b0, d=4'b0000, e=4'b1111, f=4'b1000.
5.6 Relational Operators
The relational operators are: > (greater than), < (less than), >= (not less than), <= (not greater than), == (logically equal), and ! = (logical not equal).
Relational operators evaluate to true (1) or false (0). If one of the operands is x or z, the result is x . Example: 23 > 45 : The result is false ( 0 ). 52 < 8'hxFF: The result is x.
If the operands are of different lengths, the shorter operand is zero-padded in the most significant bit direction (left) . For example: 'b1000 >= 'b01110 is equivalent to: 'b01000 >= 'b01110, which is false (0).
In the comparison of logical equality and inequality, as long as one operand contains x or z, the comparison result is unknown (x). For example, if Data = 'b11x0; Addr = 'b11x0; then Data == Addr, the comparison result is uncertain, that is The result is x.
5.7 Shift operators
There are two shift operators in Verilog HDL, namely "<<" (left shift operator) and ">>" (right shift operator).
The following describes the usage of the two respectively:
5.7.1 Left shift operator
In Verilog HDL, "-"<< represents the left shift operator. Its general expression is:
A << n;
Among them, A represents the operand to be shifted, and n represents the number of bits to be left shifted. The meaning of this expression is to shift the operand A to the left by n bits. The left shift operation is a logical shift, and 0 needs to be used to fill the vacancy shifted out, that is, 0 is filled in the lower bits. To shift left by n bits, it is necessary to fill n 0s.
Since the above code is left-shifted by 2 bits, 2 zeros are filled in the lower bits, so the running result of the above code is: a = 4'b1100.
There are three points worth noting in the left shift operation:
(1) The left shift operation does not consume logic resources, even the AND gate and the NOT gate are not needed, it is just the connection of lines .
The above code shifts the signal b to the left by two bits and assigns it to c. The corresponding hardware circuit is as follows:
(2) The left shift operation needs to store the result according to the bit width
You may have seen the following codes during the learning process: 4'b1001<<1=4'b0010 and 4'b1001<<1=5'b10010
Why is the operand also 4'b1001, which is shifted left by one bit, but the result is 4'b0010 and 5'b10010? This is because after the left shift operation, it depends on how many bits are used to store the result.
In the above code, since a is 4 bits, only 4 bits can be saved, so b is shifted left by 1 bit and assigned to 4 bit a, and the result after filling the shifted bits with 0 is a = 4'b0010;
In the above code, since a is 5 bits, it can store 5 bits of results, so b is shifted left by 1 bit and assigned to 5 bit a, and the result is a = 5'b10010 after filling the shifted bits with 0; ( 3**)
left The operand of the shift operation can be a constant or a signal**. Similarly, the shift number, constant for the left shift operation can also be a signal.
In the above code, cnt is incremented by 1 every clock, and since it is 3 bits, the value is 0~2. a is 4'b1 shifted left by cnt bits. When cnt is equal to 0, shift left by 0 bit, a is equal to 4'b1; when cnt is equal to 1, shift left by 1 bit, a is equal to 4'b10. By analogy, each clock change of a is as follows:
It should be noted that when the shift number is a signal, the integrated circuit is not a simple connection, and the selector shown in the figure below may be integrated . However, even so, the resources consumed by this hardware circuit are still relatively small.
5.7.2 Right shift operator
In Verilog HDL, use ">>" to represent the right shift operator. Its general expression is:
A >>n;
Among them, A represents the operand to be shifted, and n represents the number of bits to be shifted right. The meaning of this code is to shift the operand A to the right by n bits.
There are three points worth noting in the right shift operation:
(1) The right shift operation is a logical shift, and 0 is needed to fill the vacancy shifted out, that is, to fill the high bits with 0, and how many 0s to fill
depends on width of the signal that saves the result .
4'b0111 The result of shifting right by two bits is 2'b01. Since a has 6 bits, assigning 2 bits to 6 bits needs to add 0 to the high bit, so 4 0s need to be added. So the result of running the above code is:
a = 6'b0001
(2) Similar to the left shift operation, the right shift operation does not consume logic resources, even the AND gate and the NOT gate are not needed, it is just the
connection of lines .
The above code shifts the signal b to the left by two bits and assigns it to a. The corresponding hardware circuit is shown in the figure below.
(3) The operand of the left shift operation can be a constant or a signal. Likewise, the shift number for a right shift operation can be either a constant or a signal .
In the above code, cnt is incremented by 1 every clock, and since it is 3 bits, the value is 0~2. a is 4'b1000 shifted right by cnt bits. When cnt is equal to 0, shift right by 0 bit, a is equal to 4'b1000; when cnt is equal to 1, shift right by 1 bit, a is equal to 4'b0100. By analogy, the change of each clock of a is shown in the figure below.
Similar to the left shift operation, in the right shift operation, if the shift number is a signal, the integrated circuit is not a simple connection, but may synthesize a selector as shown in the figure below. However, also in this case, the resources consumed by such hardware circuits are still relatively small.
5.7.3 Experience Summary
Multiply by left shift
In FPGA, multiplication operation should be avoided as much as possible, because this kind of calculation needs to occupy large hardware resources, and the operation speed is relatively slow. When you have to use multiplication, try to multiply by 2 to the Nth power , so that the multiplication operation can be realized by using the left shift operation in the design, thereby greatly reducing hardware resources.
When the multiplier is a constant of 2 to the Nth power, the multiplication can be realized by shift operation. For example: a 2 is equivalent to a<<1; a 4 is equivalent to a<<2; a*8 is equivalent to a<<3, and so on. Even if the multiplier is not a constant of 2 to the Nth power, the implementation can be simplified by a shift operation. For example:
Both b and c in the above code can realize a*127, but the first line consumes one multiplication, while the second line only uses one subtractor
.
In the above code, both b and c can realize a*67, but the first line consumes one multiplication, while the second line only uses two adders, thus saving resources.
It can be noticed that the multipliers in the above two examples are all constants, so does this kind of multiplication also take time and effort to consider optimization during design? In fact, it is unnecessary, because the comprehensive tools are very powerful now. When the tool finds that the multiplier is a constant, it will automatically optimize according to the above process. That is to say, multiplying by a constant does not consume multiplier resources in essence, so you can use it with confidence.
But when the multiplier is not a constant, it is necessary to pay attention to the use of multiplication. Try to convert the signal into a form related to 2 to the Nth power. For example, when the data needs to be expanded and calculated later, do not expand the data by 100 times according to the usual thinking, but directly expand it by 128 times (It is not possible to expand decimals by 100 or 1000 times according to conventional thinking, and to amplify according to the nth power of 2, which reduces the occupancy of resource space)。
Use right shift to implement division operation
In FPGA design, it is necessary to avoid division as much as possible, and it is even strictly forbidden to use "/" for division calculation. This is because the divider will occupy a huge amount of resources, which is more than that of the multiplier, and in many cases the result cannot be obtained within one clock cycle. And when you have to use division, you should try to convert the division into the form of dividing by 2 to the Nth power, so that you canUse the right shift operationTo realize the division operation, thereby greatly reducing hardware resources .
When the divisor is a constant of 2 to the Nth power, division can be realized by shift operation. For example: a/2 is equivalent to a>>1; a/4 is equivalent to a>>2; a/8 is equivalent to a>>3, and so on.
Different from the left shift, when the divisor is not a constant of 2 to the Nth power, the implementation cannot be simplified simply by a shift operation. In summary, division should be avoided as much as possible in FPGA design.
One-hot encoding using left shift
One-hot code, also called one-hot code, is a code system in which only one bit is 1 and the others are all 0 . For example 8'b00010000, 8'b1000000 etc.
The one-hot code is very useful in design, it can be used to represent the state of the state machine to make the state machine more robust, and it can also be used in a multi-choice circuit to represent choosing one of them.
Using the left shift operation, one-hot codes can be easily generated, for example, 4'b0010 can be generated, which can be 4'b1 << 1. Similarly, a code system in which one bit is 0 and the others are 1 can also be generated. For example to generate 4'b1011, it can be ~(4'b1 <<2). Other desired numeric results can also be produced using left shifts:
For example, to generate 5'b00111, it could be (5'b1<<3)-1.
For example, to produce 5'b11100, could be ~((5'b1<<2)-1).
5.8 Conditional Operators
5.8.1 Ternary operator
**Conditional operator (?: )** in Verilog HDL syntax has three operands (that is, ternary operator), and its format is generally expressed as:
Its meaning is: when the "conditional expression" is true (i.e. logic 1), execute the "true expression"; when the "conditional expression" is false (i.e.
logic 0), execute the "false expression". That is, when condition_expr is true (that is, the value is 1), select true_expr; if condition_expr
is false (the value is 0), select false_expr. If condition_expr is x or z, the result will be the value of the bitwise operation of true_expr and false_expr according to the following logic: 0 and 0 yield 0, 1 and 1 yield 1, and x otherwise.
Application examples are as follows:
In the above expression, if s is true, assign t to r; if s is false, assign u to r.
The corresponding hardware circuit diagram is shown below.
The use of conditional operators has the following points to note :
(1) The function of the conditional expression is actually similar to a multiplexer , as shown in Figure 1.3-8. Also, it can be replaced with an if-else statement.
(2) Conditional operators can be used for conditional assignment in data flow modeling, and in this case conditional expressions act as control switches . For example:
Among them, if the expression Marks > 18 is true, then Grade_A is assigned the value of student; if Marks > 18 is false, then
Grade_C is assigned the value of student.
The corresponding hardware circuit diagram is shown below.
(3) Conditional operators can also be nested, and each "true expression" and "false expression" can itself be a conditional
expression . For example:
The meaning of the above code is: if the expression M == 1 is true, then judge whether CTL is true, if CTL is true, assign A to OUT, if it is false, assign B to OUT; if M = = 1 is false, then judge whether CLT is true, if CLT is true, assign C to OUT, if false, assign D to OUT.
The corresponding hardware circuit diagram is as follows:
5.8.2 if statement
The syntax of the "if" statement is as follows:
if(condition_1)
procedural_statement_1;
{else if(condition_2)
procedural_statement_2};
{else
procedural_statement_3};
Its meaning is: if condition_1 is satisfied, regardless of whether the rest of the conditions are satisfied, procedural_statement_1 will be executed, and neither procedural_statement_2 nor procedural_statement_3 will be executed.
If condition_1 is not satisfied and condition_2 is satisfied, then procedural_statement_2 is executed, and neither procedural_statement_1 nor procedural_statement_3 is executed.
If condition_1 is not satisfied and condition_2 is not satisfied, procedural_statement_3 is executed, and neither procedural_statement_1 nor procedural_statement_2 is executed.
Let's illustrate with an example:
if(Sum < 60) begin
Grade = C;
Total_C = Total _C + 1;
end
else if(Sum < 75) begin
Grade = B;
Total_B = Total_B + 1;
end
else begin
Grade = A;
Total_A = Total_A + 1;
end
Note that conditional expressions must always be enclosed in parentheses, and may be ambiguous if the if - if - else format is used,
as shown in the following example:
if(Clk)
if(Reset)
Q = 0;
else
Q = D;
There is a question here: Which if statement does the last else belong to? Does it belong to the condition of the first if (Clk) or the condition of the second if (Reset)? This is done in Verilog HDL by combining the else with the nearest none Else's if statement is associated to
resolve. In this example, the else is associated with the inner if statement.
Here's another example of an if statement:
if(Sum < 100)
Sum = Sum + 10;
if(Nickel_In)
Deposit = 5;
Elseif (Dime_In)
Deposit = 10;
else if(Quarter_In)
Deposit = 25;
else
Deposit = ERROR;
suggestion:
1. Conditional expressions need to be enclosed in parentheses .
2. If it is an if - if statement, please use the block statement begin — end , as shown below.
if (Clk) begin
if(Reset)
Q = 0;
else
Q = D;
end
The above two suggestions are to make the code clearer and prevent errors.
5.8.3 case statement
The case statement is a multi-way conditional branch form , and its syntax is as follows:
case(case_expr)
case_item_expr{case_item_expr} :procedural_statement
. . . . . .
[default:procedural_statement]
endcase
Under the case statement, the conditional expression case_expr is first evaluated, and then each branch item is evaluated and compared in turn, and the statement in the first branch that matches the value of the conditional expression is executed . Multiple branch items can be defined in 1 branch, and these values do not need to be mutually exclusive. The default branch overrides all other branches not covered by a branch expression.
Writing suggestions: The default item of the case statement must be written to prevent the generation of latches.
5.8.4 Select statement
There is a commonly used selection statement in Verilog syntax, and its syntax is:
vect[a +: b]或 vect [a -: b];
vect is the variable name, a is the starting position,Plus or minus signs represent ascending or descending order, b represents the width for ascending or descending order .
vect[a +: b] is equivalent to vect[a : a+b-1] , the interval of vect starts from a and counts b times in the direction greater than a; vect[a -: b] is equivalent to vect[a : a -b+1] , the interval of vect is counted b times from a to the direction where a is smaller. a can be a constant or a variable number, but b must be a constant.
Example 1 : vect[7 +: 3]; where, the starting position is 7, + represents ascending order, and the width is 3. That is, count 3 numbers from 7 to the direction greater than 7. Its equivalent form is: vect[7 +: 3]== vect[7 : 9].
Example 2 : vect[9 -: 4]; among them, the starting position is 9, - represents the descending order, and the width is 4. That is, count 4 numbers from 9 to the direction smaller than 9. Its equivalent form is: vect[9 -: 4]== vect[9 : 6].
The most common form of this syntax in actual use is to use a as a variable number. For example, you need to design
code with the following functions:
When cnt==0, assign din[7:0] to data[15:8]; when cnt==1, assign din[7:0] to data[7:0].
When designing, it can be written as: data[15-8 cnt -: 8] <= din[7:0] (At this time, 15-8 cnt needs to be
regarded as a whole, which will happen with the change of cnt changes), so that the streamlining of the code is completed.
The hardware circuit structure of the selection statement is shown in the figure below, which is essentially a selector. When cnt==0, select the latch of data[15:8], assign din[7:0] to data[15:8], and the latch of data[7:0] keeps the output unchanged ; When cnt==1, select the latch of data[7:0], assign din[7:0] to data[7:0], and the latch of data[15:8] keeps the output Change.
Experience conclusion: In actual projects, the form of selection statement vect[a +: b] or vect [a -: b] can be used for code writing, which will help to simplify the design code.
5.8.5 Experience Summary
The if statement and the case statement are two very important statements in Verilog. If and case statements have certain correlations and
differences. The same thing is that the two can achieve almost the same function. The following mainly introduces the differences between the two.
There is a priority between each branch of the if statement, and the synthesized circuit is similar to a cascade structure. Each branch of the case statement is equal, and the synthesized circuit is a multiplexer . Therefore, the delay of the logic circuit obtained by combining multiple if else-if statements may be slightly larger than that of the case statement . For beginners, they often like to use the if else-if statement in the process of learning Veriolg at the beginning, because this syntax is more straightforward to express. But in projects where the running speed is more critical, the effect of using the case statement will be better. Let's compare it through a specific case, using if statement and case statement to describe the comprehensive result of the same functional circuit.
First is the code written with the if statement:
Its synthesized RTL view is shown below.
As can be seen from the RTL diagram shown in the above figure, this circuit contains two two-to-one multiplexers, and the priority of the right side is higher than that of the left (because the value of q is directly related to the two-to-one selection on the right device connection), when en[0] is not 1, it will continue to judge en[1]. That is, inThe circuit synthesized under the if statement has priority。
Next, analyze the code described using the case statement.
Its synthesized RTL view is as follows:
As can be seen,The circuit synthesized by the logic code written by the case statement is parallel, has no priority, and does not affect the running speed of the circuit。
Although in the RTL view, there is a big difference between the circuits synthesized by the two statements, but because the current development tools are smart enough, the circuit will be automatically optimized during layout and routing, and it will eventually be mapped to the circuit inside the FPGA. There is basically no difference between them .
Finally, summarize the difference and connection between if statement and case statement:
The If statement has priority, and the part after the else is executed only when the condition under the if is not met. The case statement is parallel and has no priority, which can be clearly observed in the RTL view synthesized by the two. However, since the current simulation and synthesis tools are powerful enough, the final synthesis results of if...else... and case... statements are actually not different, they are just two different implementation methods, so there is basically no need to consider the difference between the two difference. Under the premise of not affecting the function, the designer does not need to do local optimization work, for example, there is no need to consider the difference in resource consumption of if/case statements, and no need to consider optimizing circuits. Only under the premise of affecting the function (that is, reporting an error due to sequence constraints), optimize the circuit according to the prompts.
5.9 Concatenation Operators
The concatenation operation is an operation that combines small expressions to form a large expression, and its form is as follows:
{expr1, expr2, . . ., exprN} ;
The splicing character does not consume any hardware resources, it just changes the combination of lines , you can refer to the following examples:
Concatenation of non-fixed-length constants is not allowed because the length of non-fixed-length constants is unknown. Therefore, the code shown below is not grammatical. {Dbus,5}; //concatenation operations on non-fixed-length constants are not allowed。