编程技术分享

分享编程知识,探讨技术创新

0%

简介:基于PPG的脉搏波及心率显示,低成本,易复刻

我将针对基于PPG的脉搏波及心率显示项目,详细阐述最适合的代码设计架构,并提供具体的C代码实现。本项目旨在构建一个低成本、易于复刻、可靠、高效且可扩展的嵌入式系统平台。
关注微信公众号,提前获取相关推文

项目概述与需求分析

1. 项目目标:

  • 功能性目标:
    • 实时采集PPG(光电容积脉搏波描记法)传感器数据。
    • 对PPG信号进行预处理、滤波和分析,提取脉搏波形。
    • 准确计算心率(BPM - 每分钟心跳次数)。
    • 在小型显示屏上清晰显示脉搏波形和实时心率值。
    • 通过LED指示灯或屏幕颜色变化,根据心率范围提供视觉反馈。
    • 用户友好的操作界面,可能包括一个或多个按键用于启动/停止测量或切换显示模式。
  • 非功能性目标:
    • 低成本: 采用经济实惠的硬件组件,降低BOM(物料清单)成本。
    • 易于复刻: 设计简单明了,代码结构清晰,方便其他开发者理解和复制。
    • 可靠性: 系统运行稳定可靠,能够长时间连续工作,数据采集和处理准确。
    • 高效性: 代码执行效率高,资源占用少,确保实时性和低功耗。
    • 可扩展性: 系统架构应易于扩展,未来可以添加更多功能,例如血氧饱和度(SpO2)测量、数据记录、无线传输等。
    • 用户友好性: 操作简单直观,显示信息清晰易懂。
    • 低功耗: 尽可能降低功耗,延长电池续航时间(如果使用电池供电)。

2. 硬件选型(低成本和易复刻原则):

  • 微控制器 (MCU):
    • 选项: STM32F103C8T6 (俗称“蓝 pills”), ESP32-C3, ATmega328P (Arduino Nano)。
    • 理由: STM32F103C8T6 性能适中,价格低廉,社区支持广泛,易于获取。ESP32-C3 具有Wi-Fi功能,虽然本项目初期可能不需要,但为未来扩展无线传输功能预留了可能性。ATmega328P 成本更低,但性能稍弱,适合对性能要求不高的简化版本。
    • 选定: STM32F103C8T6 作为示例,因为它在性能、成本和易用性之间取得了良好的平衡。
  • PPG传感器:
    • 选项: MAX30102, MAX30100, SFH 7050, 定制化方案(LED + 光电二极管/三极管)。
    • 理由: MAX30102 和 MAX30100 是集成度高的PPG传感器,内置LED和光电探测器,I2C接口,易于使用。SFH 7050 也是类似的集成传感器。定制化方案成本更低,但需要更多的外围电路设计和校准工作。
    • 选定: MAX30102,因为它集成度高,性能良好,且相对容易获取和使用。
  • 显示屏:
    • 选项: 0.96寸或1.3寸 OLED显示屏 (SPI或I2C接口), 128x64 LCD显示屏 (SPI或并行接口)。
    • 理由: OLED显示屏对比度高,功耗低,显示效果好,SPI接口易于连接MCU。LCD显示屏成本更低,但显示效果稍逊。
    • 选定: 0.96寸 SPI OLED显示屏,兼顾显示效果和易用性。
  • 电源:
    • 选项: USB供电,锂电池供电。
    • 理由: USB供电方便调试和开发。锂电池供电更适合便携式应用。
    • 设计: 采用USB供电,同时预留电池供电接口,方便用户根据需求选择。
  • 按键:
    • 选项: 机械按键,触摸按键。
    • 理由: 机械按键成本低,可靠性高,易于实现。触摸按键更美观,但成本稍高,且在嵌入式系统中可能不如机械按键可靠。
    • 选定: 机械按键,例如轻触按键,用于启动/停止测量或切换显示模式。
  • LED指示灯:
    • 选项: 单色LED,RGB LED。
    • 理由: 单色LED成本低,用于简单的状态指示。RGB LED可以实现更丰富的颜色指示,例如根据心率范围改变颜色。
    • 选定: RGB LED,用于更直观的心率范围指示。

3. 软件需求:

  • 驱动程序:
    • PPG传感器驱动 (MAX30102)。
    • OLED显示屏驱动 (SPI接口)。
    • GPIO驱动 (按键,LED)。
    • ADC驱动 (如果PPG传感器输出模拟信号,虽然MAX30102是I2C数字接口,这里可以作为通用ADC驱动的示例,或者用于其他可能的模拟传感器扩展)。
    • 定时器驱动 (用于采样率控制,心率计算等)。
    • I2C/SPI驱动 (用于传感器和显示屏通信)。
  • 信号处理算法:
    • PPG信号预处理 (滤波,去噪)。
    • 脉搏波形检测和提取。
    • 心率计算算法。
  • 应用程序逻辑:
    • 系统初始化。
    • 状态管理 (测量状态,显示状态,空闲状态等)。
    • 用户界面逻辑 (按键处理,显示内容更新)。
    • 心率范围指示逻辑 (LED颜色控制)。

代码设计架构

为了实现可靠、高效、可扩展的系统平台,并遵循模块化和分层设计的原则,我推荐以下代码架构:

1. 分层架构:

  • 硬件抽象层 (HAL - Hardware Abstraction Layer):
    • 功能: 直接与硬件交互,提供统一的硬件接口,屏蔽底层硬件差异。
    • 模块: hal_gpio.c/h, hal_adc.c/h, hal_timer.c/h, hal_spi.c/h, hal_i2c.c/h 等。
    • 优点: 提高代码的可移植性,方便更换底层硬件平台。
  • 设备驱动层 (Device Driver Layer):
    • 功能: 基于HAL层,为上层应用提供更高级、更易用的设备接口。
    • 模块: driver_ppg.c/h (MAX30102驱动), driver_oled.c/h (OLED显示屏驱动), driver_button.c/h, driver_led.c/h 等。
    • 优点: 将硬件操作细节封装在驱动层,使应用层代码更简洁,专注于业务逻辑。
  • 信号处理层 (Signal Processing Layer):
    • 功能: 负责PPG信号的滤波、分析和特征提取。
    • 模块: processing_ppg.c/h (PPG信号处理模块), algorithm_filter.c/h (滤波算法), algorithm_peak_detection.c/h (峰值检测算法), algorithm_heart_rate.c/h (心率计算算法) 等。
    • 优点: 将信号处理算法独立出来,方便算法的优化和替换。
  • 应用逻辑层 (Application Logic Layer):
    • 功能: 实现系统的核心业务逻辑,包括状态管理、用户界面、数据显示等。
    • 模块: app_main.c (主应用程序), ui_display.c/h (用户界面显示), state_machine.c/h (状态机管理), config.c/h (系统配置) 等。
    • 优点: 清晰地组织应用程序逻辑,提高代码的可读性和可维护性。

2. 模块化设计:

  • 每个层次和功能都划分为独立的模块,模块之间通过定义良好的接口进行通信。
  • 模块内部高内聚,模块之间低耦合。
  • 方便代码的复用、测试和维护。

3. 事件驱动或轮询机制:

  • 数据采集: 可以使用定时器中断触发ADC采样 (如果使用模拟PPG传感器),或者轮询方式读取数字PPG传感器数据。
  • UI更新: 可以定时更新显示屏,或者在数据更新时触发UI更新。
  • 按键处理: 可以使用外部中断或轮询方式检测按键事件。

4. 状态机:

  • 使用状态机管理系统的不同状态 (例如:初始化状态、测量状态、显示状态、错误状态等)。
  • 状态机可以简化程序逻辑,提高系统的可靠性和可维护性。

5. 配置管理:

  • 将系统配置参数 (例如:采样率、滤波参数、显示设置等) 集中管理在配置文件中。
  • 方便参数的修改和调整,提高系统的灵活性。

C 代码实现 (详细注释)

