縫合怪的電賽紀實
2020年9月21日,我突然收到一位教授的邀請。這位教授是我高中時課題研究的指導老師,他知道我的電子與計算機大概是什麼水平,而他邀請我參加的正是電子設計競賽。
我去做了點功課。往年,電子設計競賽都在暑假裡舉辦,今年因為特殊情況改到開學以後。教授則是學校裡負責該競賽的“出口”。因為以上種種機緣巧合,我才能有這次機會。
比賽需三人組隊,這是比較難辦的,相比之下歷屆題目比較簡單,如果是我來選題的話。我所處的理科試驗班都是些次頂尖的學數學物理的人,找不到人一起參賽,只能先把密院ECE專業的女朋友拉下水,讓她再去找一個人。有幾個人表示有興趣但是沒時間,儘管現在看來“有興趣”的意思是“有興趣一起拿個獎”。我們找了一個人聊聊,我簡述了我準備比賽的計劃,對方只是點點頭,對著電腦不知看些什麼。結束以後我跟女朋友商量說,要不就我們倆參賽吧。
中午我們去電院開會。除了我們組之外,別人都是人工智慧大二學生,參賽可以在電子電路系統實驗課程中得高分。我是來體驗一下的,因為我一直不清楚自己的水平。以前做過幾個專案,但都是自己定的目標,現在要在別人出的題中選一道完成,我能做到嗎?我的專案都是肝出來的,從原理圖到PCB到焊接到程式設計一般都要一個星期,現在要在4天3夜裡完成,我能做到嗎?一直都是一個人做的,現在要合作,雖然都是自己人但還是跟獨立完成不一樣的,我能做到嗎?
所以我要好好準備。仔細分析了一下2019年的題,感覺只有C題和D題是有可能做的。這類測控題好就好在只需電路,不涉及小車、無人機等機械結構,後者是我沒有接觸過的。
我備賽的宗旨“別讓比賽過程中的小事搞壞心態
這樣一來就可以決定要作什麼準備了:
-
再做一塊我們熟悉的開發板;
-
做一個IO模組,放常用的輸入輸出裝置,它通過I²C與開發板連線;
-
設計一系列類比電路模組,實現各種常用功能,如線性運算、精密整流等。
當時還在準備9月30日的數分高代考試,就沒有直接參與準備,把IO模組的設計丟給女朋友讓她設計年輕人的第一塊PCB。要說工作也是有的,我在淘寶上下了12個單買元器件和模組。
↑有關的、無關的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的RES
和D/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。
↑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組到中午才有人來,有一組的另兩個人晚上才來,這才完成了選題。此時,我們已經有了方案,就等第二天開始製作了。
↑類比電路模組
我一開始的想法是設計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_t
乘uint16_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取樣,後續流程相同。
↑滯回比較器的輸入與輸出波形(先用運放湊合一下)
11日開始製作裝置。上一次焊洞洞板還是剛接觸PCB的時候,那時做了一個電位器調音量的電路,最後失敗了,所以我對洞洞板一直不太看好。這次畫PCB已經來不及了,只好硬著頭皮上洞洞板了。
洞洞板上共有6個放大電路:前級放大和5種波形對應的電路。每個子電路和電源都連線到排針作輸入輸出,兩級之間暫時用杜邦線連線。先焊好前級,測試一下沒問題,心裡就有底了。一下午焊完了6個電路,調好電位器後都能正常工作。
反面的接線挺亂的,儘管元器件是按照訊號方向從左到右、電壓高低從上到下的規律排列的。典型的如GND
分佈在每個子電路的多個電阻上,我直接用電阻的引腳拉到旁邊的GND
上,每個子電路匯聚到一個點再接到最下方的一長條焊錫上。有些短程的連線要跨線,我用剪下的引腳或飛線跨接。
模擬開關進入戰局後情況就變得不太樂觀了。首先是我不知為何要用兩片CD4051,其實只要輸出端一片就夠了,這幾乎把接線的複雜程度翻了一倍。模擬開關工作在12V,微控制器輸出訊號需要轉換才能用,我就在洞洞板背面用貼片三極體和貼片電阻焊了3個反相器,輸入接排針,輸出用飛線接到兩片4051上。這一步很困難,本來就是三極體集電極和貼片電阻之間一點點地方,還要接到兩個引腳上,還那麼靠近。為了測試方便,在排針上面還加了一排高電平,這樣用跳線就可以選擇通道了。然後又把LDO、輸入輸出耦合等加進來,一步步地很費時間,做完已經是第二天凌晨了。
我們需要為4塊板找一塊底板固定起來。實驗室找不到新的大洞洞板,只能用一塊用過一點的大板,把用過的部分切下來。然後用臺鑽鑽3 mm孔,用銅柱和螺絲固定上去。板上的電源和輸入輸出用的是香蕉和BNC插座,為了焊到板上也是要先鑽孔。BNC插座的外層不沾錫,我拿從杜邦線中拆出的銅絲繞引腳一圈焊上,才使連線穩定。
最後我們聽取教授的建議,為了穩定把杜邦線連線改為焊接,我的隊友完成了這項工作。第三天晚上完成這些,然後一起把報告寫了,隊友負責軟體部分,我負責其餘全部。最後一天做的就沒有寫進報告裡了。
↑成品
實際過程當然沒有這麼順利——又到了喜聞樂見的debug環節。先講硬體問題。
虛焊出現過兩次。用探頭測波形,要按下才會有波形顯示,開始幾次稍微用力即可,後來要按到底才行。從側面看,按到底的時候焊點已經碰到了桌面,所以我推測是焊接問題。把用轉接板和排針連線的三極體換成直插的,解決了問題。此後三極體只敢用直插的了。還有一次是用錫走線沒有連線完全造成的。
輸出失真波形的平直段傾斜,測了各級的輸出後發現是最後一級耦合到地的問題,把105獨石電容換成10 μF電解電容解決了問題。
功放晶片只找到LM386。隊友按照datasheet上的電路焊了洞洞板,但揚聲器裡的聲音是一定頻率的爆音,且爆音訊率與輸入電壓正相關。想到可能是輸入耦合的問題,加了電容但是沒有改善。我又用麵包板搭了同樣的電路,換了多個晶片,都是同樣的問題。此時她正因為前兩天沒能幫上什麼忙而難過,這下電路沒做成更加沮喪,而我也一下子沒想到什麼可行的方案。但我深知不能兩個人一起頹廢,情急之下我想到了之前做的模組(還記得準備階段的目標嗎?),正好有一個用三極體擴大運放輸出電流的模組可以用作功放。板子早就切好了,焊上電阻電容二三極體,外部只需再加一個電解電容,揚聲器就能響了。
12日晚,臨寫報告時又讀了一遍題,發現忘了題中的K1
和K2
兩個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點準時叫她起床了。
↑10月4日,外灘燈光秀
比賽結束後我好好睡了一晚上,第二天睡了一上午、一中午、一下午和一晚上。
15日校測,我們帶著箱子到陌生的實驗室測試。我們復原後測試時,發現雙向失真的輸出波形達不到2 Vpp了,調電位器也沒有用。我想起製作時因為雙向失真輸出峰峰值明顯高於其他幾個波形,就把下端的分壓電阻並聯上一個相同阻值的。現在只要把這個額外的電阻取下來就可以了,不用動電烙鐵,直接用鉗子剪斷一端就好了。
我們很快就準備好了,開始關注其他組的情況,儘管不能看。有一組沒有加K1
和K2
,從而有專案無法測試。我之前注意到這點的時候思考過這兩個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經驗,分佈於我的開發經歷中;
-
等等……
我願稱之為“縫合怪”。
↑自己做的電賽限定版PCB鑰匙扣