SDI-12 for Arduino > Classes > SDI12
SDI-12 for Arduino > Modules > SDI12

#include <SDI12.h>

SDI12 class

The main class for SDI 12 instances.

Data Line States

Functions for maintaining the proper data line state.

The Arduino is responsible for managing communication with the sensors. Since all the data transfer happens on the same line, the state of the data line is very important.

Specifications

Per the SDI-12 specification, the voltage ranges for SDI-12 are:

  • When the pin is in the SDI12_HOLDING state, it is holding the line LOW so that interference does not unintentionally wake the sensors up. The interrupt is disabled for the dataPin, because we are not expecting any SDI-12 traffic.
  • In the SDI12_TRANSMITTING state, we would like exclusive control of the Arduino, so we shut off all interrupts, and vary the voltage of the dataPin in order to wake up and send commands to the sensor.
  • In the SDI12_LISTENING state, we are waiting for a sensor to respond, so we drop the voltage level to LOW and relinquish control (INPUT).
  • If we would like to disable all SDI-12 functionality, then we set the system to the SDI12_DISABLED state, removing the interrupt associated with the dataPin. For predictability, we set the pin to a LOW level high impedance state (INPUT).

As a Table

Summarized in a table:

StateInterruptsPin ModePin Level
SDI12_DISABLEDPin DisableINPUT
SDI12_ENABLEDPin DisableINPUT
SDI12_HOLDINGPin DisableOUTPUTLOW
SDI12_TRANSMITTINGAll/Pin DisableOUTPUTVARYING
SDI12_LISTENINGAll EnableINPUT

Sequencing

Generally, this flow of line states is acceptable:

HOLDING --> TRANSMITTING --> LISTENING --> TRANSMITTING --> LISTENING

If you have interference, you should force a hold, using forceHold(). The flow would then be:

HOLDING --> TRANSMITTING --> LISTENING --> done reading, forceHold() ---> HOLDING

enum SDI12_STATES { SDI12_DISABLED, SDI12_ENABLED, SDI12_HOLDING, SDI12_TRANSMITTING, SDI12_LISTENING }
The various SDI-12 line states.
using SDI12_STATES = enum SDI12::SDI12_STATES
The various SDI-12 line states.
void forceHold()
Set line state to SDI12_HOLDING.
void forceListen()
Set line state to SDI12_LISTENING.

Static member variables

These are constants that apply to all SDI-12 instances.

static SDI12* _activeObject
static pointer to active SDI12 instance
static SDI12Timer sdi12timer
The SDI12Timer instance to use for checking bit reception times.
static const uint16_t bitWidth_micros
The size of a bit in microseconds.
static const uint16_t lineBreak_micros
The required "break" before sending commands, >= 12ms.
static const uint16_t marking_micros
The required mark before a command or response, >= 8.33ms.
static const uint8_t txBitWidth
the width of a single bit in "ticks" of the cpu clock.
static const uint8_t rxWindowWidth
A fudge factor to make things work.
static const uint8_t bitsPerTick_Q10
The number of bits per tick, shifted by 2^10.
static const uint8_t WAITING_FOR_START_BIT
A mask for the rxState while waiting for a start bit; 0b11111111.
static uint16_t prevBitTCNT
Stores the time of the previous RX transition in micros.
static uint8_t rxState
Tracks how many bits are accounted for on an incoming character.
static uint8_t rxMask
a bit mask for building a received character
static uint8_t rxValue
the value of the character being built

Buffer Setup

Creating a circular buffer for incoming data.

The buffer is used to store characters from the SDI-12 data line. Characters are read into the buffer when an interrupt is received on the data line. The buffer uses a circular implementation with pointers to both the head and the tail. All SDI-12 instances share the same buffer.

The default buffer size is the maximum length of a response to a normal SDI-12 command, which is 81 characters:

  • address is a single (1) character
  • values has a maximum value of 75 characters
  • CRC is 3 characters
  • CR is a single character
  • LF is a single character

For more information on circular buffers: http://en.wikipedia.org/wiki/Circular_buffer

