×

使用ECG的心跳指示器

消耗积分:0 | 格式:zip | 大小:0.56 MB | 2023-02-08

韩刚龙

分享资料个

描述

多年来,我只是想用 LED 做一些能随着我的心跳而闪烁的东西(不仅是当我完全静止不动,但有时会时不时地跳一下)。事实证明这出奇地困难,我尝试了很多年但都失败了。但现在不是了!

实际上,所有繁重的工作都是由uECG完成的——一种小型可穿戴 ECG 设备,它是开源的,并且有一个 Arduino 友好的输出引脚(该引脚随着每次心跳而变高/变低)。处理这些引脚状态比处理 ECG 信号要容易得多,而且我已经尽力从中获得最大收益。
UPD:你可能想检查这个项目的第二次迭代,它通过无线电链路接收数据。

1. 原理图
由于我们这里只使用数字信号,所以非常简单。但作为可穿戴设备,如果大多数连接都是焊接的,它会更可靠(也更小)——对于快速测试来说,没有必要这样做,但如果你打算在一些繁重的活动中佩戴它,我强烈建议这样做。
示意图如下所示:

 
pYYBAGPi-FWAdnl0AAFSlvJRmHM499.png
 
  • LED 环的 DI 引脚连接到引脚 D11(可在代码中配置)
  • uECG 设备的 DRV 引脚连接到引脚 D3(也可配置)
  • 电池的 + 连接到 Arduino 5V 和 LED 环形 5V 输入
  • 电池 - 连接到 Arduino GND、环形 GND 和 uECG 的 GND

我直接使用 LiPo 电池作为 5V 输入 - 没有错误,如果你将它连接到 Vin - 它不会可靠地工作(Vin 上的稳压器会引入电压降,我们在这里绝对买不起)。问题是,只要输入电压不低于 3.4 伏,Arduino 就是稳定的。LiPo 电池在充满电时的起始电压为 4.2 伏,仅当剩余电量少于 15% 时才达到 3.4 伏。因此,对于任何大于 ~200 mAh 的电池,您可以获得不错的运行时间。除此之外,请记住电池应该通过一些连接器连接 :) 因为你想将它从原理图上断开并偶尔充电一次。

2. 代码
该程序以一种简单的方式工作:它不断读取 D3 引脚,并在检测到变化时 - 将该变化的时间推入 20 元素数组。第一个和最后一个元素之间的差值除以 20,即为每次节拍的平均时间(以毫秒为单位)。因此,将 1 分钟(60000 毫秒)除以该数字即可得出 BPM 值。您可以调整数组中的元素数量。较少数量的元素会导致更快的响应,但结果不太稳定(节拍检测中的任何问题都会导致计算出的 BPM 大幅跳跃)。更多的元素会提供更稳定的数据,但当 BPM 快速变化时响应更慢。

