本文讨论了SoC系统的架构设计,包括处理器核心、内存以及其他外设的互连,并详细描述了如何通过AHB-Lite总线实现高效的数据传输。AHB-Lite总线是一种简化版本的AHB总线。同时,阐述了利用寄存器映射以及其他硬件资源与软件接口的设计方法,以满足SoC系统的功能需求。本文为未来更复杂的SoC设计提供参考。

一、SoC系统下的软硬件分工

  在设计一款SOC时,首先要考虑清楚的是“软硬件分工”。我们团队在做项目的时候犯过错误,因为硬件工程师惯性思维想到所有的功能其实都可以在硬件中完成,只是单纯的想把处理这种功能的模块“放置”在总线上,这是不正确的。(”TOTALLY IN WRONG DIRECTION“——Iain)。

  有的功能只有硬件能实现,比如精准地计数、定时(利用时钟周期)、存储、按键消抖、同步器。

  有些情况下编写C语言代码可以更方便,比如算法(如滤波函数)、计算(如果用硬件做除法会极其麻烦)、控制(如传感器、OLED显示)。大多数时候处理器核心是封装好的、混淆源代码后的CPU(像ARM核这种第三方IP就不允许修改核心内部),它可以一条一条地执行指令,C语言代码经过编译之后得到机器指令,可以让CPU完成算法或者控制。

Hardware only 和System on Chip的区别

  SoC比纯硬件layout整体做下来layout面积要大不少(主要是处理器核占的比较大,还有debounce也占的大),但是软件层面的子函数切换调用显然比硬件中很多很多状态的状态机切换要舒服。

  • 硬件设计时还需要考虑设计SoC子模块中的IO寄存器有哪些;
  • SoC要考虑清楚软硬件协同工作!

例子1:计算时间差值

  举个例子:比如判断开关是否被按下这个动作,并且计算两次按下开关之间的时间。

  • 在软件层面,如果把按下开关的数据放在寄存器里,并在C语言里调用读取,可以得到开关的0和1或者是次数差值,如果想计算时间,需要在C语言里调用现在的时间计数器,将时间计数的差值算出来。而一条指令的执行往往需要多个时钟周期,在需要计算时间差值时实际上已经产生了不小的误差,甚至可能会错过。

  • 而在硬件层面,在always_ff里写判断到开关被按下,记录现在的时间计数值或两次按下的差值,就可以得到非常精准的时间差,而C语言可以调用这个时间差并进行后续运算。

例子2:想实现功能的切换

再比如:想实现功能的切换。

  • 在硬件层面,可以使用状态机进行状态跳转,在下一状态赋值。非常不灵活,改代码也要改很多。

  • 在软件层面,C语言是在while(1)循环里直接if判断,成立就调用子函数。CPU会自动编译成跳转指令,跳转过去执行其他指令。

例子3:LED显示

再比如:LED显示。

  • 在软件层面,如果利用ndigit[3:0]给4个LED进行片选,让每个LED点亮。实际上在LED点亮之前可能会执行其他的运算,会使得4个LED的点亮周期不一样,在硬件上显得有些LED会更亮有些会比较暗。

  • 在硬件层面,LED的写入以周期性规律地写入到寄存器,会让每个LED的片选点亮周期一致。

  好的软硬件配合是:硬件层面LED规律地点亮,软件层面负责将需要点亮的值的结果发到底层硬件,可以根据软件写下来地值平衡地输出到LED上。“Doing something every certain number of clock cycles is really really really easy in hardware, but it’s difficult to do in software.”

二、SoC系统

  为了实现所需功能的需求,需要在处理器核心外部挂载设备或功能模块,使用总线通信的方式是很好的思路。
怎么学习一个内核和SoC,阅读和理解内核规格说明书(spec):这是对内核设计和实现的详细描述,包括内核的接口定义、功能描述、寄存器说明等。阅读和理解这些规格说明书是理解内核设计的关键。

0. 认识ARM Cortex-M0 内核

在这里插入图片描述

