From d92965df5a93bf4a5b0e62a9eedd52f9eeaea2e9 Mon Sep 17 00:00:00 2001
From: "Jakob (XDjackieXD) Riepler" <jakob.riepler@chaosfield.at>
Date: Fri, 31 Jan 2020 22:08:59 +0100
Subject: [PATCH] feat(max86150): MAX86150 Epicardium API

Epicardium API for max86150 aligned with latest state of sensor
development on card10.

Co-Authored-by: Arist <aristkojevnikov@gmail.com>
---
 epicardium/epicardium.h        | 101 ++++++++++++--
 epicardium/main.c              |  10 ++
 epicardium/modules/hardware.c  |   7 +
 epicardium/modules/max86150.c  | 236 +++++++++++++++++++++++++++++++++
 epicardium/modules/meson.build |   1 +
 epicardium/modules/modules.h   |   6 +
 epicardium/modules/sleep.c     |   6 +-
 epicardium/modules/stream.h    |   1 +
 8 files changed, 354 insertions(+), 14 deletions(-)
 create mode 100644 epicardium/modules/max86150.c

diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 01709785..84071d04 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -133,9 +133,8 @@ typedef _Bool bool;
 #define API_MAX30001_ENABLE        0xf0
 #define API_MAX30001_DISABLE       0xf1
 
-#define API_MAX86150_INIT		0x0100
-#define API_MAX86150_GET_DATA		0x0101
-#define API_MAX86150_SET_LED_AMPLITUDE	0x0102
+#define API_MAX86150_ENABLE        0x0100
+#define API_MAX86150_DISABLE       0x0101
 
 #define API_USB_SHUTDOWN           0x110
 #define API_USB_STORAGE            0x111
@@ -194,18 +193,20 @@ API(API_INTERRUPT_DISABLE, int epic_interrupt_disable(api_int_id_t int_id));
 /** RTC Alarm interrupt.  See :c:func:`epic_isr_rtc_alarm`. */
 #define EPIC_INT_RTC_ALARM              3
 /** BHI160 Accelerometer.  See :c:func:`epic_isr_bhi160_accelerometer`. */
-#define EPIC_INT_BHI160_ACCELEROMETER   4
+#define EPIC_INT_BHI160_ACCELEROMETER	4
 /** BHI160 Orientation Sensor.  See :c:func:`epic_isr_bhi160_orientation`. */
-#define EPIC_INT_BHI160_ORIENTATION     5
+#define EPIC_INT_BHI160_ORIENTATION	5
 /** BHI160 Gyroscope.  See :c:func:`epic_isr_bhi160_gyroscope`. */
-#define EPIC_INT_BHI160_GYROSCOPE       6
+#define EPIC_INT_BHI160_GYROSCOPE	6
 /** MAX30001 ECG.  See :c:func:`epic_isr_max30001_ecg`. */
-#define EPIC_INT_MAX30001_ECG           7
+#define EPIC_INT_MAX30001_ECG		7
 /** BHI160 Magnetometer.  See :c:func:`epic_isr_bhi160_magnetometer`. */
 #define EPIC_INT_BHI160_MAGNETOMETER    8
+/** MAX86150 ECG and PPG sensor.  See :c:func:`epic_isr_max86150`. */
+#define EPIC_INT_MAX86150		9
 
 /* Number of defined interrupts. */
-#define EPIC_INT_NUM                    9
+#define EPIC_INT_NUM                    10
 /* clang-format on */
 
 /*
@@ -871,6 +872,84 @@ API(API_BME680_GET_DATA, int epic_bme680_read_sensors(
 	struct bme680_sensor_data *data
 ));
 
+/**
+ * MAX86150
+ * ======
+ */
+
+/**
+ * Configuration for a MAX86150 sensor.
+ *
+ * This struct is used when enabling a sensor using
+ * :c:func:`epic_max86150_enable_sensor`.
+ */
+struct max86150_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 PPG from the sensor in Hz.  Maximum data rate is limited
+     * to 200 Hz for all sensors though some might be limited at a lower
+     * rate.
+     *
+     * Possible values are 10, 20, 50, 84, 100, 200.
+     */
+    uint16_t ppg_sample_rate;
+};
+
+/**
+ * MAX86150 Sensor Data
+ */
+struct max86150_sensor_data {
+	/** Red LED data */
+	uint32_t red;
+	/** IR LED data */
+	uint32_t ir;
+	/** ECG data */
+	int32_t ecg;
+};
+
+/**
+ * Enable a MAX86150 PPG and ECG sensor.
+ * 
+ * Calling this function will instruct the MAX86150 to collect a 
+ * data from the sensor.  You can then retrieve the samples using 
+ * :c:func:`epic_stream_read`.
+ *
+ * :param max86150_sensor_config* config: Configuration for this sensor.
+ * :param size_t config_size: Size of ``config``.
+ * :returns: A sensor descriptor which can be used with
+ *    :c:func:`epic_stream_read` or a negative error value:
+ *
+ *    - ``-ENOMEM``:  The MAX86150 driver failed to create a stream queue.
+ *    - ``-ENODEV``:  The MAX86150 driver failed due to physical connectivity problem
+ *      (broken wire, unpowered, etc).
+ *    - ``-EINVAL``:  config->ppg_sample_rate is not one of 10, 20, 50, 84, 100, 200
+ *      or config_size is not size of config.
+ *
+ * .. versionadded:: 1.13
+ */
+API(API_MAX86150_ENABLE, int epic_max86150_enable_sensor(struct max86150_sensor_config *config, size_t config_size));
+
+/**
+ * Disable the MAX86150 sensor.
+ *
+ * :returns: 0 in case of success or forward negative error value from stream_deregister.
+ *
+ * .. versionadded:: 1.13
+ */
+API(API_MAX86150_DISABLE, int epic_max86150_disable_sensor());
+
+/**
+ * **Interrupt Service Routine** for :c:data:`EPIC_INT_MAX86150`
+ *
+ * :c:func:`epic_isr_max86150` is called whenever the MAX86150
+ * PPG sensor has new data available.
+ */
+API_ISR(EPIC_INT_MAX86150, epic_isr_max86150);
+
 /**
  * Personal State
  * ==============
@@ -1139,7 +1218,7 @@ struct bhi160_sensor_config {
 };
 
 /**
- * Enable a BHI160 virtual sensor.  Calling this funciton will instruct the
+ * Enable a BHI160 virtual sensor.  Calling this function will instruct the
  * BHI160 to collect data for this specific virtual sensor.  You can then
  * retrieve the samples using :c:func:`epic_stream_read`.
  *
@@ -1854,9 +1933,9 @@ struct max30001_sensor_config {
 };
 
 /**
- * Enable a MAX30001 ecg sensor.
+ * Enable a MAX30001 ECG sensor.
  *
- * Calling this funciton will instruct the MAX30001 to collect data for this
+ * Calling this function will instruct the MAX30001 to collect data for this
  * sensor.  You can then retrieve the samples using :c:func:`epic_stream_read`.
  *
  * :param max30001_sensor_config* config: Configuration for this sensor.
diff --git a/epicardium/main.c b/epicardium/main.c
index 831a3e12..c5e2768c 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -113,6 +113,16 @@ int main(void)
 		    NULL) != pdPASS) {
 		panic("Failed to create %s task!", "MAX30001");
 	}
+	/* MAX86150 */
+	if (xTaskCreate(
+		    vMAX86150Task,
+		    (const char *)"MAX86150 Driver",
+		    configMINIMAL_STACK_SIZE * 2,
+		    NULL,
+		    tskIDLE_PRIORITY + 1,
+		    NULL) != pdPASS) {
+		panic("Failed to create %s task!", "MAX86150");
+	}
 	/* API */
 	if (xTaskCreate(
 		    vApiDispatcher,
diff --git a/epicardium/modules/hardware.c b/epicardium/modules/hardware.c
index afc44f64..35815433 100644
--- a/epicardium/modules/hardware.c
+++ b/epicardium/modules/hardware.c
@@ -194,6 +194,11 @@ int hardware_early_init(void)
 	 */
 	max30001_mutex_init();
 
+	/*
+	 * max86150 mutex init
+	 */
+	max86150_mutex_init();
+
 	/* Allow user space to trigger interrupts.
 	 * Used for BLE, not sure if needed. */
 	SCB->CCR |= SCB_CCR_USERSETMPEND_Msk;
@@ -283,5 +288,7 @@ int hardware_reset(void)
 
 	epic_max30001_disable_sensor();
 
+	epic_max86150_disable_sensor();
+
 	return 0;
 }
diff --git a/epicardium/modules/max86150.c b/epicardium/modules/max86150.c
new file mode 100644
index 00000000..428d0caf
--- /dev/null
+++ b/epicardium/modules/max86150.c
@@ -0,0 +1,236 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include "max86150.h"
+#include "epicardium.h"
+#include "modules.h"
+#include "modules/log.h"
+#include "modules/stream.h"
+#include "gpio.h"
+#include "pmic.h"
+
+#include "FreeRTOS.h"
+#include "task.h"
+#include "queue.h"
+
+#include "api/interrupt-sender.h"
+#include "modules/modules.h"
+
+static const gpio_cfg_t max86150_interrupt_pin = {
+	PORT_1, PIN_13, GPIO_FUNC_IN, GPIO_PAD_PULL_UP
+};
+
+/* MAX86150 Task ID */
+static TaskHandle_t max86150_task_id = NULL;
+
+/* MAX86150 Mutex */
+static struct mutex max86150_mutex = { 0 };
+
+/* Stream */
+static struct stream_info max86150_stream;
+
+/* Active */
+static bool max86150_sensor_active = false;
+
+int epic_max86150_enable_sensor(
+	struct max86150_sensor_config *config, size_t config_size
+) {
+	int result = 0;
+
+	if (sizeof(struct max86150_sensor_config) != config_size) {
+		return -EINVAL;
+	}
+
+	mutex_lock(&max86150_mutex);
+	hwlock_acquire(HWLOCK_I2C);
+
+	struct stream_info *stream = &max86150_stream;
+	stream->item_size          = sizeof(struct max86150_sensor_data);
+	stream->queue =
+		xQueueCreate(config->sample_buffer_len, stream->item_size);
+	if (stream->queue == NULL) {
+		result = -ENOMEM;
+		goto out_free;
+	}
+
+	uint8_t ppg_sample_rate;
+
+	if (config->ppg_sample_rate == 10) {
+		ppg_sample_rate = MAX86150_PPG_SAMPLERATE_10;
+	} else if (config->ppg_sample_rate == 20) {
+		ppg_sample_rate = MAX86150_PPG_SAMPLERATE_20;
+	} else if (config->ppg_sample_rate == 50) {
+		ppg_sample_rate = MAX86150_PPG_SAMPLERATE_50;
+	} else if (config->ppg_sample_rate == 84) {
+		ppg_sample_rate = MAX86150_PPG_SAMPLERATE_84;
+	} else if (config->ppg_sample_rate == 100) {
+		ppg_sample_rate = MAX86150_PPG_SAMPLERATE_100;
+	} else if (config->ppg_sample_rate == 200) {
+		ppg_sample_rate = MAX86150_PPG_SAMPLERATE_200;
+	} else {
+		result = -EINVAL;
+		goto out_free;
+	}
+
+	result = stream_register(SD_MAX86150, stream);
+	if (result < 0) {
+		vQueueDelete(stream->queue);
+		goto out_free;
+	}
+
+	bool begin_result = max86150_begin();
+	if (!begin_result) {
+		result = -ENODEV;
+		vQueueDelete(stream->queue);
+		goto out_free;
+	}
+
+	max86150_setup(ppg_sample_rate);
+	max86150_get_int1();
+	max86150_get_int2();
+
+	max86150_sensor_active = true;
+	result                 = SD_MAX86150;
+
+out_free:
+	hwlock_release(HWLOCK_I2C);
+	mutex_unlock(&max86150_mutex);
+	return result;
+}
+
+int epic_max86150_disable_sensor(void)
+{
+	int result = 0;
+
+	mutex_lock(&max86150_mutex);
+	hwlock_acquire(HWLOCK_I2C);
+
+	struct stream_info *stream = &max86150_stream;
+	result                     = stream_deregister(SD_MAX86150, stream);
+	if (result < 0) {
+		goto out_free;
+	}
+
+	vQueueDelete(stream->queue);
+	stream->queue = NULL;
+
+	// disable max86150 leds
+	max86150_set_led_red_amplitude(0);
+	max86150_set_led_ir_amplitude(0);
+
+	max86150_sensor_active = false;
+
+	result = 0;
+out_free:
+	hwlock_release(HWLOCK_I2C);
+	mutex_unlock(&max86150_mutex);
+	return result;
+}
+
+static int max86150_handle_sample(struct max86150_sensor_data *data)
+{
+	//LOG_INFO("max86150", "Sample! %ld, %ld, %ld", data->red, data->ir, data->ecg);
+
+	if (max86150_stream.queue == NULL) {
+		return -ESRCH;
+	}
+
+	/* Discard overflow.  See discussion in !316. */
+	if (xQueueSend(max86150_stream.queue, data, 0) != pdTRUE) {
+		LOG_WARN("max86150", "queue full");
+		return -EIO;
+	}
+	return api_interrupt_trigger(EPIC_INT_MAX86150);
+}
+
+static int max86150_fetch_fifo(void)
+{
+	int result = 0;
+
+	mutex_lock(&max86150_mutex);
+	hwlock_acquire(HWLOCK_I2C);
+
+	struct max86150_sensor_data sample;
+
+	// There is a recommendation from Maxim not to read the entire FIFO, but rather a fixed number of samples.
+	// See https://os.mbed.com/users/laserdad/code/MAX86150_ECG_PPG//file/3c728f3d1f10/main.cpp/
+	// So we should not use max86150_check() but max86150_get_sample().
+	while (max86150_get_sample(&sample.red, &sample.ir, &sample.ecg) > 0) {
+		result = max86150_handle_sample(&sample);
+		// stop in case of errors
+		if (result < 0) {
+			break;
+		}
+	}
+	hwlock_release(HWLOCK_I2C);
+	mutex_unlock(&max86150_mutex);
+	return result;
+}
+
+/*
+ * Callback for the MAX86150 interrupt pin.  This callback is called from the
+ * SDK's GPIO interrupt driver, in interrupt context.
+ */
+static void max86150_interrupt_callback(void *_)
+{
+	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
+
+	if (max86150_task_id != NULL) {
+		vTaskNotifyGiveFromISR(
+			max86150_task_id, &xHigherPriorityTaskWoken
+		);
+		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
+	}
+}
+/* }}} */
+
+void max86150_mutex_init(void)
+{
+	mutex_create(&max86150_mutex);
+}
+
+void vMAX86150Task(void *pvParameters)
+{
+	max86150_task_id = xTaskGetCurrentTaskHandle();
+
+	mutex_lock(&max86150_mutex);
+	hwlock_acquire(HWLOCK_I2C);
+
+	/* Install interrupt callback */
+	GPIO_Config(&max86150_interrupt_pin);
+	GPIO_RegisterCallback(
+		&max86150_interrupt_pin, max86150_interrupt_callback, NULL
+	);
+	GPIO_IntConfig(
+		&max86150_interrupt_pin, GPIO_INT_EDGE, GPIO_INT_FALLING
+	);
+	GPIO_IntEnable(&max86150_interrupt_pin);
+	NVIC_SetPriority(
+		(IRQn_Type)MXC_GPIO_GET_IRQ(max86150_interrupt_pin.port), 2
+	);
+	NVIC_EnableIRQ(
+		(IRQn_Type)MXC_GPIO_GET_IRQ(max86150_interrupt_pin.port)
+	);
+
+	hwlock_release(HWLOCK_I2C);
+	mutex_unlock(&max86150_mutex);
+
+	/* ----------------------------------------- */
+
+	while (1) {
+		if (max86150_sensor_active) {
+			//LOG_INFO("max86150", "Interrupt!");
+			int ret = max86150_fetch_fifo();
+			if (ret < 0) {
+				LOG_ERR("max86150", "Unknown error: %d", -ret);
+			}
+		}
+		/*
+		 * Wait for interrupt.  After two seconds, fetch FIFO anyway
+		 *
+		 * 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 8ead5e72..474b3293 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -13,6 +13,7 @@ module_sources = files(
   'lifecycle.c',
   'light_sensor.c',
   'log.c',
+  'max86150.c',
   'max30001.c',
   'mutex.c',
   'panic.c',
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 567a2a82..0444b688 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -111,6 +111,12 @@ void disp_forcelock();
 #define BHI160_MUTEX_WAIT_MS          50
 void vBhi160Task(void *pvParameters);
 
+/* ---------- MAX86150 ----------------------------------------------------- */
+#define MAX86150_MUTEX_WAIT_MS		50
+void vMAX86150Task(void *pvParameters);
+void max86150_mutex_init(void);
+
+
 /* ---------- MAX30001 ----------------------------------------------------- */
 void vMAX30001Task(void *pvParameters);
 void max30001_mutex_init(void);
diff --git a/epicardium/modules/sleep.c b/epicardium/modules/sleep.c
index 4b94fa79..c7c060e1 100644
--- a/epicardium/modules/sleep.c
+++ b/epicardium/modules/sleep.c
@@ -176,9 +176,9 @@ void sleep_deepsleep(void)
 	/* This will fail if there is no
      * harmonic board attached */
 	max86150_begin();
-	max86150_getINT1();
-	max86150_getINT2();
-	max86150_shutDown();
+	max86150_get_int1();
+	max86150_get_int2();
+	max86150_shut_down();
 #endif
 	epic_bhi160_disable_all_sensors();
 	epic_bme680_deinit();
diff --git a/epicardium/modules/stream.h b/epicardium/modules/stream.h
index f6f93970..a953537a 100644
--- a/epicardium/modules/stream.h
+++ b/epicardium/modules/stream.h
@@ -36,6 +36,7 @@ enum stream_descriptor {
 	SD_BHI160_GYROSCOPE,
 	/** MAX30001 ECG */
 	SD_MAX30001_ECG,
+	SD_MAX86150,
 	/** Highest descriptor must always be ``SD_MAX``. */
 	SD_MAX,
 };
-- 
GitLab