Require a 16 bit 40 channel dac setup for a project

My requirement is to control a 40 channel output 16-bit DAC setup over modbus. I have the AD-5370 and I simply cannot figure out how to control it in standalone mode due to next-to-nothing documentation available.. is there any way I could achieve this using individual or multiple multi-channel DAC modules with an arduino controller to achieve my requirement?
The channels are required to output amplitude in the +10V to -10V range each and a frequency of 1KHz.

Any pointers or suggestions are welcome. Thanks.

I haven't had any direct experience in such kind of connections with Arduino, except at work where I managed some projects using Modbus to communicate with sensors for a couple of projects, but using Windows and Linux.

But the first thing coming up after reading your post is about throughput: 40 channels, 1 kHz on 16 bit means at least 40x1000x16 = 640kbit/s plus modbus communincaton overhead, giving a minimum 1Mbit/s requirement for the whole channel (and we here are excluding each device latencies and collisons).
AFIK, most Modbus/RTU over RS485 devices run at 9600 bit/s but even if they can often run much faster, 115.2 kbit/s is often the top speed, I have never seen higher Modbus speeds.
Are you sure you can manage all those DACs using Modbus? And, assuming it's possible, are you sure the packets will flow constantly enough to get a correct analog output waveform?

I try to understand your post. Please clarify your requirements and describe, what you want to do. You are talking about modbus, but modbus has nothing to do with this DAC.

https://www.analog.com/media/en/technical-documentation/data-sheets/AD5370.pdf

You're abolutely right! I focused on the Modbus problem assuming that this is its requirement. But I even see that such DAC device does not even have any a board and/or library for Arduino, so I see it pretty hard to be solved here.

The data sheet explains everything. What else do you need to know?
Plus two application notes

Apologies on not being clear. The setup is such that a GUI running on a RPi5 will send modbus packets to an arduino(such as a portenta or a pico with an ethernet module). The arduino will be controlling the 40 analog outputs.
The settling time for each channel is 5us or lesser and will control voltage range from 0v to 5v with a 16bit resolution.
In this scenario, can I use any individual DAC SPI modules with a multiplexer to achieve my purpose?

I was referring to the arduino library availability to get it up and running with ease.

So the setup as mentioned above is over Modbus TCP.. and the output is not a sinusoidal wave, rather a square wave where the voltages will be adjusted between 0v to 5v according to the individual channel output requirements.

I don't use them. Don't know if one exists. Have you done a google search.

Sure. 40 single channels will need 40 control signals, so you could multiplex them.

However, not sure why you are abandoning the AD5370, the datasheet seems perfectly adequate.

That could change the rules, but I still haven't understood if the modbus connection is a requirement, and to connect what with what.

Modbus TCP could overcome speed limitation, provided that all devices use this method and there is no converter that brings Modbus/TCP to a normal RS485 physical bus (in this case the total throughput corresponds to the minimum speed).

So the scenario (and our knowledge of your goals) is quite evolving over time now, and it and brings up some additional questions.

Such as you're now talking about a RPi sending modbus packets to Arduino, so this is required for Arduino as an input (not an output), right?
And in concrete terms, what kind of connection you're planning to use between Arduino and the DAC? And what kind of data you plan to be sent from RPi to Arduino? And since we know nothing about what this is supposed to control or how it is supposed to be managed, if the analog output is just a square wave ranging from low to up level, is it really necessary to dynamically and continuously send the level of each channel at 1 kHz rate, instead of sending just level change events?

