EcmaTC53 / spec

Ecma TC53 spec work
23 stars 11 forks source link

Is I²C read blocking or non-blocking? #4

Closed fivdi closed 3 years ago

fivdi commented 3 years ago

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 class read method, which is documented here, returns the data read which would indicate that it's blocking. The read method will have to access the target I²C peripheral to read the required data and therefore block.

Or have I misunderstood something?

phoddie commented 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.

dtex commented 3 years ago

... 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.

phoddie commented 3 years ago

@dtex - I agree that some additional text could help. Perhaps a non-normative note in the I²C section that briefly explains?

fivdi commented 3 years ago

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?

phoddie commented 3 years ago

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.

fivdi commented 3 years ago

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.

phoddie commented 3 years ago

@fivdi and @dtex - I've updated the text based on our discussion. Please let me know if I got it wrong. Thank you.

fivdi commented 3 years ago

Note: The read and write 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.

phoddie commented 3 years ago

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?

fivdi commented 3 years ago

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?

phoddie commented 3 years ago

@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.

phoddie commented 3 years ago

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:

fivdi commented 3 years ago

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 }
phoddie commented 3 years ago

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.

fivdi commented 3 years ago

Fair enough 😄 . I don't think I can provide any additional input for this issue so feel free to close it.