การใช้งานคำสั่ง always ใน Verilog: คู่มือพื้นฐานถึงขั้นสูงสำหรับนักออกแบบวงจรดิจิทัล

目次

1. บทนำ

บทบาทของคำสั่ง always ใน Verilog คืออะไร?

ในภาษา Verilog HDL ซึ่งเป็นภาษาบรรยายฮาร์ดแวร์ที่ใช้กันอย่างแพร่หลายในการออกแบบวงจรดิจิทัล คำสั่ง always มีบทบาทที่สำคัญมาก การเขียนโค้ดใน Verilog ไม่ได้บรรยายการทำงานของฮาร์ดแวร์เหมือนซอฟต์แวร์ทั่วไป แต่เป็นการ “กำหนดว่าภายใต้เงื่อนไขใด สัญญาณจะเปลี่ยนแปลงอย่างไร” ภายในนั้น คำสั่ง always ใช้สำหรับ บรรยายการทำงานเฉพาะเมื่อเกิดเงื่อนไขที่กำหนด ซึ่งเป็นโครงสร้างพื้นฐานในการออกแบบวงจร

ทำไมต้องใช้คำสั่ง always?

ใน Verilog การบรรยายพฤติกรรมของวงจรสามารถแบ่งได้เป็น 2 ประเภทหลัก

  • วงจรผสม (Combinational Circuit): เอาต์พุตเปลี่ยนแปลงทันทีเมื่ออินพุตเปลี่ยน
  • วงจรลำดับ (Sequential Circuit): เอาต์พุตเปลี่ยนแปลงตามจังหวะสัญญาณนาฬิกา (Clock)

หากใช้เพียงคำสั่ง assign เพียงอย่างเดียว ไม่สามารถบรรยายเงื่อนไขซับซ้อนหรือการจดจำสถานะได้ ตรงนี้เองที่คำสั่ง always ถูกนำมาใช้

เช่น หากต้องการบรรยายลอจิกที่มีหลายเงื่อนไข หรือวงจรที่ใช้ฟลิปฟลอปสำหรับเก็บค่า จำเป็นต้องใช้ always ร่วมกับโครงสร้างควบคุม เช่น if หรือ case

รูปแบบการใช้งานคำสั่ง always ที่พบได้บ่อย

คำสั่ง always มีหลายรูปแบบ โดยขึ้นอยู่กับชนิดของวงจรที่ต้องการออกแบบ

  • always @(*)
     → ใช้สำหรับบรรยายวงจรผสม
  • always @(posedge clk)
     → ใช้สำหรับวงจรลำดับที่ทำงานตามขอบขาขึ้นของสัญญาณนาฬิกา
  • always @(posedge clk or negedge rst)
     → ใช้สำหรับวงจรลำดับที่มีรีเซ็ตแบบอะซิงโครนัส

ดังนั้น การเข้าใจ คำสั่ง always ซึ่งเป็นหัวใจสำคัญของ Verilog ถือเป็นก้าวแรกที่สำคัญสำหรับผู้ออกแบบฮาร์ดแวร์

วัตถุประสงค์ของบทความนี้

บทความนี้จะอธิบายเกี่ยวกับคำสั่ง always ใน Verilog ตั้งแต่ ไวยากรณ์พื้นฐาน วิธีใช้งานขั้นสูง ข้อควรระวัง ไปจนถึงการขยายความใน SystemVerilog

  • ต้องการเรียนรู้การเขียน always ที่ถูกต้อง
  • ไม่เข้าใจสาเหตุของข้อผิดพลาดในการสังเคราะห์ลอจิก
  • สับสนระหว่างการใช้ = และ <=
  • อยากหลีกเลี่ยงข้อผิดพลาดที่พบบ่อยของผู้เริ่มต้น

บทความนี้จะเป็นไกด์ที่เข้าใจง่ายและใช้ได้จริงสำหรับผู้อ่านที่มีข้อสงสัยเหล่านี้

2. ไวยากรณ์พื้นฐานและประเภทของคำสั่ง always

ไวยากรณ์พื้นฐานของ always

คำสั่ง always ใน Verilog ใช้สำหรับรันโค้ดซ้ำตามเงื่อนไขที่กำหนด (sensitivity list) รูปแบบพื้นฐานคือ:

