Pages: [1] 2   Go Down
Author Topic: Low latency arduino library  (Read 2581 times)
0 Members and 1 Guest are viewing this topic.
Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

I am displeased with the way libraries like e.g. HardwareSerial and LCD4Bit handle their IO, namely by blocking until it's done.
For this reason I am experimenting with libraries based on tasks that execute in a non-blocking fashion.
Currently I have implemented HardwareSerial and LCD4Bit equivalents and I am working on Ethernet.
http://arduinos.googlecode.com/
If you like, please tell me what you think!
--jaap
Logged

SF Bay Area (USA)
Offline Offline
Tesla Member
***
Karma: 137
Posts: 6792
Strongly opinionated, but not official!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Did you really make serial.write discard characters when it would need to block?  I think that's a really bad idea, unless you want to end up re-implementing the entire print class as well...
Logged

Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Good point. I haven't yet come up with a good solution for that. I could simply make it block, but that would sort of defeat the purpose of what I set out to do.

For now I have considered responsiveness more important than serial data integrity, since a reliable serial protocol should use some sort of checksum mechanism anyway, but I agree that this is probably unacceptable for general purpose use.
Logged

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

@SlashV

Hi Jaap,

Just made a change this week to the DallasTemperature Library to make it non-blocking (so you may remove it from your list). In the existing version if one requests 12 bit precision for the temp sensor it blocks for 750 msec. This time is needed for ADC conversion to stabilize.

My patch adds a bool WaitForConversion flag that's default true to be backwards compatible. If this flag is set to false the call requestTemperatures(...)  returns asap without the built in delay. This means that the program has to do the timekeeping before getTempCByIndex(..) or equivalent can be called.

As my app needs to sample three Tempsensors every second, 750 msec active delay is no option as I need to process it, put it on a screen, log it over internet etc.  I know that I can start ADC in parallel but it still leaves only 250 msec for processing and ethernet (esp to the internet) can be timeconsuming.

My first proof of concept sketch does work quite well, see below. It does not do anything meaningfull except counting idle msec's. More important existing sketches still work as they used to.

Hope to 'publish' the patches soon.
Code:
//
// Sample of using Async reading of Dallas Temperature Sensors
//
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 2

// Reading Temp
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
DeviceAddress tempDeviceAddress;
int  precision = 12;
float T1 = 0.0;

// timekeeping vars
unsigned long lastTempRequest = 0;
int  delayInMillis = 0;

int  idle = 0;

//
// SETUP
//
void setup(void)
{
  Serial.begin(9600);
  Serial.println("Start Dallas Temperature Async demo");

  sensors.begin();
  sensors.getAddress(tempDeviceAddress, 0);
  sensors.setResolution(tempDeviceAddress, precision);

  delayInMillis = 750 / (1 << (12 - precision));        //scary? :)
  
  sensors.setWaitForConversion(false);
  sensors.requestTemperatures();
  lastTempRequest = millis();
}

//
// LOOP
//
void loop(void)
{
  if (millis() - lastTempRequest > delayInMillis) // waited long enough to read sample??
  {
    Serial.print("Temperature: ");
    T1 = sensors.getTempCByIndex(0);
    Serial.println(T1,4);
    Serial.print("  Precision: ");
    Serial.println(precision);
    Serial.print("Idle counter: ");
    Serial.println(idle);    
    Serial.println();
    
    idle = 0;
        
    // directly after fetching the temperature we request a new sample in the async mode
    // for the demo the precision rotates
    precision++;
    if (precision > 12) precision = 9;
    
    sensors.setResolution(tempDeviceAddress, precision);
    sensors.setWaitForConversion(false);        // not really needed as flag is already false
    sensors.requestTemperatures();

    // do the timekeeping
    delayInMillis = 750 / (1 << (12 - precision));
    lastTempRequest = millis();
  }
  
  // we can do usefull things here but for the demo we just count the idle time
  delay(1);
  idle++;
}
Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

I don't have such a sensor, but maybe this would be a good excuse to get one smiley-wink. Either way, I will be happy to incorporate the code.
Logged

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Last weekend I allready sent my patches to the maintainer of the DallasTemperature library and it will be in a next release. The DS18B20 are really nice temp sensors working with 8 + (1-4) bit (fractional part).

The hardwareSerial could be work in the following way. A call to print or write adds the string/int/long to an outputbuffer and returns. An interrupt routine - triggered by a timer - monitors this buffer and if a character present it sends the bit pattern over the line. This sending is done by setting the timer to the right moment the line must change. the interrupt routine will change the pin, set the timer again and returns. This way the sending is done in the background.

