基于 STM32 的智能手环设计（无 lvgl）持续更新中

一、引脚分配

器件 | 资源 | 引脚
OLED 显示屏 | 软件 IIC | SCL: PB8 SDA: PB9
MPU6050 | 硬件 IIC | I2C2: SCL: PB10 SDA: PB11
MAX30102 | 软件 IIC | SCL: PC14 SDA: PC15
DHT11 | Wire | PB6
W25Q64 | 硬件 SPI | SPI1: SS: PA4 SCK: PA5 MOSI: PA7 MISO: PA6
ESP01 | Uart | Uart2: esp01(Tx): PA3 esp01(Rx): PA2
MQ-2 | ADC/GPIO | ADC: PB0 GPIO: PB1
蜂鸣器 | GPIO | PB5
按键 (4 个) | GPIO | PA8 PA9 PA10 PA1

此图展示的设计初稿中所用到的模块和单片机资源，在实际设计中并没有完全应用，但是图中依然对其进行了保留，后续在添加功能时会对其进行调用。

二、设计可视化操作界面

开机界面

时间

网络连接信息

菜单选项

电量显示

菜单界面

天气显示

温湿度信息

脉搏信息

个人中心

步数信息

本项目并没有采用 lvgl 图形库技术，而是采用点阵绘图的形式进行可视化界面设计，这比较考验作者的美术技术，由于该该方法效率低下、且在其它各方面有明显不足，不建议使用。由于时间较忙，功能并没有开发完全，后续在空闲时会陆续将功能补齐。

三、云端设计

1. 云上 App 端设计

在数据上传上，我们借助esp8266 模块 + 点灯科技对 stm32 获取到的数据进行上传。对应我们点灯科技 App 上创建了一些数据存储单元。借助点灯科技 App，我们只需要在右上角点击编译按钮便可以自由添加我们想要的各种器件。

2. 数据上传与通信

2.1 数据上传

在数据上传上，我们采用esp8266 模块加Arduino 框架进行实现，点灯科技在 Arduino 上设计有支持 esp 系列与云端交互的软件框架，我们只需要调用去库函数，便可以完成我们想要的功能。

（1）开发环境准备

首先，我们需要获取 esp8266 的开发板支持包，网上有很多教程。我们可以通过该博客进行 esp8266 支持包的安装。(13 条消息) Arduino 搭建 Esp8266 开发环境（两种方法）arduino esp8266℡四叶草～的博客 - CSDN 博客

如果我们已经安装好了 ESP8266 的支持包，我们需要去安装 Blinker 的官方库，

首先点击 "项目"，找到 "加载库" 选项，点击它，找到管理库，点击进去。

在此对话框的输入框输入Blinker，接着选择版本，我的版本是0.3.9，然后点击安装，等待一会就可以完成。由于此处应用的是外网，所以下载速度比较慢，耐性等待即可。

（2）程序设计

首先，我们需要包含头文件。

Copy
#define BLINKER_PRINT Serial
#define BLINKER_WIFI

#include <Blinker.h>

连接 wifi 和并连接到点灯科技

(1) 首先定义三个字符数组，加上自己的信息。

Copy
//连接信息
char auth[] = "你的密钥";
char ssid[] = "你的wifi名称";
char pswd[] = "你的wifi密码";

​ (2) 在 void setup () 中调用以下函数，完成对 wifi 和点灯科技服务器的连接

Copy
Blinker.begin(auth, ssid, pswd);

​ （3）欲执行数据上传，首先我们需要创建组件，然后创建数据存储对象，最后在心跳包或者其它函数里执行数据打印，这样一来，获取到的数据将会显示在我们所创建的组件下，我们也可以同过点灯科技向 esp01 发送数据，我们这里实现了一个开关点灯功能。

注意：创建组件对象时传入的字符串，应该和我们在点灯科技上创建的组件对象名一模一样，否则无法完成我们想要的功能。

