最近無聊把一些跟自己年紀差不多大的老綜擴拆來研究(就是那種一兩張小朋友就能弄到手的殺肉件, 剛入兩聲道還很窮的時候放在客廳頂著聽得那種), 然後就發現這個時期數位化還沒有很嚴重, 中小瓦數( 每聲道小於50Watt) 的機器非常適合用來改造放置 DIY 的耳擴或者DAC線路, 最明顯的缺點也就是它的優點: 機殼空間超大, 有人喜歡迷你一點的外型可能得失望(比如 Timoball 的 DYNALO MK1 的類型), 不過對於喜歡亂加東西跟配線手殘的 DIYer 可能會愛死這種改法. 這個改裝範例就是拿 SONY 的 F410R 來塞 Soekris DAM1121 的 DAC 系統, 這種1990 年以前的機器模組化都做的不錯, 很多零件找到接點都可以繼續沿用,不像2000年後數位化很嚴重的低價機種(螢幕顯示很花俏的那種)常常前面板控制的 MCU 一掛掉就等於全掛掉, 電路板一整塊想改也沒地方下手.
之所以選小瓦數的低價機種來改是因為這類產品通常變壓器會擺在左側或右側, 留下比較完整的空間可以利用, 大瓦數的通常會是主變壓器放正中間, 擴大電路放兩旁的設計反而不好擺. 有待機功能(通常是有遙控機能的)會有一顆小瓦數的變壓器作為面板供電, 用 relay 來切換主變壓器是否過電, 好一些的甚至兩個聲道獨立給一顆變壓器, 用起來彈性更大, 比如這台改完我開始肖想找一台 Denon PMA-1500R 來當殺肉機
裝機架構與理論部份
趁著清明連假在家沒事幹的時候也大概畫了系統方塊圖, 畫著畫著很懷念以前學生時代, 要是沒 quit 有機會繼續走學術那條路, 大概會常畫這類概念圖, 搭點實驗數據跑 confererence 貼 poster 吧.為了幫助大家了解也加註中文版本
試裝機後覺得機殼的空間還夠就設計了類比部分可以切換供電模式 (鋰電池模組或超級電容模組), 這部分零件採購下去了等外部測試正常了以後才會改裝進去, 虛線部分的耳擴線路就是比較後期才會執行的計畫, 手邊從 Timoball 兄取得的 Dynalo SUSY 做好調完以後再抓料用手邊的空版做一組進來當成緩衝前級, 到時候就可以到處抱來抱去玩踢館(誤)
控制上與一般市面常見的 Soekris 1021 的差不多, 不過數位音量控制(衰減)不像 1021 板上的 MCU 有做好的接口直接接上可變電組就能用, 必須自己外掛一顆 MCU 走 3.3V 的 UART (serial) 來發指令給 DAC 主板 (送一堆 V+00, V-10, V-11 之類的字串), 基本上要切換數位濾波器, USB/SPDIF 輸入或者相位反轉(最近做了阿仁的仿 Klimo Merlin 管前級才發現這個功能有用到)都得透過這個接口, 再加上想做搖控 VR (IR 偵測器收遙控訊號, 用 PWM 驅動 VR 的DC 小馬達), 額外加入的電源管理系統還有繼電器控制, LCD 液晶顯示等等, 最後考慮到擴充性選了 arduino Due 來用
這種用 32-bit Cortex 當核心的開發板用在這種沒有牽扯到 DSP 的簡單音響電路控制來說都效能過剩了, 以穩定性而言其實 Arduino Mega 比較好(5V tolerant 也方便), 從體積來說大家也傾向用 nano 這種拇指大的版子 (比如一些 Soekris 1021 的底板), 我使用 Due 的唯一原因僅僅在於它的 UART 是3.3V 位準的不用再另外加電路 XD 又因為匪區作的 Due 很便宜所以手邊常備十來片, 測試的時候寫完懶得移植乾脆繼續用. 不過考慮到多工(比如完全獨立的電源控制系統)其實用徳儀 TI 可以跑 real-time OS 的 cortex 開發板應該會更適合.
因為機箱超大所以DIY的電路模組非常好擺, j唯一的麻煩事是固定, 對於懶得鑽孔的人來說有三樣神器是少不了的: 矽膠貼, 熱塑土跟塑膠束帶. 前兩樣玩意都是因為很厲害的 facebook 廣告推坑,好奇買下而一試成主顧的玩意. 矽膠貼黏在背面擋著 PCB 焊點用束帶一綁馬上就做好定位. 當然擺位確定了以後可以直接弄個鋁塊當底板擺進去, 之前的束帶一剪, 矽膠貼拿下電路板便可以完好無損的裝在新地方
綜擴什麼不多洞最多, RCA 直接裝, Speaker Out 可以放 USB, 放 toslink 或平衡座.
熱塑土配合銅柱可以做一些簡單的固定, 要塑型吹風機或熱風槍噴一下就行.
以 Soekris 的類比部分耗電來說 (Shift register 跟 R2R ladder ) 耗電量不大, 5V 20mA-50mA 左右, 考慮到板子上跟上一級 LDO 的效率大概不到 0.5 Watt,用一般的鋰電來供電算一下至少 20-30 hr 以上, 原本想說加個電壓偵測自動切換供電想想也不太需要, 直接設計成待機充電即可. 不過FPGA 的數位部分相比之下就是吃電大怪物了, 幾乎是10倍以上的功率. 這部分跟感覺跟音質無關就懶得改了. 鋰電池模組選用一般具有放電保護, 防過充應該就可以.
因為耗電量不高所以稍為研究了一下是否可以使用超級電容來做, 一般來說超級電容的等效串連電阻 ESR 都不小 (20 mohm 以上), 串連後直接掛在最後一級穩壓效果大概也不好, 不過掛在二級穩壓之前利用快速充放的特性應該有類似電池供電的效果.以上圖的設計來說用 16V 2F 的模組可以達成 10sec 充電&穩壓供電與 80 sec 純電容供電的工作模式, 如果可以加大充電周期的電流(或者用定電流而非定電壓的方式, 後者幾乎要 5RC 左右的時間才能從 10% 充到 90% ) 還可以拉長電容供電的比例. 到底有沒有辦法聽出差異我想試測試的重點之一(笑)
這部分用 LTspice 稍為跑過模擬確認過 PWM 控制電容充放的條件 (掛 5V LDO 50mA 當負載), 一般來說放電周期電容的電壓掉到 LDO 轉出來電壓加個0.5V-1V 都是安全的, 用16V 的來說定壓充電設在 12V, 允許掉到 6V 的話電容的儲存功率大概是 0.5C(Vi^2-Vo^2) = 0.5*2*(12^2-6^2) = 108J, 單純供應 0.3W 的負載差不多可以稱6分鐘, 但是穩態操作的時候要考慮的是充電周期充進去的能量, 這個基本上用中點電壓乘上頻均充電電流可以估出, 上圖加了五歐姆的限流電阻把充電峰值電流限在 600 mA 所以只能弄出 1:8 的充電:放電穩定操作週期.
當然另外一種可能的做法就是用 16V 20F 的大型模組, 配合高電流充電做一個 turbo 功能: 快充一分鐘, 爽聽半小時之類的 XD 不過大電流操作還需要考慮到電池保護板的品質, 成本與體積也不見得適合做到同一個機箱裡, 不過設計成外掛電源箱也許還不錯.
不過模擬時發現一個小問題, 如果超級電容的ESR 超過 20 mohm 穩壓輸出就會爆出 mV 大小的小突波, 這個在充電迴路掛電感啥的都弄不掉, 可能得實際把東西做出來以後量測才能確認是模擬才有的問題還是真得有這個突波. 原則上加大電容值電壓的浮動範圍還會限制地更小.控制充放的開關第一版設計用單向的 MOSFET, 正式版大概會改成可以過大電流的photoMOS (panasonic 出的方便小玩意, 內阻低又有隔離效果).
硬體實作 (試驗機)
最後的版本選擇先做了電電池供電切換, IR remote 遙控 VR 馬達(控制都想好卻發現原來機殼上的音量 VR 馬達已經故障了), 超級電容模組與前級電路板等到下個階段再設法補上. 極性切換因為面板上剩餘的 VR/開關 不好拉所以也先擱著.
除了電池組以外加了一塊 24VDC 的線性穩壓給繼電器供電 (from 30VAC), 原本想用專門出給 arduino 的 5V繼電器模組來減少體積(還可以直接用MCU板上的5V供電),不過手邊陸製品品質太差最後還是乖乖用自控可替換帶插座的 Omron 製品, 以後有空再用前級切換訊源用的小顆繼電器(takamisawa) 洗板. MCU (Arduino Due) 的供電則是直接從 AC 輸入拉兩條線裝了一顆 5V2A 的迷你交換式電源黏在主變壓器上.
面板簡單黏了一個 16x2 的 LCD 來顯示主要參數, 其實用 I2C 的轉板可以把接線降到只有4條, 可惜 Due 的函式庫處理起來有點小問題, 只能等下個版本用 Arduino Nano 來 layout 轉板才能解決才能解決. 用這種老模組的好處是背光跟字色的選擇很多, 甚至還有真空螢光 VFD 得可以選用, 控制都一樣可以直接替換使用. 如果要加入取樣率等資訊的話恐怕要 16x4 的大小才塞得下.
|
改成夜視效果較佳的藍光白字 LCD 屏後順便打打方波檢查一下步級響應來確認數位濾波的切換是否正常, 切換延遲的狀況.
|
Soekris 的類比 +-5V 用 TPS7A47/33 的 LDO 模組, 輸出並上台製 lelon 的 OCRZ 系列的6.3V 1000uF 固態電容 (光華隨便逛找到的現貨, 查查規格覺得OK就用了), 儀測發現有效果就加了上去.
|
lelon 的產品中 OCRZ 是 OP-CAP 中屬於 low-ESR 的版本, 以 6.3V 1000uF 來說差不多只有 7m ohm, 再上去選 ORE 意義不大, 因為接觸阻抗搞不好就比這個差距大 (當然一些長期的衰退數據還是有差). 更新一些的 OP-CAP* 系列是超長使用壽命的版本, 對 DIY 玩家來說大概還沒用到這玩意衰退可能就手賤拿其他家產品換著玩, 沒必要多花錢在上面.
|
MCU 的硬體接線大概是這樣, LCD 如果使用 I2C 轉換板來控制的話只要接四條線, 一些開關類型的控制因為要利用舊擴大機上的 VR 來模擬所以都利用了類比輸入來偵測, 要注意的地方是原來前面板上的 VR .有些點是接地的, 這個時候接來用的時候就要考慮 VR+, VR- 不能顛倒. 可惜音量 VR 上的馬達故障了就暫時沒做搖控音量.
最後裝上光纖 SPDIF 輸入, 需要的 3.3V 從 10V DC 再加一片 LM317 降壓過來使用, Toshiba 的 toslink 模組黏在 USB 輸入的上方直接飛線最短距離接到 soekris 板上. 基本上完全相信 reclock 的效能也沒有特別去處理什麼屏蔽, 反正沒有爆音就當它是正常的.
最後背板外觀大概是這樣, 未來的平衡輸出接口大概得在單端的 RCA 輸出上鑽孔才能搞定
整體布局大致上照著系統方塊圖走 (因為做到一半才開始畫系統方塊圖, 不一樣就見鬼了), 前級電路的位置估計還有機會塞進去 (電源精簡化, SPDIF 走線讓一下)
老實講最土的那種綠光黑字 LCD 顯示幕搭這種老派黑殼擴大機面板反而更有味道, 不過反白字的優點環境光源不佳的時候才會發揮出清晰, 高對比的優點.
聽感方面的驗證一樣用 HD800 跟 AD2000X 來作, 最近回家只聽兩聲道 HD800 反而被丟在公司無聊時聽. 之前裸機直上的時候用這些器材聽過一陣子了, 沒有裝機失誤, 錯誤的地迴路讓不應該出現的雜訊竄進來應該不會有太大的變化, 以儀測來說 noise floor 感覺有稍微低個 3-5dB, THD@-1dB量到 0.004% 也稍優於宣稱規格, 主要還是想認真聽看看數位濾波的差異與電池供電的改善.
搭耳擴的部分 Schiit Lyr 配 JJ 金腳新管, 主要搭 AD2000X, HD800 單端稍微聽個大概.光纖的部分算新加上, 還得花時間比較看看與 I2S 的差異. 最終電池供電的部分以這個組合來說倒是沒聽出與 LM317/337 AC-DC 有太大的差異(老實講後面還會再過兩級 LDO 穩壓), 也許需要找金耳朵來作盲測才知道有沒有差異, 不過我自己肯定過不了.
MCU 控制部分
先放上程式碼, 待續
dam1121controllerv1.ino | |
File Size: | 5 kb |
File Type: | ino |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | #include <LiquidCrystal.h> //lcd (RS, EN, D4, D5, D6, D7) LiquidCrystal lcd(35, 37, 39, 41, 43, 45); String Str; String StrEcho; String volumeLCD; int volume=0; int volumeReg=80; //initial -80dB int FSet[4]= {46,48,50,52}; // green,yellow,orange,red int relayVoltage[4] = {47, 49, 51, 53}; int filterSelect = 4; int filterSelectReg = 4; // intial Filter = F5 int polarity = 1; int polarityReg = 1; //inital polarity = Normal int batteryStatus = 0; //inital batteryStatus: AC-LDO int inputSelect = 0; int inputSelectReg = 0; //initial inputSelect: USB //LCD character byte ldoSymbol[8] = { 0b11111, 0b11011, 0b11011, 0b11111, 0b11111, 0b11111, 0b10101, 0b10101 }; byte batterySymbol[8] = { 0b01110, 0b11111, 0b10001, 0b10011, 0b10111, 0b11111, 0b11111, 0b11111 }; void setup() { Serial1.begin(115200); //DAM1121 baud rate analogReadResolution(12); //lcd initialized lcd.begin(16, 2); lcd.createChar(0, ldoSymbol); lcd.createChar(1, batterySymbol); analogWrite(DAC0, 60); //set V0, 5V ver->60 lcd.setCursor(0, 0);lcd.print("F5: mixed "); lcd.setCursor(10, 0);lcd.print("PN"); //lcd.setCursor(9, 0); lcd.write(byte(0)); lcd.setCursor(10, 0); lcd.print("AC-LDO"); lcd.setCursor(13, 0); lcd.print("AC");lcd.setCursor(15, 0); lcd.write(byte(0)); lcd.setCursor(0, 1);lcd.print("Vol:-80dB"); lcd.setCursor(11, 1);lcd.print(" USB "); //DIO initialized for (int i=0; i<4; i++){pinMode(FSet[i],INPUT); digitalWrite(FSet[i],HIGH);} for (int i=0; i<4; i++){pinMode(relayVoltage[i],OUTPUT); digitalWrite(relayVoltage[i],LOW);} } void loop() { //power input selection if (analogRead(A1)>2600){ for (int i =0; i<4; i++) {digitalWrite(relayVoltage[i],HIGH);} lcd.setCursor(13, 0); lcd.print("BA"); lcd.setCursor(15, 0); lcd.write(byte(1)); } else { for (int i =0; i<4; i++) {digitalWrite(relayVoltage[i],LOW);} lcd.setCursor(13, 0); lcd.print("AC"); lcd.setCursor(15, 0); lcd.write(byte(0)); } //filter selection filterSelect = !digitalRead(FSet[0])*8+!digitalRead(FSet[1])*4+!digitalRead(FSet[2])*2+!digitalRead(FSet[3]); // 1: B position, 2:A position, 12:A+B position & off position //delay (100); StrEcho = Serial1.readStringUntil('\n'); Serial.println(StrEcho); //add to the end of case for diagnostic if (filterSelect != filterSelectReg){ switch (filterSelect){ case 1: setFilter("F7"," soft "); break; // B position, F7, soft case 2: setFilter("F6"," min "); break;// A position, F6, min case 4: setFilter("F5"," mixed "); break;// OFF position, F5, mixed case 8: setFilter("F4"," linear "); break;// A+B position, F4, linear default: setFilter("F5"," mixed "); break;// button fail, set F5, mixed } } //polarity selection //if (analogRead(A0)<500){polarity = 0;} else {polarity =1;} //if (polarity != polarityReg){ // switch (polarity){ // case 0: // Serial1.println("PI"); StrEcho = Serial1.readStringUntil('\n'); // lcd.setCursor(10, 0);lcd.print(StrEcho); lcd.setCursor(12, 0); lcd.print(" "); break; // case 1: // Serial1.println("PN"); StrEcho = Serial1.readStringUntil('\n'); // lcd.setCursor(10, 0);lcd.print(StrEcho); lcd.setCursor(12, 0); lcd.print(" "); break; // } //} //input Selection int inputSelectVR = analogRead(A2); if (inputSelectVR> 500){inputSelect = 0;} //USB else {inputSelect = 1;} //COAX if (inputSelect != inputSelectReg){ switch (inputSelect){ case 0: Serial1.println("I0"); StrEcho = Serial1.readStringUntil('\n'); lcd.setCursor(11, 1); lcd.print(" USB "); break; case 1: Serial1.println("I1"); StrEcho = Serial1.readStringUntil('\n'); lcd.setCursor(11, 1);lcd.print("SPDIF"); break; case 3: Serial1.println("I3"); StrEcho = Serial1.readStringUntil('\n'); //not used lcd.setCursor(11, 1);lcd.print("AUTO"); break; } } // volume Setting int volume = map(analogRead(A0),100,2000,80,0); volume = constrain(volume, 0, 80); int volumeDiff = volume-volumeReg; //diff > 1, volume+ if (volumeDiff>1){ for (int i=volumeReg; i<=volume; i++){ String Volume = String(i, DEC); Serial1.print("V-"); Serial1.println(Volume); StrEcho = Serial1.readStringUntil('\n');volumeLCD = StrEcho.substring(1,4); //ex. V-02 to 02 lcd.setCursor(4, 1);lcd.print(volumeLCD); delay(3); // Serial.println(StrEcho); } //diff <1, volume- }else if (volumeDiff<-1){ for (int i=volumeReg; i>=volume; i--){ String Volume = String(i, DEC); Serial1.print("V-"); Serial1.println(Volume); StrEcho = Serial1.readStringUntil('\n'); volumeLCD = StrEcho.substring(1,4); //ex. V-02 to 02 lcd.setCursor(4, 1);lcd.print(volumeLCD); delay(3); // Serial.println(StrEcho); } } // manual test //if(Serial.available()){ // Str = Serial.readStringUntil('\n'); // Serial1.println(Str); // StrEcho = Serial1.readStringUntil('\n'); // Serial.println(StrEcho); //} volumeReg = volume; filterSelectReg = filterSelect; polarityReg = polarity; inputSelectReg = inputSelect; StrEcho = Serial1.readStringUntil('\n'); //Serial.println(StrEcho); //delay(50); } void setFilter (String cmd, String lcdStr) { Serial1.println(cmd); StrEcho = Serial1.readStringUntil('\n'); lcd.setCursor(0, 0);lcd.print(StrEcho);lcd.setCursor(2, 0);lcd.print(lcdStr); } |