diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2093919..85b84db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,28 +14,24 @@ jobs: - name: Checkout code uses: actions/checkout@v6 - - name: Install Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '4.0' - - - name: Install Build Tools and Ceedling + - name: Install Build Tools run: | sudo apt-get update - sudo apt-get install -y gcc cmake ninja-build gcovr - gem install ceedling + sudo apt-get install -y gcc cmake ninja-build + pip install gcovr - - name: Fetch Dependencies - run: cmake -B build + - name: Configure cmake and run builds + run: | + cmake -B build -S . + cmake --build build - - name: Run Tests - run: ceedling gcov:all + - name: Run Tests and create coverage + run: | + cmake --build build --target test + cmake --build build --target coverage - - name: Upload Coverage - uses: actions/upload-artifact@v7 + - name: Render Coverage Summary in CI if: always() - with: - name: code-coverage-report - path: build/ceedling/artifacts/gcov/gcovr/GcovCoverageResults.html - archive: false - retention-days: 7 + run: | + echo "## Code Coverage Summary" >> $GITHUB_STEP_SUMMARY + gcovr -r . -e "tests/" -e "build/" --markdown >> $GITHUB_STEP_SUMMARY diff --git a/CMakeLists.txt b/CMakeLists.txt index aceb612..1a04ea1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.22) -enable_language(C) +project(CommonDrivers) +enable_language(C CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) include(FetchContent) @@ -13,14 +17,6 @@ FetchContent_Declare(bmi08 GIT_TAG master ) FetchContent_MakeAvailable(bmi08) -add_library(bmi08 STATIC - ${bmi08_SOURCE_DIR}/bmi08a.c - ${bmi08_SOURCE_DIR}/bmi08g.c - ${bmi08_SOURCE_DIR}/bmi08xa.c - ${bmi08_SOURCE_DIR}/bmi088_mma.c - ${bmi08_SOURCE_DIR}/bmi088_anymotiona.c -) -target_include_directories(bmi08 PUBLIC ${bmi08_SOURCE_DIR}) message(STATUS "Resolving bmp5 sensors api dependency...") FetchContent_Declare(bmp5 @@ -29,8 +25,6 @@ FetchContent_Declare(bmp5 GIT_TAG master ) FetchContent_MakeAvailable(bmp5) -add_library(bmp5 STATIC ${bmp5_SOURCE_DIR}/bmp5.c) -target_include_directories(bmp5 PUBLIC ${bmp5_SOURCE_DIR}) message(STATUS "Resolving littlefs dependency...") FetchContent_Declare(littlefs @@ -39,32 +33,79 @@ FetchContent_Declare(littlefs GIT_TAG master ) FetchContent_MakeAvailable(littlefs) -add_library(littlefs STATIC - ${littlefs_SOURCE_DIR}/lfs.c - ${littlefs_SOURCE_DIR}/lfs_util.c -) -target_include_directories(littlefs PUBLIC ${littlefs_SOURCE_DIR}) - -message(STATUS "Dependencies resolved, now creating library...") -add_library(common_drivers STATIC - "${CMAKE_CURRENT_SOURCE_DIR}/src/flash.c" - "${CMAKE_CURRENT_SOURCE_DIR}/src/flash/gd5f1gq5xe.c" - "${CMAKE_CURRENT_SOURCE_DIR}/src/sensors/bmp581.c" - "${CMAKE_CURRENT_SOURCE_DIR}/src/sensors/bmi088.c" - "${CMAKE_CURRENT_SOURCE_DIR}/src/sensors/CD-PA1616S.c" + +message(STATUS "Resolving gtest dependency...") +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.18.0 ) -target_include_directories(common_drivers PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_SOURCE_DIR}/include/flash" - "${CMAKE_CURRENT_SOURCE_DIR}/include/sensors" +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + +message(STATUS "Resolving fff dependency...") +FetchContent_Declare( + fff + GIT_REPOSITORY https://github.com/meekrosoft/fff.git + GIT_TAG master ) -message(STATUS "Linking libary to dependencies...") -target_link_libraries(common_drivers PUBLIC bmi08 bmp5 littlefs) +add_library(common_drivers STATIC) +if(TARGET stm32cubemx) + message(STATUS "Compiling for target host (MCU), linking libraries...") + + add_library(bmi08 STATIC + ${bmi08_SOURCE_DIR}/bmi08a.c + ${bmi08_SOURCE_DIR}/bmi08g.c + ${bmi08_SOURCE_DIR}/bmi08xa.c + ${bmi08_SOURCE_DIR}/bmi088_mma.c + ${bmi08_SOURCE_DIR}/bmi088_anymotiona.c + ) + target_include_directories(bmi08 PUBLIC ${bmi08_SOURCE_DIR}) + + add_library(bmp5 STATIC ${bmp5_SOURCE_DIR}/bmp5.c) + target_include_directories(bmp5 PUBLIC ${bmp5_SOURCE_DIR}) + + add_library(littlefs STATIC + ${littlefs_SOURCE_DIR}/lfs.c + ${littlefs_SOURCE_DIR}/lfs_util.c + ) + target_include_directories(littlefs PUBLIC ${littlefs_SOURCE_DIR}) + + message(STATUS "Linking dependencies to library...") + target_link_libraries(common_drivers PUBLIC bmi08 bmp5 littlefs) -if(NOT TARGET stm32cubemx) - # Empty for now, will add stuff for tests later -else() message(STATUS "Linking stm32 drivers to library....") target_link_libraries(common_drivers PUBLIC stm32cubemx) +else() + enable_testing() + message(STATUS "Compiling for tests, linking gtest, fff, and mocks to library...") + FetchContent_MakeAvailable(googletest) + FetchContent_MakeAvailable(fff) + + add_library(bmp5 INTERFACE) + target_include_directories(bmp5 INTERFACE ${bmp5_SOURCE_DIR}) + + add_library(littlefs INTERFACE) + target_include_directories(littlefs INTERFACE ${littlefs_SOURCE_DIR}) + + add_library(bmi08 INTERFACE) + target_include_directories(bmi08 INTERFACE ${bmi08_SOURCE_DIR}) + + message(STATUS "Linking mock dependencies to library...") + target_link_libraries(common_drivers PUBLIC bmp5 littlefs bmi08) + + message(STATUS "Creating tests for library...") + add_compile_definitions(TEST) + add_subdirectory(tests) endif() + +message(STATUS "Adding source files and headers to library...") +add_subdirectory(include) +add_subdirectory(src) + +add_custom_target(coverage + COMMAND mkdir -p coverage + COMMAND gcovr -r ${CMAKE_SOURCE_DIR} -e "${CMAKE_SOURCE_DIR}/tests/" -e "${CMAKE_BINARY_DIR}/" --html-details -o coverage/index.html . + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) diff --git a/README.md b/README.md index cc8853a..feb3f83 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ To write a sensor, you must initialize a struct of the form ```c struct sensor { - bool (*read)(void*, struct packet*); + bool (*read)(void*, Packet*); void* ctx; }; ``` diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 0000000..d1c2f37 --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,6 @@ +target_include_directories(common_drivers PUBLIC + "." + "sensors" + "protocols" + "flash" +) diff --git a/include/defs.h b/include/defs.h deleted file mode 100644 index 648dc8d..0000000 --- a/include/defs.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef DEFS_H -#define DEFS_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. -// -// Of course we still need to test the read function so we use the STATIC hack -// It is static in production, but non static when running tests. -#define STATIC -// Stub HAL file to satisfy compilation during tests, eventually we might write -// and use a thin abstraction on top of the HAL. -#include "stub_hal.h" -#else -// Make static work as usual -#define STATIC static -// TODO: Abstracts over hal series -#ifdef USE_STM32_H7XX -#include "stm32h7xx_hal.h" -#elif USE_STM32_L4XX -#include "stm32l4xx_hal.h" -#else -#include "stm32f4xx_hal.h" -#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 { -#ifdef HAL_I2C_MODULE_ENABLED - struct handle_i2c { - I2C_HandleTypeDef *handle; - uint32_t address; - } i2c; -#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; -#ifdef HAL_UART_MODULE_ENABLED - struct handle_uart { - UART_HandleTypeDef *handle; - } uart; -#endif - } def; -}; - - -#endif // end DEFS_H diff --git a/include/flash.h b/include/flash.h deleted file mode 100644 index f7cec3c..0000000 --- a/include/flash.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef FLASH_H -#define FLASH_H - -#include "lfs.h" - -#include - -struct flash { - struct lfs_config config; - lfs_t lfs; -}; - -uint32_t flash_mount(struct flash *flash); -int flash_unmount(struct flash *flash); -uint32_t flash_boot_count(struct flash *flash, bool update); -uint32_t flash_open(struct flash *flash, lfs_file_t *file, const char *filename); -bool flash_append(struct flash *flash, lfs_file_t *file, const uint8_t *bytes, size_t size); -int flash_close(struct flash *flash, lfs_file_t *file); - -#endif diff --git a/include/flash/flash.h b/include/flash/flash.h new file mode 100644 index 0000000..50e2610 --- /dev/null +++ b/include/flash/flash.h @@ -0,0 +1,28 @@ +#ifndef FLASH_H +#define FLASH_H + +#include "lfs.h" + +#include + +namespace Platform { +class Flash { +protected: + bool is_ready = false; + struct lfs_config config; + lfs_t lfs; + +public: + virtual ~Flash() = default; + virtual bool init() = 0; + uint32_t mount(); + uint32_t unmount(); + uint32_t bootcount(bool update); + uint32_t open(lfs_file_t* file, const char* filename); + uint32_t close(lfs_file_t* file); + bool append(lfs_file_t* file, const uint8_t* bytes, size_t size); + bool ready() const { return is_ready; }; +}; +} // namespace Platform + +#endif diff --git a/include/flash/gd5f1gq5xe.h b/include/flash/gd5f1gq5xe.h index 0e7feb9..c52e37e 100644 --- a/include/flash/gd5f1gq5xe.h +++ b/include/flash/gd5f1gq5xe.h @@ -2,11 +2,21 @@ #define GD5F1GQ5XE_H #include "flash.h" -#include "defs.h" +#include "hal.h" +#include "protocol.h" -#include #include +#include + +namespace Platform { +class GD5F1GQ5XE final : public Flash { +private: + Protocol& protocol; -bool gd5f1gq5xe_init(struct flash *flash, struct handle_spi *spi); +public: + GD5F1GQ5XE(Protocol& protocol_); + bool init() override; +}; +} // namespace Platform #endif diff --git a/include/hal.h b/include/hal.h new file mode 100644 index 0000000..535145b --- /dev/null +++ b/include/hal.h @@ -0,0 +1,16 @@ +#ifndef HAL_H +#define HAL_H + +#ifndef TEST +#ifdef USE_STM32_H7XX +#include "stm32h7xx_hal.h" +#elif USE_STM32_L4XX +#include "stm32l4xx_hal.h" +#else +#include "stm32f4xx_hal.h" +#endif +#else +#define HAL_Delay(...) (0) +#endif + +#endif diff --git a/include/protocols/i2c.h b/include/protocols/i2c.h new file mode 100644 index 0000000..4e23e3a --- /dev/null +++ b/include/protocols/i2c.h @@ -0,0 +1,30 @@ +#ifndef PROTOCOLS_I2C_H +#define PROTOCOLS_I2C_H + +#include "protocol.h" + +/// I2C tends to be register and address heavy. This really isn't really used +/// for command based sensors so the cmd buffer tends to just be the address +/// of the register to read from instead. +/// +/// The AddressSize gets directly converted to I2C's equivalent. Note that +/// unlike qspi, I2C does not support addresses greater than two bytes. +/// We use the `mem` equivalents of I2C because "most" sensors seem to +/// to prefer the pattern of specifying an address to target, followed +/// by a write or read. The `mem` I2C functions do this in one step instead +/// of two. This contrasts with SPI where we are forced to do it in two. +namespace Platform { +class I2C final : public Protocol { +private: + I2C_HandleTypeDef* handle; + const uint32_t address; + +public: + I2C(I2C_HandleTypeDef* handle_, uin32_t address_) + : Protocol{ProtocolType::I2C}, handle{handle_}, address{address_} {} + bool read(ConstSpan cmd, Span buffer, AddressSize size) override; + bool write(ConstSpan cmd, ConstSpan buffer, AddressSize size) override; +}; +} // namespace Platform + +#endif diff --git a/include/protocols/protocol.h b/include/protocols/protocol.h new file mode 100644 index 0000000..8ba495c --- /dev/null +++ b/include/protocols/protocol.h @@ -0,0 +1,137 @@ +#ifndef PROTOCOLS_PROTOCOL_H +#define PROTOCOLS_PROTOCOL_H + +#include +#include + +/// This is our thin abstraction over the HAL. We implement a base `Protocol` +/// class which protocols like I2C, QSPI, SPI, and UART inherit from. Then +/// throughout the sensor code, we will use the `protocol.read()` and +/// `protocol.write()` methods instead. See the documentation below +/// for more information on how to implement them. And see the documentation +/// in `i2c.h`, `qspi.h`, `spi.h`, and `uart.h` for further information +/// on their implementations . +/// +/// The Protocol class was designed following the assumptions that it is going +/// to only be used for sensor and flash devices. The behavior of these devices +/// can be roughly categorized into three types +/// +/// * Register based: these sensors require you first send a register address, +/// then writes will write to that register while reads will +/// read from that register +/// * Protocol based: these sensors require you to just send some form of +/// standardized or in-house messaging format +/// * Command based: usually more complex devices like flash, they are like +/// register based, but instead requires you to send some +/// form of op code, followed by addresses or arguments. +/// +/// One can argue that command-based is a special case of register based, but +/// instead taking arguments in addition to the initial address. The bosch +/// sensors are examples of register based while the gps (cd-pa1616s) is an +/// example of protocol based. Most flash devices will be command based. +/// +/// Command based devices tend to also allow QSPI, which if you look at its +/// implementation actually has commands baked into its core. The complex +/// devices tend to require dummy cycles. You should specify dummy cycles using +/// empty bytes `0x00`, look at the spreadsheets to see how many you need. +namespace Platform { +/// Span --- +/// A quick primer on span, it just a regular C buffer but it also +/// includes the size, really convenient, we will be using this a lot +using Span = std::span; +using ConstSpan = std::span; + +/// Protocol --- +/// Every peripheral (SPI, I2C, UART, etc...) inherits from this base +/// instance. They MUST implment two methods, `read` and `write` +/// A couple of notes on implmentation (look in respective files for more info) +/// cmd: sends the address or register to perform the action on, or a command +/// code buffer: the buffer to read into or write into dummy_cycles: only used +/// by QSPI +/// For writes, you may set size of the buffer to zero to not transmit +/// anything. This is more relevant for flash which might just need to +/// send a single command and not receive anything afterwards. +/// +/// TODO: Optionally, readDMA may be implemented (this is up for debate) +/// Subclasses must set the type to the appropriate `ProtocolType`. +/// TODO: I don't really like that we have a separate parameter just +/// for QSPI, it is probably possible to remove it but I am sure +/// how to communicate to QSPI about dummy cycles otherwise, note +/// that a dummy clock of 4 cycles do exist so we can't just +/// count the number of 0x00000000 bytes and call it day. +enum class ProtocolType { SPI, UART, I2C, QSPI }; +enum class AddressSize : std::size_t { + None = 0, + Byte = 1, + Byte2 = 2, + Byte3 = 3, + Byte4 = 4 +}; +/// Some protocols require configuration, use this as the `config` +/// method as a super lightweight method to configure them. In each +/// subclass handle the config accordingly. The config can be changed +/// at run time. +enum class Config { + QSPI_Data1, // QSPI receive data across one line + QSPI_Data4, // QSPI receive data across four lines + QSPI_Address1, // QSPI send address across one line + QSPI_Address4 // QSPI send address across four lines +}; +class Protocol { +protected: + ProtocolType ptype; + +public: + /// @brief Initializes the Protocol + /// + /// Subclasses must use this constructor to define their type + /// + /// @param type must be one of SPI, UART, I2C, QSPI + explicit Protocol(ProtocolType type) : ptype{type} {} + virtual ~Protocol() = default; + + /// @brief Reads from the protocol after sending cmd + /// + /// The cmd buffer may include an command as its first element + /// or an address, handle accordingly based on the protocol. You + /// must handle dummy bytes and clocks accordingly as well. + /// + /// @param cmd span holding the address/cmd to send + /// @param buffer buffer to read into + /// @param address_size size of the address + /// @return true on error, false on success + virtual bool read(ConstSpan cmd, Span buffer, AddressSize size) = 0; + + /// @brief Writes using the protocol after sending cmd + /// + /// The cmd buffer may include an command as its first element + /// or an address, handle accordingly based on the protocol. You + /// must handle dummy bytes and clocks accordingly as well. + /// + /// @param cmd span holding the address/cmd to send + /// @param buffer buffer to write from, set to `{}` to write + /// nothing + /// @param address_size size of the address + /// @return true on error, false on success + virtual bool write(ConstSpan cmd, ConstSpan buffer, AddressSize size) = 0; + + /// @brief Configures the given protocol using the given config + /// + /// All subclasses must ignore the config if they don't + /// recognize it, otherwise they must handle the config + /// accordingly. + /// + /// @param config config object + /// @return true on error, false on success + virtual bool configure(Config config) { return false; }; + + // Not currently implemented for all protocols + virtual bool readDMA(Span buffer) { return true; }; + + /// @brief returns the type of the protocol + /// @return protocol type + ProtocolType type() const { return ptype; } +}; +} // namespace Platform + +#endif // PROTOCOLS_PROTOCOL_H diff --git a/include/protocols/qspi.h b/include/protocols/qspi.h new file mode 100644 index 0000000..34b849e --- /dev/null +++ b/include/protocols/qspi.h @@ -0,0 +1,57 @@ +#ifndef PROTOCOLS_QSPI_H +#define PROTOCOLS_QSPI_H + +#include "hal.h" +#include "protocol.h" + +/// By far the most complicated protocol. This one enforces a command based +/// communication system. You need to fill out a `QSPI_CommandTypeDef` and +/// send it before you are able to do anything. With this you can really +/// see the command and address separation. There are several important partsjko +/// +/// 1. Command: this is the op code to send +/// 2. Address: address (argument), make sure to set the appropriate +/// AddressSize, +/// you can also configure how many lines to send the address +/// over +/// 3. Data: how to receive the data, you also need to set the data buffer +/// and size of course, and how many line to get/write data over +/// +/// You can also configure the dummy cycles as well as alternate byte mode. +/// The dummy cycle is more important; I haven't seen any usage of the alternate +/// byte mode yet. Sometimes you can only recieve data over a single line, so +/// a method is provided in order to configure the dataMode, usually at startup +/// only. You can do so using `configure` method with the appropriate `Config`. +namespace Platform { +class QSPI final : public Protocol { +private: + QSPI_HandleTypeDef* handle; + uint32_t data_mode = QSPI_DATA_4_LINES; + uint32_t address_mode = QSPI_ADDRESS_1_LINE; + +public: + QSPI(QSPI_HandleTypeDef* handle_) + : Protocol{ProtocolType::QSPI} handle{handle_} {} + bool read(ConstSpan cmd, Span buffer, AddressSize size) override; + bool write(ConstSpan cmd, ConstSpan buffer, AddressSize size) override; + bool configure(Config config) override { + switch (config) { + case Config::QSPI_Data1: + data_mode = QSPI_DATA_1_LINE; + return false; + case Config::QSPI_Data4: + data_mode = QSPI_DATA_4_LINES; + return false; + case Config::QSPI_Address1: + address_mode = QSPI_ADDRESS_1_LINE; + return false; + case Config::QSPI_Address4: + address_mode = QSPI_ADDRESS_4_LINES; + return false; + } + return true; + }; +}; +} // namespace Platform + +#endif diff --git a/include/protocols/spi.h b/include/protocols/spi.h new file mode 100644 index 0000000..855e0e8 --- /dev/null +++ b/include/protocols/spi.h @@ -0,0 +1,41 @@ +#ifndef PROTOCOLS_SPI_H +#define PROTOCOLS_SPI_H + +#include "hal.h" +#include "protocol.h" + +/// SPI generally doesn't care too much about your underlying device. It can +/// handle both register and command based devices well. For Register based, you +/// generally must transmit the register address, and then you may read from or +/// write to it. For command based devices, you may group the command with +/// any arguments you want and then send the command buffer. You can then choose +/// to read or write or do nothing based on the command. Note that you must do a +/// chip select before you are able to communicate anything. This is handled for +/// you already by this implementation so you don't have to worry about it. +/// +/// SPI doesn't really care about the address size, it just sends whatever it +/// wants. Kind of demure isn't it? Hence in our implementations you may just +/// ignore the `AddressSize`, but it is good idea to still specify it since the +/// other protocols do the same. To send dummy bytes you should do this +/// +/// auto cmd_buffer = { my_command_code, some_parameters, 0x00 }; +/// +/// The 0x00 is eight bytes of dummy. SPI will just send it as is and everything +/// should work well. +namespace Platform { +class SPI final : public Protocol { +private: + SPI_HandleTypeDef* handle; + GPIO_TypeDef* port; + uint8_t pin; + +public: + SPI(SPI_HandleTypeDef* handle_, GPIO_TypeDef* port_, uint8_t pin_) + : Protocol{ProtocolType::SPI}, handle{handle_}, port{port_}, pin{pin_} { + } + bool read(ConstSpan cmd, Span buffer, AddressSize size) override; + bool write(ConstSpan cmd, ConstSpan buffer, AddressSize size) override; +}; +} // namespace Platform + +#endif diff --git a/include/protocols/uart.h b/include/protocols/uart.h new file mode 100644 index 0000000..f343842 --- /dev/null +++ b/include/protocols/uart.h @@ -0,0 +1,26 @@ +#ifndef PROTOCOLS_UART_H +#define PROTOCOLS_UART_H + +#include "protocol.h" + +/// Arguably the simplest protocol. UART just sends stuff, much like SPI, but +/// you don't have to deal with chip selects with UART, so it is even more fire +/// and forget. Since we don't have any usages of a UART sensor transmitting +/// commands, this current implementation just writes and reads directly, +/// with no intermediate step. The `cmd` buffer is ignored, just write +/// using the buffer. +namespace Platform { +class UART final : public Protocol { +private: + UART_HandleTypeDef* handle; + +public: + UART(UART_HandleTypeDef* handle_) + : Protocol{ProtocolType::UART}, handle{handle_} {} + bool read(ConstSpan cmd, Span buffer, AddressSize size) override; + bool write(ConstSpan cmd, ConstSpan buffer, AddressSize size) override; + bool readDMA(Span buffer) override; +}; +} // namespace Platform + +#endif diff --git a/include/sensor.h b/include/sensor.h deleted file mode 100644 index a6afa78..0000000 --- a/include/sensor.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef SENSOR_H -#define SENSOR_H - -#include -#include - -struct __attribute__((packed)) packet { - int16_t magic; // 2 bytes - uint32_t status; // 4 bytes - uint32_t time_us; // 4 bytes - float main_voltage_v; // 4 bytes - float pyro_voltage_v; // 4 bytes - uint8_t numSatellites; // 1 byte - uint8_t gpsFixType; // 1 byte 0 = no fix, 1 = fix, 2 = DGPS fix, etc. - float latitude_degrees; // 4 bytes - float longitude_degrees; // 4 bytes - float gps_hMSL_m; // 4 bytes altitude above mean sea level - float barometer_hMSL_m; // 4 bytes - float temperature_c; // 4 bytes - float acceleration_x_mss; // 4 bytes - float acceleration_y_mss; // 4 bytes - float acceleration_z_mss; // 4 bytes - float angular_velocity_x_rads; // 4 bytes - float angular_velocity_y_rads; // 4 bytes - float angular_velocity_z_rads; // 4 bytes - float gauss_x; // 4 bytes - float gauss_y; // 4 bytes - float gauss_z; // 4 bytes - float kf_acceleration_mss; // 4 bytes - float kf_velocity_ms; // 4 bytes - float kf_position_m; // 4 bytes - float w; // 4 bytes - float x; // 4 bytes - float y; // 4 bytes - float z; // 4 bytes - uint32_t checksum; // 4 bytes -}; - -struct sensor { - bool (*read)(void*, struct packet*); - void* ctx; -}; - -#endif // SENSOR_H diff --git a/include/sensors/CD-PA1616S.h b/include/sensors/CD-PA1616S.h deleted file mode 100644 index 8fc1c3a..0000000 --- a/include/sensors/CD-PA1616S.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - * CD-PA1616S.h - * - * Created on: Feb 21, 2025 - * Author: Mahir Shah - */ - -#ifndef INC_CD_PA1616S_H_ -#define INC_CD_PA1616S_H_ - -#include "defs.h" -#include "sensor.h" - -#include -#include - -#define BUFFER_SIZE 128 - -struct gps_ctx { - uint8_t buffer[BUFFER_SIZE]; // used for DMA reception buffer - struct handle handle; -}; -bool gps_init(struct gps_ctx *ctx, struct sensor *sensor); - -#endif /* INC_CD_PA1616S_H_ */ diff --git a/include/sensors/bmi088.h b/include/sensors/bmi088.h index 51f45db..39235f6 100644 --- a/include/sensors/bmi088.h +++ b/include/sensors/bmi088.h @@ -5,20 +5,29 @@ * Author: Dhruv Shah */ -#ifndef INC_BMI088_H_ -#define INC_BMI088_H_ +#ifndef SENSORS_BMI088_H +#define SENSORS_BMI088_H -#include "defs.h" -#include "sensor.h" #include "bmi08_defs.h" +#include "sensor.h" + +#include -#include +namespace Platform { +class BMI088 final : public Sensor { +private: + struct bmi08_dev dev; + Protocol& accel; + Protocol& gyro; -struct bmi088_ctx { - struct bmi08_dev dev; - struct handle_spi accel_spi; - struct handle_spi gyro_spi; +public: + BMI088(Protocol& accel_, Protocol& gyro_) : accel{accel_}, gyro{gyro_} { + assert(accel.type() == ProtocolType::SPI); + assert(gyro.type() == ProtocolType::SPI); + } + bool init() override; + bool read(Packet& packet) override; }; -int8_t bmi088_init(struct bmi088_ctx *ctx, struct sensor *sensor); +} // namespace Platform -#endif /* INC_BMI088_H_ */ +#endif diff --git a/include/sensors/bmp581.h b/include/sensors/bmp581.h index 1acd4eb..333b6a0 100644 --- a/include/sensors/bmp581.h +++ b/include/sensors/bmp581.h @@ -1,20 +1,27 @@ -#ifndef BMP581_H -#define BMP581_H +#ifndef SENSORS_BMP581_H +#define SENSORS_BMP581_H -#include "defs.h" -#include "sensor.h" #include "bmp5_defs.h" +#include "protocol.h" +#include "sensor.h" -#include +#include -struct bmp581_ctx { - struct bmp5_dev dev; - struct bmp5_osr_odr_press_config odr_config; - struct bmp5_int_source_select int_config; - struct handle handle; -}; +namespace Platform { +class BMP581 final : public Sensor { +private: + struct bmp5_dev dev; + struct bmp5_osr_odr_press_config odr_config; + struct bmp5_int_source_select int_config; + struct Protocol& api; -int8_t bmp581_init(struct bmp581_ctx *ctx, struct sensor *sensor); -int8_t bmp581_get_power_mode(struct bmp581_ctx *ctx, enum bmp5_powermode *powermode); +public: + BMP581(Protocol& api_) : api(api_) { + assert(api.type() == ProtocolType::SPI); + }; + bool init() override; + bool read(Packet& packet) override; +}; +} // namespace Platform #endif diff --git a/include/sensors/cd-pa1616s.h b/include/sensors/cd-pa1616s.h new file mode 100644 index 0000000..e8f0889 --- /dev/null +++ b/include/sensors/cd-pa1616s.h @@ -0,0 +1,33 @@ +/* + * CD-PA1616S.h + * + * Created on: Feb 21, 2025 + * Author: Mahir Shah + */ + +#ifndef SENSORS_CD_PA1616S_H +#define SENSORS_CD_PA1616S_H + +#include "protocol.h" +#include "sensor.h" + +#include +#include + +namespace Platform { +class GPS final : public Sensor { +private: + // used for DMA reception buffer + uint8_t buffer[128]; + Protocol& protocol; + +public: + GPS(Protocol& protocol_) : protocol(protocol_) { + assert(protocol.type() == ProtocolType::UART); + }; + bool init() override; + bool read(Packet& packet) override; +}; +} // namespace Platform + +#endif /* INC_CD_PA1616S_H_ */ diff --git a/include/sensors/sensor.h b/include/sensors/sensor.h new file mode 100644 index 0000000..ac54c1c --- /dev/null +++ b/include/sensors/sensor.h @@ -0,0 +1,56 @@ +#ifndef SENSORS_SENSOR_H +#define SENSORS_SENSOR_H + +#include +#include + +namespace Platform { +struct __attribute__((packed)) Packet { + int16_t magic = 0xBEEF; // 2 bytes + uint32_t status = 0; // 4 bytes + uint32_t time_us = 0; // 4 bytes + float main_voltage_v = 0.0f; // 4 bytes + float pyro_voltage_v = 0.0f; // 4 bytes + uint8_t numSatellites = 0; // 1 byte + uint8_t gpsFixType = 0; // 1 byte 0 = no fix, 1 = fix, 2 = DGPS fix, etc. + float latitude_degrees = 0.0f; // 4 bytes + float longitude_degrees = 0.0f; // 4 bytes + float gps_hMSL_m = 0.0f; // 4 bytes altitude above mean sea level + float barometer_hMSL_m = 0.0f; // 4 bytes + float temperature_c = 0.0f; // 4 bytes + float acceleration_x_mss = 0.0f; // 4 bytes + float acceleration_y_mss = 0.0f; // 4 bytes + float acceleration_z_mss = 0.0f; // 4 bytes + float angular_velocity_x_rads = 0.0f; // 4 bytes + float angular_velocity_y_rads = 0.0f; // 4 bytes + float angular_velocity_z_rads = 0.0f; // 4 bytes + float gauss_x = 0.0f; // 4 bytes + float gauss_y = 0.0f; // 4 bytes + float gauss_z = 0.0f; // 4 bytes + float kf_acceleration_mss = 0.0f; // 4 bytes + float kf_velocity_ms = 0.0f; // 4 bytes + float kf_position_m = 0.0f; // 4 bytes + float w = 0.0f; // 4 bytes + float x = 0.0f; // 4 bytes + float y = 0.0f; // 4 bytes + float z = 0.0f; // 4 bytes + uint32_t checksum = 0; // 4 bytes + Packet() = default; +}; + +/// Sensor --- +/// To implement a sensor, you must implement a `read` method which updates +/// the packet in place after it reads from the hardware. +/// You must also implement an init method, which allows for retrying. +/// +/// Both methods must return `true` on success. +class Sensor { +protected: +public: + virtual ~Sensor() = default; + virtual bool read(Packet& packet) = 0; + virtual bool init() = 0; +}; +} // namespace Platform + +#endif // SENSORS_SENSOR_H diff --git a/project.yml b/project.yml deleted file mode 100644 index 1446d9e..0000000 --- a/project.yml +++ /dev/null @@ -1,411 +0,0 @@ -# ========================================================================= -# Ceedling - Test-Centered Build System for C -# ThrowTheSwitch.org -# Copyright (c) 2010-25 Mike Karlesky, Mark VanderVoord, & Greg Williams -# SPDX-License-Identifier: MIT -# ========================================================================= - ---- -:project: - # how to use ceedling. If you're not sure, leave this as `gem` and `?` - :which_ceedling: gem - :ceedling_version: 1.0.1 - - # optional features. If you don't need them, keep them turned off for performance - :use_mocks: TRUE - :use_test_preprocessor: :none # options are :none, :mocks, :tests, or :all - :use_deep_preprocessor: :none # options are :none, :mocks, :tests, or :all - :use_backtrace: :simple # options are :none, :simple, or :gdb - :use_decorators: :auto # decorate Ceedling's output text. options are :auto, :all, or :none - - # tweak the way ceedling handles automatic tasks - :build_root: build/ceedling - :test_file_prefix: test_ - :default_tasks: - - test:all - - # performance options. If your tools start giving mysterious errors, consider - # dropping this to 1 to force single-tasking - :test_threads: 8 - :compile_threads: 8 - - # enable release build (more details in release_build section below) - :release_build: FALSE - -# Specify where to find mixins and any that should be enabled automatically -:mixins: - :enabled: [] - :load_paths: [] - -# further details to configure the way Ceedling handles test code -:test_build: - :use_assembly: FALSE - -# further details to configure the way Ceedling handles release code -:release_build: - :output: MyApp.out - :use_assembly: FALSE - :artifacts: [] - -# Plugins are optional Ceedling features which can be enabled. Ceedling supports -# a variety of plugins which may effect the way things are compiled, reported, -# or may provide new command options. Refer to the readme in each plugin for -# details on how to use it. -:plugins: - :load_paths: [] - :enabled: - #- beep # beeps when finished, so you don't waste time waiting for ceedling - - module_generator # handy for quickly creating source, header, and test templates - - gcov # test coverage using gcov. Requires gcc, gcov, and a coverage analyzer like gcovr - #- bullseye # test coverage using bullseye. Requires bullseye for your platform - - command_hooks # write custom actions to be called at different points during the build process - #- compile_commands_json_db # generate a compile_commands.json file - #- dependencies # automatically fetch 3rd party libraries, etc. - #- subprojects # managing builds and test for static libraries - #- fake_function_framework # use FFF instead of CMock - - # Report options (You'll want to choose one stdout option, but may choose multiple stored options if desired) - #- report_build_warnings_log - #- report_tests_gtestlike_stdout - #- report_tests_ide_stdout - #- report_tests_log_factory - - report_tests_pretty_stdout - #- report_tests_raw_output_log - #- report_tests_teamcity_stdout - -# Specify which reports you'd like from the log factory -:report_tests_log_factory: - :reports: - - json - - junit - - cppunit - - html - -# override the default extensions for your system and toolchain -:extension: - #:header: .h - #:source: .c - #:assembly: .s - #:dependencies: .d - #:object: .o - :executable: .out - #:testpass: .pass - #:testfail: .fail - #:subprojects: .a - -# This is where Ceedling should look for your source and test files. -# see documentation for the many options for specifying this. -:paths: - :test: - - +:test/** - - -:test/support - :source: - - src/** - :include: - - include/** - - build/_deps/** - :support: - - test/support - :libraries: [] - -# You can even specify specific files to add or remove from your test -# and release collections. Usually it's better to use paths and let -# Ceedling do the work for you! -:files: - :test: [] - :source: [] - -# Compilation symbols to be injected into builds -# See documentation for advanced options: -# - Test name matchers for different symbols per test executable build -# - Referencing symbols in multiple lists using advanced YAML -# - Specifiying symbols used during test preprocessing -:defines: - :test: - - TEST # Simple list option to add symbol 'TEST' to compilation of all files in all test executables - :release: [] - - # Enable to inject name of a test as a unique compilation symbol into its respective executable build. - :use_test_definition: FALSE - -# Configure additional command line flags provided to tools used in each build step -# :flags: -# :release: -# :compile: # Add '-Wall' and '--02' to compilation of all files in release target -# - -Wall -# - --O2 -# :test: -# :compile: -# '(_|-)special': # Add '-pedantic' to compilation of all files in all test executables with '_special' or '-special' in their names -# - -pedantic -# '*': # Add '-foo' to compilation of all files in all test executables -# - -foo - -# Configuration Options specific to CMock. See CMock docs for details -:cmock: - # Core conffiguration - :plugins: # What plugins should be used by CMock? - - :ignore - - :callback - - :expect_any_args - - :return_thru_ptr - :verbosity: 2 # the options being 0 errors only, 1 warnings and errors, 2 normal info, 3 verbose - :when_no_prototypes: :warn # the options being :ignore, :warn, or :erro - - # File configuration - :skeleton_path: '' # Subdirectory to store stubs when generated (default: '') - :mock_prefix: 'mock_' # Prefix to append to filenames for mocks - :mock_suffix: '' # Suffix to append to filenames for mocks - - # Parser configuration - :strippables: ['(?:__attribute__\s*\([ (]*.*?[ )]*\)+)'] - :attributes: - - __ramfunc - - __irq - - __fiq - - register - - extern - :c_calling_conventions: - - __stdcall - - __cdecl - - __fastcall - :treat_externs: :exclude # the options being :include or :exclud - :treat_inlines: :exclude # the options being :include or :exclud - - # Type handling configuration - #:unity_helper_path: '' # specify a string of where to find a unity_helper.h file to discover custom type assertions - :treat_as: # optionally add additional types to map custom types - uint8: HEX8 - uint16: HEX16 - uint32: UINT32 - int8: INT8 - bool: UINT8 - #:treat_as_array: {} # hint to cmock that these types are pointers to something - #:treat_as_void: [] # hint to cmock that these types are actually aliases of void - :memcmp_if_unknown: true # allow cmock to use the memory comparison assertions for unknown types - :when_ptr: :compare_data # hint to cmock how to handle pointers in general, the options being :compare_ptr, :compare_data, or :smart - - # Mock generation configuration - :weak: '' # Symbol to use to declare weak functions - :enforce_strict_ordering: true # Do we want cmock to enforce ordering of all function calls? - :fail_on_unexpected_calls: true # Do we want cmock to fail when it encounters a function call that wasn't expected? - :callback_include_count: true # Do we want cmock to include the number of calls to this callback, when using callbacks? - :callback_after_arg_check: false # Do we want cmock to enforce an argument check first when using a callback? - #:includes: [] # You can add additional includes here, or specify the location with the options below - #:includes_h_pre_orig_header: [] - #:includes_h_post_orig_header: [] - #:includes_c_pre_header: [] - #:includes_c_post_header: [] - #:array_size_type: [] # Specify a type or types that should be used for array lengths - #:array_size_name: 'size|len' # Specify a name or names that CMock might automatically recognize as the length of an array - :exclude_setjmp_h: false # Don't use setjmp when running CMock. Note that this might result in late reporting or out-of-order failures. - -# Configuration options specific to Unity. -:unity: - :defines: - # - UNITY_EXCLUDE_FLOAT - -# You can optionally have ceedling create environment variables for you before -# performing the rest of its tasks. -:environment: [] -# :environment: -# # List enforces order allowing later to reference earlier with inline Ruby substitution -# - :var1: value -# - :var2: another value -# - :path: # Special PATH handling with platform-specific path separators -# - #{ENV['PATH']} # Environment variables can use inline Ruby substitution -# - /another/path/to/include - -# LIBRARIES -# These libraries are automatically injected into the build process. Those specified as -# common will be used in all types of builds. Otherwise, libraries can be injected in just -# tests or releases. These options are MERGED with the options in supplemental yaml files. -:libraries: - :placement: :end - :flag: "-l${1}" - :path_flag: "-L ${1}" - :system: ['m'] # for example, you might list 'm' to grab the math library - :test: [] - :release: [] - -################################################################ -# PLUGIN CONFIGURATION -################################################################ - -# Add -gcov to the plugins list to make sure of the gcov plugin -# You will need to have gcov and gcovr both installed to make it work. -# For more information on these options, see docs in plugins/gcov -:gcov: - :summaries: TRUE # Enable simple coverage summaries to console after tests - :report_task: FALSE # Disabled dedicated report generation task (this enables automatic report generation) - :utilities: - - gcovr # Use gcovr to create the specified reports (default). - #- ReportGenerator # Use ReportGenerator to create the specified reports. - :reports: # Specify one or more reports to generate. - # Make an HTML summary report. - - HtmlBasic - # - HtmlDetailed - # - Text - # - Cobertura - # - SonarQube - # - JSON - # - HtmlInline - # - HtmlInlineAzure - # - HtmlInlineAzureDark - # - HtmlChart - # - MHtml - # - Badges - # - CsvSummary - # - Latex - # - LatexSummary - # - PngChart - # - TeamCitySummary - # - lcov - # - Xml - # - XmlSummary - :gcovr: - # :html_artifact_filename: TestCoverageReport.html - # :html_title: Test Coverage Report - :html_medium_threshold: 75 - :html_high_threshold: 90 - # :html_absolute_paths: TRUE - # :html_encoding: UTF-8 - -# :module_generator: -# :naming: :snake #options: :bumpy, :camel, :caps, or :snake -# :includes: -# :tst: [] -# :src: [] -# :boilerplates: -# :src: "" -# :inc: "" -# :tst: "" - -# :dependencies: -# :libraries: -# - :name: WolfSSL -# :source_path: third_party/wolfssl/source -# :build_path: third_party/wolfssl/build -# :artifact_path: third_party/wolfssl/install -# :fetch: -# :method: :zip -# :source: \\shared_drive\third_party_libs\wolfssl\wolfssl-4.2.0.zip -# :environment: -# - CFLAGS+=-DWOLFSSL_DTLS_ALLOW_FUTURE -# :build: -# - "autoreconf -i" -# - "./configure --enable-tls13 --enable-singlethreaded" -# - make -# - make install -# :artifacts: -# :static_libraries: -# - lib/wolfssl.a -# :dynamic_libraries: -# - lib/wolfssl.so -# :includes: -# - include/** - -# :subprojects: -# :paths: -# - :name: libprojectA -# :source: -# - ./subprojectA/source -# :include: -# - ./subprojectA/include -# :build_root: ./subprojectA/build -# :defines: [] - -:command_hooks: - :post_error: - :executable: chmod - :arguments: - - "-R" - - "+w" - - "./build/ceedling/vendor" -# :pre_mock_preprocess: -# :post_mock_preprocess: -# :pre_test_preprocess: -# :post_test_preprocess: -# :pre_mock_generate: -# :post_mock_generate: -# :pre_runner_generate: -# :post_runner_generate: -# :pre_compile_execute: -# :post_compile_execute: -# :pre_link_execute: -# :post_link_execute: -# :pre_test_fixture_execute: -# :post_test_fixture_execute: -# :pre_test: -# :post_test: -# :pre_release: -# :post_release: -# :pre_build: -# :post_build: -# :post_error: - -################################################################ -# TOOLCHAIN CONFIGURATION -################################################################ - -#:tools: -# Ceedling defaults to using gcc for compiling, linking, etc. -# As [:tools] is blank, gcc will be used (so long as it's in your system path) -# See documentation to configure a given toolchain for use -# :tools: -# :test_compiler: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :test_linker: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :test_assembler: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :test_fixture: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :test_includes_preprocessor: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :test_file_preprocessor: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :test_file_preprocessor_directives: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :release_compiler: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :release_linker: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :release_assembler: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -# :release_dependencies_generator: -# :executable: -# :arguments: [] -# :name: -# :optional: FALSE -... diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..1c7f308 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(flash) +add_subdirectory(protocols) +add_subdirectory(sensors) diff --git a/src/flash.c b/src/flash.c deleted file mode 100644 index 1684c27..0000000 --- a/src/flash.c +++ /dev/null @@ -1,99 +0,0 @@ -#include "flash.h" - -#include "lfs.h" - -#include - -// See: https://github.com/littlefs-project/littlefs/issues/564#issuecomment-2363032827 -// This gives how many bytes are needed before we need to do a fsync. Since we -// want to sync at the end of a block, this just gives offset from the end. -// -// We don't want to sync too often, especially for NAND flashes which have -// larger block sizes because littlefs does a whole scan and write of the -// partial block, which is very time-consuming. -static inline uint32_t offset(uint32_t filesize, uint32_t blocksize) -{ - // Edge case for the first block - if (filesize < blocksize) { - return blocksize - filesize; - } - - const uint32_t w = sizeof(uint32_t); - uint32_t pop = __builtin_popcount((filesize / (blocksize - (2*w))) - 1); - uint32_t n = (filesize - (w * (pop + 2))) / (blocksize - 2*w); - uint32_t offset = filesize - (blocksize - 2*w)*n - w*__builtin_popcount(n); - return blocksize - offset; -} - -uint32_t flash_mount(struct flash *flash) -{ - int err = lfs_mount(&flash->lfs, &flash->config); - if (err) { - lfs_format(&flash->lfs, &flash->config); - int res = lfs_mount(&flash->lfs, &flash->config); - if (res < 0) return -1; - } - flash_boot_count(flash, true); - return lfs_fs_size(&(flash->lfs)); -} - -int flash_unmount(struct flash *flash) -{ - return lfs_unmount(&flash->lfs); -} - -uint32_t flash_boot_count(struct flash *flash, bool update) -{ - lfs_file_t file; - uint32_t boot_count = 0; - - lfs_file_open(&flash->lfs, &file, "boot_count", LFS_O_RDWR | LFS_O_CREAT); - lfs_file_read(&flash->lfs, &file, &boot_count, sizeof(boot_count)); - - if (update) { - ++boot_count; - lfs_file_rewind(&flash->lfs, &file); - lfs_file_write(&flash->lfs, &file, &boot_count, sizeof(boot_count)); - } - - lfs_file_close(&(flash->lfs), &file); - return boot_count; -} - -uint32_t flash_open(struct flash *flash, lfs_file_t *file, const char *filename) -{ - lfs_file_open(&flash->lfs, file, filename, LFS_O_RDWR | LFS_O_CREAT | LFS_O_APPEND); - return lfs_file_size(&flash->lfs, file); -} - -bool flash_append(struct flash *flash, lfs_file_t *file, const uint8_t *bytes, size_t size) -{ - uint8_t *buf = (uint8_t *) bytes; - uint32_t filesize = lfs_file_size(&flash->lfs, file); - uint32_t off = offset(filesize, flash->lfs.cfg->block_size); - - while (size > 0) { - uint32_t write_size = size < off ? size : off; - if (off == 0) { - // Time for a new block, sync and just write whatever - lfs_file_sync(&flash->lfs, file); - // Due to constraints, the size is guaranteed to be less - // than a block size, so we can just safely do this - write_size = size; - } - - uint32_t res = lfs_file_write(&flash->lfs, file, buf, write_size); - if (res != write_size) return false; - - size -= res; - filesize += res; - buf += res; - off = offset(filesize, flash->lfs.cfg->block_size); - } - return true; -} - -int flash_close(struct flash *flash, lfs_file_t *file) -{ - return lfs_file_close(&flash->lfs, file); -} diff --git a/src/flash/CMakeLists.txt b/src/flash/CMakeLists.txt new file mode 100644 index 0000000..1d0359b --- /dev/null +++ b/src/flash/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources(common_drivers PRIVATE + "flash.cpp" + "gd5f1gq5xe.cpp" +) diff --git a/src/flash/flash.cpp b/src/flash/flash.cpp new file mode 100644 index 0000000..f843f8d --- /dev/null +++ b/src/flash/flash.cpp @@ -0,0 +1,98 @@ +#include "flash.h" + +#include "lfs.h" + +#include + +namespace { +// See: +// https://github.com/littlefs-project/littlefs/issues/564#issuecomment-2363032827 +// This gives how many bytes are needed before we need to do a fsync. Since we +// want to sync at the end of a block, this just gives offset from the end. +// +// We don't want to sync too often, especially for NAND flashes which have +// larger block sizes because littlefs does a whole scan and write of the +// partial block, which is very time-consuming. +inline uint32_t offset(uint32_t filesize, uint32_t blocksize) { + // Edge case for the first block + if (filesize < blocksize) { + return blocksize - filesize; + } + + const uint32_t w = sizeof(uint32_t); + uint32_t pop = __builtin_popcount((filesize / (blocksize - (2 * w))) - 1); + uint32_t n = (filesize - (w * (pop + 2))) / (blocksize - 2 * w); + uint32_t offset = + filesize - (blocksize - 2 * w) * n - w * __builtin_popcount(n); + return blocksize - offset; +} +} // namespace + +namespace Platform { +uint32_t Flash::mount() { + int err = lfs_mount(&lfs, &config); + if (err) { + lfs_format(&lfs, &config); + int res = lfs_mount(&lfs, &config); + if (res < 0) + return -1; + } + bootcount(true); + is_ready = true; + return lfs_fs_size(&lfs); +} + +uint32_t Flash::unmount() { return lfs_unmount(&lfs); } + +uint32_t Flash::bootcount(bool update) { + lfs_file_t file; + uint32_t current_count = 0; + + lfs_file_open(&lfs, &file, "boot_count", LFS_O_RDWR | LFS_O_CREAT); + lfs_file_read(&lfs, &file, ¤t_count, sizeof(current_count)); + + if (update) { + ++current_count; + lfs_file_rewind(&lfs, &file); + lfs_file_write(&lfs, &file, ¤t_count, sizeof(current_count)); + } + + lfs_file_close(&lfs, &file); + return current_count; +} + +uint32_t Flash::open(lfs_file_t* file, const char* filename) { + lfs_file_open(&lfs, file, filename, + LFS_O_RDWR | LFS_O_CREAT | LFS_O_APPEND); + return lfs_file_size(&lfs, file); +} + +uint32_t Flash::close(lfs_file_t* file) { return lfs_file_close(&lfs, file); } + +bool Flash::append(lfs_file_t* file, const uint8_t* bytes, size_t size) { + uint8_t* buf = (uint8_t*)bytes; + uint32_t filesize = lfs_file_size(&lfs, file); + uint32_t off = offset(filesize, lfs.cfg->block_size); + + while (size > 0) { + uint32_t write_size = size < off ? size : off; + if (off == 0) { + // Time for a new block, sync and just write whatever + lfs_file_sync(&lfs, file); + // Due to constraints, the size is guaranteed to be less + // than a block size, so we can just safely do this + write_size = size; + } + + uint32_t res = lfs_file_write(&lfs, file, buf, write_size); + if (res != write_size) + return false; + + size -= res; + filesize += res; + buf += res; + off = offset(filesize, lfs.cfg->block_size); + } + return true; +} +} // namespace Platform diff --git a/src/flash/gd5f1gq5xe.c b/src/flash/gd5f1gq5xe.c deleted file mode 100644 index 288586f..0000000 --- a/src/flash/gd5f1gq5xe.c +++ /dev/null @@ -1,329 +0,0 @@ -#include "gd5f1gq5xe.h" - -#include "defs.h" - -// Flash Commands -#define GD5F_SET_FEATURE 0x1F -#define GD5F_WRITE_ENABLE 0x06 -#define GD5F_WRITE_DISABLE 0x04 -#define GD5F_READ_TO_CACHE 0x13 -#define GD5F_READ_FROM_CACHE 0x03 -#define GD5F_PROGRAM_LOAD 0x02 -#define GD5F_PROGRAM_EXECUTE 0x10 -#define GD5F_ERASE 0xD8 -#define GD5F_READ_ID 0x9F - -// Flash Sizes -#define GD5F_BLOCK_COUNT 1024 -#define GD5F_PAGES_PER_BLOCK 64 -#define GD5F_PAGE_SIZE 2048 -#define GD5F_BLOCK_SIZE GD5F_PAGES_PER_BLOCK * GD5F_PAGE_SIZE - -// This is a NAND flash which uses 3-byte addressing, this means we have -// -// | block address | page address | -// | bytes <15-6> | bytes <5-0> | -// -// NAND flashes employ a two step read and progamming process. For reading we -// first fetch the page using the 3-byte addressing to the cache, and then use a -// second command to read from the cache, here we can index the bytes using a -// column address 12 bytes. -// -// For writing, we do the inverse. We populate the cache with bytes using the index. -// After it is filled, we can then write the page to the flash using the 3-byte addressing -// -// Note that this flash has a page size of 2048, so we actually only need 11 bytes for -// the column address. - -static void chip_select(struct handle_spi *spi) -{ - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_RESET); -} - -static void chip_deselect(struct handle_spi *spi) -{ - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); -} - -static bool spi_transmit(struct handle_spi *spi, void *buffer, const size_t size) -{ - return HAL_SPI_Transmit(spi->handle, (uint8_t*) buffer, size, HAL_MAX_DELAY); -} - -static bool spi_receive(struct handle_spi *spi, void *buffer, const size_t size) -{ - return HAL_SPI_Receive(spi->handle, (uint8_t*) buffer, size, HAL_MAX_DELAY); -} - -static int write_enable(struct handle_spi *spi) -{ - uint8_t tx = GD5F_WRITE_ENABLE; - chip_select(spi); - if (spi_transmit(spi, &tx, sizeof(tx)) != 0) { - chip_deselect(spi); - return 1; - } - chip_deselect(spi); - return 0; -} - -static bool check_id(struct handle_spi *spi) -{ - chip_select(spi); - uint8_t cmd[] = { GD5F_READ_ID, 0x00 }; - uint8_t data[] = { 0, 0 }; - if (spi_transmit(spi, cmd, sizeof(cmd)) != 0) { - chip_deselect(spi); - } - spi_receive(spi, data, sizeof(data)); - chip_deselect(spi); - // Sometimes the check is not consistent - // Investigate for now - return data[0] == 0xC8 && data[1] == 0x31; - /* assert(data[0] == 0xC8); */ - /* assert(data[1] == 0x31); */ -} - -/// Read a page from the flash into the `buffer`. -/// -/// `block` must be less than the block-count -/// `offset` must be less than than the block-size -/// `size` can be larger than the page-size, but this function will only -/// read up to the page-size boundary -/// -/// Returns the number of bytes read. -static uint32_t read_page(struct handle_spi *spi, const uint32_t block, - const uint32_t offset, void *buffer, uint32_t size) -{ - assert(block < GD5F_BLOCK_COUNT); - assert(offset < GD5F_BLOCK_SIZE); - - // This combines the block and offset to create our 3-byte address - // Notice that `block * GD5F_PAGES_PER_BLOCK` is equivalent to - // `block << 6`, which is why this code here works. - uint32_t addr = block * GD5F_PAGES_PER_BLOCK + (offset / GD5F_PAGE_SIZE); - // We can then use the column addreess to read offsets into the page - uint16_t col = offset % GD5F_PAGE_SIZE; - - uint8_t tx1[] = { - GD5F_READ_TO_CACHE, - (addr & 0xFF0000) >> 16, - (addr & 0x00FF00) >> 8, - (addr & 0x0000FF) - }; - chip_select(spi); - if (spi_transmit(spi, tx1, sizeof(tx1)) != 0) { - chip_deselect(spi); - return 0; - } - chip_deselect(spi); - HAL_Delay(2); // Required delay for cache read - - uint8_t tx2[] = { - GD5F_READ_FROM_CACHE, - (col & 0x0F00) >> 8, // first 4 bytes are not needed, - (col & 0x00FF), // remember that we only need 12 bytes - 0x00 // we need a dummy byte (from datasheet) - }; - chip_select(spi); - if (spi_transmit(spi, tx2, sizeof(tx2)) != 0) { - chip_deselect(spi); - return 0; - }; - uint32_t read_size = size <= GD5F_PAGE_SIZE - col ? size : GD5F_PAGE_SIZE - col; - if (spi_receive(spi, buffer, read_size) != 0) { - chip_deselect(spi); - return 0; - } - chip_deselect(spi); - return read_size; -} - -/// Write a page from the `buffer` into the flash -/// -/// `block` must be less than the block-count -/// `offset` must be less than than the block-size -/// `size` can be larger than the page-size, but this function will only -/// write up to the page-size boundary -/// -/// Returns the number of bytes written. -static uint32_t write_page(struct handle_spi *spi, const uint32_t block, - const uint32_t offset, const void *buffer, const uint32_t size) -{ - assert(block < GD5F_BLOCK_COUNT); - assert(offset < GD5F_BLOCK_SIZE); - - uint32_t addr = block * GD5F_PAGES_PER_BLOCK + (offset / GD5F_PAGE_SIZE); - uint16_t col = offset % GD5F_PAGE_SIZE; - - uint8_t tx1[] = { - GD5F_PROGRAM_LOAD, - (col & 0x0F00) >> 8, // similar to before, the first 4 bytes - (col & 0x00FF) // are not needed, hence the 0x0F00 - }; - chip_select(spi); - if (spi_transmit(spi, tx1, sizeof(tx1)) != 0) { - chip_deselect(spi); - return 0; - } - uint32_t write_size = size <= GD5F_PAGE_SIZE - col ? size : GD5F_PAGE_SIZE - col; - if (spi_transmit(spi, buffer, write_size) != 0) { - chip_deselect(spi); - return 0; - }; - chip_deselect(spi); - - if (write_enable(spi) != 0) { - chip_deselect(spi); - return 0; - } - - uint8_t tx2[] = { - GD5F_PROGRAM_EXECUTE, - (addr & 0xFF0000) >> 16, - (addr & 0x00FF00) >> 8, - (addr & 0x0000FF) - }; - chip_select(spi); - if (spi_transmit(spi, tx2, sizeof(tx2)) != 0) { - chip_deselect(spi); - return 0; - } - chip_deselect(spi); - - HAL_Delay(1); - return write_size; -} - -static bool read(struct handle_spi *spi, uint32_t block, uint32_t offset, void *buffer, uint32_t size) -{ - uint8_t *buf = (uint8_t *) buffer; - while (size > 0) { - uint32_t s = read_page(spi, block, offset, buf, size); - if (s == 0) return false; - size -= s; - offset += s; - buf += s; - } - return true; -} - -static bool write(struct handle_spi *spi, uint32_t block, uint32_t offset, void *buffer, uint32_t size) -{ - uint8_t *buf = (uint8_t *) buffer; - while (size > 0) { - uint32_t s = write_page(spi, block, offset, buf, size); - if (s == 0) return false; - size -= s; - offset += s; - buf += s; - } - return true; -} - -static bool erase(struct handle_spi *spi, uint32_t block) -{ - // Erase acts on blocks and not pages, so we should only have the block - // section of the address set and not the page section. - uint32_t addr = block * GD5F_PAGES_PER_BLOCK; - - if (write_enable(spi) != 0) { - chip_deselect(spi); - return false; - } - - uint8_t tx[] = { - GD5F_ERASE, - (addr & 0xFF0000) >> 16, - (addr & 0x00FF00) >> 8, - (addr & 0x0000FF) - }; - chip_select(spi); - if (spi_transmit(spi, tx, sizeof(tx)) != 0) { - chip_deselect(spi); - return false; - } - chip_deselect(spi); - - HAL_Delay(12); - return true; -} - -static bool unlock(struct handle_spi *spi) -{ - // Needed for some reason, I don't know why - HAL_Delay(5000); - if (!check_id(spi)) return false; - if (write_enable(spi) != 0) { - chip_deselect(spi); - return false; - } - - uint8_t tx[] = { - GD5F_SET_FEATURE, - 0xA0, - 0x00, - }; - chip_select(spi); - if (spi_transmit(spi, tx, sizeof(tx)) != 0) { - chip_deselect(spi); - return false; - } - chip_deselect(spi); - - HAL_Delay(5000); - return true; -} - -static int lfs_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t offset, - void *data, lfs_size_t size) -{ - struct handle_spi *spi = (struct handle_spi*) c->context; - if (!read(spi, block, offset, data, size)) return LFS_ERR_IO; - return LFS_ERR_OK; -} - -static int lfs_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t offset, - const void *data, lfs_size_t size) -{ - struct handle_spi *spi = (struct handle_spi*) c->context; - if (!write(spi, block, offset, data, size)) return LFS_ERR_IO; - return LFS_ERR_OK; -} - -static int lfs_erase(const struct lfs_config *c, lfs_block_t block) -{ - struct handle_spi *spi = (struct handle_spi*) c->context; - if (!erase(spi, block)) return LFS_ERR_IO; - return LFS_ERR_OK; -} - -static int lfs_sync(const struct lfs_config *c) -{ - return LFS_ERR_OK; -} - -bool gd5f1gq5xe_init(struct flash *flash, struct handle_spi *spi) -{ - assert(spi->handle != NULL); - flash->config = (struct lfs_config ) { - .context = spi, - .read = lfs_read, - .prog = lfs_prog, - .erase = lfs_erase, - .sync = lfs_sync, - - .read_size = GD5F_PAGE_SIZE, - .prog_size = GD5F_PAGE_SIZE, - .block_size = GD5F_BLOCK_SIZE, - .block_count = GD5F_BLOCK_COUNT, - .cache_size = GD5F_PAGE_SIZE, - .lookahead_size = 128, - .block_cycles = 512, - }; - if (!unlock(spi)) { - return false; - } - - return true; -} diff --git a/src/flash/gd5f1gq5xe.cpp b/src/flash/gd5f1gq5xe.cpp new file mode 100644 index 0000000..99192c5 --- /dev/null +++ b/src/flash/gd5f1gq5xe.cpp @@ -0,0 +1,257 @@ +#include "gd5f1gq5xe.h" + +#include "hal.h" +#include "protocol.h" + +// Flash Commands +constexpr uint8_t GD5F_SET_FEATURE = 0x1F; +constexpr uint8_t GD5F_WRITE_ENABLE = 0x06; +constexpr uint8_t GD5F_WRITE_DISABLE = 0x04; +constexpr uint8_t GD5F_READ_TO_CACHE = 0x13; +constexpr uint8_t GD5F_READ_FROM_CACHE = 0x03; +constexpr uint8_t GD5F_PROGRAM_LOAD = 0x02; +constexpr uint8_t GD5F_PROGRAM_EXECUTE = 0x10; +constexpr uint8_t GD5F_ERASE = 0xD8; +constexpr uint8_t GD5F_READ_ID = 0x9F; + +// QSPI Flash commands +constexpr uint8_t GD5F_READ_FROM_CACHE_QUAD_IO = 0xEB; +constexpr uint8_t GD5F_PROGRAM_LOAD_QUAD = 0x32; + +// Flash Sizes +constexpr uint32_t GD5F_BLOCK_COUNT = 1024; +constexpr uint32_t GD5F_PAGES_PER_BLOCK = 64; +constexpr uint32_t GD5F_PAGE_SIZE = 2048; +constexpr uint32_t GD5F_BLOCK_SIZE = GD5F_PAGES_PER_BLOCK * GD5F_PAGE_SIZE; + +namespace { +using namespace Platform; +// This is a NAND flash which uses 3-byte addressing, this means we have +// +// | block address | page address | +// | bytes <15-6> | bytes <5-0> | +// +// NAND flashes employ a two step read and progamming process. For reading we +// first fetch the page using the 3-byte addressing to the cache, and then use a +// second command to read from the cache, here we can index the bytes using a +// column address 12 bytes. +// +// For writing, we do the inverse. We populate the cache with bytes using the +// index. After it is filled, we can then write the page to the flash using the +// 3-byte addressing +// +// Note that this flash has a page size of 2048, so we actually only need 11 +// bytes for the column address. +int write_enable(Protocol* protocol) { + uint8_t cmd[] = {GD5F_WRITE_ENABLE}; + return protocol->write(ConstSpan(cmd), {}, AddressSize::None); +} + +/// Read a page from the flash into the `buffer`. +/// +/// `block` must be less than the block-count +/// `offset` must be less than than the block-size +/// `size` can be larger than the page-size, but this function will only +/// read up to the page-size boundary +/// +/// Returns the number of bytes read. +uint32_t read_page(Protocol* protocol, const uint32_t block, + const uint32_t offset, void* buffer, uint32_t size) { + assert(block < GD5F_BLOCK_COUNT); + assert(offset < GD5F_BLOCK_SIZE); + + // This combines the block and offset to create our 3-byte address + // Notice that `block * GD5F_PAGES_PER_BLOCK` is equivalent to + // `block << 6`, which is why this code here works. + uint32_t addr = block * GD5F_PAGES_PER_BLOCK + (offset / GD5F_PAGE_SIZE); + uint8_t cmd1[] = {GD5F_READ_TO_CACHE, (addr & 0xFF0000) >> 16, + (addr & 0x00FF00) >> 8, (addr & 0x0000FF)}; + protocol->write(ConstSpan(cmd1), {}, AddressSize::Byte3); + HAL_Delay(2); + + // We can then use the column addreess to read offsets into the page + uint16_t col = offset % GD5F_PAGE_SIZE; + uint32_t read_size = std::min(size, GD5F_PAGE_SIZE - col); + auto cmdlen = protocol->type() == ProtocolType::QSPI ? 5 : 4; + uint8_t cmd2[] = { + protocol->type() == ProtocolType::QSPI ? GD5F_READ_FROM_CACHE_QUAD_IO + : GD5F_READ_FROM_CACHE, + (col & 0x0F00) >> 8, // first 4 bytes are not needed, + (col & 0x00FF), // remember that we only need 12 bytes + 0x00, // we need a dummy byte (from datasheet) + 0x00 // for qspi we need another dummy byte (for 4 clock cycles) + }; + // We use the QUAD IO feature of our flash for even more read speed + // This requires us to use four address lines though + protocol->configure(Config::QSPI_Address4); + auto* buf = static_cast(buffer); + protocol->read(ConstSpan(cmd2, cmdlen), Span(buf, read_size), + AddressSize::Byte2); + protocol->configure(Config::QSPI_Address1); + + return read_size; +} + +/// Write a page from the `buffer` into the flash +/// +/// `block` must be less than the block-count +/// `offset` must be less than than the block-size +/// `size` can be larger than the page-size, but this function will only +/// write up to the page-size boundary +/// +/// Returns the number of bytes written. +uint32_t write_page(Protocol* protocol, const uint32_t block, + const uint32_t offset, const void* buffer, + const uint32_t size) { + assert(block < GD5F_BLOCK_COUNT); + assert(offset < GD5F_BLOCK_SIZE); + + uint16_t col = offset % GD5F_PAGE_SIZE; + uint32_t write_size = std::min(size, GD5F_PAGE_SIZE - col); + uint8_t cmd1[] = { + protocol->type() == ProtocolType::QSPI ? GD5F_PROGRAM_LOAD_QUAD + : GD5F_PROGRAM_LOAD, + (col & 0x0F00) >> 8, // similar to before, the first 4 bytes + (col & 0x00FF) // are not needed, hence the 0x0F00 + }; + auto* buf = static_cast(buffer); + protocol->write(ConstSpan(cmd1), ConstSpan(buf, write_size), + AddressSize::Byte2); + + if (write_enable(protocol) != 0) + return 0; + uint32_t addr = block * GD5F_PAGES_PER_BLOCK + (offset / GD5F_PAGE_SIZE); + uint8_t cmd2[] = { + GD5F_PROGRAM_EXECUTE, + (addr & 0xFF0000) >> 16, // execute essentially writes the page + (addr & 0x00FF00) >> 8, // to the flash, so you use load + (addr & 0x0000FF) // to fill a page, and execute to write it + }; + protocol->write(ConstSpan(cmd2), {}, AddressSize::Byte3); + HAL_Delay(1); + + return write_size; +} + +bool read(Protocol* protocol, uint32_t block, uint32_t offset, void* buffer, + uint32_t size) { + uint8_t* buf = (uint8_t*)buffer; + while (size > 0) { + uint32_t s = read_page(protocol, block, offset, buf, size); + if (s == 0) + return false; + size -= s; + offset += s; + buf += s; + } + return true; +} + +bool write(Protocol* protocol, uint32_t block, uint32_t offset, + const void* buffer, uint32_t size) { + uint8_t* buf = (uint8_t*)buffer; + while (size > 0) { + uint32_t s = write_page(protocol, block, offset, buf, size); + if (s == 0) + return false; + size -= s; + offset += s; + buf += s; + } + return true; +} + +bool erase(Protocol* protocol, uint32_t block) { + // Erase acts on blocks and not pages, so we should only have the block + // section of the address set and not the page section. + uint32_t addr = block * GD5F_PAGES_PER_BLOCK; + if (write_enable(protocol) != 0) + return false; + uint8_t cmd[] = { + GD5F_ERASE, + (addr & 0xFF0000) >> 16, // Erase only takes the address + (addr & 0x00FF00) >> 8, // remember that this is because + (addr & 0x0000FF) // it operates on whiole blocks + }; + protocol->write(ConstSpan(cmd), {}, AddressSize::Byte3); + HAL_Delay(12); + return true; +} + +int lfs_read(const struct lfs_config* c, lfs_block_t block, lfs_off_t offset, + void* data, lfs_size_t size) { + auto* protocol = static_cast(c->context); + if (!read(protocol, block, offset, data, size)) + return LFS_ERR_IO; + return LFS_ERR_OK; +} + +int lfs_prog(const struct lfs_config* c, lfs_block_t block, lfs_off_t offset, + const void* data, lfs_size_t size) { + auto* protocol = static_cast(c->context); + if (!write(protocol, block, offset, data, size)) + return LFS_ERR_IO; + return LFS_ERR_OK; +} + +int lfs_erase(const struct lfs_config* c, lfs_block_t block) { + auto* protocol = static_cast(c->context); + if (!erase(protocol, block)) + return LFS_ERR_IO; + return LFS_ERR_OK; +} + +int lfs_sync(const struct lfs_config* c) { return LFS_ERR_OK; } +} // namespace + +namespace Platform { +GD5F1GQ5XE::GD5F1GQ5XE(Protocol& protocol_) : protocol(protocol_) { + assert(protocol.type() == ProtocolType::SPI || + protocol.type() == ProtocolType::QSPI); + config.context = &protocol; + config.read = lfs_read; + config.prog = lfs_prog; + config.erase = lfs_erase; + config.sync = lfs_sync; + + config.read_size = GD5F_PAGE_SIZE; + config.prog_size = GD5F_PAGE_SIZE; + config.block_size = GD5F_BLOCK_SIZE; + config.block_count = GD5F_BLOCK_COUNT; + config.cache_size = GD5F_PAGE_SIZE; + config.lookahead_size = 128; + config.block_cycles = 512; +} + +bool GD5F1GQ5XE::init() { + // Needed for some reason, I don't know why + HAL_Delay(5000); + + // If using QSPI, we have to set this for some commands + protocol.configure(Config::QSPI_Data1); + // Check if the id matches + uint8_t cmd1[] = {GD5F_READ_ID, 0x00}; + uint8_t data[] = {0, 0}; + protocol.read(ConstSpan(cmd1), Span(data), AddressSize::None); + if (data[0] != 0xC8 && data[1] != 0x31) + return false; + + // Enable writes to the flash as a whole, by setting Protection + // to 0x00, so nothing is protected. + if (write_enable(&protocol) != 0) + return false; + uint8_t cmd2[] = {GD5F_SET_FEATURE, 0xA0, 0x00}; + protocol.write(ConstSpan(cmd2), {}, AddressSize::Byte2); + HAL_Delay(5000); + + if (protocol.type() == ProtocolType::QSPI) { + // Enable QE feature so we can quad reads and writes + uint8_t cmd3[] = {GD5F_SET_FEATURE, 0xB0, 0x01}; + protocol.write(ConstSpan(cmd3), {}, AddressSize::Byte2); + // All done, set it back to quad + protocol.configure(Config::QSPI_Data4); + } + + return true; +} +} // namespace Platform diff --git a/src/protocols/CMakeLists.txt b/src/protocols/CMakeLists.txt new file mode 100644 index 0000000..c17a781 --- /dev/null +++ b/src/protocols/CMakeLists.txt @@ -0,0 +1,33 @@ +# What this basically does is conditionally include the i2c, qspi, spi, and uart +# files if and only if they are enabled in cubemx. +# +# When cubemx enables the perhipherals, it adds something like this +# ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_spi.c +# to STM32_Drivers_Src. Therefore we can just iterate through it and find if the +# given drivers exist, and if they do, we can safely include our thin HAL abstraction +# for the driver. +set(PATTERNS + "_hal_i2c\\.c$" + "_hal_qspi\\.c$" + "_hal_spi\\.c$" + "_hal_uart\\.c$" +) + +set(SOURCES + "i2c.cpp" + "qspi.cpp" + "spi.cpp" + "uart.cpp" +) + +if(TARGET STM32_Drivers) + get_target_property(STM32_Drivers_Src STM32_Drivers SOURCES) + foreach(PATTERN SOURCE IN ZIP_LISTS PATTERNS SOURCES) + string(REGEX MATCH "${PATTERN}" FOUND_MATCH "${STM32_Drivers_Src}") + if(FOUND_MATCH) + # If the first pattern gets matched, then we add our file to the library + message(STATUS "Including " ${SOURCE} " in build") + target_sources(common_drivers PRIVATE "${SOURCE}") + endif() + endforeach() +endif() diff --git a/src/protocols/i2c.cpp b/src/protocols/i2c.cpp new file mode 100644 index 0000000..3912019 --- /dev/null +++ b/src/protocols/i2c.cpp @@ -0,0 +1,46 @@ +#include "i2c.h" + +#include "hal.h" +#include "protocol.h" + +#include +#include + +namespace { +uint32_t get_address_size(Platform::AddressSize size) { + assert((size == Platform::AddressSize::Byte || + size == Platform::AddressSize::Byte2) && + "I2C addressses must one or two bytes"); + switch (size) { + case Platform::AddressSize::Byte: + return I2C_MEMADD_SIZE_8BIT; + case Platform::AddressSize::Byte2: + return I2C_MEMADD_SIZE_16BIT; + default: + return 0; // shouldn't be reached + } +} + +uint16_t get_address(Platform::ConstSpan cmd, Platform::AddressSize size) { + assert(!cmd.empty() && "Address cannot be empty"); + if (size == Platform::AddressSize::Byte) + return cmd[0]; + assert((cmd.size() == 2) && "Address length must be be two bytes bytes"); + return static_cast((cmd[0] << 8) | cmd[1]); +} +} // namespace + +namespace Platform { +bool I2C::read(ConstSpan cmd, Span buffer, AddressSize size) { + auto asize = get_address_size(size); + return HAL_I2C_Mem_Read(handle, address << 1, get_address(cmd, size), asize, + buffer.data(), buffer.size(), HAL_MAX_DELAY); +} + +bool I2C::write(ConstSpan cmd, ConstSpan buffer, AddressSize size) { + auto asize = get_address_size(size); + return HAL_I2C_Mem_Write(handle, address << 1, get_address(cmd, size), + asize, buffer.data(), buffer.size(), + HAL_MAX_DELAY); +} +} // namespace Platform diff --git a/src/protocols/qspi.cpp b/src/protocols/qspi.cpp new file mode 100644 index 0000000..eaec8ca --- /dev/null +++ b/src/protocols/qspi.cpp @@ -0,0 +1,86 @@ +#include "qspi.h" + +#include + +namespace { +uint32_t get_address_size(Platform::AddressSize size) { + switch (size) { + case Platform::AddressSize::Byte: + return QSPI_ADDRESS_8_BITS; + case Platform::AddressSize::Byte2: + return QSPI_ADDRESS_16_BITS; + case Platform::AddressSize::Byte3: + return QSPI_ADDRESS_24_BITS; + case Platform::AddressSize::Byte4: + return QSPI_ADDRESS_32_BITS; + case Platform::AddressSize::None: + return QSPI_ADDRESS_NONE; + } +} + +uint32_t get_address(Platform::ConstSpan cmd, Platform::AddressSize size) { + auto numbytes = static_cast(size); + uint32_t address = 0; + assert(cmd.size() > numbytes && "You misconfigured your address size"); + for (int i = 0; i < numbytes; ++i) + address |= static_cast(cmd[numbytes - i]) << (8 * i); + return address; +} + +void configure_cmd(Platform::ConstSpan cmd, Platform::AddressSize size, + QSPI_CommandTypeDef& qcmd) { + // Configure instruction and address + qcmd.Instruction = cmd[0]; + qcmd.AddressMode = address_mode; + qcmd.AddressSize = get_address_size(size); + if (qcmd.AddressSize != QSPI_ADDRESS_NONE) + qcmd.Address = get_address(cmd, size); + else + qcmd.AddressMode = QSPI_ADDRESS_NONE; + + // Calculate dummy cycles from the number of empty bytes times 2 + // TODO: I think the times 2 portion really only works for + // quadspi with four lines, this might be fine though. + qcmd.DummyCycles = (cmd.size() - 1 - static_cast(size)) * 2; + assert(qcmd.DummyCycles < + 12); // Keep this here for now, should never trigger + + // Configure data mode + qcmd.DataMode = data_mode; + if (!buffer.empty()) + qcmd.NbData = buffer.size(); + else + qcmd.DataMode = QSPI_DATA_NONE; + + // These are generally the most common configuration, you can + // probably deviate from it but then you get some weird confusing + // stuff that we probably shouldn't deal with + qcmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; + qcmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; +} +} // namespace + +namespace Platform { +bool QSPI::read(ConstSpan cmd, Span buffer, AddressSize size) { + QSPI_CommandTypeDef qcmd; + configure_cmd(cmd, size, qcmd); + + auto res = HAL_QSPI_Command(handle, &qcmd, HAL_MAX_DELAY); + if (res != HAL_OK) + return res; + return HAL_QSPI_Receive(handle, buffer.data(), HAL_MAX_DELAY); +} + +bool QSPI::write(ConstSpan cmd, ConstSpan buffer, AddressSize size) { + QSPI_CommandTypeDef qcmd; + configure_cmd(cmd, size, qcmd); + + auto res = HAL_QSPI_Command(handle, &qcmd, HAL_MAX_DELAY); + if (res != HAL_OK) + return res; + + if (!buffer.empty()) + return HAL_QSPI_Transmit(handle, buffer.data(), HAL_MAX_DELAY); + return res; +} +} // namespace Platform diff --git a/src/protocols/spi.cpp b/src/protocols/spi.cpp new file mode 100644 index 0000000..d0ba320 --- /dev/null +++ b/src/protocols/spi.cpp @@ -0,0 +1,41 @@ +#include "spi.h" + +#include + +namespace { +/// This handy guard makes sure we call chip deselect after we return +/// from the function, either due to an error or due to a success. +struct Guard { + GPIO_TypeDef* port; + uint16_t pin; + + Guard(GPIO_TypeDef* port, uint16_t pin) : port(port), pin(pin) { + HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET); + } + + ~Guard() { HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET); } +}; +} // namespace + +namespace Platform { +bool SPI::read(ConstSpan cmd, Span buffer, AddressSize size) { + assert(!cmd.empty() && "Command buffer should not be empty"); + Guard cs = Guard(port, pin); + auto res = HAL_SPI_Transmit(handle, cmd.data(), cmd.size(), HAL_MAX_DELAY); + if (res != HAL_OK) + return res; + return HAL_SPI_Receive(handle, buffer.data(), buffer.size(), HAL_MAX_DELAY); +} + +bool SPI::write(ConstSpan cmd, ConstSpan buffer, AddressSize size) { + assert(!cmd.empty() && "Command buffer should not be empty"); + Guard cs = Guard(port, pin); + auto res = HAL_SPI_Transmit(handle, cmd.data(), cmd.size(), HAL_MAX_DELAY); + if (res != HAL_OK) + return res; + if (!buffer.empty()) + return HAL_SPI_Transmit(handle, buffer.data(), buffer.size(), + HAL_MAX_DELAY); + return res; +} +} // namespace Platform diff --git a/src/protocols/uart.cpp b/src/protocols/uart.cpp new file mode 100644 index 0000000..b40597f --- /dev/null +++ b/src/protocols/uart.cpp @@ -0,0 +1,19 @@ +#include "uart.h" + +namespace Platform { +bool UART::read(ConstSpan cmd, Span buffer, AddressSize size) { + assert(cmd.empty() && "Just read using the buffer"); + return HAL_UART_Receive(handle, buffer.data(), buffer.size(), + HAL_MAX_DELAY); +} + +bool UART::write(ConstSpan cmd, ConstSpan buffer, AddressSize size) { + assert(cmd.empty() && "Just write using the buffer"); + return HAL_UART_Transmit(handle, buffer.data(), buffer.size(), + HAL_MAX_DELAY); +} + +bool UART::readDMA(Span buffer) { + return HAL_UARTEx_ReceiveToIdle_DMA(handle, buffer.data(), buffer.size()); +} +} // namespace Platform diff --git a/src/sensors/CMakeLists.txt b/src/sensors/CMakeLists.txt new file mode 100644 index 0000000..c83a310 --- /dev/null +++ b/src/sensors/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources(common_drivers PRIVATE + "bmi088.cpp" + "bmp581.cpp" + "cd-pa1616s.cpp" +) diff --git a/src/sensors/bmi088.c b/src/sensors/bmi088.c deleted file mode 100644 index dc59e8c..0000000 --- a/src/sensors/bmi088.c +++ /dev/null @@ -1,236 +0,0 @@ -/* - * bmi088.c - * - * Created on: Oct 26, 2025 - * Author: Dhruv Shah - */ - -#include "sensor.h" -#include "defs.h" - -#include "bmi08_defs.h" -#include "bmi088.h" -#include "bmi08x.h" -#include "bmi08.h" - -#include -#include -#include - -#define CONVERT_GYRO_RAW_RANGE(raw, range) ((((float)raw * (float)range) / 32768.0f) * (M_PI / 180.0f)) - -static BMI08_INTF_RET_TYPE bmi088_read_spi(uint8_t reg_addr, uint8_t *reg_data, uint32_t len, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - struct handle_spi* spi = (struct handle_spi*) intf_ptr; - - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_RESET); - - HAL_StatusTypeDef ret = HAL_OK; - - ret = HAL_SPI_Transmit(spi->handle, ®_addr, 1, HAL_MAX_DELAY); - - if (ret != HAL_OK) { - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - return ret; - } - - ret = HAL_SPI_Receive(spi->handle, reg_data, len, HAL_MAX_DELAY); - - if (ret != HAL_OK) { - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - return ret; - } - - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - - return 0; -} - -static BMI08_INTF_RET_TYPE bmi088_write_spi(uint8_t reg_addr, const uint8_t *reg_data, uint32_t len, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - struct handle_spi* spi = (struct handle_spi*) intf_ptr; - - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_RESET); - - HAL_StatusTypeDef ret = HAL_OK; - - ret = HAL_SPI_Transmit(spi->handle, ®_addr, 1, HAL_MAX_DELAY); - - if (ret != HAL_OK) { - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - return ret; - } - - ret = HAL_SPI_Transmit(spi->handle, reg_data, len, HAL_MAX_DELAY); - - if (ret != HAL_OK) { - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - return ret; - } - - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - - return 0; -} - -static void bmi088_delay_us(uint32_t period, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - HAL_Delay(ceil((double)(period)/(1000.0))); -} - -static float bmi088_convert_accel_axis_data(struct bmi088_ctx *ctx, int16_t axis_data) -{ - return ((float)axis_data / 32768.0f * 1000 * pow(2, ctx->dev.accel_cfg.range + 1) * 1.5) * 0.00981; -} - -static float bmi088_convert_gyro_axis_data(struct bmi088_ctx *ctx, int16_t axis_data) -{ - switch (ctx->dev.gyro_cfg.range) { - case BMI08_GYRO_RANGE_2000_DPS: - return CONVERT_GYRO_RAW_RANGE(axis_data, 2000); - case BMI08_GYRO_RANGE_1000_DPS: - return CONVERT_GYRO_RAW_RANGE(axis_data, 1000); - case BMI08_GYRO_RANGE_500_DPS: - return CONVERT_GYRO_RAW_RANGE(axis_data, 500); - case BMI08_GYRO_RANGE_250_DPS: - return CONVERT_GYRO_RAW_RANGE(axis_data, 250); - case BMI08_GYRO_RANGE_125_DPS: - return CONVERT_GYRO_RAW_RANGE(axis_data, 125); - default: - return 0.0f; - } -} - -STATIC bool bmi088_read(void *context, struct packet *packet) -{ - struct bmi088_ctx *ctx = (struct bmi088_ctx*) context; - struct bmi08_sensor_data gyro_data; - struct bmi08_sensor_data accel_data; - - bmi08a_get_data(&accel_data, &ctx->dev); - bmi08g_get_data(&gyro_data, &ctx->dev); - - packet->acceleration_x_mss = bmi088_convert_accel_axis_data(ctx, accel_data.x); - packet->acceleration_y_mss = bmi088_convert_accel_axis_data(ctx, accel_data.y); - packet->acceleration_z_mss = bmi088_convert_accel_axis_data(ctx, accel_data.z); - - packet->angular_velocity_x_rads = bmi088_convert_gyro_axis_data(ctx, gyro_data.x); - packet->angular_velocity_y_rads = bmi088_convert_gyro_axis_data(ctx, gyro_data.y); - packet->angular_velocity_z_rads = bmi088_convert_gyro_axis_data(ctx, gyro_data.z); - return true; -} - -int8_t bmi088_init(struct bmi088_ctx *ctx, struct sensor *sensor) // GCOVR_EXCL_FUNCTION -{ - assert(ctx->accel_spi.handle != NULL); - assert(ctx->gyro_spi.handle != NULL); - - HAL_GPIO_WritePin(ctx->accel_spi.port, ctx->accel_spi.pin, GPIO_PIN_SET); - HAL_GPIO_WritePin(ctx->gyro_spi.port, ctx->gyro_spi.pin, GPIO_PIN_SET); - - struct bmi08_accel_int_channel_cfg accel_new_data_int_cfg; - struct bmi08_gyro_int_channel_cfg gyro_new_data_int_cfg; - - ctx->dev.intf_ptr_accel = &ctx->accel_spi; - ctx->dev.intf_ptr_gyro = &ctx->gyro_spi; - ctx->dev.intf = BMI08_SPI_INTF; - ctx->dev.variant = BMI088_VARIANT; - ctx->dev.read_write_len = 8; - ctx->dev.read = bmi088_read_spi; - ctx->dev.write = bmi088_write_spi; - ctx->dev.delay_us = bmi088_delay_us; - - ctx->dev.accel_cfg.power = BMI08_ACCEL_PM_ACTIVE; - ctx->dev.accel_cfg.range = BMI088_ACCEL_RANGE_24G; - ctx->dev.accel_cfg.bw = BMI08_ACCEL_BW_NORMAL; - ctx->dev.accel_cfg.odr = BMI08_ACCEL_ODR_800_HZ; - - ctx->dev.gyro_cfg.power = BMI08_GYRO_PM_NORMAL; - ctx->dev.gyro_cfg.range = BMI08_GYRO_RANGE_2000_DPS; - ctx->dev.gyro_cfg.bw = BMI08_GYRO_BW_116_ODR_1000_HZ; - ctx->dev.gyro_cfg.odr = BMI08_GYRO_BW_116_ODR_1000_HZ; - - accel_new_data_int_cfg.int_channel = BMI08_INT_CHANNEL_1; - accel_new_data_int_cfg.int_type = BMI08_ACCEL_INT_DATA_RDY; - accel_new_data_int_cfg.int_pin_cfg.output_mode = BMI08_INT_MODE_PUSH_PULL; - accel_new_data_int_cfg.int_pin_cfg.lvl = BMI08_INT_ACTIVE_HIGH; - accel_new_data_int_cfg.int_pin_cfg.enable_int_pin = BMI08_ENABLE; - - gyro_new_data_int_cfg.int_channel = BMI08_INT_CHANNEL_3; - gyro_new_data_int_cfg.int_type = BMI08_GYRO_INT_DATA_RDY; - gyro_new_data_int_cfg.int_pin_cfg.output_mode = BMI08_INT_MODE_PUSH_PULL; - gyro_new_data_int_cfg.int_pin_cfg.lvl = BMI08_INT_ACTIVE_HIGH; - gyro_new_data_int_cfg.int_pin_cfg.enable_int_pin = BMI08_ENABLE; - - int8_t ret = BMI08_OK; - - ret = bmi08a_soft_reset(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08xa_init(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08a_load_config_file(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08a_set_power_mode(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08xa_set_meas_conf(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08a_set_int_config(&accel_new_data_int_cfg, &ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08g_soft_reset(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08g_init(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08g_set_power_mode(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08g_set_meas_conf(&ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - - ret = bmi08g_set_int_config(&gyro_new_data_int_cfg, &ctx->dev); - - if (ret != BMI08_OK) { - return ret; - } - sensor->ctx = ctx; - sensor->read = bmi088_read; - - return BMI08_OK; -} diff --git a/src/sensors/bmi088.cpp b/src/sensors/bmi088.cpp new file mode 100644 index 0000000..0e02843 --- /dev/null +++ b/src/sensors/bmi088.cpp @@ -0,0 +1,176 @@ +/* + * bmi088.c + * + * Created on: Oct 26, 2025 + * Author: Dhruv Shah + */ + +#include "hal.h" +#include "protocol.h" +#include "sensor.h" + +#include "bmi08.h" +#include "bmi088.h" +#include "bmi08_defs.h" +#include "bmi08x.h" + +#include +#include +#include + +#define CONVERT_GYRO_RAW_RANGE(raw, range) \ + ((((float)raw * (float)range) / 32768.0f) * (std::numbers::pi / 180.0f)) + +namespace { +BMI08_INTF_RET_TYPE bosch_read(uint8_t reg_addr, uint8_t* reg_data, + uint32_t length, void* intf_ptr) { + auto* protocol = static_cast(intf_ptr); + return protocol->read(Platform::ConstSpan(®_addr, 1), + Platform::Span(reg_data, length), + Platform::AddressSize::Byte); +} + +BMI08_INTF_RET_TYPE bosch_write(uint8_t reg_addr, const uint8_t* reg_data, + uint32_t length, void* intf_ptr) { + auto* protocol = static_cast(intf_ptr); + return protocol->write(Platform::ConstSpan(®_addr, 1), + Platform::ConstSpan(reg_data, length), + Platform::AddressSize::Byte); +} + +void bosch_delay(uint32_t period, void* intf_ptr) { + HAL_Delay(ceil(static_cast(period) / (1000.0))); +} + +float bmi088_convert_accel_axis_data(struct bmi08_dev& dev, int16_t axis_data) { + return (static_cast(axis_data) / 32768.0f * 1000 * + pow(2, dev.accel_cfg.range + 1) * 1.5) * + 0.00981; +} + +float bmi088_convert_gyro_axis_data(struct bmi08_dev& dev, int16_t axis_data) { + switch (dev.gyro_cfg.range) { + case BMI08_GYRO_RANGE_2000_DPS: + return CONVERT_GYRO_RAW_RANGE(axis_data, 2000); + case BMI08_GYRO_RANGE_1000_DPS: + return CONVERT_GYRO_RAW_RANGE(axis_data, 1000); + case BMI08_GYRO_RANGE_500_DPS: + return CONVERT_GYRO_RAW_RANGE(axis_data, 500); + case BMI08_GYRO_RANGE_250_DPS: + return CONVERT_GYRO_RAW_RANGE(axis_data, 250); + case BMI08_GYRO_RANGE_125_DPS: + return CONVERT_GYRO_RAW_RANGE(axis_data, 125); + default: + return 0.0f; + } +} +} // namespace + +namespace Platform { +bool BMI088::init() { + struct bmi08_accel_int_channel_cfg accel_new_data_int_cfg; + struct bmi08_gyro_int_channel_cfg gyro_new_data_int_cfg; + + dev.intf_ptr_accel = &accel; + dev.intf_ptr_gyro = &gyro; + dev.intf = BMI08_SPI_INTF; + dev.variant = BMI088_VARIANT; + dev.read_write_len = 8; + dev.read = bosch_read; + dev.write = bosch_write; + dev.delay_us = bosch_delay; + + dev.accel_cfg.power = BMI08_ACCEL_PM_ACTIVE; + dev.accel_cfg.range = BMI088_ACCEL_RANGE_24G; + dev.accel_cfg.bw = BMI08_ACCEL_BW_NORMAL; + dev.accel_cfg.odr = BMI08_ACCEL_ODR_800_HZ; + + dev.gyro_cfg.power = BMI08_GYRO_PM_NORMAL; + dev.gyro_cfg.range = BMI08_GYRO_RANGE_2000_DPS; + dev.gyro_cfg.bw = BMI08_GYRO_BW_116_ODR_1000_HZ; + dev.gyro_cfg.odr = BMI08_GYRO_BW_116_ODR_1000_HZ; + + accel_new_data_int_cfg.int_channel = BMI08_INT_CHANNEL_1; + accel_new_data_int_cfg.int_type = BMI08_ACCEL_INT_DATA_RDY; + accel_new_data_int_cfg.int_pin_cfg.output_mode = BMI08_INT_MODE_PUSH_PULL; + accel_new_data_int_cfg.int_pin_cfg.lvl = BMI08_INT_ACTIVE_HIGH; + accel_new_data_int_cfg.int_pin_cfg.enable_int_pin = BMI08_ENABLE; + + gyro_new_data_int_cfg.int_channel = BMI08_INT_CHANNEL_3; + gyro_new_data_int_cfg.int_type = BMI08_GYRO_INT_DATA_RDY; + gyro_new_data_int_cfg.int_pin_cfg.output_mode = BMI08_INT_MODE_PUSH_PULL; + gyro_new_data_int_cfg.int_pin_cfg.lvl = BMI08_INT_ACTIVE_HIGH; + gyro_new_data_int_cfg.int_pin_cfg.enable_int_pin = BMI08_ENABLE; + + int8_t ret = BMI08_OK; + + ret = bmi08a_soft_reset(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08xa_init(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08a_load_config_file(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08a_set_power_mode(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08xa_set_meas_conf(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08a_set_int_config(&accel_new_data_int_cfg, &dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08g_soft_reset(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08g_init(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08g_set_power_mode(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08g_set_meas_conf(&dev); + if (ret != BMI08_OK) + return false; + + ret = bmi08g_set_int_config(&gyro_new_data_int_cfg, &dev); + if (ret != BMI08_OK) + return false; + + return true; +} + +bool BMI088::read(Packet& packet) { + struct bmi08_sensor_data gyro_data; + struct bmi08_sensor_data accel_data; + + bmi08a_get_data(&accel_data, &dev); + bmi08g_get_data(&gyro_data, &dev); + + packet.acceleration_x_mss = + bmi088_convert_accel_axis_data(dev, accel_data.x); + packet.acceleration_y_mss = + bmi088_convert_accel_axis_data(dev, accel_data.y); + packet.acceleration_z_mss = + bmi088_convert_accel_axis_data(dev, accel_data.z); + + packet.angular_velocity_x_rads = + bmi088_convert_gyro_axis_data(dev, gyro_data.x); + packet.angular_velocity_y_rads = + bmi088_convert_gyro_axis_data(dev, gyro_data.y); + packet.angular_velocity_z_rads = + bmi088_convert_gyro_axis_data(dev, gyro_data.z); + return true; +} +} // namespace Platform diff --git a/src/sensors/bmp581.c b/src/sensors/bmp581.c deleted file mode 100644 index 4ae2890..0000000 --- a/src/sensors/bmp581.c +++ /dev/null @@ -1,190 +0,0 @@ -#include "bmp581.h" - -#include "sensor.h" -#include "defs.h" - -#include "bmp5.h" -#include "bmp5_defs.h" - -#include -#include -#include - -#define GRAVITY_ACCEL 9.80665f // m/s^2 -#define AIR_MOLAR_MASS 0.0289644f // kg/mol -#define GAS_CONSTANT 8.31446f // J/(mol*K) - -#define TROPOPAUSE_PRESSURE 22630.0f // Pa -#define STRATOSPHERE_MIDDLE_PRESSURE 5475.0f // Pa -#define STANDARD_SEA_LEVEL_PRESSURE 101325.0f // Pa - -#define STANDARD_SEA_LEVEL_TEMP 288.15f // K -#define STRATOSPHERE_BASE_TEMP 216.65f // K - -#define TROPOPAUSE_BASE_ALTITUDE 11000.0f // m -#define STRATOSPHERE_MIDDLE_BASE_ALTITUDE 20000.0f // m - -#define TROPOSPHERE_LAPSE_RATE -0.0065f // K/m -#define UPPER_STRATOSPHERE_LAPSE_RATE 0.001f // K/m - -static inline float calc_altitude_troposphere_msl(float pressure) -{ - float exponent = (-GAS_CONSTANT * TROPOSPHERE_LAPSE_RATE) / (GRAVITY_ACCEL * AIR_MOLAR_MASS); - float pressure_ratio = pressure / STANDARD_SEA_LEVEL_PRESSURE; - float power_term = pow(pressure_ratio, exponent) - 1; - - return (STANDARD_SEA_LEVEL_TEMP / TROPOSPHERE_LAPSE_RATE) * power_term; -} - -static inline float calc_altitude_lower_stratosphere_msl(float pressure) -{ - float log_ratio = log(pressure / TROPOPAUSE_PRESSURE); - float scale_factor = (GAS_CONSTANT * STRATOSPHERE_BASE_TEMP) / (GRAVITY_ACCEL * AIR_MOLAR_MASS); - - return TROPOPAUSE_BASE_ALTITUDE - (scale_factor * log_ratio); -} - -static inline float calc_altitude_upper_stratosphere_msl(float pressure) -{ - float exponent = (-GAS_CONSTANT * UPPER_STRATOSPHERE_LAPSE_RATE) / (GRAVITY_ACCEL * AIR_MOLAR_MASS); - float pressure_ratio = pressure / STRATOSPHERE_MIDDLE_PRESSURE; - float power_term = pow(pressure_ratio, exponent) - 1; - - return STRATOSPHERE_MIDDLE_BASE_ALTITUDE + (STRATOSPHERE_BASE_TEMP / UPPER_STRATOSPHERE_LAPSE_RATE) * power_term; -} - -static inline float bmp581_estimate_altitude_msl(struct bmp5_sensor_data *data) -{ - if (data->pressure > TROPOPAUSE_PRESSURE) { - return calc_altitude_troposphere_msl(data->pressure); - } else if (data->pressure > STRATOSPHERE_MIDDLE_PRESSURE) { - return calc_altitude_lower_stratosphere_msl(data->pressure); - } else { - return calc_altitude_upper_stratosphere_msl(data->pressure); - } -} - -#ifdef HAL_I2C_MODULE_ENABLED -static BMP5_INTF_RET_TYPE read_i2c(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - struct handle_i2c *i2c = (struct handle_i2c*) intf_ptr; - HAL_StatusTypeDef res = HAL_I2C_Mem_Read(i2c->handle, i2c->address << 1, reg_addr, I2C_MEMADD_SIZE_8BIT, reg_data, length, HAL_MAX_DELAY); - return res; -} - -static BMP5_INTF_RET_TYPE write_i2c(uint8_t reg_addr, const uint8_t *reg_data, uint32_t length, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - struct handle_i2c *i2c = (struct handle_i2c*) intf_ptr; - HAL_StatusTypeDef res = HAL_I2C_Mem_Write(i2c->handle, i2c->address << 1, reg_addr, I2C_MEMADD_SIZE_8BIT, reg_data, length, HAL_MAX_DELAY); - return res; -} -#endif - -static BMP5_INTF_RET_TYPE read_spi(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - struct handle_spi *spi = (struct handle_spi*) intf_ptr; - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_RESET); - HAL_SPI_Transmit(spi->handle, ®_addr, 1, HAL_MAX_DELAY); - - HAL_StatusTypeDef res = HAL_SPI_Receive(spi->handle, reg_data, length, HAL_MAX_DELAY); - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - return res; -} - -static BMP5_INTF_RET_TYPE write_spi(uint8_t reg_addr, const uint8_t *reg_data, uint32_t length, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - struct handle_spi *spi = (struct handle_spi*) intf_ptr; - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_RESET); - HAL_SPI_Transmit(spi->handle, ®_addr, 1, HAL_MAX_DELAY); - - HAL_StatusTypeDef res = HAL_SPI_Transmit(spi->handle, reg_data, length, HAL_MAX_DELAY); - HAL_GPIO_WritePin(spi->port, spi->pin, GPIO_PIN_SET); - return res; -} - -static void delay(uint32_t period, void *intf_ptr) // GCOVR_EXCL_FUNCTION -{ - HAL_Delay(ceil((double)(period)/(1000.0))); -} - -STATIC bool bmp581_read(void *context, struct packet *packet) -{ - struct bmp581_ctx *ctx = (struct bmp581_ctx*) context; - struct bmp5_sensor_data data; - - bmp5_get_sensor_data(&data, &(ctx->odr_config), &(ctx->dev)); - - packet->barometer_hMSL_m = bmp581_estimate_altitude_msl(&data); - packet->temperature_c = data.temperature; - packet->kf_position_m = data.pressure; - return true; -} - -int8_t bmp581_init(struct bmp581_ctx *ctx, struct sensor *sensor) // GCOVR_EXCL_FUNCTION -{ - int8_t result = BMP5_OK; - - switch (ctx->handle.protocol) { - case SPI: - ctx->dev.intf_ptr = &ctx->handle.def.spi; - ctx->dev.intf = BMP5_SPI_INTF; - ctx->dev.read = read_spi; - ctx->dev.write = write_spi; - break; - case I2C: -#ifdef HAL_I2C_MODULE_ENABLED - ctx->dev.intf_ptr = &ctx->handle.def.i2c; - ctx->dev.intf = BMP5_I2C_INTF; - ctx->dev.read = read_i2c; - ctx->dev.write = write_i2c; - break; -#else - assert("I2C module is not enabled! Please choose SPI for BMP581"); - break; -#endif - default: assert("Invalid interface for bmp581, must choose either SPI or I2C"); - }; - - ctx->dev.delay_us = delay; - ctx->odr_config.odr = BMP5_ODR_240_HZ; - ctx->odr_config.press_en = BMP5_ENABLE; - ctx->int_config.drdy_en = BMP5_ENABLE; - ctx->int_config.fifo_full_en = BMP5_DISABLE; - ctx->int_config.fifo_thres_en = BMP5_DISABLE; - ctx->int_config.oor_press_en = BMP5_DISABLE; - - sensor->ctx = ctx; - sensor->read = bmp581_read; - - bmp5_soft_reset(&(ctx->dev)); - - // Initialize the device - result = bmp5_init(&(ctx->dev)); - if (result != BMP5_OK) { - return result; - } - - // Set odr frequency - result = bmp5_set_osr_odr_press_config(&(ctx->odr_config), &(ctx->dev)); - if (result != BMP5_OK) { - return result; - } - - result = bmp5_int_source_select(&(ctx->int_config), &(ctx->dev)); - if (result != BMP5_OK) { - return result; - } - // Enable interrupt handler - result = bmp5_configure_interrupt(BMP5_PULSED, BMP5_ACTIVE_HIGH, BMP5_INTR_PUSH_PULL, BMP5_INTR_ENABLE, &(ctx->dev)); - if (result != BMP5_OK) { - return result; - } - - result = bmp5_set_power_mode(BMP5_POWERMODE_NORMAL, &(ctx->dev)); - return result; -} - -int8_t bmp581_get_power_mode(struct bmp581_ctx *ctx, enum bmp5_powermode *powermode) // GCOVR_EXCL_FUNCTION -{ - return bmp5_get_power_mode(powermode, &(ctx->dev)); -} diff --git a/src/sensors/bmp581.cpp b/src/sensors/bmp581.cpp new file mode 100644 index 0000000..90ac392 --- /dev/null +++ b/src/sensors/bmp581.cpp @@ -0,0 +1,145 @@ +#include "bmp581.h" + +#include "hal.h" +#include "protocol.h" +#include "sensor.h" + +#include "bmp5.h" +#include "bmp5_defs.h" + +#include +#include +#include + +constexpr auto GRAVITY_ACCEL = 9.80665f; // m/s^2 +constexpr auto AIR_MOLAR_MASS = 0.0289644f; // kg/mol +constexpr auto GAS_CONSTANT = 8.31446f; // J/(mol*K) + +constexpr auto TROPOPAUSE_PRESSURE = 22630.0f; // Pa +constexpr auto STRATOSPHERE_MIDDLE_PRESSURE = 5475.0f; // Pa +constexpr auto STANDARD_SEA_LEVEL_PRESSURE = 101325.0f; // Pa + +constexpr auto STANDARD_SEA_LEVEL_TEMP = 288.15f; // K +constexpr auto STRATOSPHERE_BASE_TEMP = 216.65f; // K + +constexpr auto TROPOPAUSE_BASE_ALTITUDE = 11000.0f; // m +constexpr auto STRATOSPHERE_MIDDLE_BASE_ALTITUDE = 20000.0f; // m + +constexpr auto TROPOSPHERE_LAPSE_RATE = -0.0065f; // K/m +constexpr auto UPPER_STRATOSPHERE_LAPSE_RATE = 0.001f; // K/m + +namespace { +inline float calc_altitude_troposphere_msl(float pressure) { + constexpr auto exponent = (-GAS_CONSTANT * TROPOSPHERE_LAPSE_RATE) / + (GRAVITY_ACCEL * AIR_MOLAR_MASS); + float pressure_ratio = pressure / STANDARD_SEA_LEVEL_PRESSURE; + float power_term = pow(pressure_ratio, exponent) - 1; + + return (STANDARD_SEA_LEVEL_TEMP / TROPOSPHERE_LAPSE_RATE) * power_term; +} + +inline float calc_altitude_lower_stratosphere_msl(float pressure) { + constexpr auto scale_factor = (GAS_CONSTANT * STRATOSPHERE_BASE_TEMP) / + (GRAVITY_ACCEL * AIR_MOLAR_MASS); + float log_ratio = log(pressure / TROPOPAUSE_PRESSURE); + + return TROPOPAUSE_BASE_ALTITUDE - (scale_factor * log_ratio); +} + +inline float calc_altitude_upper_stratosphere_msl(float pressure) { + constexpr auto exponent = (-GAS_CONSTANT * UPPER_STRATOSPHERE_LAPSE_RATE) / + (GRAVITY_ACCEL * AIR_MOLAR_MASS); + float pressure_ratio = pressure / STRATOSPHERE_MIDDLE_PRESSURE; + float power_term = pow(pressure_ratio, exponent) - 1; + + return STRATOSPHERE_MIDDLE_BASE_ALTITUDE + + (STRATOSPHERE_BASE_TEMP / UPPER_STRATOSPHERE_LAPSE_RATE) * + power_term; +} + +inline float bmp581_estimate_altitude_msl(struct bmp5_sensor_data* data) { + if (data->pressure > TROPOPAUSE_PRESSURE) + return calc_altitude_troposphere_msl(data->pressure); + else if (data->pressure > STRATOSPHERE_MIDDLE_PRESSURE) + return calc_altitude_lower_stratosphere_msl(data->pressure); + else + return calc_altitude_upper_stratosphere_msl(data->pressure); +} + +BMP5_INTF_RET_TYPE bosch_read(uint8_t reg_addr, uint8_t* reg_data, + uint32_t length, void* intf_ptr) { + auto* protocol = static_cast(intf_ptr); + return protocol->read(Platform::ConstSpan(®_addr, 1), + Platform::Span(reg_data, length), + Platform::AddressSize::Byte); +} + +BMP5_INTF_RET_TYPE bosch_write(uint8_t reg_addr, const uint8_t* reg_data, + uint32_t length, void* intf_ptr) { + auto* protocol = static_cast(intf_ptr); + return protocol->write(Platform::ConstSpan(®_addr, 1), + Platform::ConstSpan(reg_data, length), + Platform::AddressSize::Byte); +} + +void bosch_delay(uint32_t period, void* intf_ptr) { + HAL_Delay(ceil((double)(period) / (1000.0))); +} +} // namespace + +namespace Platform { +bool BMP581::init() { + int8_t result = BMP5_OK; + dev.intf_ptr = &api; + dev.intf = BMP5_SPI_INTF; + dev.read = bosch_read; + dev.write = bosch_write; + dev.delay_us = bosch_delay; + + odr_config.odr = BMP5_ODR_240_HZ; + odr_config.press_en = BMP5_ENABLE; + int_config.drdy_en = BMP5_ENABLE; + int_config.fifo_full_en = BMP5_DISABLE; + int_config.fifo_thres_en = BMP5_DISABLE; + int_config.oor_press_en = BMP5_DISABLE; + + bmp5_soft_reset(&dev); + + // Initialize the device + result = bmp5_init(&dev); + if (result != BMP5_OK) + return false; + + // Set odr frequency + result = bmp5_set_osr_odr_press_config(&odr_config, &dev); + if (result != BMP5_OK) + return false; + + result = bmp5_int_source_select(&int_config, &dev); + if (result != BMP5_OK) + return false; + + // Enable interrupt handler + result = + bmp5_configure_interrupt(BMP5_PULSED, BMP5_ACTIVE_HIGH, + BMP5_INTR_PUSH_PULL, BMP5_INTR_ENABLE, &dev); + if (result != BMP5_OK) + return false; + + result = bmp5_set_power_mode(BMP5_POWERMODE_NORMAL, &dev); + if (result != BMP5_OK) + return false; + + return true; +} + +bool BMP581::read(Packet& packet) { + struct bmp5_sensor_data data; + + bmp5_get_sensor_data(&data, &odr_config, &dev); + packet.barometer_hMSL_m = bmp581_estimate_altitude_msl(&data); + packet.temperature_c = data.temperature; + packet.kf_position_m = data.pressure; + return true; +} +} // namespace Platform diff --git a/src/sensors/CD-PA1616S.c b/src/sensors/cd-pa1616s.cpp similarity index 65% rename from src/sensors/CD-PA1616S.c rename to src/sensors/cd-pa1616s.cpp index 7716ba7..3c5ca72 100644 --- a/src/sensors/CD-PA1616S.c +++ b/src/sensors/cd-pa1616s.cpp @@ -5,34 +5,39 @@ * Author: Mahir Shah */ -#include "CD-PA1616S.h" - -#include "sensor.h" -#include "defs.h" +#include "cd-pa1616s.h" +#include "hal.h" #include -#include -#include #include +#include +#include + +namespace Platform { +bool GPS::init() { + static constexpr uint8_t command[] = + "$PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\x0d\x0a"; + // Initialize GPS DMA Reception + if (!protocol.write({}, ConstSpan(command), AddressSize::None)) + return false; + if (!protocol.readDMA(Span(buffer))) + return false; + return true; +} // ParseGPSData: Single function to find and parse $GNGGA / $GPGGA // Returns 1 if successful, 0 otherwise -STATIC bool gps_read(void *context, struct packet *packet) -{ - struct gps_ctx *ctx = (struct gps_ctx*) context; - char *buffer = (char*) ctx->buffer; - +bool GPS::read(Packet& packet) { // Search manually for either "$GNGGA" or "$GPGGA" in buffer - const char *gga_start = NULL; + const uint8_t* gga_start = NULL; for (int i = 0; buffer[i] != '\0'; i++) { if (buffer[i] == '$') { // Check if we match "$GNGGA" or "$GPGGA" - if (strncmp(&buffer[i], "$GNGGA", 6) == 0 || - strncmp(&buffer[i], "$GPGGA", 6) == 0) - { - gga_start = &buffer[i]; - break; - } + if (memcmp(&buffer[i], "$GNGGA", 6) == 0 || + memcmp(&buffer[i], "$GPGGA", 6) == 0) { + gga_start = &buffer[i]; + break; + } } } @@ -44,14 +49,11 @@ STATIC bool gps_read(void *context, struct packet *packet) // Copy one line (until CR or LF) into a local buffer char line[120]; int idx = 0; - while (gga_start[idx] != '\0' && - gga_start[idx] != '\r' && - gga_start[idx] != '\n' && - idx < (int)sizeof(line) - 1) - { - line[idx] = gga_start[idx]; - idx++; - } + while (gga_start[idx] != '\0' && gga_start[idx] != '\r' && + gga_start[idx] != '\n' && idx < (int)sizeof(line) - 1) { + line[idx] = gga_start[idx]; + idx++; + } line[idx] = '\0'; // Split into fields by commas. We'll store them in fields[0..]. @@ -62,10 +64,11 @@ STATIC bool gps_read(void *context, struct packet *packet) for (int j = 0; j < idx; j++) { if (line[j] == ',') { - fields[fieldIndex][charIndex] = '\0'; // end current field + fields[fieldIndex][charIndex] = '\0'; // end current field fieldIndex++; charIndex = 0; - if (fieldIndex >= 20) break; + if (fieldIndex >= 20) + break; } else { if (charIndex < 19) { fields[fieldIndex][charIndex++] = line[j]; @@ -135,29 +138,11 @@ STATIC bool gps_read(void *context, struct packet *packet) uint8_t sats = (uint8_t)atoi(fields[7]); float alt = (fields[9][0] != '\0') ? atof(fields[9]) : 0.0f; - packet->latitude_degrees = lat; - packet->longitude_degrees = lon; - packet->gpsFixType = fix; - packet->numSatellites = sats; - packet->gps_hMSL_m = alt; - return true; -} - -bool gps_init(struct gps_ctx *ctx, struct sensor *sensor) -{ -#ifdef HAL_UART_MODULE_ENABLED - assert(ctx->handle.def.uart.handle != NULL); - - sensor->ctx = ctx; - sensor->read = gps_read; - - // Initialize GPS DMA Reception - char command[] = "$PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\x0d\x0a"; - HAL_UART_Transmit(ctx->handle.def.uart.handle, (uint8_t*) command, sizeof(command) - 1, HAL_MAX_DELAY); - HAL_UARTEx_ReceiveToIdle_DMA(ctx->handle.def.uart.handle, ctx->buffer, BUFFER_SIZE); + packet.latitude_degrees = lat; + packet.longitude_degrees = lon; + packet.gpsFixType = fix; + packet.numSatellites = sats; + packet.gps_hMSL_m = alt; return true; -#else - assert("UART module is not enabled! CD-PA161 only supports UART!"); - return false; -#endif } +} // namespace Platform diff --git a/test/sensors/test_bmp581.c b/test/sensors/test_bmp581.c deleted file mode 100644 index 2012156..0000000 --- a/test/sensors/test_bmp581.c +++ /dev/null @@ -1,116 +0,0 @@ -#include "unity.h" -#include "cmock.h" - -#include "mock_bmp5.h" - -#include "bmp581.h" -#include "sensor.h" - -bool bmp581_read(void *context, struct packet *packet); - -void set_up() {} -void tear_down() {} - -// Try using both calculators. First one seems to be for tropo only -// https://codingace.net/physics/barometric_pressure_to_altitude.html -// https://www.sensorsone.com/us-standard-atmosphere-altitude-pressure-calculator/ -// Select lapse-rate model and enter your mock pressure. For tests, -// we make the assumption of standard sea level conditions, ie -// pressure at 101325 Pa and temperature of around 15 celcius. - -void test_bmp581_read_sea() -{ - struct bmp581_ctx ctx = {0}; - struct packet packet; - - float fake_pressure = 101325.0 - 3.0; - float fake_temperature = 15.3; - struct bmp5_sensor_data fake = { - .pressure = fake_pressure, - .temperature = fake_temperature, - }; - - bmp5_get_sensor_data_ExpectAnyArgsAndReturn(true); - bmp5_get_sensor_data_ReturnMemThruPtr_sensor_data(&fake, sizeof(fake)); - - bool result = bmp581_read(&ctx, &packet); - - float expected_alt = 0.249734; - TEST_ASSERT_EQUAL_FLOAT(fake_temperature, packet.temperature_c); - TEST_ASSERT_EQUAL_FLOAT(fake_pressure, packet.kf_position_m); - TEST_ASSERT_FLOAT_WITHIN(0.001, expected_alt, packet.barometer_hMSL_m); -} - -void test_bmp581_read_tropo() -{ - struct bmp581_ctx ctx = {0}; - struct packet packet; - - float fake_pressure = 22630.0f + 41.0; - float fake_temperature = -51.23; - struct bmp5_sensor_data fake = { - .pressure = fake_pressure, - .temperature = fake_temperature, - }; - - bmp5_get_sensor_data_ExpectAnyArgsAndReturn(true); - bmp5_get_sensor_data_ReturnMemThruPtr_sensor_data(&fake, sizeof(fake)); - - bool result = bmp581_read(&ctx, &packet); - - float expected_alt = 10989.260441; - TEST_ASSERT_EQUAL_FLOAT(fake_temperature, packet.temperature_c); - TEST_ASSERT_EQUAL_FLOAT(fake_pressure, packet.kf_position_m); - // Formula gets less accurate higher up, so it is fine to be less strict - TEST_ASSERT_FLOAT_WITHIN(1, expected_alt, packet.barometer_hMSL_m); -} - - -// Lower stratosphere is isothermal as oppose to lapse (upper stratosphere) -void test_bmp581_read_strato_lower() -{ - struct bmp581_ctx ctx = {0}; - struct packet packet; - - float fake_pressure = 5475.0f + 1030.0; - float fake_temperature = -51.23; - struct bmp5_sensor_data fake = { - .pressure = fake_pressure, - .temperature = fake_temperature, - }; - - bmp5_get_sensor_data_ExpectAnyArgsAndReturn(true); - bmp5_get_sensor_data_ReturnMemThruPtr_sensor_data(&fake, sizeof(fake)); - - bool result = bmp581_read(&ctx, &packet); - - float expected_alt = 18906.71; - TEST_ASSERT_EQUAL_FLOAT(fake_temperature, packet.temperature_c); - TEST_ASSERT_EQUAL_FLOAT(fake_pressure, packet.kf_position_m); - // Formula gets less accurate higher up, so it is fine to be less strict - TEST_ASSERT_FLOAT_WITHIN(1, expected_alt, packet.barometer_hMSL_m); -} - -void test_bmp581_read_strato_upper() -{ - struct bmp581_ctx ctx = {0}; - struct packet packet; - - float fake_pressure = 5475.0f - 43.0; - float fake_temperature = -56.23; - struct bmp5_sensor_data fake = { - .pressure = fake_pressure, - .temperature = fake_temperature, - }; - - bmp5_get_sensor_data_ExpectAnyArgsAndReturn(true); - bmp5_get_sensor_data_ReturnMemThruPtr_sensor_data(&fake, sizeof(fake)); - - bool result = bmp581_read(&ctx, &packet); - - float expected_alt = 20049.8797; - TEST_ASSERT_EQUAL_FLOAT(fake_temperature, packet.temperature_c); - TEST_ASSERT_EQUAL_FLOAT(fake_pressure, packet.kf_position_m); - TEST_ASSERT_FLOAT_WITHIN(1, expected_alt, packet.barometer_hMSL_m); -} - diff --git a/test/support/stub_hal.h b/test/support/stub_hal.h deleted file mode 100644 index c732ff9..0000000 --- a/test/support/stub_hal.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef FAKE_STM32F4XX_HAL_H -#define FAKE_STM32F4XX_HAL_H - -#include - -typedef int HAL_StatusTypeDef; -#define HAL_OK 0 -#define HAL_MAX_DELAY 0 - -typedef struct { uint32_t dummy; } I2C_HandleTypeDef; -typedef struct { uint32_t dummy; } SPI_HandleTypeDef; -typedef struct { uint32_t dummy; } GPIO_TypeDef; -typedef struct { uint32_t dummy; } UART_HandleTypeDef; - -#define HAL_I2C_Mem_Read(...) (0) -#define HAL_I2C_Mem_Write(...) (0) -#define HAL_I2C_GetError(...) (0) -#define HAL_SPI_Transmit(...) (0) -#define HAL_SPI_Receive(...) (0) -#define HAL_GPIO_WritePin(...) (0) -#define HAL_GPIO_ReadPin(...) (0) -#define HAL_UART_Transmit(...) (0) -#define HAL_Delay(...) (0) - -#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..4b03e4f --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(common_drivers_tests) + +add_subdirectory(sensors) +add_subdirectory(mocks) +target_link_libraries(common_drivers_tests PRIVATE common_drivers fff GTest::gtest_main gmock) + +target_compile_options(common_drivers_tests PRIVATE --coverage) +target_link_options(common_drivers_tests PRIVATE --coverage) + +include(GoogleTest) +gtest_discover_tests(common_drivers_tests) diff --git a/tests/mocks/CMakeLists.txt b/tests/mocks/CMakeLists.txt new file mode 100644 index 0000000..66520dd --- /dev/null +++ b/tests/mocks/CMakeLists.txt @@ -0,0 +1 @@ +target_include_directories(common_drivers PUBLIC ".") diff --git a/tests/mocks/protocol_mock.h b/tests/mocks/protocol_mock.h new file mode 100644 index 0000000..0c028c6 --- /dev/null +++ b/tests/mocks/protocol_mock.h @@ -0,0 +1,17 @@ +#include "protocol.h" + +#include + +namespace Platform { +class MockProtocol : public Protocol { +public: + explicit MockProtocol(ProtocolType type) : Protocol(type) {} + MOCK_METHOD(bool, read, (ConstSpan cmd, Span buffer, AddressSize size), + (override)); + MOCK_METHOD(bool, write, + (ConstSpan cmd, ConstSpan buffer, AddressSize size), + (override)); + MOCK_METHOD(bool, readDMA, (Span buffer), (override)); + MOCK_METHOD(bool, configure, (Config config), (override)); +}; +} // namespace Platform diff --git a/tests/sensors/CMakeLists.txt b/tests/sensors/CMakeLists.txt new file mode 100644 index 0000000..f4f9808 --- /dev/null +++ b/tests/sensors/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(common_drivers_tests PRIVATE "bmp581_test.cpp") diff --git a/tests/sensors/bmp581_test.cpp b/tests/sensors/bmp581_test.cpp new file mode 100644 index 0000000..83271f6 --- /dev/null +++ b/tests/sensors/bmp581_test.cpp @@ -0,0 +1,135 @@ +#include "bmp5.h" +#include "bmp581.h" +#include "protocol_mock.h" + +#include +#include +#include + +/* Configure FFF to use std::function, which enables capturing lambdas */ +#define CUSTOM_FFF_FUNCTION_TEMPLATE(RETURN, FUNCNAME, ...) \ + std::function FUNCNAME +#include "fff.h" + +// Mock the bosch functions, since they are C, we can't touch them with gtest +// We assume bosch tests their functions already and so we only care about +// the logic in our code rather than in theirs. +// +// For some of our other drivers where we write more of the logic, we might +// want to properly mock the HAL as well, but we don't have to here. +DEFINE_FFF_GLOBALS; +extern "C" { +#include "bmp5.h" +#include "bmp5_defs.h" + +FAKE_VALUE_FUNC(int8_t, bmp5_soft_reset, struct bmp5_dev*); +FAKE_VALUE_FUNC(int8_t, bmp5_init, struct bmp5_dev*); +FAKE_VALUE_FUNC(int8_t, bmp5_set_osr_odr_press_config, + const struct bmp5_osr_odr_press_config*, struct bmp5_dev*); +FAKE_VALUE_FUNC(int8_t, bmp5_int_source_select, + const struct bmp5_int_source_select*, struct bmp5_dev*); +FAKE_VALUE_FUNC(int8_t, bmp5_configure_interrupt, enum bmp5_intr_mode, + enum bmp5_intr_polarity, enum bmp5_intr_drive, + enum bmp5_intr_en_dis, struct bmp5_dev*); +FAKE_VALUE_FUNC(int8_t, bmp5_get_sensor_data, struct bmp5_sensor_data*, + const struct bmp5_osr_odr_press_config*, struct bmp5_dev*); +FAKE_VALUE_FUNC(int8_t, bmp5_set_power_mode, enum bmp5_powermode, + struct bmp5_dev*); +} + +// This lets us return a value by reference using fff +auto mock_sensor_data(float pressure, float temp) { + return [=](struct bmp5_sensor_data* out, auto, auto) { + *out = {.pressure = pressure, .temperature = temp}; + return BMP5_OK; + }; +} + +// Try using both calculators to get atmosphere data and verify if our +// algorithms are correct. First one seems to be for tropo only. +// +// https://codingace.net/physics/barometric_pressure_to_altitude.html +// https://www.sensorsone.com/us-standard-atmosphere-altitude-pressure-calculator/ +// +// Select lapse-rate model and enter your mock pressure. For tests, +// we make the assumption of standard sea level conditions, ie +// pressure at 101325 Pa and temperature of around 15 celcius. +namespace Platform { +class BMP581Test : public testing::Test { + protected: + MockProtocol protocol = MockProtocol(ProtocolType::SPI); + + BMP581Test() {} + void SetUp() override { + FFF_RESET_HISTORY(); + RESET_FAKE(bmp5_get_sensor_data); + } +}; + +TEST_F(BMP581Test, ShouldReadSeaLevelCorrectly) { + float fake_pressure = 101325.0 - 3.0; + float fake_temperature = 15.3; + bmp5_get_sensor_data_fake.custom_fake = + mock_sensor_data(fake_pressure, fake_temperature); + + auto packet = Packet(); + auto sensor = BMP581(protocol); + sensor.read(packet); + + float expected_alt = 0.249734; + EXPECT_FLOAT_EQ(fake_temperature, packet.temperature_c); + EXPECT_FLOAT_EQ(fake_pressure, packet.kf_position_m); + EXPECT_NEAR(expected_alt, packet.barometer_hMSL_m, 0.01); +} + +// Formula gets less accurate higher up, so it is fine to be less strict +// with regards the the altitude check +TEST_F(BMP581Test, ShouldReadTroposphereCorrectly) { + float fake_pressure = 22630.0f + 41.0; + float fake_temperature = -51.23; + bmp5_get_sensor_data_fake.custom_fake = + mock_sensor_data(fake_pressure, fake_temperature); + + auto packet = Packet(); + auto sensor = BMP581(protocol); + sensor.read(packet); + + float expected_alt = 10989.260441; + EXPECT_FLOAT_EQ(fake_temperature, packet.temperature_c); + EXPECT_FLOAT_EQ(fake_pressure, packet.kf_position_m); + EXPECT_NEAR(expected_alt, packet.barometer_hMSL_m, 1); +} + +// Lower stratosphere is isothermal as oppose to lapse (upper stratosphere) +TEST_F(BMP581Test, ShouldReadLowerStratosphereCorrectly) { + float fake_pressure = 5475.0f + 1030.0; + float fake_temperature = -51.23; + bmp5_get_sensor_data_fake.custom_fake = + mock_sensor_data(fake_pressure, fake_temperature); + + auto packet = Packet(); + auto sensor = BMP581(protocol); + sensor.read(packet); + + float expected_alt = 18906.71; + EXPECT_FLOAT_EQ(fake_temperature, packet.temperature_c); + EXPECT_FLOAT_EQ(fake_pressure, packet.kf_position_m); + EXPECT_NEAR(expected_alt, packet.barometer_hMSL_m, 1); +} + +TEST_F(BMP581Test, ShouldReadUpperStratosphereCorrectly) { + float fake_pressure = 5475.0f - 43.0; + float fake_temperature = -56.23; + bmp5_get_sensor_data_fake.custom_fake = + mock_sensor_data(fake_pressure, fake_temperature); + + auto packet = Packet(); + auto sensor = BMP581(protocol); + sensor.read(packet); + + float expected_alt = 20049.8797; + EXPECT_FLOAT_EQ(fake_temperature, packet.temperature_c); + EXPECT_FLOAT_EQ(fake_pressure, packet.kf_position_m); + EXPECT_NEAR(expected_alt, packet.barometer_hMSL_m, 1); +} +} // namespace Platform