STM32与GY-25六轴传感器实战:从串口配置到欧拉角数据解析(附完整代码)
STM32与GY-25六轴传感器实战:从串口配置到欧拉角数据解析(附完整代码)
最近在做一个四轴飞行器的姿态控制项目,手头正好有一块GY-25六轴传感器模块。说实话,刚开始拿到这个模块时,看着它小巧的体积和简单的四根线,心里有点打鼓——这么个小东西真能稳定输出精确的欧拉角数据吗?经过几天的折腾,从硬件连接到代码调试,再到数据验证,我发现GY-25确实是个“小而美”的解决方案,特别适合那些对姿态测量有需求但又不想自己折腾复杂滤波算法的嵌入式开发者。这篇文章,我就把自己踩过的坑、验证过的代码,以及一些调试心得整理出来,希望能帮你快速上手这个模块。
1. 硬件连接与模块初识
GY-25模块的硬件接口极其简洁,这既是它的优点,也可能成为新手困惑的起点。模块通常只有四个引脚:VCC、GND、TX、RX。我手头的这块模块标称工作电压是3.3V到5V,实测下来,直接用STM32的3.3V供电完全没问题,工作电流在10mA左右,对大多数嵌入式系统来说负担不大。
注意:有些批次或不同供应商的GY-25模块,其引脚顺序可能略有差异。我遇到过TX和RX丝印位置与实际功能相反的情况,如果通信不上,第一件事就是尝试交换TX和RX的连接。
与STM32的连接非常简单,但有几个细节需要特别注意:
- 电平匹配:GY-25模块的串口是TTL电平,与STM32的USART引脚电平完全兼容,无需额外的电平转换电路。
- 电源去耦:虽然模块内部有稳压,但在VCC和GND之间并联一个0.1uF的陶瓷电容,能有效滤除电源噪声,对提高数据稳定性有帮助,尤其是在电机等干扰源附近使用时。
- 接线长度:如果使用杜邦线连接,尽量缩短长度,特别是当波特率设置较高(如115200)时,过长的导线可能引入信号完整性问题。
下面是一个典型的连接对照表,以STM32F103C8T6(蓝色pill开发板)为例:
| GY-25引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
| TX | PA10 (USART1_RX) | 模块发送,MCU接收 |
| RX | PA9 (USART1_TX) | 模块接收,MCU发送 |
这里最容易出错的就是TX/RX的交叉连接——模块的TX要接MCU的RX,模块的RX要接MCU的TX。我第一次调试时就在这里卡了半小时,总收不到数据,后来用逻辑分析仪一看,才发现接反了。
模块上电后,通常会有个蓝色或绿色的LED开始闪烁,这表明模块内部处理器已经启动并在进行传感器初始化和数据融合计算。大约需要300-500毫秒的初始化时间,之后才能正常响应指令。所以你的初始化代码里最好加个HAL_Delay(500)之类的等待。
2. 串口通信配置与协议解析
GY-25支持两种工作模式:连续输出模式和查询模式。连续模式是模块自动按固定频率发送数据帧,查询模式则需要MCU主动发送指令请求数据。对于大多数实时性要求高的应用(如飞控),连续模式更合适;而对于功耗敏感或只需要间歇读取数据的场景,查询模式能节省不少电能。
2.1 串口参数配置
模块默认的波特率可能是9600,也可能是115200,具体要看模块版本和出厂设置。我建议先用115200尝试,因为这个速率在传输9轴数据(加速度、陀螺仪、角度)时更从容。使用STM32CubeMX配置串口非常方便:
// 使用HAL库配置USART1,115200波特率,8数据位,无校验,1停止位
UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
配置好串口后,别忘了使能接收中断。GY-25的数据帧长度不固定(取决于读取的寄存器数量),所以用中断接收比轮询更可靠。
// 启动串口空闲中断(STM32F4/H7系列)或配合DMA
// 对于F1系列,可以开启接收中断并手动判断帧结束
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
2.2 通信协议深度解析
GY-25使用的是自定义二进制协议,而不是常见的MODBUS或ASCII格式。理解这个协议是成功驱动模块的关键。所有指令帧和数据帧都遵循以下基本结构:
[帧头] [地址] [功能码] [数据长度] [数据域] ... [校验和]
- 帧头:固定为0xA4,就像通信的“握手信号”
- 地址:模块地址,默认0x06,支持多设备时用于区分
- 功能码:0x03表示读寄存器,0x06表示写寄存器
- 数据长度:后续数据域的字节数
- 数据域:具体要读/写的寄存器地址和数据
- 校验和:从帧头到数据域最后一个字节的累加和(只取低8位)
举个例子,我们要读取欧拉角数据(ROLL, PITCH, YAW)和温度。根据手册,这些数据存放在特定的寄存器中:
| 数据 | 寄存器地址 | 字节数 | 说明 |
|---|---|---|---|
| ROLL | 0x14 | 2 | 横滚角,单位0.01度 |
| PITCH | 0x15 | 2 | 俯仰角,单位0.01度 |
| YAW | 0x16 | 2 | 航向角,单位0.01度 |
| 温度 | 0x1A | 2 | 温度值,单位0.01摄氏度 |
读取指令的构建过程如下:假设从0x14寄存器开始,连续读取6个寄存器(因为每个角度占2字节,三个角度共6字节)。那么数据长度就是“起始地址(2字节)+寄存器数量(2字节)”=4字节。所以完整的读指令帧是:
A4 06 03 04 00 14 00 06 XX
最后那个XX是校验和,需要计算:0xA4 + 0x06 + 0x03 + 0x04 + 0x00 + 0x14 + 0x00 + 0x06 = 0x1F5,取低8位就是0xF5。
模块的正确响应帧应该是这样的:
A4 06 03 0C 00 14 00 06 [ROLL_H] [ROLL_L] [PITCH_H] [PITCH_L] [YAW_H] [YAW_L] XX
响应帧的数据长度是0x0C(12字节),因为除了4字节的地址和数量信息,还有6字节的角度数据。校验和需要重新计算验证。
我在实际调试中发现,有时候模块会返回错误码。常见的错误有:
A4 86 02 2C:写入的寄存器地址超出范围A4 86 03 2D:写入的寄存器数据非法A4 83 03 2A:读取的寄存器数量超过总数A4 83 02 29:起始地址+数量超过寄存器范围
遇到这些错误码,首先要检查指令帧的构造是否正确,特别是寄存器地址和数量。
3. 从原始数据到可用欧拉角
收到数据帧后,真正的挑战才开始——如何把这些原始的字节转换成有物理意义的欧拉角。GY-25模块虽然直接输出角度值,但数据的格式、单位、符号约定都需要正确处理。
3.1 数据解析与单位转换
模块返回的角度数据是16位有符号整数,单位是0.01度。这意味着实际角度 = 原始值 / 100.0。例如,原始值2500对应25.00度。
但这里有个容易忽略的细节:数据的字节序。GY-25使用的是大端序(Big-Endian),即高字节在前,低字节在后。而STM32是小端序(Little-Endian)架构。如果你直接使用memcpy或类型强制转换,可能会得到错误的结果。
正确的解析方法应该是:
typedef struct {
int16_t roll; // 横滚角
int16_t pitch; // 俯仰角
int16_t yaw; // 航向角
int16_t temp; // 温度
} GY25_Data_t;
void parse_gy25_data(uint8_t* buffer, GY25_Data_t* data) {
// 手动组合高字节和低字节,注意大端序
data->roll = (buffer[8] << 8) | buffer[9];
data->pitch = (buffer[10] << 8) | buffer[11];
data->yaw = (buffer[12] << 8) | buffer[13];
data->temp = (buffer[14] << 8) | buffer[15];
// 转换为浮点数,单位转换
float roll_deg = data->roll / 100.0f;
float pitch_deg = data->pitch / 100.0f;
float yaw_deg = data->yaw / 100.0f;
float temp_c = data->temp / 100.0f;
}
温度数据的解析方式相同,单位也是0.01摄氏度。实际测试中,我发现模块的温度读数通常比环境温度高3-5度,这是因为传感器本身和内部处理器都会发热。如果对温度精度要求高,可能需要做个简单的校准偏移。
3.2 角度范围与符号约定
欧拉角有三个分量,每个都有特定的范围和方向约定:
- 横滚角(ROLL):±180°,右倾为正,左倾为负
- 俯仰角(PITCH):±180°,前倾(低头)为正,后倾(抬头)为负
- 航向角(YAW):±180°,顺时针旋转为正,逆时针为负
这个坐标系符合航空航天领域的常用约定,但和你直觉中的“正负”可能相反。在实际应用中,特别是做姿态控制时,一定要先明确坐标系定义。
当角度接近±180°边界时,要注意处理跳变问题。比如从179°增加到181°,实际应该是-179°。模块内部已经处理了这种跳变,但你的应用代码在处理角度差值时(比如计算角度变化率)也要考虑这个问题。
// 计算角度差,处理±180°边界
float angle_difference(float current, float previous) {
float diff = current - previous;
// 如果差值大于180°,说明跨过了+180/-180边界
if (diff > 180.0f) {
diff -= 360.0f;
}
// 如果差值小于-180°,反向跨边界
else if (diff < -180.0f) {
diff += 360.0f;
}
return diff;
}
这个函数在计算角速度或做角度积分时特别有用,能避免边界跳变导致的错误。
4. 实战代码:完整的驱动实现
理论讲得再多,不如一段能直接用的代码来得实在。下面是我在实际项目中验证过的GY-25驱动代码,基于STM32 HAL库,包含了初始化、数据读取、解析和错误处理。
4.1 驱动头文件定义
首先定义驱动所需的数据结构和函数接口:
// gy25_driver.h
#ifndef __GY25_DRIVER_H
#define __GY25_DRIVER_H
#include "main.h"
#include <stdbool.h>
// GY-25数据包结构
#pragma pack(push, 1) // 按1字节对齐,避免结构体填充
typedef struct {
uint8_t header; // 0xA4
uint8_t address; // 0x06
uint8_t function; // 0x03
uint8_t length; // 数据长度
uint8_t reg_addr_h; // 寄存器地址高字节
uint8_t reg_addr_l; // 寄存器地址低字节
uint8_t reg_num_h; // 寄存器数量高字节
uint8_t reg_num_l; // 寄存器数量低字节
uint8_t data[16]; // 数据域(最大)
uint8_t checksum; // 校验和
} GY25_RxPacket_t;
#pragma pack(pop)
// 解析后的欧拉角数据
typedef struct {
float roll; // 横滚角,度
float pitch; // 俯仰角,度
float yaw; // 航向角,度
float temperature; // 温度,摄氏度
uint32_t timestamp; // 时间戳,ms
bool data_valid; // 数据有效标志
} EulerAngles_t;
// 驱动状态
typedef enum {
GY25_STATE_IDLE,
GY25_STATE_WAITING_RESPONSE,
GY25_STATE_RECEIVING,
GY25_STATE_ERROR
} GY25_State_t;
// 初始化函数
bool GY25_Init(UART_HandleTypeDef* huart);
// 设置工作模式
bool GY25_SetMode(bool continuous_mode);
// 读取欧拉角数据(查询模式)
bool GY25_ReadEulerAngles(EulerAngles_t* angles);
// 获取最新数据(连续模式)
bool GY25_GetLatestData(EulerAngles_t* angles);
// 串口接收回调(在中断中调用)
void GY25_UART_RxCpltCallback(uint8_t byte);
// 错误处理
const char* GY25_GetErrorString(void);
#endif
4.2 核心驱动实现
驱动实现文件包含了所有底层通信和数据处理逻辑:
// gy25_driver.c
#include "gy25_driver.h"
#include <string.h>
static UART_HandleTypeDef* gy25_huart = NULL;
static GY25_State_t gy25_state = GY25_STATE_IDLE;
static GY25_RxPacket_t rx_packet;
static EulerAngles_t latest_angles;
static uint8_t rx_buffer[32];
static uint8_t rx_index = 0;
static uint32_t last_rx_time = 0;
static char error_string[64] = {0};
// 计算校验和
static uint8_t calculate_checksum(uint8_t* data, uint8_t len) {
uint8_t sum = 0;
for (uint8_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
// 发送指令帧
static bool send_command(uint8_t* cmd, uint8_t len) {
if (HAL_UART_Transmit(gy25_huart, cmd, len, 100) != HAL_OK) {
strcpy(error_string, "UART transmit failed");
return false;
}
return true;
}
bool GY25_Init(UART_HandleTypeDef* huart) {
if (huart == NULL) {
strcpy(error_string, "Invalid UART handle");
return false;
}
gy25_huart = huart;
gy25_state = GY25_STATE_IDLE;
memset(&latest_angles, 0, sizeof(latest_angles));
memset(&rx_packet, 0, sizeof(rx_packet));
// 启动串口接收(单个字节中断模式)
HAL_UART_Receive_IT(gy25_huart, rx_buffer, 1);
// 发送初始化指令:设置波特率115200
uint8_t init_cmd[] = {0xA4, 0x06, 0x06, 0x02, 0x01, 0x06, 0xB1};
if (!send_command(init_cmd, sizeof(init_cmd))) {
return false;
}
HAL_Delay(100); // 等待模块响应
// 设置为连续输出模式
return GY25_SetMode(true);
}
bool GY25_SetMode(bool continuous_mode) {
// 03寄存器:0为连续模式,1为查询模式
uint8_t mode = continuous_mode ? 0x00 : 0x01;
uint8_t cmd[] = {0xA4, 0x06, 0x06, 0x02, 0x03, mode, 0x00};
cmd[6] = calculate_checksum(cmd, 6); // 计算校验和
if (!send_command(cmd, sizeof(cmd))) {
return false;
}
gy25_state = GY25_STATE_WAITING_RESPONSE;
return true;
}
// 串口接收中断回调
void GY25_UART_RxCpltCallback(uint8_t byte) {
static uint8_t expected_length = 0;
uint32_t current_time = HAL_GetTick();
// 超时处理:如果两次接收间隔超过50ms,认为新帧开始
if ((current_time - last_rx_time) > 50) {
rx_index = 0;
gy25_state = GY25_STATE_RECEIVING;
}
last_rx_time = current_time;
// 存储接收到的字节
if (rx_index < sizeof(rx_buffer)) {
rx_buffer[rx_index++] = byte;
}
// 判断帧是否接收完整
if (rx_index >= 5) { // 至少收到前5个字节才能判断长度
if (rx_index == 5) {
// 第4字节是数据长度,加上固定头部5字节和校验和1字节
expected_length = rx_buffer[3] + 6;
}
if (rx_index >= expected_length) {
// 完整帧接收完成
process_received_frame();
rx_index = 0;
gy25_state = GY25_STATE_IDLE;
}
}
// 重新启动接收中断
HAL_UART_Receive_IT(gy25_huart, rx_buffer + rx_index, 1);
}
// 处理接收到的数据帧
static void process_received_frame(void) {
// 验证帧头
if (rx_buffer[0] != 0xA4) {
strcpy(error_string, "Invalid frame header");
gy25_state = GY25_STATE_ERROR;
return;
}
// 验证校验和
uint8_t calc_checksum = calculate_checksum(rx_buffer, rx_index - 1);
if (calc_checksum != rx_buffer[rx_index - 1]) {
sprintf(error_string, "Checksum error: expected 0x%02X, got 0x%02X",
calc_checksum, rx_buffer[rx_index - 1]);
gy25_state = GY25_STATE_ERROR;
return;
}
// 解析欧拉角数据(假设读取的是0x14-0x16寄存器)
if (rx_buffer[1] == 0x06 && rx_buffer[2] == 0x03) {
// 数据从第8字节开始(跳过帧头、地址、功能码、长度、寄存器地址和数量)
if (rx_index >= 17) { // 至少需要17字节(8字节头部+6字节数据+2字节温度+1校验和)
int16_t raw_roll = (rx_buffer[8] << 8) | rx_buffer[9];
int16_t raw_pitch = (rx_buffer[10] << 8) | rx_buffer[11];
int16_t raw_yaw = (rx_buffer[12] << 8) | rx_buffer[13];
int16_t raw_temp = (rx_buffer[14] << 8) | rx_buffer[15];
latest_angles.roll = raw_roll / 100.0f;
latest_angles.pitch = raw_pitch / 100.0f;
latest_angles.yaw = raw_yaw / 100.0f;
latest_angles.temperature = raw_temp / 100.0f;
latest_angles.timestamp = HAL_GetTick();
latest_angles.data_valid = true;
gy25_state = GY25_STATE_IDLE;
}
}
}
bool GY25_GetLatestData(EulerAngles_t* angles) {
if (angles == NULL || !latest_angles.data_valid) {
return false;
}
memcpy(angles, &latest_angles, sizeof(EulerAngles_t));
// 数据有效期检查(超过200ms认为数据过期)
if ((HAL_GetTick() - latest_angles.timestamp) > 200) {
latest_angles.data_valid = false;
return false;
}
return true;
}
const char* GY25_GetErrorString(void) {
return error_string;
}
4.3 主程序示例
最后看一个简单的主程序示例,展示如何集成和使用这个驱动:
// main.c
#include "main.h"
#include "gy25_driver.h"
UART_HandleTypeDef huart1;
EulerAngles_t current_angles;
int main(void) {
HAL_Init();
SystemClock_Config();
// 初始化串口1
MX_USART1_UART_Init();
// 初始化GY-25驱动
if (!GY25_Init(&huart1)) {
printf("GY-25 initialization failed: %s\r\n", GY25_GetErrorString());
while(1); // 初始化失败,停机
}
printf("GY-25 initialized successfully\r\n");
while (1) {
// 获取最新角度数据
if (GY25_GetLatestData(¤t_angles)) {
printf("Roll: %6.2f°, Pitch: %6.2f°, Yaw: %6.2f°, Temp: %5.2f°C\r\n",
current_angles.roll,
current_angles.pitch,
current_angles.yaw,
current_angles.temperature);
}
HAL_Delay(20); // 50Hz更新率
}
}
// 串口接收中断服务函数
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
}
// HAL库回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) {
if (huart->Instance == USART1) {
// 将接收到的字节传递给GY-25驱动处理
GY25_UART_RxCpltCallback(rx_buffer[0]);
}
}
这段代码实现了50Hz的姿态数据输出,完全能满足大多数实时控制应用的需求。在实际的四轴飞行器项目中,我就是用这个驱动获取姿态角,然后输入到PID控制器中。
调试时发现,如果模块放置的表面不够平整,或者有振动,PITCH和ROLL角会有小幅度的波动(±0.2°以内)。这是正常现象,因为传感器本身有噪声。在应用层可以做些简单的滤波,比如一阶低通滤波:
// 简单的一阶低通滤波
float low_pass_filter(float new_value, float old_value, float alpha) {
return alpha * new_value + (1.0f - alpha) * old_value;
}
// 使用示例
current_angles.roll = low_pass_filter(raw_roll, current_angles.roll, 0.2f);
alpha值取0.1到0.3之间,能在响应速度和平滑度之间取得不错的平衡。
最后提一个实际遇到的坑:有次代码跑得好好的,突然角度数据全乱了。排查了半天,发现是电源问题——电机启动时导致3.3V电源有个瞬间跌落。后来在GY-25的VCC引脚上加了个100uF的电解电容,问题就解决了。所以,稳定的电源对传感器精度真的很重要。
更多推荐


所有评论(0)