4–20mA R current loop Arduino tutorial Part III: receiver

Teodor Costachioiu
Aug 23, 2016 · 6 min read

Today we come to the third and last part of the 4–20mA communication tutorial using Arduino UNO boards and the pair of 4–20mA R and 4–20mA T click boards from MikroElektronika. In this post I will describe the calibration procedure and I provide the code for the receiver. I will also show a method to implement open loop and short circuit detection using the above click boards.

So. we begin with the calibration procedure. We need to know the ADC output for the situation when the current through the loop is 4mA, and the ADC output for the situation when the current through the loop is 20mA. First, both transmitters and receivers must be powered. Then the calibration code on the transmitter is used to set the current through the loop is 4mA. The following code was used to output the ADC result:

/*
4-20mA R click calibration
Reads result on 4-20mA bus and outputs the
result of the conversion on serial monitor
*/
#include <SPI.h>

// Arduino UNO with Mikroe Arduino Uno Click shield
// 4-20mA R click is placed in socket #2
// CS is pin 9
// SCK is pin 13
// MISO is pin 12
// MOSI is pin 11
#define ADC_CS 9

unsigned int ADC_result;
float ADC_avrg;

void setup() {
/* Resetting MCP3201
* From MCP3201 datasheet: If the device was powered up
* with the CS pin low, it must be brought high and back low
* to initiate communication.
* The device will begin to sample the analog
* input on the first rising edge after CS goes low. */
pinMode (ADC_CS, OUTPUT);
digitalWrite(ADC_CS, 0);
delay(100);
digitalWrite(ADC_CS, 1);

// initialize serial
Serial.begin(9600);
// initialize SPI
SPI.begin();

}

void loop() {
ADC_avrg = 0;
// Average of 100 measurements
for (int i=0; i<100; i++){
// put your main code here, to run repeatedly:
ADC_result = get_ADC();
//Serial.print("Conversion result: ");
//Serial.println(ADC_result);
// Dont print too often
delay(10);
ADC_avrg = ADC_avrg + ADC_result;
}
Serial.print("Conversion average: ");
Serial.println(ADC_avrg / 100);
delay(500);
}

unsigned int get_ADC(void){
/*
DAC works on SPI
We receive 16 bits
Of which we extract only 12 bits
MCP3201 has a strange way of formatting data
with 5 bits in the first byte and
the rest of 7 bits in the second byte
*/
unsigned int result;
unsigned int first_byte;
unsigned int second_byte;

SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE1));
digitalWrite(ADC_CS, 0);
first_byte = SPI.transfer(0);
second_byte = SPI.transfer(0);
digitalWrite(ADC_CS, 1);
SPI.endTransaction();

/* After the second eight clocks have been
sent to the device, the MCU receive register
will contain the lowest order seven bits and
the B1 bit repeated as the A/D Converter has begun
to shift out LSB first data with the extra clock.
Typical procedure would then call for the lower order
byte of data to be shifted right by one bit
to remove the extra B1 bit.
See MCP3201 datasheet, page 15
*/
result = ((first_byte & 0x1F) << 8) | second_byte;
result = result >> 1;
return result;
}

An average of 100 measurements was taken into account. One might observe that the INA196 current shunt has some variations in the output voltage, even if the current through the loop is stable up to microamps level. A method to calculate the accuracy variations is given in the INA196 datasheet, pages 17–18.

So, for 4mA through the loop I’ve got an average of 788, with a minimum of 775 and a maximum of 800.

For 20mA through the loop the ADC average output was 3964, with a minimum of 3952 and a maximum of 3982.

With the above values we can now write the receiver code:

/*
4-20mA R click receiver
Reads result on 4-20mA bus
*/
#include <SPI.h>

// Arduino UNO with Mikroe Arduino Uno Click shield
// 4-20mA R click is placed in socket #2
// CS is pin 9
// SCK is pin 13
// MISO is pin 12
// MOSI is pin 11
#define ADC_CS 9

int loop_current;
int received_data;

// Calibration data obtained by running the calibration code
const int ADC_4mA = 784;
const int ADC_20mA = 3954;

// Data min and max range
// Matches the values on the transmitter code
// But it's a good ideea to resample to a lower resolution
const int data_min_range = 0;
const int data_max_range = 1023;


void setup() {
/* Resetting MCP3201
* From MCP3201 datasheet: If the device was powered up
* with the CS pin low, it must be brought high and back low
* to initiate communication.
* The device will begin to sample the analog
* input on the first rising edge after CS goes low. */
pinMode (ADC_CS, OUTPUT);
digitalWrite(ADC_CS, 0);
delay(100);
digitalWrite(ADC_CS, 1);

// initialize serial
Serial.begin(9600);
// initialize SPI
SPI.begin();

}

void loop() {

// Read the loop current
loop_current = ReadFrom420mA();
// Error checking
if (loop_current == -1)
Serial.println("Error: open loop");
else if (loop_current == -2)
Serial.println("Error: current loop is in short circuit");
// All is OK, remapping to initial data range
else {
received_data = map(loop_current, ADC_4mA, ADC_20mA, data_min_range, data_max_range);
Serial.print("Received value is: ");
Serial.println(received_data);
}
}

unsigned int get_ADC(void){
/*
DAC works on SPI
We receive 16 bits
Of which we extract only 12 bits
MCP3201 has a strange way of formatting data
with 5 bits in the first byte and
the rest of 7 bits in the second byte
*/
unsigned int result;
unsigned int first_byte;
unsigned int second_byte;

SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE1));
digitalWrite(ADC_CS, 0);
first_byte = SPI.transfer(0);
second_byte = SPI.transfer(0);
digitalWrite(ADC_CS, 1);
SPI.endTransaction();

/* After the second eight clocks have been
sent to the device, the MCU receive register
will contain the lowest order seven bits and
the B1 bit repeated as the A/D Converter has begun
to shift out LSB first data with the extra clock.
Typical procedure would then call for the lower order
byte of data to be shifted right by one bit
to remove the extra B1 bit.
See MCP3201 datasheet, page 15
*/
result = ((first_byte & 0x1F) << 8) | second_byte;
result = result >> 1;
return result;
}

int ReadFrom420mA(void)
{
int result;
int ADC_result;
float ADC_avrg = 0;
for (int i = 0; i < 100; i++){
ADC_result = get_ADC();
// Measure every 1ms
delay(1);
ADC_avrg = ADC_avrg + ADC_result;
}
result = (int)(ADC_avrg/100);

// now we do some shortcircuit and open loop checking
// open loop
if (result < (ADC_4mA - 50)){
return -1;
}
// shortcircuit
if (result > (ADC_20mA + 50)){
return -2;
}
// everything is OK
return result;
}

In the receiver code I have implemented a simple detection routine for open loop and shortcircuit conditions. With an open loop the ADC output is somewhere around 5, so the threshold I’ve set should be more than enough. The output of the ADC in shortcircuit situations depends on where the shortcircuit is located. The worst situation is with a very long loop cable and the shortcircuit near the transmitter. Again, the threshold level must be set as to allow for the detection of shortcircuits while minimizing false indications.

Last, but not least, there’s a good idea to re sample the result to a lower resolution. In the MikroElektronika examples only 100 discrete levels are used. I guess that resampling to 8 bits should be enough for most applications.

Also in this series


Originally published at https://electronza.com on August 23, 2016. Moved to medium on April 25, 2020.

DIY electronics projects and more

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade
A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store