static uint8_t _rxBuffer
A single incoming character buffer for ALL SDI-12 objects (Rx buffer)
static uint8_t _rxBufferTail
Index of buffer head. (unsigned 8-bit integer, can map from 0-255)
static uint8_t _rxBufferHead
Index of buffer tail. (unsigned 8-bit integer, can map from 0-255)
bool _bufferOverflow
The buffer overflow status.

Constructor, Destructor, Begins, and Setters

These functions set up the SDI-12 object and prepare it for use.

int8_t _dataPin
reference to the data pin
int16_t TIMEOUT
The value to return if a parse or read times out with no return from the sensor.
SDI12()
Construct a new SDI12 instance with no data pin set.
SDI12(int8_t dataPin) explicit
Construct a new SDI12 with the data pin set.
~SDI12()
Destroy the SDI12 object.
void begin()
Begin the SDI-12 object.
void begin(int8_t dataPin)
Set the SDI12::_datapin and begin the SDI-12 object.
void end()
Disable the SDI-12 object (but do not destroy it).
void setTimeoutValue(int16_t value)
Set the value to return if a parse int or parse float times out with no return from the sensor.
int8_t getDataPin()
Get the data pin for the current SDI-12 instance.
void setDataPin(int8_t dataPin)
Set the data pin for the current SDI-12 instance.

Reading from the SDI-12 Buffer

These functions are for reading incoming data stored in the SDI-12 buffer.

int available() override
Return the number of bytes available in the Rx buffer.
int peek() override
Reveal next byte in the Rx buffer without consuming it.
void clearBuffer()
Clear the Rx buffer by setting the head and tail pointers to the same value.
int read() override
Return next byte in the Rx buffer, consuming it.
void flush() override
Wait for sending to finish - because no TX buffering, does nothing.
long parseInt(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR)
Return the first valid (long) integer value from the current position.
float parseFloat(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR)
Return the first valid float value from the current position.
int peekNextDigit(LookaheadMode lookahead, bool detectDecimal) protected
Return the next numeric digit in the stream or -1 if timeout.

Using more than one SDI-12 Object

Functions needed for multiple instances of the SDI12 class.

This library is allows for multiple instances of itself running on the same or different pins. SDI-12 can support up to 62 sensors on a single pin/bus, so it is not necessary to use an instance for each sensor.

Because we are using pin change interrupts there can only be one active object at a time (since this is the only reliable way to determine which pin the interrupt occurred on). The active object is the only object that will respond properly to interrupts. However promoting another instance to Active status does not automatically remove the interrupts on the other pin. For proper behavior it is recommended to use this pattern:

mySDI12.forceHold();
myOtherSDI12.setActive();
bool setActive()
Set this instance as the active SDI-12 instance.
bool isActive()
Check if this instance is active.

Waking Up and Talking To Sensors

These functions are needed to communicate with SDI-12 sensors (slaves) or an SDI-12 datalogger (master).

size_t write(uint8_t byte) virtual
Write out a byte on the SDI-12 line.
void sendCommand(String& cmd, int8_t extraWakeTime = SDI12_WAKE_DELAY)
Send a command out on the data line, acting as a datalogger (master)
void sendCommand(const char* cmd, int8_t extraWakeTime = SDI12_WAKE_DELAY)
Send a command out on the data line, acting as a datalogger (master)
void sendCommand(FlashString cmd, int8_t extraWakeTime = SDI12_WAKE_DELAY)
Send a command out on the data line, acting as a datalogger (master)
void sendResponse(String& resp)
Send a response out on the data line (for slave use)
void sendResponse(const char* resp)
Send a response out on the data line (for slave use)
void sendResponse(FlashString resp)
Send a response out on the data line (for slave use)

Interrupt Service Routine

Functions for handling interrupts - responding to changes on the data line and converting them to characters in the Rx buffer.

static void handleInterrupt()
Intermediary used by the ISR - passes off responsibility for the interrupt to the active object.

Enum documentation

enum SDI12::SDI12_STATES

The various SDI-12 line states.

Enumerators
SDI12_DISABLED

SDI-12 is disabled, pin mode INPUT, interrupts disabled for the pin