首先,了解一个内核需要关注技术手册提供的内核的信息,本项目所使用的ARM Cortex-M0为32位、3级流水线RISC处理器(一共有56条指令),为冯·诺依曼(Von Neumann)架构(即不能同时获取指令和数据)。为了满足低成本、低功耗的需求,M0核只支持使用AHB-Lite接口连接外设,但使用M0核搭建SoC的时候有着以下的特点:

  • Cortex M0 不支持突发事务(BURST transaction)
    HBURST[2:0] 始终为 3’b000,SINGLE表示传输是单个数据项的传输,而不是连续的突发传输。
  • Cortex M0 不支持锁定事务(Locked transactions)
    HMASTLOCK 始终为 1’b0,意味着主设备没有锁定总线,其他设备可以根据仲裁机制来竞争总线访问权。总线访问是通过仲裁器(Arbiter)进行管理,根据仲裁器的调度策略,各个设备会竞争总线访问权限。总线访问权限的控制是通过仲裁器的优先级和轮询算法来实现的,而不是通过HMASTLOCK信号的锁定机制。
  • Cortex M0 仅发出非顺序传输(non-sequential transfers, NONSEQ)
    HTRANS[1:0] 是 2’b00(空闲)或 2’b10(非顺序)。这意味着每个突发的长度为一次传输,大小 = 字节、半字或字,不能是BUSY或连续。ps,我理解的“sequential”是连续的传输。
  • Cortex M0 不支持非对齐内存访问,这要求了访问数据的内存地址是4的倍数。
    在Cortex-M0中,例如,字大小传输只能访问 0x0、0x4、0x8、0xC 等地址。同样,半字传输只能访问 0x0、0x2、0x4 等地址。未对齐的内存访问通常比对齐的访问慢,因为处理器必须在多个内存周期中获取数据并执行额外的位移和屏蔽操作以正确对齐数据。

在这里插入图片描述

M0开发工具包(包含混淆后的M0内核代码)

  • Cortex-M0 DesignStart 套件旨在用于基于 Cortex-M0 处理器的原型 SoC 的系统 Verilog 设计和仿真。作为完整 Cortex-M0 处理器的预配置和==混淆==但可综合的 Verilog 版本提供。
  • ARM GNU 交叉编译器用于编译运行,在designstart.ld里有RAM和ROM的映射,编译后将C/C++源代码编译为ARM平台可执行文件(这里的M0核遵循32位ARM® ARMv6-M 指令集)。
  • 这个工具包提供了混淆后的核,我们可以写一个AHB-interconnect模块连接子IP。

在这里插入图片描述

ps,原版Full Cortex-M0 processor 和 混淆后的Cortex-M0 processor from DesignStart的区别有很多,比如后者没有调试访问program counter,没有中断控制,没有architecture clock gating支持。本工程项目里,只关心designstart kit提供的混淆后的M0内核代码。

  1. 【Github源码】ARM® Cortex® -M0 DesignStart™ RTL Testbench

  2. 【官网说明书】ARM Cortex-M0 DesignStart RTL 测试平台用户指南

M0核的其他技术参考手册

  • 作为一个精简指令集,M0有56条左右的指令。
  1. ARM® Cortex®-M0 Technical Reference Manual

  2. ARM® Cortex®-M0 User Guide Reference Material

  3. ARM® ARMv6-M Architecture Reference Manual

  4. ARM® Cortex®-M0 Integration and Implementation Manual

AHB-Lite总线

在这里插入图片描述

  1. ARM® AMBA®3 AHB-Lite Protocol (v1.0) Specification
  2. AMBA AHB Protocol Specification

其他学习资料

M0内核处理器端口描述

在这里插入图片描述

ARM Cortex-M0内核处理器的端口描述可以分为以下几个部分:

  1. Nested Vectored Interrupt Controller (NVIC):NVIC是Cortex-M0处理器中用于中断管理的模块。它包含一些用于配置和控制中断的寄存器,以及用于响应和处理中断的逻辑。NVIC通常包括中断使能寄存器、中断优先级寄存器、中断挂起寄存器等,用于管理和处理中断请求。
  2. AHB-Lite:AHB-Lite是Cortex-M0处理器常用的片上总线接口协议,用于连接处理器核心与外设、存储器等。它包括一系列用于地址、数据、控制信号传输的总线端口,如地址总线端口、数据总线端口、控制信号端口等。通过AHB-Lite接口,Cortex-M0处理器与外部设备进行数据交换和通信。
  3. Debug:Debug部分包括用于调试和测试的相关接口和信号。这可能包括调试端口、调试信号、调试模式的配置等,用于支持处理器的调试功能和调试器的连接。
  4. IO:IO部分涉及处理器与外部IO设备(如GPIO、UART、SPI等)之间的通信。GPIO寄存器:Cortex-M0内核通常提供一组GPIO寄存器,用于配置和控制GPIO引脚。这些寄存器包括数据寄存器(用于读取和写入引脚状态)、方向寄存器(用于配置引脚输入或输出)、中断寄存器(用于配置引脚中断)等。
  5. Clock:Clock部分涉及处理器的时钟相关接口和信号。这可能包括时钟输入端口、时钟分频器、时钟使能信号等,用于提供处理器的时钟信号和时钟控制。
  • 输入端口:
