การออกแบบวงจรดิจิทัลสำหรับ FPGA เพื่อใช้งานโมดูล MCP4725 I2C DAC#

Keywords: Digital Logic Design, VHDL, Intel / Altera FPGA, Quartus Prime, MCP4725 DAC, I2C, IP Core

ความรู้และทักษะพื้นฐานที่เกี่ยวข้อง

  • อิเล็กทรอนิกส์ (Electronics): การใช้งาน MCP4725 DAC การสื่อสารผ่านบัส I2C และการศึกษาเอกสาร Datasheet ของไอซี
  • การออกแบบวงจรลอจิก (Logic Design): การออกแบบวงจรดิจิทัลโดยใช้ FSM (Finite-State Machine) การใช้งาน IP Core และการใช้งาน Intel Cyclone IV FPGA
  • การประมวลผลสัญญาณดิจิทัล: การสร้างสัญญาณแบบคาบ (เช่น สัญญาณไซน์ สามเหลี่ยม และสี่เหลี่ยม)
  • การวัด (Measurement): การใช้งานออสซิลโลสโคปแบบพกพา อุปกรณ์ USB / Logic Analyzer และตัวถอดรหัสโปรโตคอล (Protocol Decoder) สำหรับการทำงานของบัส I2C
  • การเขียนโค้ดและการใช้ซอฟต์แวร์ (Software/Coding): การเขียนโค้ดด้วย VHDL การใช้งานซอฟต์แวร์ Intel Quartus Prime (Lite Edition), VS Code IDE และ ADI Scopy

MCP4725: 12-bit I2C DAC#

MCP4725 เป็นไอซีประเภท DAC (Digital to Analog Converter) ของบริษัท Microchip Technology Inc. ใช้สำหรับการแปลงข้อมูลดิจิทัล 12 บิต ให้เป็นสัญญาณแอนะล็อก และใช้บัส I2C ที่มีสัญญาณ SCL และ SDA ในการสื่อสารข้อมูลกับชิปตัวอื่น เช่น ไมโครคอนโทลเลอร์ เป็นต้น สำหรับรายละเอียดเกี่ยวกับ MCP4725 แนะนำให้ศึกษาจากบทความ "การใช้งานโมดูล MCP4725 DAC (Digital-to-Analog Converter)"

ข้อมูลเชิงเทคนิคที่สำคัญของไอซีมีดังนี้

  • แรงดันไฟเลี้ยง (VDD) สำหรับไอซี: 2.7V ~ 5.5V
    • ถ้านำไปใช้ร่วมกับบอร์ด FPGA หรือ ไมโครคอนโทรลเลอร์ เช่น ESP32 ให้ใช้ VDD = 3.3V
  • ข้อมูลมีความละเอียด (Bit Resolution): 12 บิต
  • ช่วงแรงดันเอาต์พุต: 0V ~ VDD (Rail-to-Rail Output)
  • เชื่อมต่อด้วยบัส I2C สำหรับการเขียนโปรแกรมควบคุมการทำงานของไอซี
  • ความเร็วของบัส I2C
    • Standard-speed mode: 100kHz
    • Fast-speed mode: 400kHz
    • High-speed mode: 3.4MHz
  • มีขา A0 (1-Bit Address Pin) สำหรับกำหนดค่าบิตของอุปกรณ์
    • มีหมายเลขอุปกรณ์เลือกได้คือ: 0x60 (A0=0) หรือ 0x61 (A0=1)

การเขียนข้อมูลเพื่อใช้เป็นเอาต์พุตของ MCP4725 มีหลายรูปแบบ คือ

  • เขียนข้อมูลลงใน EEPROM (ข้อมูลไม่สูญหายเมื่อปิดไฟเลี้ยง)
  • เขียนข้อมูลลงในรีจิสเตอร์ของ DAC
  • เขียนข้อมูลลงใน EEPROM และ DAC Register

รูป: ข้อมูลบางส่วนจาก Datasheet ของไอซี MCP4725

ในบทความนี้ วงจรดิจิทัลใน FPGA ทำหน้าที่เป็น I2C Master และ MCP4725 ทำหน้าที่เป็น I2C Slave

