在本科的最后半年,因为某些特殊原因,我在导师的指导下报名了第八届集创赛的雨骤杯赛道,借此机会也为接下来的硕士阶段做准备,这次比赛也是非常成功,我们仨也都得到了满意的名次,圆梦山东。这五个月期间,作为队伍中最擅长硬件调试的同学,自然必须得学以致用,将本科期间积累的专业知识运用在比赛中,自己所负责的内容是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式中,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、多路系统供电、两个波形采集通道和两个波形发生通道,支持自行按规格设计载板安装在仪器上使用,完全满足我们的需求。
完成工作
接下来是我在本次比赛中完成工作的介绍。
软件编写
在初赛阶段,由于主要着力完成的内容为电路仿真,我们暂时没有着手设计整体硬件架构,我负责的内容是使用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的数据包收发实验。
其中,串口接收相信大家都比较熟悉,此处不再赘述,不过需要注意的是由于雨骤云编译的.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_ch和da_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,该系列运放我在毕设中使用过,性能非常出色。
- 我们作品的一个功能是测试人体心电,测试过程中势必会引入无法滤波的共模干扰,此时我们可以加入右腿驱动来针对性改善,右腿驱动电路的原理可以参考对付共模噪声、工频干扰?不妨看看右腿驱动电路
https://www.bilibili.com/video/BV14L4qekEDC
- 涉及到小模拟信号的部分,可以考虑使用磁珠对数字地跟模拟地进行单点接地,磁珠的规格根据电路的干扰频率等进行选择。
- 较为高速的数字信号,建议在两侧加上接地走线,或打过孔处理,这样可一定程度上减少干扰,保证信号质量。
软硬件的高度配合,确保系统能够站上赛场,行稳致远。
参赛复盘
使用上述的作品,配合队友开发的功能十分强大的上位机,整个系统顺利通过了分赛区决赛,不过在测试现场出现了一个没能注意的问题:板厚。由于雨骤仪器的弹针是有弹性的,当时为了节省成本在打板时选择了1.6mm板厚,测试时出现了接触不良的现象,具体现象就是DAC芯片的输出无法及时反馈到输出端,不得不用手按住触点区域才能保证工作稳定。在国赛阶段,我们使用了2.0mm板厚+沉金工艺,最大程度确保了板子工作稳定性。
同时,当时由于没有加入输出缓冲电路,因此测试出的INL与DNL等参数不理想,当时我们根据老师的建议,在DA输出端加入了一个阻值约10KΩ的下拉电阻,测试参数有一定改善。
关于上位机的开发,要学会使用多线程、信号与槽机制,这可以有效提升上位机的工作稳定性,同时避免在部分界面出现转圈圈未响应的现象。
由于赛题规定使用云编译平台,因此建议在上传代码前先在本地进行实测,确保可用后移植到雨骤仪器上,例如使用ModelSim等软件进行仿真,或者利用ILA等IP核对实体板卡进行运行仿真。当时因为没有合适的仿真板卡,耽误了大量时间,没能进一步完善作品的代码,比如加入指令反馈功能。
另外还有些关于硬件的问题,可能也需要去了解,比如作品使用的DA跟国际竞品是否Pin-to-Pin兼容,为什么需要在DA输出端加入缓冲电路,包括但不限于器件的成本等。
时刻关注参赛通知,避免错过一些关键的时间点,比如提交PPT等材料,建议提前上传一版避免后期网络拥挤无法上传。
最后,感谢导师和两位优秀的学妹与我一起并肩完成了这次比赛,也特别感谢菜鸟教程、正点原子、嘉立创EDA提供的软件支持。在我发布这篇文章之际,正值中秋佳节,“但愿人长久,千里共婵娟。”祝愿大家中秋快乐,家庭和睦,幸福美满!