输入端口描述
hclkCortex-M0处理器的时钟输入
hreset_nCortex-M0处理器的复位输入
nmi_i非屏蔽中断输入
irq_i可屏蔽中断输入
haddr_o地址总线输出
hburst_o传输类型输出
hmastlock_o主控锁定输出
hprot_o传输保护级别输出
hsize_o传输大小输出
htrans_o传输类型输出
hwdata_o数据总线输出
hwrite_o写使能输出
  • 输出端口:
输出端口描述
hrdata_i数据总线输入
hready_i传输完成信号输入
hresp_i传输响应输入
txev_o发送事件输出
rxev_i接收事件输入
lockup_o锁定输出
sys_reset_req_o系统复位请求输出
sleeping_o睡眠状态输出
vis_r0_o ~ vis_r14_o寄存器R0 ~ R14输出
vis_msp_o主堆栈指针输出
vis_psp_o进程堆栈指针输出
vis_pc_o程序计数器输出
vis_apsr_o程序状态寄存器输出
vis_tbit_oT位标志输出
vis_ipsr_o中断优先级寄存器输出
vis_control_o控制寄存器输出
vis_primask_o优先级屏蔽寄存器输出

1. AHB-Lite总线 与 ARM Cortex-M0

  AHB-Lite总线:AHB(AMBA High-performance Bus)是AMBA协议的一部分,用于连接高性能的系统模块。AHB-Lite是AHB的一个简化版本,设计用于低成本和低功耗的系统,同时保持了AHB的关键特性。AHB-Lite的主要特性包括:

  1. 单周期非阻塞读取或写入
  2. 支持突发(burst)传输
  3. 只有一个主设备(master)和一个或多个从设备(slave)
  4. 握手协议确保数据传输的正确性

  ARM Cortex-M0处理器:是ARM Holdings的一个微处理器核心,采用ARMv6-M指令集,设计用于低成本和低功耗的应用。Cortex-M0的主要特性包括:

  1. 32位RISC(Reduced Instruction Set Computer)架构。
  2. 支持Thumb-1指令集,它比原始的ARM指令集更节省存储空间。
  3. 使用von Neumann架构,数据和指令共享同一总线和内存。
  4. 内置了Nested Vectored Interrupt Controller(NVIC),支持多达32个中断源。

在这里插入图片描述

  Cortex-M0与AHB-Lite总线之间的连接通常通过一个总线接口模块进行,这个模块会将处理器的读取和写入请求转化为AHB-Lite协议的操作。在AHB-Lite总线协议中,只支持单周期突发。

2. 软硬件层面通过总线实现握手

  在软硬件系统的角度考虑整个SoC系统,要想从软件层面实现一定的功能比如读、写。握手通常是通过一系列的信号交换来实现硬件和软件之间的同步。握手的基本目标是确保数据在发送者和接收者之间正确地传输。

  • c语言在子模块的寄存器时,要先向读status寄存器,检查是否可以读数,如果可读就返回1,这样就可以执行读取该寄存器的功能;
  • c语言在子模块的寄存器时,要先向下写一个1到status寄存器,意味着请求写入,然后可以往指定寄存器写入想写入的值;

3. Memory Mapped I/O(内存映射 I/O)

  在SoC(系统片上芯片)设计中,Memory Mapped I/O(内存映射I/O)是一种常见的技术,它允许使用与内存相同的地址空间来访问I/O设备。它的基本思想是将外设寄存器映射到特定的内存地址上,使得处理器可以通过读写这些内存地址来与外设进行通信。IO device 寄存器被内存映射分配到唯一的一个地址上,软件层面C语言代码利用指针即可对指定地址进行读写操作。

在这里插入图片描述

  • 从软件层面考虑,Memory Mapped I/O 提供了一种简单的编程模型。由于外设被映射到内存地址空间,软件可以使用与访问内存相同的指令和指令操作数来访问外设。通过读写特定的内存地址,软件可以与外设进行通信。这种统一的编程模型简化了软件开发,减少了对特定外设的编程细节的处理。

  • 从硬件层面考虑,Memory Mapped I/O 的关键在于将外设寄存器与内存地址进行映射。外设寄存器与内存控制器连接,并使用特定的地址范围进行访问。处理器通过访问这些地址来读写外设寄存器,内存控制器将读写操作路由到相应的外设。这种映射简化了硬件设计,减少了专用I/O指令或接口的需求。

