How to program MAX22007 Configurable Analog Output

Abstract

The MAX22007 is a Configurable Analog Output device. It supports 4 channels and each channel can be individually programmed as a 0V to +10V Voltage output or a 0mA to +20mA current output.

A microcontroller compatible serial peripheral interface (SPI) provides access to many advanced features. This application note provides example C-code implementing setup, monitoring, and diagnostic functions in the microcontroller.

Introduction

The MAX22007 integrates four 12-bit DACs (digital-to-analog converters) to create four channels with software-configurable outputs that support either 0V to +10V or 0mA to +20mA analog outputs. Each channel can also detect the load impedance and determine if the load is a voltage or current input.

The MAX22007 functional diagram

Figure 1. The MAX22007 functional diagram.

This application note illustrates a series of functions for a faster and proven way to program the MAX22007. The functions are written in C and are easy to port to any common microcontroller. Refer to the MAX22007 data sheet for detailed information on the MAX22007 pins, operating modes, and control registers.

MAX22007 SPI

The MAX22007 serial peripheral interface (SPI) commands are 24 bits (8-bit instruction + 16-bit data) with CRC-disabled, and 32 bits with CRC-enabled. Table 1 shows the SPI command structure. The SPI mode for the MAX22007 is CPOL = 0 (CLK idle = 0) and CPHA = 0 (rising/first edge samples the data). The data/commands must first be clocked in most significant byte (MSB).

Table 1. The MAX22007 SPI Command Structure
Address Control Data
7-bits A[6:0], MSB to least significant byte (LSB) R/W bit, Read = 1, Write = 0 16-bits D[15:0], MSB to LSB

The MAX22007 data sheet has full details of the SPI read and write cycles, register tables, and instructions.

Figure 1 shows the main function blocks of the MAX22007. There are four channels that can be individually programmed to be the voltage or current output, and a control logic with a SPI port to access all the registers and hardware flags for diagnostics.

The MAX22007 supports eight logic-level GPIOs (general purpose input/output) to simplify systems which require galvanic isolation. The GPIOs can control external components such as multiplexers, FETs (field-effect transistors) or enabling power supplies to be switched, or digital signals to be read back through the isolation barrier. The device also supports the daisy-chain mode, which also reduces the number of IO pins required for isolated designs.

Source Code

This application note provides C source code examples, providing driver functions to access the multiple registers within the MAX22007 for configuration, control, and diagnostic features.

Note that the CONFIGURATION Register 0x03 LD_CNFG[3:0], bits 15 to 12, are set to 0 at power-up. All the four outputs update simultaneously upon a high to low transition on the LDAC pin. Set the LD_CNFG bits to 1 if the channels are required to be transparent and update immediately.

Global Variables for Channel/Mode Selection

        public enum Register_address
        {
            // All Registers
            REVISION_ID       = 0x00,
            STATUS_INTERRUPTS = 0x01,
            INTERRUPT_ENABLE  = 0x02,
            CONFIGURATION     = 0x03,
            CONTROL           = 0x04,
            CHANNEL_MODE      = 0x05,
            SOFT_RESET        = 0x06,
            CHANNEL0_DATA     = 0x07,
            CHANNEL1_DATA     = 0x08,
            CHANNEL2_DATA     = 0x09,
            CHANNEL3_DATA     = 0x0a,
            GPIO_CONTROL      = 0x0b,
            GPIO_DATA         = 0x0c,
            GPI_EDGE_CTRL     = 0x0d,
            GPI_EDGE_STATUS   = 0x0e,
        };

        public enum AOut_Mode
        {
            high_impedance = 0,
            AO_12V         = 1,
            AO_25mA        = 2,
            out_of_range1  = 3,
        };




 
