HDL4SE:软件工程师学习Verilog语言
Posted 饶先宏
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言相关的知识,希望对你有一定的参考价值。
5 调试器
软件设计过程中经常会用到调试功能,可以按行或者按指令一条一条执行,查看其中的数据和内存,CPU寄存器之类的内容,便于找出问题。然而电路如何调试,特别是软件实现的电路模拟器,调试就不能按照普通软件的调试来做了,你要是下载运行了前面一节的模拟器运行一下,就会明白了,如果代码中有点问题,在软件层面根本无法调试,断点都不知道设置在什么地方。
看来还得学习硬件如何调试了。FPGA调试可以通过仿真软件将每个关心的信号提取出来,放在一个所谓的波形查看软件中查看,可以看到同一时刻各个信号的取值,在连续的时钟周期中看各个信号的变化,找出可能存在的问题。FPGA还可以直接从硬件中将关心的信号数据实时取回来,也可以看到FPGA运行过程中实际的信号变化,比如Altera的SignalTap工具。不过此时要在逻辑综合时打开SignalTap功能,占用一定的FPGA资源,由于信号采样时钟频率可能赶不上实际信号的变化频率,所以得到的数据可能不全,只能做参考。ASIC的调试一般是通过JTAG口,设计时内部逻辑增加所谓的扫描链电路,将需要外部读取的寄存器挂在链上,然后通过JTAG口能够读到内部寄存器的值。
硬件调试电路板的时候采用的是示波器或者逻辑分析仪,两者都是把探头连接到需要测试的电路位置,然后进行信号采样,得到信号变化,区别在于示波器的探头比较少,信号分析功能也比较简单。逻辑分析仪则有很多探头,而且能够分析很多信号,比如是否符合某种规范,甚至能够直接解析出数据帧,比如VESA显示信号规范,USB规范,PCIE规范等等。代价就是贵,另外用起来要麻烦很多,毕竟要连接好多探头到电路板上才行。其实这个对硬件工程师也不是一件轻松的活,软件工程师最好还是请硬件工程师来做算了,当然软硬通吃的那种高级变态攻城狮就随意好了。
我们在HDL4SE中的办法是提供一个探头接口,每个电路单元实现时可以实现这个接口,这个接口来查询所能提供内部数据的所有的信号编号和名称,可以在模拟器运行开始之前收集所有的数据名称,然后在每个周期中得到信号的数据,可以在单步运行完成后来收集,存在一个磁盘文件中即可。然后提供一个波形查看器,回放这个波形, 当然也可以实时开一个界面,在每次单步运行时看到各个信号的值。
看来又不得不暂停verilog的学习了,不过古人说得好:“工欲善其事,必先利其器”,意思就是说软件工程师想学习计算机语言,得先找齐好用的工具链啊,找不到合适的,就DIY好了。
5.1 探头接口
每个电路单元,包括基本单元和module,都可以实现信号探头接口,用户用纯软件实现的module,也可以实现探头接口,来提供内部的信号监测服务。探头接口定义如下:
DEFINE_GUID(IID_HDL4SEDETECTOR, 0x4539fbfe, 0x5657, 0x48d5, 0x93, 0xfa, 0x31, 0xd2, 0xbd, 0xe2, 0x40, 0xde);
typedef struct sIHDL4SEDetector {
OBJECT_INTERFACE
int (*GetName)(HOBJECT object, const char ** pname);
int (*GetSignalCount)(HOBJECT object);
int (*GetSignalInfo)(HOBJECT object, int index, const char **pname, int * width);
int (*GetSignalValue)(HOBJECT object, int index, IBigNumber** value);
int (*GetUnitCount)(HOBJECT object);
int (*GetUnit)(HOBJECT object, int index, HOBJECT* unit);
}IHDL4SEDetector;
#define HDL4SEDETECTOR_VARDECLARE
#define HDL4SEDETECTOR_VARINIT(_objptr, _sid)
#define HDL4SEDETECTOR_FUNCDECLARE(_obj, _clsid, _localstruct) \\
static int _obj##_hdl4se_detector_GetName(HOBJECT object, const char ** pname); \\
static int _obj##_hdl4se_detector_GetSignalCount(HOBJECT object); \\
static int _obj##_hdl4se_detector_GetSignalInfo(HOBJECT object, int index, const char** pname, int * width); \\
static int _obj##_hdl4se_detector_GetSignalValue(HOBJECT object, int index, IBigNumber** value); \\
static int _obj##_hdl4se_detector_GetUnitCount(HOBJECT object); \\
static int _obj##_hdl4se_detector_GetUnit(HOBJECT object, int index, HOBJECT* unit); \\
static const IHDL4SEDetector _obj##_hdl4se_detector_interface = { \\
INTERFACE_HEADER(_obj, IHDL4SEDetector, _localstruct) \\
_obj##_hdl4se_detector_GetName, \\
_obj##_hdl4se_detector_GetSignalCount, \\
_obj##_hdl4se_detector_GetSignalInfo, \\
_obj##_hdl4se_detector_GetSignalValue, \\
_obj##_hdl4se_detector_GetUnitCount, \\
_obj##_hdl4se_detector_GetUnit, \\
};
/* return 0 to continue traversal, or stop traversal */
typedef int (*hdl4se_detector_TraversalFunc)(IHDL4SEDetector ** detector, const char * pathname, void * param);
int hdl4sedetectorTraversal(HOBJECT object, hdl4se_detector_TraversalFunc func, const char * pathname, void* param);
其中的GetName返回当前电路单元的名称指针,GetSingalCount返回当前电路单元中可以返回信号值的信号个数,一般是端口,实现时可以提供一些额外的信号,GetSingalName则返回指定索引号的信号的名称,GetSingalValue则返回指定索引号的信号值。GetUnitCount返回电路单元中子单元的个数,GetUnit则返回指定索引的子单元,这个子单元如果支持IHDL4SEDetector接口,则又可以提供前面的操作。
这样设计,可以从模拟器开始,遍历所有的树状电路单元,最终就像树状文件系统一样,每个单元有个全局名字,树根就是simulator,提供的信号就是总线信号,下面是主模块和安装的各个设备,都可以用递归的方式按树状遍历,找到每个信号。一个信号可以用所在的单元对象实例和对应的索引来标识。我们提供了遍历树的例程:
hdl4sedetectorTraversal,下面的代码可以打印指定单元及子单元的所有信号名称和宽度:
static int hdl4se_print_all_signal(IHDL4SEDetector** detector, const char * pathname, int *pwidth)
{
int i;
int count;
int width;
const char* name;
count = objectCall0(detector, GetSignalCount);
for (i = 0; i < count; i++) {
objectCall3(detector, GetSignalInfo, i, &name, &width);
printf("%s/%s, %d\\n", pathname, name, width);
*pwidth += (width + 31) / 32;
}
return 0;
}
int main(int argc, char* argv[])
{
int width;
sim = hdl4sesimCreateSimulator();
topmodule = hdl4seCreateMain(NULL, "", "main");
gui = guiCreate(0xf0000000, "digitled");
objectCall1(sim, SetTopModule, topmodule);
objectCall1(sim, AddDevice, gui);
objectCall1(sim, SetReset, 0);
width = 0;
hdl4sedetectorTraversal(sim, hdl4se_print_all_signal, "", &width);
printf("Total width=%d bits, %d word\\n", width * 32, width);
do {
objectCall0(sim, RunClockTick);
clocks++;
if (clocks == 4)
objectCall1(sim, SetReset, 1);
} while (running);
return 0;
}
5.2 波形文件
记录波形的文件格式也很关键,波形数据比较大,如果每个信号都记录的话,一个时钟周期可能会产生几十KB到几十MB的数据,但是大部分的信号其实变化很小,也就是传说中的寄存器翻转率其实并没有那么高,因此可以进行某种压缩,比如不变的信号就记录一个“同上”即可,这样可以大幅度减少记录的数量。
我们这里不详细描述波形文件格式,这个自然很重要,但是我们够用就行,不能为打个小BOSS,忘记了我们的目标。这个小BOSS有人感兴趣不?报个名上来一起开发HDL4SE!
我们的够用解决方案是,生成一个文本文件,每一行一个周期,信号之间用逗号割开,这样就成为所谓的csv格式Excel能够直接打开,简单的分析没有问题了,后面有功夫再来做个更加“利”的工具。
生成csv文件的代码如下:
typedef struct s_signal_item {
const char* unitname;
const char* signalname;
IHDL4SEDetector** detector;
int index;
int width;
} signal_item;
signal_item signal_list[] = {
{"/simulator/digitled", "nwReset", NULL, 0, 0},
{"/simulator/digitled", "wWrite", NULL, 0, 0},
{"/simulator/digitled", "bWriteAddr", NULL, 0, 0},
{"/simulator/digitled", "bWriteData", NULL, 0, 0},
{"/simulator/digitled", "wRead", NULL, 0, 0},
{"/simulator/digitled", "bReadAddr", NULL, 0, 0},
{"/simulator/digitled", "bReadData", NULL, 0, 0},
};
#define signal_list_count (sizeof(signal_list) / sizeof(signal_list[0]))
static int hdl4se_init_signal_list(IHDL4SEDetector** detector, const char* pathname, int *pcount)
{
int i, j;
int count;
int width;
const char* name;
for (j = 0; j < signal_list_count; j++) {
if (strcmp(pathname, signal_list[j].unitname) == 0) {
count = objectCall0(detector, GetSignalCount);
for (i = 0; i < count; i++) {
objectCall3(detector, GetSignalInfo, i, &name, &width);
if (strcmp(name, signal_list[j].signalname) == 0) {
signal_list[j].detector = detector;
signal_list[j].index = i;
signal_list[j].width = width;
printf("sig: %s, unit=%s, index=%d, width=%d\\n", name, pathname, i, width);
(*pcount)++;
}
}
}
}
return 0;
}
FILE* pSignalFile = NULL;
IBigNumber** signal_value = NULL;
static int hdl4se_print_signal_list_header()
{
int i;
fprintf(pSignalFile, "clocks");
for (i = 0; i < signal_list_count; i++) {
fprintf(pSignalFile, ",%s", signal_list[i].signalname);
}
fprintf(pSignalFile, "\\n");
return 0;
}
static int hdl4se_print_signal_list_detector()
{
int i;
/*
我们可以设置记录条件,比如,看到bDataRead数据不为零才记录,
此时必然是由按键消息了
*/
{
unsigned int v;
objectCall2(signal_value, SetWidth, 32, 0);
objectCall2(signal_list[6].detector, GetSignalValue, signal_list[6].index, signal_value);
objectCall1(signal_value, GetInt, &v);
if (v != 4) /* 只有F3按下的时候才记录 */
return 0;
}
fprintf(pSignalFile, "%lld", clocks);
for (i = 0; i < signal_list_count; i++) {
unsigned int v;
objectCall2(signal_value, SetWidth, 32, 0);
objectCall2(signal_list[i].detector, GetSignalValue, signal_list[i].index, signal_value);
objectCall1(signal_value, GetInt, &v);
if (signal_list[i].width == 1) {
fprintf(pSignalFile, ",%d", v & 1);
}
else {
fprintf(pSignalFile, ",%08x", v);
}
}
fprintf(pSignalFile, "\\n");
return 0;
}
int main(int argc, char* argv[])
{
int width;
int count;
sim = hdl4sesimCreateSimulator();
topmodule = hdl4seCreateMain(NULL, "", "main");
gui = guiCreate(0xf0000000, "digitled");
objectCall1(sim, SetTopModule, topmodule);
objectCall1(sim, AddDevice, gui);
objectCall1(sim, SetReset, 0);
count = 0;
pSignalFile = fopen("digitled.csv", "w");
signal_value = bigintegerCreate(32);
hdl4se_print_signal_list_header();
hdl4sedetectorTraversal(sim, hdl4se_init_signal_list, "", &count);
printf("want %d, inited %d\\n", signal_list_count, count);
do {
objectCall0(sim, RunClockTick);
hdl4se_print_signal_list_detector();
clocks++;
if (clocks == 4)
objectCall1(sim, SetReset, 1);
} while (running);
objectRelease(signal_value);
fclose(pSignalFile);
return 0;
}
修改其中的signal_list就可以记录你感兴趣的信号。可以修改hdl4se_print_signal_list_detector函数,在不同的条件组合下记录信号,有没有点像SignalTap中的触发条件啊,或者调试c语言程序中设置断点。
记录下来的文件中的几行如下:
clocks,nwReset,wWrite,bWriteAddr,bWriteData,wRead,bReadAddr,bReadData
23376,1,1,00000000,00000000,1,f0000000,00000004
23377,1,1,f0000010,3f3f3f3f,1,f0000000,00000004
23378,1,1,f0000010,3f3f3f06,1,f0000000,00000004
23379,1,1,f0000010,3f3f3f5b,1,f0000000,00000004
23380,1,1,f0000010,3f3f3f4f,1,f0000000,00000004
23381,1,1,f0000010,3f3f3f66,1,f0000000,00000004
23382,1,1,f0000010,3f3f3f6d,1,f0000000,00000004
23383,1,1,f0000010,3f3f3f7d,1,f0000000,00000004
23384,1,1,f0000010,3f3f3f07,1,f0000000,00000004
23385,1,1,f0000010,3f3f3f7f,1,f0000000,00000004
用Excel打开的界面如下:
当然没有专业的仿真工具好用,不过考虑到Excel可以进行数据分析,这样做还是有一定用处的。
5.3 波形回看器
如5.2所说,目前用Excel就够了,将来再设计个更加强大的。
5.4 实时的信号查看
先不支持,留下线索后面改进。
5.5 调试实例
如果你运行过0.0.3以前的代码并足够细心的话,会发现实现的代码中有个bug,计数器计数时到9999之后,不是到了10000,而是回到了00000,修改signal_list,
{"/simulator/digitled", "nwReset", NULL, 0, 0},
{ "/simulator/digitled", "wWrite", NULL, 0, 0 },
{ "/simulator/digitled", "bWriteAddr", NULL, 0, 0 },
{ "/simulator/digitled", "bWriteData", NULL, 0, 0 },
{ "/simulator/digitled", "wRead", NULL, 0, 0 },
{ "/simulator/digitled", "bReadAddr", NULL, 0, 0 },
{ "/simulator/digitled", "bReadData", NULL, 0, 0 },
{ "/simulator/main/or7654", "out", NULL, 0, 0 },
{ "/simulator/main/counter0", "0.nwReset", NULL, 0, 0 },
{ "/simulator/main/counter1", "0.nwReset", NULL, 0, 0 },
{ "/simulator/main/counter4", "0.nwReset", NULL, 0, 0 },
{ "/simulator/main/counter4", "1.wCounterIt", NULL, 0, 0 },
{ "/simulator/main/counter0", "2.bCounter", NULL, 0, 0 },
{ "/simulator/main/counter1", "2.bCounter", NULL, 0, 0 },
{ "/simulator/main/counter2", "2.bCounter", NULL, 0, 0 },
{ "/simulator/main/counter3", "2.bCounter", NULL, 0, 0 },
{ "/simulator/main/counter4", "2.bCounter", NULL, 0, 0 },
将前几个计数器的信号,特别是复位信号都记录下来,果然发现有异常:
每个计数器计数到最大值9时,高一位没有同时增加,溢出信号延迟了一拍输出。这样低位的时候看不出来,4位到9999时,要经过4拍才能传到第5位,造成计数序列是:
09997, 09998, 09999, 09990, 09901, 09002, 00003, 10004, 10005,
看来前面的设计还是有问题的,溢出信号不能延迟一拍给出,我们修改wOverflow的生成代码如下:
/*
main.v counter模块中,wCounterOverflow直接用组合逻辑实现,不再寄存器输出
*/
assign wCounterOverflow = (bCurrentCounter == MAXVALUE) && wCounterIt;
编译出来的汇编代码:
hdl4se_binop #(1, 1, 1, BINOP_AND)
binop_counter_overfloat(
wEQ_bCurrentCounter_MAXVALUE,
wCounterIt,
wCounterOverflow
);
目标代码相应改为:
sprintf(temp, "%d, %d, 1, %d"HDL4SE:软件工程师学习Verilog语言(十四)