fw: drivers: add AS7000 HRM driver code/demo app

This commit is contained in:
Liam McLoughlin 2025-02-13 11:40:38 +00:00
parent f19ace2e3c
commit 5b5d49cb49
15 changed files with 1809 additions and 6 deletions

View file

@ -140,7 +140,12 @@ def has_touch(ctx):
@conf @conf
def get_hrm(ctx): def get_hrm(ctx):
return None if is_robert(ctx):
return "AS7000"
elif is_silk(ctx):
return "AS7000"
else:
return None
@conf @conf

View file

@ -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 <stdio.h>
#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;
}

View file

@ -165,12 +165,11 @@ static const CertificationIds * prv_get_certification_ids(void) {
#elif defined(BOARD_SPALDING) || defined(BOARD_SPALDING_EVT) #elif defined(BOARD_SPALDING) || defined(BOARD_SPALDING_EVT)
return &s_certification_ids_spalding; return &s_certification_ids_spalding;
#elif PLATFORM_SILK && !defined(IS_BIGBOARD) #elif PLATFORM_SILK && !defined(IS_BIGBOARD)
// TODO: remove force-false if (mfg_info_is_hrm_present()) {
// if (mfg_info_is_hrm_present()) { return &s_certification_ids_silk_hr;
// return &s_certification_ids_silk_hr; } else {
// } else {
return &s_certification_ids_silk; return &s_certification_ids_silk;
// } }
#else #else
return &s_certification_ids_fallback; return &s_certification_ids_fallback;
#endif #endif

View file

@ -19,6 +19,7 @@
#include "drivers/display/ice40lp/ice40lp_definitions.h" #include "drivers/display/ice40lp/ice40lp_definitions.h"
#include "drivers/exti.h" #include "drivers/exti.h"
#include "drivers/flash/qspi_flash_definitions.h" #include "drivers/flash/qspi_flash_definitions.h"
#include "drivers/hrm/as7000.h"
#include "drivers/i2c_definitions.h" #include "drivers/i2c_definitions.h"
#include "drivers/mic/stm32/dfsdm_definitions.h" #include "drivers/mic/stm32/dfsdm_definitions.h"
#include "drivers/pmic.h" #include "drivers/pmic.h"
@ -292,6 +293,51 @@ static const I2CBus I2C_TOUCH_ALS_BUS = {
}; };
#endif #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 #if BOARD_CUTTS_BB
static I2CBusState I2C_NFC_BUS_STATE = {}; static I2CBusState I2C_NFC_BUS_STATE = {};
@ -388,9 +434,17 @@ static const I2CSlavePort I2C_SLAVE_MAG3110 = {
.address = 0x0e << 1 .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_MAX14690 = &I2C_SLAVE_MAX14690;
I2CSlavePort * const I2C_MAG3110 = &I2C_SLAVE_MAG3110; 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_EV, i2c_hal_event_irq_handler, &I2C_PMIC_MAG_BUS);
IRQ_MAP(I2C4_ER, i2c_hal_error_irq_handler, &I2C_PMIC_MAG_BUS); IRQ_MAP(I2C4_ER, i2c_hal_error_irq_handler, &I2C_PMIC_MAG_BUS);
#if BOARD_CUTTS_BB #if BOARD_CUTTS_BB
@ -399,6 +453,24 @@ IRQ_MAP(I2C1_ER, i2c_hal_error_irq_handler, &I2C_TOUCH_ALS_BUS);
#endif #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 #if BOARD_CUTTS_BB
static const TouchSensor EWD1000_DEVICE = { static const TouchSensor EWD1000_DEVICE = {
.i2c = &I2C_SLAVE_EWD1000, .i2c = &I2C_SLAVE_EWD1000,
@ -745,6 +817,7 @@ void board_init(void) {
i2c_init(&I2C_TOUCH_ALS_BUS); i2c_init(&I2C_TOUCH_ALS_BUS);
i2c_init(&I2C_NFC_BUS); i2c_init(&I2C_NFC_BUS);
#endif #endif
i2c_init(&I2C_HRM_BUS);
i2c_init(&I2C_PMIC_MAG_BUS); i2c_init(&I2C_PMIC_MAG_BUS);
spi_slave_port_init(BMI160_SPI); spi_slave_port_init(BMI160_SPI);

View file

@ -293,6 +293,7 @@ extern SPISlavePort * const BMI160_SPI;
extern I2CSlavePort * const I2C_MAX14690; extern I2CSlavePort * const I2C_MAX14690;
extern I2CSlavePort * const I2C_MAG3110; extern I2CSlavePort * const I2C_MAG3110;
extern I2CSlavePort * const I2C_AS7000;
extern VoltageMonitorDevice * const VOLTAGE_MONITOR_ALS; extern VoltageMonitorDevice * const VOLTAGE_MONITOR_ALS;
extern VoltageMonitorDevice * const VOLTAGE_MONITOR_BATTERY; extern VoltageMonitorDevice * const VOLTAGE_MONITOR_BATTERY;
@ -307,6 +308,8 @@ extern SPISlavePort * const DIALOG_SPI;
extern MicDevice * const MIC; extern MicDevice * const MIC;
extern HRMDevice * const HRM;
#if BOARD_CUTTS_BB #if BOARD_CUTTS_BB
extern TouchSensor * const EWD1000; extern TouchSensor * const EWD1000;
#endif #endif

View file

@ -18,6 +18,7 @@
#include "drivers/exti.h" #include "drivers/exti.h"
#include "drivers/flash/qspi_flash_definitions.h" #include "drivers/flash/qspi_flash_definitions.h"
#include "drivers/hrm/as7000.h"
#include "drivers/i2c_definitions.h" #include "drivers/i2c_definitions.h"
#include "drivers/mic/stm32/dfsdm_definitions.h" #include "drivers/mic/stm32/dfsdm_definitions.h"
#include "drivers/qspi_definitions.h" #include "drivers/qspi_definitions.h"
@ -267,7 +268,13 @@ static const I2CSlavePort I2C_SLAVE_AS3701B = {
.address = 0x80 .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_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_EV, i2c_hal_event_irq_handler, &I2C_PMIC_HRM_BUS);
IRQ_MAP(I2C3_ER, i2c_hal_error_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; 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 // QSPI
static QSPIPortState s_qspi_port_state; static QSPIPortState s_qspi_port_state;
static QSPIPort QSPI_PORT = { static QSPIPort QSPI_PORT = {

View file

@ -229,12 +229,15 @@ extern UARTDevice * const BT_TX_BOOTROM_UART;
extern UARTDevice * const BT_RX_BOOTROM_UART; extern UARTDevice * const BT_RX_BOOTROM_UART;
extern I2CSlavePort * const I2C_AS3701B; extern I2CSlavePort * const I2C_AS3701B;
extern I2CSlavePort * const I2C_AS7000;
extern const VoltageMonitorDevice * VOLTAGE_MONITOR_ALS; extern const VoltageMonitorDevice * VOLTAGE_MONITOR_ALS;
extern const VoltageMonitorDevice * VOLTAGE_MONITOR_BATTERY; extern const VoltageMonitorDevice * VOLTAGE_MONITOR_BATTERY;
extern const TemperatureSensor * const TEMPERATURE_SENSOR; extern const TemperatureSensor * const TEMPERATURE_SENSOR;
extern HRMDevice * const HRM;
extern QSPIPort * const QSPI; extern QSPIPort * const QSPI;
extern QSPIFlash * const QSPI_FLASH; extern QSPIFlash * const QSPI_FLASH;

View file

@ -268,6 +268,8 @@ extern void command_low_power_debug(char *enable_arg);
extern void command_audit_delay_us(void); extern void command_audit_delay_us(void);
extern void command_enter_stop(void); extern void command_enter_stop(void);
extern void dialog_test_cmds(void);
extern void command_dump_notif_pref_db(void); extern void command_dump_notif_pref_db(void);
extern void command_bt_conn_param_set( 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_start(char *tx_channel);
extern void command_btle_unmod_tx_stop(void); 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 #if MFG_INFO_RECORDS_TEST_RESULTS
extern void command_mfg_info_test_results(void); extern void command_mfg_info_test_results(void);
#endif #endif
@ -467,6 +475,12 @@ static const Command s_prompt_commands[] = {
#endif // PLATFORM_TINTIN #endif // PLATFORM_TINTIN
#endif // RECOVERY_FW #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 #if CAPABILITY_HAS_ACCESSORY_CONNECTOR
{ "accessory power", command_accessory_power_set, 1 }, { "accessory power", command_accessory_power_set, 1 },
{ "accessory stress", command_accessory_stress_test, 0 }, { "accessory stress", command_accessory_stress_test, 0 },

View file

@ -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.

1120
src/fw/drivers/hrm/as7000.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -48,3 +48,16 @@ typedef const struct HRMDevice {
OutputConfig en_gpio; OutputConfig en_gpio;
I2CSlavePort *i2c_slave; I2CSlavePort *i2c_slave;
} HRMDevice; } 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);

View file

@ -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 # vim:filetype=python

View file

@ -533,6 +533,12 @@
"spalding" "spalding"
] ]
}, },
{
"id": -89,
"enum": "HRM_DEMO",
"md_fn": "hrm_demo_get_app_info",
"ifdefs": ["SHOW_ACTIVITY_DEMO", "CAPABILITY_HAS_BUILTIN_HRM=1"]
},
{ {
"id": -90, "id": -90,
"enum": "REMINDERS", "enum": "REMINDERS",

View file

@ -491,6 +491,7 @@ typedef enum {
RESOURCE_ID_STORED_APP_GOLF = 464, RESOURCE_ID_STORED_APP_GOLF = 464,
RESOURCE_ID_BT_BOOT_IMAGE = 465, RESOURCE_ID_BT_BOOT_IMAGE = 465,
RESOURCE_ID_BT_FW_IMAGE = 466, RESOURCE_ID_BT_FW_IMAGE = 466,
RESOURCE_ID_AS7000_FW_IMAGE = 467,
RESOURCE_ID_TIMEZONE_DATABASE = 468, RESOURCE_ID_TIMEZONE_DATABASE = 468,
RESOURCE_ID_ACTION_BAR_ICON_CHECK = 469, RESOURCE_ID_ACTION_BAR_ICON_CHECK = 469,
RESOURCE_ID_GENERIC_WARNING_LARGE = 470, RESOURCE_ID_GENERIC_WARNING_LARGE = 470,

View file

@ -490,6 +490,7 @@ typedef enum {
RESOURCE_ID_STORED_APP_GOLF = 463, RESOURCE_ID_STORED_APP_GOLF = 463,
RESOURCE_ID_BT_BOOT_IMAGE = 464, RESOURCE_ID_BT_BOOT_IMAGE = 464,
RESOURCE_ID_BT_FW_IMAGE = 465, RESOURCE_ID_BT_FW_IMAGE = 465,
RESOURCE_ID_AS7000_FW_IMAGE = 466,
RESOURCE_ID_TIMEZONE_DATABASE = 467, RESOURCE_ID_TIMEZONE_DATABASE = 467,
RESOURCE_ID_FONT_FALLBACK_INTERNAL = 468, RESOURCE_ID_FONT_FALLBACK_INTERNAL = 468,
RESOURCE_ID_ARROW_DOWN = 469, RESOURCE_ID_ARROW_DOWN = 469,