סוס העבודה של Verilog התנהגותי
assign מתאר לוגיקה צירופית של משוואה אחת. ברגע שצריך if/else, case או זיכרון, עוברים ל-always. בלוק always הוא קטע קוד פרוצדורלי שרץ מחדש בכל פעם שאותות מסוימים משתנים. האותות שמפעילים את הריצה מחדש הם רשימת הרגישות.
יש שתי צורות של always שתפגשו הכי הרבה:
always @(*): רץ מחדש כש_כל_ אות שנקרא בבלוק משתנה. בונה לוגיקה צירופית.always @(posedge clk): רץ מחדש רק בקצה העולה שלclk. בונה לוגיקה סדרתית מסונכרנת לשעון (flip-flops).
קיימות צורות נוספות (@(a or b), @(negedge clk), @(posedge clk or negedge reset_n)), אבל שתי הצורות שלמעלה מכסות כמעט כל בלוק בר סינתזה שתכתבו.
always @(*) צירופי
שלושה דברים שכדאי לשים לב אליהם:
outהואreg. כל מה שמקבל השמה בתוךalwaysחייב להיותreg. מילת המפתח לא אומרת "זה flip-flop"; כאן היא אומרת רק "אני נכתב מתוך בלוק פרוצדורלי".always @(*). ה-*אומר "תתעורר בכל פעם שמשהו שאני קורא משתנה". הסימולטור מחשב את רשימת הרגישות בעצמו. אפשר לכתוב את הרשימה ידנית,always @(sel), אבל@(*)בטוח יותר, כי אות שנשכח ברשימה הוא מקור קלאסי לבאגים.- אין שעון. הבלוק הזה מתאר לוגיקה צירופית. הסינתיסייזר מייצר לוגיקה שמחשבת את
outישירות מתוךsel, בלי flip-flops ובלי צורך בפין שעון.
המקרה default הוא אופציונלי בתחביר, אבל לא ברוח. אם משמיטים אותו, כל ערך כניסה שלא טופל משאיר את out עם הערך הקודם שלו, ומזה נוצר בסינתזה latch לא מכוון. תמיד כללו את ה-default.
always @(posedge clk) סדרתי
ההבדלים העיקריים לעומת הגרסה הצירופית:
always @(posedge clk). הבלוק רץ מחדש רק בקצה העולה שלclk. בין הקצוות לא קורה כלום.- השמה לא חוסמת
<=. בתוך בלוק מסונכרן לשעון זה האופרטור הנכון. הוא אומר "תזמן ל-countלקבל את הערך החדש בסוף צעד הזמן", וזו בדיוק ההתנהגות של flip-flop. הסיבה והחלופה מוסברות ב-השמה חוסמת מול לא חוסמת. - אין צורך ב-
default. ה-ifמכסה את שני הענפים (reset ובלי reset), כך שאין סיכון ל-latch.
הסינתיסייזר מזהה את הצורה הזו, רגישות לשעון והשמה לא חוסמת, ומייצר אוגר של 4 ביטים (ארבעה flip-flops), יחד עם הלוגיקה הצירופית שמחשבת את count + 1 וה-mux שבוחר בין reset להגדלה.
ההבחנה בסינתזה
אותו קוד מקור של מודול יכול לתאר שני רכיבי חומרה שונים לגמרי, לפי הצורה של בלוק ה-always:
| בלוק | חומרה |
|---|---|
always @(*) y = expr; | לוגיקה צירופית טהורה. בלי זיכרון. |
always @(posedge clk) y <= expr; | Flip-flop. לוכד את expr פעם אחת בכל מחזור שעון. |
always @(*) if (en) y = expr; | Latch, בדרך כלל באג. במקרה ה-"else" נשמר הערך הישן. |
always @(posedge clk) if (en) y <= expr; | Flip-flop עם enable. לוכד רק כש-en גבוה. |
המקרה השלישי הוא מלכודת ה-latch. Latch הוא תא זיכרון שקוף שמחזיק את הפלט שלו כשהכניסה לא פעילה. הוא שימושי בתכנונים מסוימים, וכמעט תמיד באג כשהוא נוצר בטעות. רוב כלי הסינתזה מתריעים בקול רם כשהם מסיקים latch שלא ביקשתם. התייחסו לאזהרה הזו כאל שגיאה.
גרסאות של רשימת הרגישות
תפגשו גם כמה רשימות רגישות פחות נפוצות:
always @(a or b or c): רשימה מפורשת. Verilog-2001 הוסיפה את המפריד,:always @(a, b, c). שתי הצורות עובדות.always @(posedge clk or negedge reset_n): reset אסינכרוני. הבלוק רץ בקצה עולה של השעון או בקצה יורד של ה-reset. משתמשים בזה כשה-reset חייב להשפיע מיד, בלי לחכות לשעון הבא.always @(negedge clk): סנכרון לקצה היורד. נדיר; יש תכנונים שמשתמשים בזה ל-flip-flops "מופעלי קצה יורד" שלוכדים בקצה היורד במקום בעולה.
בתכנונים חדשים העדיפו always @(*) ללוגיקה צירופית ו-always @(posedge clk) ללוגיקה סדרתית. השתמשו ב-reset אסינכרוני רק כשהתכנון באמת צריך אותו.
שני בלוקים הם שני רכיבי חומרה
כמה בלוקים של always באותו מודול הם בלתי תלויים, וכל אחד מהם הופך לרכיב חומרה משלו:
הבלוק המסונכרן לשעון מייצר אוגר של flip-flops. הבלוק הצירופי מייצר שער XOR. הם חיים זה לצד זה, ואף אחד מהם לא יודע על השני. שני הפלטים משתנים בלוחות זמנים שונים לגמרי.
מה בלוקים של always לא יכולים לעשות
כמה דברים שנראים מפתים אבל אסורים:
- השמה ל-
wire: היעד חייב להיותreg. הקומפיילר אוכף את זה. - השמה לאותו
regמשני בלוקים שונים שלalways: יוצרת התנהגות לא מוגדרת בסימולציה ולא עוברת סינתזה. מקור הזנה אחד לכל אות. - קריאה וכתיבה של אותו אות באותו בלוק צירופי באופן שיוצר לולאת משוב:
always @(*) x = x + 1;היא לולאה באפס השהיה שהסימולטור לא יכול לפתור.
את שני הראשונים הקומפיילר תופס. השלישי מופיע לפעמים רק בזמן הסימולציה, בצורה של תקיעה.
מה הלאה
המסמך הבא, בלוק initial, עוסק באח של always: בלוק שרץ בדיוק פעם אחת בתחילת הסימולציה. הוא סוס העבודה של ה-testbenches. אחריו מגיעים הכללים של השמה חוסמת מול לא חוסמת, שקובעים אם הבלוק המסונכרן שלכם עושה את מה שהתכוונתם.
שאלות נפוצות
מה זה בלוק always ב-Verilog?
always פותח בלוק פרוצדורלי שרץ מחדש בכל פעם שהאותות ברשימת הרגישות שלו משתנים. יש שני סוגים: always @(*) בונה לוגיקה צירופית (רץ מחדש כשכל כניסה שהיא משתנה), ו-always @(posedge clk) בונה לוגיקה סדרתית (רץ מחדש בכל קצה עולה של clk). גוף של בלוק always יכול להכיל if, case, for והשמות פרוצדורליות.
מה ההבדל בין always @(*) לבין always @(posedge clk)?
always @(*) רגיש לכל אות שנקרא בבלוק, והוא מייצר לוגיקה צירופית בלי זיכרון. always @(posedge clk) רגיש רק לקצה העולה של clk, והוא מייצר flip-flops שלוכדים מצב פעם אחת בכל מחזור שעון. לראשון אין שעון ואין אוגר, לשני יש את שניהם.
מה זו רשימת רגישות ב-Verilog?
רשימת האותות אחרי @ שקובעת מתי בלוק always רץ מחדש. @(*) הוא קיצור של 'כל אות שנקרא בבלוק'. @(posedge clk) רץ רק בקצה העולה של clk. @(posedge clk or negedge reset_n) רץ בכל אחד משני האירועים, ומשמש ל-reset אסינכרוני. טעות ברשימת הרגישות היא אחד המקורות הנפוצים ביותר לפער בין סימולציה לסינתזה.
אפשר לבצע השמה ל-wire בתוך בלוק always?
לא. בלוקים של always יכולים לבצע השמה רק ל-reg (או ל-logic ב-SystemVerilog), והקומפיילר אוכף את זה. אם רוצים ש-wire יהיה הפלט של לוגיקה פרוצדורלית, מצהירים על reg ביניים, מזינים אותו בתוך always, ומחברים את ה-wire ל-reg עם assign מחוץ לבלוק. או פשוט הופכים את ה-wire ל-reg.