为了满足3000行代码的要求,我将尽可能详细地实现各个模块,并添加详细的注释。以下代码将以 STM32F103C8T6 微控制器和 MAX30102 PPG传感器 为例进行实现。

1. HAL 层 (Hardware Abstraction Layer)

hal_gpio.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

#include <stdint.h>

// GPIO 端口和引脚定义 (根据 STM32F103 具体配置)
typedef enum {
GPIO_PORT_A,
GPIO_PORT_B,
GPIO_PORT_C
// ... 可以添加更多端口
} GPIO_PortTypeDef;

typedef uint16_t GPIO_PinTypeDef;

#define GPIO_PIN_0 (1U << 0) // 引脚 0
#define GPIO_PIN_1 (1U << 1) // 引脚 1
#define GPIO_PIN_2 (1U << 2) // 引脚 2
// ... 定义到 GPIO_PIN_15

// GPIO 初始化结构体
typedef struct {
GPIO_PortTypeDef Port; // GPIO 端口
GPIO_PinTypeDef Pin; // GPIO 引脚
uint32_t Mode; // GPIO 模式 (输入/输出/复用功能等)
uint32_t Pull; // 上拉/下拉/浮空
uint32_t Speed; // 输出速度 (如果配置为输出)
} GPIO_InitTypeDef;

// GPIO 模式定义 (部分示例)
#define GPIO_MODE_INPUT (0x00000000U)
#define GPIO_MODE_OUTPUT_PP (0x00000001U) // 推挽输出
#define GPIO_MODE_OUTPUT_OD (0x00000011U) // 开漏输出
#define GPIO_MODE_AF_PP (0x00000002U) // 复用推挽输出
#define GPIO_MODE_AF_OD (0x00000012U) // 复用开漏输出
#define GPIO_MODE_INPUT_PULLUP (0x00000021U) // 上拉输入
#define GPIO_MODE_INPUT_PULLDOWN (0x00000022U) // 下拉输入
#define GPIO_MODE_ANALOG (0x00000003U) // 模拟输入

// GPIO 上拉/下拉定义
#define GPIO_PULL_NONE (0x00000000U)
#define GPIO_PULLUP (0x00000001U)
#define GPIO_PULLDOWN (0x00000002U)

// GPIO 输出速度定义 (部分示例)
#define GPIO_SPEED_FREQ_LOW (0x00000000U)
#define GPIO_SPEED_FREQ_MEDIUM (0x00000001U)
#define GPIO_SPEED_FREQ_HIGH (0x00000002U)
#define GPIO_SPEED_FREQ_VERY_HIGH (0x00000003U)

// GPIO 初始化函数
void HAL_GPIO_Init(GPIO_InitTypeDef *GPIO_InitStruct);

// 设置 GPIO 引脚输出电平
void HAL_GPIO_WritePin(GPIO_PortTypeDef Port, GPIO_PinTypeDef Pin, uint8_t PinState);

// 读取 GPIO 引脚输入电平
uint8_t HAL_GPIO_ReadPin(GPIO_PortTypeDef Port, GPIO_PinTypeDef Pin);

#endif // HAL_GPIO_H

hal_gpio.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include "hal_gpio.h"
#include "stm32f1xx_hal.h" // 包含 STM32 HAL 库头文件 (需要根据实际使用的 HAL 库调整)

// GPIO 初始化函数实现
void HAL_GPIO_Init(GPIO_InitTypeDef *GPIO_InitStruct) {
GPIO_InitTypeDef GPIO_Config;

// 根据 GPIO_InitStruct 结构体配置 STM32 HAL 库的 GPIO 初始化结构体
GPIO_Config.Pin = GPIO_InitStruct->Pin;
GPIO_Config.Mode = GPIO_InitStruct->Mode;
GPIO_Config.Pull = GPIO_InitStruct->Pull;
GPIO_Config.Speed = GPIO_InitStruct->Speed;

// 根据 GPIO 端口选择对应的 GPIO 外设 (GPIOA, GPIOB, GPIOC, ...)
GPIO_TypeDef *gpio_port;
switch (GPIO_InitStruct->Port) {
case GPIO_PORT_A:
gpio_port = GPIOA;
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟
break;
case GPIO_PORT_B:
gpio_port = GPIOB;
__HAL_RCC_GPIOB_CLK_ENABLE(); // 使能 GPIOB 时钟
break;
case GPIO_PORT_C:
gpio_port = GPIOC;
__HAL_RCC_GPIOC_CLK_ENABLE(); // 使能 GPIOC 时钟
break;
// ... 添加更多端口的时钟使能
default:
return; // 端口无效
}

HAL_GPIO_Init_Ex(gpio_port, &GPIO_Config); // 调用 STM32 HAL 库的 GPIO 初始化函数
}

// 设置 GPIO 引脚输出电平实现
void HAL_GPIO_WritePin(GPIO_PortTypeDef Port, GPIO_PinTypeDef Pin, uint8_t PinState) {
GPIO_TypeDef *gpio_port;
switch (Port) {
case GPIO_PORT_A:
gpio_port = GPIOA;
break;
case GPIO_PORT_B:
gpio_port = GPIOB;
break;
case GPIO_PORT_C:
gpio_port = GPIOC;
break;
// ... 添加更多端口
default:
return;
}

HAL_GPIO_WritePin_Ex(gpio_port, Pin, (GPIO_PinState)PinState); // 调用 STM32 HAL 库的 GPIO 写引脚函数
}

// 读取 GPIO 引脚输入电平实现
uint8_t HAL_GPIO_ReadPin(GPIO_PortTypeDef Port, GPIO_PinTypeDef Pin) {
GPIO_TypeDef *gpio_port;
switch (Port) {
case GPIO_PORT_A:
gpio_port = GPIOA;
break;
case GPIO_PORT_B:
gpio_port = GPIOB;
break;
case GPIO_PORT_C:
gpio_port = GPIOC;
break;
// ... 添加更多端口
default:
return 0; // 端口无效,返回默认值
}

return (uint8_t)HAL_GPIO_ReadPin_Ex(gpio_port, Pin); // 调用 STM32 HAL 库的 GPIO 读引脚函数
}

hal_timer.h (简化的定时器 HAL):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifndef HAL_TIMER_H
#define HAL_TIMER_H

#include <stdint.h>

// 定时器句柄 (简化,实际 HAL 库可能更复杂)
typedef void* TimerHandleTypeDef;

// 定时器初始化结构体
typedef struct {
uint32_t Prescaler; // 预分频器
uint32_t Period; // 计数周期 (自动重载值)
uint32_t ClockDivision; // 时钟分频 (如果需要)
void (*Callback)(void); // 定时器溢出回调函数
} Timer_InitTypeDef;

// 定时器初始化函数
TimerHandleTypeDef HAL_Timer_Init(Timer_InitTypeDef *Timer_InitStruct);

// 启动定时器
void HAL_Timer_Start(TimerHandleTypeDef TimerHandle);

// 停止定时器
void HAL_Timer_Stop(TimerHandleTypeDef TimerHandle);

// 设置定时器回调函数
void HAL_Timer_SetCallback(TimerHandleTypeDef TimerHandle, void (*Callback)(void));

// 获取当前定时器计数器值 (如果需要)
uint32_t HAL_Timer_GetCounter(TimerHandleTypeDef TimerHandle);

#endif // HAL_TIMER_H

hal_timer.c (简化的定时器 HAL):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include "hal_timer.h"
#include "stm32f1xx_hal.h" // 包含 STM32 HAL 库头文件