always @(sensitivity list)
begin
  // การทำงานที่จะประมวลผล
end

ส่วนสำคัญคือ “sensitivity list” ซึ่งใช้กำหนดว่า เมื่อสัญญาณใดเปลี่ยนแปลง บล็อกนี้จะถูกประมวลผล

การใช้ always @(*) (วงจรผสม)

ในวงจรผสม เอาต์พุตจะต้องเปลี่ยนทันทีเมื่ออินพุตเปลี่ยน ดังนั้นจึงใช้ @(*) ใน sensitivity list

always @(*) begin
  if (a == 1'b1)
    y = b;
  else
    y = c;
end

เมื่อเขียนเช่นนี้ หากสัญญาณ a, b, c ใดเปลี่ยนแปลง บล็อก always จะถูกรันใหม่ และเอาต์พุต y จะถูกคำนวณอีกครั้ง

ข้อดีของการใช้ @(*)

  • รวมสัญญาณอินพุตทั้งหมดเข้ามาใน sensitivity list โดยอัตโนมัติ
  • ช่วยป้องกันปัญหาความไม่ตรงกันระหว่างการจำลอง (simulation) และการสังเคราะห์ (synthesis)

การใช้ always @(posedge clk) (วงจรลำดับ)

ในวงจรลำดับ การเปลี่ยนแปลงเกิดขึ้นตามสัญญาณนาฬิกา จึงใช้ posedge clk ใน sensitivity list

always @(posedge clk) begin
  q <= d;
end

ในตัวอย่างนี้ เมื่อถึงขอบขาขึ้นของสัญญาณนาฬิกา ค่า d จะถูกบันทึกลงใน q โดยใช้การกำหนดค่าแบบ non-blocking (<=) ซึ่งเป็นรูปแบบมาตรฐานสำหรับวงจรลำดับ

posedge และ negedge

  • posedge: ทำงานเมื่อขอบสัญญาณขาขึ้น
  • negedge: ทำงานเมื่อขอบสัญญาณขาลง

เลือกใช้ตามลักษณะของวงจรที่ต้องการ

การใช้ always @(posedge clk or negedge rst) (รีเซ็ตแบบอะซิงโครนัส)

ในวงจรซับซ้อน มักต้องใช้รีเซ็ต ตัวอย่างเช่น:

always @(posedge clk or negedge rst) begin
  if (!rst)
    q <= 1'b0;
  else
    q <= d;
end

ด้วยวิธีนี้ หากสัญญาณรีเซ็ตเป็น “0” ค่าของ q จะถูกล้างทันที มิฉะนั้นจะทำงานตามนาฬิกา

การเลือกใช้ระหว่างวงจรผสมและวงจรลำดับ

ประเภทของวงจรคำสั่ง always ที่ใช้คุณสมบัติ
วงจรผสมalways @(*)เอาต์พุตเปลี่ยนตามอินพุตทันที
วงจรลำดับalways @(posedge clk)ทำงานตามจังหวะสัญญาณนาฬิกา

3. ประเภทของการกำหนดค่า (Assignment) ภายใน always

Verilog มี 2 วิธีในการกำหนดค่า

ภายในคำสั่ง always ของ Verilog สามารถใช้ตัวดำเนินการกำหนดค่า 2 แบบ:

  • =: การกำหนดค่าแบบ Blocking (Blocking Assignment)
  • <=: การกำหนดค่าแบบ Non-blocking (Non-blocking Assignment)

หากไม่เข้าใจความแตกต่างนี้ อาจทำให้เกิด การทำงานผิดพลาด หรือ ความไม่ตรงกันระหว่างผลการจำลองกับการสังเคราะห์ ได้ จึงเป็นจุดที่สำคัญมาก

การกำหนดค่าแบบ Blocking (=) คืออะไร?

Blocking Assignment หมายถึง ทำงานทีละคำสั่งตามลำดับ คล้ายกับการทำงานในซอฟต์แวร์

always @(*) begin
  a = b;
  c = a;
end

ในกรณีนี้ a = b จะทำงานก่อน จากนั้นจึงใช้ค่าของ a ไปกำหนด c ดังนั้น ลำดับของโค้ดมีผลโดยตรงต่อผลลัพธ์

การใช้งานที่เหมาะสม

  • ใช้กับวงจรผสม (Combinational Logic)
  • ใช้ในโครงสร้างควบคุม เช่น if, case

การกำหนดค่าแบบ Non-blocking (<=) คืออะไร?

Non-blocking Assignment หมายถึง ทุกคำสั่งจะถูกประเมินพร้อมกัน และค่าจะอัปเดตพร้อมกันเมื่อถึงขอบสัญญาณ เหมาะกับการบรรยายพฤติกรรมแบบขนานของฮาร์ดแวร์

always @(posedge clk) begin
  a <= b;
  c <= a;
end

ในกรณีนี้ a <= b และ c <= a จะถูกประเมินพร้อมกัน และอัปเดตหลังขอบสัญญาณนาฬิกา ทำให้ค่า c ได้ค่าของ a จากรอบก่อนหน้า

การใช้งานที่เหมาะสม

  • ใช้กับวงจรลำดับ (Sequential Logic)
  • ใช้กับรีจิสเตอร์และฟลิปฟลอป

สรุปความแตกต่างระหว่าง Blocking และ Non-blocking

คุณสมบัติBlocking (=)Non-blocking (<=)
ลำดับการทำงานทำงานตามบรรทัดจากบนลงล่างประเมินทั้งหมดพร้อมกัน อัปเดตทีเดียว
การใช้งานหลักวงจรผสมวงจรลำดับ
เวลาที่สะท้อนผลลัพธ์ทันทีหลังขอบสัญญาณนาฬิกา
ข้อผิดพลาดที่พบบ่อยเกิด latch โดยไม่ตั้งใจค่าบางตัวไม่อัปเดต

จะเกิดอะไรขึ้นถ้าใช้ผสมกัน?

การใช้ = และ <= กับสัญญาณเดียวกันภายในบล็อกเดียวถือว่าไม่ควรทำ เพราะอาจทำให้เกิดบั๊กหลังการสังเคราะห์

always @(posedge clk) begin
  a = b;
  a <= c;
end

ในตัวอย่างนี้ สัญญาณ a ถูกกำหนดค่าซ้ำสองครั้ง โดยใช้ทั้ง = และ <= ซึ่งทำให้ผลลัพธ์ไม่แน่นอน

แนวทางการเลือกใช้

  • ใช้ = ในวงจรผสม (always @(*))
  • ใช้ <= ในวงจรลำดับ (always @(posedge clk))

หากปฏิบัติตามกฎนี้ จะสามารถลดข้อผิดพลาดในการออกแบบได้มาก

4. ข้อควรระวังและข้อผิดพลาดที่พบบ่อยเมื่อใช้ always

การเขียน Sensitivity List ผิดพลาด

หากไม่ระบุสัญญาณที่ต้องตรวจจับอย่างถูกต้อง อาจทำให้เกิดบั๊ก

ใน Verilog จำเป็นต้องระบุว่า สัญญาณใดบ้างที่การเปลี่ยนแปลงจะกระตุ้นให้บล็อก always ทำงาน หากเขียนไม่ครบ จะทำให้เอาต์พุตไม่อัปเดตตามต้องการ

always @(a) begin
  if (b)
    y = 1'b1;
  else
    y = 1'b0;
end

ตัวอย่างนี้จะไม่ตอบสนองต่อการเปลี่ยนแปลงของ b ทำให้ ค่า y ไม่อัปเดตแม้ b เปลี่ยน

วิธีแก้: ใช้ @(*)

เพื่อป้องกันการเขียน Sensitivity List ผิดพลาด ควรใช้ @(*) ดังนี้:

always @(*) begin
  if (b)
    y = 1'b1;
  else
    y = 1'b0;
end

@(*) จะดึงสัญญาณที่เกี่ยวข้องทั้งหมดเข้ามาใน Sensitivity List โดยอัตโนมัติ ทำให้โค้ดปลอดภัยและบำรุงรักษาง่ายขึ้น

Latch เกิดขึ้นโดยไม่ตั้งใจ

การเขียน if / case ไม่ครบทุกเงื่อนไขอาจทำให้เกิด Latch

หากในเงื่อนไขบางกรณีไม่ได้มีการกำหนดค่า ตัวสังเคราะห์จะตีความว่าต้อง “จำค่าเดิมไว้” ส่งผลให้เกิด latch โดยไม่ตั้งใจ

always @(*) begin
  if (enable)
    y = d; // หาก enable = 0 ค่า y ไม่ถูกอัปเดต
end

โค้ดนี้ดูเหมือนถูกต้อง แต่เมื่อ enable = 0 ค่า y จะถูกเก็บไว้ ทำให้ latch ถูกสร้างขึ้นอัตโนมัติ

วิธีแก้: กำหนดค่าในทุกเงื่อนไข

always @(*) begin
  if (enable)
    y = d;
  else
    y = 1'b0; // กำหนดค่าให้ y เสมอ
end

การเขียนให้ ทุกเงื่อนไขมีค่า output กำหนดชัดเจน จะป้องกัน latch ที่ไม่ตั้งใจ

การเขียนเงื่อนไขที่ซับซ้อนเกินไป

หากใช้ if หรือ case ที่มีหลายเงื่อนไขและเขียนไม่ครอบคลุม อาจทำให้เกิด ผลลัพธ์ที่ไม่กำหนด (undefined)

ตัวอย่าง: case ไม่มี default

always @(*) begin
  case(sel)
    2'b00: y = a;
    2'b01: y = b;
    2'b10: y = c;
    // 2'b11 ไม่ได้ถูกกำหนด
  endcase
end

หากค่า sel = 2'b11 จะทำให้เอาต์พุตไม่ถูกกำหนด

วิธีแก้: เพิ่ม default

always @(*) begin
  case(sel)
    2'b00: y = a;
    2'b01: y = b;
    2'b10: y = c;
    default: y = 1'b0; // เพิ่มเพื่อความปลอดภัย
  endcase
end

การมี default ทำให้มั่นใจว่า output จะมีค่าที่แน่นอนเสมอ

การควบคุมหลายสัญญาณพร้อมกัน

หากควบคุมหลายสัญญาณในบล็อกเดียว อาจเกิด การพึ่งพาที่ไม่ตั้งใจ หรือโค้ดซับซ้อนเกินไป วิธีที่ดีคือ แยกเป็นหลายบล็อก always ตามหน้าที่

สรุปข้อผิดพลาดที่พบบ่อย

ปัญหาสาเหตุวิธีแก้
เอาต์พุตไม่อัปเดตเขียน Sensitivity List ไม่ครบใช้ @(*)
Latch เกิดขึ้นไม่ได้กำหนดค่าในทุกเงื่อนไขเขียน else หรือ default เสมอ
ผลลัพธ์ไม่แน่นอนcase ไม่ครอบคลุมทุกค่าเพิ่ม default
โค้ดซับซ้อนเกินไปควบคุมหลายสัญญาณในบล็อกเดียวแยก always เป็นหลายบล็อก

5. การขยายคำสั่ง always ใน SystemVerilog

always_comb: สำหรับวงจรผสม

ภาพรวม

always_comb ทำงานคล้ายกับ always @(*) แต่ระบุชัดเจนว่าเป็น วงจรผสม (Combinational Logic)

always_comb begin
  y = a & b;
end

ข้อดีหลัก

  • สร้าง Sensitivity List อัตโนมัติ
  • หากเกิด latch โดยไม่ตั้งใจ เครื่องมือ (tool) จะแจ้งเตือน
  • ป้องกันการชนกับตัวแปรที่เคยนิยามมาก่อน

ตัวอย่างการใช้งาน

// Verilog
always @(*) begin
  y = a | b;
end

// SystemVerilog
always_comb begin
  y = a | b;
end

always_ff: สำหรับวงจรลำดับ (Flip-Flop)

ภาพรวม

always_ff ใช้สำหรับวงจรที่ขับเคลื่อนด้วยนาฬิกา โดยต้องมีเงื่อนไข trigger เช่น posedge clk หรือ negedge rst

always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    q <= 1'b0;
  else
    q <= d;
end

ข้อดีหลัก

  • อนุญาตให้ใช้เฉพาะ <= (non-blocking) หากใช้ = จะเกิด error
  • ตรวจสอบ Sensitivity List ได้อัตโนมัติ
  • โค้ดอ่านง่ายและระบุชัดว่าเป็นวงจรลำดับ

always_latch: สำหรับวงจร Latch

ภาพรวม

always_latch ใช้เมื่อต้องการ เขียน Latch โดยตั้งใจ ซึ่งเป็นวงจรที่ตอบสนองต่อระดับสัญญาณ (level-triggered)

always_latch begin
  if (enable)
    q = d;
end

ข้อควรระวัง

  • หากมีเงื่อนไขที่ไม่กำหนดค่า จะเกิด Latch โดยชัดเจน
  • ควรใช้เฉพาะเมื่อจำเป็นจริง ๆ

การเลือกใช้คำสั่ง SystemVerilog

คำสั่งการใช้งานใน Verilogคุณสมบัติ
always_combวงจรผสมalways @(*)สร้าง Sensitivity List อัตโนมัติ, ตรวจ latch
always_ffFlip-Flopalways @(posedge clk)ปลอดภัยกว่า, บังคับใช้ non-blocking
always_latchLatchalways @(*) (กรณี if/case ไม่ครบ)ใช้เพื่อระบุ Latch โดยตั้งใจ

แนวโน้มการใช้งานในปัจจุบัน

ปัจจุบัน การใช้ SystemVerilog ได้รับความนิยมมากขึ้น เนื่องจากมีข้อดีด้านความปลอดภัยและความชัดเจน เครื่องมือสมัยใหม่ก็รองรับการตรวจสอบโค้ดที่แม่นยำ เช่น always_ff และ always_comb ซึ่งช่วยลดความผิดพลาดในการออกแบบ

โดยเฉพาะในการทำงานเป็นทีม การเลือกใช้คำสั่งเหล่านี้ช่วยให้โค้ดอ่านง่ายและบำรุงรักษาสะดวก

6. คำถามที่พบบ่อย (FAQ) เกี่ยวกับคำสั่ง always

ในส่วนนี้ จะตอบคำถามที่มักพบบ่อยเกี่ยวกับการใช้ always ใน Verilog และ SystemVerilog ซึ่งเหมาะสำหรับทั้งผู้เริ่มต้นและระดับกลาง

Q1. ควรใช้ if หรือ case ภายใน always?

A. ขึ้นอยู่กับความซับซ้อนของเงื่อนไข:

  • หากมีเงื่อนไขเพียง 2–3 แบบ → ใช้ if อ่านง่ายกว่า
  • หากมีหลายสถานะที่ต้องระบุชัดเจน → ใช้ case จะอ่านง่ายและลดความผิดพลาด

Q2. จะเกิดอะไรขึ้นถ้าไม่ใส่ Sensitivity List?

A. หาก Sensitivity List ไม่ครบ การเปลี่ยนแปลงของบางสัญญาณจะไม่กระตุ้น always ทำให้เอาต์พุตไม่อัปเดต

ผลคือ การจำลอง (simulation) และผลลัพธ์จริง (synthesis) อาจไม่ตรงกัน วิธีแก้คือใช้ @(*) หรือ always_comb

Q3. ทำไมบางครั้ง Latch จึงเกิดขึ้นโดยไม่ตั้งใจ?

A. เพราะในโค้ด if หรือ case ไม่ได้กำหนดค่าให้สัญญาณในทุกกรณี ทำให้ตัวสังเคราะห์ต้องใส่วงจรเก็บค่า (Latch)

ตัวอย่าง (ผิด):

always @(*) begin
  if (en)
    y = d; // ถ้า en=0 ค่า y ถูกเก็บไว้
end

วิธีแก้:

always @(*) begin
  if (en)
    y = d;
  else
    y = 1'b0; // กำหนดค่าในทุกกรณี
end

Q4. ใช้ = และ <= ปนกันได้หรือไม่?

A. ไม่ควรใช้ร่วมกันในบล็อกเดียวกัน โดยหลักการ:

  • วงจรผสม → ใช้ = (Blocking)
  • วงจรลำดับ → ใช้ <= (Non-blocking)

หากใช้กับสัญญาณเดียวกัน จะทำให้ผลจำลองไม่ตรงกับฮาร์ดแวร์จริง

Q5. ต่างกันอย่างไรระหว่าง always_ff และ always @(posedge clk)?

A. การทำงานใกล้เคียงกัน แต่ always_ff ช่วยเพิ่มความปลอดภัยและอ่านง่ายกว่า

การเปรียบเทียบalways @(posedge clk)always_ff
การตรวจสอบ Sensitivityผู้เขียนต้องระบุเองเครื่องมือเช็กอัตโนมัติ
การใช้ตัวกำหนดค่าอาจใช้ = ได้ (แม้จะผิด)อนุญาตเฉพาะ <= เท่านั้น
ความอ่านง่ายอาจไม่ชัดเจนว่าเป็นวงจรลำดับเห็นได้ชัดว่าเป็น Flip-Flop

Q6. ควบคุมหลายสัญญาณในบล็อกเดียวได้หรือไม่?

A. ทำได้ แต่ถ้ามีสัญญาณหลายตัวมากเกินไป อาจทำให้โค้ดซับซ้อน ควรแยกเป็นหลาย always เพื่อให้ดูแลง่าย

Q7. ถ้าใช้ <= ในวงจรผสมจะเป็นอย่างไร?

A. ในการจำลองอาจทำงานได้ แต่ เมื่อสังเคราะห์ อาจได้วงจรที่ไม่ตรงตามที่ตั้งใจ ดังนั้นควรใช้ = สำหรับวงจรผสมเสมอ

7. สรุป

always คือโครงสร้างหลักในการออกแบบ Verilog

คำสั่ง always ใน Verilog เป็น เครื่องมือสำคัญในการบรรยายทั้งวงจรผสมและวงจรลำดับ ช่วยให้ออกแบบวงจรที่ซับซ้อนได้อย่างถูกต้องและชัดเจน จึงถือเป็นความรู้พื้นฐานที่จำเป็นตั้งแต่ผู้เริ่มต้นจนถึงมืออาชีพ

ในบทความนี้ ได้อธิบายประเด็นหลักดังนี้:

  • ความแตกต่างและการใช้งานของ always @(*) และ always @(posedge clk)
  • ความต่างระหว่าง = (Blocking) และ <= (Non-blocking)
  • ข้อควรระวังในการเขียน Sensitivity List และการป้องกันการสร้าง Latch โดยไม่ตั้งใจ
  • การใช้คำสั่งใหม่ใน SystemVerilog เช่น always_comb, always_ff, always_latch
  • ตอบคำถามที่พบบ่อยเกี่ยวกับการใช้ always ในการออกแบบจริง

ความถูกต้องของโค้ดคือคุณภาพของวงจร

การเขียนโค้ดใน HDL คือการบรรยายฮาร์ดแวร์ “เขียนอย่างไร วงจรจะถูกสร้างตามนั้น” ดังนั้นความผิดพลาดเล็กน้อย เช่น การใช้ตัวดำเนินการผิดหรือเขียนเงื่อนไขไม่ครบ อาจนำไปสู่ความล้มเหลวของฮาร์ดแวร์จริงได้ การระมัดระวังในการเขียน always จึงสำคัญมาก

ก้าวต่อไป: การออกแบบระดับสูงขึ้น

เมื่อเข้าใจการใช้ always อย่างถูกต้องแล้ว สามารถก้าวไปสู่การออกแบบที่ซับซ้อนยิ่งขึ้น เช่น:

  • Finite State Machine (FSM) สำหรับวงจรที่มีการเปลี่ยนสถานะ
  • Pipeline และการประมวลผลแบบสตรีมมิ่ง
  • การสร้าง IP Core และการใช้งานจริงบน FPGA

นอกจากนี้ หากเรียนรู้เพิ่มเติมใน SystemVerilog และ VHDL จะทำให้สามารถทำงานได้กว้างขึ้นในโครงการระดับมืออาชีพ

ข้อคิดสุดท้ายสำหรับผู้ออกแบบ

การออกแบบวงจรไม่ใช่เพียง “ให้ทำงานได้” แต่ต้อง “ทำงานได้อย่างถูกต้อง มั่นคง และรองรับการขยายในอนาคต
หวังว่าบทความนี้จะช่วยให้ผู้อ่านไม่เพียงเข้าใจการใช้ always เท่านั้น แต่ยังได้รับแนวคิดในการออกแบบที่ปลอดภัยและเชื่อถือได้อีกด้วย