Copy
// 新建组件对象
BlinkerButton Button1("btn-1");
BlinkerNumber Tem("tem");
BlinkerNumber Hum("hum");
BlinkerNumber Smoke("smoke");
BlinkerNumber Maibo("maibo");
BlinkerNumber Bushu("bushu");

// 按下按键即会执行该函数
void button1_callback(const String & state) {
BLINKER_LOG("get button state: ", state);
digitalWrite(Esp01_Led, !digitalRead(Esp01_Led));
}

//云存储温湿度数据函数
void dataStorage()
{
Blinker.dataStorage("temp_chart", (float)tem);//存储温度
Blinker.dataStorage("hum_chart", (float)tem);//存储湿度
Blinker.dataStorage("smoke_chart", (float)smoke);//存储湿度
Blinker.dataStorage("maibo_chart", (int)maibo);//存储湿度
Blinker.dataStorage("bushu_chart", (uint32_t)bushu);//存储湿度
}

//心跳包
void heartbeat(){
Hum.print((float)hum);
Tem.print((float)tem);
Smoke.print((float)smoke);
Maibo.print((int)maibo);
Bushu.print((uint32_t)bushu);
}

void setup() {
// 初始化串口
Serial.begin(115200);

#if defined(BLINKER_PRINT)
BLINKER_DEBUG.stream(BLINKER_PRINT);
#endif

// 初始化有LED的IO
pinMode(Esp01_Led, OUTPUT);
digitalWrite(Esp01_Led, HIGH);
// 初始化blinker
Blinker.begin(auth, ssid, pswd);
Button1.attach(button1_callback);
Blinker.attachHeartbeat(heartbeat);
}

2.2 数据通信

在整个智能手环设计系统中，我们使用到的 esp01 只是一个辅助模块，真正执行其数据获取与交互设计的模块还是我们的 stm32。esp01 支持串口通信，因此我们便使用串口与 stm32 进行数据交互，针对此需求，我们设计了一套数据通信规则。

（1）esp01 端通信设计

我们的需求大致有两个部分，发送和上传。经过综合考虑，决定采取 esp01 和 stm32 交互通信的方式，并采用 CRC 校验和叠加校验等方式进行数据校验。基于此方式，我们在 esp01 端定义了很多数据指令，例如，stm32 向 esp01 发送oxff时，esp01 会回传获取到的时间数据（3 位时分秒 + 一位校验位）。

