Thermocosa powered by Arduino and LabVIEW

Hello, Habr!

I work at the Institute of General Physics, RAS. The profile of our laboratory is laser remote sensing, specifically lidars. If you do not know what kind of animals they are, you can read, for example, on Wikipedia . Lidars are sometimes also called laser radars. The fundamental difference and advantage of the lidar is that with its help it is possible not only to measure the distance to the sensing object by the delay of the return signal, but also to obtain (from the signal spectrum) information on the composition and properties of the object. For example, there are methods of lidar determination of the temperature profile of water by depth in reservoirs.

Non-contact measurements are only as useful as accurate, therefore, to calibrate the results of remote measurements with contact ones, it was decided to make a braid - a loop of several temperature sensors on one line.

Iron


The non-contact method using lidar allows you to measure the water temperature to a depth of several meters (it depends on transparency, it is clear that the laser beam scatters quickly and does not go far in dirty water), so the spit is small, consists of five temperature sensors placed on the cable at intervals 1 m, plus another 4 m of cable, counting from the “upper” sensor.

As the sensitive elements, I chose the digital thermometers DS18B20 ( datasheet , 320 kb) in a sealed version, like this:



Why are they like that? Because they are sealed (smile), they are already delivered with a cable 1 m long, give high accuracy and operate according to the 1-Wire protocol , which greatly simplifies communication with them.

A thoughtful study of the datasheet provided the following information. The sensors can be connected in two ways: normal, through three wires (ground, plus power and signal bus) and in spurious mode, when the sensor receives power from the data line. The "spurious" mode simplifies the connection even more (just two wires), but can sometimes distort the sensor readings. Any deterioration in accuracy is harmful to us, and 5 volts are easily accessible from the Arduino board that controls the sensors, so I decided to power the sensors in the usual way.


Thermocouple circuit Datashit

recommends using a pull-up resistor with a nominal value of 4.7 kOhm, I only found 2.2 in the farm, but this did not affect the performance of the device.

Arduino Nano with an ATMega328P controller is responsible for controlling the sensors and for their connection with the outside world, that is, with a PC.

This is how the circuit assembled on the breadboard looks like:



Here is the final version after soldering and insulating:



And this is the whole thermal braid assembly (the control electronics are not isolated):



I chose Arduino as the “brains” of the device, firstly, because that this platform is easy to learn, and secondly, because it can be controlled from a PC from under LabVIEW (hereinafter for brevity LabVIEW = LV), which is important, since the software for most of our laboratory projects is written in this environment, and the ability embedding a simple automated temperature control system Fy to other schemes is expensive.

Software


The main feature of this task is to work with the device from the LV environment, so it was decided to start programming by studying the interaction of Arduino and LV. There is practically no information on this interaction on the hub, therefore, with your permission, I will describe everything in sufficient detail.

Start


So what we need (info from here ):
  1. LV 2009 or later.
  2. NI VISA (LV module for communicating virtual devices with real ones).
  3. Arduino IDE and drivers .
  4. OneWire library for Arduino - put the ZIP contents in / [Arduino IDE installation directory] / libraries /.
  5. The LV developer offers an extension for working with Arduino boards - LabVIEW Interface for Arduino, or simply LIFA. Recently, the development of LIFA was officially discontinued; instead, NI offers to use the LINX toolkit from LabVIEW Hacker. It supports a larger number of devices and contains more tools, however, I used LIFA, because in LINX the controller firmware looks like HEX files, I did not have the desire or time to disassemble and edit them. And in LIFA, the sources are the usual Arduino sketches.
    LIFA can be installed directly from LV through the VI Package Manager interface (Tools -> VI Package Manager). After installation, the “Arduino” sub-palette will appear on the function palette:



To start working with Arduino in LV, you need to flash your controller with the LIFA_Base.ino sketch , taken from the C: / Program Files / National Instruments / LabVIEW [version] /vi.lib/LabVIEW Interface for Arduino / Firmware / LIFA_Base / folder. The specified folder contains a bunch of files - C-libraries, sources and two sketches, LabVIEWInterface.ino and LIFA_Base.ino . The first contains descriptions of all the functions for working with Arduino, the second is short and collects everything together for uploading to the controller.

Voila, we now have access to most of Arduino’s capabilities from a computer through LV. As you might imagine, the first thing I did, having figured out everything described above, was to blink the LED.

