2020年9月21日,我突然收到一位教授的邀请。这位教授是我高中时课题研究的指导老师,他知道我的电子与计算机大概是什么水平,而他邀请我参加的正是电子设计竞赛。

我去做了点功课。往年,电子设计竞赛都在暑假里举办,今年因为特殊情况改到开学以后。教授则是学校里负责该竞赛的“出口”。因为以上种种机缘巧合,我才能有这次机会。

比赛需三人组队,这是比较难办的,相比之下历届题目比较简单,如果是我来选题的话。我所处的理科试验班都是些次顶尖的学数学物理的人,找不到人一起参赛,只能先把密院ECE专业的女朋友拉下水,让她再去找一个人。有几个人表示有兴趣但是没时间,尽管现在看来“有兴趣”的意思是“有兴趣一起拿个奖”。我们找了一个人聊聊,我简述了我准备比赛的计划,对方只是点点头,对着电脑不知看些什么。结束以后我跟女朋友商量说,要不就我们俩参赛吧。

中午我们去电院开会。除了我们组之外,别人都是人工智能大二学生,参赛可以在电子电路系统实验课程中得高分。我是来体验一下的,因为我一直不清楚自己的水平。以前做过几个项目,但都是自己定的目标,现在要在别人出的题中选一道完成,我能做到吗?我的项目都是肝出来的,从原理图到PCB到焊接到编程一般都要一个星期,现在要在4天3夜里完成,我能做到吗?一直都是一个人做的,现在要合作,虽然都是自己人但还是跟独立完成不一样的,我能做到吗?

所以我要好好准备。仔细分析了一下2019年的题,感觉只有C题D题是有可能做的。这类测控题好就好在只需电路,不涉及小车、无人机等机械结构,后者是我没有接触过的。

我备赛的宗旨“别让比赛过程中的小事搞坏心态”。比如,去年C题和D题都要求设备能输出一定频率的正弦波,所以得把DAC或DDS调通;频率高了以后ADC跟不上,就得用精密整流电路和施密特触发器来检测幅度和相位;这些都不应该留到比赛现场才完成。更基本地,用按键或旋转编码器选择功能、蜂鸣器响表示测量完成、屏幕上显示测量结果等功能都是必需的。

这样一来就可以决定要作什么准备了:

  • 再做一块我们熟悉的开发板

  • 做一个IO模块,放常用的输入输出设备,它通过I²C与开发板连接;

  • 设计一系列模拟电路模块,实现各种常用功能,如线性运算、精密整流等。

当时还在准备9月30日的数分高代考试,就没有直接参与准备,把IO模块的设计丢给女朋友让她设计年轻人的第一块PCB。要说工作也是有的,我在淘宝上下了12个单买元器件和模块。

缝合怪的电赛纪实-LMLPHP

↑有关的、无关的PCB

十一期间只在家里待了三天半就去学校了,这三天半里也有大半都是焊接。作为老焊工,焊接的过程就省略了。在确认下载器可以检测到单片机以后,我就带着这些去学校了,但是且慢……

彼时我的元器件库存已经比较成系统了,但新到的那么多快递还是带来了不小的压力。最麻烦的是我要带一些元器件和模块到学校去,因为据说实验室里的阻容不怎么齐全。根据我的日记描述,仅后来选题以后感觉能用上的就有:

  • 挺全的色环电阻;

  • 挺全的独石电容;

  • 少量电解电容;

  • 2N3904、2N3906(直插不多,贴片买了,有转接板);

  • 模拟开关4051、4052、4053、4066(各2片);

  • 继电器(5个),二极管;

  • 运放LMV358(5片)或LM358(20片);

  • 数字电位器X9C103S(5片);

  • 直插74HC595(5片);

实际带的还有各种模块,包括DAC、DDS等,还有一些小车能用上的,但总体来说还是把赌注往模拟电路上押的。

在学校,除了一天半的作业时间以外,别的都是写bug与debug。所有这些bug都是有关总线的,一个I²C的,别的都是SPI的。

