Menu

Verilog FSM: מכונות מצבים סופיות בתבנית הסטנדרטית

איך כותבים FSM ב-Verilog כמו שאנשי מקצוע עושים: אוגר מצב מסונכרן לשעון, בלוק צירופי למצב הבא, והפרדה נקייה שקל לקרוא ולסנתז.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

מה זה FSM

מכונת מצבים סופית היא בקר ש:

  • מחזיק בכל רגע נתון אחד מתוך סט קבוע של מצבים עם שמות.
  • עובר בין מצבים לפי הכניסות (ואולי גם לפי המצב הנוכחי).
  • מפיק פלטים שתלויים במצב הנוכחי (ואולי גם בכניסות הנוכחיות).

זה מכסה חלק מפתיע של התכנון הדיגיטלי: בקרי רמזורים, משדרי UART, בקרי זיכרון, מטפלי פרוטוקולי רשת, מפענחי הוראות, כל דבר שיש לו מצבי פעולה בדידים.

מה שגורם ל-FSMs להרגיש שונים מלוגיקת datapath הוא שהם מצבים, לא ערכים. מונה מתקדם דרך 1, 2, 3, 4. FSM מתקדם דרך IDLE, FETCHING, BUSY, DONE: אותה צורה, אבל למצבים יש שמות ולמעברים יש משמעות.

תבנית שני התהליכים

ה-FSM הסטנדרטי ב-Verilog משתמש בשני בלוקים של always:

  1. בלוק מסונכרן לשעון שלוכד את אוגר המצב בכל קצה שעון. משתמש בהשמה לא חוסמת. זעיר, בדרך כלל שלוש שורות.
  2. בלוק צירופי שמשתמש ב-case על המצב הנוכחי כדי לחשב את המצב הבא ואת הפלטים. משתמש בהשמה חוסמת. הקוד ה"מעניין" נמצא כאן.

להפרדה הזו שלושה יתרונות: היא מכריחה אתכם לחשוב על המצב במפורש, היא מייצרת חומרה שממופה בצורה נקייה ל"אוגר אחד ועוד בלוק צירופי אחד", והיא הסטנדרט: כל מי שיקרא את ה-Verilog שלכם יזהה אותה מיד.

דוגמה מלאה: גלאי רצף

FSM קלאסי ללימוד: זיהוי רצף הביטים 1011 בכניסה טורית. מפיקים פולס של מחזור אחד כשהרצף מושלם.

המבנה של שני הבלוקים הוא מה שתואר למעלה. כדאי להצביע על פרטי הניקיון:

  • ערכי ברירת מחדל בראש הבלוק הצירופי. next_state = state (הישארו במקום) ו-detected = 1'b0 (בלי פולס) הן השמות ה"לא לעשות כלום". כל ענף של case קובע אחר כך רק את מה שונה. כך אי אפשר להסיק latch.
  • localparam לשמות המצבים. מי שקורא את המודול חושב במונחים של S0, S1, S2, S3, לא 3'd0, 3'd1. הסינתיסייזר מבצע את ההחלפה.
  • אין פלטים מהבלוק המסונכרן לשעון. כל הלוגיקה של "מה המצב הזה עושה" נמצאת בבלוק הצירופי. הבלוק המסונכרן לשעון אחראי רק על החזקת המצב הנוכחי.

Moore מול Mealy

Moore: הפלט תלוי רק במצב הנוכחי. Mealy: הפלט תלוי במצב הנוכחי וגם בכניסות הנוכחיות.

בדוגמה שלמעלה, detected נקבע בתוך הענף S3 רק כאשר in מתאים לאחת מהתבניות הצפויות שמשלימות את הרצף. זה פלט Mealy: הוא תלוי ב-in בנוסף ל-state. גרסת Moore הייתה כוללת מצב נפרד של "זה עתה זוהה", וקובעת detected = 1 בכל פעם שזה המצב הנוכחי; הפלט היה פולס מחזור אחד מאוחר יותר, אבל לעולם לא היה רגיש ל-glitch ב-in.

שני הסגנונות תקפים. Moore הוא ברירת המחדל בספרי לימוד, כי מובטח שהפלטים לא יעשו glitch כשהכניסות משתנות באמצע מחזור. Mealy מהיר יותר (אין latency של אוגר לפלטים שמונעים מכניסות) ובמקרים רבים מייצר חומרה קטנה יותר. בחרו לפי הפרוטוקול שאתם מממשים.

קידוד מצבים: בינארי, one-hot, gray

תבנית הביטים שמקצים לכל מצב משפיעה על שטח ועל מהירות:

  • בינארי (S0 = 3'd0, S1 = 3'd1, ...): אוגר המצב הקטן ביותר, ⌈log⁡2N⌉\lceil \log_2 N \rceil ביטים עבור NN מצבים. לוגיקת פענוח מקסימלית.
  • One-hot (S0 = 4'b0001, S1 = 4'b0010, ...): N ביטים ל-N מצבים. לוגיקת הפענוח טריוויאלית (כל מצב הוא wire אחד); המעברים מהירים. FPGAs בוחרים בזה לעיתים קרובות כברירת מחדל.
  • Gray code: מצבים עוקבים נבדלים בביט אחד. שימושי כשביטי המצב חוצים תחומי שעון.

רוב כלי הסינתזה המודרניים בוחרים את הקידוד בשבילכם (ל-Vivado, Quartus ו-Design Compiler יש מצב אוטומטי שמנסה כל אחד ובוחר את הטוב ביותר). לעיתים רחוקות צריך לציין. כשכן צריך: רוב הכלים מקבלים הערת attribute או pragma של (* fsm_encoding = "one_hot" *).