//********************************************************************
//*
//* Function: MAX22007_read_register
//* Description: Read one Register from MAX22007
//*
//* Input: Register-Address (take from definitions in header-file)
//* Output: 16bit register content
//*
//* if CRC is enabled, then crc8-Command is required
//*
//********************************************************************/
public UInt32 MAX22007EVKIT_read_register(Register_address address)
{
     if (CRC_Enabled == false)
     {
         max22007_port.SPI_CS0Enable();
         max22007_port.SPI_W_transaction_8( (ushort) ( ((byte)address << 1) + 0x01 )  );
         result = max22007_port.SPI_R_transaction_16();
         max22007_port.SPI_CS0Disable();
     }
     else
     {
         max22007_port.SPI_CS0Enable();
         max22007_port.SPI_W_transaction_8( (ushort) ( ((byte)address << 1) + 0x01 )  );
         result = max22007_port.SPI_R_transaction_16();
         max22007_port.SPI_CS0Disable();

         CRC_result = max22007_port.SPI_R_transaction_8();  // read the CRC
         byte CRC_TX1 = (address << 1) + 0x01;
         byte CRC_RX1 = ((result >> 8) & 0xff);
         byte CRC_RX2 = ((result     ) & 0xff);
         byte CRC_Calc = crc8(CRC_TX1, CRC_RX1, CRC_RX2);

         if (CRC_Calc != CRC_result)
         {
            result = 0xfffffffe; // return a 32 bit value to flag an error
         }
     }
}



//********************************************************************
//*
//* Function: MAX22007_write_register
//* Description: Write one Register to MAX22007
//*
//* Input: Register-Address (take from definitions in header-file)
//*        16bit data (new register content)
//*
//********************************************************************/
public void MAX22007EVKIT_write_register(Register_address address, UInt16 data)
{
    byte CRC_TX1 = (byte)((byte)address << 1);

    if (CRC_Enabled == false)
    {
        max22007_port.SPI_CS0Enable();
        max22007_port.SPI_W_transaction_8( (ushort) ( ((byte)address << 1) )  );
        max22007_port.SPI_W_transaction_16(data);
        max22007_port.SPI_CS0Disable();
    }
    else
    {
        byte CRC_TX2 = (byte)((data>> 8) & 0xff);
        byte CRC_TX3 = (byte)( data      & 0xff);
        byte CRC_Calc = crc8(CRC_TX1, CRC_TX2, CRC_TX3);

        max22007_port.SPI_CS0Enable();
        max22007_port.SPI_W_transaction_8( (ushort) ( ((byte)address << 1) )  );
        max22007_port.SPI_W_transaction_16(data);
        max22007_port.SPI_W_transaction_8( CRC_Calc );
        max22007_port.SPI_CS0Disable();
    }
}


 
// ********************************************************************
//
// Function: MAX22007_Mode_Set
// Description: Sets up MAX22007 Mode for one of the 4 Channels
//
// Input: mode:    Desired Mode
//        Channel: Desired Channel
// Output: None (The selected channel of MAX22007 will be setup by this routine)
//
// ******************************************************************** 
private void MAX22007_Mode_Set(byte Channel, AOut_Mode mode)
{
   // Set AO Mode (Register 0x05: CHANNEL_MODE)
   UInt32 previous_mode = MAX22007EVKIT_read_register(Register_address.CHANNEL_MODE);
   UInt16 new_mode = (UInt16) previous_mode;
            
   switch (Channel)
   {
     case 0:
              if (mode == AOut_Mode.high_impedance)
              {   new_mode = (new_mode & 0xeeff);   // High-Impedance, set to Voltage Mode and Power-Off - Channel 0
              }
              if (mode == AOut_Mode.AO_12V)
              {   new_mode = (new_mode & 0xefff);   // Voltage Output, set CHNL_MODE to 1 for this         Channel 0
                  new_mode = (new_mode | 0x0100);   // make sure the Channel is enabled                    Channel 0
              }
              if (mode == AOut_Mode.AO_25mA)
              {   new_mode = (new_mode | 0x1000);   // Current Output, set CHNL_MODE to 1 for this         Channel 0
                  new_mode = (new_mode | 0x0100);   // make sure the Channel is enabled                    Channel 0
              }
              break;
     case 1:
              if (mode == AOut_Mode.high_impedance)
              {   new_mode = (new_mode & 0xddff);   // High-Impedance, set to Voltage Mode and Power-Off - Channel 1
              }
              if (mode == AOut_Mode.AO_12V)
              {   new_mode = (new_mode & 0xdfff);   // Voltage Output, set CHNL_MODE to 1 for this         Channel 1
                  new_mode = (new_mode | 0x0200);   // make sure the Channel is enabled                    Channel 1
              }
              if (mode == AOut_Mode.AO_25mA)
              {   new_mode = (new_mode | 0x2000);   // Current Output, set CHNL_MODE to 1 for this         Channel 1
                  new_mode = (new_mode | 0x0200);   // make sure the Channel is enabled                    Channel 1
              }
              break;
     case 2:
              if (mode == AOut_Mode.high_impedance)
              {   new_mode = (new_mode & 0xbbff);   // High-Impedance, set to Voltage Mode and Power-Off - Channel 2
              }
              if (mode == AOut_Mode.AO_12V)
              {   new_mode = (new_mode & 0xbfff);   // Voltage Output, set CHNL_MODE to 1 for this         Channel 2
                  new_mode = (new_mode | 0x0400);   // make sure the Channel is enabled                    Channel 2
              }
              if (mode == AOut_Mode.AO_25mA)
              {   new_mode = (new_mode | 0x4000);   // Current Output, set CHNL_MODE to 1 for this         Channel 2
                  new_mode = (new_mode | 0x0400);   // make sure the Channel is enabled                    Channel 2
              }
              break;
     case 3:
              if (mode == AOut_Mode.high_impedance)
              {   new_mode = (new_mode & 0x77ff);   // High-Impedance, set to Voltage Mode and Power-Off - Channel 3
              }
              if (mode == AOut_Mode.AO_12V)
              {   new_mode = (new_mode & 0x7fff);   // Voltage Output, set CHNL_MODE to 1 for this         Channel 3
                  new_mode = (new_mode | 0x0800);   // make sure the Channel is enabled                    Channel 3
              }
              if (mode == AOut_Mode.AO_25mA)
              {   new_mode = (new_mode | 0x8000);   // Current Output, set CHNL_MODE to 1 for this         Channel 3
                  new_mode = (new_mode | 0x0800);   // make sure the Channel is enabled                    Channel 3
              }
                    break;
    }
    MAX22007EVKIT_write_register(Register_address.CHANNEL_MODE,  new_mode);
}
 
