การใช้งานโมดูล MCP4921 SPI DAC (Digital-to-Analog Converter)#

บทความนี้กล่าวถึง ตัวอย่างการใช้งานโมดูล MCP4921 DAC ในเบื้องต้น โดยใช้งานร่วมกับบอร์ดไมโครคอนโทรลเลอร์ และเขียนโปรแกรมด้วย Arduino Sketch เพื่อสร้างสัญญาณแอนะล็อก จำนวน 1 ช่องสัญญาณ

Keywords: MCP4921, Digital-to-Analog Converter (DAC), SPI Bus, Arduino Sketch, Analog Output, Analog Signal Waveform Generation


Microchip MCP492x#

ไอซี MCP4921 / MCP4922 ของบริษัท Microchip เป็นตัวอย่างของไอซีในประเภท DAC (Digital to Analog Converter) และทำหน้าที่แปลงข้อมูลดิจิทัลให้เป็นสัญญาณแอนะล็อก ไอซีดังกล่าวมีเอาต์พุต 1 ช่อง หรือ 2 ช่องสัญญาณตามลำดับ

ไอซี MCP4921 มีขาเพียง 8 ขา ในขณะที่ MCP4922 มี 14 ขา มีวงจร DAC จำนวน 2 ชุด (DAC_A และ DAC_B) และมีขา /SHDN (Shutdown) เพื่อเข้าสู่โหมดการประหยัดพลังงานได้

แต่ในบทความนี้จะขอกล่าวถึงเฉพาะไอซี MCP4921 เท่านั้น ซึ่งมีคุณสมบัติโดยสรุป ดังต่อไปนี้

  • Single DAC, Rail-to-Rail Output
  • DAC Resolution: 12 bits
  • Voltage Supply: 2.7V to 5.5V (Single-Supply)
  • SPI Interface: Mode (0,0) and (1,1)
  • SPI Frequency: 20 MHz (max.)
  • External VREF Input Pin
  • Settling Time: 4.5 µs
  • Selectable Gain (G): 1x or 2x
  • No shutdown pin

รูป: บล็อกไดอะแกรม และขาของไอซี

การส่งข้อมูลไปยังไอซี MCP492x จะใช้วิธีเลื่อนบิตเข้าที่ขา SDI (Serial Data Input) ตามจังหวะของสัญญาณ SCK (Serial Clock) เมื่อลอจิกของสัญญาณ /CS ได้เปลี่ยนจาก 1 เป็น 0

เมื่อได้เลื่อนบิตคำสั่งและข้อมูลทั้งหมด 16 บิต ไปยังรีจิสเตอร์ภายในได้ครบแล้ว จะต้องมีการสร้างสัญญาณพัลส์แบบ Low-Active ที่ขา/LDAC ซึ่งจะต้องมีความกว้างอย่างน้อย 100 ns และเกิดขึ้นหลังจากสัญญาณ /CS ได้เปลี่ยนจาก 0 เป็น 1 แล้วอย่างน้อย 40 ns

ไอซี MCP492x มีขาอินพุต VREF สำหรับการทำงานของวงจร DAC ภายในแต่ละชุด และ VREF จะต้องมีระดับแรงดันคงที่และไม่เกิน VDD แรงดันอ้างอิง VREF สามารถเลือกได้ว่า จะให้ผ่านหรือไม่ผ่านวงจรบัฟเฟอร์ (Buffer) ที่อยู่ภายในไอซี โดยทั่วไปแล้ว ก็ใช้ VREF เท่ากับแรงดันไฟเลี้ยง VDD

วงจร DAC แต่ละชุดภายในไอซี มีวงจรขยายสัญญาณ (OpAmp) ที่สามารถโปรแกรมเลือกอัตราขยายได้ (Programmable Gain) ระหว่าง 1x กับ 2x

รูป: การส่งข้อมูลด้วยบัส SPI ไปยังไอซี MCP492x

รูป: รีจิสเตอร์ Write Command Register และความหมายของบิตที่เกี่ยวของ

 


