-
Notifications
You must be signed in to change notification settings - Fork 2k
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
periph/gpio: support for extension API #12333
Conversation
Low level `gpio_*` functions that are realized by CPUs are renamed to `gpio_cpu_*`. The declaraions of GPIO API functions `gpio_*` are replaced by inline functions. If the `extend_gpio` module is not enabled, these functions simply forward the calls to `gpio_cpu_*` functions. If module `extend_gpio` is enabled, these functions are overridden by the `extend_gpio` module.
8afa27a
to
ba34c20
Compare
Low level `gpio_*` functions that are realized by CPUs are renamed to `gpio_cpu_*`.
ATmega 2560 has more than 8 port. The `GPIO_PIN` definition uses 4 bits for the pin number of a port and 4 bits for the port number.With a `gpio_t` size of 8 bit, there is no space for extesion API bit. Therefore, the size of gpio_t is changed to 16 bit.
Compile time tests for collisions of `gpio_t` values as generated by the CPU and `gpio_t` values used by the GPIO extension API.
Defines an example GPIO extension device configuration using software extension devices for testing that GPIO extension API is working.
ba34c20
to
7d28f3e
Compare
@kaspar030 Next try 😉 |
drivers/include/extend/gpio.h
Outdated
return; | ||
} | ||
#ifdef MODULE_PERIPH_GPIO | ||
gpio_toggle(pin); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be gpio_cpu_toggle(pin);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ups, of course. I thought I fixed it already after a static test failed.
Have you checked what the impact on RAM/ROM would be, if the lower level interface wouldn't be split up into the extension and periph case, but would use the However, calling For network drivers we already have a single lower level API with the |
In principle, the first approach
I tested this only for Atmega 2560 since I had too many open problems with the other architectures. Even it produces a bit less code size, it requires much more RAM. Not really clear why. Using Therefore @ZetaR60 proposed the approach to redirect the call in case of extension GPIOs. It is the approach with minimum changes and minimum risk since everything keeps working as. The price is that the performance slightly reduces and the code size increases at bit. |
Calling via function pointer is an indirect call, calling via symbol name is a direct call. An indirect has several disadvantages in terms of performance over a direct one:
But: On embedded platforms the performance penalty should be much less as far as I know, as e.g. advances stuff like dynamic branch prediction is not widely used. (E.g. the Cortex M7 has dynamic branch prediction, but the Cortex M3, M0, M0+ do not have.)
I bet this is again because the ATmegas are a Harvard architecture platform rather than using a von-Neumann architecture. (E.g. on von-Neumann platforms like ARM every variable that is marked as |
Oh yeah, of course, you are right, my fault. Sorry, I was a bit under time pressure when I tried to answer. So you were asking what the impact on RAM/ROM would be if we would always use
Right? This is something I did not test because the changes that would be necessary are really huge and risky. |
OK, I would like to give it a try to see whether this would work out, or whether this would impose to much overhead. I hope I can find the time for that soon. (But that is out of scope of this PR and should not delay this PR.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Some comments inline. Generally, this should allow extending the GPIO API fully compatible with no performance overhead if the extension module is not loaded. In case of compile time constant gpio_t
values, the compiler should optimize out the check on whether it is provided via gpio_cpu_<foo>()
or an GPIO extender even without LTO enabled.
The only disadvantage I can see is that the ABI has changed. I have absolutely no idea if anyone needs ABI compatibility, or if this is a non-issue. In case it is desired to have ABI compatibility: In the case that the extension API is not used it could be achieved by telling the linker that symbol gpio_<foo>
is an alias for symbol gpio_cpu_<foo>
. In case that the extension API is used, backward ABI compatibility is more involved. (And one could argue that the ability to use GPIO extenders with the GPIO API is new, so code compiled prior to the addition of this API and wanting ABI compatibility never worked GPIO extenders, so there is no regression).
#define SPI0_CS0_GPIO GPIO15 /**< HSPI / SPI_DEV(0) CS default pin, only used when cs | ||
parameter in spi_acquire is GPIO_UNDEF */ | ||
#define SPI0_CS0_GPIO GPIO15 /**< HSPI / SPI_DEV(0) CS default pin, only used when cs | ||
parameter in spi_acquire is GPIO_UNDEF */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated. (If you split this up into a separate PR, I'll ACK and merge that right away.)
@@ -35,19 +35,19 @@ extern "C" { | |||
* @{ | |||
*/ | |||
#define HAVE_GPIO_T | |||
typedef uint8_t gpio_t; | |||
typedef uint16_t gpio_t; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to have a performance penalty, as the ATmegas are 8bit. I think the defines in gpio_ext_h
already are using sizeof(gpio_t)
, so would it be possible to keep that uint8_t
for 8 bit platforms? (If the cost is that only one GPIO extender could be used there, I bet that users of the ATmegas would take that trade-off. And if not, they could use CFLAGS += -include <header_with_custom_gpio_t_definition.h> -DHAVE_GPIO_T
to use more GPIO extenders.)
But if this increases maintenance effort and/or lines of code more than a bit, I'd say using 16bit on ATmesags is fine. (The Arduino community are also using int
(=16 bit) for digitalRead()
and digitalWrite()
- likely due to not paying attention when defining the API. And their user base seems to be not upset with that.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know. But ATmega 2560 defines 10 ports and GPIO_CPU_PIN(x, y)
is defined by ((x << 4) | y)
. That is, with uint8_t
there is no space left for the extension flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe then 8 bit unless ATmega2560 is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be possible in that way.
drivers/extend/gpio_ext.c
Outdated
* @} | ||
*/ | ||
|
||
#if MODULE_EXTEND_GPIO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be #ifdef
instead of #if
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since enabled modules lead to defintions like
#define MODULE_EXTEND_GPIO 1
in <app>/bin/<board>/riotbuild/riotbuild.h
, the simple #if
works as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was more worried about the case the module is not used, in which MODULE_EXTEND_GPIO
is not defined
drivers/extend/gpio_ext.c
Outdated
#if MODULE_EXTEND_GPIO | ||
|
||
#include "periph/gpio.h" | ||
#include "extend/gpio.h" | ||
#include "gpio_ext_conf.h" | ||
|
||
#define ENABLE_DEBUG (0) | ||
#include "debug.h" | ||
|
||
#endif /* MODULE_EXTEND_GPIO */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't checked the (Update: It is needed.)#include
ed headers yet, but is this seems to be empty. Is this file needed?
If I recall correctly, the C standard forbids an empty complication unit. So you should add typedef int dont_be_pedantic;
at least in the case MODULE_EXTEND_GPIO
is not defined to have the file confirming with the standard. (Even though both GCC and clang don't care about empty compilation units, I believe there is no harm in being strictly compliant with the standard.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not remember, but there must have been a reason why I added this empty .c
file. Maybe it had something to do with the order in which gpio_ext_conf.h
was included. I will check it again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the file could be removed.
ABI? |
@maribu There is a further problem with this approach. The goal was to have the same low level interface for all devices to be able to use GPIO extensions in the same way as MCU GPIOs. But using the same mask for all devices will not be possible, for example, if a GPIO extender offers 16 pins per port but the MCU only 8 bit and vise versa. Also GPIO extenders of different size would be a problem. |
@maribu I went through all
@maribu What do think about the following questions:
|
Ping @maribu Did you have some time to take a look on my comment #12333 (comment)? |
@kaspar030 @maribu At the moment I have some time left and I'm willing to invest it in the extension API. Therefore, I would like to know how to proceed and how to proceed with the suggestion of the masked GPIO API at a low level. I am very interested in an extension API and have spent a lot of time in this PR. Therefore again the question, whether this PR wouldn't make sense as a short-term solution. |
There is obviously no interest on GPIO extensions 😟 |
I disagree. But sadly I currently do not have an answer to what the best approach would be. This is a non-trivial task, and sadly I currently have a bunch of other deadlines to fulfill. |
The reason here is that
For peripheral ports this should always be a single access as far as I know. E.g. on SAMD this is implements
This is again an implementation choice. The toggle port register would allow doing this in a single access. (I think the motivation was to sacrifice performance to improve readability of the code.)
I'd say the following interface would be pretty generic: typedef struct {
gpio_mask_t direction; /**< A 1 bit sets the corresponding bit to output, a zero to input */
union {
gpio_mask_t pull_resistors; /**< A 1 bit of a corresponding output pin is used to enable pull resistors (instead of floating) */
gpio_mask_t open_drain; /**< A 1 bit of a corresponding input pin is used to enable open drain (instead of push-pull) */
};
/**
* @brief For output pins: The pull direction. For open-drain input pins: If pull ups are used
*/
gpio_mask_t pull_up;
} gpio_config_t;
typedef struct {
void gpio_port_write(gpio_port_t port, gpio_mask_t values);
void gpio_port_set(gpio_port_t port, gpio_mask_t pins_to_set);
void gpio_port_clear(gpio_port_t port, gpio_mask_t pins_to_clear);
gpio_mask_t gpio_port_read(gpio_port_t port);
void gpio_port_set_config(gpio_port_t port, const gpio_config_t *config);
void gpio_port_get_config(gpio_port_t port, gpio_config_t *config);
} gpio_driver_t; |
Thanks a lot for your comments 😄 I'm sorry for pushing too much, I was probably a bit too impatient 😎 The reason was that in the last 2 weeks I had a lot of time to work on it, but in the next few weeks I will not have time for it. In addition, I have proposed an approach with this PR based on the work of @ZetaR60 a year ago. I gave a detailed explanation of why I decided to suggested this approach again. Obviously we could not find an agreement again. After that, I spent a lot of time on various performance tests to compare your and @kaspar030's suggestions. So I just wanted to know what approach I should implement in order to spend the work in the right direction and not to waste time again. And finally, I just wanted to know whether this PR could have been a short-term solution at minimum of changes with a minimum of risk and at acceptable performance decrease if GPIO extension API is enabled at all. |
I believe that his feature is essential, for example for I2C bit-banging implementations where passive HIGH is realized as OD output with pull-ups. The protocol reads bus lines after setting it to passive HIGH to check whether someone else is driving it actively to LOW to recognize arbitration losses and optionally clock stretching. |
Concerning |
Such a change of the low-level API might have consequences that we can't see at the moment. We have to be sure that all aspects and concerns are taken into account. So I wanted to ask you if you could imagine to open an RFC issue with your proposal and to invite the stakeholders to a discussion. |
Maybe even better would be a prototype implementation for two platforms as a proof of concept. That would also allow to verify that the idea is actually doable. |
Agreed. Who should do that. On one hand, you know at the best what your ideas were when you suggested the masked low level API and |
@maribu Are typedef struct {
...
} gpio_config_t; and typedef struct {
...
void gpio_port_set_config(gpio_port_t port, const gpio_config_t *config);
} gpio_driver_t; thought to be a replacement of the low level BTW, I have started with the first small changes. |
That would basically be the HAL on which |
Hmm. The implementations I was puzzled about have checked the input or output register, depending on the direction. My knowledge is limited in regard to different semantics of the memory mapped GPIO registers out there. But if separate input and output registers are provided, I would be very surprised to ready anything back from of the output register but the exact value that was written to it. In the scenario with OD output and pull-ups, I expect the following behavior: If one would set the bit x to one in the output register, pin x would go passive high. And reading back the output register would always result in bit x being one, regardless of physical state. But bit x in the input register would be zero if someone externally keeps the line low, but one if the state would reach high. If those assumptions are correct, that implementation would actually prevent detecting clock stretching, rather than allowing it. Just always checking the input register on |
Thanks a lot for your very comprehensive answer.
Completely agreed. Unfortunately, I'm not familiar enough with other platforms. I can only tell how it works on ESP32. To be able to read from a pin, the signal has to be routed through a IO matrix to the input register. Otherwise, you would just read the initial value from this input register. That is, if the port is set to Unfortunately, the documentation of the API is not very clear. It just says "a pin can be configured as input or output", which means either input or output but not both. For BTW, with #11950 I provided a fix for ESP32 GPIOs (at least I thought it is a fix) to return the last written value for output ports on |
@maribu Next question. What should the pin mask be or how should we handle it if you have ports with different widths? For example, the AVR MCU has a port witdth of 8 bits while the GPIO extender has a port width of 16 bits or vise versa, the MCU has a port width of 32 bits and the GPIO extender with only 16 bits. Do you have any idea how to deal with it? The only solution I see would be to use always 32 bits as pin mask and the according port uses only the part it supports. |
To me, the pin mask would be some unsigned integer of a width that fits the requirements of all backends compiled in. If on AVR only the periph GPIOs are used, and I would use the build system to resolve the requirements of each implementation. Let's say we would abuse #if defined(MODULE_GPIO_MASK_32BIT)
typedef uint32_t gpio_mask_t;
#elif defined(MODULE_GPIO_MASK_16BIT)
typedef uint16_t gpio_mask_t;
else
typedef uint8_t gpio_mask_t;
#endif Additionally it will be need to be defined if the first pin should be the least significant bit in the mask, or the most significant bit. On AVR and STM32 the least significant bit in the register corresponds with the first pin. Ideally, every other hardware also uses lsb for the first pin. But in any case: As the width of the mask is not guaranteed to match the number of pins (it can be bigger), it will just be easier to always use the least significant bit as the first pin. For that reason I would just define it that way and let the driver take care of it, if it is different. (Sounds bad, but e.g. on ARM there is the
|
@maribu I have an experimental implementation of following masked low-level GPIO functions for STM32 gpio_mask_t gpio_cpu_read(void *dev);
void gpio_cpu_set(void *dev, gpio_mask_t pins);
void gpio_cpu_clear(void *dev, gpio_mask_t pins);
void gpio_cpu_toggle(void *dev, gpio_mask_t pins);
void gpio_cpu_write(void *dev, gpio_mask_t values); The benchmarks of high-level functions of this implementation are:
The following questions have arisen in relation to these functions:
Further questions that are related to the other functions are:
Therefore, masked versions of these functions would have to iterate bit-wise over the mask after the high level functions produced the mask. This seems to be contradictory. |
Sounds like that the port-wide configuration function indeed makes implementations harder. (And for platforms that would allow to implement this easily, the additional work to provide a pin specific
That indeed seems to be something that could be nice to have for a user. But that could be just a
That is a good question. This seems to be something that could end up very complex to implement. Maybe it makes sense to declare the port write function as an optional feature and only provide it, if it can be implemented in a safe way. (So that only pins that are explicitly configured as some flavor of output to change their state, and pins used for SPI/I2C/GPIO input/... are guaranteed to be unaffected by an call to the port's write() function.) With that, a mask is not needed for safe usage of that function. But that So my gut feeling is that a port write() function and one that additionally takes a mask should be something that is likely best provided as an optional feature, and avoided for use in the GPIO pin API on top of the port API. |
@kaspar030 I guess the following is what you tried to suggest, right? /**
* @brief GPIO device type
*
* A GPIO device is a hardware component that provides a number of GPIO
* pins, e.g. a GPIO extender. It is defined by a device descriptor that
* contains the state and parameters of the device, as well as an associated
* driver for using the device.
*
* @note The GPIO device type isn't used for MCU GPIO ports.
*/
typedef struct {
void *dev; /**< device descriptor */
const gpio_driver_t *driver; /**< associated device driver */
} gpio_dev_t;
/**
* @brief GPIO port type
*
* A GPIO port allows the access to a certain number of GPIO pins. It is either
*
* - a register address in the case of MCU GPIO ports or
* - a pointer to a device of type `gpio_dev_t` which provides a number
* of GPIO pins, e.g. a GPIO extension device.
*/
typedef union gpio_port {
const gpio_reg_t reg; /**< register address of a MCU GPIO port */
const gpio_dev_t* dev; /**< pointer to a device that provides the GPIO port */
} gpio_port_t;
/**
* @brief GPIO pin type definition
*
* A GPIO pin is defined by a port that provides the access to the pin and
* the pin number at this port.
*/
typedef struct {
const gpio_port_t *port; /**< pointer to the port */
gpio_pin_t pin; /**< pin number */
} gpio_t;
/**
* @brief Get the driver of a GPIO port
*/
static inline const gpio_driver_t *gpio_driver_get(const gpio_port_t *port)
{
#if MODULE_EXTEND_GPIO
if ((port->reg & GPIO_CPU_REG_MASK) == GPIO_CPU_REG_GPIO) {
return &gpio_cpu_driver;
}
else {
return port->dev->driver;
}
#else
(void)port;
return &gpio_cpu_driver;
#endif
}
...
static inline int gpio_init(gpio_t gpio, gpio_mode_t mode)
{
const gpio_driver_t *driver = gpio_driver_get(gpio.port);
return driver->init(gpio.port, gpio.pin, mode);
} |
The static inline const gpio_driver_t *gpio_driver_get(const gpio_port_t *port)
{
if (!IS_USED(MODULE_EXTEND_GPIO) ||
(port->reg & GPIO_CPU_REG_MASK) == GPIO_CPU_REG_GPIO))
{
return &gpio_cpu_driver;
}
else {
return port->dev->driver;
}
} |
exactly! |
Yes, the question is whether it's more readable 😉 |
@maribu I checked all |
Contribution description
Introduction
This PR is a little rework and the completion of PRs #9860 and #9958 to implement the GPIO extension API according to the proposal in issue #9690.
After a long discussion in issue #9690, all approaches proposed by @kaspar030 (thanks again for his great ideas) in #9690 (comment) were tried out. However, it seems that the approach initially proposed by @ZetaR60 seems to be the best tradeoff of required changes, additional code/data size, performance decrease and unnoticed side effects.
Approaches in Detail
The first approach proposed in #9690 (comment) uses a structured GPIO type like the following
and would allow a completely invisible integration of extension devices by using the
GPIO_PIN
macro for CPU GPIOs as well as GPIO extension pins. That's really great. But, it required to change 275 existing files. Furthermore, a number of unsolved problems in assignment and comparison of structured pin definitions remained. To solve these assignment and comparison problems, all CPU specific and drivers would have to be changed to use the structuredgpio_t
consequently what seems to be very risky. And finally, the code/data size was more than with the implementation of this PR, e.g., for ATmega2560:Although the second approach proposed in #9690 (comment) that uses the structured GPIO type too, but uses a separate
GPIO_EXT_PIN
macro for GPIO extension pins could solve most assignment and comparison problems, there are still 275 files to be changed (without device drivers). Even though the code size/data could be reduced, e.g., for ATmega2560the approach has still a high risk of side effects caused by the changes.
Approach in this PR
The approach implemented in this PR uses the existing
gpio_t
value type as follows.A single bit in
gpio_t
value is used (by default the MSB) to divide between CPU GPIO pins and GPIO extension pins. This bit is used for redirecting a callgpio_*
to the extension API. The only change in CPU specific implementations is that thegpio_*
functions were renamed togpio_cpu_*
. For the use of GPIO extension pins, macroGPIO_EXT_PIN
is introduced.GPIO_PIN
macro is used for CPU GPIO pins as before.This approach makes the introduction of the extension API invisible for existing code. Therefore, only 24 files had to be changed and there is no risk of side effects.
The additional code/data sizes required if GPIO extension devices are used (module
extend_gpio
enabled) for all architectures are (measured withtests/periph_gpio
):To check for collisions of GPIO extension pin values with CPU GPIO pin values, the PR also includes a compile time test
tests/periph_gpio_coll
.Testing procedure
Compile time test
tests/periph_gpio_coll
has to succeed.tests/extend_gpio
has to succeed.Issues/PRs references
Rework of PR #9860 and PR #9958
Implements issue #9690 for GPIOs