☰
JY901姿态传感器串口数据解析与配置避坑指南
2026/11/3 6:53:34 网站建设 项目流程

简介:《JY901官方使用说明文档》是一份面向嵌入式开发者、机器人及无人机姿态传感应用人员的模块手册,可用于JY901高精度惯性导航模块的选型、接线、调试与校准。压缩包仅含1个PDF文件,大小约1.35MB,内容以产品说明、参数查阅和操作指导为主。文档围绕姿态解算、加速度、角速度、磁场与角度输出展开,并覆盖JY901B的气压、高度及GPS-IMU组合导航能力。读者可查阅电压范围、量程、分辨率、输出频率、引脚与轴向定义,也能了解串口和IIC连接方法,以及上位机调试、恢复出厂设置、陀螺与加计校准、磁场校准和高度置零等流程,并获取数据记录与三维姿态显示等软件操作说明,便于与Arduino、51、STM32等单片机快速联调。已有3552人学习,适合需要核对官方参数、排查连接问题或建立校准规范的工程人员参考。

1. 拿到 JY901 的第一小时:从上位机波形到能进代码的六个数

一块 15.24mm 见方的板子焊上排针,USB-TTL 一插,上位机里三条曲线跳起来,Roll/Pitch/Yaw 滚得很欢,很多人到这一步就以为成了。真把姿态传感器 JY901 的六个数搬进自己的固件,才发现上位机是个黑匣子——找帧头、验校验、拼 short、乘量纲这些活它全替你做了,一行都不在你手里。

这份官方说明文档把 JY901 的串口帧定义、寄存器地址表、校准流程、扩展口模式和 GPS 接入方式全摊开了。它不深,但足够完整,完整到你可以彻底丢掉上位机,只用串口助手加几十行代码把模块跑起来。适合手上已经有模块、要往 STM32 或 Arduino 上接的工程师,也适合想弄明白姿态角到底怎么从一堆字节里算出来的新手。

2. 拆 0x55 数据帧:11 字节定长包怎么变成 Roll/Pitch/Yaw

2.1 帧结构先钉死:帧头、类型、载荷、校验

JY901 往上位机发的每一包都是定长 11 字节,结构非常规整:0x55(帧头)+ 类型字节 + 8 字节载荷 + 1 字节校验和。类型字节决定这 8 个字节怎么解释,常见的有下面这些。

类型含义8 字节载荷备注
0x50时间YY MM DD hh mm ss msL msH默认从 2015-01-01 00:00:00.0 起算
0x51加速度Ax Ay Az 各 2 字节 + TL TH单位 g,量程 ±16g
0x52角速度wx wy wz 各 2 字节 + TL TH单位 °/s,量程 ±2000
0x53角度Roll Pitch Yaw 各 2 字节 + TL TH单位 °,范围 ±180
0x54磁场Hx Hy Hz 各 2 字节 + TL TH原始计数值
0x55端口状态D0 D1 D2 D3 各 2 字节含义随端口模式变化
0x56气压高度P0..P3 + H0..H3仅 901B,32 位小端
0x57经纬度Lon0..3 + Lat0..3需接 GPS
0x58GPS 高度/航向/地速各字段见协议表需接 GPS
0x59四元数Q0..Q3 各 2 字节见下面的注意

注意:文档 7.1.10 把四元数标成 0x59,7.1.11 的卫星定位精度也标成 0x59,两处撞车。实际用的时候别指望靠类型字节同时区分两者,需要哪个就只开哪个,或者干脆自己实测抓一包看长度和数值范围。

校验和是前 10 个字节的累加和取低 8 位。这里有个血泪经验:文档里几处校验和公式把类型字节写错了——0x50 时间包写成0x55+0x51+...,0x54 磁场包写成0x55+0x53+...,地速包写成0x55+0x54+...。照着抄就永远对不上。正确写法只有一条:帧头 + 类型 + 8 字节载荷,全部累加,取低 8 位。文档正文的公式是笔误,帧结构本身没问题。

载荷里的数值一律是小端、有符号 short。低字节在前高字节在后,拼的时候先做强转再移位,这个顺序反了就会看到"静止时角速度 300°/s"这种玄学现象。

