Problems with software serial code

I’ve been playing around with getting an XBee device talking to my Arduino via a software serial implementation. I understand the main difference between this approach and a hardware serial (UART) approach, namely the buffering and asynch nature of the UART processing. However, if I accept the limitation of no buffering and being ‘stuck’ in a DigitalRead until something arrives at the Arduino Rx pin, then I can’t see why there should be much difference in reliablity between the two approaches.

However, my recent experience tells me different. I am using the following code snippet to transmit to the XBee.

void SWprint(int data)
#define bit9600Delay 84  
#define halfBit9600Delay 42
#define bit4800Delay 188 
#define halfBit4800Delay 94 

byte rx = 6;
byte tx = 7;

void SWprint(int data)
{
  byte mask;
  //startbit
  digitalWrite(tx,LOW);
  delayMicroseconds(bit9600Delay);
  for (mask = 0x01; mask>0; mask <<= 1) {
    if (data & mask){ // choose bit
     digitalWrite(tx,HIGH); // send 1
    }
    else{
     digitalWrite(tx,LOW); // send 0
    }
    delayMicroseconds(bit9600Delay);
  }
  //stop bit
  digitalWrite(tx, HIGH);
  delayMicroseconds(bit9600Delay);
}

When I look at this code, the only area I see that could/would likely go wrong, is the timing delays/values. In other words, the values chosen for bit9600Delay for example, would need to be precisely calculated from the processor clock speed and the number of clock cycles taken by particular processing instructions. If all this was correct, then is there any other reason why the software serial approach shouldn’t work just as well as the UART? The reason I’m asking this is that I am having problems getting my Arduino to talk to my XBee device using software serial but if I use the hardware serial ports (0 and 1) on the Arduino instead, everything works just fine.

I’m looking for some guidance on what might be going wrong here.

Thanks for any help.

Hello.

When you say you’re having problems using software serial, do you mean that it works somewhat but is unreliable? Or do you mean that it’s just not working at all? If it’s the former, I’d take that as a pretty good sign that your general approach is right but you have timing issues that are resulting in a loss of synchronization between you and your target XBee device. If it’s the latter, your implementation itself might be flawed.

Glancing over your code, I see a few things that might throw off your timing.

void SWprint(int data)
{
byte mask;
//startbit
digitalWrite(tx,LOW);
delayMicroseconds(bit9600Delay);
for (mask = 0x01; mask>0; mask <<= 1) {
if (data & mask){ // choose bit
digitalWrite(tx,HIGH); // send 1
}
else{
digitalWrite(tx,LOW); // send 0
}
delayMicroseconds(bit9600Delay);
}
//stop bit
digitalWrite(tx, HIGH);
delayMicroseconds(bit9600Delay);
}

  1. How much do you trust delayMicroseconds()? I’m often highly skeptical of microsecond delay functions just because it’s exceedingly hard to do them well, and even the smallest miscalculation of overhead can result in a significant error. If you’re sure that the error of delayMicroseconds() becomes insignificant for delays much longer than one microsecond, then ignore this point.

  2. Your digitalWrite() calls are adding extra delays that could contribute to a baud rate error.

I have two suggestions for how you can potentially improve things:

  1. Replace your digitalWrite calls with direct manipulation of the appropriate PORT bits. e.g. PORTD |= (1 << PD7); PORTD &= ~(1 << PD7); This will excecute many times faster than the equivalent digitalWrite().

  2. Replace your microsecond delays with delays based on mega168 timer1. Then it won’t matter how long it takes you to actually set the outpt pins:

TCCR1B = 0x01;  // run timer1 off of the 16 MHz I/O clock (only need to call this once)

byte mask;
  //startbit
  TCNT1 = 0;
  PORTD &= ~(1 << PD7);  // digitalWrite(tx,LOW);
  while (TCNT1 < 16 * bit9600Delay);  //delayMicroseconds(bit9600Delay);
  TCNT1 = 0;
  for (mask = 0x01; mask>0; mask <<= 1) {
    if (data & mask){ // choose bit
     PORTD |= 1 << PD7;  // digitalWrite(tx,HIGH); // send 1
    }
    else{
     PORTD &= ~(1 << PD7)  // digitalWrite(tx,LOW); // send 0
    }
    while (TCNT1 < 16 * bit9600Delay);  //delayMicroseconds(bit9600Delay);
    TCNT1 = 0;
  }
  //stop bit
  PORTD |= 1 << PD7;  // digitalWrite(tx,HIGH); // send 1

  while (TCNT1 < 16 * bit9600Delay);  //delayMicroseconds(bit9600Delay);
}

If you have access to an oscilloscope you can see what the serial output looks like for both implementations. That should tell you a lot, including whether I’m being overly nitpicky or if my implentation does increase accuracy enough to result in improved performance.

  • Ben

Thanks for the suggestion Ben. To clarify, what I should have said is that I have never got the s/w serial to work, so maybe the implementation is basically flawed as you say.

How I wish I had an oscilloscope!!! In fact I have only just returned from a visit to my nearby Jaycar shop to see what they had in stock, but was advised that a better way of doing the output analysis might be to invest in a storage type scope. I'm very new to all this, but what I wanted to do was essentially what you suggested, i.e. generate a good output to the XBee (using UART) and then compare with a 'bad' output using s/w serial. I was hoping that by getting 2 waveforms side by side, I would be able to see why the one I am generating via s/w was not working, e.g. maybe the start data bit is too long, or too short, etc.

Re. your questions on accuracy of delayMicroseconds() I have no idea but assumed it would be related to the CPU clock and would be sufficiently accurate for my needs, but perhaps I'm assuming wrongly. I've taken a quick look at the code you provided and assume it can just be compiled as straightforward C? Right now I'm not sure how you set up the clock TCNT1 but I'll dig around a bit more and perhaps it'll become a bit clearer.

Thanks for your help. It is much appreciated.

--- Ok, I can see now how the code works. Thanks.

Thanks for your help. It is much appreciated.

Please let me know how things work out. Note that I didn't compile or test my sample code above, so it could be a little buggy.

Another way to test your software serial would be to connect your software serial transmit to your hardware receive (pin 0).

  • Ben

Hi Ben,

I’ve not got it working just yet - well it compiles, so it must be right :slight_smile:

Seriously though, I’m still struggling a bit with understanding some of the programming constructs you introduced. Examples are the clock counter TCNT1, the port(?) PORTD, the shift value (PD7) and so on.

I’m not asking you to provide a full explanation, but maybe a pointer to some doco would be enough for me to get a handle on what you are doing in this code. Also, what I was looking for is the read ‘equivalent’ to the write construct you provided e.g.
PORTD |= 1 << PD7;

Thanks for your help.

BTW, I should add that I have been doing a bit more ‘research’ into the timing values required for the s/w serial logic. I thought it would be useful to read from a pin that is being driven by a real UART Tx channel running at 9600 baud and measure the time between each bit being read. I did this by hooking up 2 XBee devices, one to send data, and the other to recieve the data. My timing code was running on the Arduino connected to the latter. So I would get a recognisable bit pattern, I sent the hex value 0xaa, which gives a bit pattern of 10101010. So, by reading the input pin until it goes Low, and then measuring the time taken between consecutive High and Low swings, I thought I’d be able to determine the precise number of clock cycles required in my Arduino code to replicate the 9600 baud data.

Anyhow, the results were nowhere near as consistent as I expected them to be. Here’s an example.

Start bit = 209 - time taken from line going Low to line going High again for first data bit (which is a 1)
Bit 0: 107 - time taken from line going High to line going Low for second data bit (which is a 0)
Bit 1: 87 — and so on
Bit 2: 107
Bit 3: 148
Bit 4: 107
Bit 5: 157
Bit 6: 54
Bit 7: 237 - this is probably wrong 'cos I didn’t consider the stop bit…so maybe this measurement is twice the bit width

To do the timings, I recorded the value of TCNT1 and then reset TCNT1 to 0 on each transition. What puzzles me is the variation between values, e.g. 148 or 157 compared to 54 or 87. In theory, all values should really be very close to each other, unless I’ve completely misunderstood how serial data is transmitted.

Just thought this might be interesting to someone, or maybe someone can offer some ideas on why my results appear to be so random.

Hi Ben,

I’ve not got it working just yet - well it compiles, so it must be right :slight_smile:

Seriously though, I’m still struggling a bit with understanding some of the programming constructs you introduced. Examples are the clock counter TCNT1, the port(?) PORTD, the shift value (PD7) and so on.

I’m not asking you to provide a full explanation, but maybe a pointer to some doco would be enough for me to get a handle on what you are doing in this code. Also, what I was looking for is the read ‘equivalent’ to the write construct you provided e.g.
PORTD |= 1 << PD7;

Thanks for your help.

Things like TCNT1, PORTD, and PD7 are included in your program by <avr/io.h>, which in turn includes some mega168-specific header files. <avr/io.h> is included by the Arduino precompiler.