การส่งข้อมูลไปยัง MCP4725 จะใช้วิธีเขียนข้อมูลแบบ Fast Mode Write Command ลงในรีจิสเตอร์ของ DAC เท่านั้น ซึ่งมีหลักการทำงานดังนี้

  • การส่งข้อมูลด้วยบัส I2C จะต้องเริ่มต้นด้วยการส่งสัญญาณ I2C START Condition แล้วตามข้อมูลไบต์ แล้วจบด้วย I2C STOP Condition
  • การส่งข้อมูลไปยัง MCP4725 ในตัวอย่างนี้ เป็นการเขียนข้อมูลเพียงอย่างเดียว Write Operation ไม่มีการอ่านข้อมูล (Read Operation)
  • ไบต์แรกซึ่งเรียกว่า Address Byte มีแอดเดรสของอุปกรณ์ขนาด 7 บิต (0x60) และบิต R/W=0 ซึ่งบ่งบอกว่า จะเขียนข้อมูล ดังนั้นไบต์แรกคือ 1100_0000 (bin) ซึ่งมีบิต A2 A1 A0 = 0 0 0
  • ไบต์ที่สองประกอบด้วย 4 บิต 0000 (C2 C1 PD1 PD0 ตามลำดับ) สำหรับคำสั่ง และ 4 บิต (D11 .. D8) ของข้อมูล 12 บิต เพื่อใช้เป็นเอาต์พุตของ DAC
  • ไบต์ที่สามคือ 8 บิต (D7 .. D0) ที่เหลือของข้อมูล 12 บิต
  • แต่ถ้าต้องการส่งข้อมูลต่อเนื่องครั้งละ 2 ไบต์ (ไม่ต้องส่งไบต์แอดเดรส) ไปยัง DAC ก็ทำได้ โดยยังไม่ต้องส่ง STOP Condition

รูป: ลำดับการส่งข้อมูลต่อเนื่องไปยัง MCP4725 ในโหมด Fast Mode Write Command

 


I2C Master (VHDL)#

โดยทั่วไปแล้ว โมดูล MCP4725 นิยมใช้กับไมโครคอนโทรลเลอร์ และในกรณีที่เขียนโค้ดด้วย Arduino ก็มีไลบรารี Wire และไลบรารีสำหรับ MCP4725 เพื่อความสะดวกและง่ายต่อการเขียนโค้ด เช่น

บทความนี้นำเสนอตัวอย่างการทดลองใช้งานชิป MCP4725 (ใช้โมดูล GY-4725) ร่วมกับบอร์ด FPGA และสาธิตการเขียนโค้ด VHDL เพื่อออกแบบวงจรดิจิทัล โดยเลือกใช้งาน IP Core ที่ทำหน้าที่เป็น I2C Master สำหรับการส่งข้อมูลผ่านบัส I2C ไปยังไอซี MCP4725

รูป: การเชื่อมต่อระหว่าง I2C Master IP Core กับวงจรของผู้ใช้ (User Logic)

รูป: ขา I/O ของ I2C Master IP Core

รูป: VHDL Entity ของ I2C Master IP Core

สัญญาณ ena เป็นสัญญาณอินพุตควบคุม ถ้าเป็นลอจิก 1 จะทำให้ I2C Master เริ่มทำงาน โดยอ่านค่า 7 บิตจากสัญญาณอินพุต addr ซึ่งจะมีค่า 0x60 สำหรับอุปกรณ์ MCP4725 และจะต้องกำหนดค่าลอจิกให้สัญญาณอินพุต rw ซึ่งเป็นบิตสำหรับเลือกว่า จะเขียนหรือจะอ่านข้อมูล

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

เมื่อเริ่มต้นการทำงานหลังจากกำหนดให้ ena เป็นลอจิก 1 แล้ว สัญญาณเอาต์พุต busy จะเปลี่ยนเป็นลอจิก 1 จากนั้นจึงมีการส่งสัญญาณที่เรียกว่า START Condition ตามด้วยไบต์แรก (หรือ Address Byte)

