From 5b5d49cb495483c955a695a7407fc6bf1ff12f9a Mon Sep 17 00:00:00 2001 From: Liam McLoughlin Date: Thu, 13 Feb 2025 11:40:38 +0000 Subject: [PATCH] fw: drivers: add AS7000 HRM driver code/demo app --- platform/wscript | 7 +- src/fw/apps/system_apps/hrm_demo.c | 395 ++++++ .../settings/settings_certifications.h | 9 +- src/fw/board/boards/board_robert.c | 73 ++ src/fw/board/boards/board_robert.h | 3 + src/fw/board/boards/board_silk.c | 26 + src/fw/board/boards/board_silk.h | 3 + src/fw/console/prompt_commands.h | 14 + src/fw/drivers/hrm/README_AS7000.md | 130 ++ src/fw/drivers/hrm/as7000.c | 1120 +++++++++++++++++ src/fw/drivers/hrm/as7000.h | 13 + src/fw/drivers/wscript_build | 14 + .../normal/system_app_registry_list.json | 6 + .../robert/resource/resource_ids.auto.h | 1 + .../silk/resource/resource_ids.auto.h | 1 + 15 files changed, 1809 insertions(+), 6 deletions(-) create mode 100644 src/fw/apps/system_apps/hrm_demo.c create mode 100644 src/fw/drivers/hrm/README_AS7000.md create mode 100644 src/fw/drivers/hrm/as7000.c diff --git a/platform/wscript b/platform/wscript index d3b1ef14..4059bf5e 100644 --- a/platform/wscript +++ b/platform/wscript @@ -140,7 +140,12 @@ def has_touch(ctx): @conf def get_hrm(ctx): - return None + if is_robert(ctx): + return "AS7000" + elif is_silk(ctx): + return "AS7000" + else: + return None @conf diff --git a/src/fw/apps/system_apps/hrm_demo.c b/src/fw/apps/system_apps/hrm_demo.c new file mode 100644 index 00000000..2b643b44 --- /dev/null +++ b/src/fw/apps/system_apps/hrm_demo.c @@ -0,0 +1,395 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "applib/app.h" +#include "applib/app_message/app_message.h" +#include "applib/ui/app_window_stack.h" +#include "applib/ui/text_layer.h" +#include "applib/ui/window.h" +#include "apps/system_app_ids.h" +#include "drivers/hrm/as7000.h" +#include "kernel/pbl_malloc.h" +#include "mfg/mfg_info.h" +#include "mfg/mfg_serials.h" +#include "process_state/app_state/app_state.h" +#include "services/common/hrm/hrm_manager.h" +#include "system/passert.h" + +#define BPM_STRING_LEN 10 + +typedef enum { + AppMessageKey_Status = 1, + + AppMessageKey_HeartRate = 10, + AppMessageKey_Confidence = 11, + AppMessageKey_Current = 12, + AppMessageKey_TIA = 13, + AppMessageKey_PPG = 14, + AppMessageKey_AccelData = 15, + AppMessageKey_SerialNumber = 16, + AppMessageKey_Model = 17, + AppMessageKey_HRMProtocolVersionMajor = 18, + AppMessageKey_HRMProtocolVersionMinor = 19, + AppMessageKey_HRMSoftwareVersionMajor = 20, + AppMessageKey_HRMSoftwareVersionMinor = 21, + AppMessageKey_HRMApplicationID = 22, + AppMessageKey_HRMHardwareRevision = 23, +} AppMessageKey; + +typedef enum { + AppStatus_Stopped = 0, + AppStatus_Enabled_1HZ = 1, +} AppStatus; + +typedef struct { + HRMSessionRef session; + EventServiceInfo hrm_event_info; + + Window window; + TextLayer bpm_text_layer; + TextLayer quality_text_layer; + + char bpm_string[BPM_STRING_LEN]; + + bool ready_to_send; + DictionaryIterator *out_iter; +} AppData; + +static char *prv_get_quality_string(HRMQuality quality) { + switch (quality) { + case HRMQuality_NoAccel: + return "No Accel Data"; + case HRMQuality_OffWrist: + return "Off Wrist"; + case HRMQuality_NoSignal: + return "No Signal"; + case HRMQuality_Worst: + return "Worst"; + case HRMQuality_Poor: + return "Poor"; + case HRMQuality_Acceptable: + return "Acceptable"; + case HRMQuality_Good: + return "Good"; + case HRMQuality_Excellent: + return "Excellent"; + } + WTF; +} + +static char *prv_translate_error(AppMessageResult result) { + switch (result) { + case APP_MSG_OK: return "APP_MSG_OK"; + case APP_MSG_SEND_TIMEOUT: return "APP_MSG_SEND_TIMEOUT"; + case APP_MSG_SEND_REJECTED: return "APP_MSG_SEND_REJECTED"; + case APP_MSG_NOT_CONNECTED: return "APP_MSG_NOT_CONNECTED"; + case APP_MSG_APP_NOT_RUNNING: return "APP_MSG_APP_NOT_RUNNING"; + case APP_MSG_INVALID_ARGS: return "APP_MSG_INVALID_ARGS"; + case APP_MSG_BUSY: return "APP_MSG_BUSY"; + case APP_MSG_BUFFER_OVERFLOW: return "APP_MSG_BUFFER_OVERFLOW"; + case APP_MSG_ALREADY_RELEASED: return "APP_MSG_ALREADY_RELEASED"; + case APP_MSG_CALLBACK_ALREADY_REGISTERED: return "APP_MSG_CALLBACK_ALREADY_REGISTERED"; + case APP_MSG_CALLBACK_NOT_REGISTERED: return "APP_MSG_CALLBACK_NOT_REGISTERED"; + case APP_MSG_OUT_OF_MEMORY: return "APP_MSG_OUT_OF_MEMORY"; + case APP_MSG_CLOSED: return "APP_MSG_CLOSED"; + case APP_MSG_INTERNAL_ERROR: return "APP_MSG_INTERNAL_ERROR"; + default: return "UNKNOWN ERROR"; + } +} + +static void prv_send_msg(void) { + AppData *app_data = app_state_get_user_data(); + + AppMessageResult result = app_message_outbox_send(); + if (result == APP_MSG_OK) { + app_data->ready_to_send = false; + } else { + PBL_LOG(LOG_LEVEL_DEBUG, "Error sending message: %s", prv_translate_error(result)); + } +} + +static void prv_send_status_and_version(void) { + AppData *app_data = app_state_get_user_data(); + PBL_LOG(LOG_LEVEL_DEBUG, "Sending status and version to mobile app"); + + AppMessageResult result = app_message_outbox_begin(&app_data->out_iter); + if (result != APP_MSG_OK) { + PBL_LOG(LOG_LEVEL_DEBUG, "Failed to begin outbox - reason %i %s", + result, prv_translate_error(result)); + return; + } + + dict_write_uint8(app_data->out_iter, AppMessageKey_Status, AppStatus_Enabled_1HZ); + +#if CAPABILITY_HAS_BUILTIN_HRM + if (mfg_info_is_hrm_present()) { + AS7000InfoRecord hrm_info = {}; + as7000_get_version_info(HRM, &hrm_info); + dict_write_uint8(app_data->out_iter, AppMessageKey_HRMProtocolVersionMajor, + hrm_info.protocol_version_major); + dict_write_uint8(app_data->out_iter, AppMessageKey_HRMProtocolVersionMinor, + hrm_info.protocol_version_minor); + dict_write_uint8(app_data->out_iter, AppMessageKey_HRMSoftwareVersionMajor, + hrm_info.sw_version_major); + dict_write_uint8(app_data->out_iter, AppMessageKey_HRMSoftwareVersionMinor, + hrm_info.sw_version_minor); + dict_write_uint8(app_data->out_iter, AppMessageKey_HRMApplicationID, + hrm_info.application_id); + dict_write_uint8(app_data->out_iter, AppMessageKey_HRMHardwareRevision, + hrm_info.hw_revision); + } +#endif + + char serial_number_buffer[MFG_SERIAL_NUMBER_SIZE + 1]; + mfg_info_get_serialnumber(serial_number_buffer, sizeof(serial_number_buffer)); + dict_write_data(app_data->out_iter, AppMessageKey_SerialNumber, + (uint8_t*) serial_number_buffer, sizeof(serial_number_buffer)); + +#if IS_BIGBOARD + WatchInfoColor watch_color = WATCH_INFO_MODEL_UNKNOWN; +#else + WatchInfoColor watch_color = mfg_info_get_watch_color(); +#endif // IS_BIGBOARD + dict_write_uint32(app_data->out_iter, AppMessageKey_Model, watch_color); + + prv_send_msg(); +} + +static void prv_handle_hrm_data(PebbleEvent *e, void *context) { + AppData *app_data = app_state_get_user_data(); + + if (e->type == PEBBLE_HRM_EVENT) { + PebbleHRMEvent *hrm = &e->hrm; + + // Save HRMEventBPM data and send when we get the current into. + static uint8_t bpm = 0; + static uint8_t bpm_quality = 0; + static uint16_t led_current = 0; + + if (hrm->event_type == HRMEvent_BPM) { + snprintf(app_data->bpm_string, sizeof(app_data->bpm_string), "%"PRIu8" BPM", hrm->bpm.bpm); + text_layer_set_text(&app_data->quality_text_layer, prv_get_quality_string(hrm->bpm.quality)); + layer_mark_dirty(&app_data->window.layer); + + bpm = hrm->bpm.bpm; + bpm_quality = hrm->bpm.quality; + } else if (hrm->event_type == HRMEvent_LEDCurrent) { + led_current = hrm->led.current_ua; + } else if (hrm->event_type == HRMEvent_Diagnostics) { + if (!app_data->ready_to_send) { + return; + } + + AppMessageResult result = app_message_outbox_begin(&app_data->out_iter); + PBL_ASSERTN(result == APP_MSG_OK); + + if (bpm) { + dict_write_uint8(app_data->out_iter, AppMessageKey_HeartRate, bpm); + dict_write_uint8(app_data->out_iter, AppMessageKey_Confidence, bpm_quality); + } + + if (led_current) { + dict_write_uint16(app_data->out_iter, AppMessageKey_Current, led_current); + } + + if (hrm->debug->ppg_data.num_samples) { + HRMPPGData *d = &hrm->debug->ppg_data; + dict_write_data(app_data->out_iter, AppMessageKey_TIA, + (uint8_t *)d->tia, d->num_samples * sizeof(d->tia[0])); + dict_write_data(app_data->out_iter, AppMessageKey_PPG, + (uint8_t *)d->ppg, d->num_samples * sizeof(d->ppg[0])); + } + + if (hrm->debug->ppg_data.tia[hrm->debug->ppg_data.num_samples - 1] == 0) { + PBL_LOG_COLOR(LOG_LEVEL_DEBUG, LOG_COLOR_CYAN, "last PPG TIA sample is 0!"); + } + + if (hrm->debug->ppg_data.num_samples != 20) { + PBL_LOG_COLOR(LOG_LEVEL_DEBUG, LOG_COLOR_CYAN, "Only got %"PRIu16" samples!", + hrm->debug->ppg_data.num_samples); + } + + if (hrm->debug->accel_data.num_samples) { + HRMAccelData *d = &hrm->debug->accel_data; + dict_write_data(app_data->out_iter, AppMessageKey_AccelData, + (uint8_t *)d->data, d->num_samples * sizeof(d->data[0])); + } + + PBL_LOG(LOG_LEVEL_DEBUG, + "Sending message - bpm:%u quality:%u current:%u " + "ppg_readings:%u accel_readings %"PRIu32, + bpm, + bpm_quality, + led_current, + hrm->debug->ppg_data.num_samples, + hrm->debug->accel_data.num_samples); + + led_current = bpm = bpm_quality = 0; + + prv_send_msg(); + } else if (hrm->event_type == HRMEvent_SubscriptionExpiring) { + PBL_LOG(LOG_LEVEL_INFO, "Got subscription expiring event"); + // Subscribe again if our subscription is expiring + const uint32_t update_time_s = 1; + app_data->session = sys_hrm_manager_app_subscribe(APP_ID_HRM_DEMO, update_time_s, + SECONDS_PER_HOUR, HRMFeature_BPM); + } + } +} + +static void prv_enable_hrm(void) { + AppData *app_data = app_state_get_user_data(); + + app_data->hrm_event_info = (EventServiceInfo) { + .type = PEBBLE_HRM_EVENT, + .handler = prv_handle_hrm_data, + }; + event_service_client_subscribe(&app_data->hrm_event_info); + + // TODO: Let the mobile app control this? + const uint32_t update_time_s = 1; + app_data->session = sys_hrm_manager_app_subscribe( + APP_ID_HRM_DEMO, update_time_s, SECONDS_PER_HOUR, + HRMFeature_BPM | HRMFeature_LEDCurrent | HRMFeature_Diagnostics); +} + +static void prv_disable_hrm(void) { + AppData *app_data = app_state_get_user_data(); + + event_service_client_unsubscribe(&app_data->hrm_event_info); + sys_hrm_manager_unsubscribe(app_data->session); +} + +static void prv_handle_mobile_status_request(AppStatus status) { + AppData *app_data = app_state_get_user_data(); + + if (status == AppStatus_Stopped) { + text_layer_set_text(&app_data->bpm_text_layer, "Paused"); + text_layer_set_text(&app_data->quality_text_layer, "Paused by mobile"); + prv_disable_hrm(); + } else { + app_data->bpm_string[0] = '\0'; + text_layer_set_text(&app_data->bpm_text_layer, app_data->bpm_string); + text_layer_set_text(&app_data->quality_text_layer, "Loading..."); + prv_enable_hrm(); + } +} + +static void prv_message_received_cb(DictionaryIterator *iterator, void *context) { + Tuple *status_tuple = dict_find(iterator, AppMessageKey_Status); + + if (status_tuple) { + prv_handle_mobile_status_request(status_tuple->value->uint8); + } +} + +static void prv_message_sent_cb(DictionaryIterator *iterator, void *context) { + AppData *app_data = app_state_get_user_data(); + + app_data->ready_to_send = true; +} + +static void prv_message_failed_cb(DictionaryIterator *iterator, + AppMessageResult reason, void *context) { + PBL_LOG(LOG_LEVEL_DEBUG, "Out message send failed - reason %i %s", + reason, prv_translate_error(reason)); + AppData *app_data = app_state_get_user_data(); + app_data->ready_to_send = true; +} + +static void prv_remote_notify_timer_cb(void *data) { + prv_send_status_and_version(); +} + +static void prv_init(void) { + AppData *app_data = app_malloc_check(sizeof(*app_data)); + *app_data = (AppData) { + .session = (HRMSessionRef)app_data, // Use app data as session ref + .ready_to_send = false, + }; + app_state_set_user_data(app_data); + + Window *window = &app_data->window; + window_init(window, ""); + window_set_fullscreen(window, true); + + GRect bounds = window->layer.bounds; + + bounds.origin.y += 40; + TextLayer *bpm_tl = &app_data->bpm_text_layer; + text_layer_init(bpm_tl, &bounds); + text_layer_set_font(bpm_tl, fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD)); + text_layer_set_text_alignment(bpm_tl, GTextAlignmentCenter); + text_layer_set_text(bpm_tl, app_data->bpm_string); + layer_add_child(&window->layer, &bpm_tl->layer); + + bounds.origin.y += 35; + TextLayer *quality_tl = &app_data->quality_text_layer; + text_layer_init(quality_tl, &bounds); + text_layer_set_font(quality_tl, fonts_get_system_font(FONT_KEY_GOTHIC_18)); + text_layer_set_text_alignment(quality_tl, GTextAlignmentCenter); + text_layer_set_text(quality_tl, "Loading..."); + layer_add_child(&window->layer, &quality_tl->layer); + + const uint32_t inbox_size = 64; + const uint32_t outbox_size = 256; + AppMessageResult result = app_message_open(inbox_size, outbox_size); + if (result != APP_MSG_OK) { + PBL_LOG(LOG_LEVEL_ERROR, "Unable to open app message! %i %s", + result, prv_translate_error(result)); + } else { + PBL_LOG(LOG_LEVEL_DEBUG, "Successfully opened app message"); + } + + if (!sys_hrm_manager_is_hrm_present()) { + text_layer_set_text(quality_tl, "No HRM Present"); + } else { + text_layer_set_text(quality_tl, "Loading..."); + prv_enable_hrm(); + } + + app_message_register_inbox_received(prv_message_received_cb); + app_message_register_outbox_sent(prv_message_sent_cb); + app_message_register_outbox_failed(prv_message_failed_cb); + + app_timer_register(1000, prv_remote_notify_timer_cb, NULL); + + app_window_stack_push(window, true); +} + +static void prv_deinit(void) { + AppData *app_data = app_state_get_user_data(); + sys_hrm_manager_unsubscribe(app_data->session); +} + +static void prv_main(void) { + prv_init(); + app_event_loop(); + prv_deinit(); +} + +const PebbleProcessMd* hrm_demo_get_app_info(void) { + static const PebbleProcessMdSystem s_hrm_demo_app_info = { + .name = "HRM Demo", + .common.uuid = { 0xf8, 0x1b, 0x2a, 0xf8, 0x13, 0x0a, 0x11, 0xe6, + 0x86, 0x9f, 0xa4, 0x5e, 0x60, 0xb9, 0x77, 0x3d }, + .common.main_func = &prv_main, + }; + // Only show in launcher if HRM is present + return (sys_hrm_manager_is_hrm_present()) ? (const PebbleProcessMd*)&s_hrm_demo_app_info : NULL; +} diff --git a/src/fw/apps/system_apps/settings/settings_certifications.h b/src/fw/apps/system_apps/settings/settings_certifications.h index b91cd8a0..8c2d9309 100644 --- a/src/fw/apps/system_apps/settings/settings_certifications.h +++ b/src/fw/apps/system_apps/settings/settings_certifications.h @@ -165,12 +165,11 @@ static const CertificationIds * prv_get_certification_ids(void) { #elif defined(BOARD_SPALDING) || defined(BOARD_SPALDING_EVT) return &s_certification_ids_spalding; #elif PLATFORM_SILK && !defined(IS_BIGBOARD) -// TODO: remove force-false -// if (mfg_info_is_hrm_present()) { -// return &s_certification_ids_silk_hr; -// } else { + if (mfg_info_is_hrm_present()) { + return &s_certification_ids_silk_hr; + } else { return &s_certification_ids_silk; -// } + } #else return &s_certification_ids_fallback; #endif diff --git a/src/fw/board/boards/board_robert.c b/src/fw/board/boards/board_robert.c index 46bab4f0..b8128be5 100644 --- a/src/fw/board/boards/board_robert.c +++ b/src/fw/board/boards/board_robert.c @@ -19,6 +19,7 @@ #include "drivers/display/ice40lp/ice40lp_definitions.h" #include "drivers/exti.h" #include "drivers/flash/qspi_flash_definitions.h" +#include "drivers/hrm/as7000.h" #include "drivers/i2c_definitions.h" #include "drivers/mic/stm32/dfsdm_definitions.h" #include "drivers/pmic.h" @@ -292,6 +293,51 @@ static const I2CBus I2C_TOUCH_ALS_BUS = { }; #endif +static I2CBusState I2C_HRM_BUS_STATE = {}; + +static const I2CBusHal I2C_HRM_BUS_HAL = { + .i2c = I2C2, + .clock_ctrl = RCC_APB1Periph_I2C2, + .bus_mode = I2CBusMode_FastMode, + .clock_speed = 400000, +#if BOARD_ROBERT_BB || BOARD_CUTTS_BB + // TODO: These need to be measured. Just using PMIC_MAG values for now. + .rise_time_ns = 150, + .fall_time_ns = 6, +#elif BOARD_ROBERT_BB2 + // TODO: These need to be measured. Just using PMIC_MAG values for now. + .rise_time_ns = 150, + .fall_time_ns = 6, +#elif BOARD_ROBERT_EVT + // TODO: These need to be measured. Just using PMIC_MAG values for now. + .rise_time_ns = 70, + .fall_time_ns = 5, +#else +#error "Unknown board" +#endif + .ev_irq_channel = I2C2_EV_IRQn, + .er_irq_channel = I2C2_ER_IRQn, +}; + +static const I2CBus I2C_HRM_BUS = { + .state = &I2C_HRM_BUS_STATE, + .hal = &I2C_HRM_BUS_HAL, + .scl_gpio = { + .gpio = GPIOF, + .gpio_pin = GPIO_Pin_1, + .gpio_pin_source = GPIO_PinSource1, + .gpio_af = GPIO_AF4_I2C2 + }, + .sda_gpio = { + .gpio = GPIOF, + .gpio_pin = GPIO_Pin_0, + .gpio_pin_source = GPIO_PinSource0, + .gpio_af = GPIO_AF4_I2C2 + }, + .stop_mode_inhibitor = InhibitorI2C2, + .name = "I2C_HRM" +}; + #if BOARD_CUTTS_BB static I2CBusState I2C_NFC_BUS_STATE = {}; @@ -388,9 +434,17 @@ static const I2CSlavePort I2C_SLAVE_MAG3110 = { .address = 0x0e << 1 }; +static const I2CSlavePort I2C_SLAVE_AS7000 = { + .bus = &I2C_HRM_BUS, + .address = 0x60 +}; + I2CSlavePort * const I2C_MAX14690 = &I2C_SLAVE_MAX14690; I2CSlavePort * const I2C_MAG3110 = &I2C_SLAVE_MAG3110; +I2CSlavePort * const I2C_AS7000 = &I2C_SLAVE_AS7000; +IRQ_MAP(I2C2_EV, i2c_hal_event_irq_handler, &I2C_HRM_BUS); +IRQ_MAP(I2C2_ER, i2c_hal_error_irq_handler, &I2C_HRM_BUS); IRQ_MAP(I2C4_EV, i2c_hal_event_irq_handler, &I2C_PMIC_MAG_BUS); IRQ_MAP(I2C4_ER, i2c_hal_error_irq_handler, &I2C_PMIC_MAG_BUS); #if BOARD_CUTTS_BB @@ -399,6 +453,24 @@ IRQ_MAP(I2C1_ER, i2c_hal_error_irq_handler, &I2C_TOUCH_ALS_BUS); #endif +// HRM DEVICE +static HRMDeviceState s_hrm_state; +static HRMDevice HRM_DEVICE = { + .state = &s_hrm_state, + .handshake_int = { EXTI_PortSourceGPIOI, 10 }, + .int_gpio = { + .gpio = GPIOI, + .gpio_pin = GPIO_Pin_10 + }, + .en_gpio = { + .gpio = GPIOF, + .gpio_pin = GPIO_Pin_3, + .active_high = false, + }, + .i2c_slave = &I2C_SLAVE_AS7000, +}; +HRMDevice * const HRM = &HRM_DEVICE; + #if BOARD_CUTTS_BB static const TouchSensor EWD1000_DEVICE = { .i2c = &I2C_SLAVE_EWD1000, @@ -745,6 +817,7 @@ void board_init(void) { i2c_init(&I2C_TOUCH_ALS_BUS); i2c_init(&I2C_NFC_BUS); #endif + i2c_init(&I2C_HRM_BUS); i2c_init(&I2C_PMIC_MAG_BUS); spi_slave_port_init(BMI160_SPI); diff --git a/src/fw/board/boards/board_robert.h b/src/fw/board/boards/board_robert.h index 43fc20f6..2b1f312b 100644 --- a/src/fw/board/boards/board_robert.h +++ b/src/fw/board/boards/board_robert.h @@ -293,6 +293,7 @@ extern SPISlavePort * const BMI160_SPI; extern I2CSlavePort * const I2C_MAX14690; extern I2CSlavePort * const I2C_MAG3110; +extern I2CSlavePort * const I2C_AS7000; extern VoltageMonitorDevice * const VOLTAGE_MONITOR_ALS; extern VoltageMonitorDevice * const VOLTAGE_MONITOR_BATTERY; @@ -307,6 +308,8 @@ extern SPISlavePort * const DIALOG_SPI; extern MicDevice * const MIC; +extern HRMDevice * const HRM; + #if BOARD_CUTTS_BB extern TouchSensor * const EWD1000; #endif diff --git a/src/fw/board/boards/board_silk.c b/src/fw/board/boards/board_silk.c index 65213052..2f72405f 100644 --- a/src/fw/board/boards/board_silk.c +++ b/src/fw/board/boards/board_silk.c @@ -18,6 +18,7 @@ #include "drivers/exti.h" #include "drivers/flash/qspi_flash_definitions.h" +#include "drivers/hrm/as7000.h" #include "drivers/i2c_definitions.h" #include "drivers/mic/stm32/dfsdm_definitions.h" #include "drivers/qspi_definitions.h" @@ -267,7 +268,13 @@ static const I2CSlavePort I2C_SLAVE_AS3701B = { .address = 0x80 }; +static const I2CSlavePort I2C_SLAVE_AS7000 = { + .bus = &I2C_PMIC_HRM_BUS, + .address = 0x60 +}; + I2CSlavePort * const I2C_AS3701B = &I2C_SLAVE_AS3701B; +I2CSlavePort * const I2C_AS7000 = &I2C_SLAVE_AS7000; IRQ_MAP(I2C3_EV, i2c_hal_event_irq_handler, &I2C_PMIC_HRM_BUS); IRQ_MAP(I2C3_ER, i2c_hal_error_irq_handler, &I2C_PMIC_HRM_BUS); @@ -356,6 +363,25 @@ static SPISlavePort DIALOG_SPI_SLAVE_PORT = { SPISlavePort * const DIALOG_SPI = &DIALOG_SPI_SLAVE_PORT; +// HRM DEVICE +static HRMDeviceState s_hrm_state; +static HRMDevice HRM_DEVICE = { + .state = &s_hrm_state, + .handshake_int = { EXTI_PortSourceGPIOA, 15 }, + .int_gpio = { + .gpio = GPIOA, + .gpio_pin = GPIO_Pin_15 + }, + .en_gpio = { + .gpio = GPIOC, + .gpio_pin = GPIO_Pin_1, + .active_high = false, + }, + .i2c_slave = &I2C_SLAVE_AS7000, +}; +HRMDevice * const HRM = &HRM_DEVICE; + + // QSPI static QSPIPortState s_qspi_port_state; static QSPIPort QSPI_PORT = { diff --git a/src/fw/board/boards/board_silk.h b/src/fw/board/boards/board_silk.h index 65230196..6bd52b52 100644 --- a/src/fw/board/boards/board_silk.h +++ b/src/fw/board/boards/board_silk.h @@ -229,12 +229,15 @@ extern UARTDevice * const BT_TX_BOOTROM_UART; extern UARTDevice * const BT_RX_BOOTROM_UART; extern I2CSlavePort * const I2C_AS3701B; +extern I2CSlavePort * const I2C_AS7000; extern const VoltageMonitorDevice * VOLTAGE_MONITOR_ALS; extern const VoltageMonitorDevice * VOLTAGE_MONITOR_BATTERY; extern const TemperatureSensor * const TEMPERATURE_SENSOR; +extern HRMDevice * const HRM; + extern QSPIPort * const QSPI; extern QSPIFlash * const QSPI_FLASH; diff --git a/src/fw/console/prompt_commands.h b/src/fw/console/prompt_commands.h index db65fbf1..a2c09fab 100644 --- a/src/fw/console/prompt_commands.h +++ b/src/fw/console/prompt_commands.h @@ -268,6 +268,8 @@ extern void command_low_power_debug(char *enable_arg); extern void command_audit_delay_us(void); extern void command_enter_stop(void); +extern void dialog_test_cmds(void); + extern void command_dump_notif_pref_db(void); extern void command_bt_conn_param_set( @@ -284,6 +286,12 @@ extern void command_btle_pa_set(char *option); extern void command_btle_unmod_tx_start(char *tx_channel); extern void command_btle_unmod_tx_stop(void); +#if CAPABILITY_HAS_BUILTIN_HRM +extern void command_hrm_read(void); +extern void command_hrm_wipe(void); +extern void command_hrm_freeze(void); +#endif + #if MFG_INFO_RECORDS_TEST_RESULTS extern void command_mfg_info_test_results(void); #endif @@ -467,6 +475,12 @@ static const Command s_prompt_commands[] = { #endif // PLATFORM_TINTIN #endif // RECOVERY_FW +#if CAPABILITY_HAS_BUILTIN_HRM + { "hrm read", command_hrm_read, 0}, + { "hrm wipe", command_hrm_wipe, 0}, + { "hrm freeze", command_hrm_freeze, 0}, +#endif + #if CAPABILITY_HAS_ACCESSORY_CONNECTOR { "accessory power", command_accessory_power_set, 1 }, { "accessory stress", command_accessory_stress_test, 0 }, diff --git a/src/fw/drivers/hrm/README_AS7000.md b/src/fw/drivers/hrm/README_AS7000.md new file mode 100644 index 00000000..77469442 --- /dev/null +++ b/src/fw/drivers/hrm/README_AS7000.md @@ -0,0 +1,130 @@ +Some Information About the AMS AS7000 +===================================== + +The documentation about the AS7000 can be a bit hard to follow and a bit +incomplete at times. This document aims to fill in the gaps, using +knowledge gleaned from the datasheet, Communication Protocol document +and the SDK docs and sources. + +What is the AS7000 exactly? +--------------------------- + +The AS7000 is a Cortex-M0 SoC with some very specialized peripherals. +It can be programmed with a firmware to have it function as a heart-rate +monitor. It doesn't necessarily come with the HRM firmware preloaded, so +it is not one of those devices that you can treat as a black-box piece +of hardware that you just power up and talk to. + +There is 32 kB of flash for the main application, and some "reserved" +flash containing a loader application. There is also a ROM first-stage +bootloader. + +Boot Process +------------ + +The chip is woken from shutdown by pulling the chip's GPIO8 pin low. The +CPU core executes the ROM bootloader after powerup or wake from +shutdown. The bootloader's logic is apparently as follows: + + if loader is available: + jump to loader + elif application is valid: + remap flash to address 0 + jump to application + else: + wait on UART?? (not documented further) + +We will probably always be using parts with the loader application +preprogrammed by AMS so the existence of the bootloader just makes the +communication protocol document a bit more confusing to understand. It +can be safely ignored and we can pretend that the loader application is +the only bootloader. + +Once control is passed from the ROM bootloader to the loader, the loader +performs the following: + + wait 30 ms + if application is valid and GPIO8 is low: + remap flash to address 0 + jump to application + else: + run loader application + +The reason for the 30 ms wait and check for GPIO8 is to provide an +escape hatch for getting into the loader if a misbehaving application +is programmed which doesn't allow for a command to be used to enter the +loader. + +The "Application is valid" check is apparently to check that address +0x7FFC (top of main flash) contains the bytes 72 75 6C 75. + +The Loader +---------- + +The loader allows new applications to be programmed into the main flash +over I2C by sending Intel HEX records(!). The communication protocol +document describes the protocol well enough, though it is a bit lacking +in detail as to what HEX records it accepts. Luckily the SDK comes with +a header file which fills in some of the details, and the SDK Getting +Started document has some more details. + + - The supported record types are 00 (Data), 01 (EOF), 04 (Extended + Linear Address) and 05 (Start Linear Address). + - The HEX records must specify addresses in the aliased range 0-0x7FFF + - HEX records must be sent in order of strictly increasing addresses. + +A comment in loader.h from the SDK states that the maximum record size +supported by the loader can be configured as 256, 128, 64 or 32. The +`#define` in the same header which sets the max record length is set +to 256, strongly implying that this is the record length limit for the +loader firmware which is already programmed into the parts. Programming +an application firmware is successful when using records of length 203, +which seems to confirm that the max record length is indeed 256. + +The HEX files provided by AMS appear to be standard HEX files generated +by the Keil MDK-ARM toolchain. http://www.keil.com/support/docs/1584.htm + +The Start Linear Address record contains the address of the "pre-main" +function, which is not the reset vector. Since the reset vector is +already written as data in the application's vector table, the record is +unnecessary for flash loading and is likely just thrown away by the +loader. + +Empirical testing has confirmed that neither the Start Linear Address +nor Extended Linear Address records need to be sent for flashing to +succeed. It is acceptable to send only a series of Data records followed +by an EOF record. + +The loader will exit if one of the following conditions is true: + + - An EOF record is sent + - A valid HEX record is sent which the loader doesn't understand + - Data is sent which is not a valid HEX record + - No records are sent for approximately ten seconds + +The loader exits by waiting one second for the host controller to read +out the exit code register, then performs a system reset. + +The Application +--------------- + +Due to the simplicity of the CM0 and the limited resources available on +the chip, the application firmware takes full control of the system. The +applications in the SDK are written as a straightforward mainloop with +no OS. Think of it as a very simple form of cooperative multitasking. + +The "mandatory" I2C register map mentioned in the Communication Protocol +document is merely a part of the protocol; it is entirely up to the +application firmware to properly implement it. The Application ID +register doesn't do anything special on its own: the mainloop simply +polls the register value at each iteration to see if it needs to start +or stop any "apps" (read: modules). The application firmware could +misbehave, resulting in writes to that register having no effect. + +Writing 0x01 to the Application ID register isn't special either; it is +up to the application to recognize that value and reset into the loader. +That's why the escape hatch is necessary, and the fundamental reason why +the loader application cannot run at the same time as any other +application. + +GPIO8 isn't special while the firmware is running, either. diff --git a/src/fw/drivers/hrm/as7000.c b/src/fw/drivers/hrm/as7000.c new file mode 100644 index 00000000..2ab757d5 --- /dev/null +++ b/src/fw/drivers/hrm/as7000.c @@ -0,0 +1,1120 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define FILE_LOG_COLOR LOG_COLOR_GREEN +#include "as7000.h" + +#include "board/board.h" +#include "drivers/backlight.h" +#include "drivers/exti.h" +#include "drivers/gpio.h" +#include "drivers/i2c.h" +#include "kernel/events.h" +#include "kernel/util/sleep.h" +#include "kernel/util/interval_timer.h" +#include "mfg/mfg_info.h" +#include "resource/resource.h" +#include "resource/resource_ids.auto.h" +#include "services/common/analytics/analytics.h" +#include "services/common/system_task.h" +#include "services/common/hrm/hrm_manager.h" +#include "system/logging.h" +#include "system/passert.h" +#include "system/profiler.h" +#include "util/attributes.h" +#include "util/ihex.h" +#include "util/math.h" +#include "util/net.h" + +#define STM32F4_COMPATIBLE +#define STM32F7_COMPATIBLE +#include + +// Enable this to get some very verbose logs about collecting PPG data from the HRM +// Bump this up to 2 to get very verbose logs +#define PPG_DEBUG 0 + +#if PPG_DEBUG +#define PPG_DBG(...) \ + do { \ + PBL_LOG(LOG_LEVEL_DEBUG, __VA_ARGS__); \ + } while (0); +#else +#define PPG_DBG(...) +#endif + +#if PPG_DEBUG == 2 +#define PPG_DBG_VERBOSE(...) \ + do { \ + PBL_LOG_VERBOSE(LOG_LEVEL_DEBUG, __VA_ARGS__); \ + } while (0); +#else +#define PPG_DBG_VERBOSE(...) +#endif + + + +// The datasheet recommends waiting for 250ms for the chip to boot +#define NORMAL_BOOT_DELAY_MS (250) +// We need to wait an extra second for the loader to time-out +#define LOADER_REBOOT_DELAY_MS (NORMAL_BOOT_DELAY_MS + 1000) +// Usually takes a couple ms after writing a record, but spikes of ~20ms have been observed. Let's +// be conservative. +#define LOADER_READY_MAX_DELAY_MS (50) +// Give the sensor this much time to tear down the current app and go back to the idle mode +#define SHUT_DOWN_DELAY_MS (1000) +// Number of handshakes before samples are expected +#define WARMUP_HANDSHAKES (2) + +#define EXPECTED_PROTOCOL_VERSION_MAJOR (2) + +// White Threshold is 5000 +// Black Threshold is 3500 +// Value stored in the register is in units of 64 ADC counts +// e.g. 78 * 64 = 4992 ADC-counts +// Refer to AS7000 SW Communication Protocol section 6.7 +#define PRES_DETECT_THRSH_WHITE 78 // (5000 / 64) +#define PRES_DETECT_THRSH_BLACK 54 // (3500 / 64) + +// register addresses +#define ADDR_LOADER_STATUS (0x00) +#define ADDR_INFO_START (0x00) +#define ADDR_APP_IDS (0x04) + +#define ADDR_ACCEL_SAMPLE_FREQ_MSB (0x08) +#define ADDR_ACCEL_SAMPLE_FREQ_LSB (0x09) + +// Register that allows us to compensate for clock skew between us (the host) and the sensor. The +// sensor doesn't track time accurately, and gives us a heart rate value that's in the sensors +// time domain, which will need to be translated into "real time" according to our time domain. +// If we use these registers to tell the sensor how frequently it's handshaking with us in our +// time domain, this will let the sensor do this compensation for us. +// The value programmed in here is in units of 0.1ms (value of 10000 = 1 second). +#define ADDR_HOST_ONE_SECOND_TIME_MSB (0x0a) +#define ADDR_HOST_ONE_SECOND_TIME_LSB (0x0b) + +#define ADDR_NUM_ACCEL_SAMPLES (0x0e) +#define ADDR_NUM_PPG_SAMPLES (0x0f) + +#define ADDR_ACCEL_SAMPLE_IDX (0x14) +#define ADDR_ACCEL_X_MSB (0x15) +#define ADDR_ACCEL_Y_MSB (0x17) +#define ADDR_ACCEL_Z_MSB (0x19) + +#define ADDR_PPG_IDX (0x1b) +#define ADDR_PPG_MSB (0x1c) +#define ADDR_PPG_LSB (0x1d) +#define ADDR_TIA_MSB (0x1e) +#define ADDR_TIA_LSB (0x1f) + +#define ADDR_PRES_DETECT_THRSH (0x26) + +#define ADDR_LED_CURRENT_MSB (0x34) +#define ADDR_LED_CURRENT_LSB (0x35) +#define ADDR_HRM_STATUS (0x36) +#define ADDR_HRM_BPM (0x37) +#define ADDR_HRM_SQI (0x38) + +#define ADDR_SYNC (0x39) + +// The AS7000 wants Accel Frequency given in 0.001Hz increments, this can be used to scale +#define AS7000_ACCEL_FREQUENCY_SCALE (1000) + +//! Thresholds for quality conversion. These are upper bounds on readings. +enum AS7000SQIThreshold { + AS7000SQIThreshold_Excellent = 2, + AS7000SQIThreshold_Good = 5, + AS7000SQIThreshold_Acceptable = 8, + AS7000SQIThreshold_Poor = 10, + AS7000SQIThreshold_Worst = 20, + + AS7000SQIThreshold_OffWrist = 254, + + AS7000SQIThresholdInvalid, +}; + +enum AS7000Status { + AS7000Status_OK = 0, + AS7000Status_IllegalParameter = 1, + AS7000Status_LostData = 2, + AS7000Status_NoAccel = 4, +}; + +typedef enum AS7000AppId { + AS7000AppId_Idle = 0x00, + AS7000AppId_Loader = 0x01, + AS7000AppId_HRM = 0x02, + AS7000AppId_PRV = 0x04, + AS7000AppId_GSR = 0x08, + AS7000AppId_NTC = 0x10, +} AS7000AppId; + +typedef enum AS7000LoaderStatus { + AS7000LoaderStatus_Ready = 0x00, + AS7000LoaderStatus_Busy1 = 0x3A, + AS7000LoaderStatus_Busy2 = 0xFF, + // all other values indicate an error +} AS7000LoaderStatus; + +typedef struct PACKED AS7000FWUpdateHeader { + uint8_t sw_version_major; + uint8_t sw_version_minor; +} AS7000FWUpdateHeader; + +typedef struct PACKED AS7000FWSegmentHeader { + uint16_t address; + uint16_t len_minus_1; +} AS7000FWSegmentHeader; + +//! The maximum number of data bytes to include in a reconstituted +//! Intel HEX Data record when updating the HRM firmware. +//! This is the size of the binary data encoded in the record, __NOT__ +//! the size of the HEX record encoding the data. The HEX record itself +//! will be IHEX_RECORD_LENGTH(MAX_HEX_DATA_BYTES) +//! (MAX_HEX_DATA_BYTES*2 + 11) bytes in size. +#define MAX_HEX_DATA_BYTES (96) + +// The AS7000 loader cannot accept HEX records longer than 256 bytes. +_Static_assert(IHEX_RECORD_LENGTH(MAX_HEX_DATA_BYTES) <= 256, + "The value of MAX_HEX_DATA_BYTES will result in HEX records " + "which are longer than the AS7000 loader can handle."); + + +// The sw_version_major field is actually a bitfield encoding both the +// major and minor components of the SDK version number. Define macros +// to extract the components for logging purposes. +#define HRM_SW_VERSION_PART_MAJOR(v) (v >> 6) +#define HRM_SW_VERSION_PART_MINOR(v) (v & 0x3f) + +// If this many watchdog interrupts occur before we receive an interrupt from the sensor, +// we assume the sensor requires a reset +#define AS7000_MAX_WATCHDOG_INTERRUPTS 5 + +// We use this regular timer as a watchdog for the sensor. We have seen cases where the sensor +// becomes unresponsive (PBL-40008). This timer watches to see if we have stopped receiving +// sensor interrupts and will trigger logic to reset the sensor if necessary. +static RegularTimerInfo s_as7000_watchdog_timer; + +// Incremented by s_as7000_watchdog_timer. Reset to 0 by our interrupt handler. +static uint8_t s_missing_interrupt_count; + +//! Interval timer to track how frequently the as7000 is handshaking with us +static IntervalTimer s_handshake_interval_timer; + +static void prv_enable_timer_cb(void *context); +static void prv_disable_watchdog(HRMDevice *dev); + +static bool prv_write_register(HRMDevice *dev, uint8_t register_address, uint8_t value) { + i2c_use(dev->i2c_slave); + bool rv = i2c_write_register(dev->i2c_slave, register_address, value); + i2c_release(dev->i2c_slave); + return rv; +} + +static bool prv_write_register_block(HRMDevice *dev, uint8_t register_address, + const void *buffer, uint32_t length) { + i2c_use(dev->i2c_slave); + bool rv = i2c_write_register_block(dev->i2c_slave, register_address, length, buffer); + i2c_release(dev->i2c_slave); + return rv; +} + +static bool prv_read_register(HRMDevice *dev, uint8_t register_address, uint8_t *value) { + i2c_use(dev->i2c_slave); + bool rv = i2c_read_register(dev->i2c_slave, register_address, value); + i2c_release(dev->i2c_slave); + return rv; +} + +static bool prv_read_register_block(HRMDevice *dev, uint8_t register_address, void *buffer, + uint32_t length) { + i2c_use(dev->i2c_slave); + bool rv = i2c_read_register_block(dev->i2c_slave, register_address, length, buffer); + i2c_release(dev->i2c_slave); + return rv; +} + +static bool prv_set_host_one_second_time_register(HRMDevice *dev, uint32_t average_ms) { + PPG_DBG("host one second time: %"PRIu32" ms", average_ms); + + // Register takes a reading in 0.1ms increments + uint16_t value = average_ms * 10; + + const uint8_t msb = (value >> 8) & 0xff; + const uint8_t lsb = value & 0xff; + return prv_write_register(dev, ADDR_HOST_ONE_SECOND_TIME_MSB, msb) + && prv_write_register(dev, ADDR_HOST_ONE_SECOND_TIME_LSB, lsb); +} + +static void prv_read_ppg_data(HRMDevice *dev, HRMPPGData *data_out) { + uint8_t num_ppg_samples; + prv_read_register(HRM, ADDR_NUM_PPG_SAMPLES, &num_ppg_samples); + num_ppg_samples = MIN(num_ppg_samples, MAX_PPG_SAMPLES); + + for (int i = 0; i < num_ppg_samples; ++i) { + struct PACKED { + uint8_t idx; + uint16_t ppg; + uint16_t tia; + } ppg_reading; + + // Reading PPG data from the chip is a little weird. We need to read the PPG block of registers + // which maps to the ppg_reading struct above. We then need to verify that the index that we + // read matches the one that we expect. If we attempt to read the registers too quickly back to + // back that means that the AS7000 failed to update the value in time and we just need to try + // again. Limit this to a fixed number of attempts to make sure we don't infinite loop. + const int NUM_ATTEMPTS = 3; + bool success = false; + for (int j = 0; j < NUM_ATTEMPTS; ++j) { + prv_read_register_block(HRM, ADDR_PPG_IDX, &ppg_reading, sizeof(ppg_reading)); + if (ppg_reading.idx == i + 1) { + data_out->indexes[i] = ppg_reading.idx; + data_out->ppg[i] = ntohs(ppg_reading.ppg); + data_out->tia[i] = ntohs(ppg_reading.tia); + + success = true; + break; + } + + PPG_DBG_VERBOSE("FAIL: got %"PRIu16" expected %u tia %"PRIu16, + ppg_reading.idx, i + 1, ntohs(ppg_reading.tia)); + // Keep trying... + } + + if (!success) { + // We didn't find a sample, just give up on reading PPG for this handshake + break; + } + + data_out->num_samples++; + } + + PPG_DBG("num_samples reg: %"PRIu8" read: %u", + num_ppg_samples, data_out->num_samples); +} + +static void prv_write_accel_sample(HRMDevice *dev, uint8_t sample_idx, AccelRawData *data) { + struct PACKED { + uint8_t sample_idx; + net16 accel_x; + net16 accel_y; + net16 accel_z; + } sample_data = { + .sample_idx = sample_idx, + .accel_x = hton16(data->x * 2), // Accel service supplies mGs, AS7000 expects lsb = 0.5 mG + .accel_y = hton16(data->y * 2), + .accel_z = hton16(data->z * 2) + }; + prv_write_register_block(dev, ADDR_ACCEL_SAMPLE_IDX, &sample_data, sizeof(sample_data)); +} + +static void prv_read_hrm_data(HRMDevice *dev, HRMData *data) { + struct PACKED { + uint16_t led_current; + uint8_t hrm_status; + uint8_t bpm; + uint8_t sqi; + } hrm_data_regs; + + prv_read_register_block(dev, ADDR_LED_CURRENT_MSB, &hrm_data_regs, sizeof(hrm_data_regs)); + + data->led_current_ua = ntohs(hrm_data_regs.led_current); + data->hrm_status = hrm_data_regs.hrm_status; + data->hrm_bpm = hrm_data_regs.bpm; + + if (data->hrm_status & AS7000Status_NoAccel) { + data->hrm_quality = HRMQuality_NoAccel; + } else if (hrm_data_regs.sqi <= AS7000SQIThreshold_Excellent) { + data->hrm_quality = HRMQuality_Excellent; + } else if (hrm_data_regs.sqi <= AS7000SQIThreshold_Good) { + data->hrm_quality = HRMQuality_Good; + } else if (hrm_data_regs.sqi <= AS7000SQIThreshold_Acceptable) { + data->hrm_quality = HRMQuality_Acceptable; + } else if (hrm_data_regs.sqi <= AS7000SQIThreshold_Poor) { + data->hrm_quality = HRMQuality_Poor; + } else if (hrm_data_regs.sqi <= AS7000SQIThreshold_Worst) { + data->hrm_quality = HRMQuality_Worst; + } else if (hrm_data_regs.sqi == AS7000SQIThreshold_OffWrist) { + data->hrm_quality = HRMQuality_OffWrist; + } else { + data->hrm_quality = HRMQuality_NoSignal; + } +} + +// Sequence of events for handshake pulse (when in one-second burst mode): +// - [optional] Host writes the one-second time (registers 10,11) measured for the last 20 +// samples (about one second). +// - Host reads any data/HRV-result/LED-current, as needed (see registers [14...53]) +// - Host reads the HRM-result/SYNC-byte (registers [54...57]). +// If not in HRM-mode, the host can just read the SYNC-byte (register 57). +// Reading the SYNC-byte causes the AS7000 to release the handshake-signal +// and allows deep-sleep mode (if the AS7000 is configured for this). +// This step must be the last read for this handshake-pulse. +static void prv_handle_handshake_pulse(void *unused_data) { + PPG_DBG("Handshake handle"); + + mutex_lock(HRM->state->lock); + if (!hrm_is_enabled(HRM)) { + mutex_unlock(HRM->state->lock); + return; + } + + // We keep track of the number of handshakes so that we know when to expect samples + const bool should_expect_samples = (HRM->state->handshake_count > WARMUP_HANDSHAKES); + + HRMData data = (HRMData) {}; + + // Immediately read the PPG data. The timing constraints are pretty tight (we need to read this + // within 30ms~ of getting the handshake or else we'll lose PPG data). The other registers can + // be read at anytime before the next handshake, so it's ok to do this first. + prv_read_ppg_data(HRM, &data.ppg_data); + + if (should_expect_samples) { + interval_timer_take_sample(&s_handshake_interval_timer); + } + + // Send the accel data out to the AS7000 + HRMAccelData *accel_data = hrm_manager_get_accel_data(); + const uint8_t num_samples = accel_data->num_samples; + prv_write_register(HRM, ADDR_NUM_ACCEL_SAMPLES, num_samples); + for (uint32_t i = 0; i < num_samples; ++i) { + prv_write_accel_sample(HRM, i + 1, &accel_data->data[i]); + } + data.accel_data = *accel_data; + hrm_manager_release_accel_data(); + + // Read the rest of the HRM data fields. + prv_read_hrm_data(HRM, &data); + + // Handle the clock skew register + uint32_t average_handshake_interval_ms; + uint32_t num_intervals = interval_timer_get(&s_handshake_interval_timer, + &average_handshake_interval_ms); + // Try to write the register frequently early on, and then every half second to accommodate + // changes over time. + if (num_intervals == 2 || + num_intervals == 10 || + (num_intervals % 30) == 0) { + prv_set_host_one_second_time_register(HRM, average_handshake_interval_ms); + } + + // Read the SYNC byte to release handshake signal and enter deep sleep mode. + uint8_t unused; + prv_read_register(HRM, ADDR_SYNC, &unused); + + + PPG_DBG("Handshake handle done"); + HRM->state->handshake_count++; + + + PROFILER_NODE_STOP(hrm_handling); + mutex_unlock(HRM->state->lock); + + + // PPG_DBG log out each PPG data sample that we recorded + for (int i = 0; i < data.ppg_data.num_samples; i++) { + PPG_DBG_VERBOSE("idx %-2"PRIu8" ppg %-6"PRIu16" tia %-6"PRIu16, + data.ppg_data.indexes[i], data.ppg_data.ppg[i], data.ppg_data.tia[i]); + } + + hrm_manager_new_data_cb(&data); + + if (num_samples == 0 && should_expect_samples) { + analytics_inc(ANALYTICS_DEVICE_METRIC_HRM_ACCEL_DATA_MISSING, AnalyticsClient_System); + PBL_LOG(LOG_LEVEL_WARNING, "Falling behind: HRM got 0 accel samples"); + } + +} + +static void prv_as7000_interrupt_handler(bool *should_context_switch) { + PPG_DBG("Handshake interrupt"); + + PROFILER_NODE_START(hrm_handling); // Starting to respond to handshake toggle + + // Reset the watchdog counter + s_missing_interrupt_count = 0; + + *should_context_switch = new_timer_add_work_callback_from_isr(prv_handle_handshake_pulse, NULL); +} + +static void prv_interrupts_enable(HRMDevice *dev, bool enable) { + mutex_assert_held_by_curr_task(dev->state->lock, true); + exti_configure_pin(dev->handshake_int, ExtiTrigger_Falling, prv_as7000_interrupt_handler); + exti_enable(dev->handshake_int); +} + +static void prv_log_running_apps(HRMDevice *dev) { + uint8_t app_ids = 0; + if (!prv_read_register(dev, ADDR_APP_IDS, &app_ids)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to get running apps"); + return; + } + PBL_LOG(LOG_LEVEL_DEBUG, "Running applications:"); + if (app_ids == AS7000AppId_Idle) { + PBL_LOG(LOG_LEVEL_DEBUG, " - None (idle)"); + } else { + if (app_ids & AS7000AppId_Loader) { + PBL_LOG(LOG_LEVEL_DEBUG, " - Loader"); + } + if (app_ids & AS7000AppId_HRM) { + PBL_LOG(LOG_LEVEL_DEBUG, " - HRM"); + } + if (app_ids & AS7000AppId_PRV) { + PBL_LOG(LOG_LEVEL_DEBUG, " - PRV"); + } + if (app_ids & AS7000AppId_GSR) { + PBL_LOG(LOG_LEVEL_DEBUG, " - GSR"); + } + if (app_ids & AS7000AppId_NTC) { + PBL_LOG(LOG_LEVEL_DEBUG, " - NTC"); + } + } +} + +static bool prv_get_and_log_device_info(HRMDevice *dev, AS7000InfoRecord *info, + bool log_version) { + // get the device info + if (!prv_read_register_block(dev, ADDR_INFO_START, info, sizeof(AS7000InfoRecord))) { + return false; + } + + if (log_version) { + // print out the version information + PBL_LOG(LOG_LEVEL_INFO, "AS7000 enabled! Protocol v%" PRIu8 ".%" PRIu8 + ", SW v%" PRIu8 ".%" PRIu8 ".%" PRIu8 ", HW Rev %" PRIu8, + info->protocol_version_major, info->protocol_version_minor, + HRM_SW_VERSION_PART_MAJOR(info->sw_version_major), + HRM_SW_VERSION_PART_MINOR(info->sw_version_major), + info->sw_version_minor, info->hw_revision); + } + prv_log_running_apps(dev); + return true; +} + +static bool prv_is_app_running(HRMDevice *dev, AS7000AppId app) { + uint8_t running_apps = 0; + if (!prv_read_register(dev, ADDR_APP_IDS, &running_apps)) { + return false; + } + PBL_LOG(LOG_LEVEL_DEBUG, "Apps running: 0x%"PRIx8, running_apps); + if (app == AS7000AppId_Idle) { + // no apps should be running + return running_apps == AS7000AppId_Idle; + } + return running_apps & app; +} + +//! Set the applications that should be running on the HRM. +//! +//! This commands the HRM to start or continue running any apps whose +//! flags are set, and to stop all apps whose flags are unset. Depending +//! on the firmware loaded onto the HRM, multiple apps can be run +//! concurrently by setting the logical OR of the App IDs. +static bool prv_set_running_apps(HRMDevice *dev, AS7000AppId apps) { + return prv_write_register(dev, ADDR_APP_IDS, apps); +} + +// Wait for the INT line to go low. Return true if it went low before timing out +static bool prv_wait_int_low(HRMDevice *dev) { + const int max_attempts = 2000; + int attempt; + for (attempt = 0; attempt < max_attempts; attempt++) { + if (!gpio_input_read(&dev->int_gpio)) { + break; + } + system_task_watchdog_feed(); + psleep(1); + } + return (attempt < max_attempts); +} + +// Wait for the INT line to go high. Return true if it went high before timing out +static bool prv_wait_int_high(HRMDevice *dev) { + const int max_attempts = 300; + int attempt; + for (attempt = 0; attempt < max_attempts; attempt++) { + if (gpio_input_read(&dev->int_gpio)) { + break; + } + system_task_watchdog_feed(); + psleep(1); + } + return (attempt < max_attempts); +} + +// NOTE: the caller must hold the device's state lock +static void prv_disable(HRMDevice *dev) { + mutex_assert_held_by_curr_task(dev->state->lock, true); + + // Turn off our watchdog timer + prv_disable_watchdog(dev); + + // Make sure interrupts are fully disabled before changing state + prv_interrupts_enable(dev, false); + // Put the INT pin back into a low power state that won't interfere with jtag using the pin + gpio_analog_init(&dev->int_gpio); + + PBL_LOG(LOG_LEVEL_DEBUG, "Shutting down device."); + switch (dev->state->enabled_state) { + case HRMEnabledState_PoweringOn: + new_timer_stop(dev->state->timer); + // Delay a bit so that we don't deassert the enable GPIO while in + // the loader and unintentionally activate force loader mode. + psleep(LOADER_READY_MAX_DELAY_MS); + // fallthrough + case HRMEnabledState_Enabled: + gpio_output_set(&dev->en_gpio, false); + dev->state->enabled_state = HRMEnabledState_Disabled; + break; + case HRMEnabledState_Disabled: + // nothing to do + break; + case HRMEnabledState_Uninitialized: + // the lock isn't even created yet - should never get here + // fallthrough + default: + WTF; + } + led_disable(LEDEnablerHRM); + analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME); +} + +// NOTE: the caller must hold the device's state lock +static void prv_enable(HRMDevice *dev) { + mutex_assert_held_by_curr_task(dev->state->lock, true); + if (dev->state->enabled_state == HRMEnabledState_Uninitialized) { + PBL_LOG(LOG_LEVEL_ERROR, "Trying to enable HRM before initialization."); + + } else if (dev->state->enabled_state == HRMEnabledState_Disabled) { + led_enable(LEDEnablerHRM); + analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME, AnalyticsClient_System); + + // Enable the device and schedule a timer callback for when we can start communicating with it. + gpio_output_set(&dev->en_gpio, true); + dev->state->enabled_state = HRMEnabledState_PoweringOn; + dev->state->handshake_count = 0; + new_timer_start(dev->state->timer, NORMAL_BOOT_DELAY_MS, prv_enable_timer_cb, (void *)dev, + 0 /* flags */); + + interval_timer_init(&s_handshake_interval_timer, 900, 1100, 8); + + PBL_LOG(LOG_LEVEL_DEBUG, "Enabling AS7000..."); + } +} + +// This system task callback is triggered by the watchdog interrupt handler when we detect +// a frozen sensor +static void prv_watchdog_timer_system_cb(void *data) { + HRMDevice *dev = (HRMDevice *)data; + mutex_lock(dev->state->lock); + if (dev->state->enabled_state != HRMEnabledState_Enabled) { + goto exit; + } + + // If we have gone too long without getting an interrupt, let's reset the device + if (s_missing_interrupt_count >= AS7000_MAX_WATCHDOG_INTERRUPTS) { + PBL_LOG(LOG_LEVEL_ERROR, "Watchdog logic detected frozen sensor. Resetting now."); + analytics_inc(ANALYTICS_DEVICE_METRIC_HRM_WATCHDOG_TIMEOUT, AnalyticsClient_System); + prv_disable(dev); + psleep(SHUT_DOWN_DELAY_MS); + prv_enable(dev); + } +exit: + mutex_unlock(dev->state->lock); +} + +// This regular timer callback executes once a second. It is part of the watchdog logic used to +// detect if the sensor becomes unresponsive. +static void prv_watchdog_timer_cb(void *data) { + HRMDevice *dev = (HRMDevice *)data; + if (++s_missing_interrupt_count >= AS7000_MAX_WATCHDOG_INTERRUPTS) { + system_task_add_callback(prv_watchdog_timer_system_cb, (void *)dev); + } + if (s_missing_interrupt_count > 1) { + PBL_LOG(LOG_LEVEL_DEBUG, "Missing interrupt count: %"PRIu8" ", s_missing_interrupt_count); + } +} + +// Enable the watchdog timer. This gets enabled when we enable the sensor and detects if +// the sensor stops generating interrupts. +static void prv_enable_watchdog(HRMDevice *dev) { + mutex_assert_held_by_curr_task(dev->state->lock, true); + s_as7000_watchdog_timer = (RegularTimerInfo) { + .cb = prv_watchdog_timer_cb, + .cb_data = (void *)dev, + }; + s_missing_interrupt_count = 0; + regular_timer_add_seconds_callback(&s_as7000_watchdog_timer); +} + +static void prv_disable_watchdog(HRMDevice *dev) { + mutex_assert_held_by_curr_task(dev->state->lock, true); + regular_timer_remove_callback(&s_as7000_watchdog_timer); + s_missing_interrupt_count = 0; +} + +static bool prv_start_loader(HRMDevice *dev) { + // check if the loader is already running + if (!prv_is_app_running(dev, AS7000AppId_Loader)) { + PBL_LOG(LOG_LEVEL_DEBUG, "Switching to loader"); + // we need to start the loader + if (!prv_set_running_apps(dev, AS7000AppId_Loader)) { + return false; + } + psleep(35); + + // make sure the loader is running + if (!prv_is_app_running(dev, AS7000AppId_Loader)) { + return false; + } + } + prv_log_running_apps(dev); + return true; +} + +static uint64_t prv_get_time_ms(void) { + time_t time_s; + uint16_t time_ms; + rtc_get_time_ms(&time_s, &time_ms); + return ((uint64_t)time_s) * 1000 + time_ms; +} + +static bool prv_wait_for_loader_ready(HRMDevice *dev) { + uint64_t end_time_ms = prv_get_time_ms() + LOADER_READY_MAX_DELAY_MS; + + do { + uint8_t status = 0; + if (!prv_read_register(dev, ADDR_LOADER_STATUS, &status)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed reading status"); + return false; + } + + if (status == AS7000LoaderStatus_Ready) { + // ready + return true; + } else if ((status != AS7000LoaderStatus_Busy1) && (status != AS7000LoaderStatus_Busy2)) { + // error + PBL_LOG(LOG_LEVEL_ERROR, "Error status: %"PRIx8, status); + return false; + } + psleep(1); + } while (prv_get_time_ms() < end_time_ms); + + PBL_LOG(LOG_LEVEL_ERROR, "Timed out waiting for the loader to be ready!"); + return false; +} + +static bool prv_flash_fw(HRMDevice *dev) { + // switch to the loader + if (!prv_start_loader(dev)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to start loader"); + return false; + } + + // wait for the loader to be ready + if (!prv_wait_for_loader_ready(dev)) { + PBL_LOG(LOG_LEVEL_ERROR, "Loader not ready"); + return false; + } + + const uint32_t image_length = + resource_size(SYSTEM_APP, RESOURCE_ID_AS7000_FW_IMAGE); + PBL_ASSERTN(image_length); + PBL_LOG(LOG_LEVEL_DEBUG, + "Loading FW image (%"PRIu32" bytes encoded)", image_length); + // Skip over the image header. + uint32_t cursor = sizeof(AS7000FWUpdateHeader); + while (cursor < image_length) { + // Make sure we can load enough data for a valid segment. There is + // always at least one data byte in each segment, so there must be + // strictly more data to read past the end of the header. + PBL_ASSERTN((image_length - cursor) > sizeof(AS7000FWSegmentHeader)); + // Read the header. + AS7000FWSegmentHeader segment_header; + if (resource_load_byte_range_system( + SYSTEM_APP, RESOURCE_ID_AS7000_FW_IMAGE, cursor, + (uint8_t *)&segment_header, sizeof(segment_header)) != + sizeof(segment_header)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to read FW image! " + "(segment header @ 0x%" PRIx32 ")", cursor); + return false; + } + cursor += sizeof(segment_header); + // Write all the data bytes in the segment to the HRM. + uint16_t write_address = segment_header.address; + uint32_t bytes_remaining = segment_header.len_minus_1 + 1; + while (bytes_remaining) { + uint8_t chunk[MAX_HEX_DATA_BYTES]; + const size_t load_length = MIN(MAX_HEX_DATA_BYTES, bytes_remaining); + if (resource_load_byte_range_system( + SYSTEM_APP, RESOURCE_ID_AS7000_FW_IMAGE, cursor, chunk, load_length) + != load_length) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to read FW image! " + "(segment data @ 0x%" PRIx32 ")", cursor); + return false; + } + + // Encode the chunk into an Intel HEX record and send it to the + // AS7000 loader. + uint8_t data_record[IHEX_RECORD_LENGTH(MAX_HEX_DATA_BYTES)]; + ihex_encode(data_record, IHEX_TYPE_DATA, write_address, + chunk, load_length); + if (!prv_write_register_block(dev, ADDR_LOADER_STATUS, data_record, + IHEX_RECORD_LENGTH(load_length))) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to write hex record"); + return false; + } + + // Wait for the loader to be ready, indicating that the last + // record was successfully written. + if (!prv_wait_for_loader_ready(dev)) { + PBL_LOG(LOG_LEVEL_ERROR, "Loader not ready"); + return false; + } + + system_task_watchdog_feed(); + + cursor += load_length; + write_address += load_length; + bytes_remaining -= load_length; + } + } + + // Write the EOF record, telling the loader that the image has been + // fully written. + uint8_t eof_record[IHEX_RECORD_LENGTH(0)]; + ihex_encode(eof_record, IHEX_TYPE_EOF, 0, NULL, 0); + if (!prv_write_register_block(dev, ADDR_LOADER_STATUS, + eof_record, IHEX_RECORD_LENGTH(0))) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to write EOF record"); + return false; + } + + return true; +} + +static bool prv_set_accel_sample_frequency(HRMDevice *dev, uint16_t freq) { + const uint8_t msb = (freq >> 8) & 0xff; + const uint8_t lsb = freq & 0xff; + return prv_write_register(dev, ADDR_ACCEL_SAMPLE_FREQ_MSB, msb) + && prv_write_register(dev, ADDR_ACCEL_SAMPLE_FREQ_LSB, lsb); +} + +static void prv_enable_system_task_cb(void *context) { + HRMDevice *dev = context; + mutex_lock(dev->state->lock); + if (dev->state->enabled_state == HRMEnabledState_Disabled) { + // Enable was cancelled before this callback fired. + goto done; + } else if (dev->state->enabled_state != HRMEnabledState_PoweringOn) { + PBL_LOG(LOG_LEVEL_ERROR, "Enable KernelBG callback fired while HRM was in " + "an unexpected state: %u", (unsigned int)dev->state->enabled_state); + WTF; + } + + AS7000InfoRecord info; + if (!prv_get_and_log_device_info(dev, &info, false /* log_version */)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to query AS7000 device info"); + goto failed; + } + + if (info.application_id == AS7000AppId_Loader) { + // This shouldn't happen. The application firmware should have been + // flashed during boot. + PBL_LOG(LOG_LEVEL_ERROR, + "AS7000 booted into loader! Something is very wrong."); + goto failed; + } + + // check that we can communicate with this chip + if (info.protocol_version_major != EXPECTED_PROTOCOL_VERSION_MAJOR) { + // we don't know how to talk with this chip, so bail + PBL_LOG(LOG_LEVEL_ERROR, "Unexpected protocol version!"); + goto failed; + } + + if (info.application_id != AS7000AppId_Idle) { + PBL_LOG(LOG_LEVEL_ERROR, + "Unexpected application running: 0x%" PRIx8, info.application_id); + goto failed; + } + + // the INT line should be low + if (gpio_input_read(&dev->int_gpio)) { + PBL_LOG(LOG_LEVEL_ERROR, "INT line is not low!"); + goto failed; + } + + // Set the accelerometer sample frequency + PBL_LOG(LOG_LEVEL_DEBUG, "Setting accel frequency"); + PBL_ASSERTN(HRM_MANAGER_ACCEL_RATE_MILLIHZ >= 10000 && HRM_MANAGER_ACCEL_RATE_MILLIHZ <= 20000); + if (!prv_set_accel_sample_frequency(dev, HRM_MANAGER_ACCEL_RATE_MILLIHZ)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set accel frequency"); + goto failed; + } + + // Set the presence detection threshold + uint8_t pres_detect_thrsh; + WatchInfoColor model_color = mfg_info_get_watch_color(); + switch (model_color) { + case WATCH_INFO_COLOR_PEBBLE_2_HR_BLACK: + case WATCH_INFO_COLOR_PEBBLE_2_HR_FLAME: + pres_detect_thrsh = PRES_DETECT_THRSH_BLACK; + break; + case WATCH_INFO_COLOR_PEBBLE_2_HR_WHITE: + case WATCH_INFO_COLOR_PEBBLE_2_HR_LIME: + case WATCH_INFO_COLOR_PEBBLE_2_HR_AQUA: + pres_detect_thrsh = PRES_DETECT_THRSH_WHITE; + break; + default: + pres_detect_thrsh = 1; + break; + } + if (!prv_write_register(dev, ADDR_PRES_DETECT_THRSH, pres_detect_thrsh)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set presence detection threshold"); + goto failed; + } + + // start the HRM app + PBL_LOG(LOG_LEVEL_DEBUG, "Starting HRM app"); + if (!prv_set_running_apps(dev, AS7000AppId_HRM)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to start HRM app!"); + goto failed; + } + + // Configure the int_gpio pin only when we're going to use it, as this pin is shared with + // the jtag pins and therefore can cause issues when flashing firmwares onto bigboards. + gpio_input_init_pull_up_down(&dev->int_gpio, GPIO_PuPd_UP); + + // wait for the INT line to go high indicating the Idle app has ended + if (!prv_wait_int_high(dev)) { + PBL_LOG(LOG_LEVEL_ERROR, "Timed-out waiting for the Idle app to end but we " + "probably just missed it"); + // TODO: The line only goes high for a few ms. If there is any kind of context switch while we + // wait for the line to go high we will miss this. Let's fix this the right way in PBL-41812 + // (check for this change via an ISR) for 4.2 but just go with the smallest change for 4.1 + } + + // wait for the INT line to go low indicating the HRM app is ready + if (!prv_wait_int_low(dev)) { + PBL_LOG(LOG_LEVEL_ERROR, "Timed-out waiting for the HRM app to be ready"); + goto failed; + } + + // get the running apps (also triggers the app to start) + prv_log_running_apps(dev); + + // HRM app is ready, enable handshake interrupts + prv_interrupts_enable(dev, true); + + // We are now fully enabled + dev->state->enabled_state = HRMEnabledState_Enabled; + + // Enable the watchdog + prv_enable_watchdog(dev); + + goto done; + +failed: + prv_disable(dev); +done: + mutex_unlock(dev->state->lock); +} + +static void prv_enable_timer_cb(void *context) { + system_task_add_callback(prv_enable_system_task_cb, context); +} + +void hrm_init(HRMDevice *dev) { + PBL_ASSERTN(dev->state->enabled_state == HRMEnabledState_Uninitialized); + + dev->state->lock = mutex_create(); + dev->state->timer = new_timer_create(); + dev->state->enabled_state = HRMEnabledState_Disabled; + + // Boot up the HRM so that we can read off the firmware version to see + // if it needs to be updated. + + // First, read the version from the firmware update resource. + const uint32_t update_length = resource_size( + SYSTEM_APP, RESOURCE_ID_AS7000_FW_IMAGE); + if (update_length == 0) { + // We don't have a firmware to write so there's no point in booting + // the HRM. + PBL_LOG(LOG_LEVEL_DEBUG, "No HRM FW update available"); + return; + } + + AS7000FWUpdateHeader image_header; + if (resource_load_byte_range_system( + SYSTEM_APP, RESOURCE_ID_AS7000_FW_IMAGE, 0, (uint8_t *)&image_header, + sizeof(image_header)) != sizeof(image_header)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to read HRM FW image header!"); + return; + } + PBL_LOG(LOG_LEVEL_DEBUG, "FW update image is v%" PRIu8 ".%" PRIu8 ".%" PRIu8, + HRM_SW_VERSION_PART_MAJOR(image_header.sw_version_major), + HRM_SW_VERSION_PART_MINOR(image_header.sw_version_major), + image_header.sw_version_minor); + + // Now that we know what version the image is, actually boot up the + // HRM so we can read off the version. + + PBL_LOG(LOG_LEVEL_DEBUG, "Booting AS7000..."); + + gpio_output_init(&dev->en_gpio, GPIO_OType_PP, GPIO_Speed_2MHz); +#if HRM_FORCE_FLASH + // Force the HRM into loader mode which will cause the firmware to be + // reflashed on every boot. If the HRM is loaded with a broken + // firmware which doesn't enter standby when the enable pin is high, + // the board will need to be power-cycled (entering standby/shutdown + // is sufficient) in order to get force-flashing to succeed. + gpio_output_set(&dev->en_gpio, false); + psleep(50); + gpio_output_set(&dev->en_gpio, true); + psleep(20); + gpio_output_set(&dev->en_gpio, false); + psleep(20); +#else + gpio_output_set(&dev->en_gpio, true); + psleep(NORMAL_BOOT_DELAY_MS); +#endif + + AS7000InfoRecord hrm_info; + if (!prv_get_and_log_device_info(dev, &hrm_info, true /* log_version */)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to read AS7000 version info!"); + goto cleanup; + } + + if (hrm_info.application_id == AS7000AppId_Loader || + hrm_info.sw_version_major != image_header.sw_version_major || + hrm_info.sw_version_minor != image_header.sw_version_minor) { + // We technically could leave the firmware on the HRM alone if the + // minor version in the chip is newer than in the update image, but + // for sanity's sake let's always make sure the HRM firmware is in + // sync with the version shipped with the Pebble firmware. + PBL_LOG(LOG_LEVEL_DEBUG, "AS7000 firmware version mismatch. Flashing..."); + if (!prv_flash_fw(dev)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to flash firmware"); + goto cleanup; + } + // We need to wait for the HRM to reboot into the application before + // releasing the enable GPIO. If the loader sees the GPIO released + // during boot, it will activate "force loader mode" and fall back + // into the loader. Since we're waiting anyway, we might as well + // query the version info again to make sure the update took. + PBL_LOG(LOG_LEVEL_DEBUG, "Firmware flashed! Waiting for reboot..."); + gpio_output_set(&dev->en_gpio, true); + psleep(LOADER_REBOOT_DELAY_MS); + if (!prv_get_and_log_device_info(dev, &hrm_info, true /* log_version */)) { + PBL_LOG(LOG_LEVEL_ERROR, + "Failed to read AS7000 version info after flashing!"); + goto cleanup; + } + } else { + PBL_LOG(LOG_LEVEL_DEBUG, "AS7000 firmware is up to date."); + } + +cleanup: + // At this point the HRM should either be booted and running the + // application firmware, at which point deasserting the enable GPIO + // will signal it to shut down, or the firmware update failed and the + // loader is running, where deasserting the GPIO will not do much. + gpio_output_set(&dev->en_gpio, false); +} + +void hrm_enable(HRMDevice *dev) { + if (!dev->state->lock) { + PBL_LOG(LOG_LEVEL_DEBUG, "Not an HRM Device."); + return; + } + + mutex_lock(dev->state->lock); + prv_enable(dev); + mutex_unlock(dev->state->lock); +} + +void hrm_disable(HRMDevice *dev) { + if (!dev->state->lock) { + PBL_LOG(LOG_LEVEL_DEBUG, "Not an HRM Device."); + return; + } + + mutex_lock(dev->state->lock); + prv_disable(dev); + mutex_unlock(dev->state->lock); +} + +bool hrm_is_enabled(HRMDevice *dev) { + return (dev->state->enabled_state == HRMEnabledState_Enabled + || dev->state->enabled_state == HRMEnabledState_PoweringOn); +} + +void as7000_get_version_info(HRMDevice *dev, AS7000InfoRecord *info_out) { + if (!dev->state->lock) { + PBL_LOG(LOG_LEVEL_DEBUG, "Not an HRM Device."); + return; + } + + mutex_lock(dev->state->lock); + if (!prv_get_and_log_device_info(dev, info_out, true /* log_version */)) { + PBL_LOG(LOG_LEVEL_WARNING, "Failed to read AS7000 version info"); + } + mutex_unlock(dev->state->lock); +} + +// Prompt Commands +// =============== + +#include "console/prompt.h" +#include + +void command_hrm_wipe(void) { + // HEX records to write 0xFFFFFFFF to the magic number region. + const char *erase_magic_record = ":047FFC00FFFFFFFF85"; + const char *eof_record = ":00000001FF"; + + mutex_lock(HRM->state->lock); + gpio_output_set(&HRM->en_gpio, true); + psleep(NORMAL_BOOT_DELAY_MS); + + bool success = prv_start_loader(HRM) && + prv_wait_for_loader_ready(HRM) && + prv_write_register_block(HRM, ADDR_LOADER_STATUS, + erase_magic_record, + strlen(erase_magic_record)) && + prv_wait_for_loader_ready(HRM) && + prv_write_register_block(HRM, ADDR_LOADER_STATUS, + eof_record, strlen(eof_record)) && + prv_wait_for_loader_ready(HRM); + + gpio_output_set(&HRM->en_gpio, false); + mutex_unlock(HRM->state->lock); + + prompt_send_response(success? "HRM Firmware invalidated" : "ERROR"); +} + +// Simulate a frozen sensor for testing the watchdog recovery logic +void command_hrm_freeze(void) { + HRMDevice *dev = HRM; + mutex_lock(dev->state->lock); + if (dev->state->enabled_state == HRMEnabledState_Enabled) { + prv_interrupts_enable(dev, false); + gpio_analog_init(&dev->int_gpio); + led_disable(LEDEnablerHRM); + } + mutex_unlock(dev->state->lock); +} diff --git a/src/fw/drivers/hrm/as7000.h b/src/fw/drivers/hrm/as7000.h index 1e6eadda..b15b3d34 100644 --- a/src/fw/drivers/hrm/as7000.h +++ b/src/fw/drivers/hrm/as7000.h @@ -48,3 +48,16 @@ typedef const struct HRMDevice { OutputConfig en_gpio; I2CSlavePort *i2c_slave; } HRMDevice; + +typedef struct PACKED AS7000InfoRecord { + uint8_t protocol_version_major; + uint8_t protocol_version_minor; + uint8_t sw_version_major; + uint8_t sw_version_minor; + uint8_t application_id; + uint8_t hw_revision; +} AS7000InfoRecord; + +//! Fills a struct which contains version info about the AS7000 +//! This should probably only be used by the HRM Demo app +void as7000_get_version_info(HRMDevice *dev, AS7000InfoRecord *info_out); diff --git a/src/fw/drivers/wscript_build b/src/fw/drivers/wscript_build index d5a0b212..f13c02d9 100644 --- a/src/fw/drivers/wscript_build +++ b/src/fw/drivers/wscript_build @@ -1174,4 +1174,18 @@ if mcu_family in ('STM32F2', 'STM32F4', 'STM32F7'): ], ) +if bld.get_hrm() == 'AS7000': + bld.objects( + name='driver_hrm', + source=[ + 'hrm/as7000.c', + ], + use=[ + 'driver_gpio', + 'driver_i2c', + 'fw_includes', + 'stm32_stlib' + ], + ) + # vim:filetype=python diff --git a/src/fw/shell/normal/system_app_registry_list.json b/src/fw/shell/normal/system_app_registry_list.json index a5bf7505..c650ea04 100644 --- a/src/fw/shell/normal/system_app_registry_list.json +++ b/src/fw/shell/normal/system_app_registry_list.json @@ -533,6 +533,12 @@ "spalding" ] }, + { + "id": -89, + "enum": "HRM_DEMO", + "md_fn": "hrm_demo_get_app_info", + "ifdefs": ["SHOW_ACTIVITY_DEMO", "CAPABILITY_HAS_BUILTIN_HRM=1"] + }, { "id": -90, "enum": "REMINDERS", diff --git a/tests/overrides/default/resources/robert/resource/resource_ids.auto.h b/tests/overrides/default/resources/robert/resource/resource_ids.auto.h index 1ca72dd8..3f48d5f3 100644 --- a/tests/overrides/default/resources/robert/resource/resource_ids.auto.h +++ b/tests/overrides/default/resources/robert/resource/resource_ids.auto.h @@ -491,6 +491,7 @@ typedef enum { RESOURCE_ID_STORED_APP_GOLF = 464, RESOURCE_ID_BT_BOOT_IMAGE = 465, RESOURCE_ID_BT_FW_IMAGE = 466, + RESOURCE_ID_AS7000_FW_IMAGE = 467, RESOURCE_ID_TIMEZONE_DATABASE = 468, RESOURCE_ID_ACTION_BAR_ICON_CHECK = 469, RESOURCE_ID_GENERIC_WARNING_LARGE = 470, diff --git a/tests/overrides/default/resources/silk/resource/resource_ids.auto.h b/tests/overrides/default/resources/silk/resource/resource_ids.auto.h index 46005837..f2010f7f 100644 --- a/tests/overrides/default/resources/silk/resource/resource_ids.auto.h +++ b/tests/overrides/default/resources/silk/resource/resource_ids.auto.h @@ -490,6 +490,7 @@ typedef enum { RESOURCE_ID_STORED_APP_GOLF = 463, RESOURCE_ID_BT_BOOT_IMAGE = 464, RESOURCE_ID_BT_FW_IMAGE = 465, + RESOURCE_ID_AS7000_FW_IMAGE = 466, RESOURCE_ID_TIMEZONE_DATABASE = 467, RESOURCE_ID_FONT_FALLBACK_INTERNAL = 468, RESOURCE_ID_ARROW_DOWN = 469,