Does this make sense to you?, otherwise I will produce some pseudocode as I have no experience yet programming timer interrupts on the arduino. nice exercise smiley

Rob
Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Rob, this is exactly how it works in my code (check it out!), apart from the fact that you don't need the timer. The DRE interrupt can suck the output buffer empty.

Trouble is, as @westfw pointed out: what to do when your output buffer is full while you're writing? I have now implemented an alternative to the "Print" interface that returns whether the write was successful. (Print::print returns void, so there's no way to tell). That way you can make sure no data is lost.

--Jaap

Keywords: HD44780, enc28j60, DNS, DHCP, low latency
« Last Edit: October 27, 2010, 06:30:51 pm by jaapie » Logged

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Just had a quick view on the code and saw that the outputbuffer is fixed size and a power of 2. This latter makes it easy to do the modulo calculation for the circular buffer.

wrt to the problem, this is typical a requirement error. What do you want it to do? If properly documented any choice is valid (eh well almost).

I see four strategies:

a - if full -> block;
b - if full -> print as many chars as possible;
c - if full -> print no data;
d - if full -> adapt the buffersize

add a) This means no loss of data but loss of program control. It is the choice of the programmer what is most important. In a datalog application failure of one record may or may not be acceptable.
One could implement a library behavior-flag that can be set in the program. This DontLooseData or BlockIfNeeded flag gives the app programmer the choice to change the behaviour and it has only little effect on the librarysize. For me the default value would be true => block if needed, most of the time.

add b) Unpredictable behaviour. There need to be a test before to see if there is enough space for the string, or a test afterwards to see if the string fitted in the buffer. This latter is relative easy to implement.

add c) Similar to 2, but less unpredictable as there are only two outcomes possible. Test afterwards would be my choice. Instead of
   J_ASSERT(false, "Serial write buffer full"); one just sets a boolean flag printSucceeded = true; that can be tested. Such a function makes sense  anyway (note it differs from writeable_data() )

add d) Increasing the size of the buffers when needed is a strong strategy. One can double the size, add a fixed amount, or add enough to fit the bill. At least it means that the modulo calculation (eg in newhead() ) become slower as one needs the % instead of & .  Furthermore it means the use of dynamic memory see http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1230935955 for some discussion.


My thoughts:
1) Implement the test function to see if a print("blabla"); has succeeded. It gives back the missing feedback. Just a boolean, or the # chars printed/missed?

2) Add the BlockIfNeeded flag. (see a) This gives the user the choice how the SerLib will behave on a per application basis.

3) Investigate the dynamic buffersize as this frees the programmer from thinking about the buffersize at all. I'm aware this will introduce new problems as the amount of mem is restricted.

opinion?
Rob
PS, Correct me if I'm wrong but on the receiving site is there not a similar problem when more char's are received than fit into the RXbuffer?




« Last Edit: October 28, 2010, 03:57:42 am by robtillaart » Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Regarding the suggested strategies:
I do not consider (a) an option. My goal is to never block, so any task is guaranteed to run within say 500mus.
I chose options (b) and (c). Both should work fine, but require checking the result of "write". There is no way around that if you want to avoid blocking. Option (d) could be used additionally. But like you said, memory is extremely limited so you'll want to limit the amount allocated or other memory requests might fail.

And yes, you have the same issue on the rx side, but that isn't a problem. Since there is no blocking, you'll always be in time to service the port and pull the bytes from the rx buffer before it overflows.

In fact, this is exactly the reason why I chose to create these libraries. I have a project with a mega, with 3 incoming serial lines and 1 outgoing. If you block while outputting data (like with the standard HardwareSerial), you will potentially lose input (not just serial, but also other input, unless it is handled by interrupts).

--Jaap
Logged

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Option (b) requires a test returning the #chars written. ==> more administration in both the library and in the user code (for recovery).  Furthermore there may be a problem on the receiving end (eg PC) as it receives only half a transmission. This is a serious drawback for option (b) imho.

Therefor I prefer (c) as recovery is far easier for the user of the library. The test results just in a boolean succes or failure. Recovery eauals re-write the whole string. Also the receiving end (PC) will get no half records/sentences etc.

If you implement (c) option (a) is not needed anymore as those who accept blocking as an option could write :
Code:
{Serial.println("blabla"); } while ( false == Serial.writeOK );

With respect to dynamic buffers, in theory the size stabilizes quite fast. In practice they might explode when a line drops smiley  So there should be an absolute maximum. But that could be greater than the 128 that is the current max in your implementation. Looking at a recent sketch, it sends every second a record (set of sensordata) of ~80 bytes. Add a few more sensors and debug items and I will go beyond 128 bytes.