SoC设计时C语言程序将每个输入/输出设备视为占用一个或多个内存位置。通过总线上地址的译码可以找到挂载的硬件设备的地址映射。

I/O device

  I/O device表现在寄存器级的描述上,这些寄存器为处理器(或其他系统组件)和I/O设备之间的通信提供了接口。处理器(或其他系统组件)通过读写这些寄存器来控制I/O设备。例如,要发送数据,处理器可以先检查状态寄存器,看看设备是否已经准备好。然后,处理器可以把数据写入数据寄存器,然后通过设置控制寄存器来启动传输。

  在考虑I/O设备的性能时为了配置寄存器分为:

  • read/write
  • read only,就是不提供write_enable的读取功能
  • write only,就是不提供read_enable的读取功能
  • no direct access at all,存访中间值,不让读写。

在这里插入图片描述

4. SoC和microcontroller的区别

  第n次被怼:iain评价我们不是在做soc而是在做microcontroller…

  我们总结分析了当时不完整设计两点原因:

  1. 在proposal画的soc图里I/O寄存器的端口不是专用的。举个例子:一个mircrocontroller不是专门用于显示LED的,但是它可以显示LED。而一个SoC是针对具体硬件的设计,作为SoC工程师你得明白你在设计什么硬件。一个mircrocontroller只有输入口和输出口,没有什么不同,而custom interface可以清楚地告诉你status寄存器在哪、数据计数器counter在哪。
  2. 没有握手信号。握手信号在SoC设计中起着关键作用,尤其是在数据传输和通信中。在你的设计中,如果C语言写的函数不能正确地从寄存器读取或写入数据,可能是因为有些硬件细节没有被正确处理。例如,可能需要检查握手信号的状态,以确保数据已经准备好,或者可能需要正确地配置总线协议和寄存器。

三、软件层面C语言代码

  寄存器被分配唯一的一个地址上,通过CPU向该地址写数据,即可对该外设进行配置,通过CPU向该地址写数据,即可得到该外设的状态。
【集创赛】基于arm处理器的SOC设计【2】

0. SoC的软件代码顶层设计

  在给工程师看的开发文档里的程序图最好保持一定的风格和规范,比如说ASM状态转换图、软著里需要用的“调用关系图”、应用程序控制流程等。这里的层次以主循环里的调用函数形式为主,并不需要详细的功能描述、变量调用关系描述、因为在C/C++代码编译为机器指令后,在汇编层面也是代码片段的跳转执行。

  • 许多情况下,应用程序可以将中断驱动和轮询这两种方式组合使用(见图4.6)。通过软件变量传递,中断处理程序和主处理流程间可以进行信息交换。
    在这里插入图片描述
  • 并行处理任务
    • (1)将一个长时间的处理任务划分为一系列的状态,每次处理任务时,只执行一种状态;
    • (2)使用实时操作系统(RTOS)处理多任务。

在这里插入图片描述

1. C指针对分配地址定义——外设I/O寄存器的地址映射

  在C语言中,利用指针即可对指定地址进行读写操作。将指针指向一个地址,向该指针写数据即可实现向分配到该地址的寄存器写入数据,读取该指针即可读取指向该地址的寄存器的值。
如下,定义一个基地址叫AHB_CUSTOM_BASE的为0x40000000:

  • volatile 表示使用变量的时候直接从内存拿,所修饰的变量是易变的;
#define AHB_CUSTOM_BASE       0x40000000
volatile uint32_t* CUSTOM_REGS = (volatile uint32_t*) AHB_CUSTOM_BASE;
//[0] 4000 0000   第一个32bits的I/O寄存器的地址,比如他是一个counter
//[1] 4000 0004   第二个32bits的I/O寄存器的地址,比如他是一个LED
//[2] 4000 0012   可能是status寄存器哦

2. 读取I/O寄存器(Read)

  • 首先需要定义一个读寄存器的函数
uint32_t read_CUSTOM_counter(void)        { return IO_REGS[0];}        
  • 定义一个bool函数,用于检查是否可以读数的函数用于握手
bool check_CUSTOM_status(int num_reg){  
    int status,ready;
    status = CUSTOM_REGS[2]; //[2]是CUSTOM这个设备中IO寄存器们中的Status寄存器
    ready = (status >> num_reg) &1;  //status, 每一bit对应着第几位的IO寄存器是否可读
    return (ready == 1);    //如果可读就返回1,这样就可以执行if语句里读取的命令啦
}
  • 在main函数中请求读取,需要先检查Status寄存器中DataValid是否可读,在总线上只有握手成功才可以读取
