一、硬件介绍
1、产品特点
MAX78000FTHR 开发板集成卷积神经网络加速器,将ARM Cortex-M4处理器与浮点单元 (FPU)、卷积神经网络 (CNN) 加速器和 RISC-V 内核组合在一起,包括MAX20303 PMIC,用于电池和电源管理,兼容Adafruit Feather Wing外设扩展板。评估板包括各种外设,例如CMOS VGA图像传感器、数字麦克风、低功耗立体声音频CODEC、1MB QSPI SRAM、micro SD存储卡连接器、RGB指示LED和按键;;
板载的MAX32625微控制器已预先编程有 DAPLink 固件,可通过 USB 对 MAX78000 Arm 内核进行调试和编程
标准10pin引脚JTAG接口,可调试和编程MAX78000的RISC-V内核
特性
双核:Arm Cortex-M4 FPU处理器,100MHz;RISC-V协处理器,60MHz
512KB闪存
128KB SRAM
16KB 缓存
卷积神经网络加速器
12位并行摄像头接口
MAX20303可穿戴PMIC,带电量计
Micro SD卡连接器
CMOS VGA图像传感器
低功耗、立体声音频编解码器
数字麦克风
硬件框图
系统框图
2、功能引脚示意图 / 原理图
板载按钮
SW1:用户可编程功能按钮;
连接到 P0_2
SW2:用户可编程功能按钮;
连接到 P1_7
SW3:PMIC 电源按钮;
Power_Button:当电路板处于通电状态时,按住此按钮 12 秒将执行硬断电; 当电路板处于断电状态时,按下此按钮可重新打开电路板电源。
该按钮可以被MAX78000读取,当按下按钮时,PMIC_PFN2(P3_0)信号进入逻辑低电平状态。
SW4:连接到 RSTN 的输入;
用于复位 MCU
SW5:DAPLink 固件更新按钮;
用于 DAPLINK 固件的更新; 当按下按钮上电时,将进入固件更新模式;
板载LED
D1: 该LED可由用户控制,连接到对应的GPIO端口;
LED_R:P2_0 LED_G:P2_1 LED_B:P2_2
D2:连接到MAX20303的PMIC_LEDx输出;
这些LED可以通过I2C命令进行控制; 还可以通过 I2C 命令配置为充电状态指示灯;
PMIC_LED1(Red) PMIC_LED2(Green) PMIC_LED0(Blue)
D3:DAPLink(MAX32625)状态指示灯;
由 DAPLink 控制
引脚图
脚序 | 名称 | 功能 |
1 | RST | 主复位引脚 |
2 | 3V3 | 3.3V 输出为外设提供 3.3V 电压 |
3 | 1V8 | 1.8V 输出为外设提供 1.8V 电压 |
4 | GND | 地 |
5 | P2_3 | GPIO 或 模拟输入(AIN3 通道) |
6 | P2_4 | GPIO 或 模拟输入(AIN4 通道) |
7 | P1_1 | GPIO 或 UART2_Tx |
8 | P1_0 | GPIO 或 UART2_Rx |
9 | MPC1 | GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出 |
10 | MPC2 | GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出 |
11 | P0_7 | GPIO 或 QSPI0 时钟SD卡 和 板载 QSPI SRAM 共享 |
12 | P0_5 | GPIO 或 QSPI0 MOSISD卡 和 板载 QSPI SRAM 共享 |
13 | P0_6 | GPIO 或 QSPI0 MISOSD卡 和 板载 QSPI SRAM 共享 |
14 | P2_6 | GPIO 或 LPUART_Rx |
15 | P2_7 | GPIO 或 LPUART_Tx |
16 | GND | 地 |
17 | SYS | 这是主系统电源,可在电池电压 和 USB电源 之间自动切换 (5V) |
18 | PWR | 强制关机按钮对地短路 13 秒,则关闭 PMIC |
19 | VBUS | USB_VBUS当连接到 USB 时,为外设提供 5V 电压在不使用 USB 连接时,也可以用作为电路的供电输入(最好不要,因为没有电路防止电流回流至USB) |
20 | P1_6 | GPIO |
21 | MPC3 | GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出 |
22 | P0_9 | GPIO 或 QSPI0 SDIO3SD 卡和板载 QSPI SRAM 共享 |
23 | P0_8 | GPIO 或 QSPI0 SDIO2SD卡和板载 QSPI SRAM 共享 |
24 | P0_11 | GPIO 或 QSPI0_Slave |
25 | P0_19 | GPIO |
26 | P3_1 | GPIO 或 Wake-up该引脚为 3.3V |
27 | P0_16 | GPIO 或 I2C1_SCL板载电平转换器允许通过 R15 或 R20 电阻器选择 1.8V 或 3.3V (详见原理图) |
28 | P0_17 | GPIO 或 I2C1_SDA 板载电平转换器允许通过 R15 或 R20 电阻选择 1.8V 或 3.3V (详见原理图) |
3、血氧心率模块 (MAX30102 )
MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块; 它集成了一个红光LED和一个红外光LED、光电检测器、光器件,以及带环境光抑制的低噪声电子电路,采用I2C协议进行通信;
基本原理
当光线照射到皮肤组织(通常是指尖、耳垂或手腕)时,一部分光会被组织、骨骼、静脉血等非脉动成分吸收,这部分吸收是恒定不变的。另一部分光则会穿透组织,被动脉血吸收。而动脉血会因为心脏的泵血活动而发生周期性的脉动(血液容积变化)。
主要参数:
I2C地址:0x57
名称 | 参数 |
LED峰值波长器 | 660nm / 880nm |
LED供电电压 | 3.3 ~ 5V |
检测信号类型 | 光反射信号(PPG) |
输出信号接口 | I2C接口 |
通信接口电压 |
引脚说明
MAX30102内置了两个LED光源(红光RD和红外光IRD)它们是IR和RED的驱动,不需要外部连接,内部已经连接好了;
名称 功能
VIN 电源输入:1.8V - 5V (默认:3V3)
SDA I2C数据线
SCL I2C时钟线
GND 地
RD 红色LED接地端,一般不接
IRD 红外光IR_LED接地端,一般不接
INT 中断引脚:低电平有效
系统框图
原理图
4、0.96寸OLED 黄蓝显示屏 (SSD1315)
黄蓝显示屏 (SSD1315) 是一款蓝色和黄色双色显示屏,该显示屏支持 3.3V 和 5V 电源电压。
可以使用 I2C 或 SPI 接口;
若使用 SPI 需要焊接后面;
特性
兼容 3.3V / 5V 电源
可更改 I2C 地址
支持 SPI
低功耗
黄色和蓝色双色 128×64 像素
高对比度,高亮度
宽工作温度范围:-40℃ ~ +85 ℃
原理图
二、硬件连接
模块通过硬件I2C的方式连接至开发板;
开发板 MAX30102模块 OLED模块
3V3 VIN VCC
GND GND GND
P0_16(I2C1_SCL) SCL SCL
P0_17(I2C1_SDA) SDA SDA
P0_19 INT -
实物效果
三、实现思想
实现效果
使用MAX78000FTHR开发板通过硬件I2C方式连接相关模块; 通过MAX30102模块通过手指检测心率 / 血氧数据,并在OLED屏幕上实时显示,实现采集、检测、分析等功能;
UI布局
OLED屏幕上的UI布局,总共由5个部分组成;
1、正中间显示标题【智能检测手环】
2、左上半部分【心形图标】,实现测量过程中的律动效果;
3、左下半部分【显示测量状态(等待测量 / 测量中 / 最后的结果)】
4、右上部分【显示测量过程中,实时的心率 / 血氧数值】
5、右下部分【显示测量过程中,心率实时变化的折线图】
测量前
显示等待测量画面;
测量过程中
显示当前测量的进度,以及实时数值、变化趋势图;
测量结束
显示1分钟后测得的结果;
主要流程图
四、代码编写
主要相关代码
main.c
/***** Includes *****/ #include <stdio.h> #include <stdint.h> #include <string.h> #include "board.h" #include "mxc_device.h" #include "nvic_table.h" #include "oled.h" #include "max30102.h" static u8g2_t u8g2; // 左侧宽度 #define LHS_W 64 #define TITLE_H 12 #define REALTIME_LINE_H 12 #define REALTIME_LINES 2 // 心形尺寸 #define HEART_R_BIG 8 #define HEART_R_SMALL 5 // 折线图 #define GRAPH_W 48 #define GRAPH_H 20 #define GRAPH_X (LHS_W + (64 - GRAPH_W)) // 128x64 OLED,右侧 64 区域再向右贴 #define GRAPH_Y (64 - GRAPH_H - 2) // 心率映射范围 #define HR_MIN 30 #define HR_MAX 130 /***** 数据存储 *****/ static uint8_t hr_graph[GRAPH_W]; static uint8_t graph_count = 0; static uint8_t last_avg_hr = 0; static uint8_t last_avg_spo2 = 0; static uint8_t avg_valid = 0; static uint8_t measuring = 0; // 1=正在测量 static uint32_t sum_hr = 0, sum_spo2 = 0; static uint8_t inst_hr = 0, inst_spo2 = 0; /***** I2C 初始化 *****/ static int i2c_init(void) { int error = MXC_I2C_Init(I2C_MASTER, 1, 0); if (error != E_NO_ERROR) { printf("I2C init fail:%d\n", error); return error; } MXC_I2C_SetFrequency(I2C_MASTER, I2C_FREQ); printf("I2C init success\n"); return E_NO_ERROR; } /***** 标题 *****/ static void draw_title(void) { const char *title = "智能检测手环"; u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_gb2312a); int w = u8g2_GetUTF8Width(&u8g2, title); int x = (128 - w) / 2; if (x < 0) x = 0; u8g2_DrawUTF8(&u8g2, x, TITLE_H, title); } /***** 心形布局辅助 *****/ static int calc_center_start(int total_block_h) { int start = (64 - total_block_h) / 2; if (start < 0) start = 0; return start; } static int heart_block_height(void) { int r = HEART_R_BIG; return r + r / 2 + r; // ~2.5r } /***** 绘制心形 *****/ static void draw_heart_icon_at(int top_y, uint8_t hr) { static uint8_t frame = 0; frame++; uint8_t big = (frame & 0x08) ? 1 : 0; if (hr == 0) big = 0; uint8_t r = big ? HEART_R_BIG : HEART_R_SMALL; int cx = LHS_W / 2 - 8; // 左侧区域X int heart_h = r + r / 2 + r + 13; int cy = top_y + heart_h / 2; u8g2_DrawDisc(&u8g2, cx - r / 2, cy - r / 3, r / 2, U8G2_DRAW_ALL); u8g2_DrawDisc(&u8g2, cx + r / 2, cy - r / 3, r / 2, U8G2_DRAW_ALL); u8g2_DrawTriangle(&u8g2, cx - r, cy - r / 6, cx + r, cy - r / 6, cx, cy + r); } /***** 折线图 *****/ static uint8_t hr_to_level(uint8_t hr) { if (hr < HR_MIN) hr = HR_MIN; if (hr > HR_MAX) hr = HR_MAX; if (GRAPH_H <= 1) return 0; return (uint8_t)((hr - HR_MIN) * (GRAPH_H - 1) / (HR_MAX - HR_MIN)); } static void graph_push(uint8_t hr) { uint8_t v = hr_to_level(hr); if (graph_count < GRAPH_W) { hr_graph[graph_count++] = v; } else { memmove(&hr_graph[0], &hr_graph[1], GRAPH_W - 1); hr_graph[GRAPH_W - 1] = v; } } static void draw_hr_graph(void) { int16_t base_y = GRAPH_Y + GRAPH_H - 1; u8g2_DrawFrame(&u8g2, GRAPH_X, GRAPH_Y, GRAPH_W, GRAPH_H); for (int x = 1; x < graph_count; x++) { int y1 = base_y - hr_graph[x - 1]; int y2 = base_y - hr_graph[x]; u8g2_DrawLine(&u8g2, GRAPH_X + x - 1, y1, GRAPH_X + x, y2); } } /***** 左侧文本 *****/ static void draw_left_panel(uint8_t inst_hr, uint8_t progress_percent) { u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_gb2312a); int line_h = 12; const char *lines[4]; int line_cnt = 0; if (measuring) { lines[line_cnt++] = "测量中..."; static char prog[16]; snprintf(prog, sizeof(prog), "进度:%3u%%", progress_percent); lines[line_cnt++] = prog; } else if (avg_valid) { lines[line_cnt++] = "1分钟结果"; static char hr_line[20]; static char sp_line[20]; snprintf(hr_line, sizeof(hr_line), "心率:%3u BMP", last_avg_hr); snprintf(sp_line, sizeof(sp_line), "血氧:%3u%%", last_avg_spo2); lines[line_cnt++] = hr_line; lines[line_cnt++] = sp_line; } else { lines[line_cnt++] = "等待测量"; } int heart_h = heart_block_height(); int text_h = line_cnt * line_h; int gap = 4; int total_h = heart_h + (line_cnt ? gap : 0) + text_h; int top = calc_center_start(total_h); draw_heart_icon_at(top, inst_hr); int text_y_base = top + heart_h + gap + line_h - 2; for (int i = 0; i < line_cnt; i++) { u8g2_DrawUTF8(&u8g2, 2, text_y_base + i * line_h, lines[i]); } } /***** 整体 UI 刷新 *****/ static void oled_draw_ui(uint8_t inst_hr, uint8_t inst_spo2, uint8_t progress_percent) { u8g2_ClearBuffer(&u8g2); draw_title(); draw_left_panel(inst_hr, progress_percent); // 右侧实时 u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_gb2312a); int rt_x = LHS_W + 12; int base_y = TITLE_H + REALTIME_LINE_H + 2; char line[32]; snprintf(line, sizeof(line), "心率:%3u", inst_hr); u8g2_DrawUTF8(&u8g2, rt_x, base_y, line); snprintf(line, sizeof(line), "血氧:%3u%%", inst_spo2); u8g2_DrawUTF8(&u8g2, rt_x, base_y + REALTIME_LINE_H, line); // 右下折线图 if (inst_hr) graph_push(inst_hr); draw_hr_graph(); u8g2_SendBuffer(&u8g2); } /***** 1分钟测量 *****/ static int measure_one_minute(uint8_t *avg_hr, uint8_t *avg_spo2) { measuring = 1; uint32_t sum_hr = 0, sum_spo2 = 0; uint32_t valid = 0; uint32_t samples = 0; uint8_t inst_hr = 0, inst_spo2 = 0; const uint16_t TOTAL_LOOPS = 900; // 60s 采样数据 for (uint32_t loop = 0; loop < TOTAL_LOOPS; loop++) { uint8_t hr, sp; if (max30102_get(&hr, &sp)) { inst_hr = hr; inst_spo2 = sp; sum_hr += hr; sum_spo2 += sp; valid++; } samples++; uint8_t percent = (uint8_t)((loop * 100) / TOTAL_LOOPS); oled_draw_ui(inst_hr, inst_spo2, percent); } measuring = 0; if (valid == 0) return 0; *avg_hr = (uint8_t)(sum_hr / valid); *avg_spo2 = (uint8_t)(sum_spo2 / valid); return 1; } int main(void) { uint8_t hr, sp; i2c_init(); u8g2Init(&u8g2); u8g2_ClearBuffer(&u8g2); if (max30102_init() != E_NO_ERROR) { printf("MAX30102 init failed\n"); } else { printf("MAX30102 init OK\n"); } max30102_getFirst(); // 等待手指放上 while (!max30102_get(&hr, &sp)) { oled_draw_ui(0, 0, 0); // 只显示等待与心形动画 } printf("检测到手指,开始测量\n"); while (1) { uint8_t avg_hr, avg_spo2; if (measure_one_minute(&avg_hr, &avg_spo2)) { last_avg_hr = avg_hr; last_avg_spo2 = avg_spo2; avg_valid = 1; printf("1分钟平均心率:%uBMP, 血氧:%u%%\n", avg_hr, avg_spo2); } else { printf("没有有效数据\n"); } // 显示最终结果 oled_draw_ui(last_avg_hr, last_avg_spo2, 100); MXC_Delay(3000000); // 3s后若再次检测到手指时 重新开始测量 while (!max30102_get(&hr, &sp)); } }
编译代码
使用CTRL + SHIFT + B 选择 Build 编译项目;
或使用终端 make -j16 PROJECT=GPIO,编译成 .elf 程序二进制文件;
五、程序烧录
1、用数据线连接开发板至电脑上;
2、程序烧录
使用CTRL + SHIFT + B 选择 flash & run 烧录并运行程序;
六、相关过程帖
七、演示效果
代码文件