What do you think of an additional constructor that allows the programmer to set the buffersizes from the application. no further runtime mallocs/frees. The library however should add support sizes other than powers of 2. That way the lib becomes usefull for sending arrays > 128 in one write.

Just thinking out loud smiley
Rob
"In theory there is no difference between theory and practice, but in practice there is",
Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

For the circular buffer I did some small test (below) to see the effect of masking versus modulo if no powers of two are used. Besides I also coded two alternatives. The program on my Arduino 328 gave the following results:
Code:

byte head = 10;
byte mask = 0xFF;
int size = 233;
  
int maskADD()
{
  head = (head+1) & mask;
  return head;
}

int modADD()
{
  head = (head+1) % size;
  return head;
}

int if1ADD()
{
  head++;
  if (head == size) head = 0;
  return head;
}

int if2ADD()
{
  head--;
  if (head == 0) head = size;
  return head;
}

//
// SETUP
//
void setup(void)
{
  Serial.begin(9600);
  Serial.println("Start Modulo Mask compare");
}

//
// LOOP
//
void loop(void)
{
  int newhead;
  unsigned long count = 1000000;
  
  long t1 = millis();
  for (unsigned long x=0; x< count; x++)
  {
    newhead = maskADD();
  }
  long t2 = millis();
  Serial.println(newhead, DEC);
  Serial.print("MASK: "); Serial.println(t2-t1);
  

  t1 = millis();
  for (unsigned long x=0; x<count; x++)
  {
    newhead = modADD();
  }
  t2 = millis();
  Serial.println(head, DEC);
  Serial.print("MOD:  "); Serial.println(t2-t1);  
  
  t1 = millis();
  for (unsigned long x=0; x<count; x++)
  {
    newhead = if1ADD();
  }
  t2 = millis();
  Serial.println(head, DEC);
  Serial.print("IF++: "); Serial.println(t2-t1);  
  
  t1 = millis();
  for (unsigned long x=0; x<count; x++)
  {
    newhead = if2ADD();
  }
  t2 = millis();
  Serial.println(head, DEC);
  Serial.print("IF--: "); Serial.println(t2-t1);

  Serial.println("===========================================");
}
1.000.000 calls gave the following results in millisec and relative performance against mask.

MASK: 943     100%
MOD:  15530  1647%
IF++: 1257    133%
IF--: 1006    106%

Conclusion
Modulo calculations are way too expensive, a factor 16! Using a simple if test (IF++) is only 33% slower than using the mask construct. Imho an acceptable price for the freedom of buffersize.

Note the (IF--) construction is only 6% slower than the mask method, however in the IF-- construction one must use the circular buffer backwards! The speedup comes from the fact that it tests against 0 (zero) which is faster than testing against an arbitrary value. I do not oversee the impact of using the buffer backwards in the rest of the code at the moment. If this is minimal it would really be a small price for freedom of buffersize.

Rob
Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Thanks for the tests. A circular buffer with comparison rather than mask seems feasible, however, I believe your code may have a problem. The "head" temporarily has the value of "size". If the interrupt routine kicks in at exactly that moment I have a feeling you may be in trouble.

I think you could implement the 'backward' circular buffer like this:
Code:
int if2ADD()
{
  if (head == 0)
    head = size - 1;
  else
    --head;
}

That will avoid "head" ever being out of range. I'll give that a try.

--jaap

btw. I adopted the 128 byte limit for the buffer from http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1242466935/0. I believe It should also work fine with 256 size. In fact, I don't think you need a mask at all in that case.
« Last Edit: October 28, 2010, 03:53:08 pm by jaapie » Logged

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

You're absolutely right. My code was just a speed check and not usable for real apps smiley When the buffersize is 256 it still has to wrap head and tail around so one still needs an IF, MASK or MOD operation.

If the IF-- works fine with your lib, the HardwareSerialLib might also be updated?

Rob
Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Delft, Netherlands
Offline Offline
Newbie
*
Karma: 0
Posts: 10
Arduino boy
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

> When the buffersize is 256 it still has to wrap head and tail around so one still needs an IF, MASK or MOD operation.

No, because the head and tail will wrap automatically because they are 8 bits wide.
check:
Code:
uint8_t val = 255;
++val;
print(val);

--Jaap
Logged

Global Moderator
Netherlands
Offline Offline
Shannon Member
*****
Karma: 224
Posts: 13915
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Very true, I had >= 256 in mind. Did you find time to try the reverse (IF--) buffer variant yet?
Rob
« Last Edit: October 29, 2010, 06:47:24 am by robtillaart » Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Pages: [1] 2   Go Up
Jump to: