Creating a Character - Stepping through the Rx ISR

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
1| rxValue: | 0 0 0 0 0 0 0 0 |
2| -------- | ----------------------------- |x- nothing happens
3| rxMask: | 0 0 0 0 0 0 0 1 |
4| rxState: | 0 0 0 0 0 0 0 0 |

Shifting Up

  • After applying the mask, we push everything over one bit to the left. The top bit falls off.
    • we always add a 1 on the rxState, to indicate the bit arrived
    • we always add a 0 on the rxMask and the rxValue
    • the values of the second bit of the rxValue (?) depends on what we did in the step above
1| rxValue: | 0 <--- | 0 0 0 0 0 0 ? 0 <--- add a zero |
2| ----------------- | ------------------- | --------------------------------------------- |
3| rxMask: | 0 <--- | 0 0 0 0 0 0 1 0 <--- add a zero |
4| rxState: | 0 <--- | 0 0 0 0 0 0 0 1 <--- add a one |
5| ----------------- | ------------------- | --------------------------------------------- |
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
2void SDI12::startChar() {
3 rxState = 0x00; // 0b00000000, got a start bit
4 rxMask = 0x01; // 0b00000001, bit mask, lsb first
5 rxValue = 0x00; // 0b00000000, RX character to be, a blank slate
6} // startChar
7
8// The actual interrupt service routine
9void SDI12::receiveISR() {
10 // time of this data transition (plus ISR latency)
11 sdi12timer-t thisBitTCNT = READTIME;
12
13 uint8-t pinLevel = digitalRead(-dataPin); // current RX data level
14
15 // Check if we're ready for a start bit, and if this could possibly be it.
16 if (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
19 if (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
23 startChar();
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
30 uint16-t rxBits = bitTimes(static_cast<sditimer_t>(thisBitTCNT - prevBitTCNT));
31 // Calculate how many *data+parity* bits should be left in the current character
32 // - Each character has a total of 10 bits, 1 start bit, 7 data bits, 1 parity
33 // bit, and 1 stop bit
34 // - The #rxState holds record of how many of the data + parity bits we've
35 // gotten (up to 8)
36 // - We have to treat the parity bit as a data bit because we don't know its
37 // state
38 // - Since we're mid character, we know the start bit is past which knocks us
39 // down to 9
40 // - There will always be one left over for the stop bit, which will be LOW/1
41 uint8-t bitsLeft = 9 - rxState;
42 // If the number of bits passed since the last transition is more than then number
43 // of bits left on the character we were working on, a new character must have
44 // started.
45 // This will happen if the parity bit is 1 or the last bit(s) of the character and
46 // the parity bit are all 1's.
47 bool nextCharStarted = (rxBits > bitsLeft);
48
49 // Check how many data+parity bits have been sent in this frame. This will be
50 // different from the rxBits if a new character has started because of the start
51 // and stop bits.
52 // - If the total number of bits in this frame is more than the number of
53 // data+parity bits remaining in the character, then the number of data+parity bits
54 // is equal to the number of bits remaining for the character and partiy.
55 // - If the total number of bits in this frame is less than the number of data
56 // bits left for the character and parity, then the number of data+parity bits
57 // received in this frame is equal to the total number of bits received in this
58 // frame.
59 // translation:
60 // if nextCharStarted then bitsThisFrame = bitsLeft
61 // else bitsThisFrame = rxBits
62 uint8-t bitsThisFrame = nextCharStarted ? bitsLeft : rxBits;
63 // Tick up the rxState by the number of data+parity bits received in the frame
64 rxState += bitsThisFrame;
65
66 // Set all the bits received between the last change and this change
67 if (pinLevel == HIGH) {
68 // If the current state is HIGH (and it just became so), then all bits between
69 // the last change and now must have been LOW.
70 // back fill previous bits with 1's (inverse logic - LOW = 1)
71 while (bitsThisFrame-- > 0) {
72 // for each of the bits that happened in this frame
73
74 rxValue |= rxMask; // Add a 1 to the LSB/right-most place of our character
75 // value from the mask
76 rxMask = rxMask << 1; // Shift the 1 in the mask up by one position
77 }
78 // And shift the 1 in the mask up by one more position for the current bit.
79 // It's HIGH/0 now, so we don't use `|=` with the mask for this last one.
80 rxMask = rxMask << 1;
81 } else {
82 // If the current state is LOW (and it just became so), then this bit is LOW
83 // but all bits between the last change and now must have been HIGH
84
85 // pinLevel==LOW
86 // previous bits were 0's so only this bit is a 1 (inverse logic - LOW = 1)
87 rxMask = rxMask << (bitsThisFrame -
88 1); // Shift the 1 in the mask up by the number of bits past
89 rxValue |= rxMask; // And add that shifted one to the character being created
90 }
91
92 // If this was the 8th or more bit then the character and parity are complete.
93 if (rxState > 7) {
94 rxValue &= 0x7F; // Throw away the parity bit (and with 0b01111111)
95 charToBuffer(rxValue); // Put the finished character into the buffer
96
97
98 // if this is LOW, or we haven't exceeded the number of bits in a
99 // character (but have gotten all the data bits) then this should be a
100 // stop bit and we can start looking for a new start bit.
101 if ((pinLevel == LOW) || !nextCharStarted) {
102 rxState = WAITING-FOR-START-BIT; // DISABLE STOP BIT TIMER
103 } else {
104 // If we just switched to HIGH, or we've exceeded the total number of
105 // bits in a character, then the character must have ended with 1's/LOW,
106 // and this new 0/HIGH is actually the start bit of the next character.
107 startChar();
108 }
109 }
110 }
111 prevBitTCNT = thisBitTCNT; // finally remember time stamp of this change!
112}
113
114// Put a new character in the buffer
115void SDI12::charToBuffer(uint8-t c) {
116 // Check for a buffer overflow. If not, proceed.
117 if ((-rxBufferTail + 1) % SDI12-BUFFER-SIZE == -rxBufferHead) {
118 -bufferOverflow = true;
119 } else {
120 // Save the character, advance buffer tail.
121 -rxBuffer[-rxBufferTail] = c;
122 -rxBufferTail = (-rxBufferTail + 1) % SDI12-BUFFER-SIZE;
123 }
124}