忘了说IO模块的结构了。ATmega328P单片机,I²C接开发板,SPI上接74HC595、165、DAC和OLED屏,用一片138来做片选。595接4个LED和OLED的两个控制信号,165接4个按键和旋转编码器。SPI上挂了4个设备,难免出现各种bug。

首先是595调不出来,无论怎么写都是QH亮。把板子翻过来一看,原来是595和165焊反了。用热风枪拆下来后焊到正确的位置,过程中还经历了学校的焊锡不化和烙铁头不沾锡的问题。然后165的C引脚始终读到0,考虑到这片165是用热风枪拆了两次的,就认为这个引脚坏了,于是用飞线把这个点和一个未使用的GPIO接起来了。我想过换一片165,但是找不到可以换的片,而且铺铜上的绿皮也因为不得不开到400度的电烙铁而脱落了不少,换个片很可能导致整块板报废。

用GPIO驱动165非常顺利,但是改用SPI后就发生了很奇怪的现象:第1位能正常读到,但是第2位读到的始终与第1位相同,后面的很乱讲不清楚,大体就是位与位之间有关联。我又开始怀疑165损坏,但是换用GPIO的程序还是正常。试了几次以后我把SPI时钟频率从4 MHz降到0.5 MHz,终于能正确读取了。原来我为了不让165的输出与下载器的MISO冲突,给前者串联了一个1k电阻,这使该信号的频率上限严重下降,再加上那片165多少沾点nt,4MHz就这样爆了。

某天半夜调DAC。DAC复位后应该输出0V,但写了正确的指令后用ADC测出来像是高阻。把10位数据的低4位都写0,其余从零开始增长,得到分段增长的输出电压,我感觉是数据被移位了。果然,之前想当然地选择SPI mode 3,而实际上该用mode 2。

OLED的RESD/C控制信号接在了595上,SPI与单片机直接相连,跨了两层,问题也出在这里。屏幕和接线都跟以前我做过的一个模块一样,只把u8g2的GPIO回调函数改了一下,但是无法显示,测电压以后发现电荷泵都没打开。依然是先怀疑屏幕坏了,但这要软件上调不出才能下结论。我在回调函数里加了点串口输出,发现每次在SPI上发送一两个字节之前都会先设置D/C的电平,该电平需要写595才能设置,而写595会破坏OLED的片选信号,导致始终没有完整的指令发送给OLED。解决方案是切断原来595输出到D/C的连接,改用飞线连接到另一个未使用的GPIO上。

后来屏幕成功点亮。初始化中执行了清空缓存并更新现实,但是屏幕上只有前8行像素被清零,其余的很随机,是未初始化的状态。最后发现是队友把初始化函数选错了,应该选择后缀为_f的,在内存中存放整个显存。

debug的这几天,新做的PCB也到货了,是24个模拟电路模块,两个方向上都是20*4+15+1间距*4=99 mm。没有做V割,因为出不起拼板费。我新买的小台锯就是用来切PCB的,只是放在了家里。我想着学校应该会有可用的设备,但跑遍学校也只有激光切割机,工作人员说没有切过PCB,试了一下的确不行。没办法,只能让家里人把台锯送过来。

10月8日,假期的最后一天,去实验室熟悉了一下设备。直流电源、信号发生器和数字万用表比较平凡,示波器得好好研究,尤其是触发功能。那天去的实验室里是Rohde & Schwarz的示波器,不小心碰了一下屏幕以后我们发现它竟然是触摸屏的!总之就是非常厉害。焊了一个滞回比较器模块,用带噪音的正弦波作输入,输出没有在输入接近地时快速抖动,这说明实验是成功的。在这种时候做这么简单的实验只是想找回一点自信心,因为此前几天的debug过程让我们非常担心比赛时会继续遇到各种各样的bug。

缝合怪的电赛纪实-LMLPHP

↑VSSOP-10封装,0.5 mm引脚间距