while(1)
{
    if (check_CUSTOM_status(0))   // 检查第一个32bits的I/O寄存器【0】是不是可以读的
        {
        counter = read_CUSTOM_counter(); 
        }
}

3. 写入I/O寄存器(Write)

  • 先得请求写入操作,也就是在总线上要拉起来Datavalid
void write_CUSTOM_LED(uint32_t value)   {
CUSTOM_REGS[2] = 1<<1;  // pull up datavaild, 将1左移1位写入status寄存器,请求写入
CUSTOM_REGS[1] = value;} //然后将值写入到【1】这个LED的IO寄存器里
  • 在主函数中调用
while(1)
{
    write_CUSTOM_LED(1); //写入32bit的0000....1,写入数值1。
}

四、硬件层面SystemVerilog代码

Iain McNally 讲义

0. AHB-Lite传输协议

  在AHB-Lite(高级高速总线Lite)协议中,存在两种数据传输类型:写传输(write transfer)和读传输(read transfer)。这些传输类型用于在AHB-Lite总线上进行数据的写入和读取操作。
在这里插入图片描述

  以下是相关信号的简要介绍:

  1. HCLK(AHB-Lite时钟信号):HCLK是AHB-Lite总线的时钟信号,用于同步数据传输和协议操作。
  2. HADDR[31:0](地址信号):HADDR是一个32位的信号,用于传输读取或写入操作的目标地址。它指定了要读取或写入的设备或内存的地址。
  3. HWRITE(写使能信号):HWRITE是一个单比特的信号,用于指示传输是否是写操作。当HWRITE为高电平时,表示当前传输是写操作;当HWRITE为低电平时,表示当前传输是读操作。
  4. HRDATA[31:0](读数据信号):HRDATA是一个32位的信号,用于传输从目标设备或内存读取的数据。在读传输期间,HRDATA携带来自目标设备或内存的数据。
  5. HREADY(就绪信号):HREADY是一个单比特的信号,由目标设备或内存传递给主设备,用于指示数据传输的就绪状态。当HREADY为高电平时,表示目标设备或内存已准备好进行下一次传输;当HREADY为低电平时,表示目标设备或内存暂时不可用,主设备应等待。

在这里插入图片描述

  • 在写传输中,主设备通过将目标地址和数据写入对应的信号(HADDR和HRDATA)来执行写入操作,并首先将HWRITE设置为高电平。目标设备或内存接收到写入请求后,进行相应的写入操作,并通过HREADY信号指示就绪状态。

  • 在读传输中,主设备通过将目标地址写入HADDR,并将HWRITE设置为低电平,发起读取操作。目标设备或内存根据目标地址,读取相应的数据,并将数据通过HRDATA传递给主设备。同时,目标设备或内存使用HREADY信号指示就绪状态。

这些信号在AHB-Lite协议中起着关键的作用,确保了数据传输的正确性、同步性和可靠性。

ARM® AMBA® 5 AHB 协议规范

1. 片选信号HSEL_SIGNALS

