analogdevicesinc / TMC-API

TRINAMIC's IC API

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Access to 'device' in platform SPI function?

kaysievers opened this issue · comments

The platform glue currently looks like this:

extern void tmc2130_readWriteArray(uint8_t channel, uint8_t *data, size_t length);

Could it be changed to carry the device object instead of just the uint8_t? I want to support multiple SPI buses, and it's a bit awkward to need to maintain an entirely separate mapping of objects to channel numbers, just because the object is lost a single call earlier in the chain to this function.
The channel number is easily reachable from the object, so everything would work the same way; just without losing this information.

I copied the API files without any modifications to this driver: https://github.com/versioduo/V2Stepper/blob/master/src/V2Stepper.cpp#L3

Apart from this issue, I'm happy, and everything seems to work as expected. Thanks!

This was a deliberate design decision. We wanted the device object to be completely opaque to the application code. If it wasn't for memory allocation being simply done by declaring an instance of the device object struct we may have even hidden the struct members from the public interface (aka. the header files).

Since the API and therefore the device object do not hold any information regarding the SPI (or UART) implementation - e.g. a SPI bus object - there is a mapping of device objects to e.g. SPI bus objects required. There are multiple approaches to this mapping but the one we decided to (primarily) support was having arrays of both object types and using the indices as the mapping. Since the channel number is effectively a piece of user data, different implementation/usages of the channel could also be used. This has however not been documented well - sadly still a big ToDo in this project.

I see that in the application you linked you simply stored the chip select pin in the channel member. How would having the entire device object help you in that use case? From what i can tell you then would simply write digitalWrite(tmc2130->channel, LOW) instead?

I also want to note that a better name for channel would have been icNumber instead, since this is what is basically boils down to. I have so far decided against changing that but may do that parallel to getting proper documentation for the architecture of this API. This would also more strongly suggest our intention of this value being an index for arrays.

One of the biggest advantages (imo) of this approach is that it will easily scale to multi-chip applications which may even use multiple SPI busses with multiple Chip select lines used per bus:

struct SPIObject // This represents a SPI bus including the corresponding chip select
{
	struct SPIBus *bus; // SPI bus (without chip select)
	struct Pin *chipSelect;
}

// We have 6 IC and SPI objects
TMC2130Typedef tmc2130[6] = { 0 };
SPIObject SPIList[6] = {
	// 3 ICs on the first SPI bus
	{SPI0, CS0},
	{SPI0, CS1},
	{SPI0, CS2},
	// 2 ICs on the second SPI bus
	{SPI1, CS3},
	{SPI1, CS4},
	// 1 IC on the third SPI bus	
	{SPI2, CS5},
}

void tmc2130_readWriteArray(uint8_t channel, uint8_t *data, size_t length) 
{
	digitalWrite(SPIList[channel]->chipSelect, LOW);
	SPIList[channel]->bus->readWriteData(data, length);
	digitalWrite(SPIList[channel]->chipSelect, HIGH);
}

Sure, that's like placing a stone in the shoe in the morning, so that you can be so happy when you can get out of the shoe in the evening. :)

not sure what stone you're referring to here 🤔

Just to avoid talking in circles, what would your preferred code look like with the device object being passed instead of the channel number?

Exactly like all the other functions look like, which call the user-provided platform function:

extern void tmc2130_readWriteArray(TMC2130TypeDef *tmc2130, uint8_t *data, size_t length);

That way, tmc2130->config->channel is accessible as before, but there is also access to the object itself.

Maintaining a separate mapping is straight-forward for monolithic code, but does not fit that well into the model of a separately maintained library. The data (bus + address) is already available in the instantiated object, and it does not need to be copied to a separate global map, possibly with a fixed number of maximum instances.

How are you getting your SPI object from tmc2130 when using your version of the callback?

If we were to change this, it would be a breaking change - which is why im asking for a lot of clarification here (thanks for the patience!).

One more generalized solution might be to provide a void* to be used by the user. This could then point to whatever sibling/parent object is needed. That might actually be somewhat compatible - generating warnings but working i think? I will need to test around with that

How are you getting your SPI object from tmc2130 when using your version of the callback?

The tmc object is embedded in the parent/user object. By adding it as the first element, or by simple pointer math with offsetof(), any enclosing object can be reached from the enclosed object.

It's kind of the "C version" of what C++ inheritance does. All of Linux kernel device driver C code uses this: There it is called container_of() https://www.kernel.org/doc/Documentation/driver-model/design-patterns.txt

One more generalized solution might be to provide a void* to be used by the user.

Sure, a userdata pointer which is passed along would work too.

Here is a clean example without any further tricks. I changed the imported TMC API files to pass the device object, and added a userdata pointer to the object. The callback looks like this now:

void tmc2130_readWriteArray(TMC2130TypeDef *tmc2130, uint8_t *data, size_t length) {
  SPIClass *spi = (SPIClass *)tmc2130->userdata;

  spi->beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE3));
  digitalWrite(tmc2130->config->channel, LOW);
  spi->transfer(data, length);
  digitalWrite(tmc2130->config->channel, HIGH);
  spi->endTransaction();
}

The C++ constructor assigns the SPI object directly to the userdata field, and the callback can reach it (or anything else the user code would assign here):

constexpr V2Stepper(const Config &conf, SPIClass *spi, uint8_t pin_select, uint8_t pin_step) :
  config(conf),
  _pin_select(pin_select),
  _pin_step(pin_step),
  _tmc2130({.device = {.userdata = spi}}) {}

https://github.com/versioduo/V2Stepper/blob/master/src/tmc/ic/TMC2130/TMC2130.c#L13
https://github.com/versioduo/V2Stepper/blob/master/src/tmc/ic/TMC2130/TMC2130.h#L23
https://github.com/versioduo/V2Stepper/blob/master/src/V2Stepper.cpp#L5
https://github.com/versioduo/V2Stepper/blob/master/src/V2Stepper.h#L35

Thanks for the proof-of-concept.

We will test out your proposed change in a separate branch for one chip. Seeing as your code is using the TMC2130 i'd say we will use that one so you will be able to try out the changed API.

Since the changes will most likely break API compatibility we want to bundle them with other improvements that also break backwards compatibility. Alongside such an update we will get proper documentation going, so expect it to be a while before these changes are pushed to master.

(I think i should be able to get to this by next week. I'll write updates in this issue)

The suggested changes to the API are available in the API_Revision branch. The suggested changes of issue #11 will be tried out in that branch as well.

Great. Thanks a lot.

I've imported the new files and updated the code which calls the new API. It works all fine and looks good.

https://github.com/versioduo/V2Stepper/blob/master/src/Driver.cpp#L5
https://github.com/versioduo/V2Stepper/blob/master/src/V2Stepper.h#L14

I'm happy with this the outcome of this change. We will however wait with rolling this out for all boards until we have evaluated some other backwards-incompatible changes.

I'm leaving this issue open for now until the rollout starts