2.2 状态机解析:为什么要自己写,不能用 readline

串口读上来的是一条没有边界的字节流,随时可能半包、粘包、丢字节。用readline或者按固定长度read(11)都会翻车,因为一旦某次错位一个字节,后面全部是垃圾数据。可靠做法是逐字节喂进一个状态机,校验失败只丢一个字节重新同步。

# jy901_parser.py 逐字节状态机,兼容半包/粘包/中途丢字节 import struct PKT_LEN = 11 TYPES = {0x50: "time", 0x51: "acc", 0x52: "gyro", 0x53: "angle", 0x54: "mag", 0x55: "port", 0x56: "press", 0x57: "lonlat", 0x58: "gps", 0x59: "quat"} def _sum10(frame): # 前 10 字节累加取低 8 位,就是文档里的 SUM return sum(frame[:10]) & 0xFF def feed(buf, byte): """buf 是 bytearray 缓冲区,每次喂一个字节,返回完整帧或 None""" buf.append(byte) while True: if not buf: return None if buf[0] != 0x55: # 不是帧头,直接丢 buf.pop(0) continue if len(buf) < PKT_LEN: return None # 数据不够,等下一个字节 frame = buf[:PKT_LEN] if _sum10(frame) != frame[10]: buf.pop(0) # 只丢一个字节,保住可能存在的真帧头 continue del buf[:PKT_LEN] return bytes(frame) def decode(frame): t = frame[1] if t == 0x53: # 角度包 roll = struct.unpack('<h', frame[2:4])[0] / 32768 * 180 pitch = struct.unpack('<h', frame[4:6])[0] / 32768 * 180 yaw = struct.unpack('<h', frame[6:8])[0] / 32768 * 180 temp = struct.unpack('<h', frame[8:10])[0] / 100 return {"type": "angle", "roll": roll, "pitch": pitch, "yaw": yaw, "temp": temp} if t == 0x51: # 加速度包,单位 g ax, ay, az, tt = struct.unpack('<hhhh', frame[2:10]) return {"type": "acc", "ax": ax / 32768 * 16, "ay": ay / 32768 * 16, "az": az / 32768 * 16, "temp": tt / 100} return {"type": TYPES.get(t, hex(t)), "raw": frame}

feed里最关键的一行是校验失败后的buf.pop(0),而不是del buf[:11]。因为校验失败往往意味着当前位置不是真帧头,只丢一个字节,下一个 0x55 才有机会被重新识别。这个细节决定了模块偶尔丢一帧时,你是能一秒内恢复,还是直接卡死到要重启。

decode里用struct.unpack('<h', ...)比手写(H<<8)|L更省事,'<h'本身就表示小端有符号 16 位,负数自动处理。C 语言那边对应的是(int16_t)((int16_t)DataH << 8 | DataL),注意DataH必须先转成有符号再移位,否则 -1 会变成 65280 这种大正数,这是新手最常见的符号坑。

2.3 量纲换算与轴向:别被文档里的两张图绕晕

换算系数是固定的:加速度/32768*16(g)、角速度/32768*2000(°/s)、角度/32768*180(°)、温度/100(℃)、四元数/32768。磁场只给原始计数值,没给换算系数,做航向融合时直接用原值参与运算即可,需要绝对强度就自己拿已知场强标一次。

轴向这块文档自相矛盾:第 4 章"轴向说明"写的是"向右为 X 轴,向上为 Y 轴,垂直纸面向外为 Z 轴",7.1.4 的注解里却写"向左为 X 轴,向前为 Y 轴,向上为 Z 轴",两套定义对不上。我的做法是不背文档,上电后拿模块单独绕某个物理轴慢慢转 90°,看哪个角在变、往哪个方向变,三次就能把实际轴向摸清楚,然后把映射写进代码里,后面所有逻辑都基于实测的映射。

欧拉角本身也有固有缺陷,文档里提醒得很到位:旋转顺序是 Z-Y-X,俯仰角实际范围只有 ±90°,超过之后会翻到小于 90° 并让 X 轴角度超过 180°。更麻烦的是三轴耦合,X 轴接近 90° 时,哪怕只绕 X 转,Y 轴也会大幅变化。所以云台、机械臂这类大角度场景,别拿欧拉角做插值和闭环,直接用 0x59 的四元数,或者自己在 MCU 里存四元数再转出来。

3. 写寄存器而不是点鼠标:校准、回传内容与速率的可复现配置流程

3.1 校准三件套:每一步在干掉什么误差

模块出厂时每个传感器的零偏都不一样,不校准直接用,静止时陀螺仪输出可能是 2°/s 而不是 0,积分几十秒姿态就飘到天边。文档给了三项校准,各自解决不同的问题。

陀螺/加计校准去的是零偏。操作要求模块保持水平静止,点上位机的"陀螺、加计校准",上位机会自己算出 AxOffset/AyOffset/AzOffset/GxOffset/GyOffset/GzOffset 六个值,等它们稳定下来再点"正常模式"退出校准,然后必须点保存配置。校准完,静止时陀螺仪输出会回到 0°/s 附近。

磁场校准去的是磁传感器的零点误差,直接影响航向角。做法是先把模块放到远离铁磁物体和大电流线的地方,进磁场校准模式,绕 X 轴转 360°、绕 Y 轴转 360°、绕 Z 轴转 360°,再随意翻转几圈,等 HxOffset/HyOffset/HzOffset 不再变化,退出并保存。这一步的环境比操作手法更重要——放在铁桌子上、旁边有电机或音箱,转一百圈也是白转。

高度置零只对带气压传感器的 JY901B 有效,把当前气压当作零高度基准。这个值模块不保存,掉电就没了,所以每次上电要重新做一次,别指望存进 FLASH。

提示:校准值和配置都存在模块内部 FLASH 里,掉电保持的前提是点了保存配置。没点保存,断电后一切归零,然后你会怀疑模块坏了。另外零偏会随温度漂移,做高精度倾角测量的场景,冷机热机各校一次,或者干脆在固件里做上电自动校准。

3.2 用代码发配置帧:0xFF 0xAA 开头的那 5 个字节

上位机能做的所有事,本质都是往模块发一条 5 字节的下行帧:0xFF 0xAA 地址 数据低字节 数据高字节。把这套摸清楚,产线上就可以用脚本批量配置,不用一个个点鼠标。

地址名称作用是否需重新上电
0x00SAVE保存当前配置 / 恢复出厂否
0x01CALSW1 陀螺加计校准 2 磁场校准 3 高度置零 0 退出否
0x02RSW回传内容位掩码否
0x03RATE回传速率档位否
0x04BAUD串口波特率档位是
0x05~0x0d各轴零偏加速度/角速度/磁场零偏否
0x0e~0x11D0~D3MODE扩展口模式否
0x12~0x19PWM 宽度/周期单位 us否
0x1aIICADDRIIC 地址是
0x1bLEDOFF关闭 LED 指示否
0x1cGPSBAUDGPS 连接波特率是
import serial, time def cfg(ser, addr, value): """下行帧固定 5 字节:0xFF 0xAA 地址 数据L 数据H""" ser.write(bytes([0xFF, 0xAA, addr, value & 0xFF, (value >> 8) & 0xFF])) time.sleep(0.05) # 模块处理需要时间,连续发要留间隔 def setup(ser): cfg(ser, 0x02, 0x0F) # RSW=0x0F:开时间/加速度/角速度/角度四包 cfg(ser, 0x03, 0x09) # RATE=0x09 对应 100Hz,0x06 是默认 10Hz cfg(ser, 0x1b, 0x01) # 关掉 LED,装在壳子里看不见也不耗电 cfg(ser, 0x00, 0x00) # SAVE 写 0:保存当前配置到 FLASH time.sleep(0.1) def calibrate(ser): cfg(ser, 0x01, 0x01) # 进陀螺/加计校准,此时模块必须水平静止 time.sleep(3) # 等零偏收敛,具体时长看 offset 是否稳定 cfg(ser, 0x01, 0x00) # 退出校准,回到正常模式 cfg(ser, 0x00, 0x00) # 保存,不保存等于白校 if __name__ == "__main__": ser = serial.Serial("COM5", 9600, timeout=0.05) setup(ser) calibrate(ser) ser.close()

