自制心率血氧仪

软硬件资料:https://github.com/CatIsBest/Oximeter
演示视频:https://www.bilibili.com/video/BV1eD4y1v7tK/?vd_source=272132e613b16a2fd3bd189c2fa3be6d

前言

这个小项目主要是为了找工作而做的,毕竟之前工作项目的内容不可能放出来,所以还是有必要做一个自己的小项目写简历上,这样找工作也方便一点。

关于我的专业
我的专业是生物医学工程,这是个电子信息类专业!!!很多人看到这个专业名往往会以为是生物专业或者医学专业,但它其实是电子信息专业套了个皮(至少本科阶段是这样)。

关于我的工作经历
21年6月底回家之后在家附近找了个小家电控制板厂,做OTP单片机(应广的)和51单片机(忘了哪个牌子的)软件编程,做了不到半个月觉得不合适就离开了(它原本的单片机工程师跑路了)。

由于觉得STM32对工作很有必要,花了两个多月时间补了一下STM32知识(大学的时候主要用51单片机,STM32我大学时只是买了开发板然后改了改例程跑了跑),学了一下freertos,然后去找工作。

然后在一个电动观光车公司做车载电子设备,做了差不多9个月。后面因为觉得工作内容太杂了,而且当时我想去医疗电子行业,所以辞职了。
我在那里的工作内容是软硬件都做,有一些需求是给原有设备换国产芯片,大部分需求是做一些新设备新功能。
项目有简单的,比如接收指定CAN报文之后逻辑判断一下然后开闭继电器这种,也有复杂的,比如要制定整车设备的总线私有协议并实现它们的功能。
除了跟需求部门对接、做软硬件、跟生产部门对接、维护文档等工作之外,有时出货时间非常紧张还要自己负责生产,要拧螺丝(不是网络上调侃的拧螺丝,是真的在拧螺丝<_<)。有三次出货时间紧张,我们就是买元器件、打板子、开钢网,然后在办公室焊接、接线、下载程序、测试、装外壳、拧螺丝一条龙服务。当然,只有紧急情况会这样,完了之后都会看看是什么环节没安排好然后考虑以后怎么避免这类问题。跟很多人想象的不一样的是,这些事情做起来其实并不会让人特别排斥,毕竟客户在等着要货,而我们的工资本质上就是客户给的,自然说不上排斥,但对出问题的环节感到厌烦倒是真的。

离职之后我没有着急找工作,而是先是回了趟老家,因为疫情原因我已经有差不多三年半没回老家了,回去看了爷爷奶奶,还有就是把我的户口从老家迁了出来,要回老家公安局注销原户籍。之后我在老家待了半个多月,走走亲戚啥的,喝了几桌喜酒。
从老家回来之后在家待了两个月简单的补了一下C++,看了一下QT(写上位机UI需要),又学了一下LVGL(写下位机UI需要)顺便在此期间愉快地玩耍了一波

8月下旬开始准备投简历,意外地发现我家附近有个大厂(大厂:指很大的工厂)在招单片机,就投了简历去面试拿了offer,想了想自己家门口就有工作那跑那么远干嘛,直接就确定入职,然后体检,等到9月1号正式入职,做了2个月。
部门人很少,就一个经理和一个比我早一个月入职的自动化的老哥,在此之前这经理一直是个光杆司令。入职之后随着工作的进行发现部门经理其实是在浑水摸鱼,他之前做的东西是外包给别的公司做的,做了一部分功能,现在大概是没钱让人家继续做了所以招人,想要死马当活马医。搞清楚了这些我就离职了。

好了,下面是正文。

硬件