在这里插入图片描述

  要去理解总线上设备切换的原理:第一步,可以看出这根AHB-Lite总线上共挂载了4个外设:ROM,RAM,Keyboard Interface, Display Interface。这些信号由ARM Cortex-M0核每条指令执行时所输出的HADDR选择,内存映射划分的区域,如果访问的地址HADDR在这个范围之内就说明像选中的是这个子模块,为这个子模块(HSEL_SIGNALS的第几个bit)拉高。片选到了哪个子模块,哪个子模块的HREADY拉起,

  注:ARM M0核是冯诺依曼结构的,也就是他的数据和指令是在同一片存储上,即指令和数据统一寻址,取指令和取数据都通过AHB-Lite总线,这样效率不够高。相反,哈佛结构把指令通路和数据通路分开,效率很明显更高,学的picoMIPS就是哈佛架构。

  always_comb
    if ( HADDR < 32'h2000_0000 )
      HSEL_SIGNALS = 1 << 0;
    else if ( HADDR < 32'h4000_0000 )
      HSEL_SIGNALS = 1 << 1;
    else if ( HADDR < 32'h5000_0000 )
      HSEL_SIGNALS = 1 << 2;
    else if ( HADDR < 32'h6000_0000 )
      HSEL_SIGNALS = 1 << 3;
    else
      HSEL_SIGNALS = 1 << 4;

  从AHB-Lite Interconnect出来的HSEL_SIGNALS信号,第几个bit,连接到顶层对slave子模块进行片选

// ahb_interconnect.sv
  
// AHB interconnect including address decoder, register and multiplexer
  ahb_interconnect interconnect_1 (
    .HCLK, .HRESETn, .HADDR, .HRDATA, .HREADY,
    .HSEL_SIGNALS({HSEL_OPORT,HSEL_IPORT,HSEL_RAM,HSEL_ROM}),
    .HRDATA_SIGNALS({HRDATA_OPORT,HRDATA_IPORT,HRDATA_RAM,HRDATA_ROM}),
    .HREADYOUT_SIGNALS({HREADYOUT_OPORT,HREADYOUT_IPORT,HREADYOUT_RAM,HREADYOUT_ROM})
  );
// AHBLite Slaves   
  ahb_rom rom_1 (
    .HCLK, .HRESETn, .HADDR, .HWDATA, .HSIZE, .HTRANS, .HWRITE, .HREADY,
    .HSEL(HSEL_ROM),
    .HRDATA(HRDATA_ROM), .HREADYOUT(HREADYOUT_ROM)
  );

2. 地址信号HADDR

   HADDR 是地址总线,通常用于携带当前读取或写入操作的内存地址。其宽度由系统地址总线的宽度决定。在一个 AHB-Lite 总线事务中,主设备(Master)通过 HADDR 信号将目标地址传递给从设备(Slave)。详细见前文介绍memory mapped I/O

控制信号write_enable 和 read_enable 和 word_address

   对于一个在总线上可读可写的子模块里,要生成是否可读的信号read_enable,和是否可写的信号write_ebale。这两个信号存在于子模块的内部。同时提取HADDR的[4:2]作为子模块中IO寄存器的选择信号word_address。

  • 对于只读或只写的文件,相应地没有read_enable 和write_enable 。
在所有的子模块.sv里
//Generate the control signals in the address phase
  always_ff @(posedge HCLK, negedge HRESETn)
    if ( ! HRESETn )
      begin
        read_enable <= '0;
        word_address <= '0;
        write_enable <= '0;
      end
    else if ( HREADY && HSEL && (HTRANS != No_Transfer) )
      begin
        write_enable <= HWRITE;
        read_enable <= ! HWRITE;
        word_address <= HADDR[4:2];
     end
    else
      begin
        write_enable <= '0;
        read_enable <= '0;
        word_address <= '0;
     end

3. 读数据HRDATA

  从主模块中期待读取到子模块的信号,从slave将HRDATA发到master。

子模块利用word_address产生HRDATA

  // read
  always_comb
    if ( ! read_enable )
      // (output of zero when not enabled for read is not necessary
      //  but may help with debugging)
      HRDATA = '0;
    else
      case (word_address)
        0 : HRDATA = counter_0;//nMode
        1 : HRDATA = counter_1;//nTrip
	    2 : HRDATA = counter_2;//flag
        3 : HRDATA = Status;  
        // unused address - returns zero
        default : HRDATA = '0;
      endcase

interconnect里对信号进行选择

  从各个子模块中可以传到interconnect里,共几个子模块就有几个HRDATA,需要进行选择。

  input [num_slaves-1:0][31:0] HRDATA_SIGNALS,   

  片选信号参与选择哪个子模块发送过来的信号是HRDATA,从而输出到Contex-M0核中

always_ff @(posedge HCLK, negedge HRESETn)
    if( ! HRESETn )
      mux_sel <= '0;
    else if( HREADY )
      mux_sel <= HSEL_SIGNALS;

  always_comb
    begin
      // default values
      HREADY = 1;
      HRDATA = 32'hDEADBEEF; // "hexspeak" to indicate an error has occured
      
      // since num_slaves is a parameter all of this should be unrolled at compile time
      
      for ( i = 0; i < num_slaves; i++ )
        if ( mux_sel == (1 << i) )
          begin
            HREADY = HREADYOUT_SIGNALS[i];
            HRDATA = HRDATA_SIGNALS[i];
          end

    end

  这里需要注意的是:

  • HREADYOUT控制HRDATA的读出数据周期。高:Slave输出传输结束;低:Slave需延长传输周期。在本设计里,一个周期可以完成读出数据的操作,所以所有的HREADYOUT都置1。
  assign HREADYOUT = '1; //Single cycle Write & Read. Zero Wait state operations

子模块中 status 寄存器

  • 子模块中的status寄存器存放着DataValid信号。要想在读取时实现握手,需要先检查子模块的数据是否已经准备好,即检查DataValid。
  • 在C语言中调用check_CUSTOM_status()函数来检查这个子模块内部的status寄存器。
  // Storage for status bits 
  logic [2:0] DataValid;
  logic [2:0] nextDataValid;
  
  //Act on control signals in the data phase
  // define the bits in the status register
  assign Status = { 29'd0, DataValid};
  • 信号输入成功,计数便加一,同时把datavalid拉起来
  • read_enable为1表示正在读取这个寄存器,把DataValid置0不让读。
//nMode
        if ( 信号输入成功)
          begin
            counter_0 <= counter_0 + 1;
            DataValid[0] <= 1;
          end
       else if ( read_enable && ( word_address == 0 ) )
          begin
            DataValid[0] <= 0;
          end

4. 写入数据HWDATA

  • HWDATA没有经过interconnect模块,直接连接到各个子模块里;

  • 控制信号write_enable为高时才可写;

可写的子模块中
        if ( write_enable && (word_address==0))
          begin
            Mode_counter <= HWDATA;
            Mode_write_DataValid <= nextDataValid[0];
            nextDataValid[0]<= 0;
         end
        else if( write_enable && (word_address==3))begin   //status寄存器
            nextDataValid[0] <= HWDATA[0];
            Mode_write_DataValid <= 0;
            end

  片选成功的信号进入子模块,当片选信号拉起,同时也将这个HADDR也传入到子模块里,用于确定子模块中IO寄存器。

//slave module:ahb_CUSTOM.sv
    else if ( HREADY && HSEL && (HTRANS != No_Transfer) )
      begin
        write_enable <= HWRITE;
        read_enable <= ! HWRITE;
        word_address <= HADDR[4:2];
     end

  这里需要注意的是,HREADY拉高、HSEL拉高的时候才能写入值。还记得上面C代码中如果想往IO寄存器中写入数据,需要先往STATUS寄存器中写入1

5. 读写控制HWRITE

  HWRITE是一个在读写控制中使用的信号,用于指示当前传输是否是写操作。

  HWRITE则用来区分是读取操作还是写入操作。如果HWRITE为高电平,表示这是一个写操作;如果为低电平,表示这是一个读操作。

在这里插入图片描述

  • 如果HREADY为高电平而HWRITE为低电平,那么在下一个时钟周期,接口可以开始新的读操作。
  • 如果HREADY为高电平HWRITE为高电平,那么下一个时钟周期总线将进行写入操作。
  • 如果HREADY为低电平而HWRITE为高电平,这表示当前正在进行一个写操作,但操作还未完成。

  ps,在读取IO寄存器之前应该先检查Status寄存器中对应bit的值是否为1

  //Generate the control signals in the address phase
  always_ff @(posedge HCLK, negedge HRESETn)
    if ( ! HRESETn )
      begin
        read_enable <= '0;
      end
    else if ( HREADY && HSEL && (HTRANS != No_Transfer) )
      begin
        read_enable <= ! HWRITE;
     end
    else
      begin
        read_enable <= '0;
     end

6. HREADY

  HREADY 是一个握手信号,用于指示当前事务是否已经结束。如果HREADY为高电平,那么在下一个时钟周期,接口可以开始新的事务。如果为低电平,那么它表明当前事务还没有结束,接口需要在下一个时钟周期继续进行同一个事务。

  interconnect模块中传出HREADY,HREADY信号用于指示数据传输的就绪状态,确保数据的有效传输和协议的正确执行。

  always_comb
    begin
      // default values
      HREADY = 1;
      HRDATA = 32'hDEADBEEF; // "hexspeak" to indicate an error has occured
      
      // since num_slaves is a parameter all of this should be unrolled at compile time
      
      for ( i = 0; i < num_slaves; i++ )
        if ( mux_sel == (1 << i) )
          begin
            HREADY = HREADYOUT_SIGNALS[i];
            HRDATA = HRDATA_SIGNALS[i];
          end

    end

7. 数据有效信号:DataValid和NextDataValid用于减少由于异步更新带来的可能问题

  这个是iain设计的一种机制,当datavalid为高电平,表示数据已经准备好并且是有效的;为低电平,则表示数据还未准备好或者无效。

  在这个SoC中代码片段,DataValidNextDataValid信号看起来是用来控制和同步数据输出的。根据给出的注释,我的理解是这样的:

  1. DataValid: 这个信号表明DataOut寄存器当前的数据是否有效。当DataValid为1时,意味着DataOut的数据是有效的,可以被其他设备或模块使用。当DataValid为0时,意味着DataOut的数据是无效的,不应被其他设备或模块使用。
  2. NextDataValid: 这个信号预设了下一次DataOut更新后的DataValid状态。也就是说,当你写入DataOut寄存器时,DataValid将被设置为NextDataValid的当前值。这样做可以在DataOut数据更新的同时更新其有效性状态,减少了由于异步更新带来的可能问题。

  关于使用这两个寄存器的顺序,注释中也给出了建议:首先更新NextDataValid寄存器,然后再更新DataOut寄存器。这样做可以确保当数据被写入DataOut寄存器时,DataValid立即得到正确的更新。

// Address map :
//   Base addess + 0 : 
//     Read DataOut register
//     Write DataOut register, Copy NextDataValid to DataValid
//   Base addess + 4 : 
//     Read Status register
//     Write NextDataValid register
//
// Bits within status register :
//   Bit 1   NextDataValid
//   Bit 0   DataValid


// In order to update the output, the software should update the NextDataValid
// register followed by the DataOut register.


  //Act on control signals in the data phase

  // write
  always_ff @(posedge HCLK, negedge HRESETn)
    if ( ! HRESETn )
      begin
        DataOut <= '0;
        DataValid <= '0;
        NextDataValid <= '0;
      end
    else if ( write_enable && (word_address==0))
      begin
        DataOut <= HWDATA;
        DataValid <= NextDataValid;

        // this is not synthesized but provides useful debugging information
        if ( NextDataValid )
          $display( "DataOut: ", HWDATA, " @", $time );
        else
          $display( "DataOut:--Invalid-- @", $time );

     end
    else if ( write_enable && (word_address==1))
      begin
        NextDataValid <= HWDATA[0];
     end
     
  // define the bits in the status register
  assign Status = { 30'd0, NextDataValid, DataValid};

  在给出的硬件描述语言(HDL)代码中,你可以看到这个行为的实现。当执行写入操作且地址指向DataOut寄存器(word_address==0)时,DataOut被设置为输入数据(HWDATA),DataValid被设置为NextDataValid的当前值。当地址指向NextDataValid寄存器(word_address==1)时,NextDataValid被设置为输入数据的最低位(HWDATA[0])。注意这里假设NextDataValid只需要1位数据。

  另外,Status寄存器的最低两位被定义为NextDataValidDataValid,以方便读取它们的状态。

为什么这样可以减少由于异步更新带来的可能问题

  考虑一种情况,即先更新DataOut,然后再单独更新DataValid。在这种情况下,可能存在一个短暂的时期,数据已经被更新,但DataValid标志还没有被更新。在这个短暂的时期内,其他设备或模块可能会错误地认为DataOut的新数据是无效的,或者错误地使用旧数据,因为DataValid标志还没有反映出数据的新状态。

  为了避免这个问题,我们可以使用NextDataValid来预先设定DataOut更新后的DataValid状态。这样,当我们写入DataOut时,DataValid也会立即更新,确保DataValid始终与DataOut的状态同步。这就避免了由于DataOutDataValid之间的异步更新而可能产生的问题。

iain - ARM System on Chip (ASIC version)

  ps,我们最后在FPGA板级验证的时候报了个错,只按一下trip清零清不了distance,只有前轮再转一圈才能清零。我们trip清零写在了sv代码里,所以底层的寄存器肯定清零了。但是有可能是因为trip的数据已经更新了,但是负责LED的寄存器没有清空,他错误地使用了旧的LED的数据,因为LED显示的数据是在c语言里先从寄存器读出来再写进去,说明没有读到清零的值。

  ps,Iain在他的网页introductory里放的是nonspecific interface 让你可以学习,我们要自己改成custom interface。

五、后记

  在第一次尝试做SoC设计的时候,我们组精准地踩了很多坑,一脸懵逼、次次被Iain劝退。但是感谢团队所有人,我们最终完成了非常好的SoC设计。

第一次被怼:Draft Design Proposal feedback: “It’s all wrong. Completely Wrong(^ - ^)…And you’re saying how to correct this, and What I’m saying is, DON’T DO THIS!.. It’s looks like you haven’t understood the system on chip concept”.

第二次被怼:Design Proposal feedback: “He won’t necessarily spot that he’s writing rubbish…that makes no sense…Given the structure suggested in figure one, I think you might be better of building a hardware only design…”.

(”TOTALLY IN WRONG DIRECTION“——Iain)。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