cfg函数把 16 位数值拆成低高两个字节,写零偏、PWM 宽度这类超过 255 的量时必须这么拆。SAVE这个寄存器文档写得含糊——"0 保持当前配置,1 恢复默认并保存",实际使用中写0x00就是保存当前配置,写0x01会恢复出厂并把出厂值存进去。产线脚本里两个都要用:先写 1 复位到干净状态,再配置,最后写 0 保存。

3.3 回传内容与速率的带宽账:设了 200Hz 却没跑满的原因

RSW是一个位掩码,bit0 到 bit7 分别对应 0x50 到 0x57 这八个包,默认值是 0x1E,也就是时间外的加速度、角速度、角度、磁场四包全开。想省带宽就只留角度包,写 0x08。

速率档位从 0x01 到 0x0c 依次是 0.1Hz、0.5Hz、1Hz、2Hz、5Hz、10Hz、20Hz、50Hz、100Hz、200Hz、单次、不输出。这里最容易被忽略的一点:串口带宽是硬上限。每个包 11 字节,算上起始位和停止位,一个字节实际占 10 个 bit。

开启包数每帧字节10Hz 需要100Hz 需要200Hz 需要
1111100 bps11000 bps22000 bps
4444400 bps44000 bps88000 bps
8888800 bps88000 bps176000 bps

按这张表看就明白了:默认 9600 波特率下开 4 个包,理论极限只有 21Hz 左右,你写 200Hz 它也传不动,模块会自动降频到它能承受的最大值,而且不报错。想跑满 200Hz,要么把波特率提到 115200 以上,要么把包数砍到一个。改波特率还有个坑:改完不会立刻生效,必须重新上电。

4. 串口还是 IIC:接线、电平与四路扩展口的选型账

4.1 串口接线:电平边界比接线顺序更容易出事

模块的引脚就十个:VCC、RX、TX、GND、SCL、SDA、D0 到 D3。串口接 USB-TTL 模块时,+5V→VCC、TXD→RX、RXD→TX、GND→GND,交叉这一步人人都会背,真正出问题的是电平。模块的 IO 是 TTL 电平,兼容 3.3V 和 5V 系统,但它不是 RS232 电平。把模块接到 RS232 的 DB9 口上,±12V 的电平灌进 3.3V 的 IO,模块当场报废,没有抢救余地。

供电范围 3V 到 6V,电流小于 40mA,板载 LDO 会自己稳定。这意味着你可以直接拿一片 3.3V 的 STM32 板给它供电,也可以接 5V,两种情况都不用加电平转换。有些多功能 USB-TTL 模块带电平选择拨码,接模块前确认拨在 TTL 一侧,这一步我见过太多人漏掉。

注意:模块是邮票孔封装,焊到自己的 PCB 上之后想飞线调试非常难受。建议第一版板子上留出测试点或者排针,尤其是 RX、TX、GND 三根,方便随时接 USB-TTL 看数据。

4.2 IIC 接口:上拉电阻和地址两个硬约束

IIC 走的是 SCL 和 SDA,最高 400K 全速。模块的 IIC 是开漏输出,所以总线上必须有一个上拉到 VCC 的电阻,典型值 4.7K。很多人把模块挂到 STM32 的硬件 IIC 上发现读不到数据,八成是因为自己板子上的内部上拉太弱,或者压根没上拉。

默认地址 0x50,可以通过写寄存器 0x1a 改成别的值,改完同样需要重新上电。一条总线上挂多个模块时,改地址就是唯一手段,改完记得记下谁是谁,不然调起来很痛苦。上拉电阻整条总线只留一组,挂三个模块各带一个 4.7K,等效下来只有 1.5K,容易把总线拉坏。

IIC 适合什么场景?适合低频取数,比如每秒读一次角度寄存器 0x3d 到 0x3f。它的优势是省一个串口,接线短,缺点是每次读数都要走"起始-地址-寄存器-重启-读-停止"这一长串时序,想稳定拿 200Hz 基本不现实。高频还是老老实实走串口。

4.3 四路扩展口:模拟输入、数字量和 PWM 怎么用

D0 到 D3 四路端口可以分别配置成不同功能,模式值写在寄存器 0x0e 到 0x11。D1 多一个 GPSRX 模式。

模式值功能DxStatus 的含义
0x00模拟输入(默认)12 位 ADC 计数值
0x01数字输入1 高电平,0 低电平
0x02输出高电平固定为 1
0x03输出低电平固定为 0
0x04PWM 输出高电平宽度,单位 us
0x05GPSRX(仅 D1)无意义

模拟输入的换算公式是U = DxStatus / 1024 * Uvcc。这里的Uvcc不是你的供电电压,要看 LDO:供电大于 3.5V 时Uvcc固定 3.3V,供电小于 3.5V 时Uvcc = 供电电压 - 0.2V。拿 5V 供电去测电位器,不按 3.3V 算的话,读数会偏差三分之一。

数字输入那段文档写错了:"如果是高电平,DxStatus=1,如果是低电平,DxStatus=1",两句都是 1。显然是笔误,低电平应该是 0。照文档写判断逻辑会永远进高电平分支。

PWM 输出是四路端口里最实用的玩法。高电平宽度寄存器在 0x12 到 0x15(D0 到 D3),周期寄存器在 0x16 到 0x19,单位都是 us。控制一个标准舵机就是宽度 1500us、周期 20000us,写进去模块直接输出 50Hz 的舵机信号,省掉一颗 MCU 的定时器。

# D1 配成 PWM 输出,驱动一个标准舵机到中位 cfg(ser, 0x0f, 0x04) # D1MODE = 0x04,PWM 输出 cfg(ser, 0x13, 1500) # D1PWMH = 1500us,高电平宽度(中位) cfg(ser, 0x17, 20000) # D1PWMT = 20000us,周期 50Hz cfg(ser, 0x00, 0x00) # 保存

注意寄存器 0x17 是高字节在前还是低字节在前这件事在文档里没写全,cfg里按低字节在前的约定发就行,实测不对就把两个字节对调试一次。PWM 的周期范围 1us 到 65535us,分辨率 1us,做 LED 调光、蜂鸣器、简单电机调速都够用。

5. 避坑清单:五个现场翻车记录与排查顺序

5.1 打开串口一片乱码或者干脆没数据

现象:上位机连上以后要么全是问号和方块,要么接收区一片空白。原因通常是两个,一是波特率不对,模块出厂默认 9600,但上一任使用者可能改过;二是 TX/RX 没交叉,或者 USB-TTL 模块的地线没接。

解决顺序是这样:先确认 GND 真的连上了,这一步最容易被忽略;然后点上位机的"波特率 → Auto"自动搜索,但自动搜索有个前提——模块的回传速率必须大于 5Hz,如果你之前把速率设成了 1Hz,Auto 会一直搜不到。这时候用短路法恢复出厂:断电状态下把 D2 引脚和 VCC 短接,再上电,LED 长亮约 2 秒后熄灭,出厂设置恢复完成,之后用 9600 连就行。

5.2 校验和永远对不上,偶尔还有跳变数据

现象:代码里校验失败率高得离谱,偶尔能过一帧但数值明显不对。原因大概率是照着文档的校验和公式抄的,而文档里 0x50、0x54、0x58 几个包的公式都把类型字节写错了。

解决:统一写成"前 10 字节累加取低 8 位",不要看公式里写的具体字节。如果校验通过但数值跳变,检查是不是把SUM当成了帧尾在找,或者解包时把低高字节顺序搞反了。还有一种情况是串口读得太慢,接收缓冲区溢出丢字节,把timeout调小、读循环里一次多读几个字节能缓解。

5.3 改了波特率、IIC 地址、回传速率,重启又回到原样

现象:上位机里改完参数当场有效,模块一断电再上电,全部恢复默认。原因分两种:一是没点保存配置,参数还在 RAM 里没写进 FLASH;二是改的是波特率、IIC 地址、GPS 波特率这三个必须重新上电才生效的项,改完没断电,你以为没生效,其实重启后生效了但你又没保存。

解决:养成固定顺序——改参数、点保存配置、断电重启、重新连接验证。用脚本配置的话,cfg(0x00, 0x00)这一步千万别省,而且要在所有参数写完后再发。

