Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access to 'device' in platform SPI function? #9

Open
kaysievers opened this issue Aug 13, 2020 · 13 comments
Open

Access to 'device' in platform SPI function? #9

kaysievers opened this issue Aug 13, 2020 · 13 comments

Comments

@kaysievers
Copy link

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!

@trinamic-LH
Copy link
Contributor

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.

@trinamic-LH
Copy link
Contributor

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);
}

@kaysievers
Copy link
Author

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

@trinamic-LH
Copy link
Contributor

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?

@kaysievers
Copy link
Author

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.

@trinamic-LH
Copy link
Contributor

trinamic-LH commented Aug 19, 2020

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

@kaysievers
Copy link
Author

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.

@kaysievers
Copy link
Author

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

@trinamic-LH
Copy link
Contributor

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.

@trinamic-LH
Copy link
Contributor

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

@trinamic-LH
Copy link
Contributor

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.

@kaysievers
Copy link
Author

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

@trinamic-LH
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants