第八届集创赛 雨骤杯 赛后复盘

在本科的最后半年,因为某些特殊原因,我在导师的指导下报名了第八届集创赛的雨骤杯赛道,借此机会也为接下来的硕士阶段做准备,这次比赛也是非常成功,我们仨也都得到了满意的名次,圆梦山东。这五个月期间,作为队伍中最擅长硬件调试的同学,自然必须得学以致用,将本科期间积累的专业知识运用在比赛中,自己所负责的内容是PCB的绘制,元器件的选型和云编译驱动的编写。为了能够起到抛砖引玉的作用,我将这次参赛经历在此复盘,分享给大家,可能内容上会出现一些错误,希望大家能够指出,感激不尽!

赛题内容分析

下面是自己参与的那届赛题,链接已贴出:

第八届集创赛杯赛题目——雨骤(恩捷伦®)杯

本届赛题着重于DAC的测试及应用,此处也意味着我们需要使用雨骤官方提供的参赛平台(下称雨骤仪器),完成DAC的测试与应用两个任务。其中,雨骤官方的参赛平台拥有两种开发方式,分别为使用雨骤官方提供的"Instruments Playground API",和使用雨骤云编译 “自定义RTL片上仪器”。其中,前者更适合对性能需求不高,追求简化编程的团队,而后者由于需要编写Verilog代码(毕竟硬件描述语言或多或少沾点边(bushi))更适合熟悉硬件开发、需要更高维度的自定义,或者原有的SDK无法满足性能需求的团队。

本质上,我们最关键的工作就是使用雨骤仪器,驱动我们自选的国产DAC芯片,使其输出指定的电压,进而进行后续的步骤。一般来说,DAC芯片拥有的数据接口为并口、SPI、I2C等,这就需要我们通过对雨骤仪器进行开发,来实现预期的功能,不管是通过编写上位机对雨骤仪器进行控制,还是通过云编译对雨骤仪器内部的芯片进行功能重构。因此,团队需要有一名掌握上位机开发(C、Python、LabVIEW等)的成员,由于“一体化PCB载板”是赛题的一个得分项,擅长硬件测试的成员也必不可少。“良好的人机GUI交互”也是个得分项,因此上位机的美观度也需要相应的提升,至于串口屏(内置MCU)是否允许使用,众说纷纭,建议询问赛事组委会以得到最确切的答案。

理论分析

DAC参数

根据赛题的要求,我们需要测试的参数至少包括下面两项:积分非线性(INL, Integral Nonlinearity)和差分非线性(DNL, Differential Nonlinearity),现对此进行分析:

INL为输出传输函数和理想直线之间的偏差了,DNL为转换器输出步长相对于理想步长的误差。分别可以用如下公式进行计算:

INL=A_{real} -A_{ideal}

DNL=\frac{A_{i}-A_{i-1}}{LSB}-1

INL式中,Areal为搭建系统的DAC实际输出值,Aideal为当前同样输入下DAC的理想输出值;DNL式中,LSB为理想情况下每一位的差值。因此,实际上的INL和DNL数值指的是每一个刻度的参数,一些芯片手册给出的参数可能是平均数、中位数、最大值等。

因此,我们需要测得DAC芯片在每一个输入时的输出电压,最常用的方式是向其发送0-FS(Full Scale)的数据,让其生成阶梯波,然后通过记录每个刻度下的电压值,进行汇总与计算。

事实上,由于DAC芯片的输入电压并不固定,我们在比赛后期使用满量程对应的DA输出来计算LSB的具体数值,这样可以最大程度规避电源电压波动对计算结果的影响。

DAC架构

主流的DAC架构分为两种:电阻串DAC和分段式DAC。其中,电阻串DAC内部有一串电阻连接供电与地,通过开关的形式进行选择,其优点是易于制造,不过由于其结构,其位数不可能做的很高。为了解决这个问题,分段式DAC使用多个电阻网络进行并联,通过后一级电阻网络对前一级的分压电压进一步分压,节省电路体积的情况下还增加了DAC的位数。

本次我们使用的DAC芯片就为分段式架构,为思瑞浦(3PEAK)的TPC112S4高精度DAC芯片,该芯片工作在2.7-5.5v 的电压范围内,分辨率为12位。相较于国际竞品,精度更高,INL、增益误差、功耗、最大通信速率等指标更优。

