# mini-mcu **Repository Path**: yuan_hp/mini-mcu ## Basic Information - **Project Name**: mini-mcu - **Description**: 一个没啥用,但可以玩的mini微控制器,FPFA开发 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 2 - **Created**: 2020-11-27 - **Last Updated**: 2023-12-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- typora-root-url: ./ typora-copy-images-to: images --- # mini-mcu简介 为了快速在FPGA上开发一些应用,且占用资源较少,所以设计和xilinx退出的KSPMS6指令基本一样的软核,并且实现指令可裁剪。项目仿真使用的是`iverilog和gtkwave`,指令设计和下面一样,这样自己就不用开发编译软件或者脚本!个人计划是设计这个`mini-mcu`,解决fpga上一些低速传感器的数据采集和简单的外设控制的,为了方便移植,设计的所有结构都是使用逻辑资源做的。 我使用bash写了自动根据编写的代码裁剪`mini-mcu`,也就是说,最终`mini-mcu`只会生成你是用过的命令的对应电路,这样做可以减少逻辑资源的开销,自己这次并不打算为其设计外部的外设和数据总线,只是用其做简单的模块,以加快项目开发。 # 计划指令集 | 指 令 | 描述 | 功能 | 标志位 | 备注 | | -------------------- | ------------------------------------------------------------ | ---------------------------------------- | ---------------------- | ------------------------------- | | ADD sX, kk | 寄存器sX加立即数 kk | sX = sX + kk | 影响进位C和结果标志位Z | kk:一个字节 | | ADD sX, sY | 寄存器sX和sY相加 | sX=sX + sY | 影响进位C和结果标志位Z | X,Y为0-F,表示16个寄存器 | | ADDCY sX, kk | 寄存器sX加立即数 kk和进位位 | sX = sX + kk + C | 影响进位C和结果标志位Z | 进位标志位:C | | ADDCY sX, xY | 寄存器sX,sY,进位相加 | sX=sX + sY +C | 影响进位C和结果标志位Z | | | AND sX, kk | sX与立即数kk逻辑与 | sX = sX & kk | C=0,影响结果标志Z | kk:一个字节常数 | | AND sX, sY | sX与sY逻辑与 | sX = sX & sY | C=0,影响结果标志Z | X,Y为0-F,表示16个寄存器 | | CALL aaa | 无条件调用地址为aaa的子程序 | TOS=PC, PC=aaa | 不影响当前标志位 | aaa为000-3FF,10位地址 | | CALL C, aaa | 如果进位标志C=1,调用地址为aaa的子程序 | 如果C==1,{TOS=PC, PC=aaa} | 不影响当前标志位 | aaa为000-3FF,10位地址 | | CALL NC, aaa | 如果进位标志C=0,调用地址为aaa的子程序 | 如果C==0,{TOS=PC, PC=aaa} | 不影响当前标志位 | aaa为000-3FF,10位地址 | | CALL NZ, aaa | 如果结果标志Z=0,调用地址为aaa的子程序 | 如果Z==0,{TOS=PC, PC=aaa} | 不影响当前标志位 | aaa为000-3FF,10位地址 | | CALL Z, aaa | 如果结果标志Z=1,调用地址为aaa的子程序 | 如果Z==1,{TOS=PC, PC=aaa} | 不影响当前标志位 | aaa为000-3FF,10位地址 | | COMPARE sX, kk | sX与立即数kk比较 | 如果sX==kk,ZERO=1,如果sX 计数次数 LOAD s1, 27 LOAD s0, c0 jump software_delay software_delay: LOAD s0, s0 ;pad loop to make it 10 clock cycles (5 instructions), if clk 12MHz --> 1/1.2 us SUB s0, 01 SUBCY s1, 00 SUBCY s2, 00 JUMP NZ, software_delay RETURN ``` 然后将终端路径切换到项目的根目录下。 ## step2: 编译并自动裁剪mcu 到根目录下,终端中执行`./run -c && ./run`命令,将会将汇编代码便以为十六进制的数据文件,这些十六进制数据就是机器执行指令,会被放到ROM中,但是这都不用担心,我使用bash shell编写了脚本可以自动生成,并自动裁剪。 执行命令后如果不出错,会在根目录下生成tmp文件夹,或者清空原来的重新生成文件,hex文件使我们需要的,脚本会将hex文件中内容直接写入到根目录下的`rom.v`文件,并分析文件,生成用于裁剪`mini-mcu`的宏定义文件,为`head.v`,如生成的`head.v`文件内容如下: ```verilog `define _LOAD_REG2REG_ `define _LOAD_DAT2REG_ `define _AND_KK_ `define _XOR_KK_ `define _INPUT_PP_ `define _SUB_KK_ `define _SUB_CY_KK_ `define _COMPARE_KK_ `define _CALL_ `define _JUMP_AAA_ `define _RETURN_ `define _OUTPUT_PP_ `define _CALL_Z_ `define _JUMP_Z_ `define _JUMP_NZ_ ``` `mcu.v`文件会根据这些宏定义选择性的被条件编译,从而实现裁剪`mini-mcu`,之后为完善指令,这些脚本同样可用,但是需要注意,添加指令的位置为: ```verilog //---------------------- 定义一级指令编码 -------------------- // -------- SCRIPT_BEGIN --> 脚本识别标签,勿动 localparam //LOAD LOAD_REG2REG = 6'H00, //LOAD sX, sY 装载数据 LOAD_DAT2REG = 6'H01, //LOAD sX, kk 装载立即数到寄存器 //JUMP JUMP_AAA = 6'H22, //JUMP aaa 跳转到aaa地址执行 JUMP_Z = 6'H32, //JUMP Z,aaa JUMP_NZ = 6'H36, //JUMP NZ,aaa JUMP_C = 6'H3A, //JUMP C,aaa JUMP_NC = 6'H3E, //JUMP NC,aaa //INPUT INPUT_REG = 6'H08, //INPUT sX, (sY) INPUT_PP = 6'H09, //INPUT sX, pp //OUTPUT OUTPUT_REG = 6'H2C, //OUTPUT sX,(sY) OUTPUT_PP = 6'H2D, //OUTPUT sX,pp //SHIFT SHIFT = 6'H14, //移位操作 // ADD ADD_REG = 6'H10, // ADD sX,sY ADD_KK = 6'H11, // ADD sX,kk ADD_CY_REG = 6'H12, // ADDCY sX,sX ADD_CY_KK = 6'H13, // ADDCY sX,kk // SUB SUB_REG = 6'H18, // SUB sX,sY SUB_KK = 6'H19, // SUB sX,kk SUB_CY_REG = 6'H1A, // SUBCY sX,sY SUB_CY_KK = 6'H1B, // SUBCY sX,kk // RETURN RETURN = 6'H25, // RETURN 无条件返回 RETURN_Z = 6'H31, // RETURN Z Z==0 则返回 RETURN_NZ = 6'H35, // RETURN NZ Z==1 则返回 RETURN_C = 6'H39, // RETURN C C==0 则返回 RETURN_NC = 6'H3D, // RETURN NC C==1 则返回 //CALL CALL = 6'H20, // CALL aaa CALL_Z = 6'H30, // CALL Z, aaa CALL_NZ = 6'H34, // CALL NZ, aaa CALL_C = 6'H38, // CALL C, aaa CALL_NC = 6'H3C, // CALL NC, aaa // COMPARE COMPARE_REG = 6'H1C, // COMPARE sX,sY if(sX < kk or sX < sY) Z=X,C=1 ; if(sX > kk or sX > sY) Z=X,C=0 ; if(sX == kk or sX == sY) Z=1,C=x ; COMPARE_KK = 6'H1D, // COMPARE sX,kk if(sX < kk or sX < sY) Z=X,C=1 ; if(sX > kk or sX > sY) Z=X,C=0 ; if(sX == kk or sX == sY) Z=1,C=x ; COMPARECY_REG= 6'H1E, // COMPARECY sX,sY COMPARECY_KK = 6'H1F, // COMPARECY sX,kk // TSET TEST_REG = 6'H0C, // TEST sX,sY TEST_KK = 6'H0D, // TEST sX,kk TESTCY_REG = 6'H0E, // TESTCY sX,sY TESTCY_KK = 6'H0F, // TESTCY sX,kk // LOGIC AND_REG = 6'H02, // AND sX,sY AND_KK = 6'H03, // AND sX,kk OR_REG = 6'H04, // OR sX,sY OR_KK = 6'H05, // OR sX,kk XOR_REG = 6'H06, // XOR sX,sY XOR_KK = 6'H07; // XOR sX,kk // -------- SCRIPT_END --> 脚本识别标签,勿动 ``` 要在上面的格式上进行指令扩展,脚本识别标签勿动,这是用来根据指令自动生成裁剪mcu宏定义的关键内容! 编译完毕,将会自动打开`gtkwave`显示波形。 ![image-20201127030217652](/images/image-20201127030217652.png) ## step3: 拷贝文件到工程 设计好后,可以自行拷贝`rom.v, mcu.v, head.h `到项目,连接号个信号线即可。比如我项目中使用`mini-mcu`的顶层文件为: ```verilog module top ( input clk_in, //输入系统12MHz时钟 //4bit拨码开关输入 input [3:0] sw, input [3:0] key, //按键输入 //数码管 output [8:0] seg_led_1, output [8:0] seg_led_2, //rgb output reg[2:0]rgb, //led output led1, output led2, output led3, output led4, output led5, output led6, output led7, output led8 ); wire clk ,clko,rst; reg [7:0] out; assign {led8,led7,led6,led5,led4,led3,led2,led1} = out; assign clk = clk_in; reg rst_n_in; //复位信号 reg [17:0]cnt ; always @(posedge clk) begin if(cnt>=18'h3ffff)begin rst_n_in <= 1'b1; end else begin cnt <= cnt +1; rst_n_in <= 0; end end /* divide #( .N(1) ) u1 ( .clk(clko), .rst_n(rst_n_in), .clkout(clk) ); */ //----------- mini-mcu 相关------------ wire [11:0]address; wire [17:0] instruction; wire bram_enable, read_strobe, write_strobe; reg [7:0] in_port; wire [7:0] port_id, out_port; //----------- 数码管 相关------------ reg[3:0] seg_data_1, seg_data_2; //输出引脚 always @(posedge clk )begin if(write_strobe)begin case(port_id) 8'h00:{seg_data_1,seg_data_2} <= out_port;//bcd编码的2个数码管 8'h01:out <= out_port; //LED控制 8'h02:rgb <= out_port[2:0]; //rgb default:out <=out; endcase end else out <= out; end //输入引脚 always@(*)begin if(read_strobe) begin case(port_id) 8'h00: in_port = {key[3:0],sw[3:0]}; //按键 4bit拨码开关输入 endcase end end /********************************** * 例化mini-mcu **********************************/ mcu mcu( .clk(clk), //系统时钟 .rst_n( rst_n_in), //复位 0 --> 复位 .address( address), //程序取址地址 .instruction( instruction), //指令输入 .bram_enable( bram_enable), //程序rom使能 1-->使能 .in_port( in_port), //输入口 .read_strobe( read_strobe), //输入口使能 .port_id( port_id), //io口地址 .out_port( out_port), //输出口 .write_strobe( write_strobe) //输出口写使能 ); rom rom( .clk( clk), .address( address), //程序取址地址 .instruction( instruction), //指令输入 .enable( bram_enable) //程序rom使能 1-->使能 ); /********************************** *数码管显示 是bcd码 **********************************/ seg_display seg_display( .seg_data_1(seg_data_1), .seg_data_2(seg_data_2), .seg_led_1(seg_led_1), .seg_led_2(seg_led_2) ); endmodule ``` ## step4:使用和我一样的开发板 小脚丫FPGA step- MOX2-C,不带TAGE,是U盘模式,拷贝文件就相当下载配置文件,使用这个是最方便的,因为我写了脚本,可以试试在根目录下执行`./run -g`试一试,这个命令将会编译汇编,生成rom文件,裁剪mini-mcu,拷贝有用文件到项目工程路径,工具综合,下载到开发板整个过程完成!只需要稍微调整脚本即可实现。 ![image-20201127031143032](/images/image-20201127031143032.png) ![image-20201127031207445](/images/image-20201127031207445.png) ![image-20201127031303848](/images/image-20201127031303848.png) # 设计记录 ## 基本输入输出指令 **汇编代码** ```assembly ;系统时钟为50MHz constant led_port,01 ;定义led_port为常量01 constant led_on,00000010'b ; constant led_off,00000001'b ; start: load sF, led_on; output sF,led_port ;led亮 load sF, led_off; output sF,led_port ;led灭 jump start ;跳转的开始 ``` **对应的ROM文件中的内容为:** ```verilog `timescale 1ns / 1ps module rom( input clk, input [11:0]address, //程序取址地址 output[17:0]instruction, //指令输入 input enable //程序rom使能 1-->使能 ); reg [17:0] y; assign instruction = y; always@(posedge clk) begin if(enable)begin case(address) 12'd0 : y <= 18'h01F02; 12'd1 : y <= 18'h2DF01; 12'd2 : y <= 18'h01F01; 12'd3 : y <= 18'h2DF01; 12'd4 : y <= 18'h22000; endcase end end endmodule ``` **仿真文件:** ```verilog `timescale 1ns / 1ps module tb ; reg clk,rst_n; //生成始时钟 parameter NCLK = 20; initial begin clk=0; forever clk=#(NCLK/2) ~clk; end /****************** BEGIN ADD module inst ******************/ wire [11:0]address; wire [17:0] instruction; wire bram_enable, read_strobe, write_strobe; reg [7:0] in_port; wire [7:0] port_id, out_port; mcu mcu( .clk(clk), //系统时钟 .rst_n( rst_n), //复位 0 --> 复位 .address( address), //程序取址地址 .instruction( instruction), //指令输入 .bram_enable( bram_enable), //程序rom使能 1-->使能 .in_port( in_port), //输入口 .read_strobe( read_strobe), //输入口使能 .port_id( port_id), //io口地址 .out_port( out_port), //输出口 .write_strobe( write_strobe) //输出口写使能 ); rom rom( .clk( clk), .address( address), //程序取址地址 .instruction( instruction), //指令输入 .enable( bram_enable) //程序rom使能 1-->使能 ); /****************** BEGIN END module inst ******************/ initial begin $dumpfile("wave.lxt2"); $dumpvars(0, tb); //dumpvars(深度, 实例化模块1,实例化模块2,.....) end initial begin rst_n = 1; #(NCLK) rst_n=0; #(NCLK) rst_n=1; //复位信号 repeat(10000) @(posedge clk)begin end $display("运行结束!"); $dumpflush; $finish; $stop; end endmodule ``` **仿真波形** ![image-20201126003842859](/images/image-20201126003842859.png) 可以看到每个指令需要两个周期的时钟,结果符合的还不错。