Played, now for the cause.
The 1-Wire protocol and DS18B20 temperature sensors have existed for a long time and are widespread, so I decided to look for information on the joint use of DS18B20 and Arduino. And almost immediately I came across a suitable source, and not somewhere, but on the official LabVIEW forum ( link ). Topikstarter had a similar task to me - to read the temperature sensor DS18B20 with Arduino from LabVIEW. He started searching and saw a LV diagram on YouTube with the OneWire Read VI present on it and asked the guru what the VI was and where to get it. The author of the video responded to his request and provided source codes and detailed instructions on how and what to do.

DS18B20 sensors are controlled as follows: the “master” (controller, microprocessor) sends a two-digit hexadecimal command over the data line, depending on which the sensor measures the temperature, receives bytes from the “master” to write to its memory, or sends the current contents of the memory to the data line . The author of the video modified the sketches uploaded to Arduino to work with LIFA:
  1. In the LIFA_Base.ino file, I connected the OneWire.h library,
  2. In the file LabVIEWInterface.ino in the case structure, which is responsible for processing commands coming from LV via the serial bus, he added the 0x1E option, which calls the temperature reading function, which he wrote:
    The code
    case 0x1E:  // OneWire Read
    OneWire_Read()
    break;

    This function sends a 0x44 temperature measurement command to the data line (“conversion”), waits for the conversion to complete, sends a 0xBE memory read command, reads, obtains a temperature reading from the received information and sends it to the serial bus:
    The code
    void OneWire_Read()
    {
    	OneWire ds(2);          // Create a OneWire Object "ds" on pin 2.  Hard coding for now, because I can't declare this in a case.
    	byte OneWireData[9];    // Defining stuff for the added OneWire function because I'm getting irritated with trying to make this fit into a case or function.
    	int Fract, Whole, Tc_100, SignBit, TReading;
    // Start the Conversion
    	ds.reset();      // Reset the OneWire bus in preparation for communication
    	ds.skip();       // Skip addressing, since there is only one sensor
    	ds.write(0x44);  // Send 44, the conversion command
    // Wait for the Conversion
    	delay(1000);     // Wait for the conversion to complete
    // Read back the data
    	ds.reset();                      // Reset the OneWire bus in preparation for communication
    	ds.skip();                       // Skip addressing, since there is only one sensor
    	ds.write(0xBE);                  // Send the "Read Scratchpad" command
    	for ( byte i = 0; i < 9; i++) { 
    			OneWireData[i] = ds.read();    // Read the 9 bytes into data[]
    		}
    // Scale the data
    	TReading = (OneWireData[1] << 8) + OneWireData[0];
    	SignBit = TReading & 0x8000;               // Mask out all but the MSB
    	if (SignBit)                                   // If the MSB is negative, take the Two's Compliment to make the reading negative
    		{
    			TReading = (TReading ^ 0xffff) + 1;          // 2's comp
    		}
    	Tc_100 = (6 * TReading) + TReading / 4;    // Scale by the sensitivity (0.0625°C per bit) and 100
    	Whole = Tc_100 / 100;                      // Split out the whole number portion of the reading
    	Fract = Tc_100 % 100;                      // Split out the fractional portion of the reading
     // Return the data serially
    	if (SignBit) {              // If the reading is negative, print a negative sign
    			Serial.print("-");
    		}
    	Serial.print(Whole);        // Print the whole number portion and a decimal
    	Serial.print(".");
    	if (Fract < 10) {            // if the fraction portion is less than .1, append a 0 decimal
    			Serial.print("0");
    		}
    	Serial.print(Fract);        // Otherwise print the fractional portion
    }

The proposed VI, in essence, just sends the hexadecimal number 1E to the specified serial interface port, waits for a response, and reads it:



It's quite simple.

We read one sensor manually


First of all, I edited LIFA_BASE.ino and LabVIEWInterface.ino according to the instructions and made a VI. Checked, everything works fine. Then I did something, which I later regretted. In the above topic on the LV forum, a couple of posts below, one of the participants suggested his version of the VI, which reads the temperature sensor readings, which consists, in fact, of only one sub-device - Send Receive.vi from the Arduino sub-palette:



Tempted by simplicity and not delving into the details, in my further experiments I used this simple version for nothing. No, no, everything is fine and fine, it works correctly, however, there is a certain subtlety associated with the differences between my scenario of the sensor-Arduino-LabVIEW chain and the scenario for which the VI was made from the forum. This subtlety subsequently gave me a certain amount of headache, but more on that later.

One of the features of the DS18B20 sensors is that each individual instance has its own unique 8-byte address (ROM code), wired into it during production. This theoretically allows you to hang an unlimited number of sensors on one 1-Wire line. To implement this feature, an addressing command for a specific sensor is provided.

To be addressed, you need to know the address. I found out the ROM codes of my sensors using the DS18x20_Temperature example from the OneWire library, and wrote them into five variables declared at the beginning of the program:
// DS18B20 temperature sensors' addresses:
byte sensor_1[8] = {0x28,0xFF,0xBE,0xCE,0x14,0x14,0x00,0x8A};
byte sensor_2[8] = {0x28,0xFF,0x42,0x43,0x15,0x14,0x00,0xE2};
byte sensor_3[8] = {0x28,0xFF,0xED,0x55,0x15,0x14,0x00,0x8F};
byte sensor_4[8] = {0x28,0xFF,0x3D,0x6E,0x15,0x14,0x00,0x0D};
byte sensor_5[8] = {0x28,0xFF,0x5E,0x66,0x15,0x14,0x00,0x4E};

In the proposed version, OneWire_Read does not receive any values. Add a parameter to it - the address of the sensor (byte array of 8 elements):

void OneWire_Read(byte addr[8])

Before each command is sent, we address the sensor:

// Start the Conversion
	ds.reset();                  // Reset the OneWire bus in preparation for communication
	ds.select(addr);       // Addressing
	ds.write(0x44);              // Send 44, the conversion command

// Read back the data
	ds.reset();                      // Reset the OneWire bus in preparation for communication
	ds.select(addr);           // Addressing
	ds.write(0xBE);                  // Send the "Read Scratchpad" command

and add the option for each sensor to the selection structure:
	/*********************************************************************************
	** OneWire temperature sensors reading
	*********************************************************************************/
		case 0x2E:  // sensor 1 read
			OneWire_Read(sensor_1);
		break;
		case 0x2F:  // sensor 2 read
			OneWire_Read(sensor_2);
		break;
		case 0x30:  // sensor 3 read
			OneWire_Read(sensor_3);
		break;
		case 0x31:  // sensor 4 read
			OneWire_Read(sensor_4);
		break;
		case 0x32:  // sensor 5 read
			OneWire_Read(sensor_5);
		break;

To test what happened, I made my small VI for a single survey of one sensor:



As you can see, I implemented the choice of a sensor for the survey through the case structure in the block diagram.

For the convenience of further use, I piled a small runway, as shown in the screenshot below, steamed and drew a nice icon for it and called DS18B20 Read.



Apart from the Arduino resource clusters and errors, the runway receives the sensor number for polling at the input and gives the temperature indication as a string to the output.

Hurrah! The tests were successful.

We read one sensor in automatic mode


Well, we can now manually poll one sensor. The next step is the cyclic polling of one sensor in automatic mode. To do this, I made the following block diagram:



To start, the interval is fixed, the program polls the sensor once a second and, after stopping the cycle, the user writes the collected data to the array. For convenience, I added a time stamp to each temperature display using the Get Date / Time String function.
Turn on, wait 20 seconds, stop ... And then the fun begins.
Viewing the array shows that the temperature is read only the first 5 times after starting the program, then only timestamps without temperature readings:



For a long time I could not understand what was the matter - there seemed to be no mistake on the LV side, the block diagram is outrageously simple, the Arduino sketch code is also correct, because in single manual polling mode it works without fail. What else could be? The Arduino board itself? Having watched her, I found the following. We start the program, the L LED on pin 13 flashes twice, then the RX LED flashes (the controller accepted a command for the temperature sensor sent by the PC), one second elapses (the sensor “converts” the temperature into bytes in its memory, the PC waits for a response from it), flashes TX LED (the controller received bytes from the sensor and sent them to the PC), the RX diode flashes again, the second elapses, TX flashes again, and so on in a circle until we stop the program. So, in my scheme, this kaleidoscope of lights lasted the first ~ 5 seconds,
All this catavasia led me to the idea that somewhere there was something wrong with the timing, and I started digging in this direction, changed the wait time in the VI, in the sketch, analyzed the sketch code literally by line, the block diagram of the VI elemental, but nothing helped. In the end, out of desperation, he gutted Send Receive.vi, because there was nowhere else to take the problem. Take a look at its block diagram:



Send Receive, as it should be, takes data, sends it in the specified direction and starts to wait. If no response is received within 100 milliseconds, it waits another 5 milliseconds, clears the output buffer and resends the data, for a total of 10 such attempts. Somewhere between the Send Receive, the microcontroller, and the main VI, an out of sync occurs and accumulates during operation, and because of this, at the sixth iteration of the sensor polling, there is some kind of mismatch between the sent and received commands that hangs the controller.

