北斗三号精简版协议实战:串口帧解析、异或校验与RDSS语句族 简介《北斗三号协议精简版》面向卫星导航通信协议学习、北斗三号终端联调与二次开发人员聚焦终端与外设之间数据交换的传输格式和接口规范可用于快速了解系统通信流程、语句组织方式及调试要点。资源包共1个PDF文件约405KB篇幅精简便于在项目现场或学习过程中随时查阅。内容围绕接口要求、数据传输和RDSS语句展开硬件部分涉及互连线、连接器及收发器连接数据传输涵盖串口规范、标准语句与语句样式并附有修订记录语句样式部分不仅包含EPQ定位申请、EPI定位信息、FKI反馈信息还涉及ICR查询用户卡信息、ICP输出用户卡信息、TCQ通讯申请、TCI通讯信息与PWI功率查询等常用指令。已有2241人学习适合作为协议速查与字段对照材料帮助读者理解定位、通信、查询类语句的结构与交互逻辑为终端调试、协议解析和二次开发提供参考。1. 从一串$CCEPQ说起这份精简版协议到底解决什么问题调试北斗三号终端最费时间的往往不是定位算法而是串口那头回来的$BDEPI,...里哪一位是高程、哪一位是精度指示。这份《北斗三号协议精简版》把接口要求、标准语句格式和 RDSS 语句族压缩到十几页核心就三件事4800115200 的串口怎么配、$...*hh的帧怎么校验、EPQ/EPI/FKI/TCQ/TCI/PWI 这些语句的字段怎么填。它适合已经拿到模块、需要把上位机或主控固件对接起来的开发者也适合做北斗协议教学与仿真测试的团队当基线文档。整篇按“帧结构→校验→语句族→排错→自检”的顺序落地每一步都能对着串口助手复现。2. 串口与帧结构115200 波特率下怎么把一条语句切干净2.1 波特率与 8N1默认值只是起点不是答案协议把串口参数定得很死异步串行、起始位 1 位、数据位 8 bit、停止位 1 bit、无校验、最低有效位优先波特率范围 4800115200 bps默认 115200。这套参数背后是典型的成本取舍——无校验位意味着串口本身不提供任何差错检测所有可靠性都压在语句内部的异或校验和上。所以帧解析里校验和不是可选项是唯一的兜底。默认 115200 不等于你手上的模块就是 115200。同一批模块出厂固件可能停在 9600 或 4800尤其是老型号或者被测设备被上一任使用者改过配置。拿到模块先做一次波特率探测比对着文档怀疑人生要快得多。探测思路很简单逐个波特率打开串口主动发一条查询语句看有没有$开头的可打印 ASCII 回来。import serial import time def probe_baud(port, bauds(115200, 9600, 4800, 38400, 57600)): 逐个波特率试探返回第一个能收到 $ 开头回帧的波特率 for baud in bauds: try: with serial.Serial(port, baud, bytesize8, parityN, stopbits1, timeout0.5) as ser: time.sleep(0.2) ser.reset_input_buffer() # 查询本机 IC 卡信息校验和按异或算出来是 0x68 ser.write(b$CCICR,0,00*68\r\n) data ser.read(256) if b$ in data: print(fhit {baud}: {data[:80]!r}) return baud except serial.SerialException as exc: print(f{baud} open failed: {exc}) return None代码里bytesize8, parityN, stopbits1就是协议要求的 8N1timeout0.5是读超时探测阶段不要设太长否则五个波特率轮一遍要好几秒。reset_input_buffer()的作用是丢掉打开串口瞬间的残留字节避免把上一轮的脏数据当成命中。主动发 ICR 而不是被动等是因为很多终端上电后不会主动吐语句等下去只会超时。硬件层还有一条容易被忽略终端可以通过一根线缆并联多个接收器但挂几个接收器取决于发送器驱动能力、终端输入驱动要求和有没有加终端电阻。互连线常见做法是屏蔽双绞线外加一根共地保护线长距离或高波特率下不加终端电阻波形振铃会把本来正确的帧边沿打成乱码。这类问题在串口助手里表现为“偶尔丢几个字节”换线换电阻就好了别一上来就怀疑协议解析写错。2.2 五个定界符一条语句的生死全在这几个字节上标准语句的骨架是固定的$5 位类型标识,数据字段,数据字段,...,数据字段*校验和CRLF$是起始符*是校验和定界符CR0x0D和LF0x0A是终止符逗号是字段分隔符。类型标识固定 5 位英文大写字符这是硬约束解析时不要用“读到逗号为止”去猜长度。字符十六进制作用$0x24语句开始标志,0x2C字段分隔符*0x2A数据与校验和的分界CR0x0D回车语句定界LF0x0A换行协议明确列了一组保留字符0x0D、0x0A、0x24、0x2A、0x2C、0x5C、0x5E、0x7E、0x7F。数据字段里只允许出现除这些之外的可打印 ASCII。这条规则的价值在于只要发送方守规矩接收方按逗号切分就是安全的不需要转义逻辑。反过来说如果你在业务数据里塞了逗号或换行帧边界立刻失去意义后面的字段全部错位。字段定位只能靠数逗号不能靠数字符总数。协议写得很直白数据字段的位置由分隔符计数确定不应从语句开头数字符个数。原因是字段长度可变比如 EPQ 里的高程值就是变长字段正负号还占一位。用固定偏移去切今天能跑明天换个收信方 ID 长度就翻车。类型标识的前两位有个不算秘密的规律CC开头的多为输入语句用户发给终端BD开头的多为输出语句终端上报给用户。EPQ 是CCEPQEPI 是BDEPITCQ 是CCTCQTCI 是BDTCIPWI 是BDPWI。按这个规律做分发比写一长串 if-else 清晰得多。2.3 收发骨架先把字节流切成帧再谈解析串口读回来的是连续字节流不是一条条消息。第一件事是按\r\n切帧第二件事才是校验和字段解析。下面这段骨架可以直接放进工程里改import serial def iter_sentences(ser: serial.Serial): 从串口字节流中逐条产出以 $ 开头的语句不含 CRLF buf b while True: chunk ser.read(ser.in_waiting or 1) if not chunk: continue buf chunk while b\r\n in buf: line, buf buf.split(b\r\n, 1) line line.strip() if not line.startswith(b$): continue # 丢弃上电噪声、半截帧 if b* not in line: continue # 没有校验和字段直接判废 yield line.decode(ascii, errorsreplace)ser.in_waiting or 1的意思是缓冲区里有数据就一次性读空没有就阻塞读 1 字节避免忙等把 CPU 打满。split(b\r\n, 1)只切第一个换行剩下的留在buf里等下一轮这样半截帧不会被误判。line.strip()去掉可能粘在头尾的空白但注意不要用strip()去删$或*这两个字符有语义。这段骨架只负责“切”不负责“认”。真正落地时建议再加一层按类型标识做白名单不在CCEPQ/CCICR/CCTCQ/BDEPI/BDFKI/BDICP/BDTCI/BDPWI里的帧直接丢弃并计数。现场干扰大的时候白名单能把大部分噪声挡在解析层外面日志也干净。3. 校验和与编解码异或、BCD 和汉字内码的三个实现细节3.1 异或校验区间错一个字节后面全白算校验和的定义很明确$和*之间不含这两个字符的全部字符按字节异或结果的高 4 bit 和低 4 bit 分别转成十六进制 ASCII高位在前也就是两位大写十六进制字符。注意三个关键词按字节、不含定界符、大写。def checksum(body: str) - str: body 为 $ 与 * 之间的内容例如 CCICR,0,00 value 0 for byte in body.encode(ascii): value ^ byte return f{value:02X} # 两位大写十六进制高位在前encode(ascii)保证按单字节异或不会因为 Python 字符串是 Unicode 而算错。02X里的X输出大写02保证个位数补零——校验和是0A而不是A少一位接收方就切不准。拿协议里的例子验一下$CCICR,0,00*7B。按定义取CCICR,0,00逐字节异或结果是 0x68对应字符68不是7B。也就是说文档给的这条示例校验和对不上属于笔误。这种地方最坑人照着示例抄发出去没反应还以为是串口接反了。实际做法是以设备回帧为准自己算一遍再发如果模块不响应把68和7B各试一次能回的那条就是它对校验的实现。提示校验和覆盖范围从类型标识的第一个字符开始不是从逗号开始也不是从$开始。把$或*算进去是最常见的翻车方式现象是模块完全不回 FKI。3.2 BCD、汉字内码与编码类别三种载荷三套处理TCQ 和 TCI 里有个“编码类别”字段取值 15直接决定后面“通信数据”怎么编解码。这是整份协议里最容易写出兼容性 bug 的地方。类别 1 是汉字传输内容为计算机内码每个汉字 16 bit高位在前。协议规范性引用里挂了 GB 2312-80所以常规做法是按 GB2312 编码后转十六进制字符串def hanzi_payload(text: str) - str: 编码类别 1汉字转 16 bit 内码每字节两位十六进制 raw text.encode(gb2312) if len(raw) % 2 ! 0: raise ValueError(GB2312 编码结果应为偶数字节) return raw.hex().upper()gb2312是协议引用的字符集但现场模块不一定严格实现有的按 GBK 走有的直接按 Unicode 小端塞。差别在生僻字和繁体字上会暴露——同一个字一边编出来两个字节另一边编出来四个字节。稳妥做法是先发一条只含常用字的短报文用 TCI 回读比对十六进制确认字节序和字符集之后再上业务数据。类别 2 是代码传输内容为 ASCII 字符。协议给了一个容易看错的例子代码 8 用字符8表示也就是 0x38不是数值 0x08。这类字段在组帧时按字符串拼接即可别手贱做数值转换。类别 3 是混编传输内容为汉字与代码混合输出的 BCD 码起始位为A4。这个A4是分段标记解析时不能当成普通数据。精简版没有给出完整的分段语法实际处理建议是先按A4切段段内再按类别 1/2 的规则分别解遇到不认识的分段标记就整条落日志、不猜。BCD 码还有两个边界搜救业务数据最长 117 个 BCD 码状态数据最长 159 个 BCD 码不足时低位补零。补零方向搞反接收端解出来的长度字段会变成一个巨大的数然后解析器越界。3.3 组帧与回读发出去还要能解回来只写组帧不写解析等于只做了一半。下面这组函数把“拼帧”和“验帧”凑成闭环调试时可以直接对着串口助手做回归def build(sentence_type: str, *fields) - bytes: 按标准语句格式组帧None 输出为空字段 if len(sentence_type) ! 5 or not sentence_type.isupper(): raise ValueError(类型标识必须是 5 位大写字母) body ,.join([sentence_type] [ if f is None else str(f) for f in fields]) return f${body}*{checksum(body)}\r\n.encode(ascii) def parse(line: str) - dict: 校验并拆字段字段内不允许出现逗号 line line.strip() if not line.startswith($) or * not in line: raise ValueError(帧边界不完整) body, _, cs line[1:].partition(*) expect checksum(body) if expect ! cs.upper(): raise ValueError(fchecksum mismatch: expect {expect}, got {cs}) parts body.split(,) return {type: parts[0], fields: parts[1:]}build里对类型标识做了长度和大写校验这一条能挡住大部分手写拼帧的低级错误。空字段用空字符串占位而不是省略因为逗号计数决定字段位置少一个逗号后面全错。parse先校验再切分校验不过直接抛异常不要“尽力解析”——错误的帧解析出来的数据比没有数据更危险。一个最小回归用例build(CCICR, 0, 00)应该得到$CCICR,0,00*68\r\n。把它丢进parse应该拿回类型CCICR和两个字段[0, 00]。这条用例跑通说明校验和、字段拼接、帧边界三件事都对上了。4. RDSS 语句族拆解EPQ、EPI、FKI 的字段怎么填4.1 EPQ 定位申请模式、频点、紧急标识的合法组合EPQ 是输入语句用户主动发定位报告或应急搜救申请。它一共 11 个字段字段顺序和取值范围如下序号含义取值关键备注1收信方 ID按卡分配定位报告接收方2定位报告模式151 无测高、2 有测高、3 广义 RDSS、4 普通位置报告、5 自适应3入站频点050 自适应15 对应 Lf0Lf44入站速率14四档速率5紧急标识131 应急搜救、2 标准定位、3 精密定位6高程值变长可正可负模式 1、4、5 下通常置空7搜救类型131 呼救申请、2 呼救取消、3 前向身份鉴别8频度/秒0 起0 为单次并停止连续发送9入站功率01000 最小、100 最大分辨率 0.5 dB10业务数据长度BIT 数与字段 11 配套11业务数据/状态数据BCD最长 117/159 个 BCD 码最容易填错的是模式与紧急标识的联动。模式 4普通位置报告下字段 6 和 7 要置空或填 0终端会用 RNSS 定位结果自动补本地位置你手工塞高程反而会被忽略。紧急标识为 2标准定位报告时字段 7 置空或填 0紧急标识为 3精密定位报告时字段 6 和 7 都要置空或填 0。这几条在文档里分散在注 1 到注 3抄字段表的时候很容易漏。频点字段也有个隐含边界15 映射 Lf0Lf4其中 Lf0 是北斗二号 RDSS 入站频点Lf1、Lf2 是北斗三号民用入站频点Lf4 是全球报文通信频点Lf3 在精简版里没有明确业务定义。工程上能不碰 Lf3 就不碰除非模块厂商明确告诉你它映射到什么。入站功率 0100 对应 0.5 dB 步进别按 dBm 直接填填 100 就是最大功率不是 100 dBm。一条应急搜救定位申请的组帧长这样epq build( CCEPQ, 0000001, # 收信方 ID 1, # 模式 1RDSS 定位报告无测高 1, # 入站频点 Lf0 2, # 入站速率第 2 档 1, # 紧急标识应急搜救 None, # 高程模式 1 不填 1, # 搜救类型呼救申请 0, # 频度 0单次 60, # 入站功率 0, # 业务数据长度 None, # 业务数据无 )字段数与表一致空字段保留逗号占位。发出去之后不要立刻发下一条等 FKI 回来再决定重发还是继续否则连续申请会把频度限制撞出来。4.2 EPI 定位信息输出语句的字段对齐与符号位EPI 是终端上报的定位信息字段比 EPQ 长得多典型格式是$BDEPI,...包含发信方 ID、收信方 ID、接收频点、紧急标识、定位报告模式、时间、经度、经度标志、纬度、纬度标志、高程、高程单位、高程异常、高程异常单位、精度指示、多值解、搜救业务子类型、数据长度、数据。接收频点的取值和 EPQ 的入站频点不是一套1 是 S12 是 S2C3 预留4 是 B2b。写解析器时如果复用同一张映射表S1 会被当成 Lf0数据全错。经纬度被拆成“数值 标志”两个字段经度标志 E/W纬度标志 N/S。符号不要塞进数值里也不要用正负号代替标志位。高程和高程异常各自带单位字段值都是M但两个字段必须分开读混在一起会把高程异常当成高程。注 1 规定定位报告为标准定位报告时字段 12、14、15 不为空。换句话说这三个字段能不能信取决于紧急标识是不是 2。解析时先读紧急标识再决定要不要取高程单位、高程异常、精度指示比无脑全取安全。4.3 FKI 反馈与 ICR/ICP发射成功没成功看这几个字段FKI 是终端对入站发射的反馈格式为BDFKI,hhmmss,aaa,c,x,xxxx。注意这个格式串开头没有$是文档排版漏了实际帧一定以$开头解析器不要按字面去匹配。字段 1 是指令执行时间文档写“UTC东八区”这个表述本身有歧义UTC 是零时区东八区是 UTC8两者不可能同时成立。现场做法是以设备实测为准接上串口对一次系统时间把偏差记在配置里别在代码里硬编码8。字段 2 是入站发射类型取值是字符串EPQ 应急搜救与定位报告、TCQ 报文通信申请、QRQ 查询、DTQ 定时申请、OFQ 开关机报告、MSQ 静默申请、GBQ 建组申请。字段 3 是发射情况Y成功、N失败。字段 4 失败原因 16值含义现场通常怎么处理1频率未到等频点就绪检查频点配置2发射抑制查是否被上层策略抑制3发射静默查静默申请是否未解除4功率未锁定查天线与功放链路5未检测到 IC 模块信息查卡座与卡状态6权限不足查卡权限与服务状态发射情况不是Y时失败原因可能是 0 或空剩余时间可能是0000或空。解析器要把“空”和“0”都当合法值处理不能直接int()。ICR 是查询语句格式$CCICR,x,xx指令类型 0 查本机 IC 模块、1 查本机编组、2 查下属用户、3 查 IC 模块工作模式。类型 0 和 3 时帧号填00。ICP 是它对应的输出22 个字段其中通播地址在普通型和兼收型下填 0下属用户数在普通用户机下填 0。这两个“填 0”不是可选是区分设备类型的硬标志解析时可以用来反推终端类型。4.4 TCQ/TCI报文通信的频点与长度上限对照TCQ 是发报文TCI 是收报文。TCQ 的字段依次是收信方 ID、频点、入站确认申请、编码类别、通信数据、报文通信频度。入站确认申请 1 表示不需确认、2 表示需确认频度 0 为单次连续发送要在 RMO 里关而 RMO 在精简版里没有展开这条得去查完整协议或厂商手册。长度上限按频点分档这是发长报文前必须查表的地方频点编号频点单条上限说明1Lf0120 个汉字北斗二号 RDSS 入站2Lf11000 个汉字北斗三号民用入站3Lf21000 个汉字北斗三号民用入站4Lf3文档未明确不建议使用5Lf440 个汉字全球报文通信接收侧对应关系是 S1 120 个汉字、S2C 1000 个汉字、B2b 40 个汉字。发送前按频点查上限超长就分片别指望终端帮你截断——截断后的报文长度字段对不上接收端会整条丢弃。MAX_HANZI {1: 120, 2: 1000, 3: 1000, 5: 40} # 4 对应 Lf3文档未定义 def check_len(freq: int, text: str) - None: limit MAX_HANZI.get(freq) if limit is None: raise ValueError(f频点 {freq} 无明确长度定义拒绝发送) if len(text) limit: raise ValueError(f频点 {freq} 上限 {limit} 字当前 {len(text)} 字)len(text)按字符数算不是按字节数。汉字在 GB2312 下占两字节按字节算会把上限砍半这是另一个常见误判。5. 避坑与排查现场最容易翻车的五件事5.1 校验和算错区间模块一声不吭现象帧发出去串口没有任何回帧也没有 FKI。原因把$或*算进了异或区间或者输出小写十六进制或者个位数没补零。解决固定用body line[1:].partition(*)[0]取区间输出格式固定f{value:02X}发之前先用一条已知能回的 ICR 做自检。文档里$CCICR,0,00*7B这条示例本身就算不出 7B照抄必翻车以自己算的值为准。5.2 按字符总数切字段换个小区号就错位现象解析出来的收信方 ID 跑到了模式字段上。原因用固定偏移切字符串而收信方 ID 和高程都是变长字段。解决一律按逗号split切完先校验字段数量是否符合语句定义数量不对整条丢弃并打日志。别用“字段数不对就补空”的容错补出来的数据比没有更害人。5.3 时区理解偏差FKI 时间永远差 8 小时现象FKI 里的时间和本地时间对不上日志时间线错乱。原因文档写“UTC东八区”实现方按 UTC 解设备按东八区输出或者反过来。解决拿一次已知时刻的发射做基准记录设备输出与本地时间的差值把这个偏移做成配置项别写死在代码里。换一批模块要重新标定。5.4 长报文不看频点超长被静默丢弃现象短报文正常长报文发出去没有 TCI 回来。原因用了 Lf4 却按 1000 字的上限组包实际上限只有 40 个汉字。解决发送前按频点查长度表超长主动分片每片带序号同时确认编码类别与长度单位一致按字符数而不是字节数判断。5.5 变长语句按固定结构解析PWI 直接越界现象解析 PWI 时数组下标越界或者载噪比读出一堆 0。原因PWI 的波束数量字段决定后续字段个数旧体制和新体制各有一段变长区按固定 16 个波束去读必然错。解决先读锁定数量再循环读对应组数读完旧体制再读新体制数量最后检查剩余字段是否为空。任何数量字段超过协议上限旧体制 16、新体制 63都直接判废。6. 进阶用 PWI 判断波束锁定再配一套上电自检PWI 是判断“设备到底有没有锁上星”的最直接语句格式为$BDPWI,hhmmss.ss,...。它的结构是两段变长先给旧体制出站波束锁定数量016每个波束跟三字段波束编号、S1I 载噪比、S1Q 载噪比再给新体制锁定数量063每个波束跟三字段波束编号、S2C_d 载噪比、S2C_p 载噪比。载噪比单位 dBHz0 表示未锁定。观察项未锁定特征锁定特征锁定数量0 或反复跳变稳定在 1 以上载噪比持续为 0连续多帧大于链路门限波动相邻帧差异大波动通常在几个 dB 以内波束编号频繁更换基本固定在同一编号判据不要写死一个绝对值按你的天线增益和链路预算定门限工程上常见做法是连续 510 帧载噪比都高于门限、且波动小于 23 dB才认为“稳定锁定”。只看单帧很容易被一次瞬时峰值骗过去。def parse_pwi(fields: list[str]) - dict: 按锁定数量动态解析避免固定长度越界 t fields[0] idx 1 old_cnt int(fields[idx]); idx 1 old [] for _ in range(old_cnt): beam, s1i, s1q fields[idx:idx 3] old.append((int(beam), float(s1i), float(s1q))) idx 3 new_cnt int(fields[idx]); idx 1 new [] for _ in range(new_cnt): beam, s2d, s2p fields[idx:idx 3] new.append((int(beam), float(s2d), float(s2p))) idx 3 return {time: t, old: old, new: new, rest: fields[idx:]}rest保留剩余字段方便协议扩展时快速发现“多出来的东西”。int(fields[idx])之前建议加一层数量上限判断旧体制超过 16、新体制超过 63 直接抛异常不要让它进循环。上电自检我一般固定跑四步波特率探测、发 ICR 验校验和、发 EPQ 等 FKI 看失败原因、连续读 10 帧 PWI 看锁定稳定性。四步里任何一步不过就先别碰业务逻辑。从那以后我每次拿到新模块都强制先跑一遍这套自检把波特率、时区偏移、频点长度上限三个参数写进配置文件再开工省下的排查时间比写自检脚本多得多。希望帮到你。本文还有配套的精品资源点击获取