POCSAG解码器实战:从位流同步到BCH纠错的嵌入式实现
2026/9/13 5:13:19 网站建设 项目流程

简介:POCSAG是无线电寻呼机广泛采用的数字编码标准,用于将文本消息以低带宽方式广播到多个终端。压缩包提供了围绕POCSAG解码与验证的整套工程文件,适合业余无线电爱好者、嵌入式开发者和通信协议学习者研究。包内共29个文件,总体积约60KB,包括C源代码(.c)、头文件(.h)、十六进制固件镜像(.hex)、工程配置(.prj/.mak)、编译列表(.lst/.lis)及调试过程文件(.dbg/.cof/.bak)等,既可直接阅读协议实现,也能配合烧录实验板观察真实解码结果。POCSAG网上专门资料本就稀缺,而5位/7位字符映射、同步识别和差错控制是理解该协议的主要难点,通过源码和工程文件的相互对照,可快速掌握从码元解调到字符还原的完整链路。目前已有196人浏览学习,对想入门寻呼机协议、低功耗数传或复古硬件通信的读者,是一份值得参考的实例。

1. 一份藏在 rar 里的 POCSAG 解码器,能教你处理位级信号

POCSAG 是一种比 LoRa 老得多的数字寻呼编码,但今天医院、工地和应急系统里,1200bps 的寻呼信号仍然在电波里跑。压缩包里那堆 pos.c、pos.s、uart1.c 不是桌面脚本,而是 AVR 单片机工程;这意味着你得在比特级别处理同步码、BCH 校验和位序,而不是像用 base64 解码工具那样直接查表。不少人搜 POCSAG 解码,最后找到的多是 GNU Radio 模块或论文公式,这种带 .prj、.cof、.hex 的老式 AVR Studio 工程反而少见。它适合三类人:玩 AVR/STM32 的嵌入式工程师、做低成本寻呼接收验证的无线电爱好者,以及想从海明码一路追到真实通信协议的学生。

2. POCSAG 帧结构:前导码、同步码字与 5/7 位字符映射

2.1 前导码和 512/1200/2400 bps 的选择

POCSAG 发射前先送至少 576 位交替的 101010… 前导码,接收机用它的跳变沿恢复位时钟。没有这个前导码,后续的同步码字根本对不准。前导码之后才是第一批数据,也就是说解码器要主动跟踪“曾经看到过 101010 序列,现在准备进入同步字搜索”的状态,而不是一上电就满世界找数据。

速率直接决定定时器初值。三种速率使用的帧结构完全一致,只是位周期不同,接收逻辑里唯一要改的是每 bit 对应的定时器计数。

速率 (bps)位周期 (us)32 位码字时长 (ms)典型用途
5121953.12562.5长距离寻呼
1200833.33326.667城市寻呼主力
2400416.66713.333高速数据

在 AVR 中通常用定时器溢出中断控制采样,下面的宏按 7.3728MHz 晶振和 1200bps 计算:

#define BIT_TICKS_1200 6144 /* 7.3728MHz / 1200 */ #define SAMPLE_TICKS (BIT_TICKS_1200 / 2)

BIT_TICKS_1200是一位周期内的主时钟计数,SAMPLE_TICKS是位中间采样点。如果换成 8MHz 晶振,这两个值都要重新算;只改宏不改预分频是常见翻车原因。

2.2 同步码字与批次边界

前导码结束后是一个固定同步码字 0x7CD215D8,先发最高位。同步码字后面是连续的数据码字,按 32 位一组切分。每 33 个码字构成一个批次:1 个同步码字和 16 个帧,每个帧两个码字。帧号在 0 到 15 之间循环,接收端必须维护这个计数器,因为它参与地址的低 3 位。

码字字段位长内容
bit3110=地址/空闲,1=消息
地址码字 bit30..1318地址高 18 位
地址码字 bit12..112功能位,决定消息格式
消息码字 bit30..1120消息数据
bit10..110BCH(31,21) 校验
bit01偶校验

地址实际有 21 位,低 3 位由帧号补充,所以同一个 18 位地址高段出现在不同帧时会变成不同寻呼机。解析地址时若只取码字内的高 18 位而忽略帧号,你会看到同一批呼叫被拆到 8 个不同地址上。

你以前玩 51 单片机红外遥控解码仿真图时,信号的 0/1 是靠脉宽比例判断的;POCSAG 不一样,它每一位都是等宽 NRZ,只要边沿抖动超过半个位周期就丢码。这决定了接收端尽量用输入捕获而不是普通 GPIO 轮询。

2.3 5 位数字模式和 7 位文本模式的取舍

消息码字里到底装 5 位字符还是 7 位字符,由功能位和上层寻呼协议共同决定。数字寻呼为了在低速下塞更多字符,常把 0-9、空格、括号等映射成 5 位码,一个 20 位消息字段正好放 4 个字符。字母数字寻呼则用 7 位编码,需要把多个消息码字的数据区像流水一样拼起来,再按 7 位滑窗切字符。