SDI12_ENABLED

SDI-12 is enabled, pin mode INPUT, interrupts disabled for the pin

SDI12_HOLDING

The line is being held LOW, pin mode OUTPUT, interrupts disabled for the pin

SDI12_TRANSMITTING

Data is being transmitted by the SDI-12 master, pin mode OUTPUT, interrupts disabled for the pin

SDI12_LISTENING

The SDI-12 master is listening for a response from the slave, pin mode INPUT, interrupts enabled for the pin



Function documentation

void forceHold()

Set line state to SDI12_HOLDING.

A public function which forces the line into a "holding" state. This is generally unneeded, but for deployments where interference is an issue, it should be used after all expected bytes have been returned from the sensor.


void forceListen()

Set line state to SDI12_LISTENING.

A public function which forces the line into a "listening" state. This may be needed for implementing a slave-side device, which should relinquish control of the data line when not transmitting.


SDI12()

Construct a new SDI12 instance with no data pin set.

Before using the SDI-12 instance, the data pin must be set with SDI12::setDataPin(dataPin) or SDI12::begin(dataPin). This empty constructor is provided for easier integration with other Arduino libraries.

When the constructor is called it resets the buffer overflow status to FALSE.


SDI12(int8_t dataPin) explicit

Construct a new SDI12 with the data pin set.

Parameters
dataPin The data pin's digital pin number

When the constructor is called it resets the buffer overflow status to FALSE and assigns the pin number "dataPin" to the private variable "_dataPin".


~SDI12()

Destroy the SDI12 object.

When the destructor is called, it's main task is to disable any interrupts that had been previously assigned to the pin, so that the pin will behave as expected when used for other purposes. This is achieved by putting the SDI-12 object in the SDI12_DISABLED state. After disabling interrupts, the pointer to the current active SDI-12 instance is set to null if it had pointed to the destroyed object. Finally, for AVR board, the timer prescaler is set back to whatever it had been prior to creating the SDI-12 object.


void begin()

Begin the SDI-12 object.

This is called to begin the functionality of the SDI-12 object. It sets the object as the active object, sets the stream timeout to 150ms to match SDI-12 specs, sets the timeout return value to SDI12::TIMEOUT, and configures the timer prescaler.


void begin(int8_t dataPin)

Set the SDI12::_datapin and begin the SDI-12 object.

Parameters
dataPin The data pin's digital pin number

This is called to begin the functionality of the SDI-12 object. It sets the object as the active object, sets the stream timeout to 150ms to match SDI-12 specs, sets the timeout return value to SDI12::TIMEOUT, and configures the timer prescaler. If the SDI-12 instance is created using the empty constuctor, this must be used to set the data pin.


void end()

Disable the SDI-12 object (but do not destroy it).

Set the SDI-12 state to disabled, set the pointer to the current active instance to null, and then, for AVR boards, unset the timer prescaler.

This can be called to temporarily cease all functionality of the SDI-12 object. It is not as harsh as destroying the object with the destructor, as it will maintain the memory buffer.


void setTimeoutValue(int16_t value)

Set the value to return if a parse int or parse float times out with no return from the sensor.

Parameters
value the value to return on timeout

The "standard" timeout return for an Arduino stream object when no character is available in the Rx buffer is "0." For enviromental sensors (the typical SDI-12 users) 0 is a common result value. To better distinguish between a timeout because of no sensor response and a true zero return, the timeout should be set to some value that is NOT a possible return from that sensor. If the timeout is not set, -9999 is used.


int8_t getDataPin()

Get the data pin for the current SDI-12 instance.

Returns int8_t the data pin number

void setDataPin(int8_t dataPin)

Set the data pin for the current SDI-12 instance.

Parameters
dataPin The data pin's digital pin number

int available() override

Return the number of bytes available in the Rx buffer.

Returns int The number of characters in the buffer

available() is a public function that returns the number of characters available in the Rx buffer.

To understand how: _rxBufferTail + SDI12_BUFFER_SIZE - _rxBufferHead) % SDI12_BUFFER_SIZE; accomplishes this task, we will use a few examples.

