当你第一次拿到野火指南者开发板时,可能会被这块小小的电路板上密密麻麻的元器件所震撼。但别担心,这正是嵌入式开发的魅力所在——用代码赋予硬件生命。本文将带你从零开始,用STM32F103打造一个功能完整的智能手环原型,涵盖OLED显示、运动传感器数据采集和蓝牙通信三大核心模块。
在开始编码之前,我们需要先了解整个项目的硬件组成。野火指南者开发板搭载的是STM32F103VET6芯片,这是一颗基于Cortex-M3内核的32位微控制器,具有512KB Flash和64KB RAM,完全足够支撑智能手环的基础功能实现。
所需硬件清单:
开发环境我们选择Keil MDK,这是STM32开发最常用的IDE之一。安装完成后,还需要配置以下工具链:
bash复制# 安装STM32CubeMX(用于生成初始化代码)
wget https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-configurators-and-code-generators/stm32cubemx.html
# 安装ST-Link驱动(用于程序烧录)
wget https://www.st.com/en/development-tools/stsw-link009.html
提示:初次使用Keil时,记得安装STM32F1系列的Device Family Pack,否则无法识别芯片型号。
硬件连接示意图如下:
| 模块 | 开发板接口 | 引脚功能 |
|---|---|---|
| OLED SCL | PB6 | I2C1_SCL |
| OLED SDA | PB7 | I2C1_SDA |
| MPU6050 SCL | PB10 | I2C2_SCL |
| MPU6050 SDA | PB11 | I2C2_SDA |
| HC-05 TX | PA9 | USART1_TX |
| HC-05 RX | PA10 | USART1_RX |
OLED作为智能手环的主要显示界面,需要实现时间、步数等信息的可视化呈现。我们使用的SSD1306驱动芯片通过I2C接口通信,最高支持128x64的分辨率。
首先在STM32CubeMX中配置I2C1外设:
生成代码后,需要编写OLED的驱动层。这里我们采用硬件I2C而非软件模拟,以提高刷新效率:
c复制// OLED初始化序列
void OLED_Init(void) {
HAL_Delay(100);
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用率
OLED_WriteCmd(0x3F);
// ... 其他初始化命令
OLED_WriteCmd(0xAF); // 开启显示
}
// 在指定位置显示字符串
void OLED_ShowString(uint8_t x, uint8_t y, char *str) {
uint8_t j=0;
while(str[j]!='\0') {
OLED_ShowChar(x+j*8,y,str[j]);
j++;
}
}
实际项目中,我们可以创建一个显示任务,周期性地更新屏幕内容:
c复制void Display_Task(void) {
static uint32_t prevTick = 0;
if(HAL_GetTick() - prevTick < 500) return;
prevTick = HAL_GetTick();
OLED_Clear();
OLED_ShowString(0, 0, "Steps:12345");
OLED_ShowString(0, 2, "HR:72bpm");
OLED_ShowString(0, 4, "Batt:85%");
OLED_Refresh();
}
MPU6050集成了三轴加速度计和三轴陀螺仪,通过I2C接口输出原始数据。要将其转化为可用的步数信息,需要经过一系列数据处理步骤。
传感器初始化关键参数:
c复制#define MPU6050_ADDR 0xD0
#define SMPLRT_DIV 0x19
#define CONFIG 0x1A
#define GYRO_CONFIG 0x1B
#define ACCEL_CONFIG 0x1C
#define PWR_MGMT_1 0x6B
void MPU6050_Init(void) {
I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x00); // 解除休眠
I2C_WriteByte(MPU6050_ADDR, SMPLRT_DIV, 0x07); // 采样率1kHz
I2C_WriteByte(MPU6050_ADDR, CONFIG, 0x06); // 低通滤波
I2C_WriteByte(MPU6050_ADDR, ACCEL_CONFIG, 0x01);// ±4g量程
I2C_WriteByte(MPU6050_ADDR, GYRO_CONFIG, 0x18); // ±2000°/s量程
}
步数检测算法的核心是分析加速度数据的波形特征。一个简单但有效的实现方案:
c复制#define WINDOW_SIZE 20
#define STEP_THRESHOLD 1.5f
float accelBuffer[WINDOW_SIZE];
uint16_t stepCount = 0;
void Step_Detection(float accelZ) {
static uint8_t index = 0;
static float avg = 0, variance = 0;
// 更新滑动窗口
avg += (accelZ - accelBuffer[index]) / WINDOW_SIZE;
accelBuffer[index] = accelZ;
index = (index + 1) % WINDOW_SIZE;
// 计算方差
variance = 0;
for(uint8_t i=0; i<WINDOW_SIZE; i++) {
variance += (accelBuffer[i] - avg) * (accelBuffer[i] - avg);
}
variance /= WINDOW_SIZE;
// 步数判断
if(variance > STEP_THRESHOLD && accelZ > avg) {
stepCount++;
}
}
为了提高精度,可以加入卡尔曼滤波对原始数据进行平滑处理:
c复制typedef struct {
float q; // 过程噪声协方差
float r; // 测量噪声协方差
float x; // 估计值
float p; // 估计误差协方差
float k; // 卡尔曼增益
} KalmanFilter;
float Kalman_Update(KalmanFilter *kf, float measurement) {
kf->p = kf->p + kf->q;
kf->k = kf->p / (kf->p + kf->r);
kf->x = kf->x + kf->k * (measurement - kf->x);
kf->p = (1 - kf->k) * kf->p;
return kf->x;
}
HC-05蓝牙模块通过串口与STM32通信,我们需要配置USART1为异步模式,波特率9600。在CubeMX中设置:
数据传输协议设计采用简单的帧结构:
code复制[头字节0xAA][数据长度][数据类型][数据内容][校验和]
示例代码实现:
c复制#define BLE_HEADER 0xAA
typedef enum {
DATA_STEPS = 0x01,
DATA_HR = 0x02,
DATA_BATT = 0x03
} DataType;
void BLE_SendData(DataType type, uint8_t *data, uint8_t len) {
uint8_t buf[20];
uint8_t checksum = 0;
buf[0] = BLE_HEADER;
buf[1] = len + 2; // 长度=数据长度+类型(1)+校验(1)
buf[2] = type;
for(uint8_t i=0; i<len; i++) {
buf[3+i] = data[i];
checksum += data[i];
}
buf[3+len] = checksum;
HAL_UART_Transmit(&huart1, buf, len+4, 100);
}
// 发送步数示例
void Send_StepCount(uint16_t steps) {
uint8_t data[2];
data[0] = steps >> 8;
data[1] = steps & 0xFF;
BLE_SendData(DATA_STEPS, data, 2);
}
在手机端,可以使用任何支持SPP协议的蓝牙串口APP接收数据。更专业的做法是开发一个简单的Android应用,按照协议解析数据并可视化展示。
将各模块整合为一个完整的系统需要考虑任务调度、功耗管理和用户交互等多个方面。我们采用基于HAL库的非阻塞式编程模式,避免使用delay等阻塞函数。
主循环设计:
c复制int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_I2C2_Init();
MX_USART1_UART_Init();
OLED_Init();
MPU6050_Init();
BLE_Init();
while(1) {
static uint32_t tick = 0;
if(HAL_GetTick() - tick > 100) { // 10Hz系统心跳
tick = HAL_GetTick();
Sensor_Update(); // 更新传感器数据
Display_Update(); // 刷新显示
BLE_Process(); // 处理蓝牙通信
if(Button_Pressed()) {
Enter_LowPowerMode();
}
}
}
}
功耗优化是穿戴设备的关键。STM32F103提供了多种低功耗模式,智能手环通常采用以下策略:
| 工作模式 | 电流消耗 | 唤醒方式 | 适用场景 |
|---|---|---|---|
| Run Mode | 5mA | - | 活跃使用 |
| Sleep Mode | 1.5mA | 任意中断 | 短暂空闲 |
| Stop Mode | 20μA | 外部中断/RTC | 夜间睡眠 |
| Standby Mode | 2μA | 复位/唤醒引脚 | 长期存储 |
实现动态功耗切换:
c复制void Enter_LowPowerMode(void) {
// 关闭外设时钟
__HAL_RCC_I2C1_CLK_DISABLE();
__HAL_RCC_I2C2_CLK_DISABLE();
__HAL_RCC_USART1_CLK_DISABLE();
// 配置唤醒源(如RTC闹钟或按键中断)
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
// 进入Stop模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新初始化系统
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
// ...其他外设初始化
}
基础功能实现后,可以考虑添加以下增强特性:
1. 手势识别:
c复制typedef enum {
GESTURE_NONE,
GESTURE_UP,
GESTURE_DOWN,
GESTURE_LEFT,
GESTURE_RIGHT
} GestureType;
GestureType Detect_Gesture(float accelX, float accelY) {
static float bufferX[5], bufferY[5];
static uint8_t index = 0;
bufferX[index] = accelX;
bufferY[index] = accelY;
index = (index + 1) % 5;
float diffX = bufferX[(index+4)%5] - bufferX[index];
float diffY = bufferY[(index+4)%5] - bufferY[index];
if(fabs(diffX) > fabs(diffY)) {
return diffX > 0 ? GESTURE_RIGHT : GESTURE_LEFT;
} else {
return diffY > 0 ? GESTURE_UP : GESTURE_DOWN;
}
}
2. 简易菜单系统:
c复制typedef struct {
char *title;
void (*action)(void);
} MenuItem;
MenuItem menu[] = {
{"Step Counter", Show_Steps},
{"Heart Rate", Show_HR},
{"Settings", Show_Settings},
{"Power Off", Shutdown}
};
void Show_Menu(uint8_t selected) {
OLED_Clear();
for(uint8_t i=0; i<4; i++) {
if(i == selected) {
OLED_ShowString(5, i*2, ">");
}
OLED_ShowString(15, i*2, menu[i].title);
}
}
void Handle_ButtonPress(void) {
static uint8_t selected = 0;
if(Button_Up_Pressed()) {
selected = (selected == 0) ? 3 : selected - 1;
} else if(Button_Down_Pressed()) {
selected = (selected + 1) % 4;
} else if(Button_Select_Pressed()) {
menu[selected].action();
}
Show_Menu(selected);
}
3. 数据持久化存储:
STM32F103VET6内部Flash可以用于存储用户数据,如累计步数等。关键实现:
c复制#define USER_DATA_ADDR 0x0800FC00 // 使用最后1KB空间
void Flash_WriteData(uint32_t *data, uint16_t len) {
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.PageAddress = USER_DATA_ADDR;
erase.NbPages = 1;
uint32_t pageError;
HAL_FLASHEx_Erase(&erase, &pageError);
for(uint16_t i=0; i<len; i++) {
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
USER_DATA_ADDR + i*4,
data[i]);
}
HAL_FLASH_Lock();
}
void Flash_ReadData(uint32_t *data, uint16_t len) {
for(uint16_t i=0; i<len; i++) {
data[i] = *(__IO uint32_t*)(USER_DATA_ADDR + i*4);
}
}
在实际开发过程中,以下几个调试工具和技巧能显著提高效率:
1. 逻辑分析仪使用:
2. STM32CubeMonitor:
实时监控变量变化,无需打断点:
ini复制# 配置文件示例
[Variables]
steps = 0x20000000:4 # 监控内存地址0x20000000处的4字节数据
3. 常见问题排查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| OLED不显示 | I2C地址错误 | 检查0x3C/0x3D地址选择 |
| MPU6050数据异常 | 电源噪声 | 增加0.1μF去耦电容 |
| 蓝牙连接不稳定 | 波特率不匹配 | 确认模块与代码波特率一致 |
| 系统随机复位 | 堆栈溢出 | 增大启动文件中的堆栈大小 |
| 功耗偏高 | 未关闭调试接口 | 禁用SWD接口 |
4. 性能优化技巧:
makefile复制# 在Makefile中添加优化选项
CFLAGS = -mcpu=cortex-m3 -mthumb -O2 -fdata-sections -ffunction-sections
LDFLAGS = -Wl,--gc-sections
完成这个项目后,你会发现STM32开发并没有想象中那么困难。关键在于理解硬件工作原理,掌握外设配置方法,以及培养良好的调试习惯。这个智能手环原型虽然简单,但已经包含了嵌入式系统开发的精髓——硬件驱动、传感器数据处理和无线通信。