2019年1月12日 星期六

省錢是好事嗎

故事是這樣子的,最近有一位很紅的藝人 BBB 拍了高雄市拍了一部 MV,他的宣傳詞是這樣寫的:「BBB 自掏腰包為高雄拍攝的MV『來去高雄』,懇求大家幫助負債累累的高雄市政府不用花任何一毛宣傳費,就可以讓大家湧入高雄拚觀光!達到宣傳高雄的最大效果,請大家努力、用力的轉發分享MV,靠全民的力量達到最大成效!」
剛好最近,政府宣佈因為上半年的稅收有盈餘,同樣也引發一番爭辯,畢竟現在中華台北還有這麼多債務,稅收有多是不是應該先還債?對照組正好是台北市市長,在第一任任期中主打政績即是償還市府負債。

這一連串看完突然有一點感想,跟之前看書的心得結合一下來寫篇文章。

當然我本文無意支持或反對現下政府將稅金盈餘退,或者 BBB 的影片是好是壞有效無效,純粹就貼文背後的精神來評論。
我覺得這篇貼文剛好非常傳神的傳達了兩個概念:
1. 負債累累的高雄市政府需要幫助。
2. 不花政府一毛錢就是讚,大家要多鼓勵。
相對應到退稅議題上面,就是:
1. 負債累累的政府不應該再退稅。
2. 政府不該亂花錢,應該努力減少負債。
說起來有點華國人老一輩刻苦經營儲蓄的精神。

我們常被教導/認為的省錢就是好,負債就是壞,真的嗎?

假設我們地方政府一年歲入是 400 億元,而台北捷運第一期的工程就花了 4000 億,很明顯的如果政府是無法一口氣開工所有的捷運,而必須分年編列預算分年開工;相對的政府可以借債,一口氣先借 4000 億元把捷運蓋完,然後分年的慢慢還債。雖然後者利率會造成多一些成本,可是早點同時開工的話,可以在土地還便宜的時及早徵收,儘早擺脫交通黑暗期,減少道路車禍傷亡,這些都是獲利。
用個更簡單的例子,為何長輩當年都會貸款買房呢?負債不是不好嗎?更別提那個時候借錢利率高得嚇人呢。但借錢買房卻有它的道理在,可以更早享用住房,在房子便宜的時候先買,負債+房子也可能比滿手現金來得保值。
或者再簡單一點,假設今天持有台GG的股票,每年有 2% 的收益,而銀行的利率是 1%,請在「沒有負債沒有股票」跟「欠銀行 100 萬但有等值的台GG股票」兩者間做選擇,前者沒有負債,但以賺錢來看後者才是好的。

上面幾個例子,都在說明省錢是好負債是壞的概念,其實不一定正確,重點要看錢灑出去會換到什麼資產回來,然後評估那個資產合不合理。
我第一次意識到這個觀念,在之前閱讀<21世紀資本論>時裡面的一小段:「殖民時期英法兩國持有的國外資產和帶來的收益,足以使他們承受貿易赤字同時仍有收益……汲汲營營堅守貿易順差本身沒有任何意義,持有資本的根本目的就是能在不工作的狀況下,繼續消費、累積資本」。
同樣的,所有的資產應該要攤開來看,堅守黑字不是絕對,有錢無債只意味著持有「錢」這個資產,而放棄其他可能更高報酬,拿債滾錢的選擇;畢竟長期來看,隨著通膨錢這個資產可能是最沒價值的(你也可以說錢最不值錢XD),所謂保值,就是在比哪個資產價值流失得比錢還慢;政府可以把錢換成火車、航廈、或者乾脆一點換成未來的小孩也可能更賺,至少他們有機會在未來滾出更多小孩。

當然:省錢是好、負債是錯這是個很鐵的概念,直覺上來說很像真的,強者我同學台大財金系畢業一樣逃不過這觀念的束縛(雖然我私心認為他不是不懂,只是戴著有色眼鏡所以反對啦)。

