基于CW32L系列MCU的指夹式血氧仪 - 嘉立创EDA开源硬件平台

编辑器版本 ×
标准版 Standard

1、简单易用,可快速上手

2、流畅支持300个器件或1000个焊盘以下的设计规模

3、支持简单的电路仿真

4、面向学生、老师、创客

专业版 professional

1、全新的交互和界面

2、流畅支持超过3w器件或10w焊盘的设计规模,支持面板和外壳设计

3、更严谨的设计约束,更规范的流程

4、面向企业、更专业的用户

专业版 基于CW32L系列MCU的指夹式血氧仪

简介:本项目主要基于武汉芯源半导体出品的CW32L系列低功耗MCU,实现指夹式血氧仪产品设计。

开源协议: GPL 3.0

(未经作者授权,禁止转载)

已参加:星火计划2023

创建时间: 2023-07-24 10:57:19
更新时间: 2024-04-30 20:10:16
描述

项目说明

        本项目主要基于武汉芯源半导体出品的CW32L系列低功耗MCU,实现指夹式血氧仪产品设计。

 

开源协议

        本项目遵照GPL 3.0 开源协议,可以复制、修改和传播。

 

项目相关功能

(1)主控使用武汉芯源半导体有限公司推出的CW32低功耗系列MCU设计一个高精度手夹式血氧仪;

(2)使用屏幕显示,建议采用0.96inch TFT彩屏显示;

(3)锂电池供电,可充电;

(4)低弱灌注性能,最低可达到0.2%。(可保证在信号弱、儿童、失血多、肢体冰凉的低灌注的患者进行准确测量);

(5)光强自动调节。(可根据病人的手指大小自动调节发射光强,保证信号质量更好,功耗更低,可以使用不同大小的手指、不同皮肤颜色);

(6)优秀的环境光抵消功能。(可以在室内以及光线较强的临床环境使用);

(7)可测量血氧饱和度SpO2、脉率PR和灌注指数PI;

(8)可进行屏幕方向翻转;

(9)5s快速出测量结果;

(10)血氧饱和度和脉率超限报警;

(11)无手指自动关机;

(12)电池电量报警以及电池电量低自动关机。

 

项目属性

本项目为首次公开,为本人原创项目。项目未曾在别的比赛中获奖。

 

项目进度

(1)2023.3.24    项目启动

(2)2023.4.30    外观设计、原理图及PCB设计

(3)2023.5.30    软件设计

(4)2023.6.30    联合调试及问题整改

 

设计原理

(一)工作原理分析

        由于血液中的血红细胞,其中含氧血红蛋白(HbO2)和还原血红蛋白(Hb)这两种血红蛋白对红光(660nm)和红外线(900nm)有不同的吸收能力。还原血红蛋白(Hb)吸收的红光较多,红外线较少。而含氧血红蛋白(HbO2)则相反,它吸收的红光较少,红外线较多。通过在指夹式血氧仪的同一位置设置红光LED和红外线LED灯,当光线从手指的一面穿透到另一面,被光敏二极管接收后,可产生对应比例的电压。经过算法转换处理,将输出结果显示在液晶显示屏上,作为衡量人体健康指数的仪表直观显示出来。