// ********************************************************************
//
// Function: MAX22007_convert_Voltage_to_LSB
// Description: Converts a voltage to an LSB value for the DAC
//
// Input:  float:  Voltage
// Output: UInt16  LSB Value for the DAC
//
// ********************************************************************
private UInt16 MAX22007_convert_Voltage_to_LSB (float voltage)
{
    UInt16 new_hex_value = 0;
    float result = 0;
    float phy_AO_12V_factor  = (float) 12.5 / (float) 4095;

    // check for errors
    if (voltage > 12.5)    { return 0xfffe; } // return out of range value to highlight there was an error
    if (voltage < 0)       { return 0xfffe; } // return out of range value to highlight there was an error

    // convert voltage to LSB value
    result = (voltage / phy_AO_12V_factor);
    new_hex_value = (UInt16) result;

    return new_hex_value;
}



// ********************************************************************
//
// Function: MAX22007_convert_Current_to_LSB
// Description: Converts a current in mA to an LSB value for the DAC
//
// Input:  float:  Current in mA
// Output: UInt16  LSB Value for the DAC
//
// ********************************************************************
private UInt16 MAX22007_convert_Current_to_LSB (float current_mA)
{
    UInt16 new_hex_value = 0;
    float result = 0;
    float phy_AO_25mA_factor = (float) 25 / (float) 4095;

    // check for errors
    if (current_mA > 25)    { return 0xfffe; } // return out of range value to highlight there was an error
    if (current_mA < 0)     { return 0xfffe; } // return out of range value to highlight there was an error

    // convert voltage to LSB value
    result = (current_mA / phy_AO_25mA_factor);
    new_hex_value = (UInt16) result;

    return new_hex_value;
}
 