// 简化的定时器 HAL 实现 (实际需要根据 STM32 HAL 库进行更详细的配置)
TimerHandleTypeDef HAL_Timer_Init(Timer_InitTypeDef *Timer_InitStruct) {
TIM_HandleTypeDef *htim = malloc(sizeof(TIM_HandleTypeDef)); // 分配定时器句柄内存
if (htim == NULL) {
return NULL; // 内存分配失败
}

// 选择定时器外设 (例如 TIM2, TIM3, ...) 这里假设使用 TIM2
htim->Instance = TIM2;
__HAL_RCC_TIM2_CLK_ENABLE(); // 使能 TIM2 时钟

// 配置定时器初始化结构体
htim->Init.Prescaler = Timer_InitStruct->Prescaler;
htim->Init.Period = Timer_InitStruct->Period;
htim->Init.ClockDivision = TIM_InitStruct->ClockDivision;
htim->Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数模式
htim->Init.RepetitionCounter = 0; // 重复计数器 (基本定时器不需要)
htim->Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 禁用自动重载预加载

if (HAL_TIM_Base_Init(htim) != HAL_OK) {
free(htim);
return NULL; // 初始化失败
}

// 配置定时器中断 (如果需要回调函数)
if (Timer_InitStruct->Callback != NULL) {
HAL_TIM_IRQHandlerCallbackTypeDef callback_wrapper = Timer_InitStruct->Callback; // 包装回调函数
HAL_TIM_RegisterCallback(htim, HAL_TIM_PERIOD_ELAPSED_CB_ID, callback_wrapper); // 注册回调函数
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(TIM2_IRQn); // 使能 TIM2 中断
}

return (TimerHandleTypeDef)htim; // 返回定时器句柄
}

void HAL_Timer_Start(TimerHandleTypeDef TimerHandle) {
TIM_HandleTypeDef *htim = (TIM_HandleTypeDef*)TimerHandle;
if (htim != NULL) {
HAL_TIM_Base_Start_IT(htim); // 启动定时器,使能中断
}
}

void HAL_Timer_Stop(TimerHandleTypeDef TimerHandle) {
TIM_HandleTypeDef *htim = (TIM_HandleTypeDef*)TimerHandle;
if (htim != NULL) {
HAL_TIM_Base_Stop_IT(htim); // 停止定时器,禁用中断
}
}

void HAL_Timer_SetCallback(TimerHandleTypeDef TimerHandle, void (*Callback)(void)) {
TIM_HandleTypeDef *htim = (TIM_HandleTypeDef*)TimerHandle;
if (htim != NULL && Callback != NULL) {
HAL_TIM_IRQHandlerCallbackTypeDef callback_wrapper = Callback;
HAL_TIM_RegisterCallback(htim, HAL_TIM_PERIOD_ELAPSED_CB_ID, callback_wrapper);
}
}

uint32_t HAL_Timer_GetCounter(TimerHandleTypeDef TimerHandle) {
TIM_HandleTypeDef *htim = (TIM_HandleTypeDef*)TimerHandle;
if (htim != NULL) {
return HAL_TIM_ReadCount(htim); // 读取定时器计数器值
}
return 0;
}

// 定时器中断处理函数 (需要添加到 STM32 中断向量表,并在 stm32f1xx_it.c 中实现)
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler((TIM_HandleTypeDef*)htim_tim2); // 调用 STM32 HAL 库的定时器中断处理函数
}

// HAL 库定时器中断回调函数 (需要在 stm32f1xx_hal_tim.c 中定义全局变量 htim_tim2 并初始化)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// 执行用户注册的回调函数 (如果已注册)
HAL_TIM_IRQHandlerCallbackTypeDef callback = (HAL_TIM_IRQHandlerCallbackTypeDef)htim->PeriodElapsedCallback;
if (callback != NULL) {
callback(); // 调用用户回调函数
}
}
}

hal_spi.h (简化的 SPI HAL):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#ifndef HAL_SPI_H
#define HAL_SPI_H

#include <stdint.h>

// SPI 句柄 (简化)
typedef void* SPI_HandleTypeDef;