TPC112S4 - 精密数模转换器 (DAC) - 3PEAK

本次竞赛我们使用了雨珠-S,其具备16个数字IO、多路系统供电、两个波形采集通道和两个波形发生通道,支持自行按规格设计载板安装在仪器上使用,完全满足我们的需求。

雨珠S: YuZhu | Raindrop-S

完成工作

接下来是我在本次比赛中完成工作的介绍。

软件编写

在初赛阶段,由于主要着力完成的内容为电路仿真,我们暂时没有着手设计整体硬件架构,我负责的内容是使用NI Multisim软件绘制电阻串DAC的仿真电路,并通过其计算仿真电路的INL和DNL,仿真图如上节所示。

同时,为了能够测试芯片的性能,我自行绘制了一块DAC芯片的转接板,引出了数据端口和信号输出口,便于连接控制及采集设备:

最初由于不熟悉雨骤的开发流程,我首先使用STM32对DAC芯片进行驱动,使用软件模拟的方式产生指定驱动时序,部分代码如下(示例代码为TPC112S4,需要发送2组8位指令即16位):

#define CS_H HAL_GPIO_WritePin(NSYNC_GPIO_Port, NSYNC_Pin, GPIO_PIN_SET)
#define CS_L HAL_GPIO_WritePin(NSYNC_GPIO_Port, NSYNC_Pin, GPIO_PIN_RESET)
#define SCLK_H HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET)
#define SCLK_L HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET)
#define SDI_H HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, GPIO_PIN_SET)
#define SDI_L HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, GPIO_PIN_RESET)
#define LD_H HAL_GPIO_WritePin(NLOAD_GPIO_Port, NLOAD_Pin, GPIO_PIN_SET)
#define LD_L HAL_GPIO_WritePin(NLOAD_GPIO_Port, NLOAD_Pin, GPIO_PIN_RESET)

void SPI_WR_OneByte(uint8_t data){
	uint8_t i;
	for(i = 0; i < 8; i++)
	{
        SCLK_H;		
        if(data & 0x80) SDI_H;
		else SDI_L;
		data <<= 1;
		SCLK_L;
	}
}

uint8_t Write16Reg(uint32_t d){
	uint8_t tx[2] = {(d >> 8) & 0xff, d & 0xff};
	CS_L;
	SPI_WR_OneByte(tx[0]);
	SPI_WR_OneByte(tx[1]);
	CS_H;
	SDI_L;
}

/*
Usage:
    CS_L;
	Write16Reg(da_dat);
	CS_H;
	__NOP();
	//several nops
	__NOP();
	LD_L;
	__NOP();
	//several nops
	__NOP();
	LD_H;
*/

同时,我们也在研究如何使用雨骤官方提供的Python SDK发送驱动时序,官方的SPI自2.2(大概)版本后开始支持SPI时钟极性和相位的调整,不过由于我们的DAC拥有额外的使能端,原先的SDK并不能满足我们的需求。因此我想了个笨办法:官方有提供基于位操作的GPIO读写函数,我直接从STM32的模拟SPI移植过去,不就可以了吗?

事实证明,该方法可行,不过也仅限于可行……经过逻辑分析仪抓取,发送一个通道的指令需要3.5 ms,远远达不到我们作品的需求。为此,我们选择了其后的方案:云编译。

在分赛区决赛阶段,我们开始着手编写云编译的驱动。由于云编译能操作的范围局限于16个数字IO,因此我们决定使用UART串口进行通信。由于雨珠-S的数字IO使用了三态门,因此我们需要对指定的位赋值来决定IO的方向(输入or输出),并且对形如io_x_out[x]的IO进行信号连接。代码如下:

	//IO初始化设置
	
	assign io_b_oe[4] = 1'b1;//112S4 NSS2
	assign io_b_oe[3] = 1'b1;//112S4 NSS1
	assign io_b_oe[2] = 1'b1;//112S4 SCK
	assign io_b_oe[1] = 1'b1;//112S4 DAT
	assign io_b_oe[0] = 1'b1;//112S4 NLOAD
	
	assign io_a_oe[7] = 1'b1;//仪器串口发送端
	assign io_a_oe[6] = 1'b0;//仪器串口接收端
	assign io_a_out[7] = 1'b1;//暂时用不上发送,先暂时赋1防止接收指示灯常亮
	
	assign io_b_out[4] = NSS2;//关联所有端口
	assign io_b_out[3] = NSS1;
	assign io_b_out[2] = SCK;
	assign io_b_out[1] = DAT;
	assign io_b_out[0] = NLOAD;