10月9日当然没什么心情上课啦。晚上调着DAC和DDS模块,突然群里轰来好几个文件,一看竟然是比赛题目!是网站方的失误,把题目提前放了出来,组委会也决定将错就错,提前开始了比赛,但不提早结束。

我们开始选题。A题涉及两个早就公布的芯片,我们查过淘宝上已经有很多方案了,可见别的组都早有准备;B题电源,没学过;C题小车,没学过;D题无人机,没学过;E题模拟电路,是我们准备过的方向;F题上海不选,能选也不会;G题识别,没学过。综上所述,选E题

我连夜写了一份非正式报告,包括实现方案、附加功能、可能遇到的问题等,写到早上5点。10日,教授说电子系的全都选了E题,我切身体会到了什么叫内卷。

但是换题已经没有可能了,我们只好卷下去。我们先去另一间实验室熟悉环境——我们将在那里待上4天3夜。开了一台示波器,从Windows的logo就开始不对劲,开机以后发现是花屏了。又开了一台,也花屏。我开始怀疑这间实验室是废旧仪器仓库,后来教授带着终于凑齐了一套好用的电源、信号发生器、示波器和万用表。

审题放在下一节讲。方案离不开共射放大电路,我开始用Multisim来调参数。无失真、顶部失真、交越失真都没有问题,双向失真暂时没有思路,而在底部失真的仿真中我发现了很奇怪的现象:底部失真本应是输出波形的底部被削平,但仿真结果却是底部波形上翻。用面包板搭建电路后也是如此。问教授后得知一般发射极都要加电容,在保持直流工作点的同时提高放大倍数。这下双向失真也很容易实现了。至于不加电容时波形会上翻,是因为集电极电压不能低于发射极,而放大倍数不很大时基极电压的变化就会反映在输出上。

队友用面包板搭建了一个两级负反馈三极管放大电路,本来打算用作前级小信号放大的。搭面包板的过程中,我们发现还是自己带的电阻电容比较齐全。

晚上,队友写了个C++程序试试FFT和THD。把FFT采样频率改到与正弦波频率的几倍差一点,发现THD比较小,这说明THD的测量对采样频率的精度要求不高。

说说别的组。我一早上就去实验室了,其他3组到中午才有人来,有一组的另两个人晚上才来,这才完成了选题。此时,我们已经有了方案,就等第二天开始制作了。

缝合怪的电赛纪实-LMLPHP

↑模拟电路模块

我一开始的想法是设计5个放大电路,用模拟开关来切换通道。教授说可以通过改变放大倍数实现,我觉得这个想法也不错,但是仔细思考又感觉不对:放大倍数小的时候是正弦波,大了以后要么顶部失真,要么底部失真,再大变成双向失真,所以为了同时获得顶部和底部失真需要调整直流工作点;而且这种方法不能涵盖交越失真。这些意味着需要好多级的切换。虽然各种模拟开关我都有,但是会让设计的层次不那么分明,比较复杂。最终还是采用了原来的方案。

在内卷的竞争中,我们必须在附加功能上花工夫才能脱颖而出,所以除了题目要求的功能以外我们还打算加入波形显示和声音输出的功能。

设备共由4块板组成:一块洞洞板对外接12 V电源、输入和输出,板上有LDO、模拟开关和三极管放大电路,以及与其他板连接的引脚,直流工作点都可以用电位器来调整;一块洞洞板上焊接两个模块并粘了一个扬声器,负责信号的运算,即把12 V范围内的信号耦合到5 V以内;一块开发板;一个IO模块作UI。

算法部分,FFT选用了Arduino FHT库,以1 kHz的32倍频采样,FFT规模256,计算16位线性幅度。然后根据题目里的公式取相应的FFT结果来计算THD——fht_lin_out[0]是直流分量的幅度,[8]是基频,[16]是两倍频等。一开始是16倍采样频率,规模128,为了精度和频率分辨率再加一倍,但是效果不太理想,最后只能够用就行了。

