有哪些有意思的,很cool的开源项目 ?
一个外包项目,悬赏3000元。
要求:用指定芯片做一个指夹式血氧仪。
你猜项目成本可以被压缩到多少?
本次的项目作者,是3个大学生。
可别以为大学生经验少!
他们不但将成本压缩到了三位数,同时还保证了质量,做出了产品级指压式血氧仪。
最近,他们将项目进行了开源。
他们是如何实现产品级功能的?是如何进一步压缩成本的?
我们结合他们发布的开源资料,一起看看!
项目功能
- 采用0.96inch TFT彩屏显示。
- 锂电池供电,可充电。
- 低弱灌注性能,最低可达到0.2%。可保证在信号弱、儿童、失血多、肢体冰凉的低灌注的患者进行准确测量。
- 光强自动调节。可根据病人的手指大小自动调节发射光强,保证信号质量更好,功耗更低,可以使用不同大小的手指、不同皮肤颜色。
- 优秀的环境光抵消功能。可以在室内以及光线较强的临床环境使用。
- 可测量血氧饱和度SpO2、脉率PR、灌注指数PI。
- 可进行屏幕方向翻转。
- 5s快速出测量结果。
- 血氧饱和度和脉率超限报警。
- 无手指自动关机。
- 电池电量报警以及电池电量低自动关机。
那么,想要在压缩成本的基础上,实现这些功能,该如何设计硬件代码?如何选型元器件?
在那之前,我们得先理解它的工作原理!
血氧仪工作原理
血液中,血红细胞的含氧血红蛋白(HbO2)和还原血红蛋白(Hb),对红光(660nm)和红外线(900nm)有不同的吸收能力。
- 还原血红蛋白(Hb)吸收的红光较多,红外线较少。
- 含氧血红蛋白(HbO2)吸收的红光较少,红外线较多。
指夹式血氧仪的工作原理就是:
在设备的同一位置,设置红光LED和红外线LED灯,测量血氧饱和度。
当光线从手指的一面穿透到另一面,就能检测两种血红蛋白对不同波长的光吸收的区别,所测出来的数据差被光敏二极管接收后,可产生对应比例的电压。就可以测出实际含氧量下,血氧饱和度最基本的数据和比值。
实际上要做到更高的精度,除了两个波长以外还要增加,甚至高达8个波长。
本项目为两个波长。
硬件设计思路
血氧仪由电源板和主控板组成,两块板子尺寸都不超过10cm*10cm,可以在嘉立创EDA免费打板,这样一来,就省下了PCB电路板的钱。
1.电源板
电源部分的设计,需要实现USB外接供电、电池供电、电池充电等功能。
因此整体架构包括——电源路径管理及电池充电电路、5V供电电路、3.3V供电电路。
电源板就主要围绕这三个部分,讲解设计思路。
1.1 电源路径管理及电池充电电路
电源路径管理电路采用P-MOS作为开关,通过G端电压与S端电压关系,实现USB供电与电池供电的动态切换功能。
电池充电电路采用TC4056A芯片作为主控,依托其可编程充电电流控制、充电状态指示等功能,实现单节锂电池充电功能。
USB接口增加过压、过流保护电路设计,防止插入瞬间尖峰电压对后级电路的冲击。
增加D3二极管的目的是加速P-MOS导通,防止因供电方式切换,导致主控掉电复位等问题。
原理图设计如下。
1.2 直流5V供电电路
直流5V供电电路采用MT3608芯片搭建Sepic电路,确保在电池电压下降时也能稳定提供5V电压。
原理图设计如下。
1.3 直流3.3V供电电路
直流3.3V供电电路采用AMS1117-3.3芯片构建LDO降压电路,稳定提供3.3V电压。
原理图设计如下。
1.4 PCB设计
2.主控板
主控板包括MCU电路、发射电路、接收电路、按键电路、蜂鸣器电路、TFT显示屏电路。
这六部分用于实现血氧仪主要功能。
下面也主要围绕这6个部分,讲解设计思路。
2.1 MCU电路
MCU电路采用CW32L031C8T6作为主控芯片,设计BOOT电路、SWD烧录接口及复位按钮(不焊接),受空间限制,取消外部晶振电路。
原理图设计如下:
2.2 发射电路
发射电路采用“RS2105+RS622”设计方案。
- 由RS2105电子开关芯片构成双路开关电路,用于控制发射时序;
- 由RS622芯片所包含的两路运算放大器搭配N沟道MOS管形成恒流源电路,通过PWM信号控制电流大小,以实现控制发射信号强弱的目的。
采用“660nm红光+900nm红外光”的双波长发射管,内部反向并联连接,通过上述H桥电路控制发射时序和发射功率。
原理图设计如下:
2.3 接收电路
接收电路采用RS622双路运放芯片作为核心。
- 前级与200KΩ电阻及电容构成跨阻放大电路,采集并放大“直流+交流”混合信号;
- 后级通过负反馈200KΩ电阻构成信号放大电路,放大交流信号;
前后级之间通过电容耦合,并与电阻构成高通滤波器,有效滤除直流信号。
原理图设计如下:
2.4 按键电路
独立按键设计,采用1mm超薄按键,通过并联电容构成硬件消抖电路,通过电阻接入MCU的PB03引脚,按键按下为低电平(低电平有效)。
原理图设计如下:
2.5 蜂鸣器电路(当前版本PCB受空间限制已取消)
蜂鸣器电路采用2KHz无源蜂鸣器作为核心元件,以N沟道MOS管作为开关,通过输出一定频率的PWM信号驱动蜂鸣器发声。
原理图设计如下:
2.6 TFT显示屏电路
TFT显示屏电路用于驱动0.96寸全彩LCD显示屏。
设计8P抽屉式下接FPC接口,用于连接带软排线接口的显示屏。同时以PNP三极管作为开关,通过MCU输出一定占空比的PWM信号实现屏幕背光控制。
原理图设计如下:
2.7 PCB设计
软件说明
软件部分,虽然需要很多的时间成本,但不怎么需要花钱。
而作为有彩屏“智能交互”功能的“产品”,软件部分尤为重要。
本章说明一下这三个部分:TFT显示屏、FFT算法实现、FFT结果运用。
1. TFT显示屏(2部分)
1.1 LCD初始化
#include "LCD_INIT.h"
/******************************************************************************
函数说明:LCD复位函数
入口数据:无
返回值: 无
******************************************************************************/
void Lcd_Reset(void)
{
LCD_RES_Clr();
FirmwareDelay(100);
LCD_RES_Set();
FirmwareDelay(100);
}
/******************************************************************************
函数说明:LCD_GPIO初始化函数
入口数据:dat 要写入的串行数据
返回值: 无
******************************************************************************/
void LCD_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
__RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.IT = GPIO_IT_NONE;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pins = GPIO_PIN_2| GPIO_PIN_3| GPIO_PIN_4| GPIO_PIN_5|GPIO_PIN_8;
GPIO_Init(CW_GPIOA, &GPIO_InitStruct);
}
/******************************************************************************
函数说明:LCD串行数据写入函数
入口数据:dat 要写入的串行数据
返回值: 无
******************************************************************************/
void LCD_Writ_Bus(uint8_t dat)
{
uint8_t i;
LCD_CS_Clr();
for(i=0;i<8;i++)
{
LCD_SCLK_Clr();
if(dat&0x80)
{
LCD_MOSI_Set();
}
else
{
LCD_MOSI_Clr();
}
LCD_SCLK_Set();
dat<<=1;
}
LCD_CS_Set();
}
/******************************************************************************
函数说明:LCD写入8位数据
入口数据:dat 写入的数据
返回值: 无
******************************************************************************/
void Lcd_WriteData(uint8_t dat)
{
LCD_Writ_Bus(dat);
}
/******************************************************************************
函数说明:LCD写入16位数据
入口数据:dat 写入的数据
返回值: 无
******************************************************************************/
void LCD_WR_DATA(uint16_t dat)
{
LCD_Writ_Bus(dat>>8);
LCD_Writ_Bus(dat);
}
/******************************************************************************
函数说明:LCD写入命令
入口数据:dat 写入的命令
返回值: 无
******************************************************************************/
void Lcd_WriteIndex(uint8_t dat)
{
LCD_DC_Clr();//写命令
LCD_Writ_Bus(dat);
LCD_DC_Set();//写数据
}
/******************************************************************************
函数说明:设置起始和结束地址
入口数据:x1,x2 设置列的起始和结束地址
y1,y2 设置行的起始和结束地址
返回值: 无
******************************************************************************/
void LCD_Address_Set(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2)
{
if(USE_HORIZONTAL==0)
{
Lcd_WriteIndex(0x2a);//列地址设置
LCD_WR_DATA(x1+26);
LCD_WR_DATA(x2+26);
Lcd_WriteIndex(0x2b);//行地址设置
LCD_WR_DATA(y1+1);
LCD_WR_DATA(y2+1);
Lcd_WriteIndex(0x2c);//储存器写
}
else if(USE_HORIZONTAL==1)
{
Lcd_WriteIndex(0x2a);//列地址设置
LCD_WR_DATA(x1+26);
LCD_WR_DATA(x2+26);
Lcd_WriteIndex(0x2b);//行地址设置
LCD_WR_DATA(y1+1);
LCD_WR_DATA(y2+1);
Lcd_WriteIndex(0x2c);//储存器写
}
else if(USE_HORIZONTAL==2)
{
Lcd_WriteIndex(0x2a);//列地址设置
LCD_WR_DATA(x1+1);
LCD_WR_DATA(x2+1);
Lcd_WriteIndex(0x2b);//行地址设置
LCD_WR_DATA(y1+26);
LCD_WR_DATA(y2+26);
Lcd_WriteIndex(0x2c);//储存器写
}
else
{
Lcd_WriteIndex(0x2a);//列地址设置
LCD_WR_DATA(x1+1);
LCD_WR_DATA(x2+1);
Lcd_WriteIndex(0x2b);//行地址设置
LCD_WR_DATA(y1+26);
LCD_WR_DATA(y2+26);
Lcd_WriteIndex(0x2c);//储存器写
}
}
/******************************************************************************
函数说明:LCD初始化代码
入口数据:无
返回值: 无
******************************************************************************/
void LCD_Init(void)
{
LCD_GPIO_Init();//初始化GPIO
LCD_RES_Clr();//复位
FirmwareDelay(1);
LCD_RES_Set();
//FirmwareDelay(1);
//LCD_BLK_Set();//打开背光
// FirmwareDelay(1);
Lcd_WriteIndex(0x11); //Sleep out
//FirmwareDelay(1); //Delay 120ms
Lcd_WriteIndex(0xB1); //Normal mode
Lcd_WriteData(0x05);
Lcd_WriteData(0x3C);
Lcd_WriteData(0x3C);
Lcd_WriteIndex(0xB2); //Idle mode
Lcd_WriteData(0x05);
Lcd_WriteData(0x3C);
Lcd_WriteData(0x3C);
Lcd_WriteIndex(0xB3); //Partial mode
Lcd_WriteData(0x05);
Lcd_WriteData(0x3C);
Lcd_WriteData(0x3C);
Lcd_WriteData(0x05);
Lcd_WriteData(0x3C);
Lcd_WriteData(0x3C);
Lcd_WriteIndex(0xB4); //Dot inversion
Lcd_WriteData(0x03);
Lcd_WriteIndex(0xC0); //AVDD GVDD
Lcd_WriteData(0xAB);
Lcd_WriteData(0x0B);
Lcd_WriteData(0x04);
Lcd_WriteIndex(0xC1); //VGH VGL
Lcd_WriteData(0xC5); //C0
Lcd_WriteIndex(0xC2); //Normal Mode
Lcd_WriteData(0x0D);
Lcd_WriteData(0x00);
Lcd_WriteIndex(0xC3); //Idle
Lcd_WriteData(0x8D);
Lcd_WriteData(0x6A);
Lcd_WriteIndex(0xC4); //Partial+Full
Lcd_WriteData(0x8D);
Lcd_WriteData(0xEE);
Lcd_WriteIndex(0xC5); //VCOM
Lcd_WriteData(0x0F);
Lcd_WriteIndex(0xE0); //positive gamma
Lcd_WriteData(0x07);
Lcd_WriteData(0x0E);
Lcd_WriteData(0x08);
Lcd_WriteData(0x07);
Lcd_WriteData(0x10);
Lcd_WriteData(0x07);
Lcd_WriteData(0x02);
Lcd_WriteData(0x07);
Lcd_WriteData(0x09);
Lcd_WriteData(0x0F);
Lcd_WriteData(0x25);
Lcd_WriteData(0x36);
Lcd_WriteData(0x00);
Lcd_WriteData(0x08);
Lcd_WriteData(0x04);
Lcd_WriteData(0x10);
Lcd_WriteIndex(0xE1); //negative gamma
Lcd_WriteData(0x0A);
Lcd_WriteData(0x0D);
Lcd_WriteData(0x08);
Lcd_WriteData(0x07);
Lcd_WriteData(0x0F);
Lcd_WriteData(0x07);
Lcd_WriteData(0x02);
Lcd_WriteData(0x07);
Lcd_WriteData(0x09);
Lcd_WriteData(0x0F);
Lcd_WriteData(0x25);
Lcd_WriteData(0x35);
Lcd_WriteData(0x00);
Lcd_WriteData(0x09);
Lcd_WriteData(0x04);
Lcd_WriteData(0x10);
Lcd_WriteIndex(0xFC);
Lcd_WriteData(0x80);
Lcd_WriteIndex(0x3A);
Lcd_WriteData(0x05);
Lcd_WriteIndex(0x36);
if(USE_HORIZONTAL==0)Lcd_WriteData(0x08);
else if(USE_HORIZONTAL==1)Lcd_WriteData(0xC8);
else if(USE_HORIZONTAL==2)Lcd_WriteData(0x78);
else Lcd_WriteData(0xA8);
Lcd_WriteIndex(0x21); //Display inversion
Lcd_WriteIndex(0x29); //Display on
Lcd_WriteIndex(0x2A); //Set Column Address
Lcd_WriteData(0x00);
Lcd_WriteData(0x1A); //26
Lcd_WriteData(0x00);
Lcd_WriteData(0x69); //105
Lcd_WriteIndex(0x2B); //Set Page Address
Lcd_WriteData(0x00);
Lcd_WriteData(0x01); //1
Lcd_WriteData(0x00);
Lcd_WriteData(0xA0); //160
Lcd_WriteIndex(0x2C);
}
1.2 LCD主要功能函数
#include "LCD.h"
#include "LCD_INIT.h"
#include "LCD_FONT.h"
/******************************************************************************
函数说明:在指定区域填充颜色
入口数据:xsta,ysta 起始坐标
xend,yend 终止坐标
color 要填充的颜色
返回值: 无
******************************************************************************/
void LCD_Fill(uint16_t xsta,uint16_t ysta,uint16_t xend,uint16_t yend,uint16_t color)
{
uint16_t i = ysta;
uint16_t j = xsta;
LCD_Address_Set(xsta,ysta,xend-1,yend-1);//设置显示范围
for(i=ysta;i<yend;i++)
{
for(j=xsta;j<xend;j++)
{
LCD_WR_DATA(color);
}
}
}
/******************************************************************************
函数说明:在指定位置画点
入口数据:x,y 画点坐标
color 点的颜色
返回值: 无
******************************************************************************/
void LCD_DrawPoint(uint16_t x,uint16_t y,uint16_t color)
{
LCD_Address_Set(x,y,x,y);//设置光标位置
LCD_WR_DATA(color);
}
/******************************************************************************
函数说明:显示单个字符
入口数据:x,y显示坐标
num 要显示的字符
fc 字的颜色
bc 字的背景色
sizey 字号
mode: 0非叠加模式 1叠加模式
返回值: 无
******************************************************************************/
void LCD_ShowChar(uint16_t x,uint16_t y,uint8_t num,uint16_t fc,uint16_t bc,uint8_t sizey,uint8_t mode)
{
uint8_t temp,sizex,t,m=0;
uint16_t i,TypefaceNum;//一个字符所占字节大小
uint16_t x0=x;
sizex=sizey/2;
if(sizey==30)sizex=19;
else num=num-' '; //得到偏移后的值
TypefaceNum=(sizex/8+((sizex%8)?1:0))*sizey;
LCD_Address_Set(x,y,x+sizex-1,y+sizey-1); //设置光标位置
for(i=0;i<TypefaceNum;i++)
{
if(sizey==24)temp=ascii_2412[num][i]; //调用12x24字体
else if(sizey==16)temp=ascii_1608[num][i]; //调用16x32字体
else if(sizey==30)temp=int_1930[(num+1)*90+i]; //调用16x32字体
else return;
for(t=0;t<8;t++)
{
if(!mode)//非叠加模式
{
if(temp&(0x01<<t))LCD_WR_DATA(fc);
else LCD_WR_DATA(bc);
m++;
if(m%sizex==0)
{
m=0;
break;
}
}
else//叠加模式
{
if(temp&(0x01<<t))LCD_DrawPoint(x,y,fc);//画一个点
x++;
if((x-x0)==sizex)
{
x=x0;
y++;
break;
}
}
}
}
}
/******************************************************************************
函数说明:显示字符串
入口数据:x,y显示坐标
*p 要显示的字符串
fc 字的颜色
bc 字的背景色
sizey 字号
mode: 0非叠加模式 1叠加模式
返回值: 无
******************************************************************************/
void LCD_ShowString(uint16_t x,uint16_t y,const uint8_t *p,uint16_t fc,uint16_t bc,uint8_t sizey,uint8_t mode)
{
while(*p!='\0')
{
LCD_ShowChar(x,y,*p,fc,bc,sizey,mode);
x+=sizey/2;
p++;
}
}
/******************************************************************************
函数说明:显示数字所用的辅助函数
入口数据:m底数,n指数
返回值: 无
******************************************************************************/
uint32_t mypow(uint8_t m,uint8_t n)
{
uint32_t result=1;
while(n--)result*=m;
return result;
}
(因提示存在敏感词省略部分函数,详见附件代码集)
// 横屏 UI 初始化
void transverse_UI_init()
{
LCD_Init();
LCD_Fill(0,0,160,80,BLACK);
LCD_ShowString(0,0,(const unsigned char*)"%Sp0",YELLOW,BLACK,24,1);
LCD_ShowString(48,8,(const unsigned char*)"2",YELLOW,BLACK,16,1);
LCD_ShowString(112,0,(const unsigned char*)"PR",YELLOW,BLACK,24,1);
LCD_ShowString(136,8,(const unsigned char*)"bpm",YELLOW,BLACK,16,1);
LCD_ShowBattey(56,0,1);
LCD_ShowString(63,24,(const unsigned char*)"PI %:",WHITE,BLACK,16,1);
LCD_Fill(12,40,24,46,GREEN);LCD_Fill(30,40,42,46,GREEN);
LCD_Fill(119,40,132,46,GREEN);LCD_Fill(138,40,151,46,GREEN);
}
// 竖屏 UI 初始化
void Vertical_UI_init()
{
LCD_Init();
LCD_Fill(0,0,80,160,BLACK);
LCD_ShowString(0,0,(const unsigned char*)"Sp0",YELLOW,BLACK,24,1);
LCD_ShowString(36,8,(const unsigned char*)"2",YELLOW,BLACK,16,1);
LCD_ShowString(40,0,(const unsigned char*)" %",YELLOW,BLACK,24,1);
LCD_ShowString(0,54,(const unsigned char*)"PR ",YELLOW,BLACK,24,1);
LCD_ShowString(24,62,(const unsigned char*)"bpm",YELLOW,BLACK,16,1);
LCD_ShowBattey(113,58,2);
LCD_ShowString(0,114,(const unsigned char*)"PI %:",WHITE,BLACK,16,1);
LCD_Fill(12,36,26,42,GREEN);LCD_Fill(30,36,44,42,GREEN);
LCD_Fill(12,90,26,96,GREEN);LCD_Fill(30,90,44,96,GREEN);
}
2.时序控制
控制时序说明:
- 每次发射(采样)包括四个阶段(IR发射、停止发射、RED发射、停止发射)
- 每阶段3ms,共计12ms;
- 之后为27ms的延迟(停止发射);
- 上述为一个完整发射循环,每个循环为39ms;
- 完整发射(采样)周期包括128次发射循环,共计4.992秒。
代码如下:(在BTIM1定时器中断回调函数中实现)
void BTIM1_IRQHandlerCallback(void)
{
if(SET == BTIM_GetITStatus(CW_BTIM1, BTIM_IT_OV))
{
BTIM_ClearITPendingBit(CW_BTIM1, BTIM_IT_OV);
if(IsCycleEnd == 1) //128次采样周期结束标志:1为采样中,0为采样结束
{
if(BTIM1_counter3 > 2) //计时达到3ms
{
BTIM1_counter3 = 0;
switch(SEND_status)
{
case 0: //发射红外信号(3ms)
{
SEND_status ++;
GPIO_WritePin(bsp_IN1_port, bsp_IN1_pin, GPIO_Pin_RESET);
GPIO_WritePin(bsp_IN2_port, bsp_IN2_pin, GPIO_Pin_SET);
DAC1_PWM = 0;
DAC2_PWM = 300 + DAC_PWM_PLUS;
GTIM_SetCompare1(CW_GTIM2, DAC1_PWM); //设置DAC1占空比为0
GTIM_SetCompare2(CW_GTIM2, DAC2_PWM); //设置DAC2占空比为300+调整值
GTIM_Cmd(CW_GTIM2, ENABLE);
IRorRED = 0; //设置红外或红光标志:红外
ADC_SoftwareStartConvCmd(ENABLE); //启动ADC转换
break;
}
case 1: //关闭信号发射(3ms)
{
SEND_status ++;
GPIO_WritePin(bsp_IN1_port, bsp_IN1_pin, GPIO_Pin_RESET);
GPIO_WritePin(bsp_IN2_port, bsp_IN2_pin, GPIO_Pin_RESET);
DAC1_PWM = 0;
DAC2_PWM = 0;
GTIM_SetCompare1(CW_GTIM2, DAC1_PWM); //设置占空比为0
GTIM_SetCompare2(CW_GTIM2, DAC2_PWM); //设置占空比为0
GTIM_Cmd(CW_GTIM2, DISABLE);
//ADC_SoftwareStartConvCmd(DISABLE);
break;
}
case 2: //发射红光信号(3ms)
{
SEND_status ++;
GPIO_WritePin(bsp_IN1_port, bsp_IN1_pin, GPIO_Pin_SET);
GPIO_WritePin(bsp_IN2_port, bsp_IN2_pin, GPIO_Pin_RESET);
DAC1_PWM = 300 + DAC_PWM_PLUS;
DAC2_PWM = 0;
GTIM_SetCompare1(CW_GTIM2, DAC1_PWM); //设置DAC1占空比为300+调整值
GTIM_SetCompare2(CW_GTIM2, DAC2_PWM); //设置DAC2占空比为0
GTIM_Cmd(CW_GTIM2, ENABLE);
IRorRED = 1; //设置红外或红光标志:红光
ADC_SoftwareStartConvCmd(ENABLE); //启动ADC转换
break;
}
case 3: //关闭信号发射(4ms)
{
SEND_status = 0;
GPIO_WritePin(bsp_IN1_port, bsp_IN1_pin, GPIO_Pin_RESET);
GPIO_WritePin(bsp_IN2_port, bsp_IN2_pin, GPIO_Pin_RESET);
DAC1_PWM = 0;
DAC2_PWM = 0;
GTIM_SetCompare1(CW_GTIM2, DAC1_PWM); //设置占空比为0
GTIM_SetCompare2(CW_GTIM2, DAC2_PWM); //设置占空比为0
GTIM_Cmd(CW_GTIM2, DISABLE);
//ADC_SoftwareStartConvCmd(DISABLE);
break;
}
}
}
else
{
BTIM1_counter3++;
}
}
}
}
3.算法设计(3部分)
3.1 FFT算法原理
FFT是一种DFT的高效算法,称为快速傅立叶变换(fast Fourier transform)。
DFT的运算如下:
FFT算法可分为按时间抽取算法和按频率抽取算法。
- 这种方法计算DFT对于X(K)的每个K值,需要进行4N次实数相乘和(4N-2)次相加。
- 对于N个k值,共需N*N乘和N(4N-2)次实数相加。
改进DFT算法,减小它的运算量,利用DFT中的周期性和对称性,使整个DFT的计算变成一系列迭代运算,可大幅度提高运算过程和运算量,这就是FFT的基本思想。
3.2 FFT算法实现
(1)计算三角函数表
//保存SIN值
signed char SIN_TAB[128]={
0x00, 0x06, 0x0c, 0x12, 0x18, 0x1e, 0x24, 0x2a,
0x30, 0x36, 0x3b, 0x41, 0x46, 0x4b, 0x50, 0x55,
0x59, 0x5e, 0x62, 0x66, 0x69, 0x6c, 0x70, 0x72,
0x75, 0x77, 0x79, 0x7b, 0x7c, 0x7d, 0x7e, 0x7e,
0x7f, 0x7e, 0x7e, 0x7d, 0x7c, 0x7b, 0x79, 0x77,
0x75, 0x72, 0x70, 0x6c, 0x69, 0x66, 0x62, 0x5e,
0x59, 0x55, 0x50, 0x4b, 0x46, 0x41, 0x3b, 0x36,
0x30, 0x2a, 0x24, 0x1e, 0x18, 0x12, 0x0c, 0x06,
0x00, -0x06, -0x0c, -0x12, -0x18, -0x1e, -0x24, -0x2a,
-0x30, -0x36, -0x3b, -0x41, -0x46, -0x4b, -0x50, -0x55,
-0x59, -0x5e, -0x62, -0x66, -0x69, -0x6c, -0x70, -0x72,
-0x75, -0x77, -0x79, -0x7b, -0x7c, -0x7d, -0x7e, -0x7e,
-0x7f, -0x7e, -0x7e, -0x7d, -0x7c, -0x7b, -0x79, -0x77,
-0x75, -0x72, -0x70, -0x6c, -0x69, -0x66, -0x62, -0x5e,
-0x59, -0x55, -0x50, -0x4b, -0x46, -0x41, -0x3b, -0x36,
-0x30, -0x2a, -0x24, -0x1e, -0x18, -0x12, -0x0c, -0x06};
//以下是放大128倍后的cos余弦函数数组表格,这里注意事项与上面相同,只不过选择余弦来生成
signed char COS_TAB[128]={
0x7f, 0x7e, 0x7e, 0x7d, 0x7c, 0x7b, 0x79, 0x77,
0x75, 0x72, 0x70, 0x6c, 0x69, 0x66, 0x62, 0x5e,
0x59, 0x55, 0x50, 0x4b, 0x46, 0x41, 0x3b, 0x36,
0x30, 0x2a, 0x24, 0x1e, 0x18, 0x12, 0x0c, 0x06,
0x00, -0x06, -0x0c, -0x12, -0x18, -0x1e, -0x24, -0x2a,
-0x30, -0x36, -0x3b, -0x41, -0x46, -0x4b, -0x50, -0x55,
-0x59, -0x5e, -0x62, -0x66, -0x69, -0x6c, -0x70, -0x72,
-0x75, -0x77, -0x79, -0x7b, -0x7c, -0x7d, -0x7e, -0x7e,
-0x7f, -0x7e, -0x7e, -0x7d, -0x7c, -0x7b, -0x79, -0x77,
-0x75, -0x72, -0x70, -0x6c, -0x69, -0x66, -0x62, -0x5e,
-0x59, -0x55, -0x50, -0x4b, -0x46, -0x41, -0x3b, -0x36,
-0x30, -0x2a, -0x24, -0x1e, -0x18, -0x12, -0x0c, -0x06,
0x00, 0x06, 0x0c, 0x12, 0x18, 0x1e, 0x24, 0x2a,
0x30, 0x36, 0x3b, 0x41, 0x46, 0x4b, 0x50, 0x55,
0x59, 0x5e, 0x62, 0x66, 0x69, 0x6c, 0x70, 0x72,
0x75, 0x77, 0x79, 0x7b, 0x7c, 0x7d, 0x7e, 0x7e};
unsigned char LIST_TAB[128]={
0,64,32,96,16,80,48,112,
8,72,40,104,24,88,56,120,
4,68,36,100,20,84,52,116,
12,76,44,108,28,92,60,124,
2,66,34,98,18,82,50,114,
10,74,42,106,26,90,58,122,
6,70,38,102,22,86,54,118,
14,78,46,110,30,94,62,126,
1,65,33,97,17,81,49,113,
9,73,41,105,25,89,57,121,
5,69,37,101,21,85,53,117,
13,77,45,109,29,93,61,125,
3,67,35,99,19,83,51,115,
11,75,43,107,27,91,59,123,
7,71,39,103,23,87,55,119,
15,79,47,111,31,95,63,127};
(2)FFT函数
void Fft_Imagclear(void) //fft虚部清零函数,在运行FFT函数之前需要先运行这个
{
unsigned char a; //注意这里如果是256点以上要改成u16,下面的a<128条件也要相应的修改
for(a=0;a<128;a++)
{
Fft_Image[a]=0;
}
}
signed short Fft_Real[128]; //fft实部,128数组
signed short Fft_Image[128]; //fft虚部,128数组
void FFT(void)
{
unsigned char i,j,k,b,p;
signed short Temp_Real,Temp_Imag,temp; //中间临时变量,名称也是自己定义的,但要与fft函数里面的对应
//unsigned short TEMP1; //用于求功率的,可不需要
unsigned char N=7; //这里因为128是2的7次方,如果是计算256点,则是2的8次方,N就是8,如果是512点则N=9,如此类推
unsigned short NUM_FFT=128; //这里要算多少点的fft就赋值多少,值只能是2的N次方
for( i=1; i<=N; i++) /* for(1) */
{
b=1;
b <<=(i-1); //蝶式运算,用于计算 隔多少行计算。例如第一级 1和2行计算,,,第二级
for( j=0; j<=b-1; j++) /* for (2) */
{
p=1;
p <<= (N-i);
p = p*j;
for( k=j; k<NUM_FFT; k=k+2*b) /* for (3) 基二fft */
{
Temp_Real = Fft_Real[k]; Temp_Imag = Fft_Image[k]; temp = Fft_Real[k+b];
Fft_Real[k] = Fft_Real[k] + ((Fft_Real[k+b]*COS_TAB[p])>>7) + ((Fft_Image[k+b]*SIN_TAB[p])>>7);
Fft_Image[k] = Fft_Image[k] - ((Fft_Real[k+b]*SIN_TAB[p])>>7) + ((Fft_Image[k+b]*COS_TAB[p])>>7);
Fft_Real[k+b] = Temp_Real - ((Fft_Real[k+b]*COS_TAB[p])>>7) - ((Fft_Image[k+b]*SIN_TAB[p])>>7);
Fft_Image[k+b] = Temp_Imag + ((temp*SIN_TAB[p])>>7) - ((Fft_Image[k+b]*COS_TAB[p])>>7);
//移位,防止溢出。结果已经是本值的1/64
Fft_Real[k] >>= 1;
Fft_Image[k] >>= 1;
Fft_Real[k+b] >>= 1;
Fft_Image[k+b] >>= 1;
}
}
}
}
///注意:以上已经把128点的实部和虚部求完,下一次运算前需要把所有虚部重新清零
signed short Get_fft_value(int n,int m) //获取FFT结果的实部或虚部
{
if(n==0) return Fft_Real[m];
else return Fft_Image[m];
}
3.3 FFT结果运用
(1):直接用某个频率点的值,可以做音频频谱强度显示
第n个频率点的值是数组上的Fft_Real[n]和Fft_Image[n]
(2):求某个频率点的模
模值=根号(实部平方+虚部平方),即sqrt((Fft_Real[n]*Fft_Real[n])+(Fft_Image[n]*Fft_Image[n]))
(3):清除特定频率的分量,一般用于数字滤波算法
Fft_Real[0]=Fft_Image[0]=0; //去掉直流分量,即将第0项的值清零
Fft_Real[63]=Fft_Image[63]=0; //要去除某个频率的分量,可将该频率对应的数组项的值清零
Fft_Real[0]是直流分量。Fft_Real[1]是最低频率点,也是最小频率分辨率值
说明:分辨率=采样率/采样点数N,波形峰值大小=模值/(N/2), N为采样点数。
很好,搞定了软硬件,再最后盘一下完整的选型+制作+测试过程吧!
相信看过后,在我公布成本价前,你心里也会有一个大概的成本价了!
制作过程
1.设计外观
外观设计参考主流品牌外壳方案。
用3D打印的方式制作外壳,尺寸小,成本也低。
2.核心物料选择
主控采用CW32L031C8T6芯片,单买11元1个。
血氧红外对管分别采用:
- 660-905nm双波长发射管
- PD90接收管
其中发射管正接可发射905nm红外光,反接则可发射660nm可见红光。
TFT显示屏适合人机交互,我采用0.96寸彩屏,支持横竖屏两种UI展示。
3.PCB制作与焊接
- 本项目的PCB尺寸在嘉立创免费打样范围内。
- 本次焊接主要通过加热台,因为大量采用了0402及0603贴片封装元件。贴片焊接完成后,再通过烙铁手工焊接排针、发射管、接收管、USB接口等直插元件。
- 对于LQFP封装的芯片,如果引脚连锡,可以在引脚部位涂一点助焊剂,然后使用小刀口的烙铁沿着引脚自内向外的方向多刮几下,即可顺利去除多余的焊锡。
4.功能测试与参数调试
利用逻辑分析仪对发射管控制信号进行了测试。从图中可以看出:
- 当第0通道为高电平(开关开启)时,第3通道输出PWM波形,然后所有通道关闭;
- 当第1通道为高电平(开关开启)时,第2通道输出PWM波形,然后所有通道关闭,如此持续循环。
上述信号符合设计方案要求。
具体时序控制逻辑详见软件设计部分。
在弱光环境下,通过示波器测量接收管接收到的信号波形如下所示。
进一步放大后,得到如下信号波形,符合预期采样效果。
将上述接收波形,经放大、ADC采样、滤波等处理后,得到一系列采样值。
将这些采样值,通过EXCEL处理并可视化后,得到如下折线图。
在AC信号图上可以清晰看出脉冲信号的波形。
以128个采样数据为一组,经过FFT及相关公式计算,最终可以获得脉搏、PI及SPO2%等计算结果,并在显示屏上显示出来。
全部算下来,项目的总成本仅100元。
怎么样,是不是感觉这3位大学生非常优秀?未来可期呢?
据说团队中的两人正在参加电赛,预祝他们此次比赛顺利!
文章的最后,小编想说,希望这样的外包项目活动能多多举办,让刚好想做项目的人,有目标,有作品,还能有奖金回血,一举三得!
参考资料:
[1]基于CW32L系列MCU的指夹式血氧仪 - 嘉立创EDA开源硬件平台
[2]星火计划_外包赛道_每周更新外包项目_奖金2000-8000不等
— 完 —
嘉立创EDA·知乎号
关注我,看一手优质开源项目