// ********************************************************************
//
// Function: MAX22007_DAC_Set_LSB
// Description: Writes a new LSB value to the DAC,
//              assuming it is already setup in a specific mode, use DAC_Setup first
//              If LDAC-pin is high, it must be toggled after setting up update the output
//
// Input: new DAC value in LSB
// Output: None
//
// ********************************************************************
private void MAX22007_Set_DAC(byte Channel, UInt16 LSB_code)
{
  UInt16 DAC_out_register = (UInt16) (LSB_code << 4); // Shift bits to match with register

  switch (Channel)
  {
   case 0:
            MAX22007EVKIT_write_register (Register_address.CHANNEL0_DATA, DAC_out_register); // Write AO Data register CH0
            break;
   case 1:
            MAX22007EVKIT_write_register (Register_address.CHANNEL1_DATA, DAC_out_register); // Write AO Data register CH1
            break;
   case 2:
            MAX22007EVKIT_write_register (Register_address.CHANNEL2_DATA, DAC_out_register); // Write AO Data register CH2
            break;
   case 3:
            MAX22007EVKIT_write_register (Register_address.CHANNEL3_DATA, DAC_out_register); // Write AO Data register CH3
            break;
  }

}


// ********************************************************************
//
// Function: main
// Description: The follwoing function would setup:
//              1. ALL outputs to immediately update on write
//              2. Channel 0 in Voltage mode and drive 5V
//              3. Channel 1 in Current mode and drive 10mA
//
// Input:  float:  Current in mA
// Output: UInt16  LSB Value for the DAC
//
// ********************************************************************
private void setup_main ()
{
    MAX22007EVKIT_write_register (Register_address.CONFIGURATION, 0xf000);       // Set all Latch bits

    MAX22007_Mode_Set(0, AOut_Mode.AO_12V);                          // setup Channel 0 to Voltage Mode
    MAX22007_Mode_Set(1, AOut_Mode.AO_25mA);                         // setup Channel 1 to Current Mode

    UInt16 DAC_LSB_value = 0;

    DAC_LSB_value = MAX22007_convert_Voltage_to_LSB ((float) 5.0);   // get integer value for 5.0 Volt
    MAX22007_Set_DAC(0, DAC_LSB_value);                              // write this 5V value to Channel 0

    DAC_LSB_value = MAX22007_convert_Current_to_LSB ((float)10.0);   // get integer value for 10.0 mA
    MAX22007_Set_DAC(1, DAC_LSB_value);                              // write this 10.0mA value to Channel 1

}

 
// ********************************************************************
//
// Function: MAX22007_GPIO_Setup
// Description: Sets up all 8 GPIO Pins, bit0=GPIO0, bit1=GPIO1, ...
//              Since the command includes everything Enable/Disable as well as
//              GPIO Direction, this function is faster than GPO_Set
//              because it does not have to read back the setup from the part
//
// Input:  GPIO_enable (byte)    Bit0 = GPIO0, Bit1 = GPIO1, ... (0 = Off,   1 = On)
//         GPIO_direction (byte) Bit0 = GPIO0, Bit1 = GPIO1, ... (0 = Input, 1 = Output)
//
// Output: None
//
// ********************************************************************
void MAX22007_GPIO_Setup (byte GPIO_enable, byte GPIO_direction)
{
    UInt16 new_gpio_value = (UInt16) ( ( (GPIO_enable & 0xff) << 8) + ( (GPIO_direction & 0xff) ) );
    MAX22007EVKIT_write_register(Register_address.GPIO_CONTROL,  new_gpio_value);
}