在25 MHz主频下,1 kHz的32倍频对应781.25个周期,尽管无法让ADC采样精确地发生在分数周期时刻,但也要避免0.25周期累加产生的影响。为此,以4次为周期,OCR1A寄存器的值取1次781、3次780。后来发现这点精度损失完全可以忽略不计。

IO模块的按键是事件驱动的,按下时IO模块会通过I²C通知开发板。程序切换一个波形后,需要先等待1秒让直流工作点稳定,再以0.5 s为周期执行采集、计算、显示等操作,只有空闲的时候才允许退出。尽管定义过事件启用禁用的指令,但是一是怕出现一些破坏原子性的情况,二是异步终止一个过程的流程不好写,所以我又把异步事件改回同步——在回调函数里设置标志位,在流程里需要的时刻轮询。

THD计算中需要开根号,FFT到频谱图需要取对数,都可以用二分算法来实现。开根号的过程是对结果进行二分查找。一个小细节是AVR开发中int默认是16位,uint16_tuint16_t会溢出,要先转换为uint32_t。取对数是把0-10000映射到0-48。先在Excel中生成指数表,作为数组放进程序,写一个类似std::lower_bound的二分查找算法获取index,即为结果。

示波器稳定图像的方法是使用触发功能,基于这种想法我们在256长的数据中找出第一个在512±16滞回比较器中的上升沿,作为2波形周期64项的开始。后来有了硬件滞回比较器,但还是沿用了软比较器作触发。

最后一天还想加点功能,教授说题目里没有明确说明要测THD的都是1 kHz的信号。用FFT直接计算基频是不准的,因为频率分辨率只有31.25 Hz,但FFT能给出基频的范围,即第一个峰值附近,我定义为比左右两边都大且超过最大值的一半的第一个值。找出这个index,把它右边一格对应的周期作为周期的下限。然后用连接到硬件滞回比较器的GPIO检测上升沿,累加上升沿间隔直到超过下限,记录下经过的周期数。测量16次,排序,取中间8个的平均值作为信号的周期。然后32倍频ADC采样,后续流程相同。

缝合怪的电赛纪实-LMLPHP

↑滞回比较器的输入与输出波形(先用运放凑合一下)

11日开始制作设备。上一次焊洞洞板还是刚接触PCB的时候,那时做了一个电位器调音量的电路,最后失败了,所以我对洞洞板一直不太看好。这次画PCB已经来不及了,只好硬着头皮上洞洞板了。

洞洞板上共有6个放大电路:前级放大和5种波形对应的电路。每个子电路和电源都连接到排针作输入输出,两级之间暂时用杜邦线连接。先焊好前级,测试一下没问题,心里就有底了。一下午焊完了6个电路,调好电位器后都能正常工作。

反面的接线挺乱的,尽管元器件是按照信号方向从左到右、电压高低从上到下的规律排列的。典型的如GND分布在每个子电路的多个电阻上,我直接用电阻的引脚拉到旁边的GND上,每个子电路汇聚到一个点再接到最下方的一长条焊锡上。有些短程的连接要跨线,我用剪下的引脚或飞线跨接。

模拟开关进入战局后情况就变得不太乐观了。首先是我不知为何要用两片CD4051,其实只要输出端一片就够了,这几乎把接线的复杂程度翻了一倍。模拟开关工作在12V,单片机输出信号需要转换才能用,我就在洞洞板背面用贴片三极管和贴片电阻焊了3个反相器,输入接排针,输出用飞线接到两片4051上。这一步很困难,本来就是三极管集电极和贴片电阻之间一点点地方,还要接到两个引脚上,还那么靠近。为了测试方便,在排针上面还加了一排高电平,这样用跳线就可以选择通道了。然后又把LDO、输入输出耦合等加进来,一步步地很费时间,做完已经是第二天凌晨了。

我们需要为4块板找一块底板固定起来。实验室找不到新的大洞洞板,只能用一块用过一点的大板,把用过的部分切下来。然后用台钻钻3 mm孔,用铜柱和螺丝固定上去。板上的电源和输入输出用的是香蕉和BNC插座,为了焊到板上也是要先钻孔。BNC插座的外层不沾锡,我拿从杜邦线中拆出的铜丝绕引脚一圈焊上,才使连接稳定。