Copy
void CmdMode::Run(ModeManager *manager){
cmd = 0x00;
if(Serial.available()){
cmd = Serial.read();
}
//获取时间数据指令，esp01将时分秒和校验位发送给stm32
if(cmd == 0xff){
Serial.write(hour1); //时
Serial.write(minute1); //分
Serial.write(sec1); //秒
Serial.write(hour1 + minute1 + sec1); //检验位
}
//获取温度数据指令，切换到温度接收模式
else if(cmd == 0xfe){
Mode *tem_pin;
tem_pin = manager->rd;
manager->setRd(new TemdataMode());
delete tem_pin;
}
//获取烟雾数据指令，切换到烟雾接收模式
else if(cmd == 0xfd){
Mode *tem_pin;
tem_pin = manager->rd;
manager->setRd(new SmokedataMode());
delete tem_pin;
}
//获取温湿度模式，切换到温湿度接收模式
else if(cmd == 0xfc){
Mode *tem_pin;
tem_pin = manager->rd;
manager->setRd(new MaibodataMode());
delete tem_pin;
//获取步数模式，切换到步数接收模式
else if(cmd == 0xfb){
Mode *tem_pin;
tem_pin = manager->rd;
manager->setRd(new BushudataMode());
delete tem_pin;
}
}

由于二者之间数据指令的繁多，容易对开发造成很多不必要的困扰。在程序设计过程中，我们采用23 种面向对象设计模式中的状态模式，实现对不同指令的解耦合。虽然在状态切换的过程中，我们需要不断地创建和销毁对象，但是依靠这种模式，我们的程序可维护性大大提高，并且省去了很多条件判断。

（2）stm32 端通信设计

基于我们在esp01端设计的通信程序，我们在 stm32 端只需要使用指令接着进行数据的上传的接收就行，操作简单方便，例如当我们需要上传温湿度信息，我们只需在 stm32 端发送指令0xfe，接着我们将我们需要发送的数据发给 esp01 (两位数据位（温度、湿度），校验位（这里是二者之和）)。

Copy
Serial_SendByte(0xfe);
Serial_SendByte(tem);
Serial_SendByte(hum);
Serial_SendByte(tem+hum);

四、功能设计

4.1 DHT11 温湿度信息获取

​ DHT11是一款数字温湿度传感器模块，其转换结果可直接通过单总线数字信号输出，是一种常用的环境温湿度测量模块。该模块采用高质量的温湿度传感器，具有高精度、长寿命、稳定性好等特点。它具有快速响应、输出稳定等特性，可以轻松地接口到大多数微控制器上，并通过单总线来与其通信。该模块除了可以测量环境的温湿度值之外，还可以设置上下限值，当环境温度和湿度超出设定的范围时，会产生报警信号。DHT11 模块广泛应用于家居自动化、物联网、环境监测等领域。

（1）DHT11 采用Wire 单总线通信协议，是一种半双工、异步的通信方式。作为主机的 STM32 单片机，我们事先需要给 DHT11 发送接收数据的指令，之后我们才可以进行数据接收，因此，在驱动程序的设计中，我们需要事先将 STM32 端口设置为输出模式。这里我们设置的是推挽输出，然后置端口电平为高电平。

Copy
/**
* @brief DHT11初始化
* @param 无（初始化时，我们将引脚模式设置为推挽输出）
* @retval 无
*/
void DHT11_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

GPIO_InitTypeDef GPIO_InitStruct;

GPIO_InitStruct.GPIO_Pin = DHT11_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_GPIO, &GPIO_InitStruct);

GPIO_SetBits(DHT11_GPIO, DHT11_PIN);
}

（2）Wire 单总线读取数据的周期至少60us，且在两次独立的读时隙之间至少需要1us的恢复时间，每个读时隙都由主机发起，至少拉低总线 1us。在主机发起读时隙之后，单总线器件才开始在总线上发送 0 或 1。

​ 若从机发送 1，则保持总线为高电平；若发送 0，则拉低总线。当发送 0 时，从机在该时隙结束后释放总线，由上拉电阻将总线拉回至空闲高电平状态。从机发出的数据在起始时隙之后，保持有效时间 15us，因而，主机在读时隙期间必须释放总线，并且在时隙起始后的 15us 之内采样总线状态。

（3）因此，在程序设计中。我们通过等待 30us 的操作，延时到主机可读字节的时间端。大体逻辑是：等待主机发送读指令的低电平结束 -> 等待 30us -> 读取电平 -> 等待电平结束。

Copy
/**
* @brief DHT11读字节
* @param 无（判断逻辑在于：当总线拉低后等待30us，如果之后为高电平则判断为1，否则为0）
* @retval 无
*/
uint8_t DHT11_Read_Byte(void)
{
uint8_t i, data = 0;

for (i = 0; i < 8; i++)
{
while (!GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN)); //等待低电平结束
Delay_us(30);
if (GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN)) //高电平开始
data |= (1 << (7 - i));
while (GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN)); //等待高电平结束
}

return data;
}

（4）想要对 DHT11 发送电平读取指令，将数据线转为RX 接收模式，我们需要先将总线电平拉低至少 480us，然后再将总线拉至高电平。代码中，我们在此刻重新配置 IO 口模式，将其设置为了浮空输入模式，接着对数据进行接收。

Copy
/**
* @brief DHT11数据
* @param 无
* @retval 无
*/
void DHT11_Read_Data(uint8_t* temp, uint8_t* humi)
{
uint8_t data[5], i;

GPIO_InitTypeDef GPIO_InitStruct;

GPIO_InitStruct.GPIO_Pin = DHT11_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_GPIO, &GPIO_InitStruct);

