-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: sensors abstraction #4
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| #ifndef DEFS_H | ||
| #define DEFS_H | ||
|
|
||
| #include <stddef.h> | ||
| #include <stdbool.h> | ||
| #ifdef TEST | ||
| // Mark functions you want to unit test with STATIC. We expose everything | ||
| // through `struct sensor` and `struct flash` to avoid leaking implmentation. | ||
|
|
@@ -24,33 +26,211 @@ | |
| #endif | ||
| #endif // end TEST | ||
|
|
||
| /// Common abstraction over SPI, UART, I2C | ||
| /// Use this handle struct when a sensor could be configured to use more than of | ||
| /// the protocols or if the sensor uses a protocol that might be disabled, like | ||
| /// UART or I2C. This helps isolate ifdefs to only implementation files. | ||
| enum protocol { SPI, UART, I2C }; | ||
| struct handle { | ||
| enum protocol protocol; | ||
| union { | ||
| //// | Handle Abstractions | | ||
| //// We provide a thin abstraction over the built-in STM32 handles for two | ||
| //// main reasons | ||
| //// 1. compile time errors, not all projects will enable all modules | ||
| //// 2. group required data together | ||
| //// For example, the spi handle requires CS ports and pins to work, so it is a | ||
| //// good idea to group them together. The compile time errors are due to | ||
| //// missing definitions, when HAL_I2C_MODULE_ENABLED is not defined, no I2C | ||
| //// functions are imported, which results in errors accross this code base. | ||
| //// | ||
| //// To resolve that, we gate code using ifdefs in this centralized location | ||
| //// and replace missing definitions with an empty one, which will throw an | ||
| //// error at compile and run-time when you try to use them. | ||
|
|
||
| /// SPI Handle Abstraction | ||
| /// Aside the handle typedef, a port and pin representing the CS is required | ||
| #ifdef HAL_SPI_MODULE_ENABLED | ||
| struct handle_spi { | ||
| SPI_HandleTypeDef *handle; | ||
| GPIO_TypeDef *port; | ||
| uint8_t pin; | ||
| }; | ||
|
|
||
| #define HANDLE_SPI(in_handle, in_port, in_pin) \ | ||
| (struct handle) { \ | ||
| .protocol = SPI, \ | ||
| .serial = { \ | ||
| .spi = { \ | ||
| .pin = (in_pin), \ | ||
| .port = (in_port), \ | ||
| .handle = (in_handle), \ | ||
| } \ | ||
| } \ | ||
| }; | ||
| #else | ||
| struct handle_spi { int placeholder; }; | ||
| #define HANDLE_SPI(handle, port, pin) assert(0 & "SPI is not enabled!"); | ||
| #endif | ||
|
|
||
| /// I2C Handle Abstraction | ||
| /// Aside the handle typedef, the address of the device on the line is required | ||
| #ifdef HAL_I2C_MODULE_ENABLED | ||
| struct handle_i2c { | ||
| I2C_HandleTypeDef *handle; | ||
| uint32_t address; | ||
| } i2c; | ||
| struct handle_i2c { | ||
| I2C_HandleTypeDef *handle; | ||
| uint32_t address; | ||
| }; | ||
|
|
||
| #define HANDLE_I2C(in_handle, in_address) \ | ||
| (struct handle) { \ | ||
| .protocol = I2C, \ | ||
| .serial = { \ | ||
| .i2c = { \ | ||
| .address = (in_address), \ | ||
| .handle = (in_handle), \ | ||
| } \ | ||
| } \ | ||
| }; | ||
| #else | ||
| struct handle_i2c { int placeholder; }; | ||
| #define HANDLE_I2C(handle, address) assert(0 & "I2C is not enabled!"); | ||
| #endif | ||
| // We always have spi present, so we don't have to gate it | ||
| struct handle_spi { | ||
| SPI_HandleTypeDef *handle; | ||
| GPIO_TypeDef *port; | ||
| uint8_t pin; | ||
| } spi; | ||
|
|
||
| /// UART Handle Abstraction | ||
| /// Only the handle typedef is required | ||
| #ifdef HAL_UART_MODULE_ENABLED | ||
| struct handle_uart { | ||
| UART_HandleTypeDef *handle; | ||
| } uart; | ||
| struct handle_uart { UART_HandleTypeDef *handle; }; | ||
|
|
||
| #define HANDLE_QSPI(handle) \ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HANDLE_UART?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, should be handle QSPI |
||
| (struct handle) { \ | ||
| .protocol = UART, \ | ||
| .serial = { \ | ||
| .uart = { .handle = handle } \ | ||
| }; | ||
| #else | ||
| struct handle_uart { int placeholder; }; | ||
| #define HANDLE_UART(handle) assert(0 & "UART is not enabled!"); | ||
| #endif | ||
|
|
||
| /// QSPI Handle Abstraction | ||
| /// Only the handle typedef is required | ||
| #ifdef HAL_QSPI_MODULE_ENABLED | ||
| struct handle_qspi { QSPI_HandleTypeDef *handle; }; | ||
|
|
||
| #define HANDLE_QSPI(handle) \ | ||
| (struct handle) { \ | ||
| .protocol = QSPI, \ | ||
| .serial = { \ | ||
| .qspi = { .handle = handle } \ | ||
| }; | ||
| #else | ||
| struct handle_qspi { int placeholder; }; | ||
| #define HANDLE_QSPI(handle) assert(0 & "QSPI is not enabled!"); | ||
| #endif | ||
| } def; | ||
|
|
||
| //// | Unified Handle Abstractions | | ||
| //// This handle uses a tagged union to allow a sensor to switch between | ||
| //// different serial protocols. You may pass in a handle with the I2C protocol | ||
| //// or SPI protocol and it will use the correct read and write functions. | ||
| //// | ||
| //// To create a handle, you MUST use the following methods | ||
| //// struct handle spi = HANDLE_SPI(&hspi1, port, pin); | ||
| //// struct handle i2c = HANDLE_I2C(&hi2c1, address); | ||
| //// struct handle uart = HANDLE_UART(&huart1); | ||
| //// struct handle qspi = HANDLE_QSPI(&hqspi); | ||
| //// Then you may pass the handle to the initialization function of a device. | ||
|
Comment on lines
+128
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not certain that this abstraction is needed.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comment. |
||
|
|
||
| enum protocol { SPI, UART, I2C, QSPI }; | ||
| struct handle { | ||
| enum protocol protocol; | ||
| union { | ||
| struct handle_spi spi; | ||
| struct handle_i2c i2c; | ||
| struct handle_uart uart; | ||
| struct handle_qspi qspi; | ||
| } serial; | ||
| }; | ||
|
|
||
| static inline struct handle_spi* handle_as_spi(struct handle *handle) | ||
| { | ||
| return handle->protocol == SPI ? &(handle->serial.spi) : NULL; | ||
| } | ||
|
|
||
| static inline struct handle_i2c* handle_as_i2c(struct handle *handle) | ||
| { | ||
| return handle->protocol == I2C ? &(handle->serial.i2c) : NULL; | ||
| } | ||
|
|
||
| static inline struct handle_uart* handle_as_uart(struct handle *handle) | ||
| { | ||
| return handle->protocol == UART ? &(handle->serial.uart) : NULL; | ||
| } | ||
|
|
||
| static inline struct handle_qspi* handle_as_qspi(struct handle *handle) | ||
| { | ||
| return handle->protocol == QSPI ? &(handle->serial.qspi) : NULL; | ||
| } | ||
|
|
||
| //// | Serial API Abstractions | | ||
| //// All serial protocols implement an API with the same write and read | ||
| //// functions, with differing implmentations of course. The serial api contains | ||
| //// the handle and is requested by a device in its initialization function. | ||
| //// | ||
| //// See the currently provided drivers for an example. But the gist is | ||
| //// int8_t sensor_init(struct sensor_ctx *ctx, struct sensor *sensor, struct handle *handle) | ||
| //// { | ||
| //// assert(handle->protocol == SPI); | ||
| //// sensor->ctx = ctx; | ||
| //// sensor->read = sensor_read; | ||
| //// serial_api_spi(ctx->api, handle); | ||
| //// } | ||
| //// Of course you may decide to support multiple protocols if you wish, which | ||
| //// this abstraction makes really simple. You would pass `api` around and call | ||
| //// `api->write(...)` and `api->read(...)` for writing and reading respectively. | ||
|
Comment on lines
+166
to
+181
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate the comments, they are very helpful (nothing to resolve I just want to say that I like them) |
||
|
|
||
| struct op_params { | ||
| // For sensors, cmd is where you pass either the address of the register | ||
| // to access or an actual opcode to send to the device | ||
| const void *cmd; | ||
| const size_t cmd_size; | ||
|
|
||
| // For reads, this is the buffer to read into | ||
| // For writes, this is the buffer to transmit | ||
| // Set buffer_size to zero to transmit or read nothing | ||
| void *buffer; | ||
| const size_t buffer_size; | ||
|
|
||
| // Used only for qspi, for spi and other methods you may embed dummy | ||
| // cycles directly in the cmd buffer like so | ||
| // char *cmd = [MY_OPCODE, 0x000000, 0x000000]; | ||
| // This value is ignored for non-qspi methods | ||
| const size_t dummy_cycles; | ||
| }; | ||
| /// Each protocol implements an instance of the serial api, which is then passed | ||
| /// to a sensor to allow it to use the given protocol. | ||
| struct serial_api { | ||
| struct handle *handle; | ||
| bool (*read)(struct handle*, struct op_params*); | ||
| bool (*write)(struct handle*, struct op_params*); | ||
| }; | ||
| void serial_api_spi(struct serial_api *api, struct handle *handle); | ||
| void serial_api_i2c(struct serial_api *api, struct handle *handle); | ||
| void serial_api_qspi(struct serial_api *api, struct handle *handle); | ||
|
|
||
| //// | Helper methods | | ||
| //// These are used for when both `cmd` and `buffer` are static arrays. | ||
| //// In that case you may use STATIC_CMD as a shorthand. STATIC_EXEC is | ||
| //// is a shorthand for `buffer_size = 0`, useful for when you want to | ||
| //// to send a command which doesn't return anything back. | ||
|
|
||
| #define STATIC_CMD(cmd_arr, buf_arr) \ | ||
| (struct op_params){ \ | ||
| .cmd = (cmd_arr), \ | ||
| .cmd_size = sizeof(cmd_arr), \ | ||
| .buffer = (buf_arr), \ | ||
| .buffer_size = sizeof(buf_arr), \ | ||
| .dummy_cycles = 0 \ | ||
| } | ||
|
|
||
| #define STATIC_EXEC(cmd_arr) \ | ||
| (struct op_params){ \ | ||
| .cmd = (cmd_arr), \ | ||
| .cmd_size = sizeof(cmd_arr), \ | ||
| .buffer = NULL, \ | ||
| .buffer_size = 0, \ | ||
| .dummy_cycles = 0 \ | ||
| } | ||
|
|
||
| #endif // end DEFS_H | ||
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.
Not sure if I really love this
HANDLE_SPImacro stuff. I think I see where you're coming from with having the stub implementation that would fail if someone in the device ifstruct handle_spi x = HANDLE_SPI(y,z,a), but unless a spi is explicitly configured from the .ioc file, there wouldn't be a well defined handle, or cs pin to add, to setup anyways? I think we are covering a case that is essentially impossible to run into.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 am still back and forth with the macro, you are right that compilation will not be allowed if we have an illdefined handle, but at the same time there is no error message shown indicating why. With the macro you get a clear message on why. I am willing to drop this if we think that is not an issue.
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 don't really think it will be an issue because you would not be able to put the periphal as the handle like you would be expected to. If someone tried to do
struct handle_spi bmi088 = { .... , .handle = &hspi1}there would be several failures, one of them being that there isn't even an spi1 to be a handle in the first place.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.
Yes, that is true. But the reason why you know that the compilation failed is due to missing modules is because you have worked on this before. For a new member they might assume that all modules are pulled in, or they might question why there isn't a SPI handle definition. That is not to say I am opposed to removing the macros.