diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h index b576662965726dcf83caefcb3287e87c791da319..930eb8768950ac56c043387771ed4106c21af9e5 100644 --- a/epicardium/epicardium.h +++ b/epicardium/epicardium.h @@ -3,6 +3,8 @@ #include <stdint.h> #include <errno.h> +#include <stdbool.h> +#include <stddef.h> #ifndef __SPHINX_DOC /* Some headers are not recognized by hawkmoth for some odd reason */ @@ -111,6 +113,9 @@ typedef _Bool bool; #define API_BME680_DEINIT 0xD1 #define API_BME680_GET_DATA 0xD2 +#define API_BHI160_ENABLE 0xe0 +#define API_BHI160_DISABLE 0xe1 + /* clang-format on */ typedef uint32_t api_int_id_t; @@ -859,6 +864,160 @@ API(API_PERSONAL_STATE_IS_PERSISTENT, int epic_personal_state_is_persistent()); */ API(API_STREAM_READ, int epic_stream_read(int sd, void *buf, size_t count)); +/** + * BHI160 Sensor Fusion + * ==================== + * card10 has a BHI160 onboard which is used as an IMU. BHI160 exposes a few + * different sensors which can be accessed using Epicardium API. + * + * **Example**: + * + * .. code-block:: cpp + * + * #include "epicardium.h" + * + * // Configure a sensor & enable it + * struct bhi160_sensor_config cfg = {0}; + * cfg.sample_buffer_len = 40; + * cfg.sample_rate = 4; // Hz + * cfg.dynamic_range = 2; // g + * + * int sd = epic_bhi160_enable_sensor(BHI160_ACCELEROMETER, &cfg); + * + * // Read sensor data + * while (1) { + * struct bhi160_data_vector buf[10]; + * + * int n = epic_stream_read(sd, buf, sizeof(buf)); + * + * for (int i = 0; i < n; i++) { + * printf("X: %6d Y: %6d Z: %6d\n", + * buf[i].x, + * buf[i].y, + * buf[i].z); + * } + * } + * + * // Disable the sensor + * epic_bhi160_disable_sensor(BHI160_ACCELEROMETER); + */ + +/** + * BHI160 Sensor Types + * ------------------- + */ + +/** + * BHI160 virtual sensor type. + */ +enum bhi160_sensor_type { + /** + * Accelerometer + * + * - Data type: :c:type:`bhi160_data_vector` + * - Dynamic range: g's (1x Earth Gravity, ~9.81m*s^-2) + */ + BHI160_ACCELEROMETER = 0, + /** Magnetometer (**Unimplemented**) */ + BHI160_MAGNETOMETER = 1, + /** Orientation (**Unimplemented**) */ + BHI160_ORIENTATION = 2, + /** Gyroscope (**Unimplemented**) */ + BHI160_GYROSCOPE = 3, + /** Gravity (**Unimplemented**) */ + BHI160_GRAVITY = 4, + /** Linear acceleration (**Unimplemented**) */ + BHI160_LINEAR_ACCELERATION = 5, + /** Rotation vector (**Unimplemented**) */ + BHI160_ROTATION_VECTOR = 6, + /** Uncalibrated magnetometer (**Unimplemented**) */ + BHI160_UNCALIBRATED_MAGNETOMETER = 7, + /** Game rotation vector (whatever that is supposed to be) */ + BHI160_GAME_ROTATION_VECTOR = 8, + /** Uncalibrated gyroscrope (**Unimplemented**) */ + BHI160_UNCALIBRATED_GYROSCOPE = 9, + /** Geomagnetic rotation vector (**Unimplemented**) */ + BHI160_GEOMAGNETIC_ROTATION_VECTOR = 10, +}; + +/** + * BHI160 Sensor Data Types + * ------------------------ + */ + +/** + * Vector Data. The scaling of these values is dependent on the chosen dynamic + * range. See the individual sensor's documentation for details. + */ +struct bhi160_data_vector { + /** X */ + int16_t x; + /** Y */ + int16_t y; + /** Z */ + int16_t z; +}; + +/** + * BHI160 API + * ---------- + */ + +/** + * Configuration for a BHI160 sensor. + * + * This struct is used when enabling a sensor using + * :c:func:`epic_bhi160_enable_sensor`. + */ +struct bhi160_sensor_config { + /** + * Number of samples Epicardium should keep for this sensor. Do not set + * this number too high as the sample buffer will eat RAM. + */ + size_t sample_buffer_len; + /** + * Sample rate for the sensor in Hz. Maximum data rate is limited + * to 200 Hz for all sensors though some might be limited at a lower + * rate. + */ + uint16_t sample_rate; + /** + * Dynamic range. Interpretation of this value depends on + * the sensor type. Please refer to the specific sensor in + * :c:type:`bhi160_sensor_type` for details. + */ + uint16_t dynamic_range; + /** Always zero. Reserved for future parameters. */ + uint8_t _padding[8]; +}; + +/** + * Enable a BHI160 virtual sensor. Calling this funciton will instruct the + * BHI160 to collect data for this specific virtual sensor. You can then + * retrieve the samples using :c:func:`epic_stream_read`. + * + * :param bhi160_sensor_type sensor_type: Which sensor to enable. + * :param bhi160_sensor_config* config: Configuration for this sensor. + * :returns: A sensor descriptor which can be used with + * :c:func:`epic_stream_read` or a negative error value: + * + * - ``-EBUSY``: The BHI160 driver is currently busy with other tasks and + * could not be acquired for enabling a sensor. + */ +API(API_BHI160_ENABLE, int epic_bhi160_enable_sensor( + enum bhi160_sensor_type sensor_type, + struct bhi160_sensor_config *config +)); + +/** + * Disable a BHI160 sensor. + * + * :param bhi160_sensor_type sensor_type: Which sensor to disable. + */ +API(API_BHI160_DISABLE, int epic_bhi160_disable_sensor( + enum bhi160_sensor_type sensor_type +)); + /** * Vibration Motor * =============== diff --git a/epicardium/main.c b/epicardium/main.c index 5e38e0bffe74c3dca93818bc40a50f5e1407fb09..c1a9b08b60c45b3a6b8ab4f290a3dc8cf5a001d4 100644 --- a/epicardium/main.c +++ b/epicardium/main.c @@ -50,6 +50,18 @@ int main(void) abort(); } + /* BHI160 */ + if (xTaskCreate( + vBhi160Task, + (const char *)"BHI160 Driver", + configMINIMAL_STACK_SIZE * 2, + NULL, + tskIDLE_PRIORITY + 1, + NULL) != pdPASS) { + printf("Failed to create bhi160 dispatcher task!\n"); + abort(); + } + /* API */ if (xTaskCreate( vApiDispatcher, diff --git a/epicardium/meson.build b/epicardium/meson.build index 220f0bae63c7f84e60aee6002760c8271c7587de..ea28d3664563816c5fa8a4294e9fb3ba9308622c 100644 --- a/epicardium/meson.build +++ b/epicardium/meson.build @@ -80,7 +80,7 @@ elf = executable( l0der_sources, ble_sources, version_hdr, - dependencies: [libcard10, max32665_startup_core0, maxusb, libff13, ble], + dependencies: [libcard10, max32665_startup_core0, maxusb, libff13, ble, bhy1], link_with: [api_dispatcher_lib, freertos], link_whole: [max32665_startup_core0_lib, board_card10_lib, newlib_heap_lib], include_directories: [freertos_includes], diff --git a/epicardium/modules/bhi.c b/epicardium/modules/bhi.c new file mode 100644 index 0000000000000000000000000000000000000000..5e05e67236250323473771fbba2615560b53c728 --- /dev/null +++ b/epicardium/modules/bhi.c @@ -0,0 +1,379 @@ +#include <stdio.h> +#include <string.h> + +#include "gpio.h" +#include "bhy_uc_driver.h" +#include "bhy.h" +#include "pmic.h" + +#include "FreeRTOS.h" +#include "task.h" +#include "semphr.h" +#include "queue.h" + +#include "epicardium.h" +#include "modules/log.h" +#include "modules/modules.h" +#include "modules/stream.h" + +/* Ticks to wait when trying to acquire lock */ +#define LOCK_WAIT pdMS_TO_TICKS(BHI160_MUTEX_WAIT_MS) + +/* BHI160 Firmware Blob. Contents are defined in libcard10. */ +extern uint8_t bhy1_fw[]; + +/* Interrupt Pin */ +static const gpio_cfg_t bhi160_interrupt_pin = { + PORT_0, PIN_13, GPIO_FUNC_IN, GPIO_PAD_PULL_UP +}; + +/* Axis remapping matrices */ +static int8_t bhi160_mapping_matrix[3 * 3] = { 0, -1, 0, 1, 0, 0, 0, 0, 1 }; +static int8_t bmm150_mapping_matrix[3 * 3] = { -1, 0, 0, 0, 1, 0, 0, 0, -1 }; + +/* + * From the official docs: + * + * The sic matrix should be calculated for customer platform by logging + * uncalibrated magnetometer data. The sic matrix here is only an example + * array (identity matrix). Customer should generate their own matrix. This + * affects magnetometer fusion performance. + * + * TODO: Get data for card10 + */ +/* clang-format off */ +static float bhi160_sic_array[3 * 3] = { 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0 }; +/* clang-format on */ + +/* BHI160 Fifo */ +static uint8_t bhi160_fifo[BHI160_FIFO_SIZE]; +static size_t start_index = 0; + +/* BHI160 Task ID */ +static TaskHandle_t bhi160_task_id = NULL; + +/* BHI160 Mutex */ +static StaticSemaphore_t bhi160_mutex_data; +static SemaphoreHandle_t bhi160_mutex = NULL; + +/* Streams */ +static struct stream_info bhi160_streams[10]; + +/* -- Utilities -------------------------------------------------------- {{{ */ +/* + * Retrieve the data size for a sensor. This value is needed for the creation + * of the sensor's sample queue. + */ +static size_t bhi160_lookup_data_size(enum bhi160_sensor_type type) +{ + switch (type) { + case BHI160_ACCELEROMETER: + case BHI160_MAGNETOMETER: + case BHI160_ORIENTATION: + return sizeof(struct bhi160_data_vector); + default: + return 0; + } +} + +/* + * Map a sensor type to the virtual sensor ID used by BHy1. + */ +static bhy_virtual_sensor_t bhi160_lookup_vs_id(enum bhi160_sensor_type type) +{ + switch (type) { + case BHI160_ACCELEROMETER: + return VS_ID_ACCELEROMETER; + default: + return -1; + } +} + +/* + * Map a sensor type to its stream descriptor. + */ +static int bhi160_lookup_sd(enum bhi160_sensor_type type) +{ + switch (type) { + case BHI160_ACCELEROMETER: + return SD_BHI160_ACCELEROMETER; + default: + return -1; + } +} +/* }}} */ + +/* -- API -------------------------------------------------------------- {{{ */ +int epic_bhi160_enable_sensor( + enum bhi160_sensor_type sensor_type, + struct bhi160_sensor_config *config +) { + bhy_virtual_sensor_t vs_id = bhi160_lookup_vs_id(sensor_type); + if (vs_id < 0) { + return -ENODEV; + } + + if (xSemaphoreTake(bhi160_mutex, LOCK_WAIT) == pdTRUE) { + struct stream_info *stream = &bhi160_streams[sensor_type]; + stream->item_size = bhi160_lookup_data_size(sensor_type); + /* TODO: Sanity check length */ + stream->queue = xQueueCreate( + config->sample_buffer_len, stream->item_size + ); + if (stream->queue == NULL) { + xSemaphoreGive(bhi160_mutex); + return -ENOMEM; + } + + stream_register(bhi160_lookup_sd(sensor_type), stream); + + bhy_enable_virtual_sensor( + vs_id, + VS_WAKEUP, + config->sample_rate, + 0, + VS_FLUSH_NONE, + 0, + config->dynamic_range /* dynamic range is sensor dependent */ + ); + xSemaphoreGive(bhi160_mutex); + } else { + return -EBUSY; + } + + return 0; +} + +int epic_bhi160_disable_sensor(enum bhi160_sensor_type sensor_type) +{ + bhy_virtual_sensor_t vs_id = bhi160_lookup_vs_id(sensor_type); + if (vs_id < 0) { + return -ENODEV; + } + + if (xSemaphoreTake(bhi160_mutex, LOCK_WAIT) == pdTRUE) { + struct stream_info *stream = &bhi160_streams[sensor_type]; + stream_deregister(bhi160_lookup_sd(sensor_type), stream); + vQueueDelete(stream->queue); + stream->queue = NULL; + + bhy_disable_virtual_sensor(vs_id, VS_WAKEUP); + xSemaphoreGive(bhi160_mutex); + } else { + return -EBUSY; + } + + return 0; +} +/* }}} */ + +/* -- Driver ----------------------------------------------------------- {{{ */ +/* + * Handle a single packet from the FIFO. For most sensors this means pushing + * the sample into its sample queue. + */ +static void +bhi160_handle_packet(bhy_data_type_t data_type, bhy_data_generic_t *sensor_data) +{ + uint8_t sensor_id = sensor_data->data_vector.sensor_id; + struct bhi160_data_vector data_vector; + /* + * Timestamp of the next samples, counting at 32 kHz. + * Currently unused. + */ + static uint32_t timestamp = 0; + + switch (sensor_id) { + case VS_ID_TIMESTAMP_MSW: + case VS_ID_TIMESTAMP_MSW_WAKEUP: + MXC_ASSERT(data_type == BHY_DATA_TYPE_SCALAR_U16); + timestamp = sensor_data->data_scalar_u16.data << 16; + break; + case VS_ID_TIMESTAMP_LSW: + case VS_ID_TIMESTAMP_LSW_WAKEUP: + MXC_ASSERT(data_type == BHY_DATA_TYPE_SCALAR_U16); + timestamp = (timestamp & 0xFFFF0000) | + sensor_data->data_scalar_u16.data; + break; + case VS_ID_ACCELEROMETER: + case VS_ID_ACCELEROMETER_WAKEUP: + MXC_ASSERT(data_type == BHY_DATA_TYPE_VECTOR); + if (bhi160_streams[BHI160_ACCELEROMETER].queue == NULL) { + break; + } + data_vector.x = sensor_data->data_vector.x; + data_vector.y = sensor_data->data_vector.y; + data_vector.z = sensor_data->data_vector.z; + xQueueSend( + bhi160_streams[BHI160_ACCELEROMETER].queue, + &data_vector, + BHI160_MUTEX_WAIT_MS + ); + break; + default: + break; + } +} + +/* + * Fetch all data available from BHI160's FIFO buffer and handle all packets + * contained in it. + */ +static int bhi160_fetch_fifo(void) +{ + /* + * Warning: The code from the BHy1 docs has some issues. This + * implementation looks similar, but has a few important differences. + * You'll probably be best of leaving it as it is ... + */ + + int ret = BHY_SUCCESS; + /* Number of bytes left in BHI160's FIFO buffer */ + uint16_t bytes_left_in_fifo = 1; + + if (xSemaphoreTake(bhi160_mutex, LOCK_WAIT) != pdTRUE) { + return -EBUSY; + } + + while (bytes_left_in_fifo) { + /* Fill local FIFO buffer with as many bytes as possible */ + uint16_t bytes_read; + bhy_read_fifo( + &bhi160_fifo[start_index], + BHI160_FIFO_SIZE - start_index, + &bytes_read, + &bytes_left_in_fifo + ); + + /* Add the bytes left from the last transfer on top */ + bytes_read += start_index; + + /* Handle all full packets received in this transfer */ + uint8_t *fifo_ptr = bhi160_fifo; + uint16_t bytes_left = bytes_read; + while (ret == BHY_SUCCESS && + bytes_left > sizeof(bhy_data_generic_t)) { + /* + * TODO: sizeof(bhy_data_generic_t) is probably + * incorrect and makes some measurements arrive late. + */ + bhy_data_generic_t sensor_data; + bhy_data_type_t data_type; + ret = bhy_parse_next_fifo_packet( + &fifo_ptr, + &bytes_left, + &sensor_data, + &data_type + ); + + if (ret == BHY_SUCCESS) { + bhi160_handle_packet(data_type, &sensor_data); + } + } + + /* Shift the remaining bytes to the beginning */ + for (int i = 0; i < bytes_left; i++) { + bhi160_fifo[i] = + bhi160_fifo[bytes_read - bytes_left + i]; + } + start_index = bytes_left; + } + + xSemaphoreGive(bhi160_mutex); + return 0; +} + +/* + * Callback for the BHI160 interrupt pin. This callback is called from the + * SDK's GPIO interrupt driver, in interrupt context. + */ +static void bhi160_interrupt_callback(void *_) +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + if (bhi160_task_id != NULL) { + vTaskNotifyGiveFromISR( + bhi160_task_id, &xHigherPriorityTaskWoken + ); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); + } +} +/* }}} */ + +void vBhi160Task(void *pvParameters) +{ + int ret; + + bhi160_task_id = xTaskGetCurrentTaskHandle(); + bhi160_mutex = xSemaphoreCreateMutexStatic(&bhi160_mutex_data); + + /* Take Mutex during initialization, just in case */ + if (xSemaphoreTake(bhi160_mutex, 0) != pdTRUE) { + LOG_CRIT("bhi160", "Failed to acquire BHI160 mutex!"); + vTaskDelay(portMAX_DELAY); + } + + memset(bhi160_streams, 0x00, sizeof(bhi160_streams)); + + /* Install interrupt callback */ + GPIO_Config(&bhi160_interrupt_pin); + GPIO_RegisterCallback( + &bhi160_interrupt_pin, bhi160_interrupt_callback, NULL + ); + GPIO_IntConfig(&bhi160_interrupt_pin, GPIO_INT_EDGE, GPIO_INT_RISING); + GPIO_IntEnable(&bhi160_interrupt_pin); + NVIC_SetPriority( + (IRQn_Type)MXC_GPIO_GET_IRQ(bhi160_interrupt_pin.port), 2 + ); + NVIC_EnableIRQ((IRQn_Type)MXC_GPIO_GET_IRQ(bhi160_interrupt_pin.port)); + + /* Upload firmware */ + ret = bhy_driver_init(bhy1_fw); + if (ret) { + LOG_CRIT("bhi160", "BHy1 init failed!"); + vTaskDelay(portMAX_DELAY); + } + + /* Wait for first two interrupts */ + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)); + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)); + + /* Remap axes to match card10 layout */ + bhy_mapping_matrix_set( + PHYSICAL_SENSOR_INDEX_ACC, bhi160_mapping_matrix + ); + bhy_mapping_matrix_set( + PHYSICAL_SENSOR_INDEX_MAG, bmm150_mapping_matrix + ); + bhy_mapping_matrix_set( + PHYSICAL_SENSOR_INDEX_GYRO, bhi160_mapping_matrix + ); + + /* Set "SIC" matrix. TODO: Find out what this is about */ + bhy_set_sic_matrix(bhi160_sic_array); + + xSemaphoreGive(bhi160_mutex); + + /* ----------------------------------------- */ + + while (1) { + int ret = bhi160_fetch_fifo(); + if (ret == -EBUSY) { + LOG_WARN("bhi160", "Could not acquire mutex for FIFO?"); + continue; + } else if (ret < 0) { + LOG_ERR("bhi160", "Unknown error: %d", -ret); + } + + /* + * Wait for interrupt. After two seconds, fetch FIFO anyway in + * case there are any diagnostics or errors. + * + * In the future, reads using epic_stream_read() might also + * trigger a FIFO fetch, from outside this task. + */ + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2000)); + } +} diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build index 73975adceee81bb83894ecdd15784112ce1f3221..736b3efec72ee9462833e574c8b1c7a5ae32ea01 100644 --- a/epicardium/modules/meson.build +++ b/epicardium/modules/meson.build @@ -1,4 +1,5 @@ module_sources = files( + 'bhi.c', 'bme680.c', 'buttons.c', 'dispatcher.c', diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h index 8760e691567f4e89e989cfe48d43d4c0d05cb77b..d6d045f2e8cc4e45a59e7a9a10495fa0373f9333 100644 --- a/epicardium/modules/modules.h +++ b/epicardium/modules/modules.h @@ -84,4 +84,10 @@ int hwlock_release(enum hwlock_periph p); /* Forces an unlock of the display. Only to be used in Epicardium */ void disp_forcelock(); +/* ---------- BHI160 ------------------------------------------------------- */ +#define BHI160_FIFO_SIZE 128 +#define BHI160_MUTEX_WAIT_MS 50 +void vBhi160Task(void *pvParameters); + + #endif /* MODULES_H */ diff --git a/epicardium/modules/stream.h b/epicardium/modules/stream.h index 9d137a20e6fdf5b84e9e568b5e06a294069d1606..8b121bfcb4fbc081b6e46865f000219cbdb7e981 100644 --- a/epicardium/modules/stream.h +++ b/epicardium/modules/stream.h @@ -25,6 +25,8 @@ typedef unsigned int size_t; * Please keep IDs in sequential order. */ enum stream_descriptor { + /** BHI160 Accelerometer */ + SD_BHI160_ACCELEROMETER, /** Highest descriptor must always be ``SD_MAX``. */ SD_MAX, };