Hint: this post is also available in English.

距离上次更新关于Pano的事情已经有一段时间了。期间我一直在研究Pano Logic G1上USB相关的东西,主要是为了连接手柄和U盘。一开始还有些担心LPDDR和缓存的稳定性,在RAM里执行代码会不会带来什么麻烦,不过到目前为止看起来一切正常。关于USB的事情下一次更新再提,这次就简单讲讲一些关于调试工具的事情:一个是串口,另外一个是Hard fault。

串口

如果说要看设备的输出日志,串口是最简便的方法了。起初我以为通过使用 VGA 输出的文本终端也可以代替串口终端,但很快就发现 80x30 大小的文本根本不堪使用;然而事实上 Pano Logic 并没有提供任何串口。虽然从原理图看,最初的设计中似乎有一个串口,但还是在后面的设计中被删除掉了。所以无论如何我都要用 IO 来重新实现一个自己的串口才可以继续使用串口来调试它。

这在 Pano Logic 上也不是第一次了,来自 PanoMan 项目的 Skip Hansen 已经做到了这一点:他将一根导线焊接到 LED 引脚并使用这个引脚获得串口输出。然而就像前一篇文章所述,我现在没有烙铁,所以得用其他的方法才行。

我使用 VGA SCL 引脚作为串口使用,写了一个简单的 UART 发送器来传输数据: https://github.com/zephray/VerilogBoy/blob/refactor/target/panog1/fpga/simple_uart.v。为什么要造轮子自己实现而不直接使用 Skip 的收发器,或者直接在 Tom 和 Skip 的项目上进行二次开发呢?其实答案和我选择 PicoRV32 而不是 VexRiscv 是一样的:我很久以前就将这个项目命名为 VerilogBoy,如果使用 SpinalHDL 就有一些表里不一的感觉。我也了解过一些优秀的开源 Verilog UART 控制器,但考虑到我并不需要多么奇特复杂的功能,也或许只是懒得去找那个合适的控制器,就自己写一个好了。

使用这个 UART 发送器也很简单,将它连接到总线并配置一个数据寄存器就好了。根据连接方式的不同,它也有很多可用的操作模式。如果将它的 Ready 信号连接到了总线的 Ready 信号,UART 发送器将会在传输完成前先阻止代码的执行。那么 UART 打印功能的代码将会像下面这么简单:

#define UART_TXDR *(volatile uint32_t *)0x03000100
void uart_print(char *str) {
    while (*str) UART_TXDR = *str++;
}

不用担心传输是否已经完成或者是否发生 FIFO overrun,因为它会将写入速度限制为传输速度。

或者,如果就绪信号连接到 CPU 的外部中断输入,他也可以产生传输结束中断,以实现软件 FIFO。

你是否有听说过单片机中的 Hard Fault?或者 Linux 中的段错误(核心已转储),再或者 Windows 里的“应用程序已停止工作”?这些错误通常指的是同一件事:代码访问到了它们不应该访问的内存地址。最常见的是非关联化一个空指针(指一个指向地址为0的指针)。这种错误很令人生厌,常见而不方便调试。但是不得不承认报错是很有用的,如果没人告诉我哪里出错,调试将会变得更加困难。

1553805847182-6449801553560402125.jpg

(It simply hangs when such things happens. It can be hard to determine who caused the issue on real hardware.)

(它只会在发生这种情况的时候挂起。很难确定是逻辑本身还是真正的硬件上引发了这个问题)

不幸的是,处理器核心本身不能检测到这样的错误,因为它并不知道什么是对什么是错。通常,这种情况的处理是由 MMU 或 MPU来负责的:先由程序定义有效的地址范围(取决于具体情况,可能是物理地址或者是虚拟地址)。然后,当发生非法访问的时候,由MMU 或 MPU 生成异常。在没有 MMU 或者 MPU 的系统上这个任务是由总线控制器实现的,当访问未映射的地址空间就会产生异常。PiCoRV32 就是这样一个没有MMU 或者 MPU的系统,因此我将在总线控制器实现它。

旁注1:ARM单片机的 Hard Fault 并不一定意味着发生了非法的内存访问。未对齐的内存访问、无效指令和少数其他异常也会产生这样的错误。但非法内存访问仍然是最常见的情况。

旁注2:在 ARM 单片机上,就算没有启用 MPU,访问未映射的内存空间仍会触发Hard Fault,是一个很棒的特性了。

旁注3:我曾经为 80386 实现过这样的总线控制器,但是并不能如期正常工作。原因是因为它有一个预读取队列,会在当前 EIP 后盲目地获取指令,就很容易从有效的内存空间之外获取东西。对于现代的大多数处理器,我也假设会出现一样的情况。所以就可能没有办法在处理器之外实现此类保护。但PiCoRV32相比较这种处理器还是简单很多,所以我能实现这一点。

如代码所示,很简单:捕获所有对未映射空间的访问,并生成中断。

    reg mem_valid_last;
    always @(posedge clk_rv) begin
        if (!rst_rv)
            cpu_irq <= 1'b0;
        else begin
            mem_valid_last <= mem_valid;
            if (mem_valid && !mem_valid_last && !(ram_valid || vram_valid || gpio_valid || usb_valid || uart_valid || ddr_valid))
                cpu_irq <= 1'b1;
            else
                cpu_irq <= 1'b0;
        end
    end

在固件里,创建一个 hard fault 事件处理程序,

irq:
# copy the source PC (inside gp register) to argument 0 (a0 register)
addi a0, gp, 0
# call handler
call hard_fault_handler
void hard_fault_handler(uint32_t pc) {
    // External logic is required to connect fault signal to IRQ line,
    // and ENABLE_IRQ_QREGS should be turned off.
    term_print("HARD FAULT PC = ");
    term_print_hex(pc, 8);
    while (1);
}

不要忘记在启用的时候先打开总中断的使能:

    // Set interrupt mask to zero (enable all interrupts)
    // This is a PicoRV32 custom instruction
     asm(".word 0x0600000b");

现在尝试一些会导致 hard fault 的程序,它应该如预期一样报错:

1553806036814-6631831553560531841.png

旁注4:为什么我决定将其称为“Hard Fault”而不是“Segmentation Fault”分段错误或者“Page Fault”页面错误?因为我的系统中没有 MMU,所以就没有内存分页,它也没有X86处理器那样的内存段,所以从技术上讲不是“Seg Fault”或者“Page Fault”,还是 Hard Fault,硬错误更准确些。我知道我应该把它称为总线错误,但这就有些事后诸葛亮的感觉。所以就保持现状了。

旁注5:不要将 Boot ROM 或者任何东西放在地址0的地方。这将使空指针解除引用合法,在我这种情况下将会无法捕获错误。猜猜是谁因为这个问题花了一整天来追踪一些随机的崩溃问题……

结论

这些东西不算太复杂,我只花了半天时间就实现了两个,结果证明这对于我调试我的愚蠢代码很有帮助。或许你会觉得有一个完整的JTAG调试器或者跟踪器会更好,但我在实现它后已经用完了所有可用的 GPIO,我写这篇文章只是为了给项目更新,也是为了分享一下我的一些想法。希望能在下次更新中见到你。最后,感谢阅读。

注:本文由Shellbin翻译