גרסה בשלושה בלוקים

מדי פעם תראו FSM שמפוצל ל_שלושה_: בלוק מסונכרן לשעון למצב, בלוק צירופי למצב הבא, ובלוק צירופי לפלטים. זו פשוט תבנית שני הבלוקים, כשחישוב הפלטים הוצא לבלוק משלו:

// State register
always @(posedge clk) ...

// Next-state logic
always @(*) ...

// Output logic
always @(*) begin
    case (state)
        ...
    endcase
end

סגנון הפלטים המפוצל שימושי כשהפלטים גדולים ולוגיקת המצב הבא הייתה עמוסה מדי אם הם היו באותו בלוק. ב-FSMs קטנים זה מוגזם.

מה default עושה ב-FSM

לכל משפט case של FSM צריך להיות ענף default. שתי סיבות:

  1. בטיחות: אם אוגר המצב מקבל איכשהו ערך לא חוקי (השחתה, באג, reset חלקי), default מחזיר אותו למצב ידוע.
  2. רמז לסינתזה: כשהמקרים המפורשים ממצים (מצב של 2 ביטים עם טיפול בכל 4 הערכים, למשל), default: next_state = 'x; אומר לסינתיסייזר "אני מבטיח שברירת המחדל לא ניתנת להשגה, בצע אופטימיזציה בחופשיות". אם המסלול הבלתי אפשרי כן מתרחש בסימולציה, ה-x שנוצר מתפשט וחושף את הבאג מיד.
default: begin
    next_state = S0;   // safe recovery
    // or
    next_state = 'x;   // unreachable, optimize freely
end

בחרו לפי השאלה אם הוכחתם שברירת המחדל באמת לא ניתנת להשגה.

דברים שכדאי לשים לב אליהם

שכחת ערכי ברירת המחדל בראש הבלוק הצירופי. בלי next_state = state וברירות המחדל של הפלטים, ענף שלא מבצע השמה לכל דבר יוצר latch.

הכנסת פלטים לבלוק המסונכרן לשעון. אם detected <= 1 נמצא בבלוק always @(posedge clk), הפלט עובר דרך אוגר ומופיע מחזור אחד באיחור. זה יכול להיות מכוון (פלט "registered Mealy"), אבל זו טעות תכנון נפוצה כשהמפרט דורש פולס מיידי.

ערבוב השמה חוסמת ולא חוסמת. בלוק מסונכרן לשעון: <=. בלוק צירופי: =. ערבוב של השניים באותו בלוק הוא race condition.

בלוק always צירופי שקורא את next_state ומבצע השמה ל-state. זה בונה לולאת משוב שהסימולטור לא יכול לפתור. הבלוק המסונכרן לשעון הוא הבעלים של state; הבלוק הצירופי הוא הבעלים של next_state; לעולם אל תתנו לאחד מהם לגעת במשתנה של השני.

מה הלאה

עכשיו אתם יכולים לבנות כל בקר שאתם יודעים לתאר. הפרק הבא לוקח צעד אחורה מתכנון בר סינתזה ועוסק ב-testbenches שמפעילים אותו: איך מזינים גירוי, צופים בפלטים, שומרים waveforms, ומוודאים שהמודולים שלכם באמת עושים את מה שאתם חושבים.

שאלות נפוצות

מה זו מכונת מצבים סופית ב-Verilog?

FSM הוא בקר שמחזיק אחד מתוך קבוצה קטנה של מצבים עם שמות, ועובר ביניהם לפי הכניסות. ב-Verilog, המימוש הסטנדרטי כולל שני בלוקים: always מסונכרן לשעון שמעדכן את אוגר המצב בכל קצה שעון, ו-always צירופי שמחשב את המצב הבא ואת הפלטים לפי המצב הנוכחי והכניסות.

מה תבנית ה-FSM הסטנדרטית ב-Verilog?

FSM בשני תהליכים: בלוק always @(posedge clk) מסונכרן לשעון מחזיק את אוגר המצב ומשתמש בהשמה לא חוסמת, ובלוק צירופי always @(*) משתמש ב-case על המצב הנוכחי כדי לחשב את המצב הבא ואת הפלטים. ההפרדה הזו הופכת את הקוד לקל לקריאה, ל-lint ולסינתזה נקייה.

מה ההבדל בין FSM מסוג Mealy ל-Moore?

ב-FSM מסוג Moore, הפלטים תלויים רק במצב הנוכחי. ב-FSM מסוג Mealy, הפלטים תלויים גם במצב הנוכחי וגם בכניסות הנוכחיות. מכונות Mealy מגיבות מחזור אחד מהר יותר (אין latency של אוגר בפלטים שתלויים בכניסות), אבל עלולות לייצר glitches אם הכניסות משתנות באמצע מחזור. מכונות Moore איטיות במחזור אחד אבל צפויות יותר, והן בחירת ברירת המחדל אלא אם צריך את המהירות.

איך מקודדים מצבים ב-Verilog?

משתמשים בקבועי localparam בתוך המודול: localparam IDLE = 3'd0; וכן הלאה. שלושה קידודים נפוצים: בינארי (מצבים 0, 1, 2, ..., אוגר המצב הקטן ביותר), one-hot (ביט אחד לכל מצב, פחות רמות לוגיקה לכל מעבר), ו-gray code (מצבים עוקבים נבדלים בביט אחד, וזה ממזער glitches). כלי סינתזה בדרך כלל בוחרים את הקידוד בשבילכם; לעיתים רחוקות צריך לקבע אותו.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל