Bear with me a while please while I explain. This is quite complex and I see no simple explanation.
I am building a project for which I’d like to use a 1284P. The alternative is a 2560 which would take the application in its stride (I already have that working using a hardware serial port). However the through hole mounting makes the 1284P and form factor is a good match for the requirement except for one thing. The1284P has only 2 serial ports and I need 2 1/2, the 1/2 being a receive port at 9600 baud, 8 bits of data, no parity and one stop bit. Like you get from many GPS units, being NMEA sentences.
No flexibility is required, however the implementation must be reliable. Dropped bits and bytes are unacceptable. Software serial (or one of the alternatives) seemed the obvious answer however all of them were incompatible with other parts of the project particularly the pin change interrupts (PCI) I am using for the user interface. I was getting interrupt vector collisions between the PCI and the software serial software as fatal compiler errors.
I decided I would take a look at a specific purpose implementation on a test 1280 I have to hand for such purposes. If it works on the 1280 I can make it work on a 1284P. I intended to use a pin change interrupt to detect the falling edge of the start bit and then Timer1 to sample the serial signal at appropriate intervals with an ISR.
The code is attached. I apologize in advance for the number of commented out lines and some debug lines, but at least the thinking can be seen.
It is supposed to work like this:
The bit rate period for 9600 Baud 104.17 uS. I decided that I would take only one sample per bit. The application does not have long length serial wires and the security against noise provided by say “best of 3” sampling per bit wasn’t justified, at least not get the thing working. Since the timer must be controlled by an integer I used 52uS. This is an error to be sure (about -0.16%) but it is not accumulated over more than one byte, since the algorithm resets during the stop bit.
Serial communication was developed in the era when communication devices were mechanical and it will take some convincing that a non-accumulated error of less than 1% has an impact.
For my test set up I am using A15, bit 7 on PortK of the 1280 as my serial input pin.
The whole thing is a sort of state machine, controlled by the variable receive_sequence with the following states:
const byte awaiting_start = 0;
const byte start_found = 1;
const byte bit0 = 2;
const byte bit1 = 3;
const byte bit2 = 4;
const byte bit3 = 5;
const byte bit4 = 6;
const byte bit5 = 7;
const byte bit6 = 8;
const byte bit7 = 9;
const byte stop_expected = 10;
We start at awaiting_start:
The PCI ISR routine is looking at the input serial signal and when it sees a high low transition which could be a start bit it restarts Timer 1 (set at 52uS). It also turns off the PCI by setting the mask register to 0, thus eliminating noise interrupts. The other important thing that happens is that the state is advanced to start bit found. At this point the PCI routine has done its job and the next part of the process is under the charge of timer interrupts from Timer1.
The routine read_bits implements a state machine where start found looks samples the serial stream. Some 30uS later (and yes it should be 52uS and Timer1 has many posts about stopping and starting and associated errors in timing and I read them assiduously to try to get to the bottom of this, but to no no avail at this point.) In any case 30uS is not unacceptable it being a just bit early in the bit timing. 52uS would be great but 30 will do as it puts the bit steam sample well towards the middle of the start bit. If a low is found then the serial bit receive process follows. If a high is found then we stop the timer, re-enable the PCI and return to the awaiting start state for a real start bit.
Then follows the reading of the data bits at 104uS intervals using the bit0 to bit7 states which control flow through the read_bits Timer1 ISR. The byte is built up in byte_received until we get to bit 7 and then we start to look for a stop bit. Data bits are sampled every 104uS and a weighting byte is used to build the received character.
stop_expected: At this point we re-enable the PCI for the next start bit and the serial stream is sampled. If a low is found (not a stop bit) the byte received is discarded and we do what we need to do to return to awaiting start. If the stop bit looks good the timer is stopped (it has done its job for now), byte_received_status is set to flag a character arrived, the character arrived is set into final_character. Also a circular buffer with pointer and a character count is updated, serial_buffer
And now we wait until another start bit arrives and we do the whole thing again.
In the process of debug I was using my oscilloscope to show the incoming character and a a digital write on pin 13 to show where the sample was being made. See attached photo. For each step of the state machine the pin 13 was taken high and immediately low giving a spike on pin 13 at about the time the sample is taken. For test purposes Serial1 was programmed to send a letter A into A15 and I displayed the character received on the top trace of my scope and the timing of the sampling is on the bottom trace. Sorry my X cursors are not in a more sensible place. An ideal spot would have been the 30uS timing sample in the start bit, but my bad. Also apologies for a screen photo taken by phone. I do have Rigol’s app but I am no fan of it.
This photo clearly shows that the there is a sample in the right spots, more or less. (I found that by reducing the period of Timer1 down to 26us and fiddling about with the state machine counter counter I could get this timing pretty well precise, but it made no difference to performance and it meant that there was only 26uS for stuff to happen in read_bits.)
Now this code works with a decent real life character stream. I am using a GPS module as a source of characters and as long as all we do is read the GPS with the software serial and echo the stream to the console there are no dropped bits or errors. I have checked this for thousands of characters, both by eye as it streams past and more rigorously with analysis tools. I can do this because I have the read side of Serial1 eavesdropping on the GPS source. So I have a known good source and the one under test. loop captures samples of the stream which are dumped to the console for analysis.
Beauty I thought, just feed the stream to an implementation of TinyGPS+ and we are home and hosed with a robust, if inflexible, serial using software. But no, TINYGPS+ reports checksum errors when fed from the software source and a check of those buffers you see in loop bytes_ss and bytes_ser1 recording characters shows that characters are dropped. I am seeing about 1 checksum error per second from TinyGPS+.
Now the point of this post. Why is this so? It is a mystery to me that the load in loop has an impact. TinyGPS+ will have a load sure, but I don’t think it turns the interrupt off. Sure the process is completely sensitive to interrupts. Too many other interrupts, slow interrupt routines, or turning the interrupt off altogether will be catastrophic. But I can’t see that.
Why does the feed to GPS fail when the feed to the console is fine?
my_software_serial.zip (2.99 KB)