另外一句:省錢是好的嗎?
同樣不盡然。
比如說,如果省錢很重要,為什麼我每天不走路回家呢?因為我搭公車 15 元車程大概 40 分鐘;走路 0 元大概 2-3 小時,搭公車多出來的時間,我可以多寫幾行程式多看一些書,都能比省下的公車錢更有生產力,在這裡,省錢意味著失去時間這項隱形的成本。
花錢可以換到一些隱形的東西,像是請老師是換他的經驗,買珍奶是換舌尖的快樂,或者那個讓人津津樂道的笑話:一個美國工程師拿一半的薪水請三個印度工程師,把工作都包給他們做,自己上班就可以爽爽過,買工程師是換自己的時間。
就像非常早非常早,當我剛被 Free Software 傳教的時候,都會有一段時間覺得為什麼不要整個社會、政府機關改用 Libre Office 就好了?作業系統全部換 Linux 啊,不是超省錢?碰久一點之後就會發現,要對應世界上這麼多、每個都不同規格的硬體,不像 Mac 筆電因為規格全是硬性規定,Windows 雖然沒事出點包然後使用上被大家幹到翻,在穩定、方便、統一規格上仍然把其他 Mac/Linux 壓在地上打,如果加上 Office 系列產品那又更不得了。
Window 可能要錢,但它有 Microsoft 在背後支持,有 bug 會幫你修,全系列的相容性他們會注意,花錢買得到服務;Linux 是免費的,但它沒有客戶支援,沒有相容性支援,整個體驗加上去可能是負的。
所以才有那句諺語:免費的最貴,不收錢表示它有缺點讓它不夠格收錢,那你看得透那個缺點嗎?花不花錢不是重點,重點是花了錢會換到什麼?權衡利弊,省不是一切。