// SPI 初始化结构体
typedef struct {
uint32_t Mode; // SPI 模式 (主模式/从模式)
uint32_t Direction; // 数据方向 (全双工/半双工/只接收/只发送)
uint32_t DataSize; // 数据位大小 (8位/16位)
uint32_t ClockPolarity; // 时钟极性 (CPOL)
uint32_t ClockPhase; // 时钟相位 (CPHA)
uint32_t NSS; // 片选信号管理 (软件/硬件)
uint32_t BaudRatePrescaler; // 波特率预分频器
uint32_t FirstBit; // 数据帧先发送位 (MSB/LSB)
uint32_t TIMode; // TI 模式 (如果需要)
uint32_t CRCCalculation; // CRC 校验使能
uint32_t CRCPolynomial; // CRC 多项式
} SPI_InitTypeDef;

// SPI 模式定义 (部分示例)
#define SPI_MODE_MASTER (0x00000000U) // 主模式
#define SPI_MODE_SLAVE (0x00000001U) // 从模式

// SPI 数据方向定义 (部分示例)
#define SPI_DIRECTION_2LINES (0x00000000U) // 全双工 2 线
#define SPI_DIRECTION_2LINES_RXONLY (0x00000001U) // 只接收 2 线
#define SPI_DIRECTION_1LINE (0x00000002U) // 半双工 1 线

// SPI 数据位大小定义
#define SPI_DATASIZE_8BIT (0x00000000U)
#define SPI_DATASIZE_16BIT (0x00000001U)

// SPI 时钟极性 (CPOL) 定义
#define SPI_POLARITY_LOW (0x00000000U) // 低电平空闲
#define SPI_POLARITY_HIGH (0x00000001U) // 高电平空闲

// SPI 时钟相位 (CPHA) 定义
#define SPI_PHASE_1EDGE (0x00000000U) // 第一个边沿采样
#define SPI_PHASE_2EDGE (0x00000001U) // 第二个边沿采样

// SPI 片选信号管理 (NSS) 定义
#define SPI_NSS_SOFT (0x00000000U) // 软件 NSS
#define SPI_NSS_HARD_OUTPUT (0x00000001U) // 硬件 NSS 输出
#define SPI_NSS_HARD_INPUT (0x00000002U) // 硬件 NSS 输入

// SPI 波特率预分频器定义 (部分示例)
#define SPI_BAUDRATEPRESCALER_2 (0x00000000U)
#define SPI_BAUDRATEPRESCALER_4 (0x00000001U)
#define SPI_BAUDRATEPRESCALER_8 (0x00000002U)
#define SPI_BAUDRATEPRESCALER_16 (0x00000003U)
// ... 定义到 SPI_BAUDRATEPRESCALER_256

// SPI 数据帧先发送位定义
#define SPI_FIRSTBIT_MSB (0x00000000U) // MSB 先发送
#define SPI_FIRSTBIT_LSB (0x00000001U) // LSB 先发送

// SPI 初始化函数
SPI_HandleTypeDef HAL_SPI_Init(SPI_InitTypeDef *SPI_InitStruct);

// SPI 发送一个字节数据
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// SPI 接收一个字节数据
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// SPI 发送并接收一个字节数据
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);

// 设置 SPI 片选信号电平 (软件 NSS)
void HAL_SPI_NSS_Control(SPI_HandleTypeDef hspi, uint8_t NSSState);

#endif // HAL_SPI_H

hal_spi.c (简化的 SPI HAL):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "hal_spi.h"
#include "stm32f1xx_hal.h" // 包含 STM32 HAL 库头文件
#include "hal_gpio.h" // 需要使用 GPIO 控制片选信号

