有哪些有意思的,很cool的开源项目 ?

发布时间:
2023-08-24 12:46
阅读量:
11

一个外包项目,悬赏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·知乎号

关注我,看一手优质开源项目

END