首先,在淘宝上面花4.65元买个MAX30102传感器模块(链接
然后,再花10.7元买一个0.96寸的SPI-OLED(链接

然后画一下原理图,就是最小系统、电源、模块接口(MAX30102传感器模块的符号和封装要自己画一下,其他元器件用立创的封装)
我这里主控用的HC32F460,支持国产!(其实就是便宜,而且之前用过)
在这里插入图片描述
画PCB
在这里插入图片描述
使用人肉贴片机焊接一下(PCB应该用黑色好看一点的,大意了)
在这里插入图片描述
在这里插入图片描述

OLED用排针插到板子上。
在这里插入图片描述
单个板子物料成本是27块多。(不含PCB)
在这里插入图片描述

软件

自己写一下OLED驱动,移植一下freertos和LVGL,写个“Hello Word”测试一下看看效果(众所周知,这里就应该写Word而不是World)
freertos移植可以参考野火的STM32-freertos教程,换个单片机也是一样的(都是Cortex-M内核嘛)。其实对于这个板子的功能来讲是没有使用freertos的必要的,毕竟没有“并行”处理应用的需求,但用了也无所谓。
LVGL移植可以看我之前的文章
在这里插入图片描述

OLED代码


/*******************************************************************************
 * @file 	 		oled.c
 * @author  	日常里的奇迹	@bilibili
 * @date    	2022-11-24
 * @brief			0.96寸 SPI-OLED 驱动 
********************************************************************************/

#include "oled.h"


/*******************************************************************************
 * Local function 
 ******************************************************************************/

//配置GPIO
static void Gpio_Config(void)
{
		stc_port_init_t stcPortInit;

    /* configuration structure initialization */
    MEM_ZERO_STRUCT(stcPortInit);

    stcPortInit.enPinMode = Pin_Mode_Out;
    stcPortInit.enExInt = Disable;
    stcPortInit.enPullUp = Enable;

    PORT_Init(RST_PORT, RST_PIN, &stcPortInit);
		PORT_Init(DC_PORT, DC_PIN, &stcPortInit);
}


//配置SPI
static void Spi_Config(void)
{
    stc_spi_init_t stcSpiInit;

    /* configuration structure initialization */
    MEM_ZERO_STRUCT(stcSpiInit);

    /* Configuration peripheral clock */
    PWC_Fcg1PeriphClockCmd(SPI_UNIT_CLOCK, Enable);

    /* Configuration SPI pin */
    PORT_SetFunc(SPI_SCK_PORT, SPI_SCK_PIN, SPI_SCK_FUNC, Disable);
    PORT_SetFunc(SPI_MOSI_PORT, SPI_MOSI_PIN, SPI_MOSI_FUNC, Disable);

    /* Configuration SPI structure */
    stcSpiInit.enClkDiv = SpiClkDiv64;
    stcSpiInit.enFrameNumber = SpiFrameNumber1;
    stcSpiInit.enDataLength = SpiDataLengthBit8;
    stcSpiInit.enFirstBitPosition = SpiFirstBitPositionMSB;
    stcSpiInit.enSckPolarity = SpiSckIdleLevelHigh;
    stcSpiInit.enSckPhase = SpiSckOddChangeEvenSample;
    stcSpiInit.enReadBufferObject = SpiReadReceiverBuffer;
    stcSpiInit.enWorkMode = SpiWorkMode3Line;
    stcSpiInit.enTransMode = SpiTransOnlySend;
    stcSpiInit.enCommAutoSuspendEn = Disable;
    stcSpiInit.enModeFaultErrorDetectEn = Disable;
    stcSpiInit.enParitySelfDetectEn = Disable;
    stcSpiInit.enParityEn = Disable;
    stcSpiInit.enParity = SpiParityEven;

#ifdef SPI_MASTER_MODE
    stcSpiInit.enMasterSlaveMode = SpiModeMaster;
    stcSpiInit.stcDelayConfig.enSsSetupDelayOption = SpiSsSetupDelayCustomValue;
    stcSpiInit.stcDelayConfig.enSsSetupDelayTime = SpiSsSetupDelaySck1;
    stcSpiInit.stcDelayConfig.enSsHoldDelayOption = SpiSsHoldDelayCustomValue;
    stcSpiInit.stcDelayConfig.enSsHoldDelayTime = SpiSsHoldDelaySck1;
    stcSpiInit.stcDelayConfig.enSsIntervalTimeOption = SpiSsIntervalCustomValue;
    stcSpiInit.stcDelayConfig.enSsIntervalTime = SpiSsIntervalSck6PlusPck2;
#endif

#ifdef SPI_SLAVE_MODE
    stcSpiInit.enMasterSlaveMode = SpiModeSlave;
#endif

    SPI_Init(SPI_UNIT, &stcSpiInit);
    SPI_Cmd(SPI_UNIT, Enable);
}



/*******************************************************************************
 * Function implementation - global ('extern') and local ('static')
 ******************************************************************************/


//写命令
void LCD_WrCmd(uint8_t cmd)
{
	/* Wait tx buffer empty */
	while (Reset == SPI_GetFlag(SPI_UNIT, SpiFlagSendBufferEmpty))
	{
	}	
	LCD_DC_CLR();
	/* Send data */
	SPI_SendData8(SPI_UNIT, cmd);
	Ddl_Delay1us(8);
}

//写数据
void LCD_WrDat(uint8_t data)
{
	/* Wait tx buffer empty */
	while (Reset == SPI_GetFlag(SPI_UNIT, SpiFlagSendBufferEmpty))
	{
	}
	LCD_DC_SET();
	/* Send data */
	SPI_SendData8(SPI_UNIT, data);
	Ddl_Delay1us(8);
}

//填充指定值
void LCD_Fill(uint8_t bmp_data)
{
	uint8_t y,x;
	
	for(y=0;y<8;y++)
	{
		LCD_WrCmd(0xb0+y);
		LCD_WrCmd(0x00);
		LCD_WrCmd(0x10);
		for(x=0;x<X_WIDTH;x++)
			LCD_WrDat(bmp_data);
	}
}

//设定位置(y是8的倍数)
void LCD_Set_Pos(uint8_t x, uint8_t y)
{ 
  LCD_WrCmd(0xb0+(y>>3));
  LCD_WrCmd(((x&0xf0)>>4)|0x10);
  LCD_WrCmd(x&0x0f); 
} 

//初始化 LCD/OLED
void LCD_Init(void)        
{	
	Spi_Config();
	Gpio_Config();
	
	LCD_RST_CLR();
	Ddl_Delay1ms(50);
	LCD_RST_SET();

  LCD_WrCmd(0xae);//--turn off oled panel
  LCD_WrCmd(0x00);//---set low column address
  LCD_WrCmd(0x10);//---set high column address
  LCD_WrCmd(0x40);//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
  LCD_WrCmd(0x81);//--set contrast control register
  LCD_WrCmd(0xcf); // Set SEG Output Current Brightness
  LCD_WrCmd(0xa1);//--Set SEG/Column Mapping     
  LCD_WrCmd(0xc8);//Set COM/Row Scan Direction   
  LCD_WrCmd(0xa6);//--set normal display
  LCD_WrCmd(0xa8);//--set multiplex ratio(1 to 64)
  LCD_WrCmd(0x3f);//--1/64 duty
  LCD_WrCmd(0xd3);//-set display offset	Shift Mapping RAM Counter (0x00~0x3F)
  LCD_WrCmd(0x00);//-not offset
  LCD_WrCmd(0xd5);//--set display clock divide ratio/oscillator frequency
  LCD_WrCmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec
  LCD_WrCmd(0xd9);//--set pre-charge period
  LCD_WrCmd(0xf1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
  LCD_WrCmd(0xda);//--set com pins hardware configuration
  LCD_WrCmd(0x12);
  LCD_WrCmd(0xdb);//--set vcomh
  LCD_WrCmd(0x40);//Set VCOM Deselect Level
  LCD_WrCmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)
  LCD_WrCmd(0x02);//
  LCD_WrCmd(0x8d);//--set Charge Pump enable/disable
  LCD_WrCmd(0x14);//--set(0x10) disable
  LCD_WrCmd(0xa4);// Disable Entire Display On (0xa4/0xa5)
  LCD_WrCmd(0xa7);// Enable Inverse Display On (0xa6/a7) 
  LCD_WrCmd(0xaf);//--turn on oled panel
  LCD_Fill(0xff); //clear the screen
  LCD_Set_Pos(0,0);  
	
	
	//用于测试
//	LCD_WrDat(0xff);
//	LCD_WrDat(0x81);
//	LCD_WrDat(0x81);
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0xff);
//	LCD_WrCmd(0xa5);

//	LCD_Set_Pos(56,24);
//	
//	LCD_WrDat(0xff);
//	LCD_WrDat(0x81);
//	LCD_WrDat(0x81);
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0x81);	
//	LCD_WrDat(0xff);
} 