// SPI 初始化函数实现
SPI_HandleTypeDef HAL_SPI_Init(SPI_InitTypeDef *SPI_InitStruct) {
SPI_HandleTypeDef hspi = malloc(sizeof(SPI_HandleTypeDef));
if (hspi == NULL) {
return NULL; // 内存分配失败
}

// 选择 SPI 外设 (例如 SPI1, SPI2, ...) 这里假设使用 SPI1
hspi->Instance = SPI1;
__HAL_RCC_SPI1_CLK_ENABLE(); // 使能 SPI1 时钟

// 配置 SPI 初始化结构体
hspi->Init.Mode = SPI_InitStruct->Mode;
hspi->Init.Direction = SPI_InitStruct->Direction;
hspi->Init.DataSize = SPI_InitStruct->DataSize;
hspi->Init.ClockPolarity = SPI_InitStruct->ClockPolarity;
hspi->Init.ClockPhase = SPI_InitStruct->ClockPhase;
hspi->Init.NSS = SPI_InitStruct->NSS;
hspi->Init.BaudRatePrescaler = SPI_InitStruct->BaudRatePrescaler;
hspi->Init.FirstBit = SPI_InitStruct->FirstBit;
hspi->Init.TIMode = SPI_InitStruct->TIMode;
hspi->Init.CRCCalculation = SPI_InitStruct->CRCCalculation;
hspi->Init.CRCPolynomial = SPI_InitStruct->CRCPolynomial;

if (HAL_SPI_Init_Ex(&hspi) != HAL_OK) {
free(hspi);
return NULL; // 初始化失败
}

return hspi;
}

// SPI 发送一个字节数据实现
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) {
return HAL_SPI_Transmit_Ex(&hspi, pData, Size, Timeout);
}

// SPI 接收一个字节数据实现
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) {
return HAL_SPI_Receive_Ex(&hspi, pData, Size, Timeout);
}

// SPI 发送并接收一个字节数据实现
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout) {
return HAL_SPI_TransmitReceive_Ex(&hspi, pTxData, pRxData, Size, Timeout);
}

// 设置 SPI 片选信号电平 (软件 NSS) 实现
void HAL_SPI_NSS_Control(SPI_HandleTypeDef hspi, uint8_t NSSState) {
// 假设 SPI 片选引脚连接到 GPIO 某个引脚,例如 GPIOA Pin 4
// 需要根据实际硬件连接修改
GPIO_InitTypeDef nss_gpio;
nss_gpio.Port = GPIO_PORT_A; // 假设片选引脚在 GPIOA
nss_gpio.Pin = GPIO_PIN_4; // 假设片选引脚是 Pin 4
nss_gpio.Mode = GPIO_MODE_OUTPUT_PP;
nss_gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(&nss_gpio);

HAL_GPIO_WritePin(GPIO_PORT_A, GPIO_PIN_4, NSSState); // 设置片选信号电平
}

2. 设备驱动层 (Device Driver Layer)

