售貨機(jī)設(shè)計(jì):基于VHDL狀態(tài)機(jī)的完整實(shí)現(xiàn)流程)
簡(jiǎn)介這是一份面向FPGA初學(xué)者的自動(dòng)售貨機(jī)VHDL完整實(shí)例以單個(gè)doc文檔收錄工程源碼與仿真說明約200KB正好1個(gè)文件。文檔圍繞PL_auto1實(shí)體完整實(shí)現(xiàn)貨物信息RAM存儲(chǔ)、狀態(tài)機(jī)進(jìn)程控制、5角/1元硬幣識(shí)別與退幣找零、余額計(jì)算、數(shù)碼管顯示等核心功能并覆蓋時(shí)鐘分頻、商品數(shù)量更新、購(gòu)買信號(hào)判斷等細(xì)節(jié)邏輯外部信號(hào)包含時(shí)鐘、設(shè)定、選購(gòu)、購(gòu)買、完成等控制端結(jié)構(gòu)清晰便于學(xué)習(xí)者對(duì)照理解VHDL狀態(tài)機(jī)與存儲(chǔ)器協(xié)同工作的方式。文檔還給出ModelSim、VCS等工具下的仿真思路結(jié)合注釋代碼可清晰看到價(jià)格數(shù)量置入、硬幣計(jì)數(shù)、退幣操作等流程適合數(shù)字邏輯課程設(shè)計(jì)、FPGA實(shí)驗(yàn)或自動(dòng)售貨機(jī)教學(xué)參考。目前已有1073人瀏覽學(xué)習(xí)對(duì)于想掌握VHDL工程化編寫與仿真流程的讀者值得下載參考。1. 自動(dòng)售貨機(jī)FPGA設(shè)計(jì)為什么狀態(tài)機(jī)是這道題的核心自動(dòng)售貨機(jī)這題把它當(dāng)成“加法器和比較器”來寫的人多半會(huì)在仿真階段翻車。難點(diǎn)不在余額夠不夠、找零怎么算而在“投幣之后該做什么、選貨之后該做什么”每一步都依賴上一步發(fā)生的事件。FPGA里的狀態(tài)機(jī)處理這種順序控制比硬搭組合邏輯清晰得多也好仿真、好排錯(cuò)。這篇文章把自動(dòng)售貨機(jī)抽象成投幣、選貨、出貨三個(gè)動(dòng)作按“接口抽象 → VHDL狀態(tài)建模 → RTL實(shí)現(xiàn) → Testbench仿真 → 上板驗(yàn)證”的順序講一套可以直接動(dòng)手的流程。課程設(shè)計(jì)、FPGA入門或者想復(fù)用一套控制類FSM模板的開發(fā)者都能從里面找到能落地的參數(shù)和代碼。2. 狀態(tài)與事件建模自動(dòng)售貨機(jī)VHDL代碼里的信號(hào)怎么定義寫RTL之前先做接口抽象這一步?jīng)Q定了代碼在綜合工具眼里是干凈的寄存器狀態(tài)機(jī)還是一堆沒有時(shí)序約束的網(wǎng)絡(luò)。自動(dòng)售貨機(jī)的物理外設(shè)包括硬幣識(shí)別器、選擇按鍵、出貨電機(jī)和余額顯示屏FPGA端的首要任務(wù)是把這些外設(shè)行為歸一成時(shí)鐘域里的電平或脈沖。2.1 自動(dòng)售貨機(jī)的輸入輸出事件定義常見做法是硬幣識(shí)別器檢測(cè)到一枚硬幣后輸出一個(gè)高電平脈沖商品選擇按鍵按下后同樣產(chǎn)生一個(gè)脈沖。脈沖的好處是邏輯上天然具有“一次”語(yǔ)義電平則容易被長(zhǎng)時(shí)間按下或傳感器抖動(dòng)誤判成多次操作。余額的計(jì)價(jià)單位建議直接用“角”1元硬幣等于10角0.5元硬幣等于5角商品A定價(jià)30角、商品B定價(jià)25角、商品C定價(jià)15角這樣所有價(jià)格都是整數(shù)不會(huì)出現(xiàn)浮點(diǎn)運(yùn)算。信號(hào)方向位寬含義clkin1系統(tǒng)時(shí)鐘按50 MHz設(shè)計(jì)rst_nin1復(fù)位低有效coin_1in11元硬幣脈沖coin_05in10.5元硬幣脈沖buy_ain1選擇商品A脈沖buy_bin1選擇商品B脈沖buy_cin1選擇商品C脈沖seg_dataout8數(shù)碼管段碼輸出seg_selout4數(shù)碼管位選輸出dispenseout1出貨電機(jī)驅(qū)動(dòng)脈沖高有效alarmout1余額不足提示高有效把端口定義成脈沖而不是電平后面的狀態(tài)機(jī)就不需要對(duì)輸入做邊沿檢測(cè)。脈沖信號(hào)要求外部傳感器或按鍵消抖電路保證寬度至少一個(gè)時(shí)鐘周期以上。若直接接機(jī)械按鍵必須在頂層先做消抖處理否則同一個(gè)按鍵會(huì)被采樣成連續(xù)多個(gè)有效周期這個(gè)問題在第3章的計(jì)數(shù)器參數(shù)里會(huì)具體算。2.2 狀態(tài)集合與VHDL枚舉類型這個(gè)自動(dòng)售貨機(jī)的行為可以從兩個(gè)維度觀察正在等待第一枚硬幣還是已經(jīng)處于可消費(fèi)狀態(tài)。三個(gè)狀態(tài)足夠描述完整流程IDLE是空閑待機(jī)ACCUM是投幣后等待繼續(xù)投幣或選擇商品VEND是出貨處理。用VHDL枚舉類型定義狀態(tài)綜合工具會(huì)自動(dòng)決定狀態(tài)編碼不會(huì)像手工指定獨(dú)熱碼那樣容易寫錯(cuò)。type fsm_state is (IDLE, ACCUM, VEND); signal state, next_state : fsm_state : IDLE;狀態(tài)少的時(shí)候枚舉類型比獨(dú)熱碼和格雷碼更省事代碼可讀性也更好。若未來狀態(tài)數(shù)超過8個(gè)再考慮把獨(dú)熱碼作為顯式編碼方案因?yàn)閮啥问綘顟B(tài)管理在狀態(tài)數(shù)量增大后優(yōu)勢(shì)才明顯。當(dāng)前設(shè)計(jì)里IDLE收到任意投幣脈沖后進(jìn)入ACCUMACCUM里余額足夠時(shí)選擇商品則進(jìn)入VENDVEND完成一個(gè)周期后回到ACCUM。狀態(tài)轉(zhuǎn)移條件如下表所示。當(dāng)前狀態(tài)轉(zhuǎn)移條件下一狀態(tài)動(dòng)作IDLEcoin_1或coin_05有效ACCUM余額累加ACCUMcoin_1或coin_05再次有效ACCUM余額繼續(xù)累加ACCUM選A且余額30或選B且余額25或選C且余額15VEND扣款并發(fā)出貨脈沖ACCUM選任意商品但余額不足ACCUMalarm拉高一個(gè)周期VEND無條件ACCUM回到等待選擇狀態(tài)這里VEND回到ACCUM而不是回到IDLE是為了讓用戶購(gòu)買后繼續(xù)觀察剩余余額決定是否再投幣購(gòu)買第二件商品。若設(shè)計(jì)為回到IDLE則每次消費(fèi)后余額雖然保留但狀態(tài)會(huì)跳過ACCUM邏輯上不直觀。2.3 余額寄存器用整數(shù)還是無符號(hào)數(shù)余額對(duì)外是金額對(duì)內(nèi)是一個(gè)范圍受限的整數(shù)兩種典型寫法如下。-- 方式一integer適合FSM內(nèi)直接比較和加減 signal balance : integer range 0 to 99 : 0; -- 方式二unsigned適合與數(shù)碼管BCD轉(zhuǎn)換邏輯共用 signal balance_u : unsigned(6 downto 0);用integer定義余額在狀態(tài)機(jī)里最直觀balance 30這樣的表達(dá)式不需要任何算術(shù)庫(kù)。綜合工具會(huì)依據(jù)range約束推斷寄存器位寬VHDL對(duì)integer的默認(rèn)支持也避免了std_logic_vector與算術(shù)運(yùn)算之間頻繁的類型轉(zhuǎn)換。方式二適合后續(xù)還要把余額送到顯示模塊做位分割BCD轉(zhuǎn)換通常用無符號(hào)數(shù)或位向量實(shí)現(xiàn)integer在這類位操作里反而要多一步轉(zhuǎn)換。兩個(gè)寫法都能綜合我的建議是狀態(tài)機(jī)內(nèi)部用integer頂層端口全部用std_logic_vector模塊例化時(shí)再做轉(zhuǎn)換。3. 可綜合的自動(dòng)售貨機(jī)VHDL代碼主控FSM與數(shù)碼管顯示這一章給出可直接復(fù)制到工程里的核心代碼按頂層端口、主狀態(tài)機(jī)、數(shù)碼管掃描和消抖計(jì)數(shù)器三個(gè)模塊拆開。代碼遵循同步復(fù)位、單一時(shí)鐘沿的設(shè)計(jì)規(guī)則這是FPGA開發(fā)里最穩(wěn)妥的寫法也是后面ModelSim仿真能順利跑的基礎(chǔ)。3.1 頂層模塊端口定義library ieee; use ieee.std_logic_1164.all; entity vending_machine is port ( clk : in std_logic; -- 系統(tǒng)時(shí)鐘50 MHz rst_n : in std_logic; -- 復(fù)位低有效 coin_1 : in std_logic; -- 1元硬幣脈沖 coin_05 : in std_logic; -- 0.5元硬幣脈沖 buy_a : in std_logic; -- 選擇商品A3元 buy_b : in std_logic; -- 選擇商品B2.5元 buy_c : in std_logic; -- 選擇商品C1.5元 seg_data : out std_logic_vector(7 downto 0); -- 數(shù)碼管段碼 seg_sel : out std_logic_vector(3 downto 0); -- 數(shù)碼管位選 dispense : out std_logic; -- 出貨脈沖 alarm : out std_logic -- 余額不足提示 ); end vending_machine;端口設(shè)計(jì)遵循“外部信號(hào)只進(jìn)狀態(tài)機(jī)內(nèi)部數(shù)值不直接暴露給引腳”的原則。seg_data和seg_sel的位寬按四位數(shù)碼管設(shè)計(jì)支持顯示余額的十位和個(gè)位剩余兩位留作擴(kuò)展。dispense和alarm都是單bit輸出外部接驅(qū)動(dòng)電路時(shí)需要額外做功率放大或電平轉(zhuǎn)換。3.2 主控狀態(tài)機(jī)單進(jìn)程實(shí)現(xiàn)architecture rtl of vending_machine is type fsm_state is (IDLE, ACCUM, VEND); signal state : fsm_state : IDLE; signal balance : integer range 0 to 99 : 0; begin fsm_proc : process(clk, rst_n) begin if rst_n 0 then state IDLE; balance 0; dispense 0; alarm 0; elsif rising_edge(clk) then -- 默認(rèn)賦值每個(gè)時(shí)鐘周期先清輸出脈沖避免意外保持 dispense 0; alarm 0; case state is when IDLE if coin_1 1 then balance balance 10; state ACCUM; elsif coin_05 1 then balance balance 5; state ACCUM; end if; when ACCUM -- 繼續(xù)投幣優(yōu)先于選擇商品避免同一周期內(nèi)產(chǎn)生競(jìng)態(tài) if coin_1 1 then balance balance 10; elsif coin_05 1 then balance balance 5; elsif buy_a 1 then if balance 30 then balance balance - 30; dispense 1; state VEND; else alarm 1; end if; elsif buy_b 1 then if balance 25 then balance balance - 25; dispense 1; state VEND; else alarm 1; end if; elsif buy_c 1 then if balance 15 then balance balance - 15; dispense 1; state VEND; else alarm 1; end if; end if; when VEND -- 出貨脈沖已在上一拍發(fā)出這里僅切換狀態(tài) state ACCUM; when others state IDLE; end case; end if; end process; end rtl;進(jìn)程中第一句在復(fù)位時(shí)清零所有輸出第二句在每個(gè)上升沿先把dispense和alarm置低再按狀態(tài)機(jī)邏輯決定是否拉高。這個(gè)默認(rèn)賦值習(xí)慣能防止輸出意外保持綜合后不會(huì)推斷出鎖存器。ACCCUM狀態(tài)下投幣和選貨同時(shí)到達(dá)時(shí)優(yōu)先處理投幣因?yàn)閕f-elsif結(jié)構(gòu)天然給了coin_1最高優(yōu)先級(jí)實(shí)際使用中若兩個(gè)事件同時(shí)出現(xiàn)這種處理順序可以接受。VEND狀態(tài)的代碼只做狀態(tài)切換不產(chǎn)生額外輸出所以從波形上看dispense正好持續(xù)一個(gè)時(shí)鐘周期。3.3 數(shù)碼管動(dòng)態(tài)掃描與消抖計(jì)數(shù)參數(shù)余額顯示用四位數(shù)碼管位選按1 kHz頻率循環(huán)刷新每位停留1 ms。50 MHz時(shí)鐘下掃描計(jì)數(shù)器上限是50000。查表代碼只列商品價(jià)格段碼實(shí)際工程按板子的共陰共陽(yáng)接法調(diào)整。signal scan_cnt : integer range 0 to 49999 : 0; signal pos : integer range 0 to 3 : 0; signal bcd_out : integer range 0 to 9 : 0; process(clk, rst_n) begin if rst_n 0 then scan_cnt 0; pos 0; elsif rising_edge(clk) then if scan_cnt 49999 then scan_cnt 0; if pos 3 then pos 0; else pos pos 1; end if; else scan_cnt scan_cnt 1; end if; end if; end process; -- 段碼查表只列0到5的共陽(yáng)段碼 with bcd_out select seg_data 11000000 when 0, 11111001 when 1, 10100100 when 2, 10110000 when 3, 10011001 when 4, 10010010 when 5, 10000010 when 6, 11111000 when 7, 10000000 when 8, 10010000 when 9, 11111111 when others;按鍵消抖是另一個(gè)常見的FPGA小模塊。機(jī)械按鍵抖動(dòng)時(shí)間一般在10 ms以內(nèi)消抖計(jì)數(shù)器按20 ms取兩倍余量。時(shí)鐘為50 MHz時(shí)20 ms對(duì)應(yīng)1000000個(gè)時(shí)鐘周期計(jì)數(shù)器寬度至少20 bit。消抖模塊的完整邏輯是輸入電平變化后啟動(dòng)計(jì)數(shù)器計(jì)數(shù)期間若信號(hào)穩(wěn)定則輸出有效電平否則清零重新計(jì)時(shí)。注意消抖模塊的輸出不能再直接當(dāng)脈沖用需要再做一次邊沿檢測(cè)或單周期化處理。參數(shù)計(jì)算方式取值數(shù)碼管掃描周期每位1 ms四位共4 ms計(jì)數(shù)器上限 49999消抖時(shí)間按鍵抖動(dòng)約10 ms取2倍余量20 ms / 1000000 周期消抖計(jì)數(shù)器位寬log2(1000000) 向上取整20 bit3.4 綜合時(shí)容易被忽略的狀態(tài)覆蓋單進(jìn)程狀態(tài)機(jī)雖然代碼簡(jiǎn)單但case語(yǔ)句必須帶others分支否則綜合工具可能對(duì)未定義狀態(tài)生成不期望的鎖存邏輯。FPGA的綜合工具對(duì)于狀態(tài)機(jī)若發(fā)現(xiàn)非法狀態(tài)默認(rèn)行為是進(jìn)入任意狀態(tài)這可能讓機(jī)器卡死在未定義分支。加一條when others state IDLE;基本能杜絕這個(gè)問題。余量設(shè)計(jì)上余額上限取99角而不是255是因?yàn)樯唐穬r(jià)格都在30角以內(nèi)用戶單次投幣余額不會(huì)超過10元超過后應(yīng)停止累計(jì)這一步可以額外加一個(gè)if balance 83 then balance balance 10的邊界判斷。4. ModelSim仿真自動(dòng)售貨機(jī)Testbench寫法與波形檢查仿真不是為了看波形動(dòng)起來而是為了在綜合前驗(yàn)證狀態(tài)機(jī)的轉(zhuǎn)移條件。自動(dòng)售貨機(jī)的仿真難點(diǎn)在激勵(lì)序列是否接近真實(shí)事件投幣和選貨都是脈沖脈沖之間的間隔必須放在時(shí)鐘邊沿之后否則狀態(tài)機(jī)采樣到的數(shù)據(jù)可能不穩(wěn)定。4.1 Testbench結(jié)構(gòu)與激勵(lì)信號(hào)library ieee; use ieee.std_logic_1164.all; entity vending_machine_tb is end entity; architecture sim of vending_machine_tb is constant CLK_PERIOD : time : 20 ns; -- 50 MHz signal clk : std_logic : 0; signal rst_n : std_logic : 0; signal coin_1 : std_logic : 0; signal coin_05 : std_logic : 0; signal buy_a : std_logic : 0; signal buy_b : std_logic : 0; signal buy_c : std_logic : 0; signal seg_data : std_logic_vector(7 downto 0); signal seg_sel : std_logic_vector(3 downto 0); signal dispense : std_logic; signal alarm : std_logic; begin clk not clk after CLK_PERIOD / 2; uut : entity work.vending_machine port map ( clk clk, rst_n rst_n, coin_1 coin_1, coin_05 coin_05, buy_a buy_a, buy_b buy_b, buy_c buy_c, seg_data seg_data, seg_sel seg_sel, dispense dispense, alarm alarm ); stimulus : process begin rst_n 0; wait for 100 ns; rst_n 1; -- 投1元硬幣 coin_1 1; wait until rising_edge(clk); coin_1 0; wait for 200 ns; -- 再投1元硬幣余額變?yōu)?0角 coin_1 1; wait until rising_edge(clk); coin_1 0; wait for 200 ns; -- 選擇商品C價(jià)格15角 buy_c 1; wait until rising_edge(clk); buy_c 0; wait for 500 ns; assert dispense 1 report dispense should be high after buy_c severity error; wait; end process; end sim;Testbench的關(guān)鍵操作是先低有效復(fù)位100 ns讓狀態(tài)機(jī)和balance都進(jìn)入已知初始值。隨后每個(gè)投幣或選貨脈沖都采用“先拉高信號(hào)等到下一個(gè)時(shí)鐘上升沿再拉低”的寫法這樣輸入信號(hào)在上升沿被采樣時(shí)已經(jīng)穩(wěn)定不會(huì)出現(xiàn)亞穩(wěn)態(tài)窗口。wait until rising_edge(clk)之后立刻拉低等效于輸入數(shù)據(jù)在時(shí)鐘沿前建立、時(shí)鐘沿后消除符合寄存器時(shí)序要求。4.2 三組激勵(lì)序列覆蓋主要路徑場(chǎng)景激勵(lì)預(yù)期結(jié)果余額不足投0.5元選Aalarm1state回ACCUM余額不變剛好購(gòu)買投1元三次選Adispense1余額清零多次購(gòu)買投1元兩次選Cdispense1余額5角隨后可再購(gòu)買商品C第一組場(chǎng)景驗(yàn)證的是alarm在余額不足時(shí)能否拉高一個(gè)周期。第二組場(chǎng)景驗(yàn)證扣款邏輯和VEND狀態(tài)轉(zhuǎn)移投幣三次后余額30角剛好等于商品A價(jià)格。第三組場(chǎng)景驗(yàn)證VEND返回ACCUM后余額保留第二次購(gòu)買C時(shí)扣15角后剩余5角。ModelSim里運(yùn)行可以先執(zhí)行restart -f清空舊波形再執(zhí)行run -all。4.3 仿真失敗時(shí)先檢查這三個(gè)地方狀態(tài)一直停在IDLE但不進(jìn)入ACCUM輸入脈沖只拉高了不到一個(gè)完整時(shí)鐘周期coin_1 1后面必須接wait until rising_edge(clk)不能直接wait固定時(shí)間。余額累加錯(cuò)誤檢查余額被加之前是否已經(jīng)清零或者state只是到了ACCUM而balance沒有同步賦值單進(jìn)程結(jié)構(gòu)里balance和state在同一個(gè)if分支中更新不存在時(shí)序差。波形里看不到內(nèi)部信號(hào)ModelSim默認(rèn)優(yōu)化掉仿真內(nèi)部信號(hào)命令行加vsim -voptargsacc work.vending_machine_tb然后就能把state和balance拖進(jìn)波形窗口。5. 上板前要留意的細(xì)節(jié)時(shí)鐘、復(fù)位與按鍵消抖參數(shù)仿真通過只代表邏輯正確FPGA開發(fā)板上跑又是另一套問題時(shí)鐘相位、復(fù)位釋放時(shí)間和輸出驅(qū)動(dòng)能力都會(huì)影響實(shí)際表現(xiàn)。5.1 復(fù)位引腳的處理VHDL代碼里用的是同步復(fù)位因?yàn)檫M(jìn)程的敏感列表包含clk和rst_nrst_n低電平時(shí)復(fù)位。但這代表rst_n直接進(jìn)入FPGA全局復(fù)位網(wǎng)絡(luò)外部按鍵或撥碼開關(guān)產(chǎn)生的復(fù)位信號(hào)容易帶毛刺。常見做法是在頂層加一個(gè)復(fù)位同步器用兩級(jí)寄存器把外部復(fù)位延后兩拍再送進(jìn)內(nèi)部邏輯避免異步復(fù)位釋放時(shí)剛好落在時(shí)鐘上升沿附近。signal rst_sync : std_logic_vector(1 downto 0) : 00; process(clk) begin if rising_edge(clk) then rst_sync(0) rst_n; rst_sync(1) rst_sync(0); end if; end process; -- 內(nèi)部使用 rst_sync(1) 代替 rst_n5.2 出貨信號(hào)需要輸出保持狀態(tài)機(jī)里dispense只拉高一個(gè)時(shí)鐘周期50 MHz下只有20 ns驅(qū)動(dòng)發(fā)光二極管勉強(qiáng)可見驅(qū)動(dòng)電機(jī)繼電器就完全不夠。給dispense加一個(gè)計(jì)數(shù)器把脈沖擴(kuò)展成100 ms的高電平再連接外部驅(qū)動(dòng)。擴(kuò)展邏輯最簡(jiǎn)單的方法是dispense有效時(shí)計(jì)數(shù)器清零只要計(jì)數(shù)器不滿則dispense保持高電平計(jì)數(shù)器達(dá)到上限后拉低。5.3 邊界條件驗(yàn)證清單上板前把五類情況各跑一遍不投幣直接選貨不報(bào)錯(cuò)、投幣正好等于商品價(jià)格、余額超過商品價(jià)格后找零、連續(xù)兩次購(gòu)買同一商品、投幣到余額上限停止累計(jì)。其中余額上限最容易被忽略若整數(shù)balance上限是991元硬幣繼續(xù)投入會(huì)溢出必須在累加分支里加邊界判斷保證余額不超過設(shè)計(jì)最大值。本文還有配套的精品資源點(diǎn)擊獲取