一个外包项目,悬赏3000元。
要求:用指定芯片做一个指夹式血氧仪。
你猜项目成本可以被压缩到多少?
本次的项目作者,是3个大学生。
可别以为大学生经验少!
他们不但将成本压缩到了三位数,同时还保证了质量,做出了产品级指压式血氧仪。
最近,他们将项目进行了开源。
他们是如何实现产品级功能的?是如何进一步压缩成本的?
我们结合他们发布的开源资料,一起看看!
那么,想要在压缩成本的基础上,实现这些功能,该如何设计硬件代码?如何选型元器件?
在那之前,我们得先理解它的工作原理!
血液中,血红细胞的含氧血红蛋白(HbO2)和还原血红蛋白(Hb),对红光(660nm)和红外线(900nm)有不同的吸收能力。
指夹式血氧仪的工作原理就是:
在设备的同一位置,设置红光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”设计方案。
采用“660nm红光+900nm红外光”的双波长发射管,内部反向并联连接,通过上述H桥电路控制发射时序和发射功率。
原理图设计如下:
2.3 接收电路
接收电路采用RS622双路运放芯片作为核心。
前后级之间通过电容耦合,并与电阻构成高通滤波器,有效滤除直流信号。
原理图设计如下:
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.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