下面将逐一介绍,从max30102驱动开始,最终实现心率数据向安卓手机上传,完成一个BLE应用。
FR801xH SDK 的架构如下图所示。 SDK 包含了完整的 BLE 5.0 协议栈, 包括完整的 controller, host, profile, SIG Mesh部分。 其中蓝牙协议栈的 controller 和 host 部分以及操作系统抽象层 OSAL 都是以库的形式提供, 图中为灰色部分。 MCU 外设驱动和 profile, 以及应用层的例程代码, 都是以源码的形式提供, 图中为绿色部分。
MAX30102是一个完整的脉搏血氧饱和度和心率传感器系统解决方案模块,专为可穿戴设备的苛刻要求而设计。该设备在不牺牲光学或电学性能的情况下保持非常小的解决方案尺寸。集成到可穿戴系统中需要最少的外部硬件组件。
MAX30102通过软件寄存器完全可调,数字输出数据可存储在IC内的32深FIFO中。FIFO允许MAX30102连接到共享总线上的微控制器或处理器,在共享总线上数据不是从MAX30102的寄存器连续读取的。
MAX30102的SpO2子系统包含环境光消除(ALC)、连续时间sigma-delta ADC和专有的离散时间滤波器。自动高度控制有一个内部跟踪/保持电路,以消除环境光并增加有效动态范围。SpO2 ADC的可编程满标度范围为2µA至16µA。ALC可抵消高达200µA的环境电流。
内部ADC是一个具有18位分辨率的连续时间过采样sigma-delta转换器。ADC采样率为10.24MHz。ADC输出数据速率可从50sps(每秒采样数)编程到3200sps。
MAX30102有一个片上温度传感器,用于校准SpO2子系统的温度依赖性。温度传感器的固有分辨率为0.0625°C。
设备输出数据对红外LED的波长相对不敏感,其中红色LED的波长对于正确解释数据至关重要。与MAX30102输出信号一起使用的SpO2算法可以补偿与环境温度变化相关的SpO2误差。
MAX30102集成了红色和红外LED驱动器,用于调制用于SpO2和HR测量的LED脉冲。在适当的电源电压下,LED电流可编程设定在0到50mA之间。LED脉冲宽度可从69µs编程到411µs,以允许算法根据用例优化SpO2和HR精度以及功耗。
当用户的手指不在传感器上时,该装置包括接近功能以节省功率并减少可见光发射。当SpO2或HR功能启动时(通过写入模式寄存器),IR LED在接近模式下激活,驱动电流由PILOT PA寄存器设置。当通过超过IR ADC计数阈值(在PROX\u INT\u THRESH寄存器中设置)检测到物体时,部件自动转换到正常SpO2/HR模式。要重新进入接近模式,必须重写模式寄存器(即使值相同)。
可通过将接近功能重置为0来禁用接近功能。在这种情况下,血氧饱和度或心率模式立即开始。
通常,从I2C接口读取寄存器会自动递增寄存器地址指针,这样所有寄存器都可以在无I2C启动事件的突发读取中读取。在MAX30102中,这适用于除FIFO数据寄存器(寄存器0x07)以外的所有寄存器。
读取FIFO\数据寄存器不会自动增加寄存器地址。突发读取该寄存器从同一地址反复读取数据。每个样本都包含多个字节的数据,因此应从该寄存器(在同一事务中)读取多个字节以获得一个完整的样本。
另一个例外是0xFF。在0xFF寄存器之后读取更多字节不会使地址指针返回0x00,读取的数据也没有意义。
数据FIFO由一个32采样内存组组成,它可以存储IR和Red ADC数据。由于每个样本由两个数据通道组成,因此每个样本有6个字节的数据,因此FIFO中可以存储总共192个字节的数据。
FIFO数据左对齐,如表1所示;换句话说,无论ADC分辨率设置如何,MSB位始终位于位17数据位置。FIFO数据结构的直观表示见表2。
表1。FIFO数据左对齐
FIFO数据左对齐,这意味着无论ADC分辨率设置如何,MSB始终位于同一位置。未使用FIFO数据[18]–[23]。表2显示了每个三元组字节的结构(包含每个通道的18位ADC数据输出)。
SpO2模式下的每个数据样本包含两个数据三元组(每个三元组3个字节),要读取一个样本,每个字节需要一个I2C读取命令。因此,要在SpO2模式下读取一个样本,需要读取6个I2C字节。读取每个样本的第一个字节后,FIFO读取指针将自动递增。
写/读指针用于控制FIFO中的数据流。每次向FIFO添加新样本时,写入指针都会递增。每次从FIFO读取样本时,读取指针都会递增。要从FIFO中重新读取样本,将其值减一,然后再次读取数据寄存器。
在进入SpO2模式或HR模式时,应清除FIFO写/读指针(返回0x00),以便在FIFO中没有表示的旧数据。如果VDD通电或VDD低于其UVLO电压,指针将自动清除。
表2。FIFO数据(每个通道3字节)
第一个事务:获取FIFO_WR_PTR:
START;
Send device address + write mode
Send address of FIFO_WR_PTR;
REPEATED_START;
Send device address + read mode
Read FIFO_WR_PTR;
STOP;
中央处理器评估要从FIFO读取的样本数:
NUM_AVAILABLE_SAMPLES = FIFO_WR_PTR – FIFO_RD_PTR
(Note: pointer wrap around should be taken into account)
NUM_SAMPLES_TO_READ = < less than or equal to NUM_AVAILABLE_SAMPLES >
第二个事务:从FIFO读取样本数:
START;
Send device address + write mode
Send address of FIFO_DATA;
REPEATED_START;
Send device address + read mode
for (i = 0; i < NUM_SAMPLES_TO_READ; i++) {
Read FIFO_DATA;
Save LED1[23:16];
Read FIFO_DATA;
Save LED1[15:8];
Read FIFO_DATA;
Save LED1[7:0];
Read FIFO_DATA;
Save LED2[23:16];
Read FIFO_DATA;
Save LED2[15:8];
Read FIFO_DATA;
Save LED2[7:0];
Read FIFO_DATA;
}
STOP;
START;
Send device address + write mode
Send address of FIFO_RD_PTR;
Write FIFO_RD_PTR;
STOP;
第三个事务:写入FIFO_RD_PTR寄存器。如果第二个事务成功,则FIFO_RD_PTR指向FIFO中的下一个样本,而第三个事务不是必需的。否则,处理器会适当地更新FIFO_RD_PTR,以便重新读取样本。
这里以读取数据的FIFO为例子,介绍fr8016读取max30102的fifo。
int8_t maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led)
/**
* \brief Read a set of samples from the MAX30102 FIFO register
* \par Details
* This function reads a set of samples from the MAX30102 FIFO register
*
* \param[out] *pun_red_led - pointer that stores the red LED reading data
* \param[out] *pun_ir_led - pointer that stores the IR LED reading data
*
* \retval true on success
*/
{
uint32_t un_temp;
unsigned char uch_temp;
char ach_i2c_data[6];
uint8_t Ack1,Ack2,Ack3;//,Ack4;
*pun_red_led=0;
*pun_ir_led=0;
//read and clear status register
maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);
maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);
ach_i2c_data[0]=REG_FIFO_DATA;
sensirion_i2c_start();
Ack1 = sensirion_i2c_write_byte(I2C_WRITE_ADDR); //发送设备写地址
Ack2 = sensirion_i2c_write_byte(ach_i2c_data[0]); //发送寄存器地址
sensirion_i2c_start();
Ack3 = sensirion_i2c_write_byte(I2C_READ_ADDR); //发送设备读地址
//un_temp=(unsigned char) ach_i2c_data[0];
un_temp = sensirion_i2c_read_byte(1);//读取
un_temp<<=16;
*pun_red_led+=un_temp;
//un_temp=(unsigned char) ach_i2c_data[1];
un_temp = sensirion_i2c_read_byte(1);//读取
un_temp<<=8;
*pun_red_led+=un_temp;
//un_temp=(unsigned char) ach_i2c_data[2];
un_temp = sensirion_i2c_read_byte(1);//读取
*pun_red_led+=un_temp;
// un_temp=(unsigned char) ach_i2c_data[3];
un_temp = sensirion_i2c_read_byte(1);//读取
un_temp<<=16;
*pun_ir_led+=un_temp;
//un_temp=(unsigned char) ach_i2c_data[4];
un_temp = sensirion_i2c_read_byte(1);//读取
un_temp<<=8;
*pun_ir_led+=un_temp;
//un_temp=(unsigned char) ach_i2c_data[5];
un_temp = sensirion_i2c_read_byte(0);//读取
sensirion_i2c_stop();//产生停止
*pun_ir_led+=un_temp;
*pun_red_led&=0x03FFFF; //Mask MSB [23:18]
*pun_ir_led&=0x03FFFF; //Mask MSB [23:18]
if(Ack1 || Ack2 || Ack3)//如果
return -1; //发送失败
else
return 0; //发送成功
}
上面需要使用到FR8016的SDK的模拟I2C函数,比如i2c起始,i2c读写一个字节。
#define DELAY_USECC (SENSIRION_I2C_CLOCK_PERIOD_USEC / 2)
static uint8_t sensirion_wait_while_clock_stretching(void) {
uint8_t timeout = 100;
while (--timeout) {
//co_printf("timeout= %d\r\n",timeout);
if (sensirion_SCL_read())
return STATUS_OK;
sensirion_sleep_usec(DELAY_USECC);
}
return STATUS_FAIL;
}
static int8_t sensirion_i2c_write_byte(uint8_t data) {
int8_t nack, i;
for (i = 7; i >= 0; i--) {
sensirion_SCL_out();
if ((data >> i) & 0x01)
sensirion_SDA_in();
else
sensirion_SDA_out();
sensirion_sleep_usec(DELAY_USECC);
sensirion_SCL_in();
sensirion_sleep_usec(DELAY_USECC);
if (sensirion_wait_while_clock_stretching())
return STATUS_FAIL;
}
sensirion_SCL_out();
sensirion_SDA_in();
sensirion_sleep_usec(DELAY_USECC);
sensirion_SCL_in();
if (sensirion_wait_while_clock_stretching())
return STATUS_FAIL;
// printf("sensirion_i2c_write_byte\r\n************");
nack = (sensirion_SDA_read() != 0);//判断ACK
sensirion_SCL_out();
//printf("nack =%d\r\n",nack);
return nack;
}
static uint8_t sensirion_i2c_read_byte(uint8_t ack) {
int8_t i;
uint8_t data = 0;
sensirion_SDA_in();
for (i = 7; i >= 0; i--) {
sensirion_sleep_usec(DELAY_USECC);
sensirion_SCL_in();
if (sensirion_wait_while_clock_stretching())
return STATUS_FAIL;
data |= (sensirion_SDA_read() != 0) << i;
sensirion_SCL_out();
}
if (ack)
sensirion_SDA_out();
else
sensirion_SDA_in();
sensirion_sleep_usec(DELAY_USECC);
sensirion_SCL_in();
sensirion_sleep_usec(DELAY_USECC);
if (sensirion_wait_while_clock_stretching())
return STATUS_FAIL;
sensirion_SCL_out();
sensirion_SDA_in();
return data;
}
static uint8_t sensirion_i2c_start(void) {
sensirion_SCL_in();
if (sensirion_wait_while_clock_stretching())
return STATUS_FAIL;
sensirion_SDA_out();
sensirion_sleep_usec(DELAY_USECC);
sensirion_SCL_out();
sensirion_sleep_usec(DELAY_USECC);
return STATUS_OK;
}
static void sensirion_i2c_stop(void) {
sensirion_SDA_out();
sensirion_sleep_usec(DELAY_USECC);
sensirion_SCL_in();
sensirion_sleep_usec(DELAY_USECC);
sensirion_SDA_in();
sensirion_sleep_usec(DELAY_USECC);
}
SDK 里面包含了完整的协议栈, 虽然 controller 和 host 部分是以库的形式提供, 但给出了接口丰富的 API 提供给上层应用开发调用。 Profile 则是以源码的形式提供。
GAP操作如下:
/*********************************************************************
* @fn simple_peripheral_init
*
* @brief Initialize simple peripheral profile, BLE related parameters.
*
* @param None.
*
*
* @return None.
*/
void simple_peripheral_init(void)
{
// set local device name
uint8_t local_name[] = "Simple Peripheral";
gap_set_dev_name(local_name, sizeof(local_name));
// Initialize security related settings.
gap_security_param_t param =
{
.mitm = false,
.ble_secure_conn = false,
.io_cap = GAP_IO_CAP_NO_INPUT_NO_OUTPUT,
.pair_init_mode = GAP_PAIRING_MODE_WAIT_FOR_REQ,
.bond_auth = true,
.password = 0,
};
gap_security_param_init(¶m);
gap_set_cb_func(app_gap_evt_cb);
gap_bond_manager_init(BLE_BONDING_INFO_SAVE_ADDR, BLE_REMOTE_SERVICE_SAVE_ADDR, 8, true);
gap_bond_manager_delete_all();
mac_addr_t addr;
gap_address_get(&addr);
co_printf("Local BDADDR: 0x%2X%2X%2X%2X%2X%2X\r\n", addr.addr[0], addr.addr[1], addr.addr[2], addr.addr[3], addr.addr[4], addr.addr[5]);
// Adding services to database
sp_gatt_add_service();
speaker_gatt_add_service(); //创建Speaker profile,
//按键初始化 PD6 PC5
pmu_set_pin_pull(GPIO_PORT_D, (1<<GPIO_BIT_6), true);
pmu_set_pin_pull(GPIO_PORT_C, (1<<GPIO_BIT_5), true);
pmu_port_wakeup_func_set(GPIO_PD6|GPIO_PC5);
button_init(GPIO_PD6|GPIO_PC5);
demo_LCD_APP(); //显示屏
// demo_CAPB18_APP(); //气压计
demo_SHT3x_APP(); //温湿度
// gyro_dev_init(); //加速度传感器
//
// //OS Timer
os_timer_init(&timer_refresh,timer_refresh_fun,NULL);//创建一个周期性1s定时的系统定时器
os_timer_start(&timer_refresh,1000,1);
}
GATT操作主要是在回调函数sp_gatt_read_cb和sp_gatt_write_cb里进行:
/*********************************************************************
* @fn sp_gatt_msg_handler
*
* @brief Simple Profile callback funtion for GATT messages. GATT read/write
* operations are handeled here.
*
* @param p_msg - GATT messages from GATT layer.
*
* @return uint16_t - Length of handled message.
*/
static uint16_t sp_gatt_msg_handler(gatt_msg_t *p_msg)
{
switch(p_msg->msg_evt)
{
case GATTC_MSG_READ_REQ:
sp_gatt_read_cb((uint8_t *)(p_msg->param.msg.p_msg_data), &(p_msg->param.msg.msg_len), p_msg->att_idx);
break;
case GATTC_MSG_WRITE_REQ:
sp_gatt_write_cb((uint8_t*)(p_msg->param.msg.p_msg_data), (p_msg->param.msg.msg_len), p_msg->att_idx,p_msg->conn_idx);
break;
default:
break;
}
return p_msg->param.msg.msg_len;
}
本次使用os_user_loop_event_set创建了循环任务读取max30102的心率数据。OSAL有以下部分组成:
软件定时器用来创建一个定时任务,比如10秒钟处理一次任务A,20秒处理一次任务B。
(FR8016) PC6 ->SCL(MAX30102)
(FR8016) PC7 ->SDA(MAX30102)
(FR8016) PA5 ->INT(MAX30102)
手机在应用商店下载一个 蓝牙调试器。可见我的心率在80~90之间,完成了心率数据通过BLE上传到我的安卓手机。
通过USB dongle抓包,可见fr8016与我的安卓手机建立了加密通信。
由下图可见,fr8016与max30102能正常的进行i2c通信。
修改后上传
拍摄后上传