After we have studied the fpga design of the 7-segment nixie tube, we have mastered the display principle of the nixie tube, the realization of the fpga, and the display of the system data on the nixie tube, etc. After the completion of the digital tube project, we can form a complete IP core, which can be provided to the future fpga design, or an IP in the soc design. Instead of rewriting the relevant code every time you get a project. Even if the hardware may be different, it becomes easier to change the IP of the nixie tube. A medium-sized or large-scale project is usually built through some simple IP modules. When we have more IP, it is very helpful for the design of a medium-sized or large-scale project.
1. System hierarchy in the project:
figure 1
In a project, first, we first implement a functional IP core, and after using and testing this FPGA IP, we can encapsulate this functional IP. The principles of encapsulation are:
- It has strong applicability and can be easily used repeatedly in current projects or other FPGA projects.
- It is convenient for instantiation. In some projects, many IPs with the same function will be instantiated continuously. Example: Some projects will have 8 or 16 uart modules.
- With rich interface parameters, it is convenient for the same IP to adapt to different configuration modes, instead of generating its own IP for each function.
- Additional options for different models of FPGA chips.
In many FPGA projects: We usually have a hardware platform (hardware development board), the hardware platform is composed of FPGA chip, cpu chip, clock, power chip and so on. In fpga design,
- Sometimes it is necessary to use fpga to control the operation of other chips in the hardware platform, or to communicate with other hardware platforms. (Ethernet communication, uart communication, iic communication, etc.)
- The fpga itself will complete the corresponding algorithm, (encryption, decryption, image compression, dsp algorithm)
- Data exchange with the upper cpu.
In some medium-sized or large-scale projects, under the hardware platform, fpga and cpu work together to complete certain specific functions. Therefore, it is often divided into the following types of modules
- FPGA Function IP
- FPGA registers
- CPU bus interface
- CPU software driver
- CPU software application
FPGA functional IP: Complete specific functional modules and behaviors.
FPGA register: provide parameter configuration of IP module interface, read the relevant content of IP module
CPU bus interface: link FPGA register and cpu bus, provide read signal, write signal, address, data read, data write, etc.
CPU software driver: The software driver is used to configure the FPGA registers, and through the operation of the registers, the software can read and write the FPGA IP.
CPU software application: call the software driver to realize the upper-layer application function.
2. Packaging of FPGA IP:
Here we take the 7-segment digital tube as an example to realize the design process from IP core to software application.
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: Fraser Innavotion Inc // Engineer: William Gou // // Create Date: 09/25/2018 09:28:18 AM // Design Name: // Module Name: SEG_core // Project Name: RISC-V SOC RTL // Target Devices: FII-PE7030, FII-PRX100T, FII-BM7100, FII-PRA040, FII-PRA006 // Tool Versions: vivado 18.1/18.2 // Description: // // Dependencies: // this is a simple version of risc-v integer version,aim to show up basic concept of RTL // Revision:1.0 // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module SEG_core # ( parameter MS_CNT = 50_000 ) ( input clk, input [7:0] seg_in_a, input [7:0] seg_in_b, input [7:0] seg_in_c, input [7:0] seg_in_d, input [7:0] seg_in_e, input [7:0] seg_in_f, input [5:0] dp_in, output [6:0] SEG, output DP, output [5:0] SEAT, input rst ); reg [31:0] ms_cnt = 0; always@(posedge clk or posedge rst ) if(rst) ms_cnt <= 0; else begin if(ms_cnt == MS_CNT - 1) ms_cnt <= 0; else ms_cnt <= ms_cnt + 1; end wire ms_p = (ms_cnt == MS_CNT - 1) ? 1'b1 : 1'b0; reg [7:0] count_sel; reg seg_dp = 1; reg [5:0] seg_seat = 0; always@(posedge clk or posedge rst ) if(rst) begin seg_seat <= 0; count_sel <= " "; seg_dp <= 1'b1; end else begin if(ms_p) begin case (seg_seat) 6'b10_0000: begin seg_seat <= 6'b00_0001; count_sel <= seg_in_f; seg_dp <= dp_in[0]; end 6'b00_0001: begin seg_seat <= 6'b00_0010; count_sel <= seg_in_e; seg_dp <= dp_in[1]; end 6'b00_0010: begin seg_seat <= 6'b00_0100; count_sel <= seg_in_d; seg_dp <= dp_in[2]; end 6'b00_0100: begin seg_seat <= 6'b00_1000; count_sel <= seg_in_c; seg_dp <= dp_in[3]; end 6'b00_1000: begin seg_seat <= 6'b01_0000; count_sel <= seg_in_b; seg_dp <= dp_in[4]; end 6'b01_0000: begin seg_seat <= 6'b10_0000; count_sel <= seg_in_a; seg_dp <= dp_in[5]; end default: seg_seat <= 6'b10_0000; endcase end end // seg_seg = {a,b,c,d,e,f,g,dp} /* --- a --- * | | * f b * | | * --- g --- * | | * e c * | | * --- d --- dp * */ reg [6:0] seg_seg = 7'h00; always@(*) case(count_sel) "F", "f" : seg_seg = 7'b1000_111; "r" : seg_seg = 7'b0000_101; "A", "a" : seg_seg = 7'b1110_111; "S" : seg_seg = 7'b1011_011; "E", "e" : seg_seg = 7'b1001_111; "i" : seg_seg = 7'b0000_100; "I" : seg_seg = 7'b0000_110; "H" : seg_seg = 7'b0110_111; "h" : seg_seg = 7'b0010_111; "U" : seg_seg = 7'b0111_110; "u" : seg_seg = 7'b0011_100; "b", "B" : seg_seg = 7'b0011_111; "d", "D" : seg_seg = 7'b0111_101; "o" : seg_seg = 7'b0011_101; "P", "p" : seg_seg = 7'b1100_111; "L" : seg_seg = 7'b0001_110; "n" : seg_seg = 7'b0010_101; "N" : seg_seg = 7'b1110_110; "C" : seg_seg = 7'b1001_110; "c" : seg_seg = 7'b0001_101; "g" : seg_seg = 7'b1111_011; "j", "J" : seg_seg = 7'b0111_000; " " : seg_seg = 7'b0000_000; "_" : seg_seg = 7'b0001_000; "-" : seg_seg = 7'b0000_001; "0" : seg_seg = 7'b1111_110; "1" : seg_seg = 7'b0110_000; "2" : seg_seg = 7'b1101_101; "3" : seg_seg = 7'b1111_001; "4" : seg_seg = 7'b0110_011; "5" : seg_seg = 7'b1011_011; "6" : seg_seg = 7'b1011_111; "7" : seg_seg = 7'b1110_000; "8" : seg_seg = 7'b1111_111; "9" : seg_seg = 7'b1110_011; default: seg_seg = 7'h00; endcase assign SEG = ~{seg_seg[0], seg_seg[1], seg_seg[2], seg_seg[3], seg_seg[4], seg_seg[5], seg_seg[6] }; assign DP = ~seg_dp; assign SEAT = ~seg_seat; endmodule
module SEG_core # ( parameter MS_CNT = 50_000 ) ( input clk, input[7:0] seg_in_a, input[7:0] seg_in_b, input[7:0] seg_in_c, input[7:0] seg_in_d, input[7:0] seg_in_e, input[7:0] seg_in_f, input [5:0] dp_in, output [6:0] SEG, output DP, output [5:0] SEAT, input rst );
MS_CNT: Used to set the 1ms clock, the user can set the 1ms clock through MS_CNT according to different clocks.
seg_in_x : for user data input interface
dp_in : the point used by the user to input the 7-segment digital tube
SEG: 7-segment digital tube output
DP: digital tube point output
SEAT: nixie tube position pin output
3. Register interface design:
According to different applications, different register interfaces can be designed.
register address | register description | Register initial value | Literacy |
16’h0000 | Module Version Register | 32’h0000_0001 | read only |
16’h0004 | Module Control Register | 32’h0000_0000 | read and write |
16’h001c | data 1 register | 32’h0000_0000 | read and write |
16’h0020 | data 2 register | 32’h0000_0000 | read and write |
wire [ 7: 0 ] w_addr = i_addr[ 9: 2 ]; //=============================================================================== wire [ 7: 0 ] SEG_VER = 8'h01; reg [31: 0 ] SEG_CTRL = 0; reg [31: 0 ] SEG_DATA0 = 0; reg [31: 0 ] SEG_DATA1 = 0; always @( posedge clk ) if(!rst_n) SEG_CTRL <= 0; else begin SEG_CTRL[30] <= 1'b0; // update bit if(w_addr == 1) // register address begin if( we_i[0]) SEG_CTRL[07:00] <= i_PERI_din[07:00]; if( we_i[1]) SEG_CTRL[15:08] <= i_PERI_din[15:08]; if( we_i[2]) SEG_CTRL[23:16] <= i_PERI_din[23:16]; if( we_i[3]) SEG_CTRL[31:24] <= i_PERI_din[31:24]; end end always @( posedge clk ) if(!rst_n) SEG_DATA0 <= 0; else if(w_addr == 7) // register address begin if( we_i[0]) SEG_DATA0[07:00] <= i_PERI_din[07:00]; if( we_i[1]) SEG_DATA0[15:08] <= i_PERI_din[15:08]; if( we_i[2]) SEG_DATA0[23:16] <= i_PERI_din[23:16]; if( we_i[3]) SEG_DATA0[31:24] <= i_PERI_din[31:24]; end always @( posedge clk ) if(!rst_n) SEG_DATA1 <= 0; else if(w_addr == 8) // register address begin if( we_i[0]) SEG_DATA1[07:00] <= i_PERI_din[07:00]; if( we_i[1]) SEG_DATA1[15:08] <= i_PERI_din[15:08]; if( we_i[2]) SEG_DATA1[23:16] <= i_PERI_din[23:16]; if( we_i[3]) SEG_DATA1[31:24] <= i_PERI_din[31:24]; end //=============================================================================== reg [7:0] SEG_VAL [5:0]; reg [5:0] SEG_DP = 0; initial begin SEG_VAL[0] = " "; SEG_VAL[1] = " "; SEG_VAL[2] = " "; SEG_VAL[3] = " "; SEG_VAL[4] = " "; SEG_VAL[5] = " "; SEG_DP = 0; end always @ (posedge clk) if(SEG_CTRL[30]) begin SEG_VAL[0] <= SEG_DATA0[07:00]; SEG_VAL[1] <= SEG_DATA0[15:08]; SEG_VAL[2] <= SEG_DATA0[23:16]; SEG_VAL[3] <= SEG_DATA0[31:24]; SEG_VAL[4] <= SEG_DATA1[07:00]; SEG_VAL[5] <= SEG_DATA1[15:08]; SEG_DP <= SEG_CTRL[5:0]; end //======= ======================bus read======================================= always @ (*) begin case (w_addr[7:0]) 8'h00: o_PERI_dout = {24'h00_0000, SEG_VER}; 8'h01: o_PERI_dout = SEG_CTRL; 8'h02: o_PERI_dout = SEG_STAT; 8'h07: o_PERI_dout = SEG_DATA0; 8'h08: o_PERI_dout = SEG_DATA1; default: o_PERI_dout = 0; endcase end
Module version register (00h): read by cpu to display the current module version
Module Control Register (04h): Module Control Register,
bit[30] , 1 => update 7-segment digital tube value, when the cpu writes, the fpga side is automatically cleared
bit[5:0] indicates the position of the decimal point of the 7-segment digital tube.
Data 1 register (1ch): 7-segment digital tube value
Data 2 register (20h): 7-segment digital tube value
4. CPU interface design:
module RIB_fii_SEG #( parameter [ 31: 0 ] DEV_BASEADDR = 32'hC000_1000, parameter PERI_DEEP = 4, //memory data width parameter PERI_W = 32, //memory data width parameter PERI_MSK_W = 4, //memory data mask width parameter PERI_ADDR_W = 32 //memory address width ) ( input clk, input [31:0] i_rib_saddr, input [31:0] i_rib_sdin, output [31:0] o_rib_sdout, input i_rib_svalid, output [1:0] o_rib_sready, input [3:0] i_rib_swe, input i_rib_srd, input i_rib_sop, output [6:0] SEG, output DP, output [5:0] SEAT, input rst_n ); wire [ PERI_W - 1 : 0 ] i_PERI_din = i_rib_sdin; reg [ PERI_W - 1 : 0 ] o_PERI_dout; assign o_rib_sdout = o_PERI_dout << {i_rib_saddr[1:0],3'b000}; wire [ PERI_ADDR_W - 1 : 0 ] i_addr = i_rib_saddr; wire cs_i = ( i_rib_saddr[31:12] == DEV_BASEADDR[31:12] ) ? 1'b1 : 1'b0; //=============================================================================== wire [ PERI_MSK_W - 1: 0 ] we_i = ( { PERI_MSK_W{ cs_i } } & i_rib_swe ); wire rd_i = cs_i & i_rib_srd; wire [ 7: 0 ] w_addr = i_addr[ 9: 2 ]; assign o_rib_sready = {1'b0, i_rib_svalid & cs_i}; //=============================================================================== endmodule
RISCV RIB bus:
input [31:0] i_rib_saddr, input [31:0] i_rib_sdin, output[31:0] o_rib_sdout, input i_rib_svalid, output[1:0] o_rib_sready, input [3:0] i_rib_swe, input i_rib_srd, input i_rib_sop,
RISCV RIB bus translates to address, read, write, etc. signals:
wire [PERI_W - 1 : 0 ] i_PERI_din = i_rib_sdin; reg[PERI_W-1:0] o_PERI_dout ; assign o_rib_sdout = o_PERI_dout << {i_rib_saddr[1:0],3'b000}; wire [ PERI_ADDR_W - 1 : 0 ] i_addr = i_rib_saddr; wire cs_i = ( i_rib_saddr[31:12] == DEV_BASEADDR[31:12] ) ? 1'b1 : 1'b0; //==================================================== ================================= wire [ PERI_MSK_W - 1: 0 ] we_i = ( { PERI_MSK_W{ cs_i } } & i_rib_swe ); wire rd_i = cs_i & i_rib_srd; wire[ 7: 0 ] w_addr = i_addr[ 9: 2 ]; assign o_rib_sready = {1'b0, i_rib_svalid & cs_i}; //==================================================== =================================
After some simple logic conversion, the conversion from bus to read and write logic is realized.
5. Software driver:
#define SEG_REG(offset) (*(volatile unsigned int *)(0xC0001000 + offset)) SEG_REG(0x1c) = 0x01234567; SEG_REG(0x20) = 0x89abcdef; SEG_REG(0x04) = 0xc0000008;
The driver first writes the value of the 7-segment digital tube into the register, and finally writes the control register. The control register includes the position of the decimal point and bit [30] update information.
6. Software application:
/*Entry Point for Machine Timer Interrupt Handler*/ void handle_m_time_interrupt(){ clear_csr(mie, MIP_MTIP); // clear MIP_MTIP bit // Reset the timer for 3s in the future. // This also clears the existing timer interrupt. set_timer(1); // reset the timer for 250ms // read the current value of the LEDS and invert them. if((time_cnt % 500 ) == 0) // 1s { u8_t led_reg = 0; led_reg = (time_cnt >> 2) & 0x01; led_reg = led_reg << 6; led_reg |= (ETH_REG(ETH_STAT) & 0xff); GPIO_REG(LED_VAL) = ~(led_reg); // printf("time_cnt = 0x%08x \r\n", time_cnt); } #ifdef SEG_IP if((time_cnt % 0x400 ) == 0) // 1s { if(time_cnt & 0x800) { SEG_REG(0x1c) = *((u32_t *)(&seg_ip[0])); SEG_REG(0x20) = *((u32_t *)(&seg_ip[4])); } else { SEG_REG(0x1c) = *((u32_t *)(&seg_ip[6])); SEG_REG(0x20) = *((u32_t *)(&seg_ip[10])); } SEG_REG(0x04) = 0xc0000008; } #endif time_cnt ++; // Re-enable the timer interrupt. set_csr(mie, MIP_MTIP); // set MIP_MTIP bit }
The upper application handle_m_time_interrupt() calls the lower driver.
At this point, the underlying Verilog IP development and the calling process for the upper-level software application have been implemented.