變與實(shí)踐)
1. 從“軟件思維”到“硬件思維”的認(rèn)知躍遷如果你是一名軟件工程師或者對編程有一定了解第一次接觸Verilog時(shí)大概率會感到一種熟悉的“錯(cuò)覺”。它的語法看起來很像C語言有if-else有for循環(huán)有case語句甚至還有function和task。你可能會想“這不就是另一種編程語言嗎我寫個(gè)always (posedge clk)塊里面寫點(diǎn)邏輯不就能描述硬件了”這正是Verilog入門路上最大的認(rèn)知陷阱。我見過太多新手包括當(dāng)年的我自己帶著寫軟件的慣性思維一頭扎進(jìn)去結(jié)果寫出來的代碼要么仿真結(jié)果詭異要么綜合后電路面積和時(shí)序一塌糊涂甚至根本綜合不出預(yù)期的電路。Verilog的全稱是硬件描述語言關(guān)鍵詞是“描述”而不是“編程”。它的核心是讓你用一種高級的、文本化的方式向綜合工具“描述”你想要的數(shù)字電路結(jié)構(gòu)。工具再根據(jù)你的描述把它“翻譯”成由門電路、觸發(fā)器、連線等基本單元組成的物理網(wǎng)表。舉個(gè)最簡單的例子。在軟件里你寫a b c;CPU會按順序執(zhí)行從內(nèi)存讀b讀c送到ALU做加法結(jié)果寫回a。這是一個(gè)時(shí)間序列上的操作。但在Verilog里你寫assign a b c;你描述的是一個(gè)空間結(jié)構(gòu)你要求綜合工具生成一個(gè)加法器電路它的兩個(gè)輸入端口永遠(yuǎn)連著b和c的信號線輸出端口永遠(yuǎn)連著a的信號線。只要b或c的值變化a幾乎“同時(shí)”在考慮了電路延遲后就會跟著變化。這里沒有“先讀b再讀c”的順序它們是并發(fā)的。所以學(xué)習(xí)Verilog的第一步不是去死記硬背語法而是徹底扭轉(zhuǎn)思維模式從“順序執(zhí)行”的軟件思維切換到“并行發(fā)生”的硬件思維。你要時(shí)刻想象你寫的每一行代碼最終都會變成硅片上的晶體管、連線和時(shí)鐘驅(qū)動的寄存器。這篇文章我就以一個(gè)過來人的身份帶你繞過那些我踩過的坑從最核心的思維轉(zhuǎn)換開始一步步構(gòu)建起對Verilog和數(shù)字電路設(shè)計(jì)的堅(jiān)實(shí)理解。2. 構(gòu)建你的第一個(gè)“數(shù)字世界”環(huán)境、工具與Hello World在開始描述電路之前我們需要一個(gè)“數(shù)字實(shí)驗(yàn)室”。這個(gè)實(shí)驗(yàn)室不需要昂貴的FPGA開發(fā)板初期只需要兩樣?xùn)|西一個(gè)文本編輯器和一個(gè)仿真工具。我強(qiáng)烈推薦使用VS Code作為編輯器配合諸如Verilog-HDL/SystemVerilog等插件可以獲得優(yōu)秀的語法高亮、自動補(bǔ)全和代碼格式化體驗(yàn)。這能極大提升編碼效率和減少低級錯(cuò)誤。2.1 仿真工具選型Modelsim vs. Icarus Verilog仿真工具是我們的“虛擬示波器”和“邏輯分析儀”。主流選擇有兩個(gè)Modelsim (或 Questasim)業(yè)界標(biāo)準(zhǔn)功能強(qiáng)大圖形化界面友好調(diào)試能力極強(qiáng)。對于學(xué)生或初學(xué)者Intel (原Altera) 和 AMD (原Xilinx) 都提供功能受限的免費(fèi)版本如 ModelSim-Intel FPGA Starter Edition。它的波形查看器是學(xué)習(xí)時(shí)序概念的絕佳工具。Icarus Verilog (iverilog)開源、輕量、命令行驅(qū)動。搭配GTKWave查看波形構(gòu)成了一個(gè)完全免費(fèi)、跨平臺的開源仿真環(huán)境。雖然功能不如Modelsim全面但對于學(xué)習(xí)語法和進(jìn)行中小型模塊仿真完全足夠而且其“原始”的特性迫使你更理解編譯和仿真的流程。對于純新手我建議從Icarus Verilog GTKWave開始。理由很簡單安裝配置簡單沒有許可限制能讓你更專注于語言本身而不是復(fù)雜的IDE操作。當(dāng)你需要更復(fù)雜的調(diào)試如查看多維數(shù)組、復(fù)雜數(shù)據(jù)結(jié)構(gòu)或與特定廠商的IP核協(xié)同仿真時(shí)再遷移到Modelsim也不遲。注意安裝Icarus Verilog時(shí)Windows用戶建議直接下載預(yù)編譯的安裝包。Linux用戶通過包管理器如apt install iverilog gtkwave安裝即可。確保安裝后能在命令行中執(zhí)行iverilog -v和gtkwave命令。2.2 第一個(gè)模塊點(diǎn)亮“邏輯燈”我們不寫“Hello World”我們點(diǎn)亮一盞“邏輯燈”。在數(shù)字電路里最基本的“燈”就是一個(gè)輸出端口。讓我們創(chuàng)建一個(gè)最簡單的模塊一個(gè)反相器NOT gate。// 文件名not_gate.v // 模塊定義模塊名端口列表 module not_gate ( input wire in, // 輸入端口聲明為wire類型 output wire out // 輸出端口 ); // 持續(xù)賦值語句描述一個(gè)組合邏輯 // 它意味著out 這個(gè)信號線永遠(yuǎn)等于 (~in) 這個(gè)邏輯值。 assign out ~in; endmodule這就是一個(gè)完整的Verilog模塊。我們來拆解一下module ... endmodule定義一個(gè)模塊這是Verilog描述功能的基本單元。input/output聲明端口的方向。wire是默認(rèn)的、最常見的線網(wǎng)類型用于連接組合邏輯。assign持續(xù)賦值語句。它描述的不是一個(gè)“動作”而是一種永恒的“關(guān)系”。只要in變化out就會立即在仿真中或經(jīng)過一個(gè)極短的門延遲在實(shí)際電路中變?yōu)閕n的反相值。2.3 編寫測試平臺給電路注入“激勵(lì)”電路模塊設(shè)計(jì)好了我們怎么驗(yàn)證它是對的呢這就需要編寫一個(gè)測試平臺。測試平臺本身也是一個(gè)Verilog模塊但它通常不會被綜合成實(shí)際電路使用特殊的initial塊和系統(tǒng)任務(wù)$display,$monitor等。它的作用是實(shí)例化待測模塊并模擬輸入信號的變化觀察輸出是否符合預(yù)期。// 文件名tb_not_gate.v timescale 1ns / 1ps // 時(shí)間單位 / 時(shí)間精度 module tb_not_gate(); // 測試平臺的內(nèi)部信號 reg in; // reg類型用于在initial/always塊中驅(qū)動信號 wire out; // wire類型連接待測模塊的輸出 // 實(shí)例化待測模塊將測試平臺的信號連接到模塊端口 not_gate u_not_gate ( .in (in), // .模塊端口名連接信號名 .out (out) ); // 初始化塊只在仿真開始時(shí)執(zhí)行一次用于生成測試序列 initial begin in 0; // 初始時(shí)刻in為0 #10; // 等待10個(gè)時(shí)間單位根據(jù)timescale是10ns in 1; // 10ns后in變?yōu)? #10; in 0; #10; $display(Simulation finished at time %0t, $time); $finish; // 結(jié)束仿真 end // 可選將信號變化記錄到VCD文件用于GTKWave查看波形 initial begin $dumpfile(wave.vcd); // 波形文件名稱 $dumpvars(0, tb_not_gate); // 記錄本模塊及其下所有子模塊的信號 end endmodule2.4 運(yùn)行仿真并查看波形保存好兩個(gè)文件后在命令行中執(zhí)行以下步驟編譯iverilog -o not_gate.vvp not_gate.v tb_not_gate.v-o not_gate.vvp指定編譯輸出的可執(zhí)行文件。后面列出所有需要編譯的源文件。運(yùn)行仿真vvp not_gate.vvp這會執(zhí)行編譯好的仿真程序生成wave.vcd波形文件并在控制臺輸出$display的信息。查看波形gtkwave wave.vcd打開GTKWave添加信號in, out到波形窗口。你將看到out信號始終是in信號的反相直觀地驗(yàn)證了反相器的功能。恭喜你已經(jīng)完成了從代碼到仿真的完整流程成功“點(diǎn)亮”了你的第一個(gè)數(shù)字電路。這個(gè)過程雖然簡單但包含了模塊定義、實(shí)例化、測試平臺編寫、仿真流程等所有核心環(huán)節(jié)的雛形。3. 深入核心可綜合邏輯的兩種描述風(fēng)格當(dāng)你寫的Verilog代碼最終要變成真實(shí)的電路通過FPGA或ASIC實(shí)現(xiàn)時(shí)這部分代碼必須是“可綜合的”。綜合工具只理解特定的編碼風(fēng)格??删C合邏輯主要分為兩大類組合邏輯和時(shí)序邏輯。描述它們有兩種主要風(fēng)格數(shù)據(jù)流描述和行為描述。3.1 數(shù)據(jù)流描述用assign描繪信號間的連接數(shù)據(jù)流描述使用assign持續(xù)賦值語句直接描述信號之間的邏輯函數(shù)關(guān)系。它最適合描述簡單的、沒有狀態(tài)變化的組合邏輯。module dataflow_example ( input wire a, b, c, output wire y1, y2 ); // 描述一個(gè)與門和一個(gè)或門 assign y1 a b; // y1 a AND b assign y2 (a | b) c; // y2 (a OR b) AND c endmodule綜合工具看到assign y1 a b;就知道需要生成一個(gè)二輸入的與門輸入端接a和b輸出端接y1。這種描述非常直接與最終生成的電路結(jié)構(gòu)一一對應(yīng)。3.2 行為描述用always塊定義電路行為行為描述使用always塊它描述的是在特定條件下寄存器reg值如何更新。這是描述復(fù)雜邏輯尤其是時(shí)序邏輯需要時(shí)鐘的主要方式。always塊的敏感列表決定了它何時(shí)被執(zhí)行。組合邏輯的always塊 敏感列表包含所有在右側(cè)表達(dá)式中出現(xiàn)的輸入信號。使用阻塞賦值。module behavioral_comb ( input wire [1:0] sel, input wire [3:0] d0, d1, d2, d3, output reg [3:0] out // 在always塊中賦值的信號必須聲明為reg類型 ); always (*) begin // (*) 或 (sel, d0, d1, d2, d3)表示任何輸入變化都觸發(fā) case (sel) 2b00: out d0; 2b01: out d1; 2b10: out d2; 2b11: out d3; default: out 4b0000; // 良好的編碼習(xí)慣提供default避免鎖存器 endcase end endmodule這個(gè)例子描述了一個(gè)4選1的多路選擇器。always (*)塊意味著只要sel、d0、d1、d2、d3中任何一個(gè)發(fā)生變化塊內(nèi)的語句就會重新計(jì)算并更新out的值。這里綜合出的就是一個(gè)純粹的組合邏輯MUX。關(guān)鍵陷阱不完整的敏感列表與鎖存器如果你寫always (sel)而忽略了數(shù)據(jù)輸入d0-d3那么當(dāng)sel不變而d0變化時(shí)always塊不會執(zhí)行out將保持舊值。為了保存這個(gè)舊值綜合工具會被迫生成一個(gè)鎖存器鎖存器在ASIC設(shè)計(jì)中需要謹(jǐn)慎處理在同步電路設(shè)計(jì)中通常被視為不受歡迎的“毛刺敏感”元件。因此對于組合邏輯永遠(yuǎn)使用always (*)是最安全的選擇它能自動推斷出完整的敏感列表。時(shí)序邏輯的always塊 敏感列表通常只包含時(shí)鐘邊沿posedge clk或negedge clk和可選的異步復(fù)位信號。使用非阻塞賦值。module behavioral_seq ( input wire clk, input wire rst_n, // 低電平有效的異步復(fù)位 input wire [7:0] d, output reg [7:0] q ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 異步復(fù)位只要rst_n變低立即在時(shí)鐘沿之前將q清零 q 8b0; end else begin // 時(shí)鐘上升沿觸發(fā)在clk上升沿到來時(shí)將d的值存入q q d; end end endmodule這個(gè)例子描述了一個(gè)帶異步復(fù)位的8位寄存器D觸發(fā)器。q d;是非阻塞賦值它意味著“在同一個(gè)時(shí)鐘沿所有非阻塞賦值右邊的表達(dá)式被同時(shí)計(jì)算然后在時(shí)間步結(jié)束時(shí)同時(shí)更新左邊的寄存器”。這是描述同步電路的關(guān)鍵。3.3 阻塞賦值與非阻塞賦值的黃金法則這是Verilog初學(xué)者最容易混淆和出錯(cuò)的地方。記住這個(gè)簡單的法則可以避免99%的時(shí)序邏輯錯(cuò)誤在描述組合邏輯的always (*)塊中使用阻塞賦值。在描述時(shí)序邏輯的always (posedge clk)塊中使用非阻塞賦值。為什么阻塞賦值像軟件里的“順序執(zhí)行”。在同一個(gè)always塊中a b; c a;執(zhí)行后c得到的是已經(jīng)更新過的a的值即b的值。這在組合邏輯中是正確的因?yàn)樗M了信號通過組合邏輯的瞬時(shí)傳播。非阻塞賦值模擬了硬件寄存器的并行更新。在同一個(gè)時(shí)鐘沿的always塊中a b; c a;執(zhí)行時(shí)右側(cè)的a和b使用的是它們在這個(gè)時(shí)鐘沿到來之前的舊值。所以這兩條語句是“同時(shí)”計(jì)算的c得到的是a的舊值而不是剛被賦予的b的值。這精確對應(yīng)了在時(shí)鐘上升沿所有觸發(fā)器的D端輸入被采樣并準(zhǔn)備更新到Q端而Q端的新值要到下一個(gè)時(shí)鐘沿才生效。違反這條法則常常會導(dǎo)致仿真行為與綜合后電路行為不一致產(chǎn)生難以調(diào)試的競爭冒險(xiǎn)問題。4. 從模塊到系統(tǒng)設(shè)計(jì)方法與常見電路實(shí)現(xiàn)掌握了基本描述方法后我們可以嘗試構(gòu)建更復(fù)雜的功能。數(shù)字系統(tǒng)設(shè)計(jì)通常采用自頂向下或自底向上的方法。這里我們通過幾個(gè)經(jīng)典電路來實(shí)踐模塊化設(shè)計(jì)。4.1 狀態(tài)機(jī)設(shè)計(jì)清晰、安全、可維護(hù)狀態(tài)機(jī)是數(shù)字邏輯控制的靈魂。一個(gè)清晰的狀態(tài)機(jī)設(shè)計(jì)至關(guān)重要。推薦使用“三段式”描述法它將狀態(tài)轉(zhuǎn)移邏輯、狀態(tài)寄存器更新和輸出邏輯分離結(jié)構(gòu)清晰易于綜合和調(diào)試。module fsm_sequence_detector ( input wire clk, input wire rst_n, input wire data_in, output reg sequence_found ); // 第一部分狀態(tài)定義 parameter S_IDLE 2b00; parameter S_1 2b01; parameter S_11 2b10; parameter S_110 2b11; reg [1:0] current_state, next_state; // 第二部分時(shí)序邏輯狀態(tài)寄存器更新 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state S_IDLE; else current_state next_state; end // 第三部分組合邏輯下一狀態(tài)計(jì)算 always (*) begin next_state current_state; // 默認(rèn)保持當(dāng)前狀態(tài) case (current_state) S_IDLE: next_state (data_in 1b1) ? S_1 : S_IDLE; S_1: next_state (data_in 1b1) ? S_11 : S_IDLE; S_11: next_state (data_in 1b0) ? S_110 : S_11; S_110: next_state (data_in 1b1) ? S_1 : S_IDLE; default: next_state S_IDLE; endcase end // 第四部分組合邏輯輸出計(jì)算Moore型輸出僅與當(dāng)前狀態(tài)有關(guān) always (*) begin sequence_found 1b0; // 默認(rèn)輸出 if (current_state S_110) sequence_found 1b1; end endmodule這個(gè)狀態(tài)機(jī)檢測輸入序列“1101”。三段式這里把輸出邏輯算作一段共四段的好處是第二部分一定是純粹的時(shí)序邏輯綜合出來就是一組觸發(fā)器第三和第四部分是純粹的組合邏輯。這種分離讓綜合工具能更好地優(yōu)化也讓代碼更容易理解和維護(hù)。4.2 計(jì)數(shù)器與分頻器時(shí)序的基礎(chǔ)計(jì)數(shù)器是數(shù)字系統(tǒng)中最基本的時(shí)序元件之一。module counter #( parameter WIDTH 8 // 參數(shù)化設(shè)計(jì)提高模塊復(fù)用性 )( input wire clk, input wire rst_n, input wire en, // 計(jì)數(shù)使能 output reg [WIDTH-1:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count {WIDTH{1b0}}; // 復(fù)位時(shí)清零使用復(fù)制運(yùn)算符{} else if (en) count count 1b1; // 使能時(shí)加1 // else 保持無需顯式寫出 end endmodule這個(gè)簡單的二進(jìn)制遞增計(jì)數(shù)器每個(gè)時(shí)鐘沿在en有效時(shí)加1。基于計(jì)數(shù)器可以輕松實(shí)現(xiàn)分頻器。例如要實(shí)現(xiàn)一個(gè)N分頻N為偶數(shù)的時(shí)鐘可以生成一個(gè)占空比50%的方波module clock_divider #( parameter N 10 // 分頻系數(shù)假設(shè)為偶數(shù) )( input wire clk_in, input wire rst_n, output wire clk_out ); reg [$clog2(N)-1:0] cnt; // $clog2是系統(tǒng)函數(shù)計(jì)算位寬 reg clk_out_reg; always (posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out_reg 1b0; end else begin if (cnt N/2 - 1) begin // 計(jì)數(shù)到一半時(shí)翻轉(zhuǎn) clk_out_reg ~clk_out_reg; cnt 0; end else begin cnt cnt 1; end end end assign clk_out clk_out_reg; endmodule4.3 存儲器與FIFO數(shù)據(jù)緩沖的基石小容量存儲器可以用寄存器陣列實(shí)現(xiàn)例如一個(gè)雙端口RAM先讀后寫。module simple_ram #( parameter ADDR_WIDTH 4, parameter DATA_WIDTH 8 )( input wire clk, input wire we, // 寫使能 input wire [ADDR_WIDTH-1:0] addr, input wire [DATA_WIDTH-1:0] din, output reg [DATA_WIDTH-1:0] dout ); // 用reg數(shù)組定義存儲器 reg [DATA_WIDTH-1:0] mem [(2**ADDR_WIDTH)-1:0]; always (posedge clk) begin if (we) mem[addr] din; // 寫操作 dout mem[addr]; // 讀操作輸出在時(shí)鐘沿后更新是同步讀 end endmodule對于更復(fù)雜的數(shù)據(jù)流控制FIFO是必不可少的組件。一個(gè)同步FIFO的核心是讀寫指針的控制和空滿狀態(tài)的判斷。判斷空滿狀態(tài)時(shí)通常會讓指針多出一位[ADDR_WIDTH:0]當(dāng)最高位不同而其余位相同時(shí)表示“滿”當(dāng)所有位都相同時(shí)表示“空”。這是FIFO設(shè)計(jì)的經(jīng)典模式網(wǎng)上有大量成熟可靠的代碼可以參考和修改。自己動手實(shí)現(xiàn)一個(gè)參數(shù)化的FIFO是理解指針操作和狀態(tài)機(jī)結(jié)合的絕佳練習(xí)。5. 仿真、調(diào)試與代碼質(zhì)量從能跑到可靠寫出來的代碼能通過編譯和基礎(chǔ)仿真只是第一步。確保它在各種邊界條件下都能正確工作并且綜合出的電路性能良好才是工程師價(jià)值的體現(xiàn)。5.1 編寫完備的測試平臺一個(gè)糟糕的測試平臺比沒有更危險(xiǎn)因?yàn)樗鼤o你虛假的信心。好的測試平臺應(yīng)該覆蓋所有功能點(diǎn)包括正常功能和所有定義的異常情況。進(jìn)行邊界測試對計(jì)數(shù)器測試滿量程和溢出對FIFO測試寫滿和讀空。加入隨機(jī)激勵(lì)使用$random或更高級的約束隨機(jī)驗(yàn)證方法可以發(fā)現(xiàn)確定性測試難以覆蓋的角落情況。實(shí)現(xiàn)自檢查在測試平臺中自動比較輸出與預(yù)期值并用$error報(bào)告失敗而不是依賴人工看波形。// 一個(gè)簡單的自檢查測試平臺片段 initial begin integer error_count 0; for (int i0; i1000; ii1) begin (posedge clk); data_in $random; // 隨機(jī)輸入 expected_out some_function(data_in); // 計(jì)算預(yù)期輸出 #1; // 等待一個(gè)小的delta時(shí)間讓待測模塊輸出穩(wěn)定 if (dut_out ! expected_out) begin $error(Mismatch at time %0t: input%h, dut_out%h, expected%h, $time, data_in, dut_out, expected_out); error_count; end end if (error_count 0) $display(Test PASSED!); else $display(Test FAILED with %0d errors., error_count); $finish; end5.2 利用波形調(diào)試當(dāng)測試失敗時(shí)波形是你的第一現(xiàn)場。學(xué)會高效使用波形查看器定位問題時(shí)間點(diǎn)找到輸出第一次出現(xiàn)錯(cuò)誤的時(shí)間?;厮菪盘栄刂鲥e(cuò)的信號向前追溯看是哪個(gè)模塊、哪個(gè)輸入導(dǎo)致的。檢查關(guān)鍵信號對于狀態(tài)機(jī)檢查current_state和next_state是否按預(yù)期跳轉(zhuǎn)對于計(jì)數(shù)器檢查使能、清零信號是否有效。注意仿真與現(xiàn)實(shí)的差距仿真中的#1延遲在實(shí)際電路中不存在。要小心由仿真延遲掩蓋的競爭冒險(xiǎn)問題。一個(gè)在仿真中完美的設(shè)計(jì)可能因?yàn)椴季€延遲在實(shí)際電路中產(chǎn)生毛刺。對于時(shí)鐘、復(fù)位等關(guān)鍵信號必須保證其滿足建立時(shí)間和保持時(shí)間。5.3 可綜合代碼風(fēng)格與最佳實(shí)踐為了讓綜合工具生成更優(yōu)的電路并避免潛在問題請養(yǎng)成以下習(xí)慣明確所有情況在if-else和case語句中為所有可能的輸入組合指定輸出否則會推斷出鎖存器。使用default分支或賦初值。避免異步邏輯除了全局復(fù)位/置位盡量避免使用異步邏輯。異步邏輯的時(shí)序難以分析容易導(dǎo)致系統(tǒng)不穩(wěn)定。寄存器輸出對于模塊的輸出信號盡量在頂層用寄存器打一拍再輸出。這有助于改善時(shí)序?qū)㈥P(guān)鍵路徑限制在模塊內(nèi)部。參數(shù)化設(shè)計(jì)使用parameter和localparam來定義常數(shù)而不是直接寫死數(shù)字。這極大提高了代碼的可復(fù)用性和可維護(hù)性。代碼注釋與命名信號名、模塊名要有意義。wr_en比we更好data_fifo比df好得多。詳細(xì)的注釋尤其是對復(fù)雜算法或狀態(tài)機(jī)轉(zhuǎn)移條件的注釋對于后續(xù)維護(hù)和團(tuán)隊(duì)協(xié)作至關(guān)重要。目錄結(jié)構(gòu)規(guī)范建立清晰的目錄結(jié)構(gòu)如rtl/存放設(shè)計(jì)代碼sim/存放測試平臺doc/存放文檔syn/存放綜合腳本和報(bào)告。學(xué)習(xí)Verilog就像學(xué)習(xí)一門新的“建筑語言”你用它在描述一個(gè)并發(fā)的、空間化的數(shù)字世界。從扭轉(zhuǎn)思維開始通過搭建一個(gè)個(gè)小模塊與門、寄存器、計(jì)數(shù)器、狀態(tài)機(jī)逐漸掌握描述復(fù)雜系統(tǒng)處理器、通信協(xié)議、圖像處理流水線的能力。這個(gè)過程充滿挑戰(zhàn)但當(dāng)你在示波器上看到自己設(shè)計(jì)的電路按照預(yù)想運(yùn)行時(shí)那種成就感是無與倫比的。記住多看優(yōu)秀的開源代碼多動手仿真和調(diào)試多思考“這行代碼會變成什么樣的電路”你就能在這條路上越走越穩(wěn)。