Closed fivdi closed 3 years ago
Good question.
I²C is different from digital, serial, and sockets with regard to how data is received. For digital, serial, and sockets data the remote end of the connection decides when to transmit data. For I²C, data is only ever sent in response to a request. This necessitates differences in the behavior of the API. Note that SPI is similar to I²C in this regard.
The full text from the IO Class Pattern about non-blocking behavior is relevant here:
All IO is non-blocking, consistent with ECMAScript API behavior on the web platform. That said, not all operations are instantaneous. Implementations determine how long is too long for a given operation.
The second sentence leaves the definition of non-blocking to implementations . I²C transactions are generally small (32 bytes is big). Over a modest 1 MHz connection I²C, that 32-bit transaction requires perhaps a couple milliseconds. Whether that is an acceptable delay depends on the application. In our experience, it is not a problem -- even when rendering animations on an MCU at 60 FPS.
A further consideration is that the specification is designed to efficiently support MCUs which do not provide asynchronous I²C. For such cases, this behavior is the most efficient choice.
The primary reason that an I²C would take a significant period of time is because of a timeout, typically because the device is no present. In practice, this is most likely at system start-up and represents a hard system failure, so the blocking behavior is the least of the problem. ;) The IO Class Pattern 's extensibility could be helpful here, for example by adding a timeout
property to allow the calling code to request a particular timeout duration.
We have explored truly asynchronous I²C in an independent Firmata implementation built on an implementation specification. The approach there is to first issue a read
with a count to queue up the transaction. Then, when onReadable
is invoked, the bytes are obtained by calling read
again, this time with no arguments. You can read more about that here. While the code presented is a bit verbose, the support for asynchronous reads could easily be wrapped in a module to present a more conventional and concise API.
... the specification is designed to efficiently support MCUs which do not provide
...
I had to remind myself of this many times over the past couple of years. The last paragraph of the Introduction aims to address this, but including some variation of the quoted text might make it more clear.
@dtex - I agree that some additional text could help. Perhaps a non-normative note in the I²C section that briefly explains?
Thank you for the detailed explanation. I now understand better.
A further consideration is that the specification is designed to efficiently support MCUs which do not provide asynchronous I²C. For such cases, this behavior is the most efficient choice.
When you say MCUs which do not provide asynchronous I²C do you mean MCUs which don't support non-blocking interrupt driven I²C and don't support non-blocking DMA dirven I²C? I can't actually think of such an MCU off hand but it surprises me that such an MCU would be an interesting target for running JavaScript. What would be an example of such an MCU that would be an interesting target for running JavaScript?
When you say MCUs which do not provide asynchronous I²C do you mean MCUs which don't support non-blocking interrupt driven I²C and don't support non-blocking DMA driven I²C?
That's a good definition.
I can't actually think of such an MCU off hand but it surprises me that such an MCU would be an interesting target for running JavaScript. What would be an example of such an MCU that would be an interesting target for running JavaScript?
The ESP8266 is a good example. The Moddable SDK has many examples of interesting results using modern JavaScript on that MCU. Of I²C, the Espressif data sheet for ESP8266 says (emphasis added):
Both I2C Master and I2C Slave are supported. I2C interface functionality can be realized via software programming...
Another consideration, not mentioned above, is that there are costs to asynchronous use of I²C – memory, code size, and time. For resource constrained systems, we don't want the specification to require a coding style with greater resource requirements. Consequently, asynchronous I²C is optional.
The ESP8266 is a good example.
It is indeed.
Feel free to close this issue unless you think it should remain open until this topic is addressed.
@fivdi and @dtex - I've updated the text based on our discussion. Please let me know if I got it wrong. Thank you.
Note: The
read
andwrite
methods may operate synchronously.
I somehow expected to see must rather than may here.
Does this mean that the read
method may operate asynchronously and return before it has read the data from the I²C peripheral?
Doing so does not violate the requirement that IO is non-blocking because these operations typically complete within a short period of time.
I think this might frighten some people away but I don't know how many. On the other hand, ESP8266 support should attract many.
I somehow expected to see must rather than may here.
There is no intention to prohibit asynchronous I²C operation. We already have both. I shared information about an asynchronous implementation of I²C above.
Keep in mind that the IO Class Pattern is not a specification for the full behavior of each implementation. Hosts need to have options because hosts have widely varying capabilities.
I think this might frighten some people away...
How so?
There is no intention to prohibit asynchronous I²C operation. We already have both. I shared information about an asynchronous implementation of I²C above.
Ok, I think I have finally gotten it now. My brain hadn't quite wrapped itself around the idea that a method can be implemented synchronously for one MCU and asynchronously for another MCU and that everything can still work.
I think this might frighten some people away...
How so?
When I wrote this, I hadn't quite accepted the fact that a method can be implemented synchronously for one MCU and asynchronously for another MCU and that everything can still work. I was still convinced that different implementations of the same method are either all synchronous or all asynchronous. Also, when thinking of an MCU, I wasn't thinking of an ESP8266, I was thinking of an STM32H743VI which, unlike the ESP8266, supports non-blocking interrupt driven and non-blocking DMA driven I²C. I assumed that if an STM32H743VI was being used, I²C would be synchronous as is the case with an ESP8266. This didn't appeal to me. Now that I know better, all is ok.
Out of curiosity, are you aware of another JavaScript API where a specific method is synchronous in one implementation and asynchronous in another implementation?
@fivdi, thanks for sticking with this. The IO Class Pattern does some things quite differently from the web because it has a different focus. I feel good about the foundation we've created. Still, flexibility in an API invariably creates some challenges.
Let's explore this a bit further, as perhaps there is some refinement to be made.
The API as ### specified works for both synchronous and asynchronous I²C. It relies on the client code knowing which of those the host supports. In embedded work, writing code that depends on a particular host is not at all unusual.
On systems that support asynchronous I²C, there are practical reasons to prefer synchronous I²C transactions. A sensor driver is a good example of that. Many (most) sensor drivers perform well with synchronous transactions and are much easier to develop with synchronous transactions. It would be something of a headache to maintain two versions of a sensor driver just to accommodate a host's choice in I²C. Alternatively, a driver could detect if I²C is asynchronous (by checking the result of read
) to have a single implementation that works on both. But, that is needlessly ugly and complex. I'd like developing sensor drivers to be straightforward to facilitate development of a large collection supported on a variety of MCUs.
Perhaps it would make sense to require I²C implementations to support synchronous operation and have the option to support asynchronous operation. The default would be synchronous. A client could request asynchronous with an option passed to the constructor. That would fail on synchronous-only hosts (like ESP8266). My assumption is that on devices with the capability and need to support asynchronous I²C, the overhead of also supporting synchronous I²C is acceptable, especially if it would allow for greater compatibility.
I realized this morning that the I²C implementation in the Firmata provider cannot support synchronous I²C (in theory, it could be done but since it is built on the asynchronous Serial API, it wold require a different implementation strategy).
That suggests two options for refining the above:
Many (most) sensor drivers perform well with synchronous transactions and are much easier to develop with synchronous transactions.
I agree that many sensor drivers perform well with synchronous transactions and that a lot could be achieved with synchronous transactions alone.
In general, I don't necessarily agree that sensor drivers are much easier to develop with synchronous transactions. Easier, yes, but much easier, not necessarily. I would agree that asynchronous sensor drivers are likely to be difficult to implement with the API that is being proposed by the specification. However, I think some of these difficulties can be attributed to the API rather than asynchronous programming in general. For me, the difficulties start with the read
method returning an ArrayBuffer that may or may not contain the data read from the sensor depending on whether read
is synchronous or asynchronous. If read
is asynchronous, the onReadable
callback needs to be implemented which may result in unwieldy code. To further complicate matters there are the readUint8
and readUint16
methods which would also have to be considered.
I'm now beginning to wonder if a lot of the potential difficulties related to asynchronous drivers could be resolved if read methods, irrespective of whether they are synchronous or asynchronous, returned a Promise
that will be resolved with an ArrayBuffer/unsigned 8-bit integer/unsigned 16-bit integer on success, or will be rejected if an error occurs.
For example, this Node code:
function simulateAsynchronousI2cRead() {
return new Promise((resolve, reject) => {
setTimeout(() => {
let i2cData = new Uint8Array([1, 2, 3, 4]);
resolve(i2cData.buffer);
}, 4);
});
}
function simulateSynchronousI2cRead() {
for (let i = 0; i != 1e7; i += 1) {
}
let i2cData = new Uint8Array([5, 6, 7, 8]);
return Promise.resolve(i2cData.buffer);
}
simulateAsynchronousI2cRead()
.then(console.log)
.then(() => simulateSynchronousI2cRead())
.then(console.log);
Outputs this:
ArrayBuffer { [Uint8Contents]: <01 02 03 04>, byteLength: 4 }
ArrayBuffer { [Uint8Contents]: <05 06 07 08>, byteLength: 4 }
In general, I don't necessarily agree that sensor drivers are much easier to develop with synchronous transactions. Easier, yes, but much easier, not necessarily.
I won't try to dissuade of you this position. Early on the committee recognized that there are different programming style preferences. You are advocating for one.
The committee choose to specify an API is that is as efficient as possible. That is consistent with the goal of supporting constrained devices. Node has a different goal.
The committee did work to ensure that is possible and straightforward to use the API specified to build other API styles. The listener event style is one and Promises are another. Your code sketch above also confirms that it is possible to build Promises on the API as specified. It also demonstrates the added runtime overhead.
The lowest level APIs are designed with two groups of developers in mind. One is developers that will use them, which is the perspective your comments focus on. The other is the developers implementing the APIs on the target hardware. Providing as direct a mapping as practical between the JavaScript and C APIs is important for this group. Promises are not a concept present in native IO APIs; synchronous operations and callbacks are.
It is straightforward to implement a promise based I2C (and SMBus) class building on the APIs specified. This would allow developers who prefer promises to opt-in to that style of programing without obliging all developers to use that style and without increasing baseline runtime overhead. Because the I2C API is standard across all hosts, this class could be written once by someone skilled in asynchronous JavaScript programming and used across a variety of silicon.
Fair enough 😄 . I don't think I can provide any additional input for this issue so feel free to close it.
The IO Class Pattern states that all IO is non-blocking, consistent with ECMAScript API behavior on the web platform.
On the other hand, the
I²C
classread
method, which is documented here, returns the data read which would indicate that it's blocking. Theread
method will have to access the target I²C peripheral to read the required data and therefore block.Or have I misunderstood something?