最后我们听取教授的建议,为了稳定把杜邦线连接改为焊接,我的队友完成了这项工作。第三天晚上完成这些,然后一起把报告写了,队友负责软件部分,我负责其余全部。最后一天做的就没有写进报告里了。

缝合怪的电赛纪实-LMLPHP

↑成品

实际过程当然没有这么顺利——又到了喜闻乐见的debug环节。先讲硬件问题。

虚焊出现过两次。用探头测波形,要按下才会有波形显示,开始几次稍微用力即可,后来要按到底才行。从侧面看,按到底的时候焊点已经碰到了桌面,所以我推测是焊接问题。把用转接板和排针连接的三极管换成直插的,解决了问题。此后三极管只敢用直插的了。还有一次是用锡走线没有连接完全造成的。

输出失真波形的平直段倾斜,测了各级的输出后发现是最后一级耦合到地的问题,把105独石电容换成10 μF电解电容解决了问题。

功放芯片只找到LM386。队友按照datasheet上的电路焊了洞洞板,但扬声器里的声音是一定频率的爆音,且爆音频率与输入电压正相关。想到可能是输入耦合的问题,加了电容但是没有改善。我又用面包板搭了同样的电路,换了多个芯片,都是同样的问题。此时她正因为前两天没能帮上什么忙而难过,这下电路没做成更加沮丧,而我也一下子没想到什么可行的方案。但我深知不能两个人一起颓废,情急之下我想到了之前做的模块(还记得准备阶段的目标吗?),正好有一个用三极管扩大运放输出电流的模块可以用作功放。板子早就切好了,焊上电阻电容二三极管,外部只需再加一个电解电容,扬声器就能响了。

12日晚,临写报告时又读了一遍题,发现忘了题中的K1K2两个SPDT。实验室一时半会也找不到开关,我急中生智把开发板上两个用不到的拨动开关拆了下来,焊在洞洞板上,再小改了一下接线。

然后是软件问题。动手写代码之前我就意识到了异步事件的问题,我的队友是搞不定的,所以我就安排她把ADC->FFT->THD的过程写成一个函数,不用管函数调用的位置以及前后需要做什么。但是吧,问题就在于机械革命到了Ryzen这一代品控还是继续差,队友的笔记本掉屏幕了!还好掉得早,已完成的只占一小部分,后面的代码就都是我在我用了近三年的笔记本上写的了。这是电脑的硬件问题,开发过程中的软件问题。

第一次跑半完整程序的时候,启动时间远长于预期,至少到了秒的数量级,而正常的启动时间应该不到100 ms。在代码一开始加上翻转引脚电平的指令,接逻辑分析仪发现启动过程有三次复位,并且都是硬件复位。我把初始化过程中调用的函数一个个注释掉,最后发现是ADC的初始化导致了复位。进一步分析,可能的原因是开启ADC时AREF上的0.1 μF电容会被充电,使5 V电压突然下降,降到了brown-out电压以下,触发单片机复位。我把brown-out电压从4.3 V改到2.7 V,单片机就不再复位了。

用逻辑分析仪看波形时发现I²C指令之间有10 ms量级长的间隔。两边用的I²C驱动是我自己写的异步写同步读,只有在缓冲区满时才会阻塞,从机端的处理也全部在中断中进行。仔细观察波形后我发现是从机在地址字节之后把SCL拉低了,阻止主机发送后续数据,而这是因为程序没有及时进入相应的中断去写寄存器,实际上程序还在处理上一个绘图指令,绘图是比较耗时的。我想到可以用类似的方法把指令放在缓冲区中,在定时器中断中取指令并执行。后来发现效果不怎么理想,一是间隔仍然存在,二是缓冲区受不了128根柱子的数据量,造成数据丢失,所以又换回来了。

