这里利用CH32V307和TtencentOS Tiny设计了一款物联网心率监测系统。先来看一下实物图
概要就是实现CH32V307和AD8232实现心率的测量,并将该数据发送到腾讯云上,然后有腾讯云将数据转发到微信小程序上实现远程实时查看数据,并通过将心率过高或者过低的数据推送到绑定的公众号上。
1.整体架构
2.硬件电路
2.1 单片机
单片机采用CH32V307VCT6的芯片,CH32V307系列是基于32位RISC-V设计的互联型微控制器,配备了硬件堆栈区、快速中断入口,在标准RISC-V基础上大大提高了中断响应速度。加入单精度浮点指令集,扩充堆栈区,具有更高的运算性能。CH32V307是基于32位RISC-V设计的互联型微控制器,配备了硬件堆栈区、快速中断入口,在标准RISC-V基础上大大提高了中断响应速度。加入单精度浮点指令集,扩充堆栈区,具有更高的运算性能。扩展串口UART数量到8组,电机定时器到4组。提供USB2.0高速接口(480Mbps)并内置了PHY收发器,以太网MAC升级到千兆并集成了10M-PHY模块。
硬件使用的是 TencentOS Tiny CH32V_EVB RISC-V开发套件,板载Type-C接口WCH-LINK仿真器,板载esp8266 WiFi模组,支持腾讯云固件。
传感器模块
2.2 AD8232心率模块
AD8232是一款用于心电信号测量及其他生物电测量的集成信号调理模块。该芯片可以在有运动或远程电极放置产生的噪声的情况下提取、放大及过滤微弱的生物电信号。该芯片使得模数转换器(ADC)或嵌入式微控制器(MCU)能够便捷的采集输出信号,AD8232模拟前端模块由AD8232芯片和辅助电路构成。工作原理:由于心电信号的频率范围为0.5~100 HZ,幅度范围为0~4 mV,属于低频微弱小信号。同时心电信号中混杂着诸多干扰,如肌电噪声、工频干扰、基线漂移以及运动伪迹等,所以心电信号采集模块需在有效提取出微弱的心电信号的同时将对各种噪声起到最大的抑制。心电信号的前端放大模块由AD8232以及外围电路构成,实现了心电信号的输出。
2.3 显示模块
LCD模块使用的是ST7789,一般是用于262K彩色TFT-LCD的单片控制器/驱动程序。它由960条源线和480条栅线驱动电路组成。ST7796S能够连接直接 Y为外部微处理器,接受8位/9位/16位/18位并行接口,spi和ST7796S也提供mipi接口。
这是官网提供的原理图。
2.4 通信模块
通信模块使用是ESP8266,可以直接烧录腾讯云的AT固件,连接腾讯云的物联网平台非常简单。ESP-12F是安信可用ESP8266EX芯片做的一款WiFi模组,这个模块已经把内部的电路设计好了。实际测试,ESP01和01s不如12f系列的稳定,经常出问题,要是长期使用还是建议使用12f系列。
2.5 整体电路
3 物联网云平台搭建
3.1 登录腾讯云物联网平台
登录 物联网开发平台 https://console.cloud.tencent.com/iotexplorer ,选择公共实例
进入项目列表页面,单击新建项目
击创建的项目进入产品开发中心,单击新建产品,根据页面提示填写产品基本信息。
进入控制台自定义功能新建功能页面,进入对应设置界面,以为我们这里之需要一个参数即可,就是心率的数据。
选择新建设备,如下图输入设备名,单击保存,即可创建设备。
设备查看打开后,即可查看设备的所有信息。
在这里我们需要三个设备密钥、产品 ID,这个需要写入程序中。
如果设备已经连接,我们就可以查看到相关的数据了。
3.2 配置数据流
进入数据开发列表页面,单击新建数据流。
单击已拖放的设备数据节点,界面右侧显示该节点的配置内容。
单击已拖放的数据过滤节点,界面右侧显示该节点的配置内容。在配置“数据过滤”前,必须要指定数据源,即需要将“设备数据”与“数据过滤”两个节点进行连接。
在这个设计中,我使用的是将告警信息推送到腾讯连连公众号。选择通知类型:设备告警或通知消息
设置完输入、处理与输出节点的规则后,单击页面上方保存 > 启用。当启用数据流后,只要该设备上报的数据符合定义的规则,则会触发公众号推送。
3.3 微信小程序配置
图片配置页,单击选择图片,选择自定义的产品图片并上传。
单击重新上传更换自定义产品图片,单击重置恢复默认产品图片。
通过面板配置,可以自定义产品控制面板的风格、布局、按钮样式等配置。我选择的默认的配置。
在设备中,点击二维码并扫描就可以在微信小程序中管理上这个设备。
这个是最终的显示效果
到这里物联网的配置就结束了。
4 软件开发
4.1 烧录腾讯云的IOT固件
固件在这里下载的。
QCloud_IoT_AT_ESP8266_v2.1.1_20200903_UART_1_3.rar (441.14 KB)
具体怎么烧录,还是百度一下吧,很简单。
可以使用MRS选择呢CH32V307建立一个带有TencentOS工程文件,也可以使用TencentOS自带的文件。
gitee可以下载到 TencentOS-tiny的源代码。 https://gitee.com/Tencent/TencentOS-tiny
按照这个目录查找即可。
TencentOS-tiny-master\board\TencentOS_Tiny_CH32V307_EVB
下面就是具体的代码设计了。
使用 MRS生成的工程文件,可以直接初始化串口为printf了。
我们需要配置的就是LCD显示和AD采样数据。
LCD使用的是ST7796S
这里主要讲述怎么初始化各个IO操作,具体的内容可以查看源代码。
所有的IO口都是宏定义,这样方便后期的移植操作。
//-----------------LCD端口定义----------------
#define LCD_SCLK_Clr() GPIO_WriteBit(GPIOE,GPIO_Pin_1,0)//SCL=SCLK
#define LCD_SCLK_Set() GPIO_WriteBit(GPIOE,GPIO_Pin_1,1)
#define LCD_MOSI_Clr() GPIO_WriteBit(GPIOD,GPIO_Pin_1,0)//SDA=MOSI
#define LCD_MOSI_Set() GPIO_WriteBit(GPIOD,GPIO_Pin_1,1)
#define LCD_RES_Clr() GPIO_WriteBit(GPIOD,GPIO_Pin_3,0)//RES
#define LCD_RES_Set() GPIO_WriteBit(GPIOD,GPIO_Pin_3,1)
#define LCD_DC_Clr() GPIO_WriteBit(GPIOE,GPIO_Pin_0,0)//DC
#define LCD_DC_Set() GPIO_WriteBit(GPIOE,GPIO_Pin_0,1)
#define LCD_BLK_Clr() GPIO_WriteBit(GPIOD,GPIO_Pin_0,1)//BLK
#define LCD_BLK_Set() GPIO_WriteBit(GPIOD,GPIO_Pin_0,0)
初始化代码包括
void LCD_GPIO_Init(void);//初始化GPIO
void LCD_Writ_Bus(u8 dat);//模拟SPI时序
void LCD_WR_DATA8(u8 dat);//写入一个字节
void LCD_WR_DATA(u16 dat);//写入两个字节
void LCD_WR_REG(u8 dat);//写入一个指令
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2);//设置坐标函数
void LCD_Init(void);//LCD初始化
操作接口的代码包括
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color);//指定区域填充颜色
void LCD_DrawPoint(u16 x,u16 y,u16 color);//在指定位置画一个点
void LCD_DrawLine(u16 x1,u16 y1,u16 x2,u16 y2,u16 color);//在指定位置画一条线
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2,u16 color);//在指定位置画一个矩形
void Draw_Circle(u16 x0,u16 y0,u8 r,u16 color);//在指定位置画一个圆
void LCD_ShowChinese(u16 x,u16 y,u8 *s,u16 fc,u16 bc,u8 sizey,u8 mode);//显示汉字串
void LCD_ShowChinese12x12(u16 x,u16 y,u8 *s,u16 fc,u16 bc,u8 sizey,u8 mode);//显示单个12x12汉字
void LCD_ShowChinese16x16(u16 x,u16 y,u8 *s,u16 fc,u16 bc,u8 sizey,u8 mode);//显示单个16x16汉字
void LCD_ShowChinese24x24(u16 x,u16 y,u8 *s,u16 fc,u16 bc,u8 sizey,u8 mode);//显示单个24x24汉字
void LCD_ShowChinese32x32(u16 x,u16 y,u8 *s,u16 fc,u16 bc,u8 sizey,u8 mode);//显示单个32x32汉字
void LCD_ShowChar(u16 x,u16 y,u8 num,u16 fc,u16 bc,u8 sizey,u8 mode);//显示一个字符
void LCD_ShowString(u16 x,u16 y,const u8 *p,u16 fc,u16 bc,u8 sizey,u8 mode);//显示字符串
u32 mypow(u8 m,u8 n);//求幂
void LCD_ShowIntNum(u16 x,u16 y,u16 num,u8 len,u16 fc,u16 bc,u8 sizey);//显示整数变量
void LCD_ShowFloatNum1(u16 x,u16 y,float num,u8 len,u16 fc,u16 bc,u8 sizey);//显示两位小数变量
void LCD_ShowPicture(u16 x,u16 y,u16 length,u16 width,const u8 pic[]);//显示图片
配置初始化ADC的代码,这里不需要使用DMA,直接采样即可。
s16 Calibrattion_Val = 0;
void ADC_Function_Init(void)
{
ADC_InitTypeDef ADC_InitStructure={0};
GPIO_InitTypeDef GPIO_InitStructure={0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE );
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE );
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
ADC_BufferCmd(ADC1, DISABLE); //disable buffer
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
Calibrattion_Val = Get_CalibrationValue(ADC1);
ADC_BufferCmd(ADC1, ENABLE); //enable buffer
}
u16 Get_ADC_Val(u8 ch)
{
u16 val;
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 );
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));
val = ADC_GetConversionValue(ADC1);
return val;
}
u16 Get_ConversionVal(s16 val)
{
if((val+Calibrattion_Val)<0) return 0;
if((Calibrattion_Val+val)>4095||val==4095) return 4095;
return (val+Calibrattion_Val);
}
在应用代码创建一个任务
void application_entry(void *arg)
{
tos_task_create(&task3, "task3", task3_entry, NULL,0, task3_stk, TASK3_STK_SIZE, 0);// Create task3
}
下面就是编写任务代码。
在TencentOS Tiny需要创建一个定时器,来实现20ms的采样。
// 创建一个软件定时间器
tos_timer_create(&os_tmr_handler, // 指向定时器程序的指针
20, // 计时器运行的时间间隔ms
20, // 定时器重新启动运行的时间段
os_tmr_handler_callback, // 计时器到期时调用的回调函数
K_NULL, // 回调的参数
TOS_OPT_TIMER_PERIODIC); // 定期运行,也可以选择单次运行
// 启动定时器
tos_timer_start(&os_tmr_handler);
在定时器任务里面,决定采样的速度。其中flag_arr通知采样的标志。
volatile u8 flag_arr=0;
// 调用的回调函数
void os_tmr_handler_callback(void *arg)
{
flag_arr=1;
}
在任务代码里面开始初始化ESP8266,并建立与腾讯云的连接。
char payload[256] = {0};
mqtt_state_t state;
这里是秘钥,按照自己的实际情况填写。
char *product_id = "xxxxxx";
char *device_name = "xxxx";
char *key = "xxxxxx==";
device_info_t dev_info;
printf("Config ESP8266\r\n");
memset(&dev_info, 0, sizeof(device_info_t));
/**
* Please Choose your AT Port first, default is HAL_UART_0(LPUART)
*/
esp8266_tencent_firmware_sal_init(HAL_UART_PORT_2);
esp8266_tencent_firmware_join_ap("Redmi_5G", "123456789");
strncpy(dev_info.product_id, product_id, PRODUCT_ID_MAX_SIZE);
strncpy(dev_info.device_name, device_name, DEVICE_NAME_MAX_SIZE);
strncpy(dev_info.device_serc, key, DEVICE_SERC_MAX_SIZE);
tos_tf_module_info_set(&dev_info, TLS_MODE_PSK);
mqtt_param_t init_params = DEFAULT_MQTT_PARAMS;
if (tos_tf_module_mqtt_conn(init_params) != 0) {
printf("module mqtt conn fail\n");
} else {
printf("module mqtt conn success\n");
}
if (tos_tf_module_mqtt_state_get(&state) != -1) {
printf("MQTT: %s\n", state == MQTT_STATE_CONNECTED ? "CONNECTED" : "DISCONNECTED");
}
static char topic_name[TOPIC_NAME_MAX_SIZE] = {0};
static char topic_name_sub[TOPIC_NAME_MAX_SIZE] = {0};
int size = snprintf(topic_name, TOPIC_NAME_MAX_SIZE, "$thing/up/property/%s/%s", product_id, device_name);
if (size < 0 || size > sizeof(topic_name) - 1) {
printf("topic content length not enough! content size:%d buf size:%d", size, (int)sizeof(topic_name));
}
size = snprintf(topic_name_sub, TOPIC_NAME_MAX_SIZE, "$thing/down/property/%s/%s", product_id, device_name);
if (size < 0 || size > sizeof(topic_name) - 1) {
printf("topic content length not enough! content size:%d buf size:%d", size, (int)sizeof(topic_name));
}
if (tos_tf_module_mqtt_sub(topic_name_sub, QOS0, default_message_handler) != 0) {
printf("module mqtt sub fail\n");
} else {
printf("module mqtt sub success\n");
}
当采集到数据的时候,上传代码为:
memset(payload, 0, sizeof(payload));
sprintf(payload, "{\\\"method\\\":\\\"report\\\"\\,\\\"clientToken\\\":\\\"123\\\"\\,\\\"params\\\":{\\\"heart\\\":%d\\}}",currentBPM);
printf("message publish: %s\n", payload);
if (tos_tf_module_mqtt_pub(topic_name, QOS0, payload) != 0) {
printf("module mqtt pub fail\n");
break;
} else {
printf("module mqtt pub success\n");
}
这里是我们的ADC采样代码
vl = Get_ConversionVal(Get_ADC_Val(ADC_Channel_4));
因为信号中包括了很多的噪声,所以需要添加一个滤波代码。可以使用高通滤波。
float FirstOrderFilter(fof_Type *fof, float in)
{
if(fof->type)
{
fof->out = fof->a * fof->out + fof->a * (in - fof->in_old);
fof->in_old = in;
}
else
fof->out = fof->a * in + (1 - fof->a) * fof->out;
return fof->out;
}
fof_Init(&fof_2, 5, 30, 1); //高通滤波器,截止频率为30Hz
在代码中检测脉冲的波峰,并计算心率大小。
vl = Get_ConversionVal(Get_ADC_Val(ADC_Channel_4));
highFilter= FirstOrderFilter(&fof_2, vl );
这是计算心率的方式
if( detectPulse( highFilter ) )
{
if(lastBeat ==0)
{
lastBeat =tos_systick_get();
} else
{
currentBeat = tos_systick_get();;
uint32_t beatDuration = currentBeat - lastBeat;
lastBeat = currentBeat;
float rawBPM = 0;
if(beatDuration > 0)
{
rawBPM = (float)60000.0 / (float)beatDuration;
valuesBPM[bpmIndex] = rawBPM;
valuesBPMSum = 0;
for(int i=0; i
这里是画线的方式,为了减小刷屏占用的时间,需要把之前的线擦除,然后重新画线。实现代码
LCD_DrawLine((dot_index-1)*2,120-pre_pix,(dot_index)*2,120-(uint8_t)pix[dot_index],BLACK);
pre_pix = (uint8_t)pix[dot_index];
pix[dot_index] = (uint8_t)((highFilter-max_minus)/scale);
LCD_DrawLine((dot_index-1)*2,120-(uint8_t)pix[dot_index-1],(dot_index)*2,120-(uint8_t)pix[dot_index],RED);
这就是最终效果。
---------------------
作者:51xlf
链接:https://bbs.21ic.com/icview-3272416-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。