加载中
加载中
表情图片
评为精选
鼓励
加载中...
分享
加载中...
文件下载
加载中...
修改排序
加载中...
SSTV(慢扫描电视)的调制
BG7ZDQBI4PYM
1.中北大学无线电协会
中文摘要
本文对业余无线电常用的SSTV(慢扫描电视)图像通讯模式进行了解析,并以实际代码编写的背景分享了若干存在的问题。
关键词
业余无线电SSTV
Ham RadioSSTV

一、前情提要


本人最近正在着手推进某个项目,其中要实现的一个重要功能就是 SSTV 的调制。我最开始想,找点轮子胡乱组装一下,能跑起来就行了嘛。

结果没想到,试了好几个 GitHub 上的项目,要么就是莫名其妙跑不起来,要么就是效率极其低下,使用 PD-120 模式调制一张640*496的图片,耗时动辄50秒以上,这绝对是难以忍受的……

最后我决定还是自己用 C语言重新实现 SSTV 的调制。在这个过程中,@罗布Carrot BI4PYM 提供了极大的帮助,我也学习了解了不少的内容,也踩到了不少的坑,刚好就在这里和大家分享一下。

如图:最终实现了大约6500%的性能提升。(测试平台:Raspberry Pi Zero 2W)

image.png

Github地址:SSTV-Modulation

二、SSTV 的组成


SSTV 由标识头和图像数据组成。

1. VIS 标识头

接触过 SSTV 的朋友应该都知道, SSTV 有非常多种模式。那么,该怎么让接收者知道你使用的是哪种调制模式呢?自然需要先朝对方打个招呼,告知对方使用何种模式后再开始传输。


所有标准 SSTV 模式在开始前都使用一个独特的数字代码来向接收系统标识该模式。

该代码称为 VIS,即垂直间隔信号代码(Vertical Interval Signal code)。

该代码由七位二进制数组成,按小端序排列。


我们可以参考随本文上传的一份手册来获得某种模式的代码(在文末)。

image.png

以 PD-120 模式为例:

手册中标明该模式代码为“95 d”。将其转换为二进制,即“1011111”。又因为采取小端序排列,所以应按“1111101”的顺序传输。


那么,该怎么传输“1”和“0”呢?

我们通过不同频率的音调来传输"1" 和 "0"。


“1”对应频率: 1100 Hz

“0”对应频率: 1300 Hz

每一位二进制数对应的音调持续30 ms。


所以PD-120模式对应的音频传输如下:

值(0或1)

频率(Hz)

持续时间(ms)

1

1

1100

30

2

1

1100

30

3

1

1100

30

4

1

1100

30

5

1

1100

30

6

0

1300

30

7

1

1100

30


不过,在传输 VIS 码前还要先传输一些引导音。

标识头不光包含了 VIS 代码,还包含了一些额外的信息。如下是整个标识头的时序定义:


时长 (ms)

频率 (Hz)

类型

300

1900

引导音

10

1200

中断

300

1900

引导音

30

1200

VIS 起始位

30

bit0

1100 Hz = 1, 1300 Hz = 0

30

bit1

1100 Hz = 1, 1300 Hz = 0

30

bit2

1100 Hz = 1, 1300 Hz = 0

30

bit3

1100 Hz = 1, 1300 Hz = 0

30

bit4

1100 Hz = 1, 1300 Hz = 0

30

bit5

1100 Hz = 1, 1300 Hz = 0

30

bit6

1100 Hz = 1, 1300 Hz = 0

30

偶校验*

偶数 = 1300 Hz, 奇数 = 1100 Hz

30

1200

VIS 结束位


*校验模式为偶校验,即包括校验位在内的八位二进制码中的“1”应为偶数个。

C
// 偶校验 int parity = 0; for (int i = 0; i < 7; i++) { if (vis_code[i] == '1') { parity++; } } int parity_bit = (parity % 2 == 0) ? 0 : 1; tone((parity_bit == 0) ? 1300 : 1100, 30, 0);