至于为什么有两个NSS,是因为我们在后续使用了两片DA,需要使用分别两个片选端进行控制。

我们需要对指定DA的指定通道进行写操作,因此我们需要使用一种高效的方式对指令进行打包发送,最直观的方式就是使用数据包的形式。

数据包一般包含“帧头、长度、数据、帧尾、校验”等部分,为了简化编程,我们使用的数据包格式如下:

(帧头0xXX|通道数0x00-0x07|DA数据高八位|DA数据低八位|帧尾0xYY)

接下来就是Verilog代码的编写了,我们需要使用纯逻辑的编程方式,编写串口数据接收、数据包解析、DA驱动等功能模块,其中,各个功能模块又需要使用若干个always块进行控制,这需要我们对整个系统有强烈的全局观念,以避免时钟出现错误导致系统混乱,从而无法实现预期的功能。

该部分代码参考正点原子领航者ZYNQ的教程编写,主要参考了UART串口通信实验和基于UART的数据包收发实验。

ZYNQ领航者V2开发板

其中,串口接收相信大家都比较熟悉,此处不再赘述,不过需要注意的是由于雨骤云编译的.v文件存在格式规范,因此串口的接收引脚需要换成形如io_x_in[x]的形式,才能够正常使用。

代码最关键,也是我当时调试起来最头疼的地方就是数据包的解析了,数据包的解析最常用的就是状态机的思想,但是由于自身开发经验不足,一开始属实踩了不少坑,比如:

	always @(posedge clk or negedge rst_n) begin//状态装填
		if(!rst_n)
			uart_stat_curr <= STATE_IDLE;
		else
			uart_stat_curr <= uart_stat_next;
	end

	always @* begin//状态机切换控制
		uart_stat_next = STATE_IDLE;
		case(uart_stat_curr)
			STATE_IDLE: begin
				if(switch_en)
					uart_stat_next = STATE_CHS;
				else if(uart_result == ERR_HEAD)
					uart_stat_next = STATE_IDLE;
				else
					uart_stat_next = STATE_IDLE;
			end
			STATE_CHS: begin
				if(switch_en)
					uart_stat_next = STATE_DAT_MSB;
				else if(uart_result == ERR_CHS)
					uart_stat_next = STATE_IDLE;
				else
					uart_stat_next = STATE_CHS;
			end
			STATE_DAT_MSB: begin
				if(switch_en)
					uart_stat_next = STATE_DAT_LSB;
				else
					uart_stat_next = STATE_DAT_MSB;
			end
			STATE_DAT_LSB: begin
				if(switch_en)
					uart_stat_next = STATE_FINISH;
				else
					uart_stat_next = STATE_DAT_LSB;
			end
			STATE_FINISH: begin
				if(switch_en)
					uart_stat_next = STATE_DAOUT;
				else if(uart_result == ERR_END)
					uart_stat_next = STATE_IDLE;
				else
					uart_stat_next = STATE_FINISH;
			end
			STATE_DAOUT: begin
				if(da_okay)
					uart_stat_next = STATE_IDLE;
				else
					uart_stat_next = STATE_DAOUT;
			end
			default: uart_stat_next = STATE_IDLE;
		endcase
	end

此处的组合逻辑,我一开始错误的使用了<=非阻塞赋值,导致状态解析的时候解析到错误的通道数后,状态锁住,无法进行后续操作,更正后问题解决。

