Go Down

Topic: Software serial implementation (Read 527 times) previous topic - next topic

tobyb121

Jan 03, 2013, 04:15 pm Last Edit: Jan 03, 2013, 04:22 pm by tobyb121 Reason: 1
Hi there,

I'm working on a little project then needs to have two serial devices connected to it, it basically reads analog voltages from A0-5, and a gps module connected via a serial port at 9600 baud, does a little bit of processing and sends this data over a second serial port.
The second serial port needs be full duplex, as it is used to also receive control commands from a pc, so I have naturally used the hardware serial port for this. The GPS module therefore needs to be connected using some sort of software UART.
I looked at first at the Software Serial library, but the problem with that is that it from what I can tell from the code, it blocks the processor during receive. The module is fixed at 9600 b/s, a full set of NMEA messages could easily be 4-500 bytes and so this will block the processor for nearly half a second, time that needs to be spent measuring A0-5, which I want to be happening about 10 times a second.

I therefore decided to build my own software serial implementation using an external interrupt on pin 2, and timer1, I know this is a very resource heavy way to do it, but I don't need these functions for anything else so that doesn't concern me.

I've written the code below to test my iplementation, using an uno, with pin 0 (HW UART Rx) connected directly to pin 2. I then just send it characters from realterm and it should echo them back to me. The problem is that it seems to miss the first bit every time, so for example 'a' (B01100001) gets returned as 0xC2,0xFF (B11000010 B11111111) i.e. it reads the start bit as the LSB of 'a' reads bits 0-6 as 1-7 then the last bit, a 0, is recognised as a new byte, then it gets all 1's (no transmission).

I cannot understand at all from my code why it reads the start bit as bit 0, but I've been trying to get it to work for ages now.

Would someone more experienced than myself mind having a look at my code and seeing if they can work out the problem.
I have made this to work with an uno, though I think should work with any 16MHz processor.

Thanks,

Tobyb121

EDIT: One other thing I should mention, in debugging this I added an array that would store value of micros() at each timer interrupt and the initial falling edge, this showed that the first timer interrupt happens about very quickly (<20us I think) after the falling edge, implying that it tirggers immediately after exiting the ext interrupt routine

Code: [Select]

#define BUFFER_SIZE 128 //Number of bytes to store in a buffer

byte _i;                //current bit
char _b;                //shift byte to store current byte
char buf[BUFFER_SIZE];  //buffer to store received bytes
byte _ptrRead;          //position in buffer to read from          
byte _ptrWrite;         //position in buffer to write to

int _baud;              //period of each bit in instruction cycles

void initialiseSerial(int baud){
       _ptrRead=0;
       _ptrWrite=0;
 
pinMode(PIN2,INPUT);

_baud=2000000/baud;    //Calculate baud (based on 16MHz processor)
       
SREG|=B10000000;       //Global interrupt enable

       // Setup timer1 to interrupt on overflow, internal clock, 1:8 prescaler
TCCR1A=0;
TCCR1B=B00000010;
TIMSK1=B00000000;

       //External Interrupt enable on pin2 falling edge
EICRA=B00000010;
EIMSK=B00000001;
}

void Timer1InterruptRoutine(){
       // reset timer to trigger in next bit
TCNT1=0xFFFF-_baud;
       
       //if less than eight bytes have been written
if(_i<8){
_b|=digitalRead(PIN2)<<_i;  //read the pin and add it to the temp byte
_i++;
}
else{  //when all bytes have been received
buf[_ptrWrite++]=_b;       //Add the temp byte to the buffer  
TIMSK1=B00000000;          //Switch off the timer
EIMSK=B00000001;           //Re-enable the falling edge interrupt
}

}


ISR(TIMER1_OVF_vect){
Timer1InterruptRoutine();
}

//Fired when falling edge detected on pin 2
void Int0InterruptRoutine(){
       //reset temporary storage variables
_b=0;
_i=0;

       //Disable external interrupt for now
EIMSK=B00000000;

       //Enable timer and set it to overflow in 1.5*baud ticks, in middle of bit0
TCNT1=0xFFFF-3*_baud/2;
TIMSK1=B00000001;
}

ISR(INT0_vect){
Int0InterruptRoutine();
}