Arduino FHT输入为16位带符号数,ADC只能读到10位,这样输出的幅度很小,精度不高。把ADC读到的值左移6位再在低位补上高6位作为输入,却得不到正确的FFT输出和THD值。检查后发现忘记考虑符号了,只需把MSB翻转即可。

开发板处多次出现内存不足的情况。一开始FFT规模为128,OLED缓冲区占1KB,要显示的字符串太多,占用了很多内存,我就把它们移到flash中去,留出了一百多字节的空余内存。后来FFT规模改到256,内存怎么都省不出来了,只能改OLED的显示方式,只给它分配256字节显存,每幅图像分4次绘制。

以上bug其实占用了有效比赛时间的大部分,所以我其实是没什么时间睡觉的。每天回一次寝室拿东西,晚上睡在实验室楼下大厅的沙发上。有一天打算1点睡,结果debug到4点才睡;又有一天打算4点睡,队友开玩笑说我怕是要7点睡了,结果我真的7点准时叫她起床了。

缝合怪的电赛纪实-LMLPHP

↑10月4日,外滩灯光秀

比赛结束后我好好睡了一晚上,第二天睡了一上午、一中午、一下午和一晚上。

15日校测,我们带着箱子到陌生的实验室测试。我们复原后测试时,发现双向失真的输出波形达不到2 Vpp了,调电位器也没有用。我想起制作时因为双向失真输出峰峰值明显高于其他几个波形,就把下端的分压电阻并联上一个相同阻值的。现在只要把这个额外的电阻取下来就可以了,不用动电烙铁,直接用钳子剪断一端就好了。

我们很快就准备好了,开始关注其他组的情况,尽管不能看。有一组没有加K1K2,从而有项目无法测试。我之前注意到这点的时候思考过这两个SPDT有什么用,思考的结果是为了验证THD是真实的,即给一个外来的波形要求测THD,否则THD是可以用现成设备测好后硬编码进程序的,尽管我在实验室没有找到过可以测THD的设备。大三的几组都做了很大的板,板上甚至有三极管的阵列,以至于我以为他们是选电源题的。后来听说这是他们为了大作业做的。

我们的测试很顺利,并且两个开关的功能正如我所预料的。题目要求能测量1 kHz、2 Vpp的方波和三角波的THD,然后与标准答案比对。我们的设备测量得有一点误差,但在合理的误差范围内。

到了18日位于隔壁华师大的市测,测试的项目大致相同,波形换成了理论THD为8%和20%的,频率仍然是1 kHz。进入这个模式前专家先告诉我,设备显示的THD要能在切换波形时自动更新,这是一个得分点。专家让我调设备进入测量模式,此时还没有信号源,因此我以设备需要先测量信号频率为由拒绝了,并要求先开启信号源,专家同意了。因为要自动更新,所以得在屏幕上THD的瞬时值和平均值中选择前者来读,第一个是8.5%,第二个20.5%,上下波动均不超过0.2%,我很满意。

最后得了一等奖,但此时这已经是次要的了。我很累。听说电赛不能找室友组队,因为室友以后还要一起住;而我选择了最亲近的人一起参赛,可以想象我们经历了多少困难。如果还有机会,我不想再参赛了。

回想起来,比赛的几天我没有过任何重大技术突破,所有技术要点都是炒冷饭。那么时间都用哪去了呢?大概是以上十几个bug吧。我不想以后回忆起这段参赛经历只想得起我睡得多少、有多累,而是经验——那些踩过的坑、避过的雷、de过的bug,故为此文。

那些借用来的技术要点以及它们的来源包括:

  • I²C通信、整个流程控制框架,来自我高中时的课题,本博客没有;

  • 三极管放大电路、FFT,自制蓝牙音箱的手册

  • 示波器,AVR单片机教程——示波器

  • 二分算法,来自遥远的C++、DSA记忆;

  • 硬件、软件的debug经验,分布于我的开发经历中;

  • 等等……

我愿称之为“缝合怪”。

缝合怪的电赛纪实-LMLPHP

↑自己做的电赛限定版PCB钥匙扣

10-28 05:39