软硬件资料: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,®_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要什么下载码,还要关注公众号巴拉巴拉的,太麻烦了,算了。