Digital I/O on the mega168 breaks up the I/O pins into ports: port b, port c, and port d. Port D has 8 pins, usually referred to as PD0 - PD7. These correspond to Arduino digital pins 0 - 7. Port C has 6* I/O pins (PC0 - PC5) that can double be used as either digital I/Os or analog inputs. These correspond to Arduino analog inputs 0 - 5 (or arduino digital pins 14 - 19). Port B has 8 pins (PB0 - PB7), but PB6 and PB7 are reserved for the external resonator on the Arduinos, so you can forget about using those. PB0 - PB5 correspond to Arduino digital pins 8 - 13.

Each port has three associated 8-bit AVR registers, and each bit of these registers corresponds to a digital I/O pin:

  1. Data Direction Register (DDRx) is used to determine if a pin is an input or an output. If the DDRx bit in question is set, the associated pin is an output, otherwise it is an input.
  2. PORT register (PORTx) is used to control the output behavior of pins. If the PORTx bit in question is set and the corresponding DDRx bit is set (pin is output), the pin drives high. If the PORTx bit is set and the corresponding DDRx bit is clear (pin is input), the pin is weakly pulled high. If the PORTx bit is clear and the corresponding DDRx bit is set (pin is output), the pin drives low. If the PORTx bit is clear and the corresponding DDRx bit is clear (pin is input), the pin is in a high-Z state (floating input).
  3. PIN register (PINx) is used to read the input value of pins. You can read the input value of a pin even if the pin is designated as an output (in which case the result will almost certainly be the value you are driving the pin to).

So if I want to set drive pin 7 (PD7, or Arduino digital pin 7) of port D high, I would set bit 7 of DDRD and set bit 7 of PORTD:

DDRD |= 1 << 7; // the same as DDRD |= 1 << PD7;
PORTD |= 1 << 7;

This sets bit 7 without changing any of the rest of the bits in DDRD or PORTD. If I didn’t care about the rest of the bits in DDRD or PORTD I could just do:

DDRD = 0x80; // the same as DDRD = 1 << 7;
PORTD = 0x80;

If I want to drive pin 7 of port D low, I would use the following construct:

DDRD |= 1 << 7; // pd7 → output
PORTD &= ~(1 << 7); // clear bit 7 of PORTD while leaving the rest of the bits unchanged

This is because the bitwise NOT operator (~) turns (1 << 7) into a mask where all of the bits are 1 except for bit 7, which is a 0. When you and this byte with PORTD, you are anding bit 7 of PORTD with a 0, which is always 0, and you are anding the rest of the bits of PORTD with a 1, which leaves them unchanged.

To read the input value of a pin, you would want to check that pin’s corresponding bit in its PINx register. You can do this by masking PINx for the bit you care about, as follows:

if (PIND & (1 << 7)) // if input value of pin PD7 is HIGH
if (!(PIND & (1 << 7))) // if input value of pin PD7 is LOW

In general, I feel it’s good for people who are using AVRs to understand the underlying registers that are used for digital I/O, but feel free to use the Arduino functions like digitalWrite() unless you need the digital I/O to execute optimally fast.

  • Ben

Excellent overview Bens. It all makes perfect sense and demystifies much of what I have seen in the ATMega 'underbelly' up until now.

Many thanks.

Anyhow, the results were nowhere near as consistent as I expected them to be. Here's an example.

Start bit = 209 - time taken from line going Low to line going High again for first data bit (which is a 1) Bit 0: 107 - time taken from line going High to line going Low for second data bit (which is a 0) Bit 1: 87 --- and so on Bit 2: 107 Bit 3: 148 Bit 4: 107 Bit 5: 157 Bit 6: 54 Bit 7: 237 - this is probably wrong 'cos I didn't consider the stop bit...so maybe this measurement is twice the bit width

To do the timings, I recorded the value of TCNT1 and then reset TCNT1 to 0 on each transition. What puzzles me is the variation between values, e.g. 148 or 157 compared to 54 or 87. In theory, all values should really be very close to each other, unless I've completely misunderstood how serial data is transmitted.

I'd expect the times to be very regular, so I think there's some problem with your (my?) timing routines. This could be the result of some unexpected delays in your code that keep you from detecting the very start of each bit. One way to combat this would be to set up a pin-change interrupt on your RX line, and use that interrupt to read/reset the timer count TCNT1. The only thing that would delay your pin-change interrupt from executing would be other interrupts, but you could disable these in your setup() if you want to make a really accurate test. For example, you might consider disabling the timer0 overflow interrupt that drives millis(), and you might want to disable the serial-received interrupt.

The mega168 datasheet explains how you can set up pin change interrupts, but I can help you out with it if that's something you want to try.

  • Ben