So, if Modbus/TCP is only used to make RPi communicate with Arduino, throughput may not be an issue (currently I have never done Modbus/TCP communication with Arduino so I can't tell you more at least for this aspect), but before even evaluating the processing speed of Arduino, it would still be necessary to evaluate the communication between Arduino and the DAC chip (I have not looked into the DAC datasheet to understand how it communicates).

Since for Arduino it seems that there are no libraries to manage that DAC (at least I haven't found any), perhaps by giving us more information about the project as a whole, we could try alternatives. Unless all of this (the use of Modbus, that an RPi sends data at 1 kHz, that Arduino manages the DAC, for example) is an already established requirement and therefore not modifiable, in which case I'm sorry, I really don't know what else to recommend.

I am using this code which is a slightly tweaked version of GitHub - vexuk1971/AD537x-Arduino-Uno-R3-Control: Control an AD537x Eval Board with an Arduino - only Uno R3 supported yet

I am not able to understand the parameters that need to typed in serial monitor to view an output. BY default when I am measuring the voltage from channel 1, I get 0.004v, when I type in C I get -0.007V. when I type in DW, I see 6.68V... My connections are as per the suggestions mentioned in the code from the above link.. I use a 12v Vdd and a -12v Vss and a 5v Vcc from the Uno..

#include "SPI.h"

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
// Control of the AD5370 EVAL board using arduino uno R3/Mega
//
// AD5370 datasheet:
//  https://www.analog.com/media/en/technical-documentation/data-sheets/AD5370.pdf
//
// EVAL-AD5370 board datasheet:
//  https://www.analog.com/media/en/technical-documentation/evaluation-documentation/EVAL-AD5370_5372_5373EB.pdf
//
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

#define MY_DEBUG_VERSION

//===============================
// Pin assignments
//===============================

// SPI communication with EVAL-AD5370
// The Arduino Uno pins for the hardware SPI port are as follows:  
//   10 - SS // slave select - unused here
//   11 - MOSI/SDI
//   12 - MISO/SDO // read data from device - unused  here 
//   13 - SCLK

//--------------
// Control pins
//--------------

constexpr uint8_t resetPin = 4; // RESET
constexpr uint8_t busyPin  = 5; // BUSY, unused here   
constexpr uint8_t clrPin   = 6; // CLR
constexpr uint8_t ldacPin  = 7; // LDAC, load DAC
constexpr uint8_t csPin    = 8; // CS/SYNC

//===============================
// Defines
//===============================

#define SPI_FREQ 1000000UL
#define SPI_BIT_ORDER MSBFIRST
#define SPI_MODE SPI_MODE1

// TODO: check where XREGMIN value came from
#define XREGMIN 21844
#define XREGMAX 0xFFFF

// for default values of offset and gain, see AD5370 datasheet, page 15, table 7, registers M and C
#define DEFAULT_GAIN   0x3FFF
#define DEFAULT_OFFSET 0x2000

#define CHANMAX       40            // AD5370 - 40 Channels max.
     
// AD5370 Register modes, see datasheet, page 21, table 9
#define MXREG         0xc0          //b11000000
#define MOFFSET       128           //b10000000
#define MGAIN         64            //b01000000

//===============================
// Variables
//===============================

enum channelMode_t{MCHAN, MALL}; // MCHAN -> modify a specified channel; MALL -> modify all channels
enum registerMode_t{XREG, OFFSET, GAIN}; // XREG -> set current X register; OFFSET -> set OFFSET register; GAIN -> set GAIN register

bool clrPinIsLow;

uint16_t xreg[CHANMAX];   // X reg Buffer Array
uint16_t offset[CHANMAX]; // Offset Buffer Array
uint16_t gain[CHANMAX];   // Gain Buffer Array

//===============================
// Utils
//===============================

void resetBufferArrays()
{
  for(int i=0;i<CHANMAX;i++) 
  {
    xreg[i]   = XREGMIN;
    offset[i] = DEFAULT_OFFSET;
    gain[i]   = DEFAULT_GAIN;
  }
}

//===================================================
// AD5370 control pins Reset/Clear/Load management
//===================================================

void initDacControlPins()
{
  // initialize all control pins to the inactive state

  pinMode(csPin, OUTPUT);
  digitalWrite(csPin, HIGH); // CS pin, active LOW, this pin is managed by the SPI low level routines

  pinMode(resetPin, OUTPUT);
  digitalWrite(resetPin, HIGH); // RESET pin, active LOW

  pinMode(clrPin, OUTPUT);
  digitalWrite(clrPin, HIGH); // CLR pin, active LOW

  pinMode(ldacPin, OUTPUT);  
  digitalWrite(ldacPin, HIGH); // LDAC pin, active LOW  
}

//-----------------------------------------------------------------
// RESET pin
//
// For pin operations, see the datasheet, page 19, RESET FUNCTION
//
// Timing:
// Reset pulse LOW minimum time to reset the DAC is 30nS
// The time taken by the device to reset is 300 uS
//-----------------------------------------------------------------

void resetDac() 
{
#ifdef MY_DEBUG_VERSION  
  Serial.println("Pulsing RESET");
#endif  

  digitalWrite(resetPin, LOW);
  delayMicroseconds(10); // maybe not required, minimum duration is 30nS
  digitalWrite(resetPin, HIGH);
  delayMicroseconds(1000); // Reset Time after rising edge of Reset Pin, the minimum is 300 uS
}

//-------------------------------------------------------------------
// CLR pin
//
// From the AD5730 datasheet, page 19, CLEAR FUNCTION
//
// CLR is an active low input that should be high for normal
// operation. The CLR pin has in internal 500 kΩ pull-down
// resistor. When CLR is low, the input to each of the DAC output
// buffer stages, VOUT0 to VOUT39, is switched to the externally
// set potential on the relevant SIGGND pin. While CLR is low, all
// LDAC pulses are ignored. When CLR is taken high again, the
// DAC outputs remain cleared until LDAC is taken low. The contents
// of the input registers and DAC registers are not affected by taking
// CLR low. To prevent glitches from appearing on the outputs, CLR
// should be brought low by writing to the offset DAC whenever
// the output span is adjusted.
//--------------------------------------------------------------------

void setClearLow()
{
#ifdef MY_DEBUG_VERSION  
  Serial.println("CLR pin LOW");
#endif  

  clrPinIsLow = true;
  digitalWrite(clrPin, LOW);
}

void setClearHigh()
{
#ifdef MY_DEBUG_VERSION  
  Serial.println("CLR pin HIGH");
#endif  

  clrPinIsLow = false;
  digitalWrite(clrPin, HIGH);
}

void toggleClear()
{
  if (clrPinIsLow)
    setClearHigh();
  else
    setClearLow();  
}

//------------------------------------------------------------
// LDAC pin
//
// See the AD5370 datasheet, page 19, BUSY AND LDAC FUNCTIONS
//------------------------------------------------------------

void loadDac() 
{
#ifdef MY_DEBUG_VERSION  
  Serial.println("Pulsing LDAC");
#endif  

  digitalWrite(ldacPin, LOW);
  delayMicroseconds(10); // TODO: check if that delay is required
  digitalWrite(ldacPin, HIGH);
  delayMicroseconds(10); // TODO: check if that delay is required
}

//=========================
// SPI low level I/O
//=========================

void spiWrite(uint8_t *writeList, size_t writeListLen)
{
  // for the SPI basics, see
  // https://docs.arduino.cc/learn/communication/spi/  

  // start the SPI transaction
  SPI.beginTransaction(SPISettings(SPI_FREQ, SPI_BIT_ORDER, SPI_MODE));

  // select the SPI device
  digitalWrite(csPin, LOW);

  // TODO: check if a delay is needed here 

  // start transferring (or requesting for) the bytes;  
  // response will overwrite writeList, not used here (MISO pin unconnected)
  SPI.transfer(writeList, writeListLen);

  // TODO: check if a delay is needed here 

  // Deselect the SPI device
  digitalWrite(csPin, HIGH);

  // End the transaction
  SPI.endTransaction();
}

// write a special function command to the dac and call loadDac
// for special functions codes and operations, see datasheet, page 23
void writeFunction(uint8_t function, uint16_t value)
{
  // Write special function
  // function: 6 bit function code, the byte's upper 2 bits must be 0
  // value: Value to write. 0-65535

#ifdef MY_DEBUG_VERSION
  Serial.println("Write special function:");
  Serial.print("Function 0x");
  Serial.println(function, HEX);
  Serial.print("Value 0x");
  Serial.println(value, HEX);
#endif
  
  uint8_t x = B00000000;
  uint8_t a = function;
  
  uint8_t writeList[3];
  writeList[0] = x + a;
  writeList[1] = uint8_t(value>> 8);
  writeList[2] = value & 0xff;

  spiWrite(writeList, sizeof(writeList));
  loadDac();
}

// send a 24 bit value; does not call loadDac -> only set register(s) without modifying DAC outputs
void writeValueInt(registerMode_t reg, channelMode_t cmod, uint8_t channel, uint16_t value)
{ 
  // Write XREG/OFFSET/GAIN value to specific output (cmod == MCHAN) or to all channels (cmod == MALL)  
  // if cmod == MALL, the channel parameter is ignored

#ifdef MY_DEBUG_VERSION
  if(reg==XREG) Serial.println("XREG:");
  else if(reg==OFFSET) Serial.println("OFFSET:");
  else if(reg==GAIN) Serial.println("GAIN:");

  if(cmod==MALL)
  {
    Serial.print("Write to all channels: ");
  } 
  
  else 
  {
    Serial.print("Write to channel #");
    Serial.print(channel);
    Serial.print(": ");
  }

  Serial.println(value, DEC);
#endif

  uint8_t x=0; // mode bits
  uint8_t a=0; // address bits; 0 -> all channels; 8+channel number -> specific channel
                                                                    
  switch(reg)
  {
    case XREG:    x=MXREG; break;    //192  - Dac X1A or X1B Register select
    case OFFSET:  x=MOFFSET; break;  //128  - Offset register select
    case GAIN:    x=MGAIN; break;    //64   - Gain Register select
    default:      x=MXREG;           // default is Dac X Register selected
  }

  if (channel >= CHANMAX)
  {
    Serial.println("Error: writeValueInt: channel number out of range");
    return;
  }
  
  if (cmod == MCHAN)
  {
    a=channel+8; // Mode MCHAN -> a specific channel is selected - add group offset and channel number
  } 
      
  uint8_t writeList[3];

  writeList[0] = x + a;               //summary Dac Register + Channel, a is 0 for mode MALL 
  writeList[1] = uint8_t(value >> 8); //value high byte
  writeList[2] = value & 0xff;        //value low byte

  spiWrite(writeList, sizeof(writeList));
}

//==========================================
// XREG/GAIN/OFFSET register groups writing 
//==========================================

// write the same value in all channels from root to target
// if root or target >= CHANMAX, write all channels
void initRegister(registerMode_t reg, uint8_t root, uint8_t target, uint16_t value )
{
  uint16_t *pArray;  
  channelMode_t mode=MALL;

  if((root < CHANMAX) && (target < CHANMAX)) 
  {
    mode=MCHAN; 

    if (root > target)
    {
      // swaps root and target, root should be always <= target
      uint8_t temp = root;
      root = target;
      target = temp;
    }
  }  

#ifdef MY_DEBUG_VERSION
  if(reg==XREG) Serial.println("XREG:");
  else if(reg==OFFSET) Serial.println("OFFSET:");
  else if(reg==GAIN) Serial.println("GAIN:");

  if(mode==MALL)
  {
    Serial.print("Init all channels to ");
  } 
  
  else 
  {
    Serial.print("Init channels from #");
    Serial.print(root);
    Serial.print(" to #");
    Serial.print(target);    
    Serial.print(" to ");
  }

  Serial.println(value, DEC);
#endif

  switch(reg)
  {
    case XREG:    pArray=xreg; break;                                      
    case OFFSET:  pArray=offset; break;                                   
    case GAIN:    pArray=gain; break;
    default:      pArray=xreg;                                           
  }

  if (mode == MALL)
  {
    writeValueInt(reg, MALL, 0, value); //simply write one time only a value to all channels/groups

    for(uint8_t i=0; i<CHANMAX; i++) 
      pArray[i]=value;    //update complete register Array
  }

  else if (mode == MCHAN)
  {
    for(uint8_t i=root; i<=target; i++)
    {
      pArray[i]=value;                     //update register Array from root to target channel
      writeValueInt(reg, MCHAN, i, value); //write value from root to target channel in desired Register
    }
  }

  // required, writeValueInt does not call loadDac()
  loadDac();
}

// copy the values of a buffer array to all corresponding channels from root to target
// if root or target >= CHANMAX, write all channels
void writeFromArray(registerMode_t reg, uint8_t root, uint8_t target)
{      
  uint16_t *pArray;
  uint16_t value=0;
  channelMode_t mode=MALL;

  if((root < CHANMAX) && (target < CHANMAX)) 
  {
    mode=MCHAN; 

    if (root > target)
    {
      uint8_t temp = root;
      root = target;
      target = temp;
    }
  }  

#ifdef MY_DEBUG_VERSION
  if(reg==XREG) Serial.println("XREG:");
  else if(reg==OFFSET) Serial.println("OFFSET:");
  else if(reg==GAIN) Serial.println("GAIN:");

  if(mode==MALL)
  {
    Serial.println("Writing buffer array to all channels");
  } 
  
  else 
  {
    Serial.print("Writing buffer array values to channels from #");
    Serial.print(root);
    Serial.print(" to #");
    Serial.println(target);    
  }

#endif

  switch(reg)
  {
    case XREG:    pArray=xreg; break;                                      
    case OFFSET:  pArray=offset; break;                                   
    case GAIN:    pArray=gain; break;
    default:      pArray=xreg;                                              
  }

  if (mode==MALL)
  {
    for(uint8_t i=0; i<CHANMAX; i++)
    {
      value=pArray[i]; 
      writeValueInt(reg, MCHAN, i, value); 
    }
  }

  else if (mode==MCHAN)
  {
    for(uint8_t i=root; i<=target; i++)
    {
      value=pArray[i]; 
      writeValueInt(reg, MCHAN, i, value); 
    }
  } 

  // required, writeValueInt does not call loadDac()
  loadDac();
}

//================================
// User serial command processing
//================================

void serialEvent()
{
  //
  // Listens and executes the user commands entered on serial input
  //
  // Available commands are as follows:
  // 
  // 1) Control pin management
  //    ----------------------
  //  
  // "C" toggle CLR pin state
  // "L" pulses LDAC
  // "R" reset all registers
  //
  // 2) Set register type to be used for the multiple channels operations
  //    -----------------------------------------------------------------
  //
  // "X" set register type to XREG (DAC current X register)
  // "O" set register type to OFFSET
  // "G" set register type to GAIN
  //
  // 3) Multiple channels operations: updates all registers of assigned type for channel numbers from root to target
  //    ------------------------------------------------------------------------------------------------------------ 
  //
  // "F" set the root channel number for the "I" and "W" commands below; channel numbers start from 0
  // "T" set the target channel number for the "I" and "W" commands below; channel numbers start from 0
  //
  // "I" writes an assigned value to the AD5730 channels from root to target included; if root or target channels numbers are >= CHANMAX writes all channels
  // "W" writes values of the buffer arrays for the selected register type to the AD5730 corresponding channels from root to target included; 
  //        if root or target channel numbers are >= CHANMAX writes all channels
  //
  // 4) Single channel operation, always uses the XREG register type
  //    ------------------------------------------------------------
  //
  // "@" sets channel number for single channel X register operation
  // "$" sets and write the value to the X register of DAC channel set by the "@" command above. 
  // 
  // 5) Internal xreg buffer array management
  //    -------------------------------------
  //
  // "/" is used as delimiter between values to fill the xreg array: fills the current position of the xreg array with the preceding number and increments the current position
  // "P" print all values in the xreg buffer array
  //
  // ----------------------
  // COMMAND USAGE EXAMPLES
  // ----------------------
  //
  // IMPORTANT NOTE: the Arduino IDE Serial Monitor baud rate must be set to 115200 baud to communicate with this program
  //
  // To test an example command string, copy (without the double quotes) and paste it in the input field of the Arduino IDE Serial Monitor, then press the Enter key
  // Executing the "C", "L", "R", "I", "W", "$", "P" commands, the program will print some info on the Arduino IDE Serial Monitor 
  //
  // a) Example command string for single channel operation: 
  //    
  //  "31@65000$"
  //
  // where:
  //  "31@" set the channel #31
  //  "65000$" write the value 65000 to the DAC X register of the set channel (#31)
  // Please notice that the channel number must always be set first
  //
  // b) Example command string for multiple channel operation:
  //
  //  "X34F39T65000I"
  //
  // where: 
  //  "X" will set the register type to XREG
  //  "34F" will set the root channel number to 34
  //  "39T" will set the target channel number to 39
  //  "65000I" will write the value 65000 to the DAC X registers of the channels numbers from root to target
  //

  static uint32_t serialdata=0;
  static uint8_t i=0; // current update position for voltage array filling
  static uint8_t channel=CHANMAX, root=CHANMAX, target=CHANMAX;
  static registerMode_t reg=XREG;

  while (Serial.available())
  {
    char inChar = Serial.read();

    if(inChar > 47 && inChar < 58) 
    {
      // digit input, update the current number in serialdata
      serialdata*=10; 
      serialdata+=(inChar -48); 

      if (serialdata > 0xFFFF)
      {
        Serial.println("Command error: input number overflow");
        serialdata = 0xFFFF;
      }

      continue;
    } 
    
    switch(inChar)
    {
      case 'X': reg=XREG; serialdata=0; break;    //Register type X register
      case 'O': reg=OFFSET; serialdata=0; break;  //Register type Offset
      case 'G': reg=GAIN; serialdata=0; break;    //Register type Gain

      case 'F': 
        root = serialdata < CHANMAX ? serialdata : CHANMAX;
        serialdata=0;
        break; //root Channel Symbol use for Array and Initial operation

      case 'T': 
        target = serialdata < CHANMAX ? serialdata : CHANMAX;
        serialdata=0;        
        break; //target Channel Symbol use for Array and Initial operation

      case 'I': 
        {
          initRegister(reg, root, target, serialdata);
          reg=XREG;
          root=CHANMAX;
          target=CHANMAX;
          serialdata=0;
        }

        break;

      case 'W':  
        {
          writeFromArray(reg, root, target);
          reg=XREG;
          root=CHANMAX;
          target=CHANMAX;
          serialdata=0;
        }

        break;

      case 'C': toggleClear(); serialdata=0; break;
      case 'L': loadDac(); serialdata=0; break;     
      case 'R': resetDac(); serialdata=0; break;    

      case '@': 
        { //Channel Symbol for specific Dac Channel
          channel=serialdata; 
          //if(channel < CHANMAX) chipselect(0);    //channel 0 to CHANMAX selected chip 0
          //if(channel > CHANMAX) chipselect(1);    //channel CHANMAX to CHAN2CHIP selected chip 1
          serialdata=0; 
        }

        break;

      case '/': 
        { //Value separator symbol increments channel and wrote value to buffer xreg array
          xreg[i] = serialdata;
          serialdata = 0;
          i++;
          if(i>=CHANMAX) i=0;
          break;  
        }

      case '$': 
        { //Value symbol for specific Dac Channel
          if(channel<CHANMAX)
          {
            writeValueInt(XREG, MCHAN, channel, serialdata);
            loadDac();
            xreg[channel] = serialdata;
          }

          else
          {
            Serial.println("$ command: channel number out of range");
          }

          channel = CHANMAX;
          serialdata=0;
        }

        break;

      case 'P': 
        { //Print all data from voltage Array //later change to desired Register Array
          Serial.println("Xreg buffer array:");
          Serial.print("Current update position: ");
          Serial.println(i);

          for(uint8_t n=0; n<CHANMAX; n++)
          {
            Serial.print("Channel #");
            Serial.print(n);
            Serial.print(" Val ");
            Serial.println(xreg[n], DEC);
          }

          serialdata=0;          
        }

        break;
    }
  } 
}

//----------------------------
// Arduino setup() and loop()
//----------------------------

void setup() 
{
  Serial.begin(115200);
  
  // set all control pins as outputs and their output values to inactive
  initDacControlPins();

  resetBufferArrays();
  
  SPI.begin();

  resetDac();
  loadDac();
}

void loop() 
{
  // the program uses the serialEvent function to parse user input and send the appropriate commands to the AD5370
  // the serialEvent function call is not here because it is called automatically after exiting from the loop function
  // see here:
  // https://www.arduino.cc/reference/it/language/functions/communication/serial/serialevent/
}


Thank you for your detailed observations... My client requires the modbus protocol so as to be able to connect it to their in-house software to test out varying voltage outputs. The module will be connected to an ECU to simulate various in-car device voltages. Besides the modbus and a 16-bit resolution, we are free to suggest solutions, the arduino connects to the DAC via SPI. Since we have a portenta we are looking to use this as the h7 with breakout board has ethernet built-in..

So there actually is a library.
So what is the problem?

the problem im facing is to understand how the serial monitor parameters work and how to use them to achieve required voltages at each channel...

Are you using the 5372 Eval board?

Looks to me completely different. I am guessing someone has tried to port it from AVR to generic Arduino. The parts // TODO: check if a delay is needed here are a big red flag suggesting the code probably won't work.

You need to reduce the number of variables. Start with a known and unmodified working hardware and software setup. If you are using some third party software, study it thoroughly to understand what and how it works, comparing it with the datasheet.

Once you have that setup working, and you understand how the AD5370 works, how the 3rd party code works, and to enter commands to do what you want, then start porting to another platform.

Since you have the code, you should be able to read it and understand what every line does.

Either way, you will need to understand the AD5370. Tbh, I doubt switching to a different DAC will help understanding.

ok I am able to get something working now..
if I assign a value of 0 to a channel I get a voltage of 6.68v... if I assign 65000, I see a value of -11v... how does this mapping work? shouldn't I be getting +12V for value 0?

The code you list is quite different from that at the GitHub site.
Plus I don't even see a 'D' command

What do you have VREF set to?

And where exactly are you measuring the voltage?