文章目录
本次测评fr8016的模拟i2c外设和蓝牙功能,通过芯片的IO口模拟I2C通信时序与心率模块max30102通信,采集人体心率,将结果通过BLE上传给安卓手机显示。运用到SDK的内容有
- BLE协议栈GATT、GAP
- IO模拟I2C
- 系统定时任务CLOCK
- 操作系统抽象层OSAL
下面将逐一介绍,从max30102驱动开始,最终实现心率数据向安卓手机上传,完成一个BLE应用。
一、FR8016资源与SDK架构
1.1 FR8016资源
- FLASH Cache RAM:这是一个8KB的数据RAM,主要由缓存控制器使用。缓存控制器直接从外部QSPI FLASH执行,从而减少对FLASH的访问。
- UART:异步串行接口,具有32字节深度的FIFO,波特率从4800到921600不等。
- SPI:串行外设接口,具有128字节深度和8位宽度的FIFO。最大总线频率为24MHz。
- I2C:这是用于传感器和/或主机MCU通信的主I2C接口。它包括一个8位宽、8字节深的RX FIFO和一个8位宽、10字节深的TX FIFO。最大总线频率为2MHz。
- QSPI Controller:与Quad SPI Flash设备的接口,最大总线频率为24MHz
- SARADC:差分逐次逼近寄存器模数转换器。它支持多达8个外部模拟输入通道,10位宽度和1MHz采样率。
- RADIO Transciver:此模块实现2.4GHz的蓝牙低能量协议的射频部分。
- clock generator:此块负责系统的计时。它包含一个24MHz晶体振荡器,用于系统的有源模式。还有一个62.5 kHz振荡器(RC62.5K),精度(<300 ppm)。RCX振荡器可用作休眠时钟,以提高功耗,同时减少系统的物料清单。
- timer:2个独立的16位计数器计时器.
- PWM:PWM模块电路实现脉宽调制波输出。6通道数字PWM输出模块,超高达(1/48M)分辨率。另外还有另一个3通道PMU PWM输出模块,具有1/(PMU系统clk)分辨率。
- Keyboard scanner: 该电路实现了对键盘矩阵的扫描和去抖动,并在不需要CPU的情况下对可配置的动作产生中断。
- AHB/APB bus:执行AHB和APB规范的AMBA Lite版本。
- I2S和PDM端口:该部分通过脉冲密度调制(PDM)和IC间声音(I2S)接口实现音频流。它支持使用PDM接口和内部编解码器块的数字麦克风、模拟麦克风和单声道扬声器.I2S接口具有64 16位宽度FIFO深度、最大24MHz总线速度和16 kHz/8kHz、16位采样率。PDM接口具有64个16位宽度FIFO深度、1MHz/2MHz总线速度和16 kHz/8kHz、16位采样率。
- audio codec:它由1路16位∑ΔADC和1路16位∑ΔDAC组成,采样率高达48kHz。内部麦克风偏置等于0.9*编解码器电源电压,输入PGA放大器-17.25dB-30dB增益范围,输出耳机PA,音频编解码器块内输出功率高达50mW。
- 电源管理。一个复杂的电源管理电路,带有降压型DC-DC转换器和几个LDO,可通过PMU模块打开/关闭。即使FR801xH处于睡眠/深度睡眠模式,也会提供额外的引脚为外部设备供电.它还包括用于蓄电池充电的恒流/恒压(CCCV)充电器和充电状态燃油表电路。CCCV充电器电流从48mA到258mA不等。
该模块还包括一个LDO输出,最大驱动能力为125mA,输出电压范围为1.8~3.5V。引脚垫驱动能力为12mA。所有焊盘的总驱动能力等于LDO输出驱动能力,即125mA。
1.2 SDK架构
FR801xH SDK 的架构如下图所示。 SDK 包含了完整的 BLE 5.0 协议栈, 包括完整的 controller, host, profile, SIG Mesh部分。 其中蓝牙协议栈的 controller 和 host 部分以及操作系统抽象层 OSAL 都是以库的形式提供, 图中为灰色部分。 MCU 外设驱动和 profile, 以及应用层的例程代码, 都是以源码的形式提供, 图中为绿色部分。
二、心率芯片max30102通信协议及驱动编程
2.1 max30102介绍
2.1.1 功能图
2.1.2 详细说明
MAX30102是一个完整的脉搏血氧饱和度和心率传感器系统解决方案模块,专为可穿戴设备的苛刻要求而设计。该设备在不牺牲光学或电学性能的情况下保持非常小的解决方案尺寸。集成到可穿戴系统中需要最少的外部硬件组件。
MAX30102通过软件寄存器完全可调,数字输出数据可存储在IC内的32深FIFO中。FIFO允许MAX30102连接到共享总线上的微控制器或处理器,在共享总线上数据不是从MAX30102的寄存器连续读取的。
2.1.3 血氧饱和度子系统
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。
2.1.4 温度传感器
MAX30102有一个片上温度传感器,用于校准SpO2子系统的温度依赖性。温度传感器的固有分辨率为0.0625°C。
设备输出数据对红外LED的波长相对不敏感,其中红色LED的波长对于正确解释数据至关重要。与MAX30102输出信号一起使用的SpO2算法可以补偿与环境温度变化相关的SpO2误差。
2.1.5 LED驱动器
MAX30102集成了红色和红外LED驱动器,用于调制用于SpO2和HR测量的LED脉冲。在适当的电源电压下,LED电流可编程设定在0到50mA之间。LED脉冲宽度可从69µs编程到411µs,以允许算法根据用例优化SpO2和HR精度以及功耗。
2.1.6 接近功能
当用户的手指不在传感器上时,该装置包括接近功能以节省功率并减少可见光发射。当SpO2或HR功能启动时(通过写入模式寄存器),IR LED在接近模式下激活,驱动电流由PILOT PA寄存器设置。当通过超过IR ADC计数阈值(在PROX\u INT\u THRESH寄存器中设置)检测到物体时,部件自动转换到正常SpO2/HR模式。要重新进入接近模式,必须重写模式寄存器(即使值相同)。
可通过将接近功能重置为0来禁用接近功能。在这种情况下,血氧饱和度或心率模式立即开始。
2.2 i2c通信协议
2.2.1 从FIFO读取
通常,从I2C接口读取寄存器会自动递增寄存器地址指针,这样所有寄存器都可以在无I2C启动事件的突发读取中读取。在MAX30102中,这适用于除FIFO数据寄存器(寄存器0x07)以外的所有寄存器。
读取FIFO\数据寄存器不会自动增加寄存器地址。突发读取该寄存器从同一地址反复读取数据。每个样本都包含多个字节的数据,因此应从该寄存器(在同一事务中)读取多个字节以获得一个完整的样本。
另一个例外是0xFF。在0xFF寄存器之后读取更多字节不会使地址指针返回0x00,读取的数据也没有意义。
2.2.2 FIFO数据结构
数据FIFO由一个32采样内存组组成,它可以存储IR和Red ADC数据。由于每个样本由两个数据通道组成,因此每个样本有6个字节的数据,因此FIFO中可以存储总共192个字节的数据。
FIFO数据左对齐,如表1所示;换句话说,无论ADC分辨率设置如何,MSB位始终位于位17数据位置。FIFO数据结构的直观表示见表2。
表1。FIFO数据左对齐
2.2.3FIFO数据每个通道包含3个字节
FIFO数据左对齐,这意味着无论ADC分辨率设置如何,MSB始终位于同一位置。未使用FIFO数据[18]–[23]。表2显示了每个三元组字节的结构(包含每个通道的18位ADC数据输出)。
SpO2模式下的每个数据样本包含两个数据三元组(每个三元组3个字节),要读取一个样本,每个字节需要一个I2C读取命令。因此,要在SpO2模式下读取一个样本,需要读取6个I2C字节。读取每个样本的第一个字节后,FIFO读取指针将自动递增。
2.2.4 写/读指针
写/读指针用于控制FIFO中的数据流。每次向FIFO添加新样本时,写入指针都会递增。每次从FIFO读取样本时,读取指针都会递增。要从FIFO中重新读取样本,将其值减一,然后再次读取数据寄存器。
在进入SpO2模式或HR模式时,应清除FIFO写/读指针(返回0x00),以便在FIFO中没有表示的旧数据。如果VDD通电或VDD低于其UVLO电压,指针将自动清除。
表2。FIFO数据(每个通道3字节)
2.3 驱动编程
2.3.1 从FIFO读取数据的伪代码示例
第一个事务:获取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,以便重新读取样本。
2.3.2 编程实现
这里以读取数据的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组件一】蓝牙协议栈GATT和GAP编程
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;
}
四、【SDK组件二】操作系统抽象层OSAL
本次使用os_user_loop_event_set创建了循环任务读取max30102的心率数据。OSAL有以下部分组成:
- os_task_create:创建一个任务。 最多支持 20 个任务。 任务不分优先级。 消息按抛送的顺序进行处理
- os_task_delete:删除一个已经创建的任务。
- os_msg_post:向某个已经创建的目标任务抛一个消息事件
- os_user_loop_event_set:用户设置一个跑在 while(1)循环中的函数, 便于用户做一些循环检测或者操作处理。 该循环处理事件优先级最低, 另外用户在需要睡眠时应先清除这个事件。
- os_user_loop_event_clear:清除循环处理事件。
五、【SDK组件三】系统定时任务CLOCK
软件定时器用来创建一个定时任务,比如10秒钟处理一次任务A,20秒处理一次任务B。
- os_timer_init:初始化一个软件定时器。 最多支持 50 个定时器。 使用软件定时器之前, 必须调用该函数进行初始化。
- os_timer_start:启动一个软件定时器
- os_timer_stop:停止一个软件定时器
六、测试
连接图
(FR8016) PC6 ->SCL(MAX30102)
(FR8016) PC7 ->SDA(MAX30102)
(FR8016) PA5 ->INT(MAX30102)
测试一:功能验证
手机在应用商店下载一个 蓝牙调试器。可见我的心率在80~90之间,完成了心率数据通过BLE上传到我的安卓手机。
测试二:BLE安全
通过USB dongle抓包,可见fr8016与我的安卓手机建立了加密通信。
测试三:逻辑分析仪分析
由下图可见,fr8016与max30102能正常的进行i2c通信。
项目地址
修改后上传
视频链接
拍摄后上传