driver_oled.h (OLED 显示屏驱动头文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef DRIVER_OLED_H
#define DRIVER_OLED_H

#include <stdint.h>
#include "hal_spi.h" // 假设 OLED 使用 SPI 接口

// OLED 驱动句柄 (简化)
typedef void* OLED_HandleTypeDef;

// OLED 初始化函数
OLED_HandleTypeDef DRIVER_OLED_Init(SPI_HandleTypeDef hspi);

// OLED 清屏函数
void DRIVER_OLED_Clear(OLED_HandleTypeDef oled_handle);

// OLED 显示字符函数
void DRIVER_OLED_DrawChar(OLED_HandleTypeDef oled_handle, uint8_t x, uint8_t y, char chr, uint8_t size, uint8_t color);

// OLED 显示字符串函数
void DRIVER_OLED_DrawString(OLED_HandleTypeDef oled_handle, uint8_t x, uint8_t y, const char *str, uint8_t size, uint8_t color);

// OLED 设置像素点函数
void DRIVER_OLED_DrawPixel(OLED_HandleTypeDef oled_handle, uint8_t x, uint8_t y, uint8_t color);

// OLED 显示图形函数 (例如,脉搏波形) - 可以后续添加

#endif // DRIVER_OLED_H

driver_oled.c (OLED 显示屏驱动源文件 - SSD1306 驱动示例):

#include "driver_oled.h"
#include "hal_gpio.h"
#include "hal_delay.h" // 假设有 HAL 延时函数

#define OLED_DC_PIN   GPIO_PIN_8   // 数据/命令选择引脚 (DC) - 假设连接到 GPIOA Pin 8
#define OLED_RES_PIN  GPIO_PIN_9   // 复位引脚 (RES) - 假设连接到 GPIOA Pin 9
#define OLED_CS_PIN   GPIO_PIN_4   // 片选引脚 (CS) - 假设连接到 GPIOA Pin 4 (软件 NSS)
#define OLED_SPI_HANDLE  hspi_oled // SPI 句柄 (在初始化时赋值)

static SPI_HandleTypeDef hspi_oled; // 全局 SPI 句柄,用于 OLED 驱动

// 初始化 OLED 引脚 GPIO
static void OLED_GPIO_Init(void) {
    GPIO_InitTypeDef gpio_init_struct;

    // DC 引脚配置
    gpio_init_struct.Port = GPIO_PORT_A;
    gpio_init_struct.Pin = OLED_DC_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(&gpio_init_struct);

    // RES 引脚配置
    gpio_init_struct.Port = GPIO_PORT_A;
    gpio_init_struct.Pin = OLED_RES_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(&gpio_init_struct);

    // CS 引脚配置 (软件 NSS,HAL_SPI_NSS_Control 函数会配置)
    // 在 HAL_SPI_Init 中已经配置了 CS 引脚,这里不需要重复配置
}

// 发送命令到 OLED
static void OLED_SendCommand(uint8_t cmd) {
    HAL_GPIO_WritePin(GPIO_PORT_A, OLED_DC_PIN, GPIO_PIN_RESET); // DC=0:发送命令
    HAL_SPI_NSS_Control(OLED_SPI_HANDLE, GPIO_PIN_RESET);     // CS=0:使能 SPI 从设备
    HAL_SPI_Transmit(OLED_SPI_HANDLE, &cmd, 1, HAL_MAX_DELAY);
    HAL_SPI_NSS_Control(OLED_SPI_HANDLE, GPIO_PIN_SET);       // CS=1:禁用 SPI 从设备
}

// 发送数据到 OLED
static void OLED_SendData(uint8_t data) {
    HAL_GPIO_WritePin(GPIO_PORT_A, OLED_DC_PIN, GPIO_PIN_SET);   // DC=1:发送数据
    HAL_SPI_NSS_Control(OLED_SPI_HANDLE, GPIO_PIN_RESET);     // CS=0:使能 SPI 从设备
    HAL_SPI_Transmit(OLED_SPI_HANDLE, &data, 1, HAL_MAX_DELAY);
    HAL_SPI_NSS_Control(OLED_SPI_HANDLE, GPIO_PIN_SET);       // CS=1:禁用 SPI 从设备
}

// OLED 初始化序列 (SSD1306 初始化命令)
static void OLED_InitSequence(void) {
    HAL_Delay(100); // 上电延时
    HAL_GPIO_WritePin(GPIO_PORT_A, OLED_RES_PIN, GPIO_PIN_RESET); // 复位 OLED
    HAL_Delay(100);
    HAL_GPIO_WritePin(GPIO_PORT_A, OLED_RES_PIN, GPIO_PIN_SET);
    HAL_Delay(100);

    OLED_SendCommand(0xAE);  //display off
    OLED_SendCommand(0x20);  //set memory addressing mode
    OLED_SendCommand(0x10);  //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
    OLED_SendCommand(0xB0);  //set page start address for page addressing mode,0-7
    OLED_SendCommand(0xC8);  //Set COM Output Scan Direction
    OLED_SendCommand(0x00);  //---set low column address
    OLED_SendCommand(0x10);  //---set high column address
    OLED_SendCommand(0x40);  //--set start line address - set maps address to display ram
    OLED_SendCommand(0x81);  //set contrast control register
    OLED_SendCommand(0xFF);  //亮度调节 0x00~0xff
    OLED_SendCommand(0xA1);  //set segment re-map 0 to 127 - column address 0 mapped to SEG0
    OLED_SendCommand(0xA6);  //set normal display
    OLED_SendCommand(0xA8);  //set multiplex ratio(1 to 64)
    OLED_SendCommand(0x3F);  //1/64 duty
}

欢迎关注我的其它发布渠道