5 位码字符5 位码字符
0-9'0'-'9'14'-'
10空格15')'
11U16'('
12[17/
13]18备用

解码时最忌讳从消息码字边界直接按字节取字符。5 位模式虽然每个码字刚好 4 个字符,但跨码字拼接发生时,7 位模式需要维护一个 bit 游标,否则第二段消息会整体错位。我一般在代码里先把消息码字的数据位压进一个bitbuf[],最后统一按 5 或 7 位截取,绝不中途转换。

3. 从位流到消息:同步捕获、码字解析与 Python 验证

3.1 同步捕获:滑窗找 0x7CD215D8

真正的解码不依赖“前导码之后一定跟着同步字”这一假设,而是持续用一个 32 位移位寄存器接收位。每收到一位,寄存器左移一次并把新位放在最低位,然后与 0x7CD215D8 比较。匹配成功时,当前批次边界就对上了,之后每 32 位切一个码字。

uint32_t shifter = 0; for (;;) { int b = read_bit_from_radio(); shifter = ((shifter << 1) | (b & 1)) & 0xFFFFFFFF; if (shifter == 0x7CD215D8) { /* 进入码字同步状态 */ break; } }

read_bit_from_radio()由定时器采样实现,返回值必须是严格的 0 或 1。把shifter左移、新位放 LSB,对应 POCSAG 最高位先行的约定;如果反过来右移,同步字永远匹配不上。同步逻辑建议用状态机管理:

状态进入条件要做的事
搜索前导码连续 16 位呈 1010 交替确认速率,准备采样
搜索同步字32 位值等于 0x7CD215D8清零帧号
接收码字同步后每 32 位一组交给 BCH 校验和消息解析

3.2 码字拆分和消息提取的 Python 原型

拿到一段 POCSAG 基带位流后,先用 Python 验证逻辑远比在单片机上下断点快。下面函数接收已经做过位同步的 0/1 列表,从同步码字之后开始解析地址和消息。

def load_32(bits, i): v = 0 for j in range(i, i + 32): v = (v << 1) | bits[j] return v def parse_stream(bits, sync_end): addr = None func = 0 msg_bits = [] i = sync_end frame = 0 while i + 32 <= len(bits): codeword = load_32(bits, i) i += 32 if codeword & 0x80000000: # 消息码字:取20位数据,按发送顺序放入msg_bits data = (codeword >> 11) & 0xFFFFF for b in range(19, -1, -1): msg_bits.append((data >> b) & 1) else: if addr is not None and len(msg_bits) >= 7: text = '' for k in range(0, len(msg_bits) - 6, 7): v = 0 for bit in msg_bits[k:k + 7]: v = (v << 1) | bit text += chr(v) print(f'addr={addr} func={func} text={text!r}') addr_high = (codeword >> 13) & 0x3FFFF func = (codeword >> 11) & 0x3 addr = addr_high | (frame & 0x7) msg_bits = [] frame = (frame + 1) % 16

load_32按最高位优先装配 32 位整数。消息码字里的(codeword >> 11) & 0xFFFFF恰好把低 11 位校验和偶校验扔掉,留下 20 位消息数据。地址码字里的功能位是(codeword >> 11) & 0x3,地址高 18 位要用(codeword >> 13) & 0x3FFFF。这段代码没有做 BCH 纠错,因此信号稍差时会出现打印乱码。

3.3 为什么 5 位模式先拼比特流再查表

如果是数字寻呼,消息码字的 20 位数据同样要先顺序压进一个位数组,再每 5 位查一次字符表。因为 5 位模式每个消息码字正好 4 个字符,看起来可以从码字边界直接切,但地址与消息之间可能夹杂空闲码字;只要有一个空闲码字没被正确处理,后续位游标就乱了。统一拼位数组后,空闲码字只是不产生消息数据,不会让解析错位。

def decode_numeric(msg_bits, table): out = [] for i in range(0, len(msg_bits) - 4, 5): v = 0 for bit in msg_bits[i:i + 5]: v = (v << 1) | bit out.append(table[v] if v < len(table) else '?') return ''.join(out)

不同寻呼台对空闲码字的处置略有差异,有的会在消息结束后连发两三个空闲码字,有的只发一个。解码器不能一看到地址码字就重置消息缓冲,要同时判断后续码字是否为空闲;这些边界条件在 Python 原型中跑通后再进 C 能省很多时间。

4. 在 AVR 工程里落地:文件清单、串口输出与接收链路

4.1 谁才是主程序:从 pos.c 到 pos.mak

压缩包里的文件覆盖了源码、汇编和编译产物。pos.c大概率是 POCSAG 解码主逻辑,pos.s很可能是汇编级位流处理,uart1.c负责串口发送;pos.prj是 AVR Studio 工程,pos.mak是 makefile,pos.hex是最终烧录文件,pos.lst是汇编清单。iom128v.hiot2313v.h分别对应 ATmega128 和 AT90S2313,说明这套代码在两种芯片上移植过。