LVGL接口代码

/**
 * @file lv_port_disp_templ.c
 *
 */

/*******************************************************************************
 * @file 	 		lv_port_disp_templ.c
 * @author  	日常里的奇迹	@bilibili
 * @date    	2022-11-24
 * @brief			0.96寸 SPI-OLED 与LVGL接口 
********************************************************************************/

/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/
#if 1

/*********************
 *      INCLUDES
 *********************/
#include "lv_port_disp_template.h"
#include <stdbool.h>

#include "oled.h"
/*********************
 *      DEFINES
 *********************/
 
#define MY_DISP_HOR_RES    128
#define MY_DISP_VER_RES    64
 
 
#ifndef MY_DISP_HOR_RES
    #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
    #define MY_DISP_HOR_RES    320
#endif

#ifndef MY_DISP_VER_RES
    #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now.
    #define MY_DISP_VER_RES    240
#endif

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void disp_init(void);

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
//        const lv_area_t * fill_area, lv_color_t color);

/**********************
 *  STATIC VARIABLES
 **********************/
	//图像缓存
	static uint8_t image_buffer[MY_DISP_HOR_RES][MY_DISP_VER_RES/8];
/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void lv_port_disp_init(void)
{
    /*-------------------------
     * Initialize your display
     * -----------------------*/
    disp_init();

    /*-----------------------------
     * Create a buffer for drawing
     *----------------------------*/

    /**
     * LVGL requires a buffer where it internally draws the widgets.
     * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
     * The buffer has to be greater than 1 display row
     *
     * There are 3 buffering configurations:
     * 1. Create ONE buffer:
     *      LVGL will draw the display's content here and writes it to your display
     *
     * 2. Create TWO buffer:
     *      LVGL will draw the display's content to a buffer and writes it your display.
     *      You should use DMA to write the buffer's content to the display.
     *      It will enable LVGL to draw the next part of the screen to the other buffer while
     *      the data is being sent form the first buffer. It makes rendering and flushing parallel.
     *
     * 3. Double buffering
     *      Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
     *      This way LVGL will always provide the whole rendered screen in `flush_cb`
     *      and you only need to change the frame buffer's address.
     */

    /* Example for 1) */
    static lv_disp_draw_buf_t draw_buf_dsc_1;
    static lv_color_t buf_1[MY_DISP_HOR_RES * MY_DISP_VER_RES];                          /*A buffer for 32 rows*/
    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * MY_DISP_VER_RES);   /*Initialize the display buffer*/

    /* Example for 2) */
//    static lv_disp_draw_buf_t draw_buf_dsc_2;
//    static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];                        /*A buffer for 10 rows*/
//    static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];                        /*An other buffer for 10 rows*/
//    lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);   /*Initialize the display buffer*/

    /* Example for 3) also set disp_drv.full_refresh = 1 below*/
//    static lv_disp_draw_buf_t draw_buf_dsc_3;
//    static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES];            /*A screen sized buffer*/
//    static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES];            /*Another screen sized buffer*/
//    lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,
//                          MY_DISP_VER_RES * LV_VER_RES_MAX);   /*Initialize the display buffer*/

    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/

    static lv_disp_drv_t disp_drv;                         /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = MY_DISP_HOR_RES;
    disp_drv.ver_res = MY_DISP_VER_RES;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = disp_flush;

    /*Set a display buffer*/
    disp_drv.draw_buf = &draw_buf_dsc_1;

    /*Required for Example 3)*/
    //disp_drv.full_refresh = 1;

    /* Fill a memory array with a color if you have GPU.
     * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
     * But if you have a different GPU you can use with this callback.*/
    //disp_drv.gpu_fill_cb = gpu_fill;

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
	/*You code here*/
	LCD_Init();
	
	for(uint32_t i = 0;i < MY_DISP_VER_RES / 8;i++)
	{
		for(uint32_t j = 0;j < MY_DISP_HOR_RES;j++)
		{			
			image_buffer[j][i] = 0xff;
		}		
	}
}

volatile bool disp_flush_enabled = true;

/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL
 */
void disp_enable_update(void)
{
    disp_flush_enabled = true;
}

/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL
 */
void disp_disable_update(void)
{
    disp_flush_enabled = false;
}