// Read a byte from the buffer and increment the pointer
char readByte(){
char b;
b=buf[_ptrRead++];
_ptrRead%=BUFFER_SIZE;
return b;
}

// Check if there are bytes available to read
byte bytesAvailable(){
if(_ptrWrite<_ptrRead)
return _ptrWrite+BUFFER_SIZE-_ptrRead;
else
return _ptrWrite-_ptrRead;
}


void setup()
{
       pinMode(PIN3,OUTPUT);
       pinMode(PIN2,INPUT);
Serial.begin(9600);
initialiseSerial(9600);
}

void loop()
{
if(bytesAvailable())
 Serial.print((char)readByte());  //Continuously check the buffer and write any received bytes to the hardware serial
}

PaulS

Quote
I looked at first at the Software Serial library, but the problem with that is that it from what I can tell from the code, it blocks the processor during receive.

It blocks the processor during the receive of ONE character.

Quote
a full set of NMEA messages could easily be 4-500 bytes and so this will block the processor for nearly half a second

You don't have to receive them all. You should be able to tell the GPS which one(s) you want.


tobyb121

Thanks for the quick reply, I have looked at reducing the NMEA sentances that are sent, unfortunatly the data sheet is rubbish so I havn't got this to work yet, however I will still need at least the GGA and RMC sentances, which is likely to be around 200 characters, and so will block for about 0.2s, and is still a bit long for me. I think that reducing the sentances will help, but I'm still going to have a problem.

PeterH

If you only want to perform an analog read every 100ms and consume stuff read from SoftwareSerial at 9600 bps then I don't see a problem. SoftwareSerial is interrupt based so as long as you check for received data often enough to prevent the 64-byte receive buffer from overflowing it should be fine.

All you need is a loop() which: reads a character from the SoftwareSerial port if it is available, and buffers it locally; processes the buffered input when a complete NMEA sentence has been received; performs the analog read if is it time to do that.
I only provide help via the forum - please do not contact me for private consultancy.

Nick Gammon


The second serial port needs be full duplex, as it is used to also receive control commands from a pc, so I have naturally used the hardware serial port for this. The GPS module therefore needs to be connected using some sort of software UART.


Not necessarily. The high-volume connection would be better as hardware serial. Occasional commands from  the PC could be software serial (and indeed a lower baud rate if necessary). Although, it might be better to have software serial (connected to the PC) be at as high a baud rate as you can comfortably get. That way the blocking in software serial is minimized.
http://www.gammon.com.au/electronics

tobyb121

Thanks, for the thoughts and comments, I've actually just got it working, the key was to clear the timer and external interrupt flags in the necesary places.

@PeterH
I've looked through the Software Serial source and it uses interrupts for detecting the start of a byte, but then blocks while it reads each bit, when you have one byte after another it effectively doesn't top blocking.

@Nick Gammon
Now that I've got my software serial working I'm thinking to do this, the reason I had gone for the hardware connection with the PC was that there is quite alot of data going from the arduino to the PC, what I'm thinking to do is use the Tx from the HW UART for sending data to the PC, the HW Rx to the GPS and then use the Software UART for receiving data from the PC. It will probably make the code a bit less readable.

Thanks for your help,

Tobyb121

Nick Gammon


I've looked through the Software Serial source and it uses interrupts for detecting the start of a byte, but then blocks while it reads each bit, when you have one byte after another it effectively doesn't top blocking.


That's why I would use Software Serial for the lower-volume incoming data (like commands from the PC).

Anyway, at 9600 baud you have 104 uS between bits, so once the last bit in a byte was received, and interrupts re-enabled, that is plenty of time for the Uart interrupt to kick in.
http://www.gammon.com.au/electronics

dhenry

Quick suggestions:

Code: [Select]
TCNT1=0xFFFF-_baud;

Use
Code: [Select]
TCNT1=-_baud;
instead.

Code: [Select]
       
        //if less than eight bytes have been written
if(_i<8){
_b|=digitalRead(PIN2)<<_i;  //read the pin and add it to the temp byte
_i++;
}


Use masks, like

Code: [Select]

        //if less than eight bytes have been written
if(_i){
_b|=(digitalRead(PIN2))?(_i):0x00;  //read the pin and add it to the temp byte
_i=_i<<1;
}


I would also define PIN2 so it can be easily reconfigured by the user.

Go Up