▷ ตัวอย่างการเขียนโค้ด Arduino สำหรับบอร์ด Arduino Uno R4#

โค้ดตัวอย่างต่อไปนี้ จะใช้กับบอร์ด Arduino Uno R4 Wi-Fi ซึ่งทำงานด้วยแรงดันไฟเลี้ยง +5V และเริ่มต้นการทำงานด้วยฟังก์ชัน setup() มีการเปิดใช้งานวงจรตัวนับ (Hardware Timer) ของไมโครคอนโทรลเลอร์ RA4M1 (32-bit Arm Cortex-M4F, 48MHz) โดยใช้ฟังก์ชันของคลาส FspTimer ซึ่งเป็นส่วนหนึ่งของ "Arduino Core for Renesas" และมีการเปิดใช้งานอินเทอร์รัพท์ของวงจรตัวนับ ด้วยอัตราคงที่ เช่น 50kHz

วงจรตัวนับภายใน RA4M1 มี 2 ประเภท ได้แก่

  • AGT (Asynchronous General Purpose Timers): 16-bit
  • GPT (General Purpose PWM Timers): 16-bit / 32-bit

ในโค้ดตัวอย่างนี้ได้เลือกใช้ GPT Timer

ทุกครั้งที่มีการเกิดอินเทอร์รัพท์จากวงจรตัวนับ จะมีการเรียกใช้ฟังก์ชัน timerCallback(...) และเรียกฟังก์ชัน writeDAC(...) เพื่อส่งบิตคำสั่ง 4 บิต และข้อมูลขนาด 12 บิต รวมเป็น 16 บิต ไปยังไอซี MCP4921 โดยใช้บัส SPI ในโหมด (0,0) และได้ตั้งค่าความถี่ไว้ 5MHz โดยประมาณ

แรงดันไฟเลี้ยงและขาสัญญาณที่ใช้ในการสื่อสารข้อมูลกับไอซี MCP4921 (ใช้แรงดันไฟเลี้ยง +5V) มีดังนี้

  • SCK : D13
  • SDI : D11
  • /CS : D10
  • /LDAC: D9
  • VDD : 5V
  • VREFA: 5V
  • AVSS : GND

ข้อมูลเป็นแบบ 8-bit Unsigned และอ่านค่าตามลำดับจากอาร์เรย์ของค่าคงที่ SAMPLES_TABLE[...] ที่ได้จากการชักตัวอย่าง 256 ตัวเลข (NUM_SAMPLES) ของฟังก์ชันรูปคลื่นไซน์ ในช่วงหนึ่งคาบ และมีค่าอยู่ในช่วง 0..255

การอัปเดตค่าสำหรับ DAC จะเกิดขึ้นด้วยอัตรา 50kHz หรือ 50k Samples/sec ดังนั้นจะได้สัญญาณรูปไซน์ที่มีความถี่เท่ากับ 50kHz / 256 = 195.3Hz

// Arduino Uno R4 WiFi + MCP4921
// Date: 2024-11-14

#include <SPI.h>
#include <FspTimer.h>  // Use the FspTimer class

const int CS_PIN   = 10; // Chip select pin (active-low)
const int LDAC_PIN = 9;  // Load DAC output (active-wlow)

const int NUM_SAMPLES = (1 << 8);
uint16_t SAMPLES_TABLE[ NUM_SAMPLES ];
uint32_t sample_index = 0;

FspTimer timer;

void writeDAC(uint16_t value) {
  // MCP4921 16-bit write command register
  // Bit 15: #A/B  = 0 for DAC A selection 
  // Bit 14: BUF   = 0 for Unbuffered VREF
  // Bit 13: #GA   = 1 for 1x Gain
  // Bit 12: #SHDN = 1 for Enabled (Not Shutdown)
  // Bits 14-12 : Config bits
  // Bits 11-0  : 12-bit data value

  uint16_t cmd_data = (0b0011 << 12) | (value & 0x0FFF);
  // Begin SPI transaction with specified settings 
  SPI.beginTransaction(SPISettings(5000000, MSBFIRST, SPI_MODE0)); 
  digitalWrite( CS_PIN, LOW );  // Select the device
  SPI.transfer16( cmd_data );   // Write command and data
  digitalWrite( CS_PIN, HIGH ); // Deselect the device
  SPI.endTransaction();         // End SPI transaction
  digitalWrite( LDAC_PIN, LOW ); // Latch DAC output register
  digitalWrite( LDAC_PIN, HIGH );
}