虽然 VIS 指的仅的是七位模式标识码,但是包括引导音、校验位等在内的整个标识头都被习惯性的称为 VIS。

2. 图像数据

标识头传输完成后,便立即进入图像数据的传输阶段。

SSTV 的图像数据采用逐行扫描的方式,每一行对应几段音频信号。


不同模式的 SSTV 选用的色彩模式不同,具体需要参考手册所给出的色彩模式。不过,具体原理大差不差:

我们知道每种色彩强度是由 8 位二进制数表示的,即 0~255 。那么,如何在用音调实现色彩强度的传输呢?


SSTV 协议规定了,色彩强度使用从 1500Hz2300Hz 800Hz 范围表示,色彩强度和频率是线性对应的:

FREQ=1500+色彩强度×3.1372549// 颜色频率乘数,由 800255 得出

传输过程中,信号的频率按照像素点的颜色通道强度值从 1500 Hz2300 Hz 线性变化。传输每一行时,系统会将像素点逐一转换为相应的频率,并以固定的采样率生成音频信号。接收系统接收完毕后,便会按色彩通道关系将其组装起来,复原图像原有的色彩。


在每行数据的传输中,SSTV 系统都会发送一段特定的的同步脉冲,从而确保图像的行与行之间能够准确对齐。每种模式的同步脉冲时序、频率、长度都不尽相同,需要阅读手册中提供的时序表来进行实现。


为了便于说明,本段使用 Scottie-DX 模式举例:

image.png

Scottie-DX 采用 R, G, B 色彩模式,图像宽 320px,每行传输时间 345.6ms


根据手册可知,在 VIS 标识头传输完毕后,首先传输一个 1200 Hz @ 9.0 ms 的起始扫描脉冲。该脉冲仅在第一行传输前发送一次,为整个图像传输提供同步起点的信号。接着,传输一个 1500 Hz @ 1.5 ms 的分离脉冲,用于区分不同的颜色通道。


分离脉冲结束后,紧接着开始传输第一行的绿色通道的数据。每个像素根据其的绿色通道的强度,生成对应频率的音调:

f=1500+Green×COLOR_FREQ_MULT

每个像素的绿色分量值 Green (0~255)决定对应的频率,每个像素的传输持续时间为 1.08 ms(1.08ms=345.6ms/320px)。从该行的第一个像素开始,依次传输每个像素的绿色通道强度。


依据手册的时序,绿色扫描结束后,紧接着传输一个 1500 Hz @ 1.5 ms 的分离脉冲,然后像绿色扫描一样开始下一个颜色(蓝色)的扫描。蓝色扫描结束后,紧接着传输一个 1200 Hz @ 9.0 ms 的同步脉冲和一个 1500 Hz @ 1.5 ms 的同步沿,然后继续进行红色扫描。红色扫描结束后,该行传输完毕,开始下一行的传输 (步骤②到⑧)。接收端此时可以依据所接收到的内容将RGB三个通道组装到,一起复原出彩色图像的第一行。


当图像的所有行扫描完毕后,程序自动结束。你也可以额外增加一个结束音。


如下是一段简化后的调制函数:

C
//tone(频率, 时长, 相位):传输信号的函数 //rgb(颜色通道, 像素横坐标, 像素纵坐标):读取某处像素颜色强度的函数 //COLOR_FREQ_MULT:颜色频率乘数 // 函数:调制 Scottie-DX void generate_scottie_dx() { // 起始同步脉冲,仅第一行 tone(1200, 9, 0); // 图像数据部分 for(int line = 0; line < 256; line++) { // 分离脉冲 tone(1500, 1.5, sign(oldercos) * asin(olderdata) + abs(sign(oldercos) - 1) / 2 * PI); // 绿色扫描 for(int x = 0; x < 320; x++) { tone(1500 + rgb("g",x,line)*COLOR_FREQ_MULT, 1.08, 相位略); } // 分离脉冲 tone(1500, 1.5, sign(oldercos) * asin(olderdata) + abs(sign(oldercos) - 1) / 2 * PI); // 蓝色扫描 for(int x = 0; x < 320; x++) { tone(1500 + rgb("b",x,line)*COLOR_FREQ_MULT, 1.08, 相位略); } // 同步脉冲与同步沿 tone(1200, 9, sign(oldercos) * asin(olderdata) + abs(sign(oldercos) - 1) / 2 * PI); tone(1500, 1.5, sign(oldercos) * asin(olderdata) + abs(sign(oldercos) - 1) / 2 * PI); // 红色扫描 for(int x = 0; x < 320; x++) { tone(1500 + rgb("r",x,line)*COLOR_FREQ_MULT, 1.08, 相位略); } } }


除了 R, G, B 色彩模式外,还有一种 Y, R-Y, B-Y 色彩模式。

转化关系如下:

Y=16+0.003906×(65.738×R+129.057×G+25.064×B)

RY=128+0.003906×(112.439×R94.154×G18.285×B)

BY=128+0.003906×(37.945×R74.494×G+112.439×B)

不同SSTV模式的色彩模式和调制时序都不尽相同,需要按照手册操作(手册在文末)。


值得一提的是,并不是所有的SSTV模式都是逐单行扫描,部分模式可能是逐两行扫描的,例如PD系列。

image.png

PD 系列 SSTV 采用 Y, R-Y, B-Y 色彩模式。


依据手册,PD 系列的 SSTV 模式首先传输偶数行的Y通道强度(PD 系列 SSTV 从第 0 行记起),然后传输该偶数行和其下奇数行的 R-Y, B-Y 强度中值,最后传输奇数行的Y通道强度。实现了一次扫描两行。


简化的传输过程如下:

步骤

传输内容

1

传输第 0 行 Y 通道强度

2

传输第 0 行和第 1 行 R-Y 通道中值

3

传输第 0 行和第 1 行 B-Y 通道中值

4

传输第 1 行 Y 通道强度

5

传输第 2 行 Y 通道强度

6

传输第 2 行和第 3 行 R-Y 通道中值

7

传输第 2 行和第 3 行 B-Y 通道中值

8

传输第 3 行 Y 通道强度


由于模式实在是多种多样,没有办法全部讲解,便也只能拿如上两种模式举例了。

其他模式具体的调制还需要依照手册进行(手册在文末)。

三、碰到的一些问题

最开始,我想直接将调制生成的音频从声卡输出,但是试验了半天实在难以实现,不得已只能先用个 wav 容器将调制出来的音频装起来。调制完一段音频发现,文件尺寸居然高达25兆……然后就在想办法削减文件大小。


众所周知,一个WAV文件的大小与一下几个参数有关:

  1. 采样率

  2. 位深度

  3. 通道数

其中通道数和位深度都已经尽量调整到了最低要求,接下来就是向采样率开刀。在前面的解析中,我们已经知道所有 SSTV 模式的最大频率并不超过 2500Hz ,依据奈奎斯特定理:


fsample=2×fmax

理论上只需要 5000Hz 的采样率即可正常存纳调制出的信号。冗余起见,我最终使用了 6000Hz 的采样率。文件尺寸缩减了80%以上。

缩减百分比=(1600044100)×10086.4%

存储占用问题先告一段落。


接下来是生成的音频的问题。最开始我想的比较简单,在生成信号时只考虑了频率和时长问题,忽略了相位问题。但是,实际调制出来的音频极其刺耳,在频谱里有非常多的杂乱的频率分量。相位不连续的突变导致了杂乱频率分量的产生,要调制的信号产生了极大的失真。


8729207deb858544281d37a7db464f46.png


image.png