(二)设计需求分析

        整体上,采用透射式方案,相比与传统的反射式方案(常见于智能手表)采样准确度提高数倍。

        【设计要求1】根据设计要求和实际性能需求,我们选择采用武汉芯源半导体有限公司推出的CW32L031C8T6芯片作为项目主控,该芯片除支持低功耗特性外,集成了主频高达 48MHz 的 ARM® Cortex®-M0+ 内核及高速嵌入式存储器(多至 64K 字节 FLASH 和多至 8K 字节 SRAM),提供三组UART、一组SPI和一组I2C通信接口,提供12 位高速 ADC和五组通用、基本定时器以及一组高级控制 PWM 定时器,外设资源完全满足项目需求。

       【设计要求2】屏幕选择采用0.96inch TFT彩屏,接口方式需要满足硬件及结构设计要求。

       【设计要求3】供电方面,需要设计USB TYPE-C外接和锂电池两种供电方式,并支持动态路径管理功能;锂电池尺寸及电量需要满足性能和结构设计要求。

       【设计要求4】为满足低弱灌注性能要求,需要设计信号多级滤波和放大电路,放大倍数以测试数据为依据合理选择。灌注性能通过灌注指数PI指标反映,即要求能测量出DC信号占比达到千分之2水平的AC信号。

       【设计要求5】根据病人的手指大小自动调节发射光强,需要设计可以通过PWM信号控制电压、进而控制恒流电流的光信号发射电路。

       【设计要求6】设计专门的遮光机制,以应对强环境光应用场景信号采集需求。

       【设计要求7】设计血氧饱和度SpO2%计算模型:先计算R值:R=(RED:AC/DC)/(IR:AC/DC),再通过R值计算血氧饱和度:SpO2%=110-R*25;脉率PR科通过PPG信号的AC部分过滤获得;设计灌注指数PI%计算模型:PI%=AC/DC *100%,范围为:0.2%-20%。

       【设计要求8】设计通过按键完成横屏、竖屏切换功能。

       【设计要求9】得益于CW32L031的高效数据处理能力,满足信号采样和FFT计算等任务要求,可在5s内快速得出测量结果。

       【设计要求10】设计超限报警机制,采用蜂鸣器和屏幕显示红色醒目数值,在声光两方面即时提醒用户数据异常。

       【设计要求11】设计通过代码监测采样信号,达到阈值自动关机,以节约电池电量。

       【设计要求12】通过设计电池电压监测功能,实现低电量报警及达到阈值电压以下自动关机功能。 

(三)设计方案

       由于本方案涉及功能模块和元器件较多,为保证各部分设计的准确性,采用两阶段设计方式进行。第一阶段设计了基于CW32L031C8T6的核心板和外部电路扩展板,对各部分电路及软件代码进行了独立测试和联合调试;第二阶段根据前期调试结果,设计了适合正式环境使用的电源板和主控板。限于篇幅,以下仅展示第二阶段设计方案。

1.电源板

1.1 设计思路

        电源支持USB外接供电、电池供电及电池充电等功能,整体架构包括电源路径管理及电池充电电路、5V供电电路和3.3V供电电路三个部分,如下图所示。

aSv6Cg0EAr7AxxxWPsI5gP0JCdQzk7URzYp9URHd.png

1.2 电源路径管理及电池充电电路

        电源路径管理电路采用P-MOS作为开关,通过G端电压与S端电压关系,实现USB供电与电池供电的动态切换功能。电池充电电路采用TC4056A芯片作为主控,依托其可编程充电电流控制、充电状态指示等功能,实现单节锂电池充电功能。USB接口增加过压、过流保护电路设计,防止插入瞬间尖峰电压对后级电路的冲击。增加D3二极管的目的是加速P-MOS导通,防止因供电方式切换导致主控掉电复位等问题。

        原理图设计如下。

zZc6hM5zvm180SjDN1neHV8T3SsQ42VRMIORXkRP.png

1.3 直流5V供电电路

        直流5V供电电路采用MT3608芯片搭建Sepic电路,确保在电池电压下降时也能稳定提供5V电压。

        原理图设计如下。

WhdMYXduzUwhdQCx1NSOW9xLaDWdlKn2gctAECts.png

1.4 直流3.3V供电电路

        直流3.3V供电电路采用AMS1117-3.3芯片构建LDO降压电路,稳定提供3.3V电压。

        原理图设计如下。

G9fzdaNphdgjYq5gSw2zX1OdmjFGN3uJKbQjHCev.png

1.5 PCB设计

ofwlJCtPhugKZD8IEMKwQk5Qinai10yWLw8hg7ou.png

2.   主控板

2.1 设计思路

        主控板包括MCU电路、发射电路、接收电路、按键电路、蜂鸣器电路及TFT显示屏电路等部分,用于实现血氧仪主要功能。

2.2 MCU电路

        MCU电路采用CW32L031C8T6作为主控芯片,设计BOOT电路、SWD烧录接口及复位按钮(不焊接),受空间限制取消外部晶振电路。

        原理图设计如下:

WlPUVyXmh1f4MzuvT4jlQOs4WTH47V4NMzmD3rc3.png

2.3 发射电路

        发射电路采用“RS2105+RS622”设计方案。由RS2105电子开关芯片构成双路开关电路,用于控制发射时序;由RS622芯片所包含的两路运算放大器搭配N沟道MOS管形成恒流源电路,通过PWM信号控制电流大小,以实现控制发射信号强弱的目的。采用“660nm红光+900nm红外光”的双波长发射管,内部反向并联连接,通过上述H桥电路控制发射时序和发射功率。

        原理图设计如下:

5tJoPIzksbEl6z6o3PGB2ZUHfNO79XNHIQEFT1P8.png

2.4 接收电路

        接收电路采用RS622双路运放芯片作为核心。前级与200KΩ电阻及电容构成跨阻放大电路,采集并放大“直流+交流”混合信号;后级通过负反馈200KΩ电阻构成信号放大电路,放大交流信号;前后级之间通过电容耦合,并与电阻构成高通滤波器,有效滤除直流信号。

        原理图设计如下:

VSoNziMizq3XNPtIh89aheGj3ue5X7uflbClzH3H.png

2.5 按键电路

        独立按键设计,采用1mm超薄按键,通过并联电容构成硬件消抖电路,通过电阻接入MCU的PB03引脚,按键按下为低电平(低电平有效)。

        原理图设计如下:

EjXZcDSlMCDWeIwYKXubWbaPaACbFbQAvRWFNzwk.png

2.6 蜂鸣器电路(当前版本PCB受空间限制已取消)

        蜂鸣器电路采用2KHz无源蜂鸣器作为核心元件,以N沟道MOS管作为开关,通过输出一定频率的PWM信号驱动蜂鸣器发声。

        原理图设计如下:

UfPPIJEQffZINrUPgo1aqOPlrYRWqog8fHGDrg5R.png

2.7 TFT显示屏电路

        TFT显示屏电路用于驱动0.96寸全彩LCD显示屏,设计8P抽屉式下接FPC接口,用于连接带软排线接口的显示屏。同时以PNP三极管作为开关,通过MCU输出一定占空比的PWM信号实现屏幕背光控制。

        原理图设计如下:

K8jyX6OxgbR8lLLH0OiAfABBQcn6s2onECi3XNXP.png

2.8 PCB设计

84X2OVPbVHJhRIsiEkfWEGihj9P5RBX1W2pEUYzw.png

 

软件说明

        主控采用武汉芯源半导体出品的CW32L031C8T6芯片,该芯片具有48MHz主频,能提供丰富的片上资源,低功耗特性十分适合电池供电应用场景。

1. TFT显示屏

(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);

}

(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秒。

LHK2Sp9AvAW7eMB4TWbUGSYMzv3kJa9YqKVy7mlp.png

        代码如下:(在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.1 FFT算法原理

        FFT是一种DFT的高效算法,称为快速傅立叶变换(fast Fourier transform)。傅里叶变换是时域一频域变换分析中最基本的方法之一。在数字处理领域应用的离散傅里叶变换(DFT:Discrete Fourier Transform)是许多数字信号处理方法的基础。

        DFT的运算如下:

gJdIFAqRiZ9jHb5ThlB6A7IdG5tcyEkW2Fmj1UKx.png

        其中,J344dtjE2K4y89C5Bu5CNSMcFqFpyJc6HxeHL9Aa.png

        FFT是一种DFT的高效算法,称为快速傅立叶变换(fast Fourier transform)。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为采样点数。

 

外观设计

        本次设计充分参考了主流品牌指夹式血氧仪外壳方案,通过对现有公模外壳的3D建模,完成本作品的外观设计方案。

 

zu00ZqOi80IxqiDmoJcHcgBxXKyNIwgsxQqiDi99.png

 

 

制作过程

(一)核心物料选择

1.   主控

        主控采用武汉芯源半导体出品的CW32L031C8T6芯片,该芯片具有48MHz主频,能提供丰富的片上资源,低功耗特性十分适合电池供电产品。

xAMHvX6FjsgM0N1xg7HcfFpxg2cNSqvjeZ9OMItR.png

 

2.   发射管、接收管

        血氧红外对管分别采用660-905nm双波长发射管和PD90接收管。其中发射管正接可发射905nm红外光,反接则可发射660nm可见红光。

scGcuhCTfdPR2rvRIh0Su3SxdGlXV8Wf5jTjSCAR.png

3.   TFT显示屏

        人机交互方面,考虑到指夹式血氧仪实际大小以及血氧饱和度和脉率超限时需要显示红色醒目字体以更好地提醒用户数值异常,采用0.96寸彩屏,并且支持横竖屏两种UI展示。

7kda1k0zuIXX9s6IAR5DAXlziCbVAYbrTG71kjx9.png nLu8QFxoYIql4vqrW3a4LYXqFXlRTRyWfJEpRN7Y.png

(二)PCB制作

1.   制作方法

        本项目所有PCB尺寸均在10cm*10cm范围内,因此可以通过嘉立创每月2次的免费打样机会进行制作。只需将利用立创EDA软件绘制的PCB文件导出为Gerber制板文件,然后通过下单助手发送给嘉立创即可完成。

2.   实物展示

(1)核心板

vsZrx7fs48GZ2IVlwG8veDPCCQ7OMomyehQEUoni.png    olQOM0WMTf5T2QQYovPfMfGb6WChs0lYievm58Hg.png

(2)扩展板

8hOLDaUSs2fE6DPfQy8bfH7eAQMJhffTEH0xF6pU.png FPYtVzP9yWnHAGcaUUB5FWRyBQop1vepDGW1QWjm.png 

(3)电源板

KE5HGqsAgmjw2BcjGkQMgiiEIbz5nCLymmM8tvaO.png XofsnSXi8tO6uE5TXMYyZD10oY5ZcX0gFovJuPdH.png 

(4)主控板

SiLxTIqN2iVLIiJigqsIsttx04g1Djlp04989rqL.png dZ5USkInLbNdYOiczy4tSAW05dbXOHIQGxUjPxqM.png 

(三)贴片焊接

1.   焊接方法

        由于受设备及PCB空间限制,大量采用0402及0603贴片封装元件,因此主要通过加热台进行焊接。对于部分排针、发射管、接收管、USB接口等直插元件,则在贴片元件焊接完成后,通过烙铁手工焊接。

2.   焊接难点与注意事项

        焊接难点主要集中在0.5mm引脚间距的各类芯片、Type-C接口及0402、0603封装的贴片元件上。对于LQFP封装的芯片,可以先通过加热台焊接。如果焊接后引脚存在连锡的情况,可以在引脚部位涂一点助焊剂,然后使用小刀口的烙铁沿着引脚自内向外的方向多刮几下,即可顺利去除多余的焊锡。

qVrtN8D1gIwTj01VmjI94p8ExA0RWqtBY3TQwk3u.png

3.   焊接成品展示

T6xCA4sJt84RO32kTWaS37NY81MQZJoTSQ4dVZOZ.png

ZDbgwX8pBknLSthHAh1jUNnKXiWRiM9GNaLwFH78.png

 

(四)功能测试与参数调试

1.   功能测试

1.1 发射功能

        利用逻辑分析仪对发射管控制信号进行了测试,第0通道和第1通道分别为RS2105的两路开关信号,第2通道和第3通道分别为控制电流大小的PWM信号,波形如下图所示。从图中可以看出,当第0通道为高电平(开关开启)时,第3通道输出PWM波形,然后所有通道关闭;当第1通道为高电平(开关开启)时,第2通道输出PWM波形,然后所有通道关闭,如此持续循环。上述信号符合设计方案要求。具体时序控制逻辑详见软件设计部分。

0TCkMksgUcKL2gf21NUPK5L2BUv5XjxSJmx6418R.png

1.2 接收功能

        在弱光环境下,通过示波器测量接收管接收到的信号波形如下所示。

a7NE01MoufRgPZmlf82fOtXvbNae8UYsBLLtjDtA.png

        进一步放大后,得到如下信号波形,符合预期采样效果。

Szr1hi2w6TWZw0fFW8FEDRGCcgdujgauJzKZ9Qrp.png

1.3 其他功能

        由于受设备及PCB空间限制,初步版本去除了蜂鸣器报警、电池电压监测等功能。待后续版本补充设计并完善。

2.   参数调试

        将上述接收波形,经放大、ADC采样、滤波等处理后,得到一系列采样值。将这些采样值,通过EXCEL处理并可视化后,得到如下折线图。在AC信号图上可以清晰看出脉冲信号的波形。

FVAqO6eyU7lzhSoudJO5TjwcXgBCrzUsNsObhU1J.png

q5wrHt47cH26FU6X1sREGcZKiURJ2XgGWECnsM79.png

        以128个采样数据为一组,经过FFT及相关公式计算,最终可以获得脉搏、PI及SPO2%等计算结果,并在显示屏上显示出来。

HTZi5gMhZx1gKiKKzNL3QuXLPbqP82MdrMYpP3TO.png

 

实物展示

gsreRd1LdQiVAiyIFE72D7CJ653nCQyoiimDziiW.png vEN2N5ymIFnSpfndp5rGJgZUXyIy8AqTejJ1Gq0A.png

DKcSxPYVUi8i8ZMyxqBNzer9Ug8HyXPcZw29rI4d.png A1R9MWE2173bJCovpAAXoA1AZPlSbLdiqcqqetPm.png

 

项目总结

        第一次参与由嘉立创和武汉芯源半导体发布的星火计划外包项目——指夹式血氧仪。刚看到该项目时,正好刚经历完第二轮疫情,因此感觉日常可见的血氧仪没那么复杂,于是凭借着一时冲动报了名。在项目完成过程中,各种硬件、软件问题层出不穷,从原理图设计、PCB制板、贴片元件焊接,到代码编写、信号调试、外壳制作、组装、文档编写,每一步都经历着对放弃和坚持的选择,每一个难题都需要通过大量的查阅资料、认真的学习研究和反复的动手实践才能彻底解决。正是这样一步一步走来,才有了上面的文档和真正的收获。直到目前,该项目还只是完成了最初的版本,尚有一些进阶功能还未实现,性能和准确性还有待提高,距离题目要求还有差距。因此,可能是出于对新人的鼓励,本项目得以顺利结题,我要真诚地感谢主办方提供这么好的学习平台和项目资源。同时,我也承诺将会对血氧仪的功能持续进行迭代完善。

 

 
 
 
设计图
原理图
1 /
PCB
1 /
未生成预览图,请在编辑器重新保存一次
工程视频/附件
序号 文件名称 下载次数
1

17a8646d6f4545bdcc42b01feef92d7b.mp4

334
2

血氧仪.rar

1296
侵权投诉
相关工程
换一批
加载中...
添加到专辑 ×

加载中...

温馨提示 ×

是否需要添加此工程到专辑?

温馨提示
动态内容涉嫌违规
内容:
  • 153 6159 2675

服务时间

周一至周五 9:00~18:00
  • 技术支持

support
  • 开源平台公众号

MP