As experience shows, a simple-looking solution is not always the best, so I redid my DS18B20 Read.vi:



I admit honestly, I can’t say exactly what was the matter, there is not enough depth of understanding of the microcontroller’s interaction with the PC. But as a result of my attempts, the problem disappeared, and I did not go into it.

We read all the sensors in automatic mode


Being able to read one sensor in auto mode, gash reading all five at once is a matter of technology. To do this, I added another function to LabVIEWInterface.ino - OneWire_Read_All ():
The code
void OneWire_Read_All()
{
	OneWire ds(2);
	byte Data[9];
	int Fract, Whole, Tc_100, SignBit, TReading;
	ds.reset();
	ds.skip();       // Addressing to all sensors on the line
	ds.write(0x44);
	delay(1000);
// reading sensor 1
	ds.reset();
	ds.select(sensor_1);           // Addressing to sensor 1
	ds.write(0xBE);
	for ( byte i = 0; i < 9; i++)
	{ 
		Data[i] = ds.read();
    }
	TReading = (Data[1] << 8) + Data[0];
	SignBit = TReading & 0x8000;
	if (SignBit)
	{
		TReading = (TReading ^ 0xffff) + 1;
	}
	Tc_100 = (6 * TReading) + TReading / 4;
	Whole = Tc_100 / 100;
	Fract = Tc_100 % 100;
	if (SignBit)
	{
		Serial.print("-");
	}
	Serial.print(Whole);
	Serial.print(",");
	if (Fract < 10)
	{
		Serial.print("0");
	}
	Serial.print(Fract);
	Serial.print(" ");
// reading sensor 2    
	ds.reset();
	ds.select(sensor_2);           // Addressing to sensor 2
	ds.write(0xBE);
	for ( byte i = 0; i < 9; i++)
	{ 
		Data[i] = ds.read();
	}
	TReading = (Data[1] << 8) + Data[0];
	SignBit = TReading & 0x8000;
	if (SignBit)
	{
		TReading = (TReading ^ 0xffff) + 1;
	}
	Tc_100 = (6 * TReading) + TReading / 4;
	Whole = Tc_100 / 100;
	Fract = Tc_100 % 100;
	if (SignBit)
	{
		Serial.print("-");
	}
	Serial.print(Whole);
	Serial.print(",");
	if (Fract < 10)
	{
		Serial.print("0");
	}
	Serial.print(Fract);
	Serial.print(" ");
// reading sensor 3    
	ds.reset();
	ds.select(sensor_3);           // Addressing to sensor 3
	ds.write(0xBE);
	for ( byte i = 0; i < 9; i++)
	{ 
		Data[i] = ds.read();
	}
	TReading = (Data[1] << 8) + Data[0];
	SignBit = TReading & 0x8000;
	if (SignBit)
	{
		TReading = (TReading ^ 0xffff) + 1;
	}
	Tc_100 = (6 * TReading) + TReading / 4;
	Whole = Tc_100 / 100;
	Fract = Tc_100 % 100;
	if (SignBit)
	{
		Serial.print("-");
	}
	Serial.print(Whole);
	Serial.print(",");
	if (Fract < 10)
	{
		Serial.print("0");
	}
	Serial.print(Fract);
	Serial.print(" ");
// reading sensor 4    
	ds.reset();
	ds.select(sensor_4);           // Addressing to sensor 4
	ds.write(0xBE);
	for ( byte i = 0; i < 9; i++)
	{ 
		Data[i] = ds.read();
	}
	TReading = (Data[1] << 8) + Data[0];
	SignBit = TReading & 0x8000;
	if (SignBit)
	{
		TReading = (TReading ^ 0xffff) + 1;
	}
	Tc_100 = (6 * TReading) + TReading / 4;
	Whole = Tc_100 / 100;
	Fract = Tc_100 % 100;
	if (SignBit)
	{
		Serial.print("-");
	}
	Serial.print(Whole);
	Serial.print(",");
	if (Fract < 10)
	{
		Serial.print("0");
	}
	Serial.print(Fract);
	Serial.print(" ");
// reading sensor 5    
	ds.reset();
	ds.select(sensor_5);           // Addressing to sensor 5
	ds.write(0xBE);
	for ( byte i = 0; i < 9; i++)
	{ 
		Data[i] = ds.read();
	}
	TReading = (Data[1] << 8) + Data[0];
	SignBit = TReading & 0x8000;
	if (SignBit)
	{
		TReading = (TReading ^ 0xffff) + 1;
	}
	Tc_100 = (6 * TReading) + TReading / 4;
	Whole = Tc_100 / 100;
	Fract = Tc_100 % 100;
	if (SignBit)
	{
		Serial.print("-");
	}
	Serial.print(Whole);
	Serial.print(",");
	if (Fract < 10)
	{
		Serial.print("0");
	}
	Serial.print(Fract);
}