然后将 BPM 映射到颜色(当 BPM 从低到高时,蓝色->绿色->黄色->粉红色->红色),并映射到 LED 的数量:80 BPM 八段亮起,110 - 11 等等(比例也可在代码中调整)。

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif
// DI pin of LED ring
#define PIN            11
// number of pixels in the ring
#define NUMPIXELS      16
// input pin for connecting uECG
int in_pin = 3;
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixels.begin(); // This initializes the NeoPixel library.
pinMode(in_pin, INPUT); //set pin to input mode
digitalWrite(in_pin, 1); //enable PULLUP: this is critical, uECG doesn't have internal pull-up
}
//we store last 20 heartbeats to averge BPM over them
//with higher value, it will become more reliable,
//but it will take more time to see output change when BPM changes
#define BEAT_HIST 20
long beats[BEAT_HIST];
void push_beat(long ms) //shift all beats in array and insert current one
{
for(int x = 0; x < BEAT_HIST-1; x++)
{
beats[x] = beats[x+1];
}
beats[BEAT_HIST-1] = ms;
}
int get_bpm() //using time difference between first and last beats
{
long dt = beats[BEAT_HIST-1] - beats[0];
long bpm = BEAT_HIST * 60000 / dt;
return bpm;
}
long last_pix_upd = 0; //to keep track of when we updated pixels previous time
int prev_in_state = 0; //previous state of input pin: we want to process only changes of state
void loop()
{
long ms = millis();
int in_state = digitalRead(in_pin); //1 when no beat detected, 0 in beat
if(in_state == 1 && prev_in_state == 0) //react only to change
{
push_beat(ms);
}
prev_in_state = in_state;
if(ms - last_pix_upd > 10) //don't update pixels too often
{
int r, g, b;
last_pix_upd = ms;
int bpm = get_bpm();
int max_bright = 120; //value of maximum brightness, max 255. But you don't always want it at max :)
float dd = 20; //change in BPM between color tones (blue->green->yellow->pink->red)
float t1 = 90, t2, t3, t4; //t1 - "base" BPM, lower than t1 would be blue
t2 = t1 + dd;
t3 = t2 + dd;
t4 = t3 + dd;
//code for changing color depending in which t1...t4 range we are now
if(bpm < t1){ r = 0; g = 0; b = max_bright; }
else if(bpm < t2) { r = 0; g = max_bright * (bpm-t1)/dd; b = max_bright - g; }
else if(bpm < t3) { r = max_bright * (bpm-t2)/dd; g = max_bright - r; b = r/4; }
else if(bpm < t4) { r = max_bright; g = 0; b = max_bright/2 - max_bright * (bpm-t3)/(2*dd); }
else {r = max_bright; g = 0; b = 0; }
if(in_state) //when not in beat, 1/4 intensity, so only beats are highlighted
{
r *= 0.25;
g *= 0.25;
b *= 0.25;
}
int on_pixels = (bpm+5)/10; //number of used LEDs: for 60 BPM, 6 LEDs will be on, for 120 - 12 etc
for(int i=0;i<NUMPIXELS;i++)
{
if(i < on_pixels) pixels.setPixelColor(i, pixels.Color(r,g,b));
else pixels.setPixelColor(i, pixels.Color(0,0,0)); //turn off all other LEDs
}
pixels.show();
}
}

3. 组装成可穿戴设备
将 Arduino 放在环内很方便 - 它的尺寸几乎完美匹配。电池也适合附近。不要忘记 uECG 是放在胸部上的——因此您需要一根带连接器的电线,首先放置它,然后穿上带有其他组件的衬衫,然后插入连接器。否则戴上它真的很不方便-相信我,我试过了))

 
pYYBAGPi-ImAJ5blAAlxQ5SW6zs489.jpg
所有组件到位
 
 
poYBAGPi-I2AIfaqAAF3Ff8hFR4918.jpg
我只是从里面把所有东西都粘在衬衫上
 

基本上就是这样 - 如果一切都正确完成,在您插入所有连接器后的 30 秒内,它将开始闪烁并指示 BPM。

4. 现场测试
我在步行和跑步时对其进行了测试 - 发现在跑步过程中,电池会在 ECG 传感器上方反弹,从而扭曲其读数。当我稍微移动它时,结果发现连接 uECG 和 Arduino 的电线太短,每一步都会拉动 ECG 传感器,再次扭曲读数。总的来说,我只有在行走和站立时才能获得可靠的节拍,但在跑步时却不行——但我认为我会改进这一点。传感器本身,当我将它与不同的衬衫一起使用时,在运行期间也能正确显示 BPM(通过其应用程序检查)。

此外,事实证明,胸部上的 LED 看起来很酷,但实际上毫无用处。低头看脉真的很不方便。我想在下一次迭代中,我会制作某种可以指示节拍的腕带。

PS 如果你对 uECG 项目感兴趣——你可以查看它的hackaday 页面,那里有很多技术细节、PCB 设计、讨论和项目日志


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !

'+ '

'+ '

'+ ''+ '
'+ ''+ ''+ '
'+ ''+ '' ); $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code ==5){ $(pop_this).attr('href',"/login/index.html"); return false } if(data.code == 2){ //跳转到VIP升级页面 window.location.href="//m.obk20.com/vip/index?aid=" + webid return false } //是会员 if (data.code > 0) { $('body').append(htmlSetNormalDownload); var getWidth=$("#poplayer").width(); $("#poplayer").css("margin-left","-"+getWidth/2+"px"); $('#tips').html(data.msg) $('.download_confirm').click(function(){ $('#dialog').remove(); }) } else { var down_url = $('#vipdownload').attr('data-url'); isBindAnalysisForm(pop_this, down_url, 1) } }); }); //是否开通VIP $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code == 2 || data.code ==5){ //跳转到VIP升级页面 $('#vipdownload>span').text("开通VIP 免费下载") return false }else{ // 待续费 if(data.code == 3) { vipExpiredInfo.ifVipExpired = true vipExpiredInfo.vipExpiredDate = data.data.endoftime } $('#vipdownload .icon-vip-tips').remove() $('#vipdownload>span').text("VIP免积分下载") } }); }).on("click",".download_cancel",function(){ $('#dialog').remove(); }) var setWeixinShare={};//定义默认的微信分享信息,页面如果要自定义分享,直接更改此变量即可 if(window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ var d={ title:'使用ECG的心跳指示器',//标题 desc:$('[name=description]').attr("content"), //描述 imgUrl:'https://'+location.host+'/static/images/ele-logo.png',// 分享图标,默认是logo link:'',//链接 type:'',// 分享类型,music、video或link,不填默认为link dataUrl:'',//如果type是music或video,则要提供数据链接,默认为空 success:'', // 用户确认分享后执行的回调函数 cancel:''// 用户取消分享后执行的回调函数 } setWeixinShare=$.extend(d,setWeixinShare); $.ajax({ url:"https://www.elecfans.com/app/wechat/index.php?s=Home/ShareConfig/index", data:"share_url="+encodeURIComponent(location.href)+"&format=jsonp&domain=m", type:'get', dataType:'jsonp', success:function(res){ if(res.status!="successed"){ return false; } $.getScript('https://res.wx.qq.com/open/js/jweixin-1.0.0.js',function(result,status){ if(status!="success"){ return false; } var getWxCfg=res.data; wx.config({ //debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId:getWxCfg.appId, // 必填,公众号的唯一标识 timestamp:getWxCfg.timestamp, // 必填,生成签名的时间戳 nonceStr:getWxCfg.nonceStr, // 必填,生成签名的随机串 signature:getWxCfg.signature,// 必填,签名,见附录1 jsApiList:['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ','onMenuShareWeibo','onMenuShareQZone'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: setWeixinShare.title, // 分享标题 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享给朋友”按钮点击状态及自定义分享内容接口 wx.onMenuShareAppMessage({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 type: setWeixinShare.type, // 分享类型,music、video或link,不填默认为link dataUrl: setWeixinShare.dataUrl, // 如果type是music或video,则要提供数据链接,默认为空 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ”按钮点击状态及自定义分享内容接口 wx.onMenuShareQQ({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口 wx.onMenuShareWeibo({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ空间”按钮点击状态及自定义分享内容接口 wx.onMenuShareQZone({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); }); }); } }); } function openX_ad(posterid, htmlid, width, height) { if ($(htmlid).length > 0) { var randomnumber = Math.random(); var now_url = encodeURIComponent(window.location.href); var ga = document.createElement('iframe'); ga.src = 'https://www1.elecfans.com/www/delivery/myafr.php?target=_blank&cb=' + randomnumber + '&zoneid=' + posterid+'&prefer='+now_url; ga.width = width; ga.height = height; ga.frameBorder = 0; ga.scrolling = 'no'; var s = $(htmlid).append(ga); } } openX_ad(828, '#berry-300', 300, 250);