5.4 静止时航向角缓慢漂移,转一圈回来对不上原点

现象:模块平放在桌上不动,Yaw 值几分钟内缓慢变化,或者手动转 360° 回到原位,读数差了十几度。原因几乎都是磁场没校准,或者校准环境不干净。

解决:先把模块拿到远离电机、音箱、铁质桌腿、大电流线的地方,重新做一次磁场校准,三轴各转 360° 之后再随意翻转几圈,等三个 offset 稳定、退出、保存。校准完还是漂,检查模块附近有没有磁铁、螺丝刀、手机,这些都会毁掉航向角。另外磁场校准对温度敏感,冬天在室外校完拿到室内,可能要重来一次。

5.5 设了 200Hz,实测只有 20Hz 左右

现象:上位机里速率改成 200Hz,实际数波形一秒只有二十来帧。原因是串口带宽不够,模块自动降频了,而且不报任何错。

解决:按第 3 章那张带宽表算账。9600 波特率下开四个包,理论极限就是 21Hz。要么把回传内容砍到只留角度包,要么把波特率提到 115200 甚至 460800。改波特率之后记得断电重启,再重新连接。

6. 验收与组合导航:静止方差、已知转角与 GPS 接入

配置跑通、数据能解出来,只能说明链路通了,不代表精度达标。我一般用两个动作做验收,成本很低,但能立刻暴露校准问题。

第一个是静止方差。模块放在平整的桌面上,什么都别碰,采 60 秒,算三轴角速度的均值和标准差。均值就是残余零偏,校准做得好应该在 0.5°/s 以内,超过 1°/s 我会回去重新校准一次;标准差反映噪声水平,突然变大基本是附近有振动源或者供电不稳。姿态角这边看峰峰值,静止时如果 Roll/Pitch 抖动超过 0.5°,说明滤波没跑稳或者模块没放平。

第二个是已知转角。找一个直角块或者带刻度的转台,把模块贴上去,从一个已知角度转到另一个已知角度,看 Yaw 的变化量对不对。转 90° 如果读出来 87°,误差 3°,那基本是磁场校准没做好;如果误差是无规律的乱跳,检查周围干扰。这一步比看静止数据有用得多,因为动态误差和静态误差往往是两回事。

# 静止 60 秒验收:残余零偏 + 噪声水平 import serial, time, statistics from jy901_parser import feed, decode ser = serial.Serial("COM5", 115200, timeout=0.02) buf, wx = bytearray(), [] t0 = time.time() while time.time() - t0 < 60: for b in ser.read(64): f = feed(buf, b) if f and f[1] == 0x52: # 只取角速度包 wx.append(decode(f).get("wx", 0)) print("样本数", len(wx)) print("残余零偏 %.3f °/s" % statistics.mean(wx)) print("噪声标准差 %.3f °/s" % statistics.pstdev(wx))

接 GPS 的话,先把 GPS 模块的 TX 接到 JY901 的 D1 引脚,把 D1 模式改成 0x05(GPSRX),再把 GPS 波特率寄存器 0x1c 设成和 GPS 模块输出一致的值,默认 9600。然后在 RSW 里打开 0x57(经纬度)和 0x58(GPS 高度、航向、地速)两个包。时间包 0x50 会自动用 GPS 时间填充,但要注意那是 UTC,比北京时间晚 8 小时,直接显示会差一天里的 8 个钟头。

经纬度换算这里文档又埋了个坑。格式是 NMEA-0183 的ddmm.mmmmm去掉了小数点,所以度数是Lon / 10000000,分是(Lon % 10000000) / 100000。文档里度数那一行写的是除以 100000000,多了一个零,照着写会得到 11.6 度而不是 116 度。第一次接 GPS 的时候,务必拿一个已知坐标点对一次,一分钟就能确认换算对不对。

从那以后我每次焊完新板子上电,第一件事是用 9600 波特率把模块恢复到出厂,再按项目需要的包和速率重配一遍,最后才动解析代码——顺序反了,你会在"到底是硬件问题还是我代码问题"里耗掉一整晚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询