串口通信

串口通信

七月 05, 2025 次阅读

设计概要如下:

在前述“简易8位通用CPU设计”的基础上扩充UART异步串行通信收发器硬件,实现以特定波特率与PC完成串口通信的功能,并以通用CPU嵌入式系统
程序(机器语言或者汇编语言)完成与实际PC串口的通信。

你说你不知道简易8位通用CPU设计?自觉跳转:8位通用CPU设计

硬件上的修改

首先要说明的是,本题使用的是波特率为9600的 uart 通信,因此我们需要每隔 1s /9600 执行一次数据读取或者发送,而我们的开发板频率是50MHz,故我们
需要计算执行一次数据发送或者数据接收:

1分频时执行一次数据处理所需指令周期 = 50,000,000 / 9600 / 6 = 868
2分频时执行一次数据处理所需指令周期 = 50,000,000 / 9600 / 6 / 2 = 434
4分频时执行一次数据处理所需指令周期 = 50,000,000 / 9600 / 6 / 4 = 217

说明一下,只有四分频才能保证 CPU 在处理数据的时候不会出现数据访问冲突,但这并不意味着我们需要采用4分频,因为指令处理频率越高数据帧读取出差错可能性越小
具体采用几分频,得测试哪种方式下数据帧接收错误率最低。因此我们需要一个计数器,用于计数,868和434显然是不能用8为存储下去的,但不管我们采用几分频,都可以采用
217为一个周期计数,比如说我们采用1分频,,那么我们可以让计数每隔4个指令周期才加一,示例如下:

if (~x) begin
    if (~xx)
        cnt <= next_cnt;
        xx = ~xx;
    end
    x = ~x;
end

在上一题的基础上,需要添加两个新指令 in 和 out,用于串口通信的输入和输出:

  • 指令编码表:
编号 编码(二进制) 汇编指令 操作 备注
0 0000xxyy mov rx, ry ry <= rx
1 0001xxyy mov rx, (ry) (ry) <= rx
2 001000yy iiiiiiii mov i, ry ry <= i
3 010000rr jmp r PC <= r 无条件跳转
4 010001rr jc r PC <= r 进位时跳转
5 010010rr jz r PC <= r 结果为0时跳转
6 010100yy in ry ry <= uart receive 将uart的rx线的值输入到指定寄存器的最高位
7 010101yy out ry uart send <= ry 将指定寄存器的最低位输出到uart的tx线
8 0110xxyy mov (rx), ry ry <= (rx)
9 01110000 rtn PC <= 0
10 1000xxyy and rx, ry ry <= rx & ry
11 1001xxyy or rx, ry ry <= rx | ry
12 101000xx not rx rx <= ~rx
13 1011xxyy xor rx, ry ry <= rx ^ ry
14 1100xxyy shl rx, ry ry <= ry << rx
15 1101xxyy add rx, ry ry <= ry + rx
16 1110xxyy shr rx, ry ry <= ry >> rx
17 1111xxyy sub rx, ry ry <= ry - rx

010100yy 对应的是 in 指令,用于将接受到的数据存到指定寄存器的最高位

010101yy 对应的是 out 指令,用于发送指定寄存器的最低位(将寄存器最低位传到 tx)

首先说一说怎么实现串口通信吧,基本原理如下:

serial_port_communication

呐,这张图上面的61引脚和62引脚分别对应的是 rxd32 和 txd32,也就是发送引脚和输出引脚,假设我们顶层模块中设置的发送端口为rx,接收端口
位tx,那么就需要将这两个端口分别连接到61号引脚以及62号引脚(在.qsf配置文件中加上如下配置):

set_location_assignment PIN_61 -to rx    // 将61号引脚与 rx 端口连接作为接收端口
set_location_assignment PIN_62 -to tx    // 将62号引脚与 rx 端口连接作为发送端口

添加 in 指令和 out 指令

其中 in 指令和 out 指令的实现如下:

首先,需要添加几个新的信号:

output reg reg_bus_in, // 寄存器组输入,将需要存入指定寄存器最高位的数据写进去,若 reg_en_in 为高电平,则会将该数据写入指定寄存器的最高位(人话:ry[7] <= reg_bus_in)
output reg reg_en_in, // 寄存器组输入使能,配合上面那玩意使用的
input reg_bus_out, // 寄存器组输出,将需要发送的指定寄存器最低位的数据(reg_bus_out)写出去,若 reg_en_out 为高电平,则会将该数据写入 tx 等待发送(人话
:reg_bus_out <= ry[0])
output reg reg_en_out // 寄存器组输出使能,配合上面那玩意使用的

因此,寄存器除了判断总线仲裁是否选择了它,更需要判断是否需要它配合实现 uart 通信,因此,寄存器组需要被改装:

module reg_file(
    input         clk,
    input       rst,
    // 双地址总线接口
    input  [2:0]  t_addrbus,    // 目标地址总线
    input  [2:0]  s_addrbus,
    
    output reg [7:0]  databus0, // 双向数据总线(dMDR与寄存器组之间交互线)
    output reg [7:0]  databus1, // 双向数据总线(dMDR与寄存器组之间交互线)
    output reg [7:0]  databus2, // 双向数据总线(dMDR与寄存器组之间交互线)
    output reg [7:0]  databus3, // 双向数据总线(dMDR与寄存器组之间交互线)
    input [7:0]  inbus,  // 输入数据总线(来自ALU或其他模块)
    // 控制信号
    input cur_reg_bus_in, // 控制单元寄存器组输出
    input cur_reg_en_in, // 控制单元寄存器组使能信号
    output reg cur_reg_bus_out, // 当前寄存器组输出
    input cur_reg_en_out, // 当前寄存器组使能信号
    output [7:0] r3dbg
);
    assign r3dbg = databus3;
    
    //------------------------------------------
    // 同步写逻辑
    //------------------------------------------
    always @(negedge clk) begin
        // 目标寄存器写入(地址有效时)
        if (cur_reg_en_in) begin
            case (t_addrbus[1:0]) // 低2位选择寄存器
                2'd0: databus0[7] <= cur_reg_bus_in; // R0
                2'd1: databus1[7] <= cur_reg_bus_in; // R1
                2'd2: databus2[7] <= cur_reg_bus_in; // R2
                2'd3: databus3[7] <= cur_reg_bus_in; // R3
            endcase
        end
        else if (cur_reg_en_out) begin
            case(s_addrbus[1:0]) // 低2位选择寄存器
                2'd0: cur_reg_bus_out <= databus0[0]; // R0
                2'd1: cur_reg_bus_out <= databus1[0]; // R1
                2'd2: cur_reg_bus_out <= databus2[0]; // R2
                2'd3: cur_reg_bus_out <= databus3[0]; // R3
            endcase
        end
        else if (t_addrbus[2] == 1'b0) begin  // 最高位=0时有效
            case (t_addrbus[1:0]) // 低2位选择寄存器
                2'd0: databus0 <= inbus; // R0
                2'd1: databus1 <= inbus; // R1
                2'd2: databus2 <= inbus; // R2
                2'd3: databus3 <= inbus; // R3
            endcase
        end

        if (rst) begin
            // 全部寄存器复位为0
            databus0 <= 8'b0;
            databus1 <= 8'b0;
            databus2 <= 8'b0;
            databus3 <= 8'b0;
        end
    end
endmodule

在 always 控制块中,添加了两个使能信号的判断(cur_reg_en_in 和 cur_reg_en_out),当信号有效时,就会将指定寄存器最低位的数据写到 cur_reg_bus_out,或者
将 cur_reg_bus_in 写到指定寄存器的最高位,而 这两个使能信号是由 CU 控制单元去控制生效的,当 CU 将这两个信号任意一个使能后,寄存器组就会将对于的工作处理完,
而后 CU 就可以利用 cur_reg_bus_in 和 cur_reg_bus_out 来处理接受和发送了,下面是 CU 中 in 指令和 out 指令的处理:

执行阶段1:operand高两位用于判断指令为 in 指令还是 out 指令,00 则为 in 指令,01 则为 out 指令
  • in指令逻辑:

让指定寄存器最高位接受数据,并将 rx 中的数据接收到 reg_bus_in 中,寄存器组将会利用 reg_bus_in 存储数据,当然最重要的一步,让 reg_en_in 为1

  • out指令逻辑:

让指定寄存器最低位发送数据,让 reg_en_in 为1,这一步后指定寄存器会将最低为存储到 reg_bus_out 当中,下一步将会利用到该数据

// EXEC1
4'b0101: begin
    case(operand[3:2])
        // in ry // ry <= uart receive
        2'b00: begin
            t_addrbus <= {1'b0, operand[1:0]};
            reg_bus_in <= rx;               // 寄存器组输入
            reg_en_in <= 1;                 // 寄存器组输入使能信号
        end
        // out ry // uart send
        2'b01: begin
            s_addrbus <= {1'b0, operand[1:0]};
            reg_en_out <= 1;
        end
    endcase
end
执行阶段2
  • in指令逻辑:

寄存器组停止输入,且停止使能信号输出

  • out指令逻辑:

寄存器组停止输出,且停止使能信号输出

// EXEC2
4'b0101: begin
    case(operand[3:2])
        // in ry step2(in)
        2'b00: begin
            t_addrbus <= 3'b111;                // 停止寄存器组输入
            reg_en_in <= 0;                     // 寄存器组输入使能信
        end
        2'b01:begin
            s_addrbus <= 3'b111;                // 停止寄存器组输出
            reg_en_out <= 0;                    // 寄存器组输出使能信
        end
    endcase
end

可能会有人问了,out 指令不是还要将数据写入到 tx 当中吗,这咋感觉像是两个指令都完成了,要做收尾工作了呢?你别急,你考虑过的我肯定也考虑过了,
若此时 out 指令直接将数据输出到 tx,很有可能寄存器还没有将数据写入到 reg_bus_out 当中,此时就有可能将错误数据写入到 tx 当中

执行阶段3(独属于 out 指令的阶段)
4'b0101: begin
    case(operand[3:2])
            2'b01:begin
        tx <= reg_bus_out;               // 寄存器组输出使能
        end
    endcase
end

在第三阶段再将数据写入 tx, 就肯定不会出问题了

相信细心的观众肯定发现了,我的立即数指令变化了,这是因为串口通信的确需要8位立即数的计算:

CU 立即数 mov 指令逻辑修改

当然,立即数只能存下8位数据,那么我们要怎么做才能保证传送8位数据呢,再怎么样操作数只有4位,一个指令码应该很难实现吧?

对了,那就使用两个指令码来处理立即数 mov 操作,这意味着该指令会让 pc 自增两次,因为需要读取两个指令码,下面是指令的实现:

之前 IR 发送到总线的数据是指令码的低五位,通过这种方式就需要 IR 直接将指令码八位全部发送到总线上去:

always @(posedge clk) begin
    // 指令加载阶段
    if (ir_in) begin
        databus <= inbus;    // 锁存总线数据(被修改部分)
    end
end
执行阶段1

虽然我们要读取两个指令码,但是实际上我们执行的逻辑是第一个指令码的逻辑,第二个指令码是一个完全的操作数,因此在 pc 自增后,我们要记住上一条是 mov 命令:

reg imm_get_en;    // 立即数获取使能(若为1,则继续执行 mov i ry指令)
reg [1:0] imm_reg; // 立即数寄存器(因为在 pc 自增后会丢失 mov 指令数据,因此需要提前将指定寄存器的编号锁存下来)
// EXEC1
    4'b0010: begin
        imm_reg <= operand[1:0];         // 锁存立即数读取的寄存器编号
        pc_out <= 1;                     // PC输出至数据总线
        s_addrbus <= 3'b110;             // 指令存储器读取指令
        imm_get_en <= 1;                 // 立即数获取使能
    end

首先让 imm_reg 锁存指定寄存器的编号,然后让 PC 输出下一条指令的编号,将 imm_get_en 信号使能设置为1,后面的执行阶段中会先
特判 imm_get_en 是否为1,若为1则执行 mov i ry 指令的逻辑

执行阶段2
if (imm_get_en) begin
    pc_out <= 0;                       // PC停止输出
    pc_inc <= 1;                       // PC自增
    s_addrbus <= 3'b111;               // 停止源地址发送数据
    iMDRout <= 1;                      // 指令数据存储器输出数据至总线
    ir_in <= 1;                        // 指令寄存器从总线中读取指令
end

执行阶段1已经将 imm_get_en 设置为1了,进入执行阶段2后就直接进入该逻辑部分了,此时让 pc 自增,为后面的阶段做准备,让指令存储器输出数据到总线,
同时让指令寄存器从总线读取指令(准确来说是立即数)

执行阶段3
if (imm_get_en) begin
    pc_inc <= 0;                       // PC停止自增
    iMDRout <= 0;                      // 指令数据存储器停止输出数据至总线
    ir_in <= 0;                        // 指令寄存器停止读取指令
    imm_out <= 1;                      // 立即数输出使能
    t_addrbus <= {1'b0, imm_reg};      // 立即数写入寄存器
end

接下来就是立即数输出使能了,让指定寄存器读取数据,需要被写入的寄存器之前已经被锁存了,当时若不锁存,这个时候就无法获取到寄存器编号了,因为 pc 已经自增了

执行阶段4
if (imm_get_en) begin
    imm_out <= 0; // 立即数输出停止
    imm_get_en <= 0; // 立即数获取使能复位
    t_addrbus <= 3'b111; // 停止目标地址接受数据
end

这一阶段没什么好说的,将之前被激活的信号关掉

CU单元算是一个大改,最终代码如下:

module CU (
    // 时钟与复位
    input         clk,
    input         rst,          // 全局复位信号
    
    // 指令接口
    input  [3:0]  opcode,
    input  [3:0]  operand,

    // 程序地址总线控制
    output reg      pc_reset,       // PC清零
    output reg        pc_out,         // PC地址输出使能
    output reg        pc_inc,         // PC自增
    output reg        pc_jump,        // PC跳转
    
    // 指令寄存器总线控制
    output reg        iMDRout,        // iMDR写入总线

    // 寄存器组总线控制
    // ALU总线控制
    output reg        ALUAin,         // ALU数据A输入
    output reg        ALUBin,         // ALU数据B输入
    output reg        ALUout,         // ALU数据输出
    output reg [2:0]  ALUop,          // ALU操作选择
    input             cf,             // 进位标志(CF)
    input             zf,             // 零标志(ZF)

    // 数据存储器总线控制
    output reg        dmdr_iin,       // dMDR和总线输入使能
    output reg        dmdr_iout,      // dMDR和总线输出使能(数据总线输出)
    output reg        mem_as_target,  // 数据存储器作为目标地址总线
    output reg        mem_as_source,

    output reg        ir_in,          // 从总线加载指令(上升沿有效)
    output reg        imm_out,        // 立即数输出(低五位)

    output reg [2:0]  s_addrbus,      // 源地址选择
    output reg [2:0]  t_addrbus,      // 目标地址选择
    output reg [7:0]  cnt,
    output     [7:0]  next_cnt,
    input      rx, // UART接收线(可选)
    output reg tx, // UART发送线(可选)
    output reg reg_bus_in, // 寄存器组输入(R0-R3)
    output reg reg_en_in, // 寄存器组输入使能(R0-R3)
    input  reg_bus_out, // 寄存器组输出(R0-R3)
    output reg reg_en_out // 寄存器组输出使能(R0-R3)
);
    reg imm_get_en; // 立即数获取使能
    reg [2:0] cur_sta;
    reg fetch1_wait; // 等待信号
    reg [1:0] imm_reg; // 立即数寄存器
    parameter 
        FETCH1   = 3'b000,
        FETCH2   = 3'b001,
        EXEC1    = 3'b010,
        EXEC2    = 3'b011,
        EXEC3    = 3'b100,
        EXEC4    = 3'b101;

    Incrementer cnt_inc(
        .op(cnt),
        .next_op(next_cnt)
    );
    always @(negedge clk) begin
        case (cur_sta)
            // 取指阶段
            FETCH1: begin
                pc_out <= 1;            // PC输出至数据总线
                s_addrbus <= 3'b110;    // 指令存储器读取指令
                t_addrbus <= 3'b111;    // 无目标地址
                if (fetch1_wait) begin
                    fetch1_wait <= 1'b0; // 取指等待信号复位
                    cur_sta <= FETCH1; // 保持在FETCH1状态
                end else begin
                    cur_sta <= FETCH2;    // 状态转移到FETCH2
                end
            end
            FETCH2: begin
                pc_out <= 0;            // PC停止总线输出
                s_addrbus <= 3'b111;    // 停止指令存储器读取指令
                iMDRout <= 1;           // 指令数据存储器输出数据至总线
                ir_in <= 1;             // 指令寄存器从总线中读取指令
                pc_inc <= 1;            // PC自增
                cur_sta <= EXEC1;    // 状态转移到EXEC1
            end
            EXEC1: begin
                pc_inc <= 0;            // PC停止自增
                iMDRout <= 0;           // 指令数据存储器停止输出数据至总线
                ir_in <= 0;             // 指令寄存器停止读取指令
                
                // Start
                case (opcode)
                    // move rx, ry step1(move)
                    4'b0000: begin
                        s_addrbus <= {1'b0, operand[3:2]};    // rx发送数据信息至数据总线
                        t_addrbus <= {1'b0, operand[1:0]};   // ry接受数据总线上的数据信息
                    end
                    // move rx, (ry) step1(间接寻址)
                    4'b0001: begin
                        s_addrbus <= {1'b0, operand[3:2]};    // rx发送数据信息至数据总线
                        dmdr_iin <= 1;                        // MDR接收总线数据
                    end
                    // move i ry
                    4'b0010: begin
                        imm_reg <= operand[1:0]; // 锁存立即数读取的寄存器编号
                        pc_out <= 1;                     // PC输出至数据总线
                        s_addrbus <= 3'b110;             // 指令存储器读取指令
                        imm_get_en <= 1;              // 立即数获取使能
                    end
                    // jump
                    4'b0100: begin
                        case (operand[3:2])
                            // ry
                            2'b00: begin
                                pc_jump <= 1;               // PC跳转
                                s_addrbus <= {1'b0, operand[1:0]}; // 直接跳转到地址
                            end
                            // jc ry
                            2'b01: begin
                                if (cf) begin
                                    pc_jump <= 1;           // PC跳转
                                    s_addrbus <= {1'b0, operand[1:0]}; // 直接跳转到地址
                                end else begin
                                    pc_jump <= 0;           // PC不跳转
                                end
                            end
                            // jz ry
                            2'b10: begin
                                if (zf) begin
                                    pc_jump <= 1;           // PC跳转
                                    s_addrbus <= {1'b0, operand[1:0]}; // 直接跳转到地址
                                end else begin
                                    pc_jump <= 0;           // PC不跳转
                                end
                            end
                        endcase
                    end
                    4'b0101: begin
                        case(operand[3:2])
                        // in ry // ry <= uart receive
                        2'b00: begin
                            t_addrbus <= {1'b0, operand[1:0]};
                            reg_bus_in <= rx;               // 寄存器组输入
                            reg_en_in <= 1;                 // 寄存器组输入使能信号
                        end
                        // out ry // uart send
                        2'b01: begin
                            s_addrbus <= {1'b0, operand[1:0]};
                            reg_en_out <= 1;
                        end
                        endcase
                    end
                    // move (rx), ry step1(间接寻址)
                    4'b0110: begin
                        s_addrbus <= {1'b0, operand[3:2]};    // rx输出地址数据至数据总线
                        mem_as_source <= 1;
                    end
                    // rtn step1(rtn)
                    4'b0111: begin
                        pc_reset <= 1;                       // PC清零
                    end
                    // and/or/xor/add/sub/shl/shr rx ry step1(alua <= rx)
                    4'b1000, 4'b1001, 4'b1011, 4'b1101, 4'b1111, 4'b1100, 4'b1110: begin
                        s_addrbus <= {1'b0, operand[3:2]};
                        ALUAin <= 1;
                    end
                    default: cur_sta <= cur_sta;
                endcase
                cur_sta <= EXEC2;    // 状态转移到EXEC2

            end
            EXEC2: begin
                if (imm_get_en) begin
                    pc_out <= 0;                       // PC停止输出
                    pc_inc <= 1;                       // PC自增
                    s_addrbus <= 3'b111;                // 停止源地址发送数据
                    iMDRout <= 1;                  // 指令数据存储器输出数据至总线
                    ir_in <= 1;                     // 指令寄存器从总线中读取指令
                end else begin
                case (opcode)
                    // move rx, ry endstep
                    4'b0000: begin
                        s_addrbus <= 3'b111;
                        t_addrbus <= 3'b111;
                    end
                    // move rx, (ry) step2(move)
                    4'b0001: begin
                        dmdr_iin <= 0;                        // MDR停止接收总线数据
                        s_addrbus <= {1'b0, operand[1:0]};    // ry发送地址信息至数据总线
                        // t_addrbus <= 3'b100;                 // 通过总线数据地址信息将对应内存数据存至dMDR
                        mem_as_target <= 1;                // 数据存储器作为目标地址总线
                    end
                    // jump step2(jump)
                    4'b0100: begin
                        pc_jump <= 0;                       // PC停止跳转
                        s_addrbus <= 3'b111;                // 停止源地址发送数据
                        t_addrbus <= 3'b111;                // 停止目标地址接受数据
                    end
                    4'b0101: begin
                        case(operand[3:2])
                        // in ry step2(in)
                        2'b00: begin
                            t_addrbus <= 3'b111;                // 停止寄存器组输入
                            reg_en_in <= 0;                     // 寄存器组输入使能信
                        end
                        2'b01:begin
                            s_addrbus <= 3'b111;                // 停止寄存器组输出
                            reg_en_out <= 0;                    // 寄存器组输出使能信
                        end
                        endcase
                    end
                    // move (rx), ry step2(move)
                    4'b0110: begin
                        mem_as_source <= 0;
                        dmdr_iout <= 1;                         // dMDR输出数据至数据总线
                        // s_addrbus <= 3'b100;                    // 主存根据地址数据总线访存
                        t_addrbus <= {1'b0, operand[1:0]}; // ry接受数据总线上的数据
                    end
                    // rtn endstep
                    4'b0111: begin
                        pc_reset <= 0;                          // PC停止清零
                    end
                    // and rx ry step2(alub <= ry)
                    4'b1000: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b100;
                    end
                    // or rx ry step2(alub <= ry)
                    4'b1001: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b101;
                    end
                    // xor rx ry step2(alub <= ry)
                    4'b1011: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b111;
                    end
                    // not ry step2(alub <= ry)
                    4'b1010:  begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b110;
                    end
                    // shl rx, ry (alub <= ry)
                    4'b1100: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b010;
                    end
                    // and rx ry step2(alub <= ry)
                    4'b1101: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b000;
                    end
                    // shr rx ry(alub <= ry)
                    4'b1110: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b011;
                    end
                    // sub rx ry step2(alub <= ry)
                    4'b1111: begin
                        s_addrbus <= {1'b0, operand[1:0]};
                        ALUAin <= 0;
                        ALUBin <= 1;
                        ALUop  <= 3'b001;
                    end
                    default: cur_sta <= cur_sta;
                endcase
                end
                // 状态转移到EXEC3
                cur_sta <= EXEC3;
            end
            EXEC3: begin
                if (imm_get_en) begin
                    pc_inc <= 0;                       // PC停止自增
                    iMDRout <= 0;                  // 指令数据存储器停止输出数据至总线
                    ir_in <= 0;                     // 指令寄存器停止读取指令
                    imm_out <= 1;                  // 立即数输出使能
                    t_addrbus <= {1'b0, imm_reg}; // 立即数写入寄存器
                end else begin
                case (opcode)
                    // move rx, (ry) endstep
                    4'b0001: begin
                        s_addrbus <= 3'b111;                      // 停止源地址发送数据  
                        t_addrbus <= 3'b111;                 // 停止目标地址接受数据
                        mem_as_target <= 0;
                    end
                    4'b0101: begin
                        case(operand[3:2])
                        2'b01:begin
                            tx <= reg_bus_out;               // 寄存器组输出使能
                        end
                        endcase
                    end
                    // move (rx), ry endstep
                    4'b0110: begin
                        dmdr_iout <= 0;                          // dMDR停止输出数据至数据总线
                        t_addrbus <= 3'b111;                     // ry停止接受数据总线上的数据
                        s_addrbus <= 3'b111;                     // 停止源地址发送数据
                    end
                    // and/or/xor/add/sub/shl/shr rx ry step3 (result <= databus)
                    4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1101, 4'b1111, 4'b1100, 4'b1110: begin
                        //clear
                        s_addrbus <= 3'b111;
                        ALUBin <= 0;
                        //operate
                        ALUout <= 1;                         
                        t_addrbus <= {1'b0, operand[1:0]};        // 计算结果写入ry
                    end
                    default: cur_sta <= cur_sta;
                    endcase
                // 状态转移到EXEC4
                end
                cur_sta <= EXEC4;
            end
            EXEC4: begin
                if (imm_get_en) begin
                    imm_out <= 0; // 立即数输出停止
                    imm_get_en <= 0; // 立即数获取使能复位
                    t_addrbus <= 3'b111; // 停止目标地址接受数据
                end else begin
                    case (opcode)
                    // (and/or/not/xor/add/sub/shl/shr rx ry) / add/sub i endstep
                    4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1100, 4'b1101, 4'b1110, 4'b1111, 4'b1100, 4'b1110: begin
                        ALUout <= 0;                         
                        t_addrbus <= 3'b111;
                    end
                    default: cur_sta <= cur_sta;
                    endcase
                end
                // 状态转移到FETCH1
                cur_sta <= FETCH1;
                if(cnt[3] & cnt[4] & cnt[6] & cnt[7]) cnt <= 0;
                // if(cnt == 19) cnt <= 0;
                // if (cnt[0]) cnt <= 0;
                // 这一部分后面会讲到,这里先不用关心
                else begin
                    if (~x) begin
                        if (~xx)
                            cnt <= next_cnt;
                        xx = ~xx;
                    end
                    x = ~x;
                end
            end
            default: cur_sta <= FETCH1;
        endcase

        if (rst) begin
            cur_sta <= FETCH1; // 复位时状态机回到FETCH1
            pc_reset <= 1'b0;
            pc_out <= 1'b0; // PC输出停止
            pc_inc <= 1'b0; // PC自增停止
            iMDRout <= 1'b0; // 指令存储器输出停止
            ALUAin <= 1'b0; // ALU A输入停止
            ALUBin <= 1'b0; // ALU B输入停止
            ALUout <= 1'b0; // ALU输出停止
            ALUop <= 3'b000; // ALU操作码清零
            dmdr_iin <= 1'b0; // 数据存储器输入停止
            dmdr_iout <= 1'b0; // 数据存储器输出停止
            ir_in <= 1'b0; // 指令寄存器输入停止
            imm_out <= 1'b0; // 立即数输出停止
            s_addrbus <= 3'b111; // 源地址总线置空
            t_addrbus <= 3'b111; // 目标地址总线置空
            fetch1_wait <= 1'b1; // 清除等待状态
            cnt <= 0;
            tx <= 1;
        end
    end

    reg x = 0;
    reg xx = 0;
    
endmodule

主存配合 uart 通信所必需的要求

always @(negedge clk) begin
    // 总线输入到MDR
    if (dmdr_iin)
        databus <= inbus;
    // MDR写到内存
    else if (mem_as_target)
        memory[mem_addr[2:0]] <= databus;
    // 内存读到MDR
    else if (mem_as_source)
        databus <= memory[mem_addr[2:0]];
    // new part
    if (memory[6] == memory[7]) begin // 14和15寄存器相等时,输出PWM调光信号
        memory[5][0] <= 1'b1; // 13寄存器最低位为1
    end
    memory[7] <= cnt;
    // new part end
end

assign mem_addr = inbus;

其中 memory[7] 会一直存储当前所处的周期计数,而 memory[6] 用于存放 uart 通信中期望接收数据或发送数据所达到的周期,因此,memory[5][0] 就用来存放
两个内存中数据是否相同的判断,当判断为1,uart 就会接受1 bit 数据。

软件上的实现

下面开始详解汇编代码的编写:

  • Part 1:检测起始信号
L0:
    xor r3, r3              // 00 | 10111111
    not r3                  // 01 | 10100011
    in r0                   // 02 | 01010000
    // 下降沿后, and 指令会设置一个零标志
    and r0, r3              // 03 | 10000011

    // 零标志时跳转至 recv_pre
    mov recv_pre, r0        // 04 | 00100000
                            // 05 | 00001000 : recv_pre
    jz r0                   // 06 | 01001000

    // 不跳转回到开头继续等待
    rtn                     // 07 | 01110000

刚开始,我们需要循环检测低电平,在 uart 协议当中,起始位为低电平,我们不断地通过 in 指令读取数据至 r0 寄存器的最高位,而 r3 在刚开始时被清零并取反,
r3 变成全1,当收到信号为高电平时零标志位为0,就会触发 rtn 指令,回到开头继续循环,信号变成低电平后零标志位为1,将 recv_pre 指令地址起始地址存到 r0,并跳转到
该地址

  • Part 2:计时器取中值

我们要做到在发送信号后的中间时刻取数,这样才能保证数据不会取偏。我将解释全部放到代码当中,方便理解

// 第一个下降沿后,计算采样点的计数(当前计数+108)
// 可直接存储立即数(自己实现,我不敢改了,反正能跑)
recv_pre:                   // 08
    // r3=108 (以下的步骤全都是在给r3 赋值108,其实我上面已经改成了8位立即数存储,但当时写这个汇编代码的时候并不支持,后期修改后反而还跑不了了,所以就索性不改了)
    mov 5, r0               // 08 | 00100000
                            // 09 | 00000101
    mov 3, r3               // 0a | 00100011
                            // 0b | 00000011
    shl r0, r3              // 0c | 11000011
    mov 0xc, r0             // 0d | 00100000
                            // 0e | 00001100
    add r0, r3              // 0f | 11010011
    // 计数器  上面说过,主存的最后一块地址用于存储当前的周期计数,因此这里的操作是将数据拿出来,以此为基准,在相对于这个时间点后的108个指令周期接受一次数据
    // r1 = ucnt - 108
    mov ucnt, r0            // 10 | 00100000
                            // 11 | 00000111 : ucnt

    mov (r0), r1            // 12 | 01100001
    sub r3, r1              // 13 | 11111101

    // if (r1 < 0) r1 = r1 + 216
    // 可直接使用 jc 判断(你知道我想说什么的)
    // 这里是通过判断减后最高位是不是1来判断 r1 是否小于0,若是1,说明建成了一个负数,需要加上216补全
    mov 7, r2               // 14 | 00100010
                            // 15 | 00000111
    mov 1, r0               // 16 | 00100000
                            // 17 | 00000001
    shl r2, r0              // 18 | 11001000
    and r0, r1              // 19 | 10000001
    mov 1f, r0              // 1a | 00100000
                            // 1b | 00100001 : 1f
    jz r0                   // 1c | 01001000
    mov 1, r0               // 1d | 00100000
                            // 1e | 00000001
    // r3 << 1 (r3 = 216)
    // 跑这里来说明就是 r1 < 0 了,得加216补全
    shl r0, r3              // 1f | 11000011
    add r3, r1              // 20 | 11011101
    1:                      // 21

    // (testpoint) = r1
    // 将期望接受数据的周期数存储到测试点(内存地址6)当中,在硬件逻辑中当检测到内存6和内存7单元相同时会把地址7变为1
    mov testpoint, r0       // 21 | 00100000
                            // 22 | 00000110 : testpoint
    mov r1, (r0)            // 23 | 00010100
    // (attp) = 0,先将 r1 清零,将 attp 内存地址存放的数据变为0(因为这里是起始位,需要等过去,不能执行数据接收)(起始位不是真正的数据,下一个比特才是)
    xor r1, r1              // 24 | 10110101
    mov attp, r0            // 25 | 00100000
                            // 26 | 00000101 : attp
    mov r1, (r0)            // 27 | 00010100

    // 8个采样周期,作为循环计数
    // (recv_bcnt) = 8
    // 内存地址0用于存放数据接收剩余周期数(一共8个周期,因为总共需要接收8个数据位)
    mov 8, r1               // 28 | 00100001
                            // 29 | 00001000
    mov recv_bcnt, r0       // 2a | 00100000
                            // 2b | 00000000 : recv_bcnt
    mov r1, (r0)            // 2c | 00010100
  • Part 3:数据接收决策点
// 1.5个周期后是第一个采样点,所以先跳过一个采样点
// 因为指令读取 attp 的行为对于时钟计数有延迟,这也是为什么需要让指令执行频率尽量高,因为我们需要让所谓的1.5个周期足够的小
// 实际上有很小的概率把第一个采样点跳过,导致数据错误
L2:                         // 2d
    // 检测内存5是不是1,是的话就说明可以接收数据了
    mov attp, r0            // 2d | 00100000
                            // 2e | 00000101 : attp
    mov (r0), r1            // 2f | 01100001
    mov 1, r0               // 30 | 00100000
                            // 31 | 00000001
    and r0, r1              // 32 | 10000001
    mov L2, r0              // 33 | 00100000
                            // 34 | 00101101 : L2
    // 不是1,继续等待
    jz r0                   // 35 | 01001000
    // 是1,进入数据接收Part,顺手把r0清零(养成好习惯,后面每一bit数据都要检测attp,因为硬件上内存只有给attp赋值位1的权限,所以attp清零必须依靠汇编代码)
    xor r1, r1              // 36 | 10110101
    mov attp, r0            // 37 | 00100000
                            // 38 | 00000101
    mov r1, (r0)            // 39 | 00010100
  • Part 4:数据接收
recv:                       // 3a
    // attp 在 ucnt 到达 testpoint 时会自动设为1
    // 检测 attp 即可得知是否到达当前采样点
    mov attp, r0            // 3a | 00100000
                            // 3b | 00000101 : attp
    mov (r0), r1            // 3c | 01100001
    mov 1, r0               // 3d | 00100000
                            // 3e | 00000001
    and r0, r1              // 3f | 10000001
    // 没到采样点时跳转回recv
    mov recv, r0            // 40 | 00100000
                            // 41 | 00111010 : recv
    jz r0                   // 42 | 01001000

    // 否则开始采样,先右移将上次采样结果保留
    // 再使用 in 指令将这次的接收线的值存下来
    mov 1, r0               // 43 | 00100000
                            // 44 | 00000001
    shr r0, r3              // 45 | 11100011
    in r3                   // 46 | 01010011

    // 将 attp 置 0,以等待下次的采样点到达
    xor r1, r1              // 47 | 10110101
    mov attp, r0            // 48 | 00100000
                            // 49 | 00000101 : attp
    mov r1, (r0)            // 4a | 00010100

    // 循环计数减1
    mov recv_bcnt, r0       // 4b | 00100000
                            // 4c | 00000000 : recv_bcnt
    mov (r0), r1            // 4d | 01100001
    mov 1, r0               // 4e | 00100000
                            // 4f | 00000001
    sub r0, r1              // 50 | 11110001
    mov recv_bcnt, r0       // 51 | 00100000
                            // 52 | 00000000 : recv_bcnt
    mov r1, (r0)            // 53 | 00010100
    mov 0xff, r0            // 54 | 00100000
                            // 55 | 11111111
    and r0, r1              // 56 | 10000001

    // 循环计数为0时进入发送阶段
    mov send_pre, r0        // 57 | 00100000
                            // 58 | 01011101 : send_pre
    jz r0                   // 59 | 01001000

    // 否则跳回recv
    mov recv, r0            // 5a | 00100000
                            // 5b | 00111010 : recv
    jmp r0                  // 5c | 01000000

此时 r3 寄存器的值为接收到的数据帧的8个数据位,结束位直接忽略

  • Part5 数据发送准备
// 发送准备阶段
send_pre:                   // 5d
    xor r1, r1              // 5d | 10110101

    // 因为是发送,采样点简单设为0就行,不需要特殊计算
    mov testpoint, r0       // 5e | 00100000
                            // 5f | 00000110 : testpoint
    mov r1, (r0)            // 60 | 00010100

    mov attp, r0            // 61 | 00100000
                            // 62 | 00000101 : attp
    mov r1, (r0)            // 63 | 00010100

    // 8个采样周期,作为循环计数,这里懒得改符号名了,所以还是 recv_bcnt
    // (recv_bcnt) = 8
    mov 8, r1               // 64 | 00100001
                            // 65 | 00001000
    mov recv_bcnt, r0       // 66 | 00100000
                            // 67 | 00000000 : recv_bcnt
    mov r1, (r0)            // 68 | 00010100
  • Part6 数据发送
// 加载起始位
L1:                         // 69
    mov attp, r0            // 69 | 00100000
                            // 6a | 00000101 : attp
    mov (r0), r1            // 6b | 01100001
    mov 1, r0               // 6c | 00100000
                            // 6d | 00000001
    and r0, r1              // 6e | 10000001
    mov L1, r0              // 6f | 00100000
                            // 70 | 01101001 : L1
    jz r0                   // 71 | 01001000

    // r0 归零,并输出 r0
    xor r0, r0              // 72 | 10110000
    out r0                  // 73 | 01010100

    // 将 attp 置 0,以等待下次的采样点到达
    xor r1, r1              // 74 | 10110101
    mov attp, r0            // 75 | 00100000
                            // 76 | 00000101 : attp
    mov r1, (r0)            // 77 | 00010100

// 发送8个数据位
send:                       // 78
    mov attp, r0            // 78 | 00100000
                            // 79 | 00000101 : attp
    mov (r0), r1            // 7a | 01100001
    mov 1, r0               // 7b | 00100000
                            // 7c | 00000001
    and r0, r1              // 7d | 10000001

    // 没到采样点时跳回 send
    mov send, r0            // 7e | 00100000
                            // 7f | 01111000 : send
    jz r0                   // 80 | 01001000

    // 先输出 r3 ,再将 r3 右移,这样下次再执行到这里时,输出的最低位依然是要发送的位
    out r3                  // 81 | 01010111
    mov 1, r0               // 82 | 00100000
                            // 83 | 00000001
    shr r0, r3              // 84 | 11100011

    // attp 置 0,不再重复说明
    xor r1, r1              // 85 | 10110101
    mov attp, r0            // 86 | 00100000
                            // 87 | 00000101 : attp
    mov r1, (r0)            // 88 | 00010100

    // 循环计数减1
    mov recv_bcnt, r0       // 89 | 00100000
                            // 8a | 00000000 : recv_bcnt
    mov (r0), r1            // 8b | 01100001
    mov 1, r0               // 8c | 00100000
                            // 8d | 00000001
    sub r0, r1              // 8e | 11110001
    mov recv_bcnt, r0       // 8f | 00100000
                            // 90 | 00000000 : recv_bcnt
    mov r1, (r0)            // 91 | 00010100
    mov 0xf, r0             // 92 | 00100000
                            // 93 | 00001111
    and r0, r1              // 94 | 10000001

    // 循环计数为0时发送完成,即将发送结束位
    mov tr_end, r0          // 95 | 00100000
                            // 96 | 10011011 : tr_end
    jz r0                   // 97 | 01001000

    // 否则回到 send,发送下一位
    mov send, r0            // 98 | 00100000
                            // 99 | 01111000 : send
    jmp r0                  // 9a | 01000000

发送逻辑和接收逻辑相似,因此解释会少很多

  • Part7 结束位发送
// 传输完成,发送结束位
tr_end:                     // 9b
    mov attp, r0            // 9b | 00100000
                            // 9c | 00000101 : attp
    mov (r0), r1            // 9d | 01100001
    mov 1, r0               // 9e | 00100000
                            // 9f | 00000001
    and r0, r1              // a0 | 10000001

    // 同样读取 attp ,没到采样点时跳回 tr_end
    mov tr_end, r0          // a1 | 00100000
                            // a2 | 10011011 : tr_end
    jz r0                   // a3 | 01001000

    // 加载结束位
    mov 1, r0               // a4 | 00100000
                            // a5 | 00000001
    out r0                  // a6 | 01010100

    // 回到开头接收下一个字节
    rtn                     // a7 | 01110000

汇编代码完整版:

section text

// 等待第一个下降沿
L0:
    xor r3, r3              // 00 | 10111111
    not r3                  // 01 | 10100011
    in r0                   // 02 | 01010000
    // 下降沿后, and 指令会设置一个零标志
    and r0, r3              // 03 | 10000011

    // 零标志时跳转至 recv_pre
    mov recv_pre, r0        // 04 | 00100000
                            // 05 | 00001000 : recv_pre
    jz r0                   // 06 | 01001000

    // 不跳转回到开头继续等待
    rtn                     // 07 | 01110000

// 第一个下降沿后,计算采样点的计数(当前计数+108)          // 可直接存储立即数
recv_pre:                   // 08
    // r3=108
    mov 5, r0               // 08 | 00100000
                            // 09 | 00000101
    mov 3, r3               // 0a | 00100011
                            // 0b | 00000011
    shl r0, r3              // 0c | 11000011
    mov 0xc, r0             // 0d | 00100000
                            // 0e | 00001100
    add r0, r3              // 0f | 11010011

    // r1 = ucnt - 108
    mov ucnt, r0            // 10 | 00100000
                            // 11 | 00000111 : ucnt

    mov (r0), r1            // 12 | 01100001
    sub r3, r1              // 13 | 11111101

    // if (r1 < 0) r1 = r1 + 216                         // 可直接使用 jc 判断
    mov 7, r2               // 14 | 00100010
                            // 15 | 00000111
    mov 1, r0               // 16 | 00100000
                            // 17 | 00000001
    shl r2, r0              // 18 | 11001000
    and r0, r1              // 19 | 10000001
    mov 1f, r0              // 1a | 00100000
                            // 1b | 00100001 : 1f
    jz r0                   // 1c | 01001000
    mov 1, r0               // 1d | 00100000
                            // 1e | 00000001
    // r3 << 1 (r3 = 216)
    shl r0, r3              // 1f | 11000011
    add r3, r1              // 20 | 11011101
    1:                      // 21

    // (testpoint) = r1
    mov testpoint, r0       // 21 | 00100000
                            // 22 | 00000110 : testpoint
    mov r1, (r0)            // 23 | 00010100
    // (attp) = 0
    xor r1, r1              // 24 | 10110101
    mov attp, r0            // 25 | 00100000
                            // 26 | 00000101 : attp
    mov r1, (r0)            // 27 | 00010100

    // 8个采样周期,作为循环计数
    // (recv_bcnt) = 8
    mov 8, r1               // 28 | 00100001
                            // 29 | 00001000
    mov recv_bcnt, r0       // 2a | 00100000
                            // 2b | 00000000 : recv_bcnt
    mov r1, (r0)            // 2c | 00010100

// 1.5个周期后是第一个采样点,所以先跳过一个采样点
// 因为指令读取 attp 的行为对于时钟计数有延迟,
// 实际上有很小的概率把第一个采样点跳过,导致数据错误
L2:                         // 2d
    mov attp, r0            // 2d | 00100000
                            // 2e | 00000101 : attp
    mov (r0), r1            // 2f | 01100001
    mov 1, r0               // 30 | 00100000
                            // 31 | 00000001
    and r0, r1              // 32 | 10000001
    mov L2, r0              // 33 | 00100000
                            // 34 | 00101101 : L2
    jz r0                   // 35 | 01001000

    xor r1, r1              // 36 | 10110101
    mov attp, r0            // 37 | 00100000
                            // 38 | 00000101
    mov r1, (r0)            // 39 | 00010100

recv:                       // 3a
    // attp 在 ucnt 到达 testpoint 时会自动设为1
    // 检测 attp 即可得知是否到达当前采样点
    mov attp, r0            // 3a | 00100000
                            // 3b | 00000101 : attp
    mov (r0), r1            // 3c | 01100001
    mov 1, r0               // 3d | 00100000
                            // 3e | 00000001
    and r0, r1              // 3f | 10000001
    // 没到采样点时跳转回recv
    mov recv, r0            // 40 | 00100000
                            // 41 | 00111010 : recv
    jz r0                   // 42 | 01001000

    // 否则开始采样,先右移将上次采样结果保留
    // 再使用 in 指令将这次的接收线的值存下来
    mov 1, r0               // 43 | 00100000
                            // 44 | 00000001
    shr r0, r3              // 45 | 11100011
    in r3                   // 46 | 01010011

    // 将 attp 置 0,以等待下次的采样点到达
    xor r1, r1              // 47 | 10110101
    mov attp, r0            // 48 | 00100000
                            // 49 | 00000101 : attp
    mov r1, (r0)            // 4a | 00010100

    // 循环计数减1
    mov recv_bcnt, r0       // 4b | 00100000
                            // 4c | 00000000 : recv_bcnt
    mov (r0), r1            // 4d | 01100001
    mov 1, r0               // 4e | 00100000
                            // 4f | 00000001
    sub r0, r1              // 50 | 11110001
    mov recv_bcnt, r0       // 51 | 00100000
                            // 52 | 00000000 : recv_bcnt
    mov r1, (r0)            // 53 | 00010100
    mov 0xff, r0            // 54 | 00100000
                            // 55 | 11111111
    and r0, r1              // 56 | 10000001

    // 循环计数为0时进入发送阶段
    mov send_pre, r0        // 57 | 00100000
                            // 58 | 01011101 : send_pre
    jz r0                   // 59 | 01001000

    // 否则跳回recv
    mov recv, r0            // 5a | 00100000
                            // 5b | 00111010 : recv
    jmp r0                  // 5c | 01000000

// 发送准备阶段
send_pre:                   // 5d
    xor r1, r1              // 5d | 10110101

    // 因为是发送,采样点简单设为0就行,不需要特殊计算
    mov testpoint, r0       // 5e | 00100000
                            // 5f | 00000110 : testpoint
    mov r1, (r0)            // 60 | 00010100

    mov attp, r0            // 61 | 00100000
                            // 62 | 00000101 : attp
    mov r1, (r0)            // 63 | 00010100

    // 8个采样周期,作为循环计数,这里懒得改符号名了,所以还是 recv_bcnt
    // (recv_bcnt) = 8
    mov 8, r1               // 64 | 00100001
                            // 65 | 00001000
    mov recv_bcnt, r0       // 66 | 00100000
                            // 67 | 00000000 : recv_bcnt
    mov r1, (r0)            // 68 | 00010100

// 加载起始位
L1:                         // 69
    mov attp, r0            // 69 | 00100000
                            // 6a | 00000101 : attp
    mov (r0), r1            // 6b | 01100001
    mov 1, r0               // 6c | 00100000
                            // 6d | 00000001
    and r0, r1              // 6e | 10000001
    mov L1, r0              // 6f | 00100000
                            // 70 | 01101001 : L1
    jz r0                   // 71 | 01001000

    // r0 归零,并输出 r0
    xor r0, r0              // 72 | 10110000
    out r0                  // 73 | 01010100

    // 将 attp 置 0,以等待下次的采样点到达
    xor r1, r1              // 74 | 10110101
    mov attp, r0            // 75 | 00100000
                            // 76 | 00000101 : attp
    mov r1, (r0)            // 77 | 00010100

// 发送8个数据位
send:                       // 78
    mov attp, r0            // 78 | 00100000
                            // 79 | 00000101 : attp
    mov (r0), r1            // 7a | 01100001
    mov 1, r0               // 7b | 00100000
                            // 7c | 00000001
    and r0, r1              // 7d | 10000001

    // 没到采样点时跳回 send
    mov send, r0            // 7e | 00100000
                            // 7f | 01111000 : send
    jz r0                   // 80 | 01001000

    // 先输出 r3 ,再将 r3 右移,这样下次再执行到这里时,输出的最低位依然是要发送的位
    out r3                  // 81 | 01010111
    mov 1, r0               // 82 | 00100000
                            // 83 | 00000001
    shr r0, r3              // 84 | 11100011

    // attp 置 0,不再重复说明
    xor r1, r1              // 85 | 10110101
    mov attp, r0            // 86 | 00100000
                            // 87 | 00000101 : attp
    mov r1, (r0)            // 88 | 00010100

    // 循环计数减1
    mov recv_bcnt, r0       // 89 | 00100000
                            // 8a | 00000000 : recv_bcnt
    mov (r0), r1            // 8b | 01100001
    mov 1, r0               // 8c | 00100000
                            // 8d | 00000001
    sub r0, r1              // 8e | 11110001
    mov recv_bcnt, r0       // 8f | 00100000
                            // 90 | 00000000 : recv_bcnt
    mov r1, (r0)            // 91 | 00010100
    mov 0xf, r0             // 92 | 00100000
                            // 93 | 00001111
    and r0, r1              // 94 | 10000001

    // 循环计数为0时发送完成,即将发送结束位
    mov tr_end, r0          // 95 | 00100000
                            // 96 | 10011011 : tr_end
    jz r0                   // 97 | 01001000

    // 否则回到 send,发送下一位
    mov send, r0            // 98 | 00100000
                            // 99 | 01111000 : send
    jmp r0                  // 9a | 01000000

// 传输完成,发送结束位
tr_end:                     // 9b
    mov attp, r0            // 9b | 00100000
                            // 9c | 00000101 : attp
    mov (r0), r1            // 9d | 01100001
    mov 1, r0               // 9e | 00100000
                            // 9f | 00000001
    and r0, r1              // a0 | 10000001

    // 同样读取 attp ,没到采样点时跳回 tr_end
    mov tr_end, r0          // a1 | 00100000
                            // a2 | 10011011 : tr_end
    jz r0                   // a3 | 01001000

    // 加载结束位
    mov 1, r0               // a4 | 00100000
                            // a5 | 00000001
    out r0                  // a6 | 01010100

    // 回到开头接收下一个字节
    rtn                     // a7 | 01110000

section data

recv_bcnt: 8

preserved: resb 5 - .

attp: 0
testpoint: 0
ucnt:

汇编代码完成,没写汇编器,靠人编的(见汇编代码右侧注释),然后利用一个简单的程序将该汇编代码中的所有8位指令码提取出来,相信各位的软件能力,简单demo呈现:

#include <iostream>
#include <fstream>
#include <string>

int cnt = 0;  // 全局计数器

std::string extractor(const std::string& input) {
    std::string result;
    if(input.find("(replaced)") != std::string::npos) {
        return result;  // 如果没有 "(replaced)",直接返回原始字符串
    }
    int sz = input.size();
    for(int i = 0; i <= sz - 8; ++i)
    {
        bool flag = false;
        for(int j = 0; j < 8; ++j)
        {
            if(input[i + j] >= '0' && input[i + j] <= '9') continue;
            else {
                flag = true;  // 如果有非数字字符,标记为true
                break;
            }
        }
        if(!flag) {  // 如果没有非数字字符
            result += input.substr(i, 8);  // 提取8个字符
            std::cout << "Extracted: " << result << std::endl;  // 输出提取的字符串
            ++cnt;  // 增加计数器
            return result;
        }
    }
    return ""; // 如果没有找到符合条件的字符串,返回空字符串
}

void processFile(const std::string& inputFilename) {
    // 构造输出文件名
    size_t dotPos = inputFilename.find_last_of('.');
    std::string outputFilename = inputFilename.substr(0, dotPos) + "_ic.hex";

    // 打开输入文件
    std::ifstream inputFile(inputFilename);
    if (!inputFile.is_open()) {
        std::cerr << "Error: Could not open input file " << inputFilename << std::endl;
        return;
    }

    // 打开输出文件
    std::ofstream outputFile(outputFilename);
    if (!outputFile.is_open()) {
        std::cerr << "Error: Could not create output file " << outputFilename << std::endl;
        inputFile.close();
        return;
    }

    // 逐行处理文件
    std::string line;
    while (std::getline(inputFile, line)) {
        std::string result = extractor(line);
        if (!result.empty()) {  // 只有结果非空时才写入
            outputFile << result << '\n';
        }
    }

    // 关闭文件
    inputFile.close();
    outputFile.close();

    std::cout << "Processing completed. Results written to " << outputFilename << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <input_file.xxx>" << std::endl;
        return 1;
    }
    std::string inputFilename = argv[1];
    processFile(inputFilename);
    std::ofstream outputFile(inputFilename.substr(0, inputFilename.find_last_of('.')) + "_ic.hex", std::ios::app);
    for(int i = cnt; i < 256; ++i){
        outputFile << "00000000\n";  // 填充空行
    }
    return 0;
}

用法如下:

./[可执行程序名称] [文件名]

比如说我的可执行程序时 icex.exe,需要提取8位指令码的文件为 uart.asm,那么我就输入如下命令:

./icex.exe uart.asm

超频

感兴趣的可以看看,结论先放出来,不分配是最好的
接下来我们需要决策一下使用几分频:

首先试一试四分频(数据传输安全,但帧率低):

serial_port_communication

然后试试二分频(数据传输不太安全,但帧率变高):

serial_port_communication

最后试一试不分频(数据传输没有保障,但帧率极高):

serial_port_communication

那么,我们该如何计算正确率呢,没错,再写一个小demo:

#include <iostream>
#include <fstream>
#include <string>

double calculater(const std::string &inputFilename, const std::string &compare_num)
{
    std::ifstream inputFile(inputFilename);
    if (!inputFile.is_open()) {
        std::cerr << "Error: Could not open input file " << inputFilename << std::endl;
        return -1;
    }
    int total_num = 0, true_num = 0;
    std::string word;
    while(inputFile >> word)
    {
        ++total_num;
        if(word == compare_num) ++true_num;
    }
    return (double)(true_num) / total_num;
}


int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <input_file.xxx>" << "compare_str" << std::endl;
        return 1;
    }
    std::string inputFilename = argv[1];
    std::cout << calculater(inputFilename, argv[2]);
    return 0;
}

检测正确率如下,从上至下分别是四分频,二分频以及不分频:

PS C:\altera\90\quartus\project\core8> ./acc.exe check.txt 8A
0.807823
PS C:\altera\90\quartus\project\core8> ./acc.exe check.txt 8A
0.880952
PS C:\altera\90\quartus\project\core8> ./acc.exe check.txt 8A
0.955782

最后把所有的代码(适配uart版本)呈现给大家:

适配 uart 的 CPU 代码
core8.v
uart 汇编代码
uart.asm
辅助\测试工具 demo
icex.cpp
accurancy.cpp