เมื่อได้รับบิตที่เรียกว่า ACK (หรือ Acknowledge Bit) หลังจากส่งไบต์แรกแล้ว ก็จะส่งไบต์ที่สองถัดไป แต่ถ้าสัญญาณ ack_error เป็นลอจิก 1 แสดงว่า ไม่ได้รับสัญญาณบิต ACK จากไอซี MCP4725 มีความผิดพลาดเกิดขึ้น ให้จบการเขียนข้อมูลไปยังไอซี

เมื่อได้รับบิตที่เรียกว่า ACK (ค่าของ ack_error เป็นลอจิก 0) หลังจากส่งไบต์ที่สองแล้ว ก็จะส่งไบต์ที่สามถัดไป ดังนั้นจึงต้องอัปเดตค่าให้กับสัญญาณอินพุต data_wr เพื่อใช้เป็นข้อมูลไบต์ถัดไป แล้วรอตรวจสอบสัญญาณ ack_error เพื่อดูว่า ได้รับบิต ACK หรือไม่

เมื่อส่งข้อมูลครบสามไบต์ เพื่ออัปเดตค่าให้เอาต์พุตของ MCP4725 DAC ในหนึ่งรอบ ก็ให้สัญญาณ ena เป็น 0 เพื่อจบการส่งข้อมูลไปยังไอซี ซึ่งจะจบด้วยการสร้างสัญญาณที่เรียกว่า STOP Condition

 


ตัวอย่างการเขียนโค้ด VHDL สาธิตการใช้งาน I2C Master สำหรับ MCP4725#

โค้ด VHDL ต่อไปนี้สาธิตการใช้งาน I2C Master IP Core โดยสร้างวงจรในส่วน User Logic เพื่อเขียนข้อมูลไปยังไอซี MCP4725 ความถี่ของสัญญาณ Clock ของบอร์ด FPGA ที่ใช้คือ 50MHz และความถี่ของบัส I2C ก็ถูกกำหนดด้วยค่าคงที่ I2C_CLK_SPEED เช่น ลองเลือกค่าต่อไปนี้ในการทดลอง

  • 100_000 (100kHz)
  • 400_000 (400kHz)
  • 1_000_000 (1MHz)

ข้อมูลที่จะนำไปใช้สำหรับ DAC Output ได้จากข้อมูลถูกคำนวณและเก็บเอาไว้ในตาราง หรือ อาร์เรย์ที่มีชื่อว่า sample_table มีจำนวนข้อมูล 2^M = 256 (M=8) และใช้ sample_index เป็นตัวเลือกข้อมูลในอาร์เรย์ดังกล่าว sample_index จะมีค่าเพิ่มขึ้นทีละหนึ่ง อยู่ในช่วง 0 ถึง 255 และวนซ้ำใหม่

File: mcp4725_dac.vhd

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;

entity mcp4725_dac is
  port (
    NRESET  : in std_logic;
    CLK     : in std_logic;
    I2C_SDA : inout std_logic;
    I2C_SCL : inout std_logic;
    STATUS  : out std_logic
  );
end mcp4725_dac;

architecture behavior of mcp4725_dac is
  -- 7-bit I2C device address for MCP4725 (0x60)
  constant I2C_ADDR      : std_logic_vector(6 downto 0) := "1100000";
  constant I2C_CLK_SPEED : integer := 1_000_000; -- I2C speed

  constant BW        : integer := 12; -- 12 bits for DAC output
  constant M         : integer := 8;
  constant MAX_INDEX : integer := 2**M - 1;

  subtype sample_t is std_logic_vector((BW - 1) downto 0);
  type sample_table_t is array(0 to MAX_INDEX) of sample_t;

  function init_table return sample_table_t is
    variable LUT : sample_table_t;
    variable x   : REAL;
  begin
    for i in 0 to MAX_INDEX loop
      -- staircase function (16 levels)
      LUT(i) := std_logic_vector(to_unsigned(i mod 16, 4)) & x"ff";
    end loop;
    return LUT;
  end function;

  signal sample_table : sample_table_t := init_table;
  signal sample_index : integer range 0 to MAX_INDEX := 0;

  type state_type is (ST_IDLE, ST_START, ST_WR_1, ST_WR_2, ST_STOP);
  signal state : state_type := ST_IDLE;

  signal busy        : std_logic;
  signal ack_error   : std_logic;
  signal ena         : std_logic := '0';
  signal rw          : std_logic := '0';
  signal data_wr     : std_logic_vector( 7 downto 0);
  signal data_buffer : std_logic_vector(15 downto 0);
  signal busy_prev   : std_logic_vector( 1 downto 0);
  signal wait_cnt    : integer := 0;

begin

  -- I2C Master Instantiation
  i2c_master_inst : entity work.i2c_master
    generic map (
      input_clk => 50_000_000, -- Assume system clock of 50 MHz
      bus_clk   => I2C_CLK_SPEED
    )
    port map (
      clk       => CLK,
      reset_n   => NRESET,
      ena       => ena,
      addr      => I2C_ADDR,
      rw        => rw,
      data_wr   => data_wr,
      busy      => busy,
      ack_error => ack_error,
      sda       => I2C_SDA,
      scl       => I2C_SCL
    );

  STATUS <= busy; -- Use the STATUS output to monitor the busy signal

  process (NRESET, CLK)
  begin
    if NRESET = '0' then
      state        <= ST_IDLE;
      ena          <= '0';
      rw           <= '0';
      busy_prev    <= (others => '0');
      sample_index <= 0;
      wait_cnt     <= 10000;

    elsif rising_edge(CLK) then

      busy_prev <= busy_prev(0) & busy;

      case state is
        when ST_IDLE =>
          -- Prepare the data (Fast Write Mode)
          data_buffer <= "0000" & sample_table(sample_index);
          state       <= ST_START;
          ena         <= '0';

        when ST_START =>
          -- Set up for I2C transmission  
          ena     <= '1'; -- Start I2C transaction
          rw      <= '0'; -- write operation  
          data_wr <= data_buffer(15 downto 8); -- the high byte 
          state   <= ST_WR_1;

        when ST_WR_1 =>
          if I2C_SCL = '0' and busy = '1' then
            data_wr <= data_buffer(7 downto 0); -- the low byte         
          end if;
          if busy_prev = "10" then -- busy goes low
            if ack_error = '0' then -- ACK
              state <= ST_WR_2;
            else -- No ACK
              ena   <= '0';
              state <= ST_STOP;
            end if;
          end if;

        when ST_WR_2 =>
          if busy_prev = "01" then -- busy goes high
            ena <= '0';
          elsif busy_prev = "10" then -- busy goes low
            if ack_error = '0' then -- ACK
              if sample_index = MAX_INDEX then
                sample_index <= 0;
              else
                sample_index <= sample_index + 1;
              end if;
              state <= ST_IDLE;
            else -- No ACK
              state <= ST_STOP;
            end if;
          end if;

        when ST_STOP =>
          if wait_cnt = 0 then
            wait_cnt     <= 10000;
            ena          <= '0';
            state        <= ST_IDLE;
            sample_index <= 0;
          else
            wait_cnt <= wait_cnt - 1;
          end if;

        when others =>
          state <= ST_IDLE;

      end case;
    end if;
  end process;

end behavior;

 


การทดสอบการทำงานของวงจรโดยใช้บอร์ด FPGA#

โค้ดตัวอย่าง VHDL ได้ถูกนำไปทดลองใช้กับอร์ด FPGA เลือกใช้ชิป Intel / Altera Cyclone IV EP4CE6E22C8 และคอมไพล์โค้ดด้วยซอฟต์แวร์ Quartus Prime Lite Edition v22.1

ตัวอย่างไฟล์ Tcl Script เพื่อกำหนดตำแหน่งและระดับแรงดันไฟฟ้าของขาสัญญาณ มีดังนี้ (หากใช้บอร์ดอื่น ก็จะต้องตั้งค่าให้ถูกต้องตามบอร์ด FPGA ได้ที่เลือกใช้งาน)

# Pin & Location Assignments
# set_global_assignment -name FAMILY "Cyclone IV E"
# set_global_assignment -name DEVICE EP4CE6E22C8

