מה זה FSM
מכונת מצבים סופית היא בקר ש:
- מחזיק בכל רגע נתון אחד מתוך סט קבוע של מצבים עם שמות.
- עובר בין מצבים לפי הכניסות (ואולי גם לפי המצב הנוכחי).
- מפיק פלטים שתלויים במצב הנוכחי (ואולי גם בכניסות הנוכחיות).
זה מכסה חלק מפתיע של התכנון הדיגיטלי: בקרי רמזורים, משדרי UART, בקרי זיכרון, מטפלי פרוטוקולי רשת, מפענחי הוראות, כל דבר שיש לו מצבי פעולה בדידים.
מה שגורם ל-FSMs להרגיש שונים מלוגיקת datapath הוא שהם מצבים, לא ערכים. מונה מתקדם דרך 1, 2, 3, 4. FSM מתקדם דרך IDLE, FETCHING, BUSY, DONE: אותה צורה, אבל למצבים יש שמות ולמעברים יש משמעות.
תבנית שני התהליכים
ה-FSM הסטנדרטי ב-Verilog משתמש בשני בלוקים של always:
- בלוק מסונכרן לשעון שלוכד את אוגר המצב בכל קצה שעון. משתמש בהשמה לא חוסמת. זעיר, בדרך כלל שלוש שורות.
- בלוק צירופי שמשתמש ב-
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, ...): אוגר המצב הקטן ביותר, ביטים עבור מצבים. לוגיקת פענוח מקסימלית. - 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. שתי סיבות:
- בטיחות: אם אוגר המצב מקבל איכשהו ערך לא חוקי (השחתה, באג, reset חלקי),
defaultמחזיר אותו למצב ידוע. - רמז לסינתזה: כשהמקרים המפורשים ממצים (מצב של 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). כלי סינתזה בדרך כלל בוחרים את הקידוד בשבילכם; לעיתים רחוקות צריך לקבע אותו.