HDL4SE:软件工程师学习Verilog语言
Posted 饶先宏
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言相关的知识,希望对你有一定的参考价值。
8 用c语言写自定义基本单元
本节本来是要继续学习verilog语言的,但是软件开发进度条太沉重了,怎么推也推不动,只能慢慢磨了。没有软件的配合,学习效果就差了一些,因此把后面的内容提到前面来。当然也是因为沉迷于编程及游戏,资料都没有心思写了。
本节还是通过一个例子,来说明如何在HDL4SE中用c语言写自己的基本单元库,然后让verilog来调用。基本的想法是,用c语言写一个LCOM对象,实现基本单元所需要的IHDL4SEUnit, IHDL4SEDetector等接口,当然顺便实现IDList接口以便它能加入到双向链表中。用verilog描述它的模型,然后就可以包含在其他的verilog代码中通过实例化使用了。
这种基本单元可大可小,可以是一个小的运算单元,比如一个深度神经网络中用的基本乘累加算子,可以是一个比较大的单元,比如干脆就是一个RISC-V的核。我们这个例子是要实现一个俄罗斯方块游戏,基本单元就是游戏机的控制器。当然游戏机的显示器也可以看成是一个基本单元,作为设备挂在HDL4SE模拟器上,其实也是一个基本单元,只是没有用verilog代码直接调用罢了。此时verilog代码就很简单了,就是把控制器与模拟环境连接在一起,具体方式看后面的说明。
你可能会觉得有点投机取巧,用C语言来写个控制器,然后游戏就跑起来了,其中verilog代码几乎就没有了,仅仅是把游戏控制器和游戏面板连接起来。然而这正是做HDL4SE项目的初衷。HDL4SE可以支持用C语言来写数字电路基本单元模型,有几个理由:
- 在系统设计初期,很多模块都没有确定如何设计,此时就写verilog代码是不合适的,但是用c语言写一个原型系统,相对就轻松多了。因此才开始可以用c语言参与搭建系统,可以视为CModel。毕竟写c还是要比写verilog 的RTL描述要容易很多。
- 随着系统的概要设计迭代次数增加,逐步稳定,一部分模块可以用verilog来写了,此时将c语言模型和verilog编写的电路联合在一起模拟,可以检验verilog实现的正确性,而且能极大地加快仿真速度。有些部分甚至只是辅助开发的模块,其实最终都不需要写成RTL代码。
- 很多不适于用RTL编写的测试输入可以用c语言来写,这样可以简化系统复杂度,比如想做个深度神经网络识别的verilog代码,要从JPG文件中直接读图片数据,就可以用c语言写一个JPEG文件解码模块挂在系统中模拟,否则真用verilog RTL来写,好难啊,这里就不堪细说了,做过的都知道。即使写出来了,这个模块运行起来也快不起来,占用了很多仿真时间。一般FPGA开发或ASIC开发项目只好用软件将JPEG文件解码后转换成verilog能直接读的文本文件,然后用verilog代码来读,其实已经很不方便了。
- 可以在仿真过程中增加一些交互的内容,比如做GPU开发,可以马上看到绘图结果,做信号处理,直接看到信号处理结果(比如直接挂个FFT分析模块在系统中,并能动态显示频谱分布),做游戏,至少要能玩起来。这个比用纯的verilog RTL编玩后仿真,然后再去分析输出信号波形图或者输出文件,要爽很多。当然直接拉低了做数字电路那帮人的逼格。软件工程师从此包打天下,数字电路?软件开发而已。
- 可以将设备驱动程序软件和硬件一起模拟,让驱动程序软件在早期就介入系统设计,减少软硬设计之间的摩擦,这个就不多说了,做过类似项目的都知道其中的好处。
我们还是从一个俄罗斯方块游戏例子开始,然后介绍其中的基本单元库的c语言实现,最后用verilog代码把它们在HDL4SE模拟器中连接起来,构成一个实际能玩的游戏。后面会随着学习深入,编译器软件的完善,逐步将c语言实现的部分替换成verilog实现,最终形成一个用verilog写的俄罗斯方块游戏。烧在FPGA里,可以做成一个游戏机呢,做成专用的ASIC芯片,可以用来造游戏机。
8.1 俄罗斯方块游戏
俄罗斯方块游戏看上去是这个样子的:
当然你可能玩过的有些不同,不过基本上还是一样的,左边是游戏的显示面板,一组形状不同的方块从上面不断掉落下来,其中黑色的是空的,带颜色的是实心的,如果它落到下面有一个实心的块被面板中已经有的实心块或者面板的底部挡住,此时这组方块就变成面板的一部分,你可以通过左右键控制方块组左右运动,也可以按旋转键(我们的实现用向上键来实现)来旋转方块,当然如果位置已经调整的不错了,可以用向下键加速方块组下落。面板中的方块如果在水平上构成一个全部是实心方块的行,这一行就会消失,面板中上面的方块都往下移动一行,空出来的地方又可以放方块了。当然由于一组方块最多是4x4的,因此有可能一次最多有四行都变成实心行,同时消失。如果落下来的块没地方放了,那就GAME OVER,我们这里选择自动重新开始。
右边的上方是后面会掉落下一个方块组的结构。下面有三行字,L开始的一行代表总共有多少个实心行消失,P开始的一行表示目前的速度,我们的系统中速度从0–6000,C开始的一行表示得分,我们的计分规则是:
开局200分,左右移动方块组扣一分,旋转方块组扣一分,向下加速一次加两分,消除一个单行加10分,一次消除两行得40分,一次消除三行得160分,一次消除4行得640分。当前的速度就是得分除以10。得分越高,速度越快。得分为零就从头开始,得分超出显示范围就通关。
讲了这么多可能都烦了,其实游戏还是很简单的。假如你想做这么一个游戏的话,其实分两个部分,一个是游戏的面板,一个是游戏的控制模块。游戏面板要能动态显示游戏的内容,同时也接收按键信息送到控制代码,控制模块则来控制游戏的进程,并控制显示的内容。
8.2 显示面板
显示面板提供一个类似于帧存的可写地址区域,我们的显示面板有16x24个方块位置,一个4x4的下一块形状,三个整数表示消除行数,速度和得分。每个方块有16种颜色,因此每个方块用4位表示,三个整数各用一个32位整数表示。这样,显示面板的访问地址定义如下(偏移地址):
偏移地址 | 说明 |
---|---|
0x00 | 只读,表示键盘状态,低四位有效,分别表示键盘上的右,左,下,上四个键的状态, 1表示键按下,0表示没有按下。 |
0x10–0xcf | 每个32位表示8个方块的颜色,总共有16x24个方块,这样占1536位,192个8位字节地址。 |
0xe0,0xe4 | 总共8个字节,64位,存储下一块组(4x4个块)的颜色。 |
0xf0 | 得分 |
0xf4 | 消除的行数 |
0xf8 | 速度 |
按照HDL4SE的模拟器设计,显示面板作为设备挂在模拟器中,跟一般的verilog模块一样,应实现LCOM接口IHDL4SEUnit,可选实现IHDL4SEDetector。
下面就是LCOM的八股文了,软件工程师都比较自由,每个人都想写出自己风格的代码,这在软件工程中简直就是一个灾难,为此LCOM规定了编写代码的规范,包括函数命名,要实现的接口函数的名称,参数名称和类型,返回值,实现一个接口所需要做的若干事务等等,确保每个人实现后能够连接在一起,并且可以相互看得懂代码结构。这里俗称LCOM八股文。
8.2.1 LCOM八股 – 起
定义接口IID和接口,一个类CLSID,显示面板对象没有定义新的接口,因此只有一个CLSID,为方便使用,还定义了一个快速对象生成的函数,当然还定义了面板的大小,24行,每行16个方块。这部分是给外部模块调用的,因此比较规范,只是说明要做个什么对象。
DEFINE_GUID(CLSID_TERRISDEVICE, 0x81de7969, 0xf783, 0x4023, 0xbd, 0xe6, 0xf8, 0x15, 0xf8, 0xd5, 0x9c, 0x5);
DEFINE_GUID(PARAMID_BASEADDR,0x55faed75, 0xefbe, 0x461d, 0x81, 0x99, 0x90, 0x58, 0x88, 0xab, 0x47, 0x93);
IHDL4SEUnit** guiCreate(unsigned int baseaddr, const char* name);
#define XCOUNT 16
#define YCOUNT 24
8.2.2 LCOM八股 – 承
然后定义类实现需要的数据结构以及相关接口的实现定义,把接口定义的函数和数据都声明出来,大部分是LCOM八股的规定动作。这部分描述需要实现的接口,当然也定义一些实现相关的数据,给后面的转留下一些伏笔,算是承上启下的部分吧:
#define WIDTH 600
#define HEIGHT 800
/*
terris panel & keyboard
*/
typedef struct _sTerrisDevice {
OBJECT_HEADER
INTERFACE_DECLARE(IHDL4SEUnit)
HDL4SEUNIT_VARDECLARE
INTERFACE_DECLARE(IHDL4SEDetector)
HDL4SEDETECTOR_VARDECLARE
DLIST_VARDECLARE
IHDL4SEModule** parent;
char* name;
GLFWwindow* window;
int width;
int height;
int count;
unsigned int baseaddr;
unsigned int panelvalue[YCOUNT][XCOUNT]; /* 00000010--000000cc*/
unsigned int nextblock[4][4]; /* 000000e0, 000000e4*/
unsigned int score; /* 000000f0*/
unsigned int level; /* 000000f4*/
unsigned int speed; /* 000000f8*/
unsigned int keypressed; /* 00000000 */
unsigned int portdata[9];
int wRead; /* 上周期的读信号 */
unsigned int bReadAddr; /* 决定当前周期是否响应 */
int wRead_cur;
unsigned int bReadAddr_cur;
/*
从设备的8个输入端口
0.wClk
1.nwReset
2.wWrite
3.bWriteAddr
4.bWriteData
5.bWriteMask
6.wRead
7.bReadAddr
8.bReadData
*/
IHDL4SEUnit** fromunit[9];
int fromindex[9];
}sTerrisDevice;
OBJECT_FUNCDECLARE(terrisdevice, CLSID_TERRISDEVICE);
HDL4SEUNIT_FUNCDECLARE(terrisdevice, CLSID_TERRISDEVICE, sTerrisDevice);
HDL4SEDETECTOR_FUNCDECLARE(terrisdevice, CLSID_TERRISDEVICE, sTerrisDevice);
DLIST_FUNCIMPL(terrisdevice, CLSID_TERRISDEVICE, sTerrisDevice);
OBJECT_FUNCIMPL(terrisdevice, sTerrisDevice, CLSID_TERRISDEVICE);
QUERYINTERFACE_BEGIN(terrisdevice, CLSID_TERRISDEVICE)
QUERYINTERFACE_ITEM(IID_HDL4SEUNIT, IHDL4SEUnit, sTerrisDevice)
QUERYINTERFACE_ITEM(IID_HDL4SEDETECTOR, IHDL4SEDetector, sTerrisDevice)
QUERYINTERFACE_ITEM(IID_DLIST, IDList, sTerrisDevice)
QUERYINTERFACE_END
8.2.3 LCOM八股 – 转
这部分是八股文的核心,开题后,就要实现其中的各个功能,转的核心要点就是要按照开题的要求,逐个去实现每个接口的函数。这部分包括两个小部分,一个是实现LCOM对象的一些通用函数,一转:
static const char* terrisdeviceModuleInfo()
{
return "1.0.0-20210610.0829 Terris Panel";
}
static int keypressed = 0;
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
keypressed = 0;
if (action != GLFW_PRESS)
return;
if (key >= GLFW_KEY_RIGHT && key <= GLFW_KEY_UP) {
key -= GLFW_KEY_RIGHT;
keypressed = (1 << key);
}
}
static int terrisdeviceCreate(const PARAMITEM* pParams, int paramcount, HOBJECT* pObject)
{
sTerrisDevice* pobj;
int i;
pobj = (sTerrisDevice*)malloc(sizeof(sTerrisDevice));
if (pobj == NULL)
return -1;
*pObject = 0;
HDL4SEUNIT_VARINIT(pobj, CLSID_TERRISDEVICE);
INTERFACE_INIT(IHDL4SEUnit, pobj, terrisdevice, hdl4se_unit);
INTERFACE_INIT(IHDL4SEDetector, pobj, terrisdevice, hdl4se_detector);
DLIST_VARINIT(pobj, terrisdevice);
pobj->baseaddr = 0xF0000000;
pobj->name = NULL;
pobj->parent = NULL;
for (i = 0; i < paramcount; i++) {
if (pParams[i].name == PARAMID_HDL4SE_UNIT_NAME) {
if (pobj->name != NULL)
free(pobj->name);
pobj->name = strdup(pParams[i].pvalue);
}
else if (pParams[i].name == PARAMID_BASEADDR) {
pobj->baseaddr = pParams[i].i32value;
}
}
for (i = 0; i < 9; i++) {
pobj->fromunit[i] = NULL;
}
memset(pobj->panelvalue, 0, sizeof(pobj->panelvalue));
pobj->panelvalue[12][7] = 4;
pobj->panelvalue[12][8] = 9;
pobj->panelvalue[12][9] = 3;
pobj->panelvalue[11][9] = 13;
if (!glfwInit())
return -1;
pobj->width = WIDTH;
pobj->height = HEIGHT;
pobj->wRead = 0;
pobj->count = 0;
pobj->wRead_cur = 0;
pobj->window = glfwCreateWindow(WIDTH, HEIGHT, "TERRIS", NULL, NULL);
if (!pobj->window)
{
glfwTerminate();
return -2;
}
pobj->keypressed = 0;
glfwSetWindowAspectRatio(pobj->window, WIDTH, HEIGHT);
glfwSetKeyCallback(pobj->window, key_callback);
glfwMakeContextCurrent(pobj->window);
gladLoadGL(glfwGetProcAddress);
glfwSwapInterval(1);
glfwGetFramebufferSize(pobj->window, &pobj->width, &pobj->height);
/* 返回生成的对象 */
OBJECT_RETURN_GEN(terrisdevice, pobj, pObject, CLSID_TERRISDEVICE);
return EIID_OK;
}
static void terrisdeviceDestroy(HOBJECT object)
{
sTerrisDevice* pobj;
int i;
pobj = (sTerrisDevice*)objectThis(object);
if (pobj->name != NULL)
free(pobj->name);
for (i = 0; i < 9; i++) {
objectRelease(pobj->fromunit[i]);
}
glfwTerminate();
memset(pobj, 0, sizeof(sTerrisDevice));
free(pobj);
}
static int terrisdeviceValid(HOBJECT object)
{
sTerrisDevice* pobj;
pobj = (sTerrisDevice*)objectThis(object);
return 1;
}
第二部分是每个接口函数的实现,再转:
static int terrisdevice_hdl4se_unit_Connect(HOBJECT object, int index, HOBJECT from, int fromindex)
{
sTerrisDevice* pobj;
IHDL4SEUnit** unit = NULL;
pobj = (sTerrisDevice*)objectThis(object);
if (index >= 0 && index < 9) {
if (0 == objectQueryInterface(from, IID_HDL4SEUNIT, (void**)&unit)) {
pobj->fromunit[index] = unit;
pobj->fromindex[index] = fromindex;
return 0;
}
else {
return -2;
}
}
return -1;
}
static int terrisdevice_hdl4se_unit_ConnectPart(HOBJECT object, int index, int start, int width, HOBJECT from, int fromindex)
{
/* 不支持部分连接 */
return 0;
}
#define isMyAddr(addr) ((addr & 0xFFFFFF00) == (pobj->baseaddr & 0xFFFFFF00))
static int terrisdevice_hdl4se_unit_GetValue(HOBJECT object, int index, int width, IBigNumber** value)
{
int i;
int sel;
sTerrisDevice* pobj;
pobj = (sTerrisDevice*)objectThis(object);
if (index != 8) /* 只响应8.ReadData端口 */
return -1;
if (pobj->wRead == 0)
return -2; /* 上周期没有读命令,不响应,高阻状态 */
if (pobj->bReadAddr == 0) {
/* 偏移地址为0,读按键状态 */
objectCall1(value, AssignInt32, pobj->keypressed);
pobj->portdata[7] = pobj->keypressed;
return 0;
}
return -2;
}
static int terrisdevice_hdl4se_unit_ClkTick(HOBJECT object)
{
int i, j;
int reset;
sTerrisDevice* pobj;
IBigNumber** temp = bigintegerCreate(32);
pobj = (sTerrisDevice*)objectThis(object);
pobj->keypressed = keypressed;
for (i = 0; i < 9; i++) {
if (0 == objectCall3(pobj->fromunit[i], GetValue, i, 32, temp)) {
objectCall1(temp, GetInt32, &pobj->portdata[i]);
}
}
objectRelease(temp);
/* 读nwReset信号,看是否复位 */
reset = pobj->portdata[0] & 1;
if (reset == 0) {
pobj->keypressed = 0;
}
/* 读写命令,看是否写 */
if (reset != 0) {
int wWrite = 0;
unsigned int bWriteAddr = 0xFFFFFFFF;
int wRead = 0;
unsigned int bReadAddr = 0xFFFFFFFF;
wWrite = pobj->portdata[2] & 1;
bWriteAddr = pobj->portdata[3];
if (wWrite && isMyAddr(bWriteAddr)) {
bWriteAddr &= 0xff;
unsigned int data = pobj->portdata[4];
if (bWriteAddr >= 0x10 && bWriteAddr < 0xd0) {
/*每个地址更新8个方块,每个方块4位,每一行
不超过16个方块,这样两个字更新一行*/
int yindex, xindex;
yindex = (bWriteAddr - 0x10);
yindex /= 4;
if (yindex / 2 < YCOUNT) {
int i;
for (i = 0; i < 8; i++) {
int xindex;
xindex = i + (yindex & 1) * 8;
if (xindex < XCOUNT) {
pobj->panelvalue[yindex / 2][xindex] = data & 0xf;
data >>= 4;
}
}
}
}
else if (bWriteAddr == 0xe0 || bWriteAddr == 0xe4) {
int offs;
offs = (bWriteAddr == 0xe0) ? 0 : 2;
pobj->nextblock[0 + offs][0] = data & 0xf; data >>= 4;
pobj->nextblock[0 + offs][1] = data & 0xf; data >>= 4;
pobj->nextblock[0 + offs][2] = data & 0xf; data >>= 4;
pobj->nextblock[0 + offs][3] = data & 0xf; data >>= 4;
pobj->nextblock[1 + offs][0] = data & 0xf; data >>= 4;
pobj->nextblock[1 + offs][1] = data & 0xf; data >>= 4;
pobj->nextblockHDL4SE:软件工程师学习Verilog语言(十四)