//拉低至少480us，进入RX接收模式
GPIO_ResetBits(DHT11_GPIO, DHT11_PIN);
Delay_us(600);
GPIO_SetBits(DHT11_GPIO, DHT11_PIN);
Delay_us(30);
//在读数据时，我们需要把GPIO口切换
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DHT11_GPIO, &GPIO_InitStruct);

if (!GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN))
{
while (!GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN)); //等待低电平结束
while (GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN)); //等待高电平开始
for (i = 0; i < 5; i++)
data[i] = DHT11_Read_Byte();
//校验和为前 4 个字节数据相加
if (data[4] == (data[0] + data[1] + data[2] + data[3]))
{
*humi = data[0];
*temp = data[2];
}
}
}

4.2 Max30102 脉搏检测

​ Max30102 是一款高度集成的精密心率传感器模块，采用了专有的光学传感技术和先进的数字信号处理装置，可以快速准确地测量人体的心率、脉搏氧饱和度（SpO2）等生理指标。该模块内置高亮度 LED 光源和光敏探测器，可以在极短的时间内采集出受测者皮肤区域的反射光信号，并将其转换成电信号输出。Max30102 支持不同的采样率和分辨率设置，同时通过内置 DC 可调增益放大器和 8 位 ADC 实现了片上数据滤波、定标和校准等功能。该模块还提供了I2C 接口进行通信，可以轻松地接口到大多数微控制器上，并可以直接获取数据、设定阈值、控制 LED 发光等操作。目前，Max30102 模块主要应用于健康监测、医疗设备、体育训练等领域。

（1）I2C（Inter-Integrated Circuit）是一种串行通信总线协议，通常用于集成电路之间的通信。它是由 Philips 公司（现在的 NXP 公司）推出的，常用于各种嵌入式系统的通信。I2C 协议采用了双线制，其中一条叫作SDA（Serial Data），另一条叫作SCL（Serial Clock）。SDA 线用于数据传输，SCL 线则用于时钟同步，通过这两条线，设备之间可以进行双向通信。根据其性质，我们可知它是一种半双工、同步的通信协议。

（2）根据 MAx30102 的数据手册，我们收集到了，有关该芯片的一些数据指令，我们使用宏定义将其罗列在此

Copy
#define max30102_WR_address 0xAE

#define I2C_WRITE_ADDR 0xAE
#define I2C_READ_ADDR 0xAF

//register addresses
#define REG_INTR_STATUS_1 0x00
#define REG_INTR_STATUS_2 0x01
#define REG_INTR_ENABLE_1 0x02
#define REG_INTR_ENABLE_2 0x03
#define REG_FIFO_WR_PTR 0x04
#define REG_OVF_COUNTER 0x05
#define REG_FIFO_RD_PTR 0x06
#define REG_FIFO_DATA 0x07
#define REG_FIFO_CONFIG 0x08
#define REG_MODE_CONFIG 0x09
#define REG_SPO2_CONFIG 0x0A
#define REG_LED1_PA 0x0C
#define REG_LED2_PA 0x0D
#define REG_PILOT_PA 0x10
#define REG_MULTI_LED_CTRL1 0x11
#define REG_MULTI_LED_CTRL2 0x12
#define REG_TEMP_INTR 0x1F
#define REG_TEMP_FRAC 0x20
#define REG_TEMP_CONFIG 0x21
#define REG_PROX_INT_THRESH 0x30
#define REG_REV_ID 0xFE
#define REG_PART_ID 0xFF

（3）依据数据指令集，我们调用数据线（SDA）进行指令发送，并将其封装成函数形式。

Copy
void max30102_init(void);
void max30102_reset(void);
u8 max30102_Bus_Write(u8 Register_Address, u8 Word_Data);
u8 max30102_Bus_Read(u8 Register_Address);
void max30102_FIFO_ReadWords(u8 Register_Address,u16 Word_Data[][2],u8 count);
void max30102_FIFO_ReadBytes(u8 Register_Address,u8* Data);

void maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data);
void maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data);
void maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led);

4.3 时间与天气的获取

（1）时间获取

​ 对于时间获取 Blinker 官方库有其封装的 API 函数，我们只需对其进行调用，便可以实现对时间获取，接着利用串口通信，我们可以将时间数据打包，发送给 STM32，最后显示在可视化界面中。

Copy
//时间数据获取
hour1 = Blinker.hour();
minute1 = Blinker.minute();
sec1 = Blinker.second();

Copy
//时间数据回传
if(cmd == 0xff){
Serial.write(hour1);
Serial.write(minute1);
Serial.write(sec1);
Serial.write(hour1 + minute1 + sec1);
}

（2）天气的获取

4.4 步数的计算和获取

4.5 个人中心设计

​ 个人中心实际上提供了对佩戴者健康信息的实时监测，主要在两个方面：“脉搏与血氧饱和度监测”、"吸烟监测"。利用到了烟雾模块和Max30102 模块，通过对烟雾、脉搏、血氧浓度的定时监测，并一次为参考，判断佩戴者是否吸烟、心率、血氧是否正常，并将数据实时上传到云端，实现对佩戴者健康状态的实时监测，从何保障佩戴者的身体健康。

（1）我们所使用到的烟雾模块是 MQ2，该模块共有 4 个引脚，分别是 VCC、GND、AO、DO，其中 AO 和 DO 分别对应数字输出和模拟输出端口，我们使用到的口为 AO 模拟输出端口。

（2）读取数据时我们需要将 STM32 的端口（需要选择支持 ADC 功能的端口）模式配置为模拟输入模式，然后，STM32 内置 ADC 模块会自动完成 ADC 模数转化。最后我们将模拟值读出，为了将数据转化为百分比的格式，我们用当前值进行归一化处理映射到 0~1 之间，乘上 100，最后得到我们想要的百分比格式。

Copy
#include "Smoke.h" // Device header

/**
* @brief ADC初始化
* @param 无
* @retval 无
*/
void Smoke_Init(void)
{
//定义相关结构体，进行参数配置
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
//开启ADC和GPIOA所在总线
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//配置时钟分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO模式。模式为ADC输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//
ADC_RegularChannelConfig(ADC1,ADC_Channel_8,1,ADC_SampleTime_55Cycles5);
//配置ADC的相关配置参数
ADC_InitStructure.ADC_Mode =ADC_Mode_Independent; //设置为单独工作模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //指定数据为右对齐
//配置ADC相关的参数
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //不使用外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //关闭连续转化模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//关闭扫描模式
ADC_InitStructure.ADC_NbrOfChannel = 1; //ָ指定ADC通道
ADC_Init(ADC1,&ADC_InitStructure);
//启动ADC1
ADC_Cmd(ADC1,ENABLE);

ADC_ResetCalibration(ADC1); //重置校准寄存器
while(ADC_GetResetCalibrationStatus(ADC1) == SET); //等待校准完成
ADC_StartCalibration(ADC1); //开启校准功能
while(ADC_GetCalibrationStatus(ADC1) == SET); //等待开启完成
}

/**
* @brief ADC获取模拟值量化后的数据
* @param 无
* @retval 经过转化后的ADc值
*/
uint16_t Smoke_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE); //软件开启ADC1的时钟
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET); //等待ADc转化完成
return ADC_GetConversionValue(ADC1); //返回ADC转化后的值
}

（3）为了实时监测佩戴者健康这状态且避免监测频率过高而阻塞主程序运行，影响流畅度，将脉搏、血氧浓度、烟雾获取的任务进行定时，我们设置的采样间隔是 1 分钟，我们可以在点灯科技 APP 或者手环上的个人中心上查看到具体数据。如果这三组数据任何一组数据低于正常值，手环会进行报警，并且将数据发送到云端，提醒路人和家属。