后来引入了一些变量,实现了相位的连续,解调出来的图像也恢复了正常。

C
double olderdata; // 前一个幅度,用于连续相位 double oldercos; // 前一个COS,用于连续相位 // 函数:生成并写入指定频率和持续时间和初始相位的正弦波音频 void write_tone(double frequency, double duration_ms, double phi) { uint32_t num_samples = SAMPLE_RATE * duration_ms / 1000; delta_lenth += SAMPLE_RATE * duration_ms / 1000 - num_samples; if (delta_lenth >= 1) { num_samples += (int)delta_lenth; delta_lenth -= (int)delta_lenth; } double phi_samples = SAMPLE_RATE * phi; short buffer[num_samples]; for (uint32_t i = 0; i < num_samples; ++i) { buffer[i] = (short)(32767 * sin((2 * PI * frequency * i + phi_samples) / SAMPLE_RATE)); } fwrite(buffer, sizeof(short), num_samples, file); total_samples += num_samples; olderdata = sin((2 * PI * frequency * num_samples + phi_samples) / SAMPLE_RATE); oldercos = cos((2 * PI * frequency * num_samples + phi_samples) / SAMPLE_RATE); } tone(freq, time, sign(oldercos) * asin(olderdata) + abs(sign(oldercos) - 1) / 2 * PI);


但是接下来还有一个问题:因为像素时长与采样率的乘积不是一个整数,直接取整会导致每像素的时长都有些许缩短。

dc74c4fe2ff9874dca6c16c9a1604fe7.png

最后BI4PYM 使用累积误差补偿,将小数位累加起来,当误差超过一个采样时长时补上一个采样时长,消除了时长误差。这一部分的代码实现也位于上个 C语言 代码块中。


以上大致就是碰到的一些问题了。很感谢BI4PYM提供的一系列帮助和理论支持~


目前的程序虽然基于C语言的性能优势,获得了巨大的性能提升,但是整体性能仍然有待优化,欢迎大家提出改进意见。


附件:

attachment iconProposal for SSTV Mode Specifications.pdf108.19KBPDF28次下载预览

attachment iconImage Communication on Short Waves.pdf17.21MBPDF28次下载预览

修改记录:2024/12/22 12:20 调整排版,修改部分函数名混乱的代码,修改错别字,添加附件


[修改于 2个月0天前 - 2024/12/22 12:28:50]

+0.5  科创币    罗布Carrot    2024/12/21 你做的好啊,你做的好啊(赞赏)
来自:电子信息 / 无线电计算机科学 / 软件综合
3
 
11
新版本公告
~~空空如也
WernerPleischner
2个月0天前 IP:广东
940612

正弦完了再反正弦没必要,DDS一个FM波只需要用一个累加器对频率积分出相位然后函数查表就好了。浮点可以都改成定点,相位可以把2pi归一化成2^n,这样只需要移位就能取模进index。不要累加符号长度,直接用时间查表算现在应该发哪个符号。


引用
评论
1
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
山雨欲来风满楼作者
2个月0天前 IP:山西
940623
引用WernerPleischner发表于1楼的内容
正弦完了再反正弦没必要,DDS一个FM波只需要用一个累加器对频率积分出相位然后函数查表就好了。浮点可

您的建议确实能很大的提升整个程序的效率,这也是我打算对程序后续的性能优化的方向。

初期开发的时候出于简化的因素没有考虑那么多,后续对于相位归一化之类的改进还要再研究研究具体实现方式。


引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
zkf0100007
1个月25天前 IP:湖南
940741

不错,GITHUB已star


引用
评论
1
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

山雨欲来风满楼
进士 机友 笔友
文章
4
回复
33
学术分
0
2022/12/24注册,15时12分前活动

业余无线电爱好者,呼号BG7ZDQ。

中北大学飞行器设计与工程专业在读。

主体类型:个人
所属领域:无
认证方式:手机号
IP归属地:海南
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的