set_location_assignment PIN_86 -to NRESET
set_location_assignment PIN_23 -to CLK
set_location_assignment PIN_50 -to STATUS
set_location_assignment PIN_51 -to I2C_SDA
set_location_assignment PIN_52 -to I2C_SCL

set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to NRESET
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to CLK
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to STATUS
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to I2C_SDA
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to I2C_SCL

 

ถ้าต้องการสร้างรูปคลื่นสัญญาณไซน์ (Sinusoidal Waveform) สามารถลองใช้โค้ดต่อไปนี้ สำหรับฟังก์ชัน init_table เพื่อกำหนดค่าเริ่มต้นให้อาร์เรย์ sample_table

  function init_table return sample_table_t is
    variable LUT : sample_table_t;
    variable x   : REAL;
  begin
    for i in 0 to MAX_INDEX loop
      -- sinusoidal waveform (with DC offset)
      x      := (1.0 + SIN(2.0 * MATH_PI * real(i) / real(2**M)))/2.0;
      LUT(i) := std_logic_vector(to_unsigned(integer(x*real(2**BW-1)),BW));
    end loop;
    return LUT;
  end function;

ถ้าต้องการสร้างรูปคลื่นสัญญาณสามเหลี่ยม (Triangle Waveform) ก็สามารถลองใช้โค้ดต่อไปนี้

  function init_table return sample_table_t is
    variable LUT : sample_table_t;
    variable x   : REAL;
  begin
    for i in 0 to MAX_INDEX loop
      -- triangle waveform (with DC offset)
      x      := 1.0 - real(2*abs(MAX_INDEX/2 - i)) / real(2**M);
      LUT(i) := std_logic_vector(to_unsigned(integer(x*real(2**BW-1)),BW));
    end loop;
    return LUT;
  end function;

 

รูป: บอร์ด Cyclone IV FPGA ที่ได้นำมาทดลอง

ตัวอย่างการวัดสัญญาณเอาต์พุตของ MCP4725 มีดังนี้

รูป: สัญญาณตามฟังก์ชันขั้นบันได (Staircase Waveform)

รูป: สัญญาณคลื่นสามเหลี่ยม (Triangle Waveform)

รูป: สัญญาณตามฟังก์ชันไซน์ (Sinusoidal Waveform)

ถัดไปเป็นตัวอย่างการวิเคราะห์สัญญาณของบัส I2C (SCL และ SDA) และสัญญาณเอาต์พุต STATUS โดยใช้อุปกรณ์ USB Logic Analyzer

รูป: ตัวอย่างรูปคลื่นสัญญาณ เมื่อเลือกใช้ฟังก์ชันขั้นบันได (Staircase Waveform)

ลำดับข้อมูลสำหรับเอาต์พุต (ไม่รวมไบต์แอดเดรส) จากข้อมูลตัวอย่าง มีดังนี้ 00_FF, 01_FF, 02_FF ... 0F_FF

 


กล่าวสรุป#

บทความนี้ได้นำเสนอตัวอย่างการออกแบบวงจรดิจิทัลสำหรับ FPGA เพื่อทดลองใช้งานโมดูล MCP4725 DAC (12-bit Analog Resolution) เชื่อมต่อด้วยบัส I2C และสร้างสัญญาณเอาต์พุต-แอนะล็อก 1 ช่อง และได้สาธิตการใช้งาน I2C Master IP Core ที่ได้มีการเผยแพร่อยู่ในอินเทอร์เน็ต และได้เลือกมาทดลองใช้งานเป็นตัวอย่าง

การสื่อสารข้อมูลด้วยบัส I2C เพื่อสร้างสัญญาณรายคาบ โดยใช้ไอซี MCP4725 DAC มีข้อจำกัดคือ ความเร็วในการส่งข้อมูล ดังนั้นความถี่ของสัญญาณเอาต์พุตที่ได้จึงไม่สูงมาก ในเชิงเปรียบเทียบ ไอซีประเภท SPI DAC จะใช้ความถี่ได้สูงกว่า I2C DAC ในการส่งข้อมูลบิต

บทความที่เกี่ยวข้อง

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2024-10-13 | Last Updated: 2024-11-13