void timerCallback(timer_callback_args_t __attribute((unused)) *p_args) {
  writeDAC( SAMPLES_TABLE[sample_index++] ); // Update DAC output
  if (sample_index >= NUM_SAMPLES) sample_index = 0;
}

void timerInit() {
  timer_mode_t timer_mode = TIMER_MODE_PERIODIC;
  uint8_t timer_type = GPT_TIMER;  
  float timer_freq_hz = 50e3f;  // Timer tick rate = 50kHz
  int timer_index = FspTimer::get_available_timer(timer_type);

  if (timer_index < 0) {
      timer_index = FspTimer::get_available_timer(timer_type, true);
  }
  if (timer_index >= 0) {
     timer.begin(timer_mode, timer_type, timer_index, 
                 timer_freq_hz, 0.0, timerCallback, nullptr);    
     timer.setup_overflow_irq();
     timer.open();
     timer.start();
  }
}

void setup() {
   pinMode( CS_PIN, OUTPUT );
   digitalWrite( CS_PIN, HIGH );
   pinMode( LDAC_PIN, OUTPUT );
   digitalWrite( LDAC_PIN, HIGH );
   SPI.begin();
   const uint32_t A = (1 << 11) - 1; // Scale
   for (int i = 0; i < NUM_SAMPLES; i++) {
      SAMPLES_TABLE[i] = (uint16_t)(A*(1+sin(2*PI*i/NUM_SAMPLES)));
   }
   timerInit();
}

void loop() {
}

รูป: ตัวอย่างการต่อวงจรทดลองบนเบรดบอร์ด

รูป: สัญญาณเอาต์พุต (DC Coupling) รูปคลื่นไซน์ที่มีความถี่ 195Hz

รูป: สัญญาณเอาต์พุต (AC Coupling)

รูป: ตัวอย่างการวัดสัญญาณ CH1: /CS และ CH2: SCK

จากรูปคลื่นสัญญาณที่วัดได้ จะเห็นได้ว่า สัญญาณ SCK มีความถี่ 5MHz และช่วงที่เป็นลอจิก 0 สำหรับ /CS มีระยะเวลาประมาณ 7.6us และมีอัตราการอัปเดตเอาต์พุตเท่ากับ 50kHz

 


▷ ตัวอย่างการเขียนโค้ด Arduino สำหรับบอร์ด ESP32#

ในเชิงเปรียบเทียบโค้ดตัวอย่างถัดไป แตกต่างจากโค้ดที่แล้วในบางส่วน เช่น วงจรตัวนับ และวงจรสำหรับบัส SPI และจะใช้สำหรับบอร์ดไมโครคอนโทรลเลอร์ ESP32 ในตัวอย่างนี้ ได้เลือกใช้วงจร VSPI สำหรับเชื่อมต่อกับบัส SPI ดังนั้นจึงใช้ขา GPIO ดังนี้

  • MOSI=23, MISO=19, SCLK=18, CS=5
// WeMos Lolin32 Lite + MCP4921
// Date: 2024-11-14

#include <SPI.h>

#define TIMER_FREQ_HZ   (1e6)   // 1MHz HW timer
#define FS              (50e3)  // 50kHz output update rate

const int CS_PIN   = 5; // Chip select pin (active-low)
const int LDAC_PIN = 2; // Load DAC output (active-low)

const int NUM_SAMPLES = (1 << 8);
uint16_t SAMPLES_TABLE[ NUM_SAMPLES ];
uint32_t sample_index = 0;