always @(posedge clk or negedge rst_n) begin//串口数据处理
		if(!rst_n) begin
			uart_result <= 4'd0;
			switch_en <= 1'b0;
		end
		else begin
			switch_en <= 1'b0;
			case(uart_stat_curr)
				STATE_IDLE: begin
					if(uart_done_flag) begin
                        uart_result <= 4'd0;
						if(uart_data == 8'hXX)//帧头
							switch_en <= 1'b1;
						else
							uart_result <= ERR_HEAD;
					end
				end
				STATE_CHS: begin
					if(uart_done_flag) begin//通道数>8时状态机锁s,问题在于组合逻辑使用了非阻塞赋值,现该问题已解决
						if(uart_data < 8'h08) begin
							da_ch <= uart_data;
							switch_en <= 1'b1;
						end
						else
							uart_result <= ERR_CHS;
						// da_ch <= uart_data;
						// switch_en <= 1'b1;
					end
				end
				STATE_DAT_MSB: begin//更换为112S4,因此高四位自动舍弃,为确保控制效率不加入判断机制
					if(uart_done_flag) begin
						da_val[15:8] <= uart_data;
						switch_en <= 1'b1;
					end
				end
				STATE_DAT_LSB: begin
					if(uart_done_flag) begin
						da_val[7:0] <= uart_data;
						switch_en <= 1'b1;
					end
				end
				STATE_FINISH: begin
					if(uart_done_flag) begin
						if(uart_data == 8'hYY) begin//帧尾
							da_ready_flag <= 1'b1;
							switch_en <= 1'b1;
						end
						else
							uart_result <= ERR_END;
					end
				end
				STATE_DAOUT: begin
					if(da_okay) begin
						da_ready_flag <= 1'b0;
						uart_result <= 4'd0;
						switch_en <= 1'b0;
					end
				end
				default: begin
					uart_result <= 4'd0;
					switch_en <= 1'b0;
				end
			endcase
		end
	end

这块状态的跳转部分我前前后后写了将近一个星期,究其原因还是自己对整个系统的所有信号缺乏全局观念,导致原本不需要进行操作的信号被控制,进而系统混乱,无法达成预期的效果。经过上述步骤,需要控制的DA通道以及数据已经被存放至da_chda_val[15:0]两个变量,同时控制da_ready_flag来触发DA发送模块开始发送。

DA的驱动部分,使用了多个always块负责片选、装载、数据和时钟端口的控制,受限于篇幅,该部分代码暂时不放出,其根据数据手册编写,不过还存在优化的空间,后续可以进一步改进。

将这些语句放置在同一个digital_io.v中,提交至雨骤云编译平台,根据设备型号选择设备代码(具体参考仪器操场左下角显示的序列号),然后提交编译,坐和放宽~(bushi

综上所述,云编译负责的内容就是通过串口的方式沟通上位机与DAC芯片,不过也因此被评委老师说“工作量太少”,其实我的代码依旧有不少有待优化的地方,比如像MODBUS一样加入指令反馈的功能,只不过受限于时间未付诸实践。

硬件设计

对于整个参赛作品来说,更关键的部分就是电路的设计,其将直接决定后续系统的稳定与否,结果准确与否。

在初赛阶段的培训,雨骤官方提供了PCB版图示例和引脚定义供我们参考,由于自己长期以来习惯使用嘉立创EDA进行电路的设计,因此我将组委会提供的pcbdoc导入至工程文件,由于版图文件较大,此处不做完整展示,仅介绍一些关键的部分。

  • 官方提供的载板pcbdoc,绝对不要动弹针触点,螺栓,板框等关键部分!轻则引脚错位导致功能不正常,重则无法正常安装在仪器上,建议使用锁定功能将其锁定以防不慎修改。

  • 电源走线记得加粗,能够有效提升电路载流能力和工作稳定性。

  • 涉及差分的信号注意控制等长,尤其是信号频率较高的情况下,两侧可以铺铜打过孔加强抗干扰性能。

  • 对于对电源质量要求较高的应用(如DA参数测试项目),可以使用LDO确保供电质量纯净。本次我们使用了DC-DC+LDO的供电架构为DAC芯片供电,从雨骤仪器取12V,经过DC-DC降压至5.5V,然后使用低纹波LDO降压至5V,经过实测,相较于直接使用雨骤仪器的5V,测试出的INL和DNL有10-30%的优化。
  • 我们使用的DA为电压输出型,并内置缓冲电路,不过为了确保测试结果,我们还是在DA输出端加入了一级低噪放跟随,运放的型号为AD8607,该系列运放我在毕设中使用过,性能非常出色。

  • 较为高速的数字信号,建议在两侧加上接地走线,或打过孔处理,这样可一定程度上减少干扰,保证信号质量。

软硬件的高度配合,确保系统能够站上赛场,行稳致远。

参赛复盘

使用上述的作品,配合队友开发的功能十分强大的上位机,整个系统顺利通过了分赛区决赛,不过在测试现场出现了一个没能注意的问题:板厚。由于雨骤仪器的弹针是有弹性的,当时为了节省成本在打板时选择了1.6mm板厚,测试时出现了接触不良的现象,具体现象就是DAC芯片的输出无法及时反馈到输出端,不得不用手按住触点区域才能保证工作稳定。在国赛阶段,我们使用了2.0mm板厚+沉金工艺,最大程度确保了板子工作稳定性。

同时,当时由于没有加入输出缓冲电路,因此测试出的INL与DNL等参数不理想,当时我们根据老师的建议,在DA输出端加入了一个阻值约10KΩ的下拉电阻,测试参数有一定改善。

关于上位机的开发,要学会使用多线程、信号与槽机制,这可以有效提升上位机的工作稳定性,同时避免在部分界面出现转圈圈未响应的现象。

由于赛题规定使用云编译平台,因此建议在上传代码前先在本地进行实测,确保可用后移植到雨骤仪器上,例如使用ModelSim等软件进行仿真,或者利用ILA等IP核对实体板卡进行运行仿真。当时因为没有合适的仿真板卡,耽误了大量时间,没能进一步完善作品的代码,比如加入指令反馈功能。

另外还有些关于硬件的问题,可能也需要去了解,比如作品使用的DA跟国际竞品是否Pin-to-Pin兼容,为什么需要在DA输出端加入缓冲电路,包括但不限于器件的成本等。

时刻关注参赛通知,避免错过一些关键的时间点,比如提交PPT等材料,建议提前上传一版避免后期网络拥挤无法上传。

最后,感谢导师和两位优秀的学妹与我一起并肩完成了这次比赛,也特别感谢菜鸟教程、正点原子、嘉立创EDA提供的软件支持。在我发布这篇文章之际,正值中秋佳节,“但愿人长久,千里共婵娟。”祝愿大家中秋快乐,家庭和睦,幸福美满!

### 解析 Import Error 的常见原因 当遇到 `ImportError: cannot import name 'Generic'` 错误时,通常意味着尝试从模块中导入的对象不存在或无法访问。此问题可能由多种因素引起: - 版本不兼容:不同库之间的版本冲突可能导致此类错误。 - 安装缺失:目标库未正确安装或路径配置有误。 - 导入语句不当:可能存在循环依赖或其他语法层面的问题。 ### 针对 Generic 类型的具体解决方案 对于特定于 `Generic` 的情况,考虑到 Python 中 `Generic` 是 typing 模块的一部分,在处理该类别的 ImportError 时可采取如下措施[^1]: #### 方法一:确认typing模块可用性 确保环境中已安装标准库中的 typing 模块,并且其版本支持所使用的特性。可以通过以下命令验证: ```bash python -c "from typing import Generic; print(Generic)" ``` 如果上述命令执行失败,则可能是由于 Python 或者相关扩展包的版本过低造成的。此时应考虑升级至更高版本的解释器以及对应的开发工具链。 #### 方法二:调整导入方式 有时直接通过顶层命名空间来获取所需组件会更稳定可靠。修改代码以采用这种做法可能会解决问题: ```python from collections.abc import Iterable # 如果是迭代器相关接口 from typing import TypeVar, Protocol # 对于协议和泛型定义 T = TypeVar('T') class MyContainer(Protocol[T]): ... ``` 注意这里并没有显式提到 `Generic` ,而是利用了更为基础的数据结构抽象基类或是其他替代方案实现相同功能[^2]。 #### 方法三:排查环境变量设置 检查系统的 PYTHONPATH 和虚拟环境配置是否正常工作。任何异常都可能导致某些第三方软件包找不到必要的资源文件而引发类似的错误提示。建议清理并重建项目专属的工作区以便排除干扰项的影响。 #### 示例修正后的代码片段 假设原始代码试图这样引入 `Generic` : ```python from some_module import Generic # 可能导致 ImportError ``` 改为遵循官方文档推荐的方式后变为: ```python from typing import Generic # 正确的做法 ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值