שני האופרטורים
בתוך בלוקים של always ו-initial יש ב-Verilog שני אופרטורי השמה:
=הוא השמה חוסמת (blocking). מעדכנים את צד שמאל עכשיו, לפני שעוברים להוראה הבאה.<=הוא השמה לא חוסמת (non-blocking). מחשבים את צד ימין עכשיו, ומתזמנים את העדכון של צד שמאל לסוף צעד הזמן הנוכחי.
מחוץ לבלוקים פרוצדורליים (ב-assign) קיים רק =: assign a <= b היא שגיאת תחביר. בתוך בלוקים פרוצדורליים שניהם חוקיים, והבחירה הנכונה ביניהם היא ההחלטה החשובה ביותר ב-Verilog למתחילים.
מה השמה חוסמת עושה
השמה חוסמת מתנהגת כמו שהייתם מצפים משפת תוכנה: שורה אחר שורה, מלמעלה למטה. הוראה N מסתיימת לפני שהוראה N+1 מתחילה:
זה סדרתי לחלוטין. כל הוראה רואה את ההשפעות של ההוראות שלפניה, בדיוק כמו האינטואיציה מתוכנה.
מה השמה לא חוסמת עושה
השמה לא חוסמת בנויה כמו חומרה. כל צדי הימין מחושבים לפי הערכים בתחילת צעד הזמן. כל צדי השמאל מתעדכנים בסוף צעד הזמן. סדר ההוראות בקוד המקור לא משנה את התלויות בין האותות:
הקטע הזה מממש סיבוב משולש a → b → c → a בצעד אחד. עם השמה חוסמת הייתם צריכים משתנה זמני כדי לא לדרוס אחד מהם. עם השמה לא חוסמת ההחלפה קורית באופן אטומי, כי כל צד ימין קורא את הערכים שלפני הצעד.
כך בדיוק מתנהגים שלושה flip-flops בקצה שעון: כולם לוכדים את הכניסות שלהם באותו רגע, בלי קשר לתלויות ביניהם.
הכלל שמציל אתכם
השתמשו ב-<= בבלוקים מסונכרנים לשעון. השתמשו ב-= בבלוקים צירופיים.
זה הכול. שננו אותו. כתבו אותו בלי לחשוב. רוב ה-race conditions, הפערים בין סימולציה לסינתזה, והבאגים מסוג "אצלי זה עובד אבל לא בסינתזה" נובעים מהפרה של הכלל הזה.
לכלל יש סיבה חומרתית: בלוקים מסונכרנים לשעון ממדלים flip-flops שדוגמים את הכניסות שלהם בו זמנית. בלוקים צירופיים ממדלים לוגיקה שמתפשטת הכי מהר שהיא יכולה. הסמנטיקה של אופרטורי ההשמה תואמת את שתי ההתנהגויות האלה.
כל <= קורא את הערך הנוכחי של אוגר המקור, ומתזמן לאוגר היעד לקבל את הערך הזה בסוף צעד הזמן. התוצאה היא בדיוק מה ש-shift register בחומרה עושה: כל flip-flop לוכד את הערך של השכן שלו, כולם באותו קצה שעון, בלי race.
עכשיו חשבו מה היה קורה עם השמה חוסמת:
// WRONG - this is not a shift register!
always @(posedge clk) begin
out[3] = out[2]; // out[3] becomes out[2]
out[2] = out[1]; // out[2] becomes out[1], which we just set above
out[1] = out[0];
out[0] = in;
end
כל הוראה דורסת את המקור לפני שההוראה הבאה קוראת אותו. במחזור שעון אחד, in היה מתפשט עד out[3], כי כל שורה רואה את הערך שנכתב זה עתה בשורה שלפניה. ההתנהגות בחומרה אמיתית (שעובדת בסמנטיקה לא חוסמת) הייתה שונה לגמרי ממה שהסימולטור הראה.
בלוקים צירופיים: השמה חוסמת היא הנכונה
ב-always @(*) השמה חוסמת היא הנכונה. אין flip-flops, אין כלל של לכידה בו זמנית שצריך לאכוף, ומשתני ביניים שימושיים:
sum מחושב קודם, ואז result משתמש בערך שחושב זה עתה. הלוגיקה הצירופית מתכווצת לרכיב חומרה אחד: result = ~(a + b). לא מופיעים flip-flops, כי אין שעון.
אם הייתם משתמשים כאן ב-<=, הסימולטור עדיין היה מעדכן את sum לפני ש-result מחושב מולו (כי שני העדכונים קורים בסוף הצעד), אבל הסדר היה שונה בדקות, והרבה כלי סינתזה מתלוננים. אל תערבבו; בחרו את האופרטור שמתאים לסוג הבלוק.
הטעות שכואבת הכי הרבה
הנה היא: בלוק מסונכרן לשעון עם השמה חוסמת.
// BUG: race condition waiting to happen
always @(posedge clk) begin
a = b;
b = c;
c = a;
end
בסימולציה, הסימולטור עשוי לטפל ב-a קודם, אחר כך ב-b ואז ב-c, ולהפיק סט ערכים אחד. החומרה תפיק סט אחר, כי flip-flops אמיתיים לוכדים בו זמנית. השניים מתפצלים בשקט, ותבזבזו יום שלם על חיפוש הבאג. השתמשו ב-<= בבלוקים מסונכרנים לשעון.
למה בכלל יש שני אופרטורים
המתכננים של Verilog יכלו לבחור סמנטיקת השמה אחת ולהיצמד אליה. הם לא עשו זאת, כי השפה צריכה למדל שתי התנהגויות חומרה שונות:
- לוגיקה צירופית: אותות מתפשטים ברציפות, התלויות חשובות, והשאלה "מה השער הזה מחשב" הגיונית.
- לוגיקה סדרתית: מגיע קצה שעון, כל ה-flip-flops לוכדים בו זמנית, והתלויות בין הכניסות והיציאות של ה-flip-flops מנותקות.
השמה חוסמת היא בשביל הראשונה. השמה לא חוסמת היא בשביל השנייה. האופרטור בוחר את הסמנטיקה; הסימולטור עושה את השאר.
מה הלאה
עכשיו יש לכם את הכללים לכתוב כל בלוק פרוצדורלי נכון. הפרק הבא עולה מבלוקים בודדים למבני בקרת הזרימה שנכנסים לתוכם: if/else, case ולולאות for. הכללים של השמה חוסמת מול לא חוסמת חלים גם בתוך כולם.
שאלות נפוצות
מה ההבדל בין השמה חוסמת להשמה לא חוסמת ב-Verilog?
השמה חוסמת (=) מעדכנת את היעד מיד, לפני שההוראה הבאה רצה, והיא ממדלת ביצוע סדרתי. השמה לא חוסמת (<=) מתזמנת את העדכון לסוף צעד הזמן הנוכחי: כל צד ימין של השמה לא חוסמת מחושב לפי הערכים הישנים של כל האותות, ואז כל צדי השמאל מתעדכנים בצעד מתואם אחד. כך בדיוק מתנהגים flip-flops.
מתי להשתמש ב-= ומתי ב-<= ב-Verilog?
הכלל: השתמשו ב-<= בבלוקים מסונכרנים לשעון always @(posedge clk), וב-= בבלוקים צירופיים always @(*). הכלל הזה לבדו מונע את כל משפחת ה-race conditions שנוצרות מהשמות מעורבות. בתוך בלוקי initial של testbench, = הוא הבחירה הרגילה; <= מופיע שם לעיתים רחוקות.
למה צריך השמה לא חוסמת ב-Verilog?
כי flip-flops בחומרה לוכדים את הכניסות שלהם כולם בו זמנית בקצה השעון. אם הייתם משתמשים ב-= (השמה חוסמת) בקוד מסונכרן לשעון, סדר ההוראות בקובץ היה משנה איזה אות רואה את הערך החדש של איזה אות אחר: race condition בין הסימולציה לחומרה האמיתית. <= תואם את התנהגות החומרה, כי הוא מחשב קודם את כל צדי הימין ורק אז מבצע את כל העדכונים.
מה קורה אם מערבבים = ו-<= באותו בלוק always ב-Verilog?
מקבלים race condition. הערבוב מייצר חומרה שההתנהגות שלה תלויה בפרטים הפנימיים של הסימולטור (סדר תזמון האירועים), וגרוע מזה, הסימולציה עלולה לא להתאים למה שהסינתזה מייצרת. רוב כלי ה-lint מסמנים את זה כשגיאה. הפתרון הוא לבחור באחד מהם לפי תפקיד הבלוק: לא חוסמת לבלוק מסונכרן לשעון, חוסמת לבלוק צירופי.