文件类型阅读顺序
pos.cC1. 先找同步字常量
pos.s汇编2. 看外部中断入口
uart1.cC3. 串口输出格式
pos.prj工程文件4. 确认芯片型号
pos.hex烧录文件5. 最后与编译产物比对

拿到这类老工程,不要急着烧录。先打开pos.lst,看开头的处理器型号与中断向量表布局。AVR Studio 3/4 时代工程经常把 AT90S2313 的头文件和 ATmega128 的工程混在一起,直接烧pos.hex基本都会因中断向量错位而乱码。

4.2 串口输出与调试信息的最小实现

解码结果最终要从串口吐出来。下面是一段兼容 ATmega128 的发送函数,寄存器名要随编译器版本调整。

void uart_putc(unsigned char c) { while (!(UCSR0A & (1 << UDRE0))); UDR0 = c; } void uart_puts(const char *s) { while (*s) uart_putc(*s++); }

UCSR0AUDRE0位表示发送缓冲空;不等待直接写UDR0,前一个字符会被覆盖。如果编译时报UCSR0A未定义,把它改成旧的UCSRA即可。这段代码只负责透传,真正的消息格式化应当在内存里完成,不要在中断里调用uart_puts,否则长消息会让解码中断超时。

调试时我会在每条消息前输出ADR=... FUN=...,消息内容统一按十六进制再跟一份 ASCII。这样即使文本解码有误,也能从 HEX 里看出位序问题。

4.3 接收模块接线与位采样参数

POCSAG 接收模块一般输出解调后的 NRZ 数据脚。数据脚接到 AVR 的外部中断引脚,中断里记录跳变沿;然后用定时器在每位的中间点采样。这个方法和你之前看 pt2272 芯片解码原理视频里的脉宽判决不同:pt2272 靠高电平时长区分数据,POCSAG 则必须在固定的位周期中心采样,提前或落后都会误码。

参数值(1200bps)
位周期833.333 us
晶振7.3728 MHz
每 bit 计数6144
中间采样点416 us 附近

真正开始调板子前,先用信号发生器或另一块板子把已知位流输入数据脚。若串口完全无输出,优先看数据脚空闲电平是否合适;如果输出乱码但能看到地址,再把示波器探头放在数据脚上,检查边沿是否抖动超过 20%。这是接收链路最常见的两个问题,和 POCSAG 本身无关。

4.4 动手前先做的三件事

第一,备份原始 hex,防止刷坏后找不到原版。第二,用文本编辑器打开pos.prj,确认芯片型号和时钟频率,直接改.mak里的MCU_TARGET也可以。第三,不要动定时器初值。位采样相位一旦偏移,即使同步码能抓到,后续 1200 位里也会慢慢错开。想调速率,就先看uart1_init里的分频数,再回到pos.c的计算宏。

5. BCH 纠错与解码器验证:从 Hamming 到 POCSAG 的最后一步

5.1 10 位校验的 BCH 生成多项式

本科做海明校验编码解码实验时,海明码的监督位很少;POCSAG 把校验位扩到 10 位,构成 BCH(31,21) 码,能纠正 1 位错误。每个码字前 21 位是信息区,后 10 位是校验区,最后 1 位是偶校验。接收端把前 31 位对生成多项式做模 2 除法,余数非零即出现错误。下面给出 C 实现。

#define BCH_POLY 0x5B9 /* POCSAG 源码里常见的生成多项式 */ uint16_t bch_remainder(uint32_t code) { code &= 0x7FFFFFFF; for (int i = 30; i >= 10; i--) { if (code & (1u << i)) { code ^= (BCH_POLY << (i - 10)); } } return code & 0x3FF; }

code &= 0x7FFFFFFF把偶校验位隔开,循环从最高位往下逐位做模 2 长除法。返回值是 10 位校验余数;对接收码字调用,余数全 0 表示通过 BCH 校验,否则与本地校正子表比较,可以定位出错位。在 AVR 上逐位循环开销较高,pos.c里如果已经有查表逻辑,尽量沿用它的表。

5.2 用人为错误验证解码器

验证解码器是否真的在纠错,不能只看误码率。我一般把一段已知消息的位流抓下来,先跑一遍确认地址和文本都对,然后把某个消息码字的第 15 位取反再跑。如果输出不变,说明纠错路径生效;如果输出错字,多半是纠错发生在消息解析之后,或者校验位位置算错。

由此也能看出同步码字是个例外:POCSAG 规定同步字固定,不做 BCH 纠错。若你的解码器把同步字也送去查表,弱信号下会出现大量伪同步。

5.3 从 hex 反推一段固件是否包含 POCSAG 逻辑

拿到只有 hex 没有源码的固件,想确认它是不是在解 POCSAG,最直接的方法是反汇编后搜索同步码字常量。用avr-objdump -D -m avr pos.hex转出汇编,然后在输出里找0x7CD215D8的加载指令。能找到这个常量,说明固件大概率实现同步字匹配;还能找到 BCH 查表地址的话,基本就能确认核心逻辑完整。这个搜索方法对pos.hex000.hex都适用。

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

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

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

立即咨询