这个基于STM32F103C8T6的电子沙粒模拟项目,是我在嵌入式系统开发领域的一次有趣尝试。通过0.96寸OLED显示屏和MPU6050六轴传感器的组合,实现了20个"沙粒"的物理模拟和倾斜控制交互。整个系统运行在72MHz主频的STM32F103C8T6上,通过精心设计的算法实现了约22fps的流畅动画效果。
主控芯片选择:
STM32F103C8T6这颗Cortex-M3内核的MCU,具有64KB Flash和20KB RAM,对于这个项目来说恰到好处。我选择它的原因主要有三点:
显示模块选型:
0.96寸OLED(SSD1306驱动)采用I2C接口,相比SPI版本节省了IO口。分辨率128x64虽然不高,但足以呈现清晰的粒子效果。这里有个细节需要注意:OLED的I2C地址通常是0x78(7位地址0x3C),但不同厂家可能有差异。
运动传感器配置:
MPU6050集成了3轴加速度计和3轴陀螺仪,通过I2C接口与MCU通信。实际使用中发现,它的原始数据输出会有噪声,因此我在初始化时配置了5Hz的数字低通滤波器(DLPF),这在后续的倾斜检测中起到了关键作用。
根据项目需求,我设计了如下引脚分配:
code复制OLED:
- SCL → PB8
- SDA → PB9
MPU6050:
- SCL → PB10
- SDA → PB11
这种分配方案有几点考虑:
提示:实际焊接时,建议为I2C线路加上拉电阻(通常4.7kΩ)。虽然开发板可能已经集成,但自制电路时容易忽略这点。
整个软件采用经典的三层架构:
code复制应用层 (main.c)
├─ 沙粒模拟算法
├─ 倾斜控制逻辑
└─ 碰撞检测系统
驱动层
├─ OLED驱动 (ssd1306.c)
├─ MPU6050驱动 (mpu6050.c)
└─ I2C底层 (myiic.c)
硬件层
├─ STM32外设
├─ OLED显示屏
└─ MPU6050传感器
这种设计的优势在于:
粒子状态结构体:
c复制typedef struct {
uint8_t x; // X坐标 (0-122)
uint8_t y; // Y坐标 (0-59)
int8_t dir_x; // X方向:1=右,-1=左,0=静止
int8_t dir_y; // Y方向:1=下,-1=上,0=静止
uint8_t first_run; // 首次运行标志
} LightPoint;
这个结构体设计有几个精妙之处:
内存布局优化:
c复制LightPoint points[20]; // 粒子状态数组
uint8_t old_x[20], old_y[20]; // 上一帧位置
uint8_t new_x[20], new_y[20]; // 新位置
通过分离新旧位置存储,实现了"双缓冲"效果,确保画面更新时的连续性。
绘制函数:
c复制void Draw_Point(uint8_t x, uint8_t y, uint8_t mode) {
for(uint8_t j=y; j<y+POINT_HEIGHT; j++) {
for(uint8_t i=x; i<x+POINT_WIDTH; i++) {
if(mode == 1) OLED_DrawPoint(i,j);
else OLED_ClearArea(i,j,1,1);
}
}
}
这个函数的几个关键点:
实测发现:直接操作显存比逐个画点效率高约30%,但会占用更多RAM。在资源受限的STM32上,这种权衡是必要的。
AABB碰撞检测:
c复制uint8_t CheckCollision(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) {
return (x1 < x2 + POINT_WIDTH && x1 + POINT_WIDTH > x2 &&
y1 < y2 + POINT_HEIGHT && y1 + POINT_HEIGHT > y2);
}
这个算法之所以高效,是因为:
全环境碰撞检测:
c复制uint8_t CheckAllCollisions(uint8_t index, uint8_t x, uint8_t y) {
// 边界检查
if(x > (127-POINT_WIDTH) || y > (63-POINT_HEIGHT))
return 1;
// 粒子间碰撞检查
for(uint8_t i=0; i<POINT_NUM; i++) {
if(i != index && CheckCollision(x,y, points[i].x,points[i].y))
return 1;
}
return 0;
}
实际调试中发现,将边界检查放在循环外,性能提升了约15%。这是嵌入式开发中典型的空间换时间优化。
当粒子移动受阻时,系统会尝试以下方向优先级:
c复制if(CheckAllCollisions(i, temp_x, temp_y)) {
uint8_t can_move = 0;
if(points[i].dir_y == 1) { // 向下受阻
if(!CheckAllCollisions(i, temp_x-1, temp_y)) { // 尝试左下
temp_x--; can_move=1;
}
else if(!CheckAllCollisions(i, temp_x+1, temp_y)) { // 尝试右下
temp_x++; can_move=1;
}
// 其他方向...
}
if(!can_move) { // 完全受阻
temp_x = points[i].x;
temp_y = points[i].y;
}
}
这个算法模拟了沙粒在重力作用下的自然堆积行为:
c复制void mpu6050_Init(void) {
mpu6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); // X轴陀螺时钟
mpu6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); // 100Hz采样
mpu6050_WriteReg(MPU6050_CONFIG, 0x06); // 5Hz DLPF
mpu6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);// ±16g量程
}
这些配置的考虑:
c复制mpu6050_GetData(&AccX, &AccY, &AccZ, &GyroX, &GyroY, &GyroZ);
if(AccX > TILT_THRESHOLD) global_dir_x = 1;
else if(AccX < -TILT_THRESHOLD) global_dir_x = -1;
else global_dir_x = 0;
if(AccY > TILT_THRESHOLD) global_dir_y = 1;
else if(AccY < -TILT_THRESHOLD) global_dir_y = -1;
else global_dir_y = 1; // 默认向下
这里的几个技巧:
传统做法是每次修改后立即刷新,但本项目采用批量更新策略:
实测显示,这种方式比逐个更新快2-3倍,因为减少了I2C通信开销。
通过将粒子数组声明为全局静态变量,确保其分配在快速访问的RAM区域:
c复制static LightPoint points[20] __attribute__((section(".ram_data")));
在STM32中,这种显式内存区域指定可以避免总线竞争,提升约10%的访问速度。
c复制while(1) {
uint32_t start = HAL_GetTick();
// 所有处理逻辑...
while(HAL_GetTick() - start < MOVE_SPEED); // 确保25ms周期
}
这种精确的延时控制保证了动画的流畅性,避免了因处理时间波动导致的卡顿。
现象:粒子在移动时出现闪烁
原因:擦除和绘制不同步导致
解决方案:
现象:需要大幅度倾斜才有反应
排查步骤:
现象:OLED或MPU6050无响应
诊断方法:
当前20个粒子的限制主要来自:
优化方案:
可以引入:
通过按键增加:
这个项目最让我满意的地方是,用简单的算法实现了相当逼真的物理效果。在资源受限的STM32上,通过精心设计的数据结构和算法优化,证明了嵌入式系统也能处理有趣的物理模拟。下一步我计划加入更多交互元素,比如通过触摸屏添加/移除粒子,或者实现多设备联机互动。