As you can see, it, with a few changes, is a function of reading a single sensor repeated 5 times.

I also had to slightly modify the DS18B20 Read.vi - made it universal, both for polling individual sensors (it receives a number from 1 to 5 at the input), and for everyone at once (6 at the input). I also changed the number of bytes read from the buffer, because when polling all sensors immediately at the output of the VI, the line is almost 6 times longer, and increased the buffer polling interval:



Hurray, comrades! Everything works exactly as I wanted.

Calibration


It would seem that everything is ready, here you can calm down, but during the tests all five sensors, placed in the same conditions (a glass of water), gave slightly different readings. Therefore, they had to be calibrated.
To do this, we needed: a mercury thermometer with a division value of 0.01 degrees Celsius, a laboratory rack with a foot, a glass, a little ice from the freezer, an electric kettle and water. The improvised installation looked like this:



I apologize for the quality of the photos and for the mess in the laboratory.

For several temperatures, the readings of a mercury thermometer and sensors were recorded, and calibration curves were constructed for each sensor.

As an example, a calibration curve for sensor No. 1.



According to the parameters of the obtained curves, I made calibration corrections to the data produced by the program.
Also, using the same “setup”, the error given by the braid was estimated by comparing the readings of the sensors and a mercury thermometer. For different sensors at different temperatures, it differs slightly and averages 0.08 degrees Celsius.

Finishing touches


The LIFA interface for working with Arduino provides a lot of possibilities - working with LCD displays, servomotors, IR control, etc., all this is useful, but in my case it is completely unnecessary, and therefore I pretty drastically cut down the contents of LabVIEWInterface. ino , LIFA_BASE.ino , LabVIEWInterface.h and the LIFA_Base folders, removing everything unnecessary from there. I will not give listings here, if anyone wants to look, contact me, I will provide all the sources with pleasure.

For the control program, I made just such a front panel:



The Arduino shawl was packaged in a heat-shrink tube sealed from the ends for environmental protection:





The device is ready:



Summary


Cost of components and materials:
  1. Arduino Nano - 1,900 rubles;
  2. 5 DS18B20 temperature sensors - 1950 rubles;
  3. 10 m cable - 150 rubles;
  4. Trivia (heat shrink, cable ties, ...) - 200 rubles;

In total - 4200 rubles.

Now let's think about it. For sale there are factory heat mowers, it is easy to google, for example, "TK-10/10 thermocosa" with an average cost of 13,000 rubles. You may ask: “But what for was it soared if there are analogues of industrial production of comparable cost, giving the same or negligible accuracy, obviously better-tuned, more reliable and high-quality?” I will answer, there are several reasons for this:

  1. /*Говоря не о серьёзной научной аппаратуре, а об устройствах, подобных описываемому выше.*/ Покупая готовое решение, ты вынужден верить цифрам характеристик, которые указал производитель. Это нормально при применении прибора на производстве или в быту, но не для научных целей. Я не говорю, что производитель намеренно даёт ложные сведения, но, как правило, ты ничего не знаешь о тонкостях внутреннего устройства, о методиках оценки параметров прибора, использованных при его изготовлении, а они могут оказаться неточными или содержать неуместные допущения. В общем, вы поняли, главный принцип научного мировоззрения — «Ничего не принимай на веру». Другое дело, если собираешь прибор сам буквально по детальке, сам задаёшь логику его работы и оцениваешь его точность по выбранным тобой методам.
  2. From an educational point of view, making a braid has brought valuable experience in working with a soldering iron, programming Arduino and understanding its connection with a computer through LabVIEW, especially in light of the fact that I continue to study the Arduino-LV-PC bundle in a project that I switched to at the end of this.
  3. To a lesser extent, but the issue of value also mattered.


Thank you all for your attention! If you have any questions / suggestions / criticism, I’m always glad to listen, I will provide the source code for the sketches and VI-shek with pleasure, as I wrote above, please contact.

PS My programming skills have not gone far from “Hello world!”, So do not judge strictly if I used some terms inaccurately or not quite for the intended purpose.

Also popular now: