Non-blocking extension to Wire library

1,210 views
Skip to first unread message

Paul Stoffregen

unread,
Mar 17, 2017, 5:46:59 PM3/17/17
to devel...@arduino.cc
One of my many goals this year is a non-blocking extension to the Wire
library. Ideally I would like to see this adopted by the official
Arduino Wire library. I believe everyone benefits in the long term when
we developers work together for API compatibility.

The 2 blocking functions are Wire.endTransmission() and Wire.requestFrom().

https://www.arduino.cc/en/Reference/WireEndTransmission

https://www.arduino.cc/en/Reference/WireRequestFrom

Each of these currently accepts an optional parameter for whether or not
to send a stop condition. Perhaps another optional parameter could be
added for a function to be called? Such as:

Wire.requestFrom(address, length, true, myfunction);

When given a function to call, requestFrom would immediately return.
Later when the transfer completes, the function would be called.

Or maybe a complete function would be giving with something like:

Wire.onRequestFrom(function);

The library already has onReceive(function) and onRequest(function) used
for slave mode. Maybe 2 more of these could be added for master mode,
and the traditional requestFrom and endTransmission would become
non-blocking if the user has set up a completion callback function?

Or maybe there's some better non-blocking API than event callback functions?

Of course, I'm sure someone reading this is thinking "why do this at
all, the Wire library has worked well for 10 years". On lower
performance boards with only a single I2C port, the blocking Wire
library usually is perfectly fine. But times are changing...

Many newer boards have 2 or more I2C ports. If you have 2 or more
ports, you can't even get more than 1 communicating at the same time
when your only master-mode API waits for each transfer to complete.

Newer ARM & ESP boards are also much faster and capable of doing other
work during an I2C transfer. People are increasingly building higher
performance projects, like complex LED animations and audio synthesis,
where the time taken for an I2C transfer becomes an issue. Even for
networking, time taken waiting for I2C can mean not responding to clients.

I'd really like to hear your opinions about how non-blocking Wire should
be done.



Paul Carpenter

unread,
Mar 18, 2017, 8:03:42 AM3/18/17
to Developers
Yes it would be a good idea and yes it needs a lot of thought

With more modern devices MEMS accelerometers and other more complex devices it is not uncommon for the data to be read to be 16 plus bytes so the blocking time is considerable.

If we only consider the UNO as being the ONLY platform, the other platforms suffer. Just looking at three boards we have
  1. Uno, 16 MHz 1kB RAM
  2. Mega 16 MHz 8kB RAM
  3. Zero  48 MHz 32kB RAM (32 bit)
  4. Now defunct but still supported Due 84MHz  96kb RAM (32 bit 2 x I2C)

All newer boards are likely to have more RAM and/or faster clock, faster clock alone will mean a lot more wasted cycles on blocking calls and more functions should be made non-blocking.


There are issues with RequestFrom and EndTransmission that make it more complicated to make them non-blocvking, but it is obviously possible as many other platforms have non-blocking I2C (or SMBUS that runs over I2C) in desktop PCs so the API CAN be made non-blocking. However it is more than just callback functions that are likely to be required.


First of all error handling EndTransmission has return codes of

  • 0:success
  • 1:data too long to fit in transmit buffer
  • 2:received NACK on transmit of address
  • 3:received NACK on transmit of data
  • 4:other error

Missing error status

  • Bus Busy or bus fault
  • Indication if any bytes sent successfully before error
Where as RequestFrom has NO error codes returns number of bytes and available is only tells you 0  or positive numbers of bytes available (no negative value for errors). Assumes device is still there, I dread to think how it handles NACK part way through transfer as i have not looked deeply into the code yet.

So some form of error handling and reporting is required even if Linux style errno on global byte and common formating ranges for ALL API, anything interrupt driven would have to set its own internal error flag anyway, for later calls to use.

Secondly missing functions/methods on existing API (and true of other peripheral libraries) is ability to get information like
  • Current buffer size (RX and TX which might be different)
  • Number of peripherals
  • Maximum capabilities of the peripheral (I have run I2C on many things from 100kHz to 2 MHz) even standard controllers these days support many clock speeds from 0 to 400 kHz,
  • get current clock speed
  • get current status
  • Ability to change buffer sizes (just like previous discussions on Serial ports)
Nearly all the above you have to know beforehand for every possible platform and then do conditionals in your code on pltform defines making the sketch less platform agnostic. The class and streams should be able to report details in a platform agnostic way.

Thirdly we get to buffering as platforms get faster and more SRAM also the devices more complex or the need to read/write 256 byte or larger pages from devices the need for control opver buffer sizes becomes  more necessary. The receiving and sending is still potentially the same problem as serial and ring buffers.

Fourthly as Arduino does not inherently support scheduling even simple scheduling, make API more external scheduling friendly that needs more thoughts as well.

Now requirements for non-blocking nature of Wire, you will need to consider is calling functions the best way to signal complete or for example a form of txavailable or txdone required as well

Personally I2C and SPI (maybe others) should be built up as a Bus transaction series of steps then left to non-blocking way to perform the operation, especially as many devices need write an internal address/command restart bus read 'n; bytes. Some SPI devices need this type of transaction method as well. You can stream the actual data after the transaction.

You are welcome to discuss this with me offline first to hash around ideas

Todd Krein

unread,
Mar 18, 2017, 9:38:07 AM3/18/17
to devel...@arduino.cc
+1
--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Thomas Roell

unread,
Mar 18, 2017, 9:54:06 AM3/18/17
to devel...@arduino.cc
Paul C/S, thanx for the nice writeup.

For STM32L4 we had this problem addressed a while ago, but not in all the aspects you raise. The API looks like this:

    bool transfer(uint8_t address, const uint8_t *txBuffer, size_t txSize, uint8_t *rxBuffer, size_t rxSize, bool stopBit, void(*callback)(uint8_t));
    bool done(void);
    uint8_t status(void);

Let me take you quickly throu how that works before commenting on the why. The "Wire.transfer()" function is a composite out of beginTransmission/endTransmisstion/requestFrom, first a bunch of bytes are written, and then a bunch of bytes are read. When done that callback function is called with the same status code a "endTransmission" would return. If callback is NULL, then no callback is called (well ...). However you can check via "Wire.done()" whether the last transfer complete or not, and with "Wire.status()" you can query the status of the last completed transfer. There can only be one transfer pending. If you issue another one while there is one pending, "Wire.transfer()" returns "false". A normal "Wire.beginTransfer()" and "Wire.requestFrom()" will block till the a currently running transfer is complete. From within the callback, the next transfer can be started.

The reason not to use the beginTransmission/endTransmission/requestFrom scheme was that it limits you in quite a few ways. One is that the default buffer size for AVR is 32 bytes. Some devices (EEPROMs, GNSS via I2C) would like to read/write longer chunks. Some implementations have bigger buffer size, so a lot of code out there is non-portable and will break in unpredictable ways. For that reason alone it seems that passing buffers directly seemed to be a wise idea.

The next issue is really issuing transfers as efficiently as possible. If you read a MPU9250 @ 1000Hz, you consumed already half your bus bandwidth on a 400kHz I2C bus. So having to extract the data via "Wire.available()" and "Wire.read()" in the callback adds extra overhead in terms of runtime in the callback (which has to be in some interrupt context), and then you have to use multiple API calls to setup the next transfer if you want to chain a read from say a pressure sensor or a GNSS. With the explicit passing of buffers, you can simply issue the next transfer before you need to look at the data of the previous transfer.

A callback per transfer is needed as you may want to chain a sequence of transfers. While it's possible to have one callback per Wire instance, and simply use a state-machine within the callback, user feedback was that this is too complex to deal with. Their typical coding ended up changing the single callback before every transfer (tried this with two test customers early on).

Lastly I don't think the "stopBit" part of the API is a good idea to begin with. Most I2C transactions are writes followed by reads. Having a composite transfer API avoids in most cases an open restart condition on the I2C bus. Less chance for a hung I2C bus.

As implemented this API for STM32L4 has a few shortcomings. One as pointed out by Paul C is that the status codes are insufficient. Does a normal user care ? I don't know. All the NACK and falling off the bus have been mapped to a "4 other". There is no indication how much of a transfer succeed. It may or may not have. And to be honest I don't know whether one can do better. If you read data, you want it all. If you read 16 bytes and get only 4 before the I2C device craps out, well, it does not help you, because you need to look at all 16 bytes. If a write of 4 bytes fails after 3 bytes, all you know is that 2 byte writes were successful. You have no indication as to whether the 3rd byte was seen or processed by the I2C device or not. So I could not really say that having more error codes would help a great deal. If a transfer failed, it might have succeeded partially, but there is no guarantee about what parts succeeded.

Having said all of that about error code, we also added:

void reset(void);

This allows the usual I2C reset sequence on the bus to recover a hung bus. This had been necessary in another non-Arduino project where the I2C controller every now and then timed out and hung the bus ...

Next, a couple of observations with the current Arduino API. AVR has added a rather interesting function that I missed when originally addressing async transfers:

         uint8_t requestFrom(uint8_t address, uint8_t quantity, uint32_t iaddress, uint8_t isize, uint8_t sendStop)


That is similar to the composite transfer that STM32L4 uses, "iaddress" is really a up to 4 byte chunk that is written initially, whereby "isize" could be 0, 1, 2, 3, 4 (implemented only up to 3) which says how many bytes of "iaddress" are used.

For 95% of the cases this composite transfer flavour would be good enough, especially to read sensor data. But that does not fully resolve the buffer size issue.

In a previous e-mail it has been suggested to allow user buffers to be passed in. As was discussed before in another thread for "Serial". Turns out that just a few days afterwards a long customer discussion prompted us to actually implement exactly that on the read side of "Serial", and a few weeks later the same request came in for "I2S", for a different reason. So there is something to this idea, as it solves real problems. With "Wire" it's slightly different though. In theory the API as is would allow you an implementation with only 1 buffer, as transmit and receive cannot overlap. Even with adding async transfers, you could use one single buffer, as you can only have one outstanding transmit operation (it get's convoluted, but not that much). So the point is how would you allow different buffer sizes if it would affect 2 buffers for one implementation, but only 1 buffer for others. You really don't want to force a malloc() every time you do a "Wire.begin()" ...

A async transfer scheme based upon the upper variant of "requestFrom" seems to be reasonable, if one would assume that you can kick off the next one in a callback, while you have not read the data from the previous one. That means that the receive side of the buffer system has to work as a ringbuffer that can buffer more than one request. However this conflicts with the current API which more or less resets the read pointer for every blocking requestFrom ... Not a biggy as this would really affect only the async variant.

API naming. The STM32L4 variant has a sync variant of this "transfer" operation, and uses polymorphism to select (if there is no callback (even if NULL), then it's the sync variant. This turned out to be a bad idea. For the next big revision we will need to take the plunge and use "transferAsync" instead. So whatever API flavour is agreed upon here, labeling it as an "Async" operation is wise.

Now to the design failures of our API ... One thing that confuses users a lot if that you cannot reuse the buffers till the operation is complete. But that is really a consequence of avoiding extra buffering. The next idea was that there is no composite tx/tx2 operation, which would be nice to write to a device, say an EEPROM. Not sure that those are really biggies though ...

Can we leave SPI for now out of the discussion for complexity reasons ? It's a good oen to have as well, but "Wire" is really, really urgent for many of us. Having single platform solutions (Teensy's i2c_t3.h vs. STM32L4's Wire.transfer()) is not good in the longrun.

- Thomas



--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+unsubscribe@arduino.cc.

Andrew Kroll

unread,
Mar 18, 2017, 3:10:08 PM3/18/17
to devel...@arduino.cc
Couple of notes, observations and my own experiences.

First and foremost you want to be able to make things as easy as possible for the end user. Especially a person who is totally new to everything.
Second is that you really want to be able to make an API that allows a developer to be able to create a dependency in ways that will involve advanced programming. Which is pretty much us.

What we need to do is have our cake and be able to eat it too... But the cake is a lie! We can HIDE complexity behind the scenes. In order to do such a feat we only need to look at the Serial class as a perfect example of this:

  • Allows blocking and non-blocking.
  • Has a built-in generic callback as a weak function that the user can override in a generic way.
  • Atomic, even works in an ISR!
  • Has a very nice exposed API for ever situation I can find.
How would this apply here?

in order to solve problems like this it helps to make it bigger. In other words:
  1. describe what the goals are
  2. break the chunks down into smaller parts
  3. optimize and reuse what is a base API with what amounts to inline wrappers for the end user.
I think we pretty much have the first part done, so I'll happily suggest a one-fits-all possibility that directly applies to the second, based on what we already do for Serial, plus some thing that I think are missing and could be handy for many situations.

  • atomically buffer everything for your basic user method calls, maintain the transmissions and receptions using IRQs provided by the hardware. If there are no IRQs, you could use a timer, or poll in main()...  Buffers can be either or both hardware and software. Buffers sizes should be able to be settable IN THE SKETCH and have a default. No buffer in hardware? emulate it! Transfers can happen any way the dev of said API wants... ISR based, DMA, whatever. The end user shouldn't have to care.
  • An available method that returns 0 for nothing, < 0 for errors and > 1 for amount available
  • An availableForWrite, which tells the end user how much you can write before it is going to block you.
  • A XXXeventRead  the standard weak callback
  • A XXXeventWrite method (Missing from serial!!!) This would call when the write buffer has just became empty.
  • A peek method that allows you to look at the next available object without yanking it from the buffer. Yes, I said object. This would allow some really nice extensions to the find methods, and not disturb the data in the buffer, unless there was a match.
  • A buffer flush, which blocks until the buffer is empty.
... and then the rest of the methods that find objects in the data stream/packet

  • The find filters can become non-blocking and simply called on the read event.
  • Writing too much can be avoided
  • Knowing to read or write can be automatic in several ways for different situations
  • All the dirty details can be hidden from the novice user, but fully exposed to the advanced programmer.
I think this sort of approach totally beats to death proposals for specifying a callback. it would not be required.
All the other want and need would be fulfilled.

--
Visit my github for awesome Arduino code @ https://github.com/xxxajk

Thibaut VIARD

unread,
Mar 18, 2017, 3:30:31 PM3/18/17
to devel...@arduino.cc
+1, another set of good arguments.

By the way, is there any interest in keeping Wire (and SPI) as libraries, even if they are coming with core installations?

Thibaut

Thomas Roell

unread,
Mar 18, 2017, 3:41:58 PM3/18/17
to devel...@arduino.cc
Does it matter whether they are libraries or not? Latter one allows for to have per library examples... 

Thibaut VIARD

unread,
Mar 18, 2017, 4:15:48 PM3/18/17
to devel...@arduino.cc
No matter at all Thomas ;-)
Was just a side question.

Thomas Roell

unread,
Mar 18, 2017, 4:25:33 PM3/18/17
to devel...@arduino.cc
It's an interesting question. The other way around as well. Suppose I add RS485 support to Uart.h. Where are are the examples supposed to go? 

Thibaut VIARD

unread,
Mar 18, 2017, 5:19:50 PM3/18/17
to devel...@arduino.cc
In the core API case, all examples go to the initial default list (blink, etc...).

Paul Stoffregen

unread,
Mar 18, 2017, 5:28:12 PM3/18/17
to devel...@arduino.cc
On 03/18/2017 06:54 AM, Thomas Roell wrote:
> It's a good oen to have as well, but "Wire" is really, really urgent
> for many of us. Having single platform solutions (Teensy's i2c_t3.h
> vs. STM32L4's Wire.transfer()) is not good in the longrun.

Yes, you're right, platform specific APIs can't be good in the long
term. That why I haven't adopted Brian's i2c_t3.h API... at least not
yet....

I fear the Arduino ecosystem is fragmenting, much like commercial unix
from the 1980s to mid-1990s. If you and I and ChipKit and ESP and
regular STM32 craft 5 different APIs, and Arduino remains without an
important feature, various library authors implement it differently (and
often badly) with chip-specific register access, usually for only a few
of the official Arduino boards. This just can't be good for the
long-term health of the Arduino platform.

My hope is we can reach a consensus. I hope it can be concisely
stated. Or at least we could make this conversation inspiring, rather
than far more text than anyone would bother to read. I believe many of
the good ideas previously proposed here stalled only because they were
drowned in a sea of low-value & off-topic text.

But ultimately this is Arduino's call. Massimo, Cristian, when you guys
see this next week, please get involved on this one. Please? We all
want to work together and contribute to improving Arduino's ecosystem,
even if some of these long-winded messages may not necessarily feel that
way. This is the sort of platform level decision only you guys can
ultimately decide.


Brian Schmalz

unread,
Mar 18, 2017, 5:41:42 PM3/18/17
to Arduino Developer's List
I think that the best place for this leadership to come from would be Arduino. If there were an API set that Arduino adopted for I2C, I know we would attempt to follow it as closely as possible. Same for all the other standard libraries. We want to be as Arduino compatible as possible, but a lot of our users need to go outside the very 'simple' APIs that exits at this point to get more advanced features - like non-blocking I2C.

*Brian
chipKIT developer

Paul Carpenter

unread,
Mar 18, 2017, 6:00:11 PM3/18/17
to devel...@arduino.cc
I agree Arduino need to get a feel of how to progress things.

I quite like the starting point of Thomas's passing buffer addresses
as this has more flexibility. Personally I would pass the address of a
struct or class, for less parameter passing which I find many learners get
wrong with many parameters on each call.

This gives ability for any user size buffers and as many buffers as needed
Having a class or stuct means if something changes rarely pass just the
address across. Also you CAN make different objects for different I2C
devices. Having myself used things from GPIO, DAC, ADC, digital pots,
light meters, ASICs, FPGAs even security camera JPEG compression devices.

Despite others who feel users do not use error codes, well often
ALL of us do not use error or return values, but having them there give the
ability to add checks in development at least. Also being able to determine
if something has failed during operation. If you have a class you can have
standard places for status and error codes.

Most users do NOT have oscilloscopes, so if nothing else adding a print
after a transaction gives a start on getting data about software problems
as without data everything else is guesswork.

The classic return code IGNORED by ALMOST everyone is the number of bytes
sent on print, even at desktop and mainframe level.

The blocking nature of most core functions, lack of useful error codes
and too often having to track down problems into how the core or libraries
are doing things, has caused me to use other platforms for several jobs.
--
Paul Carpenter | pa...@pcserviceselectronics.co.uk
<http://www.pcserviceselectronics.co.uk/> PC Services
<http://www.pcserviceselectronics.co.uk/LogicCell/> Logic Gates Education
<http://www.pcserviceselectronics.co.uk/pi/> Raspberry Pi Add-ons
<http://www.pcserviceselectronics.co.uk/fonts/> Timing Diagram Font
<http://www.badweb.org.uk/> For those web sites you hate

Thomas Roell

unread,
Mar 18, 2017, 6:19:25 PM3/18/17
to Developers
As pointed out by somebody else one has to understand who the audience for such non-blocking code is.

My personal point of view is that it is not the novice, but the way more advanced user and the guru who writes the library for peripheral XYZ that makes use of that. 

I am having a hard time explaining to a entry level use who she/he might want to make use of that, because it requires an understanding of certain concepts.

The point if am driving at is that for such a non-blocking API I'd error on the side of efficiency over simplicity. If the bus bandwidth becomes a problem then the library writer for IMU xyz needs a way to make best use of the bus.

Having said that, personally I'd prefer a common shared approach over an island solution. Fragmentation is a problem. So any kind of solution needs to be as backwards compatible as possible (i.e. same error codes as base, old code MUST be able to work without changes, no modal extension that turns on/off callbacks and has odd side-effects if coe from multiple sources needs to work together).

- Thomas

Thomas Roell

unread,
Mar 18, 2017, 6:31:12 PM3/18/17
to Developers
Andrew,

I fullheartedly disagree with you.

The "Serial" example is pretty bad. UART traffic is stream based and async. I2C is transaction based, where you are either the master or the slave. The distinction cannot be hidden in any meaningful way.

On the second level "Serial" is quite bad in itself. The idea of "serialEvent()" being called every now and then without clear specified semantics is a constant source of bugs, and slows down any code that doesn't' really want to use it. It does not scale with the number of serial ports (on of the devices we support needs to deal with 7 serial type sources. There is no portable way to find out whether the chunk of bytes you wrote has been sent, unless you do the brute-force "Serial.flush()". On the other hand, there is no way to get a async callback when data arrives (so you can do some processing or buffering in the background). There is no way to actually reset the receive buffer to empty it (before say a baudrate switch). 

So "Serial" is a pretty bad stating point to begin with.

The idea of basing this ob weak aliases (over say explicite callbacks) is a problem too. There is way to much tricky stuff that depends on the linker to check whether you need or need not to call the code behind the weak alias (or undefined function like for "serialEvent").

Lastly please also consider the complexity and maintainabilty of the driver level code. The more convoluted approach are there to abstract things, the more code you require, and the slower it will execute. A nice balance is in order that allows a simple implementation that does not take away too much valueable FLASH space that the real user application could take advantage of.

- Thomas
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Paul Carpenter

unread,
Mar 18, 2017, 6:57:36 PM3/18/17
to devel...@arduino.cc
Totally agree and for most users the bus speed of most serial users is
9600 or 115200 or in one case I had 3 x 250000 (with long pauses, which
compared to 400 kHz I2C and the manipulations required is a different ball
game.

Serial has many issues as far as I am concerned and would now never use it
if a project relied on good serial communications.

I2C is basically a block transaction at its lowest level, where as most
serial is a stream of individual async bytes at its lowest level. That can
have any amount of gaps in it.
> * Allows blocking and non-blocking.
> * Has a built-in generic callback as a weak function that the
> user can override in a generic way.
> * Atomic, even works in an ISR!
> * Has a very nice exposed API for ever situation I can find.
>
> How would this apply here?
>
> in order to solve problems like this it helps to make it bigger. In
> other words:
>
> 1. describe what the goals are
> 2. break the chunks down into smaller parts
> 3. optimize and reuse what is a base API with what amounts to
> inline wrappers for the end user.
>
> I think we pretty much have the first part done, so I'll happily
> suggest a one-fits-all possibility that directly applies to the
> second, based on what we already do for Serial, plus some thing that
> I think are missing and could be handy for many situations.
>
> * atomically buffer everything for your basic user method calls,
> maintain the transmissions and receptions using IRQs provided
> by the hardware. If there are no IRQs, you could use a timer,
> or poll in main()... Buffers can be either or both hardware
> and software. Buffers sizes should be able to be settable *IN
> THE SKETCH* and have a default. No buffer in hardware? emulate
> it! Transfers can happen any way the dev of said API wants...
> ISR based, DMA, whatever. The end user shouldn't have to care.
> * An available method that returns 0 for nothing, < 0 for errors
> and > 1 for amount available
> * An availableForWrite, which tells the end user how much you
> can write before it is going to block you.
> * A XXXeventRead the standard weak callback
> * A XXXeventWrite method (Missing from serial!!!) This would
> call when the write buffer has just became empty.
> * A peek method that allows you to look at the next available
> object without yanking it from the buffer. Yes, I said object.
> This would allow some really nice extensions to the find
> methods, and not disturb the data in the buffer, unless there
> was a match.
> * A buffer flush, which blocks until the buffer is empty.
>
> ... and then the rest of the methods that find objects in the data
> stream/packet
>
> * The find filters can become non-blocking and simply called on
> the read event.
> * Writing too much can be avoided
> * Knowing to read or write can be automatic in several ways for
> different situations
> * All the dirty details can be hidden from the novice user, but
> fully exposed to the advanced programmer.
>
> I think this sort of approach totally beats to death proposals for
> specifying a callback. it would not be required.
> All the other want and need would be fulfilled.

Todd Krein

unread,
Mar 18, 2017, 10:25:06 PM3/18/17
to devel...@arduino.cc

I tend to agree.

 

But to throw out a slight alternative, having dug through the AVR Wire libraries and low level drivers a lot (particularly to get low-end AVRs and TINY’s working, both as master and slave), I really question if this library is worth evolving. I agree that backward compatibility is very important, and perhaps to beginning programmers the existing Wire mindset makes sense, so I really hesitate to change it and possibly break a lot of old sketches.

 

It’s clear that the majority of folks on this thread have used I2C long enough to have a better feel for the inherent block/transaction orientation of I2C, and perhaps even for the challenges of trying to emulate an I2C slave. (Shudder.) Very few people need to understand the complexities of I2C stop conditions, or restart, etc. The overwhelming majority, I wager, just want a ReadNBytesFromSlaveXAddressZ, and WriteNBytestoSlaveY/WriteNBytesToSlaveYAddressZ. (yes, yes, the address could be prepended into the buffer, but why make it more complicated for the newbie?) We also have a much wider selection of CPUs being supported that we did when the Wire library was first written, everything from high-end ST32’s to low-end Tiny’s from Adafruit.

 

Given that, I would suggest that we look at a new architecture for the I2C library (Call it Wire2 if you like), and re-implement it from scratch using the very good suggestions for non-blocking operation, adjustable buffer sizes, and better slave emulation support. I think is this one of the rare places where Arduino really got it wrong, so let’s stop trying to contort the old interface into something it really wasn’t designed to support.

To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

 

--
You received this message because you are subscribed to the Google Groups "Developers" group.

To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Todd Krein

unread,
Mar 18, 2017, 10:27:21 PM3/18/17
to devel...@arduino.cc
+1

I think this is a huge problem. When we're trying to teach kids how to use Arduino, it's hard enough to get them programming, much less making them have to worry about which processor variant they are using and how to configure it. Being able to tell someone "Pick whatever Arduino support board you like, and all you need is the arsuino.cc/reference web page" is incredibly powerful.

-----Original Message-----
From: Paul Stoffregen [mailto:pa...@pjrc.com]
Sent: Saturday, March 18, 2017 2:28 PM
To: devel...@arduino.cc
Subject: Re: [Developers] Re: Non-blocking extension to Wire library

--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Andrew Kroll

unread,
Mar 19, 2017, 2:09:37 AM3/19/17
to devel...@arduino.cc
I guess you missed the point. Queues actually can enhance performance.
You buffer (queue), send and get your replies in the background serially... 
This is why I said that it should be using objects, which could be anything from a uint8_t to a struct, or anything else you want to throw at it. You then can use the built-in callback. the callback does not have to be from main(), although for some SoC it MAY make sense to do that.
The point is, the regular or new user should not care how it is done, but know that it just gets done. Serial has a very easy to understand interface. You begin, and read/write *data*. Who cares if that is a byte or a packet? What is even better with something like i2c is that when you send something, you know you will get a reply back in the order that you sent them -- which is a SERIAL operation on OBJECTS.


To unsubscribe from this group and stop receiving emails from it, send an email to developers+unsubscribe@arduino.cc.

Paul Carpenter

unread,
Mar 19, 2017, 4:58:22 AM3/19/17
to devel...@arduino.cc
If you have I2C with just ONE GPIO port and update rates less than 100 Hz
Serial and all the overheads of the buffers madness MIGHT work for ONLY SLOW
applications with ONE device attached.

Serial is a point to point link, for random micro events

I2C is a BUS with UPTO 127 devices on the SAME bus, with block Transactions
that can in theory be 0 to infinity bytes long. In reality the Block
Transactions can be 1 byte to most I have seen is 1kB, 256 bytes for RAM,
EEPROM/FRAM, RTC, FLASH to read a page at a time is not uncommon.

The QUEUES of serial ARE the problem for I2C, as the buffer sizes of the
QUEUES are much bigger than the smallest element, and for MOST users who
could not notice a spin lock delay fine.

The fact that the buffers are FIXED size regardless of application or
number of ports you actually want to use.

Serial is fine if you are sending 3 byte sequences twice a second on
ONE serial port, and your application can sit spin locking as the strings
you suddenly want to print, are too much for TX buffer.

If your serial is only being used for Serial Monitor it is reasonable enough
but don't send too much in one go.

IT is WAY TOO RESTRICTIVE with its queues especially their sizing which is
INEFFICIENT organised

Paul Stoffregen

unread,
Mar 27, 2017, 7:47:51 AM3/27/17
to devel...@arduino.cc
Ten days ago I started this thread in hopes Arduino might adopt
non-blocking API for the Wire library. After 20 messages, there seems
to be strong consensus non-blocking I2C is valuable and needed, but of
course differing opinions on the details.

So far, there's been zero input from any Arduino Team members. It's
difficult to know if there is no interest from Arduino, or merely a lack
of time to read 20 lengthy messages.

Here's a short summary of the proposals.

1: Thomas Roell proposed adding a new Wire.transfer() function.

Wire.transfer(address, txBuffer, txSize, rxBuffer, rxSize, stop,
callbackFunction);
Wire.done();
Wire.status();

2: I proposed overloading Wire.endTransmission() & Wire.requestFrom().

Wire.endTransmission(stop, callbackFunction);
Wire.requestFrom(address, length, stop, callbackFunction);

3: Andrew Kroll proposed changing Wire's behavior closely mimic
Serial. I believe this could be best summarized "transfer bytes as you
go, rather than build up in a buffer and do all at once". (seems to be
consensus against this idea)

Wire.beginTransmission(address); - behavior change
Wire.availableForWrite();
Wire.endTransmission(stop); - behavior change
Wire.requestFrom(address, length, stop); - behavior change
Wire.available(); - behavior change

4: Paul Carpenter proposed defining more error codes for
Wire.endTransmission(), and raised many good points about implementation.

5: Todd Krein proposed Wire2, with a new API (to be determined?)

Please forgive me for glossing over many details in the lengthy messages
last week. I tried to keep this summary concise.

Rob Tillaart

unread,
Mar 27, 2017, 7:53:29 AM3/27/17
to Arduino Developers
Just a thought,

technically the TXbuffer could use the same memory as the RX buffer ?
ok retransmit is not possible anymore but it could reduce memory footprint by half

Thomas Roell

unread,
Mar 27, 2017, 7:57:38 AM3/27/17
to devel...@arduino.cc
It's not clearly specified anywhere last time I ran into that.

In theory you could start a new Wire.endTransmission() right after a Wire.requestFrom() and then afterward read the data from the last Wire.requestFrom(). It seems that the API implies that the read buffer index gets reset at the begin of Wire.requestFrom(), and the write buffer index gets reset at Wire.beginTransmission().

- Thomas

Thomas Roell

unread,
Mar 27, 2017, 10:06:32 AM3/27/17
to devel...@arduino.cc
Don't really want to bury Paul's summary and request to get some folks from the Arduino Core Team involved.

Wanted to summarize a couple of things but never got around to and forgot. Sorry about that.

Paul suggested to simply add a "callback" to "Wire.requestFrom()" and "Wire.endTransmission()". That is about where I started with for our async code as well, but discarded the idea for a bunch of reasons.

First off, the current implementation for AVR uses a 32 byte rx and 32 byte tx buffer. That is not enough for many use cases, like a I2C EEPROM or a GNSS using I2C, or a IMU that uses FIFO reads. Since there is no standard size, it's tricky to write portable code, other than limiting transfers to 32 bytes. It might be reasonable to increase the size, but then again space limited scenarios will be more tricky, especially if they don't require larger buffers.

In your callback for 'Wire.requestFrom()" you have to read back all data before you can issue the next "Wire.beginTransmission()" or "Wire.requestFrom()". So you spend possibly a lot of time in the callback, in an interrupt context. It seems to be wiser to pass pointers to buffers directly, so that the application can process the read back data in a non-interrupt context without the extra overhead.

Typical uses for async operations are sensor reads. Code for sensors (I2CDev library and such) use typically internal APIs like "readBytes(address, index, length, p_data)". If this style of API is used, there is a lot of overhead with the classical Wire API. Going async it means there are 2 callbacks (first for the index write, and then for the data read). That seems to be rather inefficient.

The Wire.transfer() approach I suggested solves all those problems elegantly. Here the code for the "readBytes()" (converted to be sync, just for clarity)

void readBytes(uint8_t address, uint8_t index, uint32_t length, uint8_t *data)
{
    Wire.transfer(address, &index, 1, data, length, true, NULL);

    while (!Wire.done()) { continue; }
}  


What I personally really dislike about the Wire API is this "stopBit" part. By breaking up composite write -> read transfers into 2 separate API sequences, this "stopBit" argument is needed. But at the risk of hanging the I2C bus. It's one of those hard to debug errors without logic analyzer. A composite "Wire.transfer()" API takes away this problem for most cases (except for write -> write). So less chance for the user to hang themselfs.

The argument about "Wire2" (or pick your name) about a new "Wire" class is interesting. I feel that sync and async Wire.transfer() kind of does exactly that. A new, fixed API, WITHOUT breaking the old API. It feels more like going from the single byte "SPI.transfer()" to "SPI.transfer(buf, count)"  (which BTW also uses a data pointer).

One last word about the implementation. For STM32L4 we only use async transfers for I2C. All the classic APIs and the sync "Wire.transfer()" are layered ontop of the async "Wire.transfer()" internally. Point being that going async does not mean necessarily a substantial increase in code size.

- Thomas


Massimo Banzi

unread,
Mar 27, 2017, 10:19:02 AM3/27/17
to Arduino Developers
Paul 

Sorry about not getting involved before in the discussion, I have been travelling a lot as usual.

I would not change the current Wire library massively because a lot of people use it and we don't want to break any existing code.

Either we add new methods to the library that implement the new behaviour or we got for a new library (which would probably be a bit of a problem because it would lead to intense bikeshedding)

Overloading current methods without breaking existing code is the way to go if we are able to use a consistent API style with the rest of the Arduino code.

I'm happy to go through the whole thread in mode detail later today and give you a more informed opinion

m


Massimo Banzi <m.b...@arduino.cc>

Todd Krein

unread,
Mar 27, 2017, 12:49:13 PM3/27/17
to devel...@arduino.cc

I don’t think that works if you’re multi-master.

 

From: Rob Tillaart [mailto:rob.ti...@gmail.com]
Sent: Monday, March 27, 2017 4:53 AM
To: Arduino Developers <devel...@arduino.cc>
Subject: Re: [Developers] Re: Non-blocking extension to Wire library

 

Just a thought,

To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

 

--
You received this message because you are subscribed to the Google Groups "Developers" group.

To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Thomas Roell

unread,
Mar 28, 2017, 10:22:12 PM3/28/17
to devel...@arduino.cc
Is "Wire" supposed to be multi-master capable via the standard API ?

- Thomas

To unsubscribe from this group and stop receiving emails from it, send an email to developers+unsubscribe@arduino.cc.

 

--
You received this message because you are subscribed to the Google Groups "Developers" group.

To unsubscribe from this group and stop receiving emails from it, send an email to developers+unsubscribe@arduino.cc.

--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+unsubscribe@arduino.cc.

Reply all
Reply to author
Forward
0 new messages