mirror of
https://github.com/google/pebble.git
synced 2025-03-15 08:41:21 +00:00
fw: drivers: add AS7000 HRM driver code/demo app
This commit is contained in:
parent
f19ace2e3c
commit
5b5d49cb49
15 changed files with 1809 additions and 6 deletions
|
@ -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
|
||||
|
|
395
src/fw/apps/system_apps/hrm_demo.c
Normal file
395
src/fw/apps/system_apps/hrm_demo.c
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 },
|
||||
|
|
130
src/fw/drivers/hrm/README_AS7000.md
Normal file
130
src/fw/drivers/hrm/README_AS7000.md
Normal 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
1120
src/fw/drivers/hrm/as7000.c
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue