简介: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) | 典型用途 |
|---|---|---|---|
| 512 | 1953.125 | 62.5 | 长距离寻呼 |
| 1200 | 833.333 | 26.667 | 城市寻呼主力 |
| 2400 | 416.667 | 13.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 位。
| 码字字段 | 位长 | 内容 |
|---|---|---|
| bit31 | 1 | 0=地址/空闲,1=消息 |
| 地址码字 bit30..13 | 18 | 地址高 18 位 |
| 地址码字 bit12..11 | 2 | 功能位,决定消息格式 |
| 消息码字 bit30..11 | 20 | 消息数据 |
| bit10..1 | 10 | BCH(31,21) 校验 |
| bit0 | 1 | 偶校验 |
地址实际有 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 | ')' |
| 11 | U | 16 | '(' |
| 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) % 16load_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.h和iot2313v.h分别对应 ATmega128 和 AT90S2313,说明这套代码在两种芯片上移植过。
| 文件 | 类型 | 阅读顺序 |
|---|---|---|
| pos.c | C | 1. 先找同步字常量 |
| pos.s | 汇编 | 2. 看外部中断入口 |
| uart1.c | C | 3. 串口输出格式 |
| 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++); }UCSR0A的UDRE0位表示发送缓冲空;不等待直接写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.hex和000.hex都适用。
本文还有配套的精品资源,点击获取