/*Flush the content of the internal buffer the specific area on the display
 *You can use DMA or any hardware acceleration to do this operation in the background but
 *'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
	if(disp_flush_enabled) {
		
		uint32_t width = area->x2 - area->x1 + 1;
		uint32_t height = area->y2 - area->y1 + 1;
		
		
		uint32_t top_line = 0;
		if(area->y1 % 8 != 0)
			top_line = 8 - (area->y1 % 8);
		
		uint32_t page = (height - top_line)/8;
		
		uint32_t bottom_line = height - 8*page - top_line;
		
		//填充内容
		//top_line
		for(int32_t j = top_line - 1;j >= 0;j--)			
		{
			for(uint32_t i = 0;i < width;i++)
			{
				if(color_p->full)
					image_buffer[area->x1 + i][area->y1 / 8] |= (0x80 >> j);
				else
					image_buffer[area->x1 + i][area->y1 / 8] &= ~(0x80 >> j);
				
				color_p++;
			}
		}
		
		//page
		for(int32_t j = 0;j < page;j++)			
		{
			for(int32_t k = 7;k >= 0;k--)			
			{
				for(uint32_t i = 0;i < width;i++)
				{
					if(color_p->full)
						image_buffer[area->x1 + i][(area->y1 + top_line) / 8 + j] |= (0x80 >> k);
					else
						image_buffer[area->x1 + i][(area->y1 + top_line) / 8 + j] &= ~(0x80 >> k);
					
					color_p++;
				}
			}
		}

		//bottom_line
		for(int32_t j = 0;j < bottom_line;j++)			
		{
			for(uint32_t i = 0;i < width;i++)
			{
				if(color_p->full)
					image_buffer[area->x1 + i][area->y2 / 8] |= (0x01 << j);
				else
					image_buffer[area->x1 + i][area->y2 / 8] &= ~(0x01 << j);
				
				color_p++;
			}
		}
			
		
		//写入整个图像
		for(uint32_t i = 0;i < MY_DISP_VER_RES / 8;i++)
		{
			LCD_Set_Pos(0,8*i);
			for(uint32_t j = 0;j < MY_DISP_HOR_RES;j++)
			{			
				LCD_WrDat(image_buffer[j][i]);
			}		
		}
		
	}
			
//        /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/

//        int32_t x;
//        int32_t y;
//        for(y = area->y1; y <= area->y2; y++) {
//            for(x = area->x1; x <= area->x2; x++) {
//                /*Put a pixel to the display. For example:*/
//                /*put_px(x, y, *color_p)*/
//                color_p++;
//            }
//        }
//    }

    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/
    lv_disp_flush_ready(disp_drv);
}

/*OPTIONAL: GPU INTERFACE*/

/*If your MCU has hardware accelerator (GPU) then you can use it to fill a memory with a color*/
//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
//                    const lv_area_t * fill_area, lv_color_t color)
//{
//    /*It's an example code which should be done by your GPU*/
//    int32_t x, y;
//    dest_buf += dest_width * fill_area->y1; /*Go to the first line*/
//
//    for(y = fill_area->y1; y <= fill_area->y2; y++) {
//        for(x = fill_area->x1; x <= fill_area->x2; x++) {
//            dest_buf[x] = color;
//        }
//        dest_buf+=dest_width;    /*Go to the next line*/
//    }
//}


#else /*Enable this file at the top*/

/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif

MAX30102驱动

参考商家提供的STM32代码自己写一下MAX30102驱动,把传感器数据读出来测试一下看看
在这里插入图片描述
MAX30102驱动代码



/*******************************************************************************
 * @file 	 		MAX30102.c
 * @author  	日常里的奇迹	@bilibili
 * @date    	2022-11-24
 * @brief			MAX30102 驱动 
********************************************************************************/

#include "MAX30102.h"
#include "math.h"


/*******************************************************************************
 * Global variable definitions (declared in header file with 'extern')
 ******************************************************************************/

// 用于存放 MAX30102 数据的缓冲区
float red_buffer[BUFFER_SIZE+16];           	
float ir_buffer[BUFFER_SIZE+16]; 


/*******************************************************************************
 * Local variable definitions ('static')
 ******************************************************************************/

// 用于标志 MAX30102 数据的缓冲区是否填满,填满了才会进行数据处理
static uint8_t buffer_is_full = 0;

//  MAX30102 数据的缓冲区的索引
static uint16_t index = 0;

/************************************   IIC    ************************************/


/*******************************************************************************
 * Function implementation - global ('extern') and local ('static')
 ******************************************************************************/
 
 
/**
 ******************************************************************************
 ** \brief  Master transmit data
 **
 ** \param  u16DevAddr            The slave address
 ** \param  pu8TxData             Pointer to the data buffer
 ** \param  u32Size               Data size
 ** \param  u32TimeOut            Time out count
 ** \retval en_result_t           Enumeration value:
 **         - Ok:                 Success
 **         - ErrorTimeout:       Time out
 ******************************************************************************/
en_result_t I2C_Master_Transmit(uint16_t u16DevAddr, uint8_t *pu8TxData, uint32_t u32Size, uint32_t u32TimeOut)
{
    en_result_t enRet;
    I2C_Cmd(I2C_UNIT, Enable);

    I2C_SoftwareResetCmd(I2C_UNIT, Enable);
    I2C_SoftwareResetCmd(I2C_UNIT, Disable);
    enRet = I2C_Start(I2C_UNIT,u32TimeOut);
    if(Ok == enRet)
    {

#ifdef I2C_10BITS_ADDRESS
        enRet = I2C_Trans10BitAddr(I2C_UNIT, u16DevAddr, I2CDirTrans, u32TimeOut);
#else
        enRet = I2C_TransAddr(I2C_UNIT, (uint8_t)u16DevAddr, I2CDirTrans, u32TimeOut);
#endif

        if(Ok == enRet)
        {
            enRet = I2C_TransData(I2C_UNIT, pu8TxData, u32Size,u32TimeOut);
        }
    }

    I2C_Stop(I2C_UNIT,u32TimeOut);
    I2C_Cmd(I2C_UNIT, Disable);

    return enRet;
}

/**
 ******************************************************************************
 ** \brief  Master receive data
 **
 ** \param  u16DevAddr            The slave address
 ** \param  pu8RxData             Pointer to the data buffer
 ** \param  u32Size               Data size
 ** \param  u32TimeOut            Time out count
 ** \retval en_result_t           Enumeration value:
 **         - Ok:                 Success
 **         - ErrorTimeout:       Time out
 ******************************************************************************/
en_result_t I2C_Master_Receive(uint16_t u16DevAddr, uint8_t *pu8RxData, uint32_t u32Size, uint32_t u32TimeOut)
{
    en_result_t enRet;

    I2C_Cmd(I2C_UNIT, Enable);
    I2C_SoftwareResetCmd(I2C_UNIT, Enable);
    I2C_SoftwareResetCmd(I2C_UNIT, Disable);
    enRet = I2C_Start(I2C_UNIT,u32TimeOut);
    if(Ok == enRet)
    {
        if(1ul == u32Size)
        {
            I2C_AckConfig(I2C_UNIT, I2c_NACK);
        }

#ifdef I2C_10BITS_ADDRESS
        enRet = I2C_Trans10BitAddr(I2C_UNIT, u16DevAddr, I2CDirReceive, u32TimeOut);
#else
        enRet = I2C_TransAddr(I2C_UNIT, (uint8_t)u16DevAddr, I2CDirReceive, u32TimeOut);
#endif

        if(Ok == enRet)
        {

            enRet = I2C_MasterDataReceiveAndStop(I2C_UNIT, pu8RxData, u32Size, u32TimeOut);
        }

        I2C_AckConfig(I2C_UNIT, I2c_ACK);
    }

    if(Ok != enRet)
    {
        I2C_Stop(I2C_UNIT,u32TimeOut);
    }
    I2C_Cmd(I2C_UNIT, Disable);
    return enRet;
}

/**
 ******************************************************************************
 ** \brief   Initialize the I2C peripheral for master
 ** \param   None
 ** \retval en_result_t                Enumeration value:
 **          - Ok:                     Success
 **          - ErrorInvalidParameter:  Invalid parameter
 ******************************************************************************/
en_result_t Master_Initialize(void)
{
	  /* Initialize I2C port*/
    PORT_SetFunc(I2C_SCL_PORT, I2C_SCL_PIN, I2C_GPIO_SCL_FUNC, Disable);
    PORT_SetFunc(I2C_SDA_PORT, I2C_SDA_PIN, I2C_GPIO_SDA_FUNC, Disable);

    /* Enable I2C Peripheral*/
    PWC_Fcg1PeriphClockCmd(I2C_FCG_USE, Enable);
	
    en_result_t enRet;
    stc_i2c_init_t stcI2cInit;
    float32_t fErr;

    I2C_DeInit(I2C_UNIT);

    MEM_ZERO_STRUCT(stcI2cInit);
    stcI2cInit.u32ClockDiv = I2C_CLK_DIV2;
    stcI2cInit.u32Baudrate = I2C_BAUDRATE;
    stcI2cInit.u32SclTime = 0ul;
    enRet = I2C_Init(I2C_UNIT, &stcI2cInit, &fErr);

    I2C_BusWaitCmd(I2C_UNIT, Enable);

    return enRet;
}



/************************************   MAX30102    ************************************/


/*******************************************************************************
 * Local function 
 ******************************************************************************/


/**********************************************************************
 * @brief 	 		从 MAX30102 读一帧数据,存放到本文件的数据缓冲区,供中断服务调用
 * @parameter  	
 * @return    	
  ********************************************************************/
static void MAX30102_Read_Fream(void)
{
	uint8_t reg_temp;
  
  //read and clear status register
  MAX30102_Read_Reg(REG_INTR_STATUS_1,&reg_temp);
	
  if(reg_temp & 0x40)
	{
		uint16_t un_temp,red_data,ir_data;
		uint8_t ach_i2c_data[6];
		
		//read a frame
		uint8_t address =REG_FIFO_DATA;
		I2C_Master_Transmit(DEVICE_ADDRESS,&address,1,TIMEOUT);
		I2C_Master_Receive(DEVICE_ADDRESS,ach_i2c_data,6,TIMEOUT);
		
		if(!buffer_is_full)
		{
			un_temp=ach_i2c_data[0];
			un_temp<<=14;
			red_data+=un_temp;
			un_temp=ach_i2c_data[1];
			un_temp<<=6;
			red_data+=un_temp;
			un_temp=ach_i2c_data[2];
			un_temp>>=2;
			red_data+=un_temp;
			
			un_temp=ach_i2c_data[3];
			un_temp<<=14;
			ir_data+=un_temp;
			un_temp=ach_i2c_data[4];
			un_temp<<=6;
			ir_data+=un_temp;
			un_temp=ach_i2c_data[5];
			un_temp>>=2;
			ir_data+=un_temp;
			
			red_buffer[index] = red_data;
			ir_buffer[index] = ir_data;			
			
			//buffer is full
			if(index >= BUFFER_SIZE - 1)
			{
				index = 0;
				buffer_is_full = 1;
			}
			else
			{
				index++;
			}
		}
	}
}






/*******************************************************************************
 * Function implementation - global ('extern') and local ('static')
 ******************************************************************************/

/**********************************************************************
 * @brief 	 		写 MAX30102 寄存器
 * @parameter  	reg 寄存器地址
								data 数据
 * @return    	Ok 成功
								ErrorTimeout 超时
  ********************************************************************/
en_result_t MAX30102_Write_Reg(uint8_t reg,uint8_t data)
{
	uint8_t a[2];
	a[0] = reg;
	a[1] = data;
	return I2C_Master_Transmit(DEVICE_ADDRESS,a,2,TIMEOUT);
}

/**********************************************************************
 * @brief 	 		读 MAX30102 寄存器
 * @parameter  	reg 寄存器地址
								data 数据
 * @return    	Ok 成功
								ErrorTimeout 超时
  ********************************************************************/
en_result_t MAX30102_Read_Reg(uint8_t reg,uint8_t * data)
{
	uint8_t a = reg;
	I2C_Master_Transmit(DEVICE_ADDRESS,&a,1,TIMEOUT);
	
	return I2C_Master_Receive(DEVICE_ADDRESS,data,1,TIMEOUT);
}

/**********************************************************************
 * @brief 	 		获取 MAX30102 数据缓冲区状态
 * @parameter  	
 * @return      0	缓冲区没满
                非0 缓冲区满了
  ********************************************************************/
uint8_t Get_Buffer_State(void)
{
	return	buffer_is_full;
}	

/**********************************************************************
 * @brief 	 		把缓冲区状态清零
 * @parameter  	
 * @return    	
  ********************************************************************/
void Clear_Buffer_State(void)
{
	buffer_is_full = 0;
}	

/**********************************************************************
 * @brief 	 		获取缓冲区索引
 * @parameter  	
 * @return    	
  ********************************************************************/
uint16_t Get_Buffer_Index(void)
{
	return index;
}	

/**********************************************************************
 * @brief 	 		MAX30102 复位
 * @parameter  	
 * @return    	
  ********************************************************************/
void Max30102_reset(void)
{
	MAX30102_Write_Reg(REG_MODE_CONFIG,0x40);
}



/**********************************************************************
 * @brief 	 		MAX30102 中断服务
 * @parameter  	
 * @return    	
  ********************************************************************/
void ExtInt02_Callback(void)
{
    if (Set == EXINT_IrqFlgGet(ExtiCh02))
    {
        
			 MAX30102_Read_Fream();
			
       /* clear int request flag */
       EXINT_IrqFlgClr(ExtiCh02);
    }
}

/**********************************************************************
 * @brief 	 		MAX30102 中断初始化
 * @parameter  	
 * @return    	
  ********************************************************************/
static void MAX30102_Int_Init(void)
{
    stc_exint_config_t stcExtiConfig;
    stc_irq_regi_conf_t stcIrqRegiConf;
    stc_port_init_t stcPortInit;

    /* configuration structure initialization */
    MEM_ZERO_STRUCT(stcExtiConfig);
    MEM_ZERO_STRUCT(stcIrqRegiConf);
    MEM_ZERO_STRUCT(stcPortInit);

    /**************************************************************************/
    /* External Int Ch.1                                                      */
    /**************************************************************************/
    stcExtiConfig.enExitCh = ExtiCh02;

    /* Filter setting */
    stcExtiConfig.enFilterEn = Enable;
    stcExtiConfig.enFltClk = Pclk3Div8;
    stcExtiConfig.enExtiLvl = ExIntLowLevel;
    EXINT_Init(&stcExtiConfig);

    /* Set External Int */
    MEM_ZERO_STRUCT(stcPortInit);
    stcPortInit.enExInt = Enable;
    PORT_Init(INT_PORT, INT_PIN, &stcPortInit);

    /* Select External Int Ch.1 */
    stcIrqRegiConf.enIntSrc = INT_PORT_EIRQ2;

    /* Register External Int to Vect.No.000 */
    stcIrqRegiConf.enIRQn = Int000_IRQn;

    /* Callback function */
    stcIrqRegiConf.pfnCallback = &ExtInt02_Callback;

    /* Registration IRQ */
    enIrqRegistration(&stcIrqRegiConf);

    /* Clear pending */
    NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn);

    /* Set priority */
    NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIORITY_DEFAULT);

    /* Enable NVIC */
    NVIC_EnableIRQ(stcIrqRegiConf.enIRQn);
}


/**********************************************************************
 * @brief 	 		MAX30102 初始化
 * @parameter  	
 * @return    	
  ********************************************************************/
void MAX30102_Init(void)
{
	//Initialize IIC
	Master_Initialize();
	//Initialize interrupt
	MAX30102_Int_Init();
	
	Max30102_reset();
	
	//Initialize MAX30102
	MAX30102_Write_Reg(REG_INTR_ENABLE_1,0xc0); INTR setting
	MAX30102_Write_Reg(REG_INTR_ENABLE_2,0x00);//
	MAX30102_Write_Reg(REG_FIFO_WR_PTR,0x00);//FIFO_WR_PTR[4:0]
	MAX30102_Write_Reg(REG_OVF_COUNTER,0x00);//OVF_COUNTER[4:0]
	MAX30102_Write_Reg(REG_FIFO_RD_PTR,0x00);//FIFO_RD_PTR[4:0]
	
	MAX30102_Write_Reg(REG_FIFO_CONFIG,0x0f);//sample avg = 1, fifo rollover=false, fifo almost full = 17
	MAX30102_Write_Reg(REG_MODE_CONFIG,0x03);//0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED
	MAX30102_Write_Reg(REG_SPO2_CONFIG,0x27);	// SPO2_ADC range = 4096nA, SPO2 sample rate (100 Hz), LED pulseWidth (400uS)  
	MAX30102_Write_Reg(REG_LED1_PA,0x32);//Choose value for ~ 10mA for LED1
	MAX30102_Write_Reg(REG_LED2_PA,0x32);// Choose value for ~ 10mA for LED2
	MAX30102_Write_Reg(REG_PILOT_PA,0x7f);// Choose value for ~ 25mA for Pilot LED
	
//	MAX30102_Write_Reg(REG_TEMP_CONFIG,0x01);// Enable Die Temperature
//	MAX30102_Write_Reg(REG_PROX_INT_THRESH,0x80);// Set Proximity Interrupt Threshold

}


血氧心率算法

看了一下商家参考代码的心率计算方法,做法是FFT之后取系数最大的那个频率来作为心率的频率,如果采样时间按5.12s来的话,那么相邻系数间的频率间隔是 60*1/5.12 = 11.7 次/分钟,简单地来说就是测得的心率变化是11.7的倍数…实际效果不是很好。如果要降低这个误差的话采样时间要大幅延长,于是想试试自己写个心率算法。

首先考虑一下心率的根本定义:R波(心电最高的那个峰)之间的间隔取倒数再乘60得到的频率(次/分钟)。
那么问题的关键就是分辨R波并获得它们之间的时间间隔,我的想法是数据减去平均值得到上下波动的交流波形,让后检测过零点,通过过零点来获得时间间隔。(其实一开始是想检测波峰,但效果不是很好)血氧饱和度算法就用参考代码的。
确定好方法,接下来进入数据环节。

首先,缓冲区数据满了之后先滤波,再做数据处理。

/**********************************************************************
 * @brief 	 		取5个点做中值数滤波
 * @parameter  	buffer 缓冲区指针
								center_index 缓冲区数据索引
 * @return    	float 滤波后的值
  ********************************************************************/
static float Median_Filter(float * buffer,uint16_t center_index)
{
	//取出5个数
	float temp[5];
	for(uint16_t i = 0;i < 5;i++)
	{
		temp[i] = buffer[center_index - 2 + i];
	}
	
	//冒泡排序
	int j,i;
	float tem;
	for (i = 0; i < 5-1;i ++)//size-1是因为不用与自己比较,所以比的数就少一个
	{
		int count = 0;
		for (j = 0; j < 5-1 - i; j++)	//size-1-i是因为每一趟就会少一个数比较
		{
			if (temp[j] > temp[j+1])//这是升序排法,前一个数和后一个数比较,如果前数大则与后一个数换位置
			{
				tem = temp[j];
				temp[j] = temp[j+1];
				temp[j+1] = tem;
				count = 1;
				
			}
		}
		if (count == 0)			//如果某一趟没有交换位置,则说明已经排好序,直接退出循环
				break;	
	}
	
	tem = 0.7*(double)temp[2] + 0.1*(double)temp[1] + 0.1*(double)temp[3] + 0.05*(double)temp[0] + 0.05*(double)temp[4];
	return tem;
}

/**********************************************************************
	* @brief 	 		对缓冲区做中值滤波
 * @parameter  	
 * @return    	
  ********************************************************************/
static void Buffer_Median_Filter(float * buffer)
{

	uint16_t i;
	//处理red数据
	temp_buffer[0] = (buffer[0] + buffer[1])/2;
	temp_buffer[1] = (buffer[1] + buffer[2])/2;	
	for(i = 2;i < BUFFER_SIZE-2;i++) 
	{
			temp_buffer[i] = Median_Filter(buffer,i);			
	}
	temp_buffer[BUFFER_SIZE-2] = (buffer[BUFFER_SIZE-3] + buffer[BUFFER_SIZE-2])/2;
	temp_buffer[BUFFER_SIZE-1] = (buffer[BUFFER_SIZE-2] + buffer[BUFFER_SIZE-1])/2;			
	for(i = 0;i < BUFFER_SIZE;i++)
	{
		buffer[i] = temp_buffer[i];
	}
}
/**********************************************************************
 * @brief 	 		处理心率血氧数据,在缓冲区满了的时候调用本函数
 * @parameter  	
 * @return    	
  ********************************************************************/
uint16_t Blood_Data_Process(void)
{	
	float n_denom;
	uint16_t i;
	
	float dc_red =0; 
	float dc_ir =0;
	
	//获得直流数据
	for (i=0 ; i<BUFFER_SIZE ; i++ ) 
	{
		dc_red += red_buffer[i] ;
		dc_ir +=  ir_buffer[i] ;
		
	}
	dc_red =dc_red/BUFFER_SIZE ;
	dc_ir =dc_ir/BUFFER_SIZE ;
	
	//如果直流没有达到阈值就不用处理了
	if(dc_ir < IR_THRESHOLD)
	{
		spO2_num = 0;
		Heart_Rate = 0;
		Clear_Buffer_State();
		Chang_to_Standby();
		
		return 0;
	}
		
	
	//中位数滤波,去掉极大或极小的噪点
	Buffer_Median_Filter(red_buffer);
	Buffer_Median_Filter(ir_buffer);
	
	//移动平均滤波
	for(i = 1;i < BUFFER_SIZE-1;i++) 
	{
			n_denom= ( red_buffer[i-1] + 2*red_buffer[i] + red_buffer[i+1]);
			red_buffer[i]=  (double)n_denom/4.00; 
			
			n_denom= ( ir_buffer[i-1] + 2*ir_buffer[i] + ir_buffer[i+1]);
			ir_buffer[i]=  (double)n_denom/4.00; 			
	}
	
	
	//获得交流数据
	for (i=0 ; i<BUFFER_SIZE ; i++ )  
	{
		red_buffer[i] =  red_buffer[i] - dc_red ; 
		ir_buffer[i] =  ir_buffer[i] - dc_ir ; 
	}
	
	Buffer_Median_Filter(red_buffer);
	Buffer_Median_Filter(ir_buffer);
	
	//计算血氧饱和度
	Calculate_SpO2(dc_red,dc_ir,BUFFER_SIZE);
	//计算心率
	Heart_Rate_Process();	
	
	//清除缓冲区状态
	Clear_Buffer_State();
	
	//切换到数据显示界面
	Chang_to_Data();
	
	return 0;
}

滤完波之后再来计算心率,我这里用过零点检测的方法,先把所有由负到正的过零点找出来,然后计算过零点之间的间距,再从这些间距的中位数开始找合适的间距。为了提高准确度,我使用了一个静态变量来存放上一次的间距,而每次间距都是当前的中位数间距和上一次间距的平均,这样的测试时间变长的时候结果可以更准确。(大概…吧)心率计算效果依然不是很好,但看上去比参考代码好一点点。

/**********************************************************************
 * @brief 	 		从中位数开始找到大于阈值的数
 * @parameter  	buffer 缓冲区指针
								num 数据个数
								threshold 阈值
 * @return    	uint16_t 从中位数开始找到大于阈值的数或最大值
  ********************************************************************/
static float Find_Melia(uint16_t * buffer,uint16_t num,uint16_t threshold)
{
	uint16_t temp[num];
	for(uint16_t i = 0;i < num;i++)
	{
		temp[i] = buffer[i];
	}
	
	//冒泡排序
	int j,i;
	float tem;
	for (i = 0; i < num-1;i ++)//size-1是因为不用与自己比较,所以比的数就少一个
	{
		int count = 0;
		for (j = 0; j < num-1 - i; j++)	//size-1-i是因为每一趟就会少一个数比较
		{
			if (temp[j] > temp[j+1])//这是升序排法,前一个数和后一个数比较,如果前数大则与后一个数换位置
			{
				tem = temp[j];
				temp[j] = temp[j+1];
				temp[j+1] = tem;
				count = 1;
				
			}
		}
		if (count == 0)			//如果某一趟没有交换位置,则说明已经排好序,直接退出循环
				break;	
	}	
	
	tem = 0;
	uint16_t number = 0;
	//从中位数开始找到大于阈值的数
	for(i = num/2;i < num - 1;i++)
	{			
		if(temp[i] > threshold)
		{
			tem = tem + temp[i];
			number++;
		}
	}
	if(tem == 0)
		tem = temp[num - 1];
	else
		tem = tem / number;
	
	return tem;
}

/**********************************************************************
 * @brief 	 		通过过零点计算心率,计算之前需要把把缓冲区信号处理为交流信号
 * @parameter  	
 * @return    	
  ********************************************************************/
static float Heart_Rate_Process(void)
{
	//找出所有(由负到正)过零点的索引 从缓冲区第3个元素开始找,到BUFFER_SIZE - 3为止,
	//因为头两个点和最后两个点没有进行中位数滤波,所以排除以避免干扰
	uint16_t zero_cross_index[BUFFER_SIZE/2 + 1];
	uint16_t zero_cross_num = 0;
	for(uint16_t i = 2;i < BUFFER_SIZE - 2;)
	{
		if((red_buffer[i] < 0) && (red_buffer[i + 1] > 0))
		{
			zero_cross_index[zero_cross_num] = i;
			zero_cross_num++;
			i = i + 2;
		}
		else
		{
			i++;
		}
	}	
	
	if(zero_cross_num < 2)
		return 0;

	
	//计算过零点之间的间距
	for(uint16_t i = 0;i < zero_cross_num - 1;i++)
	{
		zero_cross_index[i] = zero_cross_index[i + 1] - zero_cross_index[i];
	}
	
	//找到合适的间距
	static float old_interval = 0;
	float index_interval = Find_Melia(zero_cross_index,zero_cross_num - 1,30);
	if((index_interval <= 25) || (index_interval >= 150))
	{
		old_interval = 0;
		return 0;
	}
	
	
	if(old_interval == 0)
		old_interval = index_interval;
	
	index_interval = (index_interval + old_interval)/2;
	old_interval = index_interval;
	
	Heart_Rate = 60 * 100.0 /index_interval;
	
	return Heart_Rate;
}

血氧计算我直接用的参考代码的算法

/**********************************************************************
 * @brief 	 		计算血氧
 * @parameter  	
 * @return    	
  ********************************************************************/
static float Calculate_SpO2(float dc_red,float dc_ir,uint16_t cache_nums)
{
	uint16_t i;
	float ac_red,ac_ir;
	
	for(i = 0;i < FFT_N;i++)
	{
		s1[i].real = red_buffer[i];
		s2[i].real = ir_buffer[i];
	}	
	
	//快速傅里叶变换
	FFT(s1);
	FFT(s2);
	
	//解平方
	for(i = 0;i < FFT_N;i++) 
	{
		s1[i].real=sqrtf(s1[i].real*s1[i].real+s1[i].imag*s1[i].imag);
		s1[i].real=sqrtf(s2[i].real*s2[i].real+s2[i].imag*s2[i].imag);
	}
	//计算交流分量
	for (i=1 ; i<FFT_N ; i++ ) 
	{
		ac_red += s1[i].real ;
		ac_ir +=  s2[i].real ;
	}
	
	 
	//这是商家参考代码的心率算法
	//读取峰值点的横坐标
	int s1_max_index = find_max_num_index(s1, 30);
	int s2_max_index = find_max_num_index(s2, 30);
	//心率是最大的频率分量所对应的频率
	Heart_Rate1 = 60.00 * ((100.0 * s1_max_index )/ FFT_N);

	//这是商家参考代码的血氧算法
	float R = (ac_ir*dc_red)/(ac_red*dc_ir);
	float temp = -45.060*(double)R*(double)R+ 30.354 *(double)R + 94.845;
	if(temp > 99)
		spO2_num =99;
	else
		spO2_num = temp;
	
	
	return 	spO2_num;	
}

界面

再剩下来就是写一写UI界面了,这部分可以看一下我之前的文章
这里只补充一些现在用到的新内容。

动画组件
我想在手指未靠近时做一个待机动画,使用LVGL的动画组件。先打开文档看一下动画组件。
在这里插入图片描述
主要看一下它的例程代码
在这里插入图片描述
然后按照例程那样使用就好。需要自己导入图片。
这里我从网上找了个胡桃摇的gif,然后用PS处理它的一个个图层得到二值图像。
在这里插入图片描述
在这里插入图片描述
然后保存成PNG
在这里插入图片描述
之后去在线图片转换那里转成C数组
在这里插入图片描述
然后使用动画组件

	//设置待机动画
	lv_obj_t * animimg0 = lv_animimg_create(lv_scr_act());
	lv_obj_center(animimg0);
	lv_animimg_set_src(animimg0, (lv_img_dsc_t**) anim_imgs, 36);
	lv_animimg_set_duration(animimg0, 1800);
	lv_animimg_set_repeat_count(animimg0, LV_ANIM_REPEAT_INFINITE);
	lv_animimg_start(animimg0);

其实处理图片可以用matlab会更好,处理起来比PS自由(废话),以后有时间用matlab来处理看看,可以试一下把图像里面的图形边界画出来。(大概思路就是先变成灰度图,然后在邻域里面作差分,再按照阈值划分变成二值图像)。

我这里界面只是简单做了待机界面和数据显示界面的切换,想好看一点的话可以自己加图标,处理好布局。

其他

写算法的时候我是通过进入调试状态然后查看缓冲区数据的
在这里插入图片描述
说实话这样不太直观,其实可以通过USB接口连电脑,用虚拟串口获取数据后画图的,不过我这版硬件没连MCU的USB接口(主要是因为我现在还不太了解USB接口,用USB转串口又嫌麻烦),后续我啥时候又想起这个小东西可以把这个USB及上位机做一下。(TODO

另外,写算法的时候找了些其他人的文章,看到一篇有意思的:STM32+ MAX30102通过指尖测量心率+血氧饱和度
本想下载一下这篇文章的资料看看的,结果csdn要什么下载码,还要关注公众号巴拉巴拉的,太麻烦了,算了。

  • 9
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值