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", width, width, BINOP_AND);
IHDL4SEUnit** unit_binop_overflow = hdl4seCreateUnit(module_counter, CLSID_HDL4SE_BINOP, temp, "unit_binop_overflow");
objectCall3(unit_binop_overflow, Connect, 0, unit_binop_EQ_bCurrentCounter_MAXVALUE, 2);
objectCall3(unit_binop_overflow, Connect, 1, unit_counter, 1);
objectCall3(unit_counter, Connect, 3, unit_binop_overflow, 2);

运行后,还是发现9999到10000时,并没有正确跳到10000,还是显示00000,但是记录的信号内容已经变成(已经增加记录译码器的输出):
在这里插入图片描述
内部计数器的值正确了,但是生成写设备命令时,写数据晚了一拍,这样在写4567LED时,写的数据还是前一拍的数据,值没有正确显示,因为我们是在4567变化时才写4567LED的(一次只能更新4个数码管),所以后面没有机会更新了。分析其原因,我们在主模块中生成地址时,是根据溢出信号来进行选择的,现在溢出信号等于比数据早一拍出来,因此导致地址和数据不匹配。我们用个简单的办法来处理,就是计数器输出的溢出信号寄存一拍,然后用来生成地址和数据选择,这样处理后,显示和抓取的信号都正常了。
如果没有这个调试手段,这个bug还真难以查出来,只能靠走查代码才行了,如果逻辑复杂点,那就很难了。Excel还是功能不够,最好用的是一个查询功能,看来得开发一个好用点的波形分析工具出来,哪怕是用Excel小程序来做也行啊,比如做一个Excel下的综合查询功能,能快速找到所需要的信号组合。
【请参考】
1.HDL4SE:软件工程师学习Verilog语言(四)
2.HDL4SE:软件工程师学习Verilog语言(三)
3.HDL4SE:软件工程师学习Verilog语言(二)
4.HDL4SE:软件工程师学习Verilog语言(一)
5.LCOM:轻量级组件对象模型
6.LCOM:带数据的接口
7.工具下载:在64位windows下的bison 3.7和flex 2.6.4
8.git: verilog-parser开源项目
9.git: HDL4SE项目
10.git: LCOM项目
11.git: GLFW项目

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饶先宏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值