hw_timer_t *timer = NULL; // Timer handle

void writeDAC(uint16_t value) {
  // MCP4921 16-bit write command register
  // Bit 15: #A/B  = 0 for DAC A selection 
  // Bit 14: BUF   = 0 for Unbuffered VREF
  // Bit 13: #GA   = 1 for 1x Gain
  // Bit 12: #SHDN = 1 for Enabled (Not Shutdown)
  // Bits 14-12 : Config bits
  // Bits 11-0  : 12-bit data value

  uint16_t cmd_data = (0b0011 << 12) | (value & 0x0FFF);
  // Begin SPI transaction with specified settings 
  SPI.beginTransaction( SPISettings(5000000, MSBFIRST, SPI_MODE0)); 
  digitalWrite( CS_PIN, LOW );   // Select the device
  SPI.transfer16( cmd_data );    // Write command and data
  digitalWrite( CS_PIN, HIGH );  // Deselect the device
  SPI.endTransaction();          // End SPI transaction
  digitalWrite( LDAC_PIN, LOW ); // Latch DAC output register
  digitalWrite( LDAC_PIN, HIGH );
}

// Timer interrupt callback function
void IRAM_ATTR timerCallback() {
  writeDAC( SAMPLES_TABLE[sample_index++] ); // Update DAC output
  if (sample_index >= NUM_SAMPLES) sample_index = 0;
}

// Initialize the timer for periodic sampling
void timerInit() {
  timer = timerBegin( TIMER_FREQ_HZ );
  if (timer == NULL){
    Serial.println("Timer init failed!!");
  }
  timerAlarm( timer, (uint32_t)(TIMER_FREQ_HZ/FS) /*period (ticks)*/, 
              true /*autoreload*/, 0 /*autoreload count*/ );
  timerAttachInterrupt( timer, &timerCallback );
  timerStart( timer );
}

void setup() {
   pinMode( CS_PIN, OUTPUT );
   digitalWrite( CS_PIN, HIGH );    
   pinMode( LDAC_PIN, OUTPUT );
   digitalWrite( LDAC_PIN, HIGH );

   // Uses default VSPI pins: MOSI=23, MISO=19, SCLK=18, and CS=5
   SPI.begin(); // Initialize VSPI 

   const uint32_t A = (1 << 11) - 1; // Scale
   for (int i = 0; i < NUM_SAMPLES; i++) {
      SAMPLES_TABLE[i] = (uint16_t)(A*(1+sin(2*PI*i/NUM_SAMPLES)));
   }
   timerInit();
}

void loop() {
}

รูป: การต่อวงจรทดลองโดยใช้บอร์ด WeMos Lolin32 Lite ร่วมกับไอซี MCP4921

รูป: สัญญาณเอาต์พุตรูปคลื่นไซน์ (วัดความถี่ได้ 188Hz)

อัตราการอัปเดตสำหรับ DAC วัดได้ 48.1kHz ซึ่งต่ำกว่าค่าที่ได้กำหนดไว้ในโค้ด (50kHz) ดังนั้นความถี่ของสัญญาณเอาต์พุตรูปคลื่นไซน์ที่เป็นจริง คำนวณได้จาก 48.1kHz/ 256 = 187.9Hz และวัดได้ประมาณ 188Hz

รูป: การวัดสัญญาณ CH1: /CS และ CH2: SCK

สัญญาณ /CS มีช่วงที่เป็นลอจิก 0 ซึ่งเป็นช่วงที่มีการสื่อสารข้อมูลผ่านบัส SPI มีระยะเวลาประมาณ 4.89us

 


กล่าวสรุป#

บทความนี้นำเสนอตัวอย่างการเขียนโค้ด Arduino สำหรับบอร์ด Arduino Uno R4 และ ESP32 และการต่อวงจรใช้งานร่วมกับไอซี MCP4921 SPI DAC เพื่อสร้างสัญญาณแอนะล็อก (รูปคลื่นไซน์) เป็นเอาต์พุตหนึ่งช่องสัญญาณ

 


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

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