事實上若綜觀國際商場的現況,諸如共享單車、網購平台、叫車服務等,這幾年都出現透過灑大錢補助來吸引使用者,負債買下市佔率,及早建立規模優勢排除競爭者的手法(當然排除之後活不活得下去是另一回事),這跟做好自己產品吸引消費者上門的手法大相競庭。
HTC 不就是一個血淋淋的例子,作為最早推出智慧型手機的廠商,後來卻慢慢在行銷、通路的資源戰上被對手壓了過去?
堅守省錢少舉債思維的中華台北,宛若二戰時日本拿全民精英射手對付美軍機槍的手法,賺到省下來的小利卻不一定擋不過人家用資源硬推出來的優勢,平常都稱讚中國狼性中國小確幸,來到行銷廣告的時候,眼下就來個標準的範例了,果然還是老話一句:一隻手指指著別人的時候,四隻手指指著自己。
啊不過話說回來我打了這麼多,個人資產配置還是都以現金為主,果然是:一隻手指指著別人的時候,四隻手指指著自己,啊哈哈哈…嗚嗚(誒。

落落長說了一堆不相干的東西,我想可以整理兩個 take-away:
  • 負債不是壞事,重點是要看負債換來什麼;錢只是資產的一個表現,要看整體資產是否有所增長,或者有機會成長。
  • 省錢不是好事,重點是花錢能換來什麼?能不能換到時間、方便、穩定、機會?省錢必有成本,你有沒有意識到呢?

2019年1月6日 星期日

用 PEG 寫一個 C parser 續

自從去年十月把 nixie tube clock 完工之後,好像都在耍廢之類的,結果 11/12 月兩個月都沒有發文,其實這兩個月裡面,有的時間都在改之前寫的 C parser,其實整體完成度愈來愈高了,今天發個文來整理一下到底做了啥。
這次做了幾個改變,主要的修正就是加上 expression, declaration, statment 的處理,也學到不少東西,這裡一一列一下:

macro

這是要對應之前寫的 parse_fail,本來 parse_fail 的用意,就是在剖析出錯的時候,把程式終結掉,然後丟一點錯誤訊息出來;本來我的實作是一個函式,利用 unreachable! 丟出錯誤:
fn parse_fail(pair: Pair<Rule>) -> ! {
  let rule = pair.as_rule();
  let s = pair.into_span().as_str();
  unreachable!("unexpected rule {:?} with content {}", rule, s)
}
這樣的實作會有個問題,在程式終止之後的位置一律都會在 parse_fail 這個函式裡,而不是真正出錯的剖析函式,要除錯必須開 stack trace 才能做到。為了避免這個狀況,我們改用 macro 實作 parse_fail,這樣 unreachable! 就會在出錯的位置展開,在終止程式的時候給出正確的位置。
關於 macro 小弟在很早的時候有寫過一篇貧乏的介紹文,改起來也很簡單,把原本作為函式參數傳進來的 pair,由 macro 的 $x:expr 取代,然後用 $x 取代本來 code 裡面所有的 pair,如下文:
macro_rules! parse_fail {
  ( $x:expr ) => {
    {
      let rule = $x.as_rule();
      let s = $x.into_span().as_str();
      unreachable!("unexpected rule {:?} with content {}", rule, s)
    }
  }
}
這樣所有程式裡的 parse_fail(pair) 就會自動開展成下面的三行程式碼了。

另外有一個要注意的是,假設我 parse_fail 的 macro 寫在模組的 helper.rs 裡面,那麼在寫模組的 lib.rs 時,mod helper 要在所有其他 mod 之前,並加上 #[macro_use] 修飾,這跟 rust 模組的編譯流程有關,macro 是跟順序有關的,在 mod helper 之後這個 parse_fail 的 macro 才有定義,後面的 mod 才能使用這個 macro,詳細可以參考這篇

如果想讓使用 extern crate 的人也能使用這個 macro,就要在定義 macro 的時候在前面加上 #[macro_export] 的標籤,每個 macro 都需要單獨 export 才行。

處理 expression 的正確姿勢:

如果有看上一篇,會看到我用 PEG 套件 pest 的 precedence climbing 的功能來完成對 expression 的剖析,但其實那是不完整的,原因在於我們把的做法是把 expression 直接導向 unary_expr (op_binary unary_expr)* 的組合,這樣我們看到 expression,把它展開來就可以得到一大串 unary_expr 跟 op_binary 交錯的序列,把這串東西丟進 precedence climbing 裡面就能建好 expression tree 了。
但實際上的 C 語言比這還要複雜,expression 下面還有 assignment expression,conditional expression 等等,這些 expression 是必須存在的,例如在變數 decl 的地方就會需要 assignment expression,我們本來的寫法把 assignment expression 等等都抹掉了要怎麼辦?把它們加回去要怎麼讓本來的 precedence climbing 的 code 還能正確運作?
後來發現的正確處理方法是這樣的,在文法的部分要把 assignment expression 等東西加回去,裡面用到的三元運算子 ?: ,assignment operator =, +=, -= 等等都從 op_binary 裡面排除,像是這樣:
logicalOR_expr = _{ unary_expr ~ (op_binary ~ unary_expr)* }
conditional_expr = _{ logicalOR_expr ~ ( op_qmark ~ silent_expression ~ op_colon ~ conditional_expr)? }
assignment_expr = _{ (unary_expr ~ op_assign)* ~ conditional_expr }
silent_expression = _{ assignment_expr ~ (op_comma ~ assignment_expr)* }
expression = { assignment_expr ~ (op_comma ~ assignment_expr)* }
原本的 expression 現在只剩 logicalOR_expr,其他的都要拉出來自立條目,讓其他的文法如 declaration 能使用它,但同時都使用 _{} 讓剖析後他們不會吐一個 node 出來,這樣看到 expression 之後,展開來仍然是一串 unary_expr 跟 operator 交錯的序列。

這樣做的好處是 precedence climbing 仍然可以沿用,所有的 operator 都算在 expression 頭下,壞處是我們必須依文法去調整一些文法要不要吐出 node,現在的實作在有兩個特例:
一個是如上面所示,conditional expression 的規定是 logical_OR_expression ? expression : conditional_expression,有一個 expression 在裡面,這會違反我們的假設:把 expression 展開來看都會是 unary_expr 跟 operator 的組合,因此我們要加上一個特別的 silent_expression 在剖析完之後不會生成 expression node ,而是完全展開。

另一個剛好是反過來的狀況,在 C 的 initializer 文法(6.7.9)是這樣定的:
initializer -> assignment_expression | "{" initializer-list "}"
但…我們的 assignment_expression 是不存在的,如果 initializer 真的剖析為 assignment_expression,展開 initializer 只會得到「一團 unary_expr 跟 operator 的組合」,會跟 initiailizer-list 搞在一起,所以反過來我新增了一個 initializer_expr,把 assignment expression 封起來:
initializer_expr -> assignment_expression
initializer -> initializer_expr | "{" initializer-list "}"
這樣拿到 initializer 就能放心展開,再看內容物是 initializer_expr 或 initializer-list 來決定下一步,如果是 initializer_expr 就能放心的丟給 precedence climbing 去建 expression 了。

上面兩個例子都沒什麼道理可言,基本上就是見招拆招,大致就是兩條好像在說廢話的規則:
  1. 會展開的 rule 裡面出現這團 rule 的開頭,則開頭的 rule 代換成自動展開的版本。
  2. 會展開的 rule 跟其他 rule 並列,要再多包一層不會展開的版本。

! tag for = and ==

在這次修改之前都沒什麼機會用到 ! tag,也就是 PEG 裡的 Not predicate,這次在處理更複雜的 expression 遇到,某些狀況 = 的優先權高過 == 以致 == 先被剖析成 = 了。
這時候 op_assign_eq 就要改為:
op_assign_eq = { "=" ~ !"=" }
來確保 = 之後沒有接著其他的 =。

comment

comment = _{ "/*" ~ (!"*/" ~ any)* ~ "*/" | "//" ~ (!"\n" ~ any)* ~ "\n" }
comment 也是這次的修改之一,同樣利用了 ! 的特性,上面兩條其實都滿直覺的:
開頭是 /* 再來只要不是 */ 的內容,就可以匹配任何字元;開頭是 // 再來只要不是換行就可以匹配任何字元。
這兩個例子都使用了 Not predicate,功能很類似 C 裡面的 peek,偷看一下後面的東西而不消耗任何東西。

Hidden grammar:

這點比較不是程式的問題,而是 C 規格的問題,注意以下這些都符合 C grammar,但在工作上千萬別這麼寫,大概有十成的機率你會被電到天上飛
  • volatile, restrict, const 隨便加,加幾個都沒關係
  • 其實可以不用 type,這是符合文法的,gcc 在這裡會直接給你一個 int。
  • 也可以宣告型別,儲存類型什麼的,最後…沒變數。
所以可以寫像是:
int const volatile const volatile const volatile const volatile const volatile const;
const * restrict restrict restrict a;
說真的,看到這樣寫 code 我也會把人電到天上飛,其實我也不知道為什麼 C grammar 要允許這樣的文法就是,看到 gcc 編譯過我差點笑死。

現在離大致完成還有一個最大的難關,就是 declaration 那邊還有 struct, union, enum 等著處理,文法上是還好,更大的問題是不知道怎麼寫轉出來的 AST,之前我大部分都參考強者我學長 suhorng 大大的 haskell 實作,或者參考一些 LLVM 的 IR 實作…當然是沒辦法到 LLVM 那麼複雜啦QQ。
總之最近進度嚴重卡關,這才是我為什麼在這裡打住寫篇文的原因(誒。

自己自幹 AST,配上最近工作上做的一些改動,讓我有了下面這個體會:
資訊源自於數學,本身是無窮的,正如數線上有無數的正整數,無窮的有理數,比無窮更無窮的無理數;數學這個「概念」本身就有無限的資訊
但有了電腦一切就不一樣了,我們只有有限的位元能夠近似數學的概念,所以就有了取捨。
用 64 位元可以表示到 18446744073709551615,大約是 10^19,於是 10^19 -> ∞ 的資訊就被捨去了;同理我們決定浮點數用 IEEE 754 表示,有些小數就是無法表示,無窮的資訊對上有限資源,其間的差距令人絕望。

就如我們把 C code parse 成 AST,AST 裡面要保留多少資訊?像我這樣基本上只保留了簡單的 AST node,隨便建顆樹而已;LLVM 的 IR 就是許多嚴僅設計的物件,保留程式語言的繼承關係跟內部的屬性設計,在處理上就有更多能運用的資訊。
工作上需要的是用電腦處理幾何的資訊,像是點、線、四邊形,那麼一個線段的物件要儲存什麼資訊?可以用起點終點來表示一條線,基於效率跟空間考量,我們可能可以存一下線段是不是垂直的、水平、甚至是不是斜上跟斜下,但要不要存一個 double 的斜率呢?這就要看平常是不是很常需要算斜率了。存更多的東西自然可以方便做些處理,但線段更新時也要更新更多的資訊。

捨去是面對資訊時的必要,資訊工程處理的問題一直都不是資訊太少而是資訊太多,而要捨去什麼資訊、保留什麼,這不是科學而是技藝,這些都不是數學,不會有一個標準的答案,而是視需求去選擇,需要經驗、工具、模擬、除錯、測試……用實驗跟說理得到一個最佳近似的解;正如大學學系的名字:資訊<工程>學系。

2018年10月27日 星期六

自幹世界線變動率探測儀(Nixie Tube Clock):後記

世界線變動率探測儀系列文終於接近尾聲了,能看到這邊想必大家也煩了,做一趟電路自己也學到很多,覺得非常值得;如果能讓看系列文的大家也學到東西,我想這前後加起來 8 篇快 14000 字的文章就有寫的價值。
是說自從我發了文之後,好像真的有不少人覺得世界線變動率探測儀是不是真的在探測什麼東西XDDD,其實它就只是個輝光管做的時鐘而已 (._.),真的很想知道它為什麼叫這個名字…就請去看 Steins;Gate

從8月左右開始動工,動工到現在連 Steins;Gate 0 都已經演完了,東西才做出來。

若說這次學到最大的教訓,就是:限流電阻很重要,限流電阻很重要,限流電阻很重要。說來漸愧,上路之前都沒好好看 spec 或是看人做的東西,有些地方一定要加限流電阻的都忘了加XDD。
例如 nixie tube 限流是 3 mA ,剛拿到高壓電路一時興起就給它直接打下去,當然 nixie tube 是沒壞啦(俄國管子真耐操www),但就高電流把管子內打出一堆電漿,還在疑惑怎麼拍起照來都是一團糊糊的,後來看看才發現要加 22K 限流電阻,小數點則是實驗後發現要 75 K 限流電阻。
然後 TLP521 也是,沒加 220 ohm 限流電阻 5V 直接灌下去,馬上就超過 arduino 的限流 40 mA,然後疑或為啥 arduino 電壓輸出到不了 5 V。
另外也不要偷懶,每個元件例如 LED, nixie tube ,限流電阻該加的每個元件都要加一個,共用電阻是絕對 NG,如同這裡說的:那是因為每個元件會有不同的特性,調小電阻的同時,就可能有元件吃到過大的電流導致燒毀,畫 layout 的時候一度把所有 nixie tube 的電阻用同一個,幸好有一天睡前躺在床上突然大徹大悟把它改掉了。

然後功率的部分也要認真對待,為了這個 project 久違的把我的工程計算機拿了出來,幾個元件電阻的功率都要算一下,不過 IN-14 相對來說,1.5 mA 的電流不算大,這上面所有的電阻都能用 0603 解決,比較危險的只有陽極驅動的 470k 電阻。
所有設計都在 github ,主要就是 code 跟電路板 gerber layout,高興的話拿去跟板廠說要洗板他就會幫你洗板子出來,買個元件插一插,就能做出自己的世界線變動率探測儀,或者其實我有多洗一些板子,要的話也可以跟我買(誤。
最終整體成本大概如下:電路板 4000 元,輝光管 2000 元,元件加一加約莫 800 元吧,當然跟外面賣的產品比還是便宜一半,花自己時間就是了。

其實這版 1.01 問題不少,包括先前提過的:
  • 腳太近:控制 180 V 的 MPSA42/92 選了 TO-92 的 footprint,它的腳位間距只有 0.25 mm,不及 180 V 建議需要的 0.4 mm,某種程度上可能會有危險。
  • 看錯輝光管的腳位,導致數字 1-9 全部反過來,該打屁股。
  • LED 關不掉又太亮了,這個可能是最輕微的啦,而且可以再買電阻修正。
  • Layout 不夠緻密,浪費面積
以上未來可能出一個 v1.02 來修正吧,但我說真的沒實際驗證過,出個 v1.02 做錯了害大家噴錢我又不能負責(yay,而且 easyEDA 改 layout 好麻煩,想到就不想做XDDD。

如果未來哪天我想不開,也許有可能會做個第二版,不過我是覺得不會這麼快啦,我目前給自己第二版的目標,包括至少要有:
  • 雙層板,下層控制上層放燈管,挑戰總面積最小,不然像這版寬度達到 10 幾公分,根本不能像動畫裡面那樣拿起來。
  • 使用 MC34063 以外,高頻一點的開關電路,做到更高頻、效率更好的升壓方式,例如網路上有人販售的高壓電路板,可以做到非常小,效率又高,用電池就能推得動一堆管子。
  • 嘗試使用 SMT 的晶片,元件能用平面的就用平面,縮小面積。
我猜至少要幾年以上吧,現下有第一版就非常滿意了,有機會的話想找人做個木盒跟壓克力盒把它裝在裡面www。

本作品的完成,有許多要感謝的人:
  • 強者我同學 小新大大 起了頭,讓我們有動力完成製作。
  • 強者我同學 強強林大大 在過程中給予幫助,出借工具、焊接空間。
  • 戀戀科技的 Marten 大大給予硬體電路板製作的指導。
  • 強者我學妹 昱廷大大 幫買淘寶高壓電路板。
  • JKL 代購幫助我買到 ebay nixie tube。
  • 在製作過程中大量參考 復古咖啡大大 的製作。
  • 工作狂人大大 的網站助我學到許多 PCB 相關的知識,有一次整個週末都在刷大大的網站。
還有瀏覽了許多網站,幫助我解除設計上疑惑,像是 Arduino 官網、論壇,都多少幫了點忙,在此不一一介紹。

回首開工的日子,果然能好好做好一件事,會需要時間,但回頭來看,也非常值得,我想就用一張藍光的 Steins;Gate 世界線來收個尾吧。

這張超明顯的顯示 LED 太亮的問題呀(yay

自幹世界線變動率探測儀(Nixie Tube Clock):寫 code

到了這邊木已成舟(無誤,電路板沒做好的話,程式寫再多都沒有用www,只能硬著頭皮去修或者認命掏銀子出來重洗了),再來就是不斷的寫 code 跟燒 code,在洗板的時候已經預留了燒錄程式碼的接點,只要把對應的針腳從作為燒錄器的 Arduino 板子接到電路板上就能燒錄了。
寫Code的時候要注意,如果一不小心把所有的燈管都打開,高壓電路有可能會推不動,淘寶上買的高壓電路板是推得動,我的不行,我猜跟電感的好壞有關;這版電路因為有用上 74HC238跟 74CD4514,原則上來說是不會發生這種事。

建議可以先從低壓的部分先測試,因為我們在 78M05 輸出到所有需要 5V 的電路上有斷路器,這樣就可以先斷路,用外接的 5V 驅動整個電路了。
測試 LED,把 LED 腳位輪流打開就行了:

靠北,超級炫砲……
LED 5050 RGB,我藍、紅是用 270 歐姆,綠色用 620 歐姆,結果他爸還是超級亮…我個人是覺得有點太亮,測程式的時候都快被閃瞎了,有點妨害看燈管,如果可以的話應該要再換大一點的電阻,把光度調小一點。
Github 裡面有附上 74HC238 跟 74CD4514 的測試程式,可以執行後一個一個量測兩個晶片的輸出,看看有沒有正常動作。

軟體中燈管要顯示的數字存在 Byte 裡面,0-9 對應 0-9,左點跟右點分別是 10 跟 11,照著 Byte 的位元寫給 74CD4514,12-15 則是會把 74CD4514 的 INH 降下來把燈管關掉。
void writeNum(byte num) {
  if (num >= 12) {
    digitalWrite(DISPLAY_E, HIGH);
  } else {
    byte mappedNum = mapNum(num);
    digitalWrite(DISPLAY_E, LOW);
    digitalWrite(DISPLAY_3, (mappedNum >> 3) & 0x1);
    digitalWrite(DISPLAY_2, (mappedNum >> 2) & 0x1);
    digitalWrite(DISPLAY_1, (mappedNum >> 1) & 0x1);
    digitalWrite(DISPLAY_0, (mappedNum >> 0) & 0x1);
  }
}
這裡要特別注意一下,如果要取出數字裡某個 bit 的時候,要用的運算符號是 & bitwise and ,不是 ^ bitwise xor 噢 ^.<
為什麼會有那個 mapNum ,其實是我 v1.01 的 layout …畫錯了,不確定是不是用的 footprint 的問題還是我自己蠢,總之陰極控制的接線是錯的,從 1-9 的接線完全反過來了,幸好這個 bug 可以用軟體修正回來,寫一個函式去轉數字就好了,這個也是 v1.02 修正。我覺得機率最高的原因,是我在看參考資料的圖的時候,沒有注意到它是 Bottom view,在設定 schematic to layout 的時候寫反了,最後 layout 也就錯了。
燈管的選擇 writeTube 和這裡一樣,0-7 各 bit 送給 74HC238 的三個腳位,就能開關某一支燈管,很簡單可以打包成一個函式來選擇燈管,首先我們就先寫一個掃描的程式,用 for loop確認每個燈管每隻腳位都有正常運作。
正常。
有了這兩個小函式,可以把燈管的值保存在全域變數陣列 display,然後用一個函式來更新它,只要這個函式在 loop() 中,燈管就會不斷顯示 display 的值;delay 只能是 delay(1) 或 delay(2) ,算是實驗得到的經驗值,不放 delay 的話切換速度太快,所有管子的數字會混在一起,delay(3) 的話則是看得出有點在閃。
void updateTube() {
  for (int tube = 0; tube < NTUBE; tube++) {
    writeTube(tube);
    writeNum(display[tube]);
    delay(2);
  }
}

使用 DS1307 記錄時間:

知道如何顯示數字之後,再來就是去接 RTC 時間,這裡我是使用別人包好的 DS1307RTC,照著範例把 library 引入之後,在 setup 呼叫 setSyncProvider(RTC.get);
即可和 DS1307 同步,之後就可以很方便的用 year(), month() 等函式拿到現在的時間值,非常方便。

DS1307 Interrupt:

DS1307 的 SQW 腳位能設定穩定輸出方波,搭配 arduino interrupt 就能每秒呼叫函式做點事情,我們已經把 DS1307 的 SQW 腳位接給 ATmega328p 的 digital pin 2,只要透過 Wire 向 ds1307 設定(請參考 ds1307 datasheet),其中 DS1307_ADDR 是 DS1307 的 I2C 位址 0x68,再對偏移 0x7 的位址寫入設定 0x10 即可,DS1307 可以設定輸出 1kHz, 4.096 kHz, 8.192 kHz 和 32.768 kHz 的方波:
Wire.begin();
Wire.beginTransmission(DS1307_ADDR);
Wire.write(0x07);
Wire.write(0x10);  // Set Square Wave to 1 Hz
Wire.endTransmission();
設定完 ds1307 就會輸出頻率 1Hz 的方波,我們可以把這個方波接給 ISR 來做計算開機秒數的工作:
void ISR_RTC() {
  toggle = !toggle;
  ++secCount;
}
attachInterrupt(digitalPinToInterrupt(RTC_SQW), ISR_RTC, RISING);
後來發現算秒數好像不能幹嘛XDDD

使用 DS1307 NV RAM 記錄資訊:

為了記錄世界線的資料,我們要用 DS1307 上面 56 bytes 的 NVRam 記錄資訊,不知道為什麼 DS1307 library 都不支援這功能,參考 arduino 論壇包了兩個函式來讀寫 NVRam,RAM_OFFSET 是 0x8,這樣就能把世界線的資料記錄在 NVRam 裡面了。
void writeNVRam(byte offset, byte *buf, byte nBytes) {
  Wire.beginTransmission(DS1307_ADDR);
  Wire.write(RAM_OFFSET + offset);
  for (int i = 0; i < nBytes; i++) {
    Wire.write(buf[i]);
  }
  Wire.endTransmission();
}

void readNVRam(byte offset, byte *buf, byte nBytes) {
  Wire.beginTransmission(DS1307_ADDR);
  Wire.write(RAM_OFFSET + offset);
  Wire.endTransmission();
  Wire.requestFrom( (uint8_t)DS1307_ADDR, nBytes);
  for (byte i = 0; i < nBytes; i++) {
    buf[i] = Wire.read();
  }
}
剩下的好像就是把上面的 code 猛攪一陣,就可以做出各種不同功能了。
要讀取按鈕的動態,可以參考官方文件 Debounce
整個 loop 裡面的流程,大概就是保存一個 state 的變數:然後依序
檢查 button 1 有沒有按下,有的話做一點事 -> 檢查 button 2 有沒有按下,有的話做一點事;再來依不同的 state ,設定 LED 跟輝光管顯示值的 display 陣列,最後呼叫 updateLED() 跟 updateTube() 顯示資訊。
我是設計有五個模式:自動切換/時間HH.MM.SS/日期YYYYMMDD/世界線顯示/全關省電模式,詳情請參考 github repository,這樣把自己寫的爛 code 展現在大家眼前感覺真是羞恥。

整體 code 差不多就是這樣啦,硬體能動之後改軟體什麼的都不算太難了,想玩的話可以弄一些,像是發送第一封 D-Mail:

2018年10月25日 星期四

自幹世界線變動率探測儀(Nixie Tube Clock):焊接

靜候三天,板子終於送到了,我在等的週末去光華把 BOM 表印出來去光華搬了一批元件,最貴的還是 LED 跟 BJT,其他買一堆電阻不過小錢…。
空板照,我是選藍色的阻焊,如果照動畫設定應該是黃色的,不過不管啦我喜歡藍色。

正面照:
背面照:

板子回來發現意外的順利,所有插件的腳位都沒問題可以直接穿過。
另外也有一些錯誤,中間高壓陽極的控制電路,MPSA92 的 footprint,我不小心用成 TO-92 腳位,以致焊點間的距離只有 0.25 mm,應該要用 TO-226 的腳位會比較好;第一個是焊接的時候很容易不小心就糊在一起,第二是高壓離這麼近其實有點危險,我覺得某幾個地方是真的有點漏電,即便管子控制全關的時候,還是會看到管子有很微弱的發光,可能升到 v1.02 的時候改進吧。
在焊接的時候有一位神級大大幫忙,才知道原來不小心焊錫糊在一起,用助焊劑一抹就開了,太神啦,對助錫劑刮目相看。

銲接之前工具要準備好,除了必備的烙鐵之外,推薦要買一隻好的尖頭鑷子,尖到可以刺人穿刺傷害 +20% 的那種,因為我們用的零件如 0603 尺寸都非常小,要好好的焊它就需要一支尖頭鑷子才行。

因為是二手管的關係,我們要對付的就是那該死的針腳,不知道到底是從哪裡拆/怎麼拆的,可以拆成這樣每支腳的長度都不一樣?甚至還有一支腳短到不能用的,因為我們沒有管座可用(它那腳位的狀況大概也不能用管座),我這裡的解法是去買 1 * 40 圓孔排針長腳,可以一支一支用鉗子剪開來,再一支一支,這樣我們就可以隨意插拔輝光管。
不能用普通的排母,輝光管的針腳太細,在排母裡面無法固定,圓孔排針裡面的彈簧才固定得住;也有人會用杜邦端子,用夾的把針腳夾住也是可以,只是杜邦端子要解開也沒那麼容易,不符合我們快速插拔的本意。


總之就是先把輝光管像上圖一樣插滿排針,接著將圓孔 IC 座一一插進 PCB 孔中,很不巧圓孔 IC 座的尺寸,跟我畫的 PAD 大小 0.914mm 超接近,然後這批管腳又塗了某種膠,讓它不像一般的元件一樣好折,在那邊用尖嘴鉗喬啊喬的,光插管子就插了快 1 hr 左右吧,說實在 Nixie Clock 愈做愈覺得這東西真是淘汰的好呀,要高壓驅動、浪費電、體積大、腳位又多又呈圓形很難安裝,真的還是懷古就好。
好不容易插好之後,一樣將 IC 針腳焊到電路板上,本來是不想焊的,不過發現不焊多少有點接觸不良。

其他部分大概都沒什麼,焊接只有一個要點就是:焊錫會黏在熱的東西上面,我的操作一般的拿烙鐵、焊點、焊錫三點相對位置呈三角,烙鐵頂住焊點加熱,焊錫輕碰烙鐵融化後就會自然流到焊點上了,如果是 SMD 元件的話簡化的步驟大概如下:
  • 在要放元件的一端先上一點焊錫。
  • 用鑷子夾起元件一端插入剛才上好的焊錫內(重新加熱焊錫讓它包住元件),拿開烙鐵,焊錫冷卻之後元件就被固定住;切記一定要先拿開烙鐵再放開鑷子,如果先放開鑷子,元件就會因為焊錫的表面張力而立起來或歪掉
  • 接著焊元件另外一端,因為元件已被固定這裡簡單焊就好了。
如果有助焊劑的話,上點助焊劑可以焊的比較漂亮一點,但也比較花時間。
還有一個常見的…直覺?一般而言通常是由烙鐵融化焊錫,讓焊錫自然流到焊點上,不過有時候,因為角度之類的關係,焊錫就是不會流過去,只會一直留在烙鐵上面;這時候的直覺反應就是不斷的給焊錫,但其實愈給只是停愈多在烙鐵上面,它不上到焊點只是角度不對而不是焊錫不夠多,這時候把烙鐵抹乾淨換角度重來會比較好。

這樣全焊完大概兩個工作天,趁著國慶假日借強者我同學強強林的實驗室把元件都焊完,全部粗估大概有 600 個焊點吧 (yay),幸好回家上電之後:
拿鱷魚夾夾二極體的輸出點,高壓電路正常動作:


用外接 5V 測試,LED 電路正常動作,也可以用 TX/RX 介面燒 code 進去跑:


測試過後,高壓電路、5V 電路、控制電路、LED 電路、RTC 電路都有正常運作,兩天的辛苦都值得了。
Related Posts Plugin for WordPress, Blogger...