// ********************************************************************
//
// Function: MAX22007_GPO_Set
// Description: Sets GPOs high or low, bit0=GPIO0, bit1=GPIO1, ...
//              GPOs must be setup and enabled prior this use MAX22007_GPO_Set
//
// Input: GPO Setting, bit0=GPIO0, bit1=GPIO1, ... (0 = Low, 1 = High)
// Output: None
//
// ********************************************************************
void MAX22007_GPO_Set (byte GPO_Setting)
{
    UInt16 GPO_data = (UInt16) ((GPO_Setting<<8) & 0xff00);                  // Shift bits for GPO

    MAX22007EVKIT_write_register(Register_address.GPIO_DATA, GPO_Setting);   // Write new GPO settings
                                                                             // Inputs are read only, no need to
                                                                             // worry about writing these bits
}

// ********************************************************************
//
// Function: MAX22007_GPI_Get
// Description: Gets all GPI readings high or low, bit0=GPIO0, bit1=GPIO1, ...
//              GPIs must be setup and enabled prior this use MAX22007_GPI_Get
//
// Input: None
// Output: GPI Setting, bit0=GPIO0, bit1=GPIO1, ... (0 = Low, 1 = High)
//
// ********************************************************************
byte  MAX22007_GPI_Get ()
{
    UInt32 gpi_result = MAX22007EVKIT_read_register(Register_address.GPIO_DATA);  // read GPI Data
    byte result = (byte) (gpi_result & 0xff);

    return result;
}

CRC Function

The CRC function and calculation are described in more detail in the AN7072 for a device with 24-bit registers. The MAX22007 has only 16-bit registers. The CRC calculation in AN7072 is the same concept for the MAX22007, but the calculation is only 3 bytes instead of 4 as described in the AN7072. This following function can be used as-is for MAX22007.

// ********************************************************************
//
// Function: MAX22007_crc8
// Description: Calculates CRC for MAX22007 commands (read or write)
//
// Input: BYTE1:     Command byte (register address + R/W bit)
//        BYTE2:     MS-Byte of the register value
//        BYTE3:     LS-Byte of the register value
// Output byte:      crc8 of Input 
//                   -> for write commands send result as the CRC code
//                   -> for read commands compare result to check for errors
//
// ******************************************************************** 
public byte MAX22007_crc8(byte BYTE1, byte BYTE2, byte BYTE3)
{
    byte crc8_start = 0x00;
    byte crc8_poly  = 0x8c; // rotated 0x31, which is our polinomial
    byte crc_result = crc8_start;

    // BYTE1
    for (int i=0; i<8; i++)
    {
        if( ( (( BYTE1>>i ) ^ (crc_result) ) & 0x01 ) > 0 )      // IF(XOR(C6;BITAND(D5;2^4)/2^4)
        { crc_result = (byte) (crc8_poly ^ crc_result>>1 );  }   // BITXOR($D$1;BITAND((D5*2);31))
        else
        { crc_result = (byte) (crc_result>>1);               }
    }

    // BYTE2
    for (int i=0; i<8; i++)
    {
        if( ( (( BYTE2>>i ) ^ (crc_result) ) & 0x01 ) > 0 )      // IF(XOR(C6;BITAND(D5;2^4)/2^4)
        { crc_result = (byte) (crc8_poly ^ crc_result>>1 );  }   // BITXOR($D$1;BITAND((D5*2);31))
        else
        { crc_result = (byte) (crc_result>>1);               }
    }

    // BYTE3
    for (int i=0; i<8; i++)
    {
        if( ( (( BYTE3>>i ) ^ (crc_result) ) & 0x01 ) > 0 )      // IF(XOR(C6;BITAND(D5;2^4)/2^4)
        { crc_result = (byte) (crc8_poly ^ crc_result>>1 );  }   // BITXOR($D$1;BITAND((D5*2);31))
        else
        { crc_result = (byte) (crc_result>>1);               }
    }
    return crc_result;
}

Conclusion

This application note shows how the MAX22007 can be programmed for all possible use cases. The MAX22007 EV kit was used to test this code. The C-code examples in this application note are a proven solution to implement an interface quickly and easily between popular microcontrollers and the MAX22007.