To start take the buffer below that has SDI12_BUFFER_SIZE = 10. The message "abc" has been wrapped around (circular buffer).

_rxBufferTail = 1 // points to the '-' after c
_rxBufferHead = 8 // points to 'a'

[ c ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ a ] [ b ]

The number of available characters is (1 + 10 - 8) % 10 = 3

The '' or modulo operator finds the remainder of division of one number by another. In integer arithmetic 3 / 10 = 0, but has a remainder of 3. We can only get the remainder by using the the modulo ''. 3 % 10 = 3. This next case demonstrates more clearly why the modulo is used.

_rxBufferTail = 4 // points to the '-' after c
_rxBufferHead = 1 // points to 'a'

[ a ] [ b ] [ c ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ]

The number of available characters is (4 + 10 - 1) % 10 = 3

If we did not use the modulo we would get either ( 4 + 10 - 1 ) = 13 characters or ( 4 + 10 - 1 ) / 10 = 1 character. Obviously neither is correct.

If there has been a buffer overflow, available() will return -1.


int peek() override

Reveal next byte in the Rx buffer without consuming it.

Returns int The next byte in the character buffer.

peek() is a public function that allows the user to look at the character that is at the head of the buffer. Unlike read() it does not consume the character (i.e. the index addressed by _rxBufferHead is not changed). peek() returns -1 if there are no characters to show.


void clearBuffer()

Clear the Rx buffer by setting the head and tail pointers to the same value.

clearBuffer() is a public function that clears the buffers contents by setting the index for both head and tail back to zero.


int read() override

Return next byte in the Rx buffer, consuming it.

Returns int The next byte in the character buffer.

read() returns the character at the current head in the buffer after incrementing the index of the buffer head. This action 'consumes' the character, meaning it can not be read from the buffer again. If you would rather see the character, but leave the index to head intact, you should use peek();


long parseInt(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR)

Return the first valid (long) integer value from the current position.

Parameters
lookahead the mode to use to look ahead in the stream, default is LookaheadMode::SKIP_ALL
ignore a character to ignore in the stream, default is '\x01'
Returns long The first valid integer in the stream

The value of lookahead determines how parseInt looks ahead in the stream. See LookaheadMode enumeration at the top of the file. Lookahead is terminated by the first character that is not a valid part of an integer. Once parsing commences, 'ignore' will be skipped in the stream.


float parseFloat(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR)

Return the first valid float value from the current position.

Parameters
lookahead the mode to use to look ahead in the stream, default is LookaheadMode::SKIP_ALL
ignore a character to ignore in the stream, default is '\x01'
Returns long The first valid float in the stream

The value of lookahead determines how parseInt looks ahead in the stream. See LookaheadMode enumeration at the top of the file. Lookahead is terminated by the first character that is not a valid part of an integer. Once parsing commences, 'ignore' will be skipped in the stream.


int peekNextDigit(LookaheadMode lookahead, bool detectDecimal)

Return the next numeric digit in the stream or -1 if timeout.

Parameters
lookahead the mode to use to look ahead in the stream
detectDecimal True to accept a decimal point ('.') as part of a number
Returns int The next numeric digit in the stream

bool setActive()

Set this instance as the active SDI-12 instance.

Returns bool True indicates that the current SDI-12 instance was not formerly the active one and now is. False indicates that the current SDI-12 instance is already the active one and the state was not changed.

A method for setting the current object as the active object; returns TRUE if the object was not formerly the active object and now is.

  • Promoting an inactive to the active instance will start it in the SDI12_HOLDING state and return TRUE.
  • Otherwise, if the object is currently the active instance, it will remain unchanged and return FALSE.

bool isActive()

Check if this instance is active.

Returns bool True indicates that the curren SDI-12 instace is the active one.

isActive() is a method for checking if the object is the active object. Returns true if the object is currently the active object, false otherwise.


size_t write(uint8_t byte) virtual

Write out a byte on the SDI-12 line.

Parameters
byte The character to write
Returns size_t The number of characters written

Sets the state to transmitting, writes a character, and then sets the state back to listening. This function must be implemented as part of the Arduino Stream instance, but is NOT intenteded to be used for SDI-12 objects. Instead, use the SDI12::sendCommand() or SDI12::sendResponse() functions.


void sendCommand(String& cmd, int8_t extraWakeTime = SDI12_WAKE_DELAY)

Send a command out on the data line, acting as a datalogger (master)

Parameters
cmd the command to send
extraWakeTime The amount of additional time in milliseconds that the sensor takes to wake before being ready to receive a command. Default is 0ms - meaning the sensor is ready for a command by the end of the 12ms break. Per protocol, the wake time must be less than 100 ms.

A publicly accessible function that sends a break to wake sensors and sends out a command byte by byte on the data line.


void sendCommand(const char* cmd, int8_t extraWakeTime = SDI12_WAKE_DELAY)

Send a command out on the data line, acting as a datalogger (master)

Parameters
cmd the command to send
extraWakeTime The amount of additional time in milliseconds that the sensor takes to wake before being ready to receive a command. Default is 0ms - meaning the sensor is ready for a command by the end of the 12ms break. Per protocol, the wake time must be less than 100 ms.

A publicly accessible function that sends a break to wake sensors and sends out a command byte by byte on the data line.


void sendCommand(FlashString cmd, int8_t extraWakeTime = SDI12_WAKE_DELAY)

Send a command out on the data line, acting as a datalogger (master)

Parameters
cmd the command to send
extraWakeTime The amount of additional time in milliseconds that the sensor takes to wake before being ready to receive a command. Default is 0ms - meaning the sensor is ready for a command by the end of the 12ms break. Per protocol, the wake time must be less than 100 ms.

A publicly accessible function that sends a break to wake sensors and sends out a command byte by byte on the data line.


void sendResponse(String& resp)

Send a response out on the data line (for slave use)

Parameters
resp the response to send

A publicly accessible function that sends out an 8.33 ms marking and a response byte by byte on the data line. This is needed if the Arduino is acting as an SDI-12 device itself, not as a recorder for another SDI-12 device.


void sendResponse(const char* resp)

Send a response out on the data line (for slave use)

Parameters
resp the response to send

A publicly accessible function that sends out an 8.33 ms marking and a response byte by byte on the data line. This is needed if the Arduino is acting as an SDI-12 device itself, not as a recorder for another SDI-12 device.


void sendResponse(FlashString resp)

Send a response out on the data line (for slave use)

Parameters
resp the response to send

A publicly accessible function that sends out an 8.33 ms marking and a response byte by byte on the data line. This is needed if the Arduino is acting as an SDI-12 device itself, not as a recorder for another SDI-12 device.


static void handleInterrupt()

Intermediary used by the ISR - passes off responsibility for the interrupt to the active object.

On espressif boards (ESP8266 and ESP32), the ISR must be stored in IRAM



Variable documentation

static const uint16_t SDI12::bitWidth_micros

The size of a bit in microseconds.

1200 baud = 1200 bits/second ~ 833.333 µs/bit


static uint8_t SDI12::rxState

Tracks how many bits are accounted for on an incoming character.

  • if 0: indicates that we got a start bit
  • if >0: indicates the number of bits received

static uint8_t SDI12::rxMask

a bit mask for building a received character

The mask has a single bit set, in the place of the active bit based on the rxState.


static uint8_t SDI12::_rxBuffer

A single incoming character buffer for ALL SDI-12 objects (Rx buffer)

Increasing the buffer size will use more RAM. If you exceed 256 characters, be sure to change the data type of the index to support the larger range of addresses. To adjust the size of the buffer, change the value of SDI12_BUFFER_SIZE in the header file.


int16_t SDI12::TIMEOUT

The value to return if a parse or read times out with no return from the sensor.

The timeout return for an Arduino stream object when no character is available in the Rx buffer is "0." For enviromental sensors (the typical SDI-12 users) 0 is a common result value. To better distinguish between a timeout because of no sensor response and a true zero return, the timeout should be set to some value that is NOT a possible return from that sensor. If the timeout is not set, -9999 is used.