简易CPU设计入门:本系统中的通用寄存器(五)

项目代码下载

请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。

下载本项目代码

准备好了项目源代码以后,我们接着去讲解。

本节前言

上一节,我主要是讲解了寄存器写操作。本节,我开始来讲解寄存器读操作。能否写完,这不好说。上一节的字数实在是多。写完以后,我是觉得挺耗费时间的。

本节代码文件,主要是指位于【......cpu_me01\code\Ctrl_Center\】路径里面的【register_element.v】。

一. 寄存器读使能信号

图1,控制中心模块的部分注释代码

复制代码
/*********************************************
ctrl_sig_inner[0]:register write enable:寄存器写使能
ctrl_sig_inner[1]:register read enable:寄存器读使能
ctrl_sig_inner[2]:random memory write ebable:内存写使能
ctrl_sig_inner[3]:random memory read enable:内存读使能
ctrl_sig_inner[4]:Arithmetic and Logic calculate:算术逻辑运算
ctrl_sig_inner[5]:reserve:保留
ctrl_sig_inner[6]:reserve:保留
ctrl_sig_inner[7]:reserve:保留
ctrl_sig_inner[8]:reserve:保留
ctrl_sig_inner[9]:reserve:保留
ctrl_sig_inner[10]:reserve:保留
ctrl_sig_inner[11]:reserve:保留
ctrl_sig_inner[12]:reserve:保留
ctrl_sig_inner[13]:reserve:保留
ctrl_sig_inner[14]:reserve:保留
ctrl_sig_inner[15]:reserve:保留
还有一种运算叫做读取立即数,将立即数放入内部寄存器。
此运算不需要通过内部信号的参与。
************************************************/

在图1中,我们主要看行号39所示的注释代码。也就是下面的东西。

复制代码
ctrl_sig_inner[1]:register read enable:寄存器读使能

这一行代码的含义是,当内部控制信号总线的位1为1的时候,它是代表着寄存器读使能信号。也就是,控制中心要从某一个通用寄存器里面读取数据了。

二. 变量介绍

我们来看一看寄存器元素模块的代码。
图2

在图2里面,我是展示了两处红色框线。这两处红色框线所示的变量,就是我们本节所要关注的变量了。注意,上一节所用的【work_ok_represent】,我们本节依然是要使用的。因为,寄存器读操作完成了以后,我们还是需要向控制中心传递工作完成信号。

上一节,我们是没有使用19行的【data_sig_represent】的,因为上一节,我们不需要使用它。。而在本节,可就必须要去使用了。具体用法,下面会有讲解。

三个【get_time】组的节拍变量,使我们本节的特色。由于本节不涉及写操作,所以呢,本节,我们不使用【write_time】组的节拍变量。

而reg_id,这个肯定是要使用的。寄存器写操作需要指定写入哪个寄存器,所以需要指定寄存器id。既然需要寄存器id号这个参数,所以呢,我们就需要去使用reg_id这个变量了。写操作时,需要有寄存器id这个参数的参与,在读操作时,我们同样需要指定寄存器id号。不然的话,一共有8个通用寄存器呢,你说说,我们到底是读取哪个通用寄存器的数据呢?

所以,无论是读寄存器的操作还是写寄存器的操作,reg_id这个变量,都是需要去使用的。

三. get_time变量的逻辑

我们来看一下get_time变量的代码逻辑。
图3

截图上的代码字体还是显得小。我还是将代码放在下面的代码块中。

复制代码
always @(posedge sys_clk or negedge sys_rst_n)
	if (sys_rst_n == 1'b0)
		get_time <= 1'b0;
	else if (ctrl_sig_inner[1] == 1'b1 && addr_sig_inner[3:0] == reg_id)
		get_time <= 1'b1;
	else
		get_time <= 1'b0;

从图3与代码块可以看到,get_time的基本逻辑就是,默认状态之下,它是0值。只有在满足了某某条件之后,它才会是1值。而那个条件,【ctrl_sig_inner[1] == 1'b1 && addr_sig_inner[3:0] == reg_id】,它其实是由控制中心模块设置的。这个条件的满足,只维持一个时钟周期。也就是说,get_time,在平常的情况下,都是0值。只有在某某条件满足了以后,才维持着一个时钟周期的高电平。

接下来呢,我们来看一看这个条件。

ctrl_sig_inner[1] == 1'b1 && addr_sig_inner[3:0] == reg_id

这个条件的右半部分,它是在判断,内部地址信号总线【addr_sig_inner】的位3到位0的值与本模块实例的 寄存器 id 号是否相等。

条件的坐班部分,它是在判断内部控制信号总线【ctrl_sig_inner】的位1是否为1。内部控制喜好总线若是为1,它就表示着开启了寄存器读使能信号,表示将要进行寄存器读使能操作。

所以呢,这个条件表达式的意思是,如果控制中心通过在内部控制信号总线【ctrl_sig_inner】的位1写入1,其它位写0,而开启了寄存器读使能信号,并且,在内部地址信号总线上指定了8个通用寄存器的其中一个寄存器的id号,则要进行满足此条件的相关操作。

要执行的相关操作是什么呢?

就是【get_time <= 1'b1;】

这样一来,【get_time】的逻辑,我们就讲完了。我们再来看一看【get_time_d1】和【get_time_d2】的代码逻辑。
图4

在我们的这个代码文件里,你可以找到图4所示的,关于【get_time_d1】的代码,但是你找不到【get_time_d2】的代码。实际上,我这里,根本就没有写关于【get_time_d2】的代码。

这是因为,当初在申请变量的时候,我担心变量不够用,所以,申请了【get_time_d1】和【get_time_d2】两个延时变量,而实际上却只用了一个。我个人,其实也是这种习惯。总担心资源不够用,所以,在准备阶段,总是希望多准备一些。

我自己在做菜和买菜的时候,也总是担心菜不够吃,所以呢,我倾向于多买些菜。结果呢,菜做出来,就容易做多了。得吃个三四天才能吃完。结果呢,第一顿还行,到了后面,菜热的次数多了,里面的东西都坨了,我自己也变得缺乏食欲了。但是呢,扔了还觉得可惜。

炖鸡肉,炖鱼,都是这样的。

我的老家是 在吉林省,东北有一个家常菜,叫做大拌凉菜,有的呢,也叫做家常凉菜。我自己去拌凉菜的时候,也常常是说,拌完了以后,就是一大盆子凉菜。不过呢,凉菜还好,凉菜比较下饭。一大盆子凉菜,我吃了三四天以后,假如还有的话,我还是会觉得吃得过瘾。

有空的话呢,大家可以学习着去做东北家常凉菜,或者是,亲自去东北菜馆品尝一下东北家常凉菜。当然了,最好是东北人做的家常凉菜,你可以从中体会到那种,特别地爽口,特别地下饭的那种家常凉菜。

我们接着来讲解。

从图4来看,大家也能够体会到,【get_timne_d1】,其实就是对【get_time】延时一个时钟周期,所形成的东西。所以呢,【get_timne_d1】与【get_timne】,主要的作用,就是用来计时,用来打拍子。

我们接着往下看。

四. data_sig_represent 的代码逻辑

图5

图5讲解了【data_sig_represent】的代码逻辑。默认的情况下,它是被赋值为高阻抗z值。当【get_time】为1的时候,它被赋值为0值。而在延后一个时钟周期的【get_time_d1】为1的时候,【data_sig_represent】被赋值为data_reg的值。

在这里,data_reg,它是本代码文件的同名模块【register_element】的输出端口变量,如下图所示。
图6

这个data_reg变量,我们在上一节讲过了,它是在寄存器写操作中,被写入数据的。在系统复位时,它是0值。这个变量的主要用途,就是用来保存数据。data_reg中的数是多少,则该通用寄存器中的数就是多少。

可以说,data_reg,它是通用寄存器的核心变量。

寄存器读操作,它的主要的目的,就是让某一个通用寄存器的模块内部的【data_reg】变量的值,传递到内部数据信号总线【data_sig_inner】中。

在图6的第7行中,我们看到,【data_sig_inner】是一个【inout】端口类型的【wire】型变量,它本身不能用于时序逻辑之中。我们为【data_sig_inner】设置了一个时序逻辑代理,如下图所示。
图7

在图7中,在95行的位置上,我们看到,【data_sig_inner】与【data_sig_represent】绑定在了一起。这样一来,在图5里面,当【get_time】为1,从而【data_sig_represent】变量被赋值为0值的时候,【data_sig_inner】也跟着【data_sig_represent】同时变为了0值。

同理,在图5里面,在延后一个时钟周期的【get_time_d1】为1,从而【data_sig_represent】被赋值为【data_reg】的值的时候,【data_sig_inner】也跟着【data_sig_represent】同时变为了【data_reg】的值。

所以呢,图5所示的代码逻辑,本质上,其实是对【data_sig_inner】的赋值。平时的时候,【data_sig_inner】被赋值为高阻抗z值。当【get_time】为1的时候,【data_sig_inner】通过代理变量【data_sig_represent】被非阻塞赋值为0值。当【get_time_d1】为1的时候,【data_sig_inner】通过代理变量【data_sig_represent】被非阻塞赋值为【data_reg】的值。

五. 寄存器读操作的总线逻辑

我们想要读取通用寄存器的值,我们的目的,就在于通过代理变量【data_sig_represent】,将某一个通用寄存器的模块内部的【data_reg】变量的值,传递给【data_sig_inner】变量。

而在本篇文章的第四分节的讲述,我们可以看到,我们是先给【data_sig_represent】赋值为0,再赋值为【data_reg】变量的值。为啥不是直接将【data_reg】变量的值赋值给【data_sig_represent】呢?

这一点,其实,我们在寄存器写操作里面,也就是在上一节,我们已经是讲过了。

在这里,【data_sig_inner】变量是一个输入输出类型的变量,同时,它也是一种总线类型的变量。

寄存器元素模块有【data_sig_inner】成员,而8个通用寄存器是有寄存器元素模块实例化得来,所以,8个通用寄存器都会存在着【data_sig_inner】成员。而在控制中心里面,同样是存在着【data_sig_inner】成员,这些个不同的模块,它们都存在着同名的【data_sig_inner】成员,因此,【data_sig_inner】属于是一个总线。

对于总线,它的一般的逻辑是,平时的时候,所有的与之有物理连接的模块,均与之在逻辑上断开连接。这个时候,总线的状态,为高阻抗状态,其值为高阻抗z值。

而在有事的时候,则是会有一个一个模块与总线建立连接,向总线写入数值。

在寄存器读操作里面,控制中心会通过内部控制信号总线发布寄存器读使能信号,此时,所有的通用寄存器都会收到寄存器读使能信号。然而,在发布寄存器读使能信号的时候,控制中心还会在内部地址信号总线上指定寄存器id值。

在8个通用寄存器里面,仅有一个通用寄存器的 reg_id 变量的值,等于内部地址信号总线上指定的寄存器 id号,所以呢,只有与指定寄存器id号相等的寄存器,它的get_time与get_time_d1才会相继变为1,它的data_sig_inner才会被先后赋值为0值与data_reg的值。

而其余的,与内部地址信号总线上指定的id号不等的通用寄存器,它们的get_time与get_time_d1始终都会是0值,因而它们的data_sig_represent 与 data_sig_inner 始终都会是高阻抗z值,因而它们在寄存器读操作的微操作周期中,始终都是与【data_sig_inner】总线断开连接的。

在寄存器读操作里面,8个通用寄存器,与内部地址信号总线上指定的寄存器id号不等的7个通用寄存器,它们始终与【data_sig_inner】总线处于断开连接的状态。而只有与指定的寄存器id号相等的寄存器,它的模块内部的【get_time】与【get_time_d1】才会相继变为1,因而,它的模块内部的输入输出端口类型的【data_sig_inner】才会通过代理变量【data_sig_represent】被先后赋值为0值和data_reg的值。

当指定的id号的通用寄存器,向其内部的【data_sig_inner】变量赋值一个非高阻抗的值的时候,这个通用寄存器就与总线【data_sig_inner】建立了连接。

8个通用寄存器里面,有7个寄存器与【data_sig_inner】总线断开连接,只有与指定的id号相等的寄存器,才与【data_sig_inner】总线建立了连接。这个时候,建立了连接的这个通用寄存器,它向它自己模块内部的【data_sig_inner】变量写入了什么值,则【data_sig_inner】总线就等于什么值。

当响应了寄存器读操作的通用寄存器的模块内部的【get_time】为1的时候,这个寄存器的模块内部的【data_sig_inner】变量就通过代理变量【data_sig_represent】被非阻塞赋值为0值。而此时,这个通用寄存器与【data_sig_inner】总线建立了连接,【data_sig_inner】总线的值等于这个通用寄存器的模块内部的【data_sig_inner】变量的值,也变为了0值。是从高阻抗值z变为0值的。

当响应了寄存器读操作的通用寄存器的模块内部的【get_time_d1】为1的时候,这个通用寄存器的模块内部的【data_sig_inner】变量就通过代理变量的非阻塞赋值的方式,被赋值为了data_reg的值了。而此时,这个通用寄存器与【data_sig_inner】总线建立了连接,【data_sig_inner】总线的值等于这个通用寄存器的模块内部的【data_sig_inner】变量的值,也变为了【data_reg】的值。这是从之前的0值,变为现在的【data_reg】的值的。

这样一来,通过向通用寄存器的模块内部的【data_sig_represent】变量赋值,就实现了给模块内部的【data_sig_inner】变量赋值,进而实现了给【data_sig_inner】总线赋值。

值既然都传递给【data_sig_inner】总线了,那么,控制中心模块,也就可以接收到来自指定寄存器的data_reg所保存的值了。

上面提问的问题,我们还没有回答,为啥我们要先给【data_sig_inner】总线传递0值,再传递【data_reg】所保存的值呢?直接传递【data_reg】的值不是更方便直接吗?

我们需要往总线里面传递数据。总线,在通常情况下,与之有物理连接的各个线路,在逻辑上都与它处于断开连接的状态。这个时候,总线是处于高阻态的。我们想要给一个本来处于高阻态的总线,传递一个有效值,那么,我们需要在传递有效值之前,先传递一个无意义值。在本节,我们给【data_sig_inner】总线传递的0值,就属于无意义值。如果直接给【data_sig_inner】总线传递有效值,那么,【data_sig_inner】总线从高阻态直接变化为有效值,则接收方在时钟上升沿的时候,很可能检测不到这个有效值。而如果我们在给【data_sig_inner】总线传递有效值之前,先给【data_sig_inner】总线传递一个无意义的非高阻抗值,那么,【data_sig_inner】总线就可以接收到有效值了。

当然了,我们先给【data_sig_inner】总线传递无意义值0,再给【data_sig_inner】总线传递有效的来自【data_reg】的值,则中间的无意义值0,接收方依然是检测不到它的。但是,这个无意义值0,无论能否检测到,我们都不关心。我们所关心的是,要确保接收方,也就是控制中心模块,能够确定地检测到有效值,检测到来自【data_reg】的值。

涉及总线逻辑的时候,我觉得,讲起来还是挺难的。不知道,本节的讲解,大家听的怎么样。对于总线逻辑,我建议大家多参考上一节的讲解。上一节内容的链接如下。

简易CPU设计入门:本系统中的通用寄存器(四)-CSDN博客

六. 通过 work_ok_inner 变量传递完成信号

我们来看代码截图。
图8

在图8里面,我们本节需要重点关注的,是红色框线所示的部分。从图8的红色框线部分可以看到,对于寄存器读操作来讲,平时的时候,work_ok_represent 都是处于高阻态的。当【get_time】为1的时候,【work_ok_represent 】被非阻塞赋值为0。当【get_time_d1】为1的时候,【work_ok_represent 】被非阻塞赋值为1。

通过下图所示的代码,我们可以知道,【work_ok_represent】变量是【work_ok_inner】的时序逻辑代理。
图9

也就是,当【get_time】为1的时候,【work_ok_inner】通过代理变量【work_ok_represent 】被非阻塞赋值为0。当【get_time_d1】为1的时候,【work_ok_inner】通过代理变量【work_ok_represent 】被非阻塞赋值为1。

【work_ok_inner】变量,通过上一节的学习,我们已经知道,它属于是一个总线类型的变量。8个通用寄存器的模块内部都有这个变量,控制中心里面也有。8个通用寄存器与控制中心,共享【work_ok_inner】总线。

这样一来,当初控制中心在内部地址信号总线上指定寄存器 id 号时,与这个id号相等的寄存器,当这个指定的寄存器通过模块内部的【work_ok_represent 】变量给模块内部的【work_ok_inner】变量先后赋值为0和1的时候,【work_ok_inner】总线也就相继变为了0值和1值。

当【work_ok_inner】总线变为1值的时候,控制中心就可以检测到这个1值了。当控制中心检测到【work_ok_inner】总线为1的时候,就证明,寄存器读操作完成了。

所以呢,各位,我们本节的讲解,差不多就该结束了。

结束语

这两节的篇幅,我认为是比较长的。我个人在讲解的时候,我也觉得,讲得挺费劲的。

但愿大家能够学好这两节的内容吧。

祝大家学习愉快。

相关推荐
芯片和软件研究所17 小时前
【tinyGTC】北斗授时授频 GPSDO 驯服钟的PPS和10M时钟测量
单片机·嵌入式硬件·北斗·时间同步·时频技术·授时·信号测量
Escene202118 小时前
Realtek HoneyGUI (1)
单片机·嵌入式硬件·物联网
做一个快乐的小傻瓜18 小时前
XCKU5P引脚约束
fpga开发
开压路机18 小时前
进程控制
linux·服务器
香蕉鼠片18 小时前
跨平台开发到底是什么
linux·windows·macos
波特率11520019 小时前
FreeRTOS当中的Mail Queue使用教程(CMSIS_v1)
单片机·操作系统·freertos
潜创微科技20 小时前
4K 转 MIPI 硬核方案|ITE IT6616 HDMI1.4 转 MIPI CSI/DSI 转换芯片解析
嵌入式硬件·音视频
bukeyiwanshui20 小时前
20260417 DNS实验
linux
代码中介商20 小时前
Linux 帮助手册与用户管理完全指南
linux·运维·服务器
三佛科技-1341638421221 小时前
FT32F103系列与APM32F103,STM32F103之间的对比,能否替换?
单片机·嵌入式硬件·物联网·智能家居·pcb工艺