Search for symbols, directories, files, pages or
topics. You can omit any prefix from the symbol or file path; adding a
: or / suffix lists all members of given symbol or
directory.
Use ↓
/ ↑ to navigate through the list,
Enter to go.
Tab autocompletes common prefix, you can
copy a link to the result using ⌘L while ⌘M produces a Markdown link.
Here we'll walk step-by-step through how the SDI-12 library (and NeoSWSerial) create a character from the ISR. Unlike SoftwareSerial which listens for a start bit and then halts all program and other ISR execution until the end of the character, this library grabs the time of the interrupt, does some quick math, and lets the processor move on. The logic of creating a character this way is harder for a person to follow, but it pays off because we're not tieing up the processor in an ISR that lasts for 8.33ms for each character. [10 bits @ 1200 bits/s] For a person, that 8.33ms is trivial, but for even a "slow" 8MHz processor, that's over 60,000 ticks sitting idle per character.
So, let's look at what's happening.
How a Character Looks in SDI-12
First we need to keep in mind the specifications of SDI-12:
We use inverse logic that means a "1" bit is at LOW level and a "0" bit is HIGH level.
characters are sent as 10 bits
1 start bit, which is always a 0/HIGH
7 data bits
1 parity bit
1 stop bit, which is always 1/LOW
Static Variables we Need
And lets remind ourselves of the static variables we're using to store states:
prevBitTCNT stores the time of the previous RX transition in micros
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
WAITING-FOR-START-BIT is a mask for the rxState while waiting for a start bit, it's set to 0b11111111
rxMask is 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
rxValue is the value of the character being built
Following the Mask
Waiting for a Start Bit
The rxState, rxMask, and rxValue all work together to form a character. When we're waiting for a start bit rxValue is empty, rxMask has only the bottom bit set, and rxState is set to WAITING-FOR-START-BIT:
1| rxValue: | 0 0 0 0 0 0 0 0 |
2| -------- | ----------------------------- |
3| rxMask: | 0 0 0 0 0 0 0 1 |
4| rxState: | 1 1 1 1 1 1 1 1 |
The Start of a Character
After we get a start bit, the startChar() function creates a blank slate for the new character, so our values are:
1| rxValue: | 0 0 0 0 0 0 0 0 |
2| -------- | ----------------------------- |
3| rxMask: | 0 0 0 0 0 0 0 1 |
4| rxState: | 0 0 0 0 0 0 0 0 |
The Interrupt Fires
When an interrupts is received, we use capture the time if the interrupt in thisBitTCNT. Then we subtract prevBitTCNT from thisBitTCNT and use the bitTimes() function to calculate how many bit-times have passed between this interrupt and the previous one. (There's also a fudge factor in this calculation we call the rxWindowWidth.)
Bit by Bit
For each bit time that passed, we apply the rxMask to the rxValue.
Keep in mind multiple bit times can pass between interrupts - this happens any time there are two (or more) high or low bits in a row.
We also leave time for the (high) start and (low) stop bit, but do anything with the rxState, rxMask, or rxValue for those bits.
A LOW/1 Bit
if the data bit received is LOW (1) we do an |= (bitwise OR) between the rxMask and the rxValue
1| rxValue: | 0 0 0 0 0 0 0 1 |
2| -------- | ----------------------------- |^- bit-wise or puts the one
3| rxMask: | 0 0 0 0 0 0 0 1 | from the rxMask into
4| rxState: | 0 0 0 0 0 0 0 0 | the rxValue
A HIGH/0 Bit
if the data bit received is HIGH (0) we do nothing
6| ----------------- | ^ falls off the top | ------- added to the bottom ^ |
A Finished Character
After 8 bit times have passed, we should have a fully formed character with 8 bits of data (7 of the character + 1 parity). The rxMask will have the one in the top bit. And the rxState will be filled - which just happens to be the value of WAITING-FOR-START-BIT for the next character.
1| rxValue: | ? ? ? ? ? ? ? ? |
2| -------- | ----------------------------- |
3| rxMask: | 1 0 0 0 0 0 0 0 |
4| rxState: | 1 1 1 1 1 1 1 1 |
The Full Interrupt Function
Understanding how the masking creates the character, you should now be able to follow the full interrupt function below.
1// Creates a blank slate of bits for an incoming character
2voidSDI12::startChar(){
3rxState=0x00;// 0b00000000, got a start bit
4rxMask=0x01;// 0b00000001, bit mask, lsb first
5rxValue=0x00;// 0b00000000, RX character to be, a blank slate
6}// startChar
7
8// The actual interrupt service routine
9voidSDI12::receiveISR(){
10// time of this data transition (plus ISR latency)
11sdi12timer-tthisBitTCNT=READTIME;
12
13uint8-tpinLevel=digitalRead(-dataPin);// current RX data level
14
15// Check if we're ready for a start bit, and if this could possibly be it.
16if(rxState==WAITING-FOR-START-BIT){
17// If we are waiting for a start bit and the pin is low it's not a start bit, exit
18// Inverse logic start bit = HIGH
19if(pinLevel==LOW){return;}
20// If the pin is HIGH, this should be a start bit.
21// Thus startChar(), which sets the rxState to 0, create an empty character, and a
22// new mask with a 1 in the lowest place
23startChar();
24}else{
25// If we're not waiting for a start bit, it's because we're in the middle of an
26// incomplete character and therefore this change in the pin state must be from a
27// data, parity, or stop bit.
28
29// Check how many bit times have passed since the last change