APRS功能可以用对讲机来传输数字信息,以便在没有运营商网络的环境下共享位置,实现类似微信的位置共享功能,也可以用它来发短消息。
有人买直接带aprs的对讲机,比较贵。也有人用普通对讲机和手机通过音频线对接,然后手机上运行aprsdroid软件来实现同样的功能。
它的原理跟sstv比较类似,都是两次fm,把数据进行编码(手机中完成),然后用音频的高低音来表示编码10101(手机中完成),再把这个编码再次做fm调制(对讲机中实现)最终发射出去。
portapack可以把手机和对讲机合并在一起,还是利用sstv中类似的方法就能完成。
furrtek固件里已经有一个aprs tx软件,虽然是黄色图标(代表不完善),但是根据一位朋友的反馈,已经可以用它发射信号,然后用对讲机+aprsdroid来成功解码。
现在我来做一个aprs rx就行。
我打算先用一个portapack的aprs tx来发射,然后用hackrf+电脑来做fm解调代替对讲机,然后用aprsdroid来解码体验一下。
然后把aprsdroid解码部分用c++实现,再往portapack移植就行了。
但是我发现aprsdroid程序虽然开源,但是用scala语法写的,我还不太熟悉。
所以我就直接看了portapack里的各种代码,我发现portapack中的aprs tx程序只是做了上层编码,它的底层是afsk tx。
发射部分调用关系如下:
ui_aprs_tx.cpp (make_aprs_frame) -> aprs.cpp (make_ui_frame) -> ax25.cpp (shared_memory.bb_data.data) -> proc_afsk.cpp (word_ptr)
而portapack的接收部分里已经有了一个afsk rx了,用它来解调,只要做好上层解码就行了。
我仔细看了一下proc_afsk.cpp(发射调制)和proc_afskrx.cpp(接收调制)。
proc_afsk.cpp(execute函数)
if (cur_bit)
tone_phase += afsk_phase_inc_mark;
else
tone_phase += afsk_phase_inc_space;
tone_sample = sine_table_i8[(tone_phase & 0xFF000000U) >> 24];
delta = tone_sample * fm_delta;
phase += delta;
sphase = phase + (64 << 24);
re = (sine_table_i8[(sphase & 0xFF000000U) >> 24]);
im = (sine_table_i8[(phase & 0xFF000000U) >> 24]);
buffer.p[i] = {re, im};
这部分代码跟sstv发射的对应部分差不多,通过操作tone_phase,就能发射两次fm的信号,只不过sstv里影响tone_phase的是像素点的颜色值,而这里是mark和space。我觉得它们就对应1和0。
porc_afskrx.cpp (execute函数)
一开始在做第一次的fm解调,从无线电信号变为音频信号,相当于是一个对讲机。
得到音频信号后要再做一次fm解调。下方代码就在做这个事,跟我sstv接收里的音频fm解调效果差不多的,早知道我就直接参考它这里代码了。
原理就是sdrsharp讲解里也讲过的,把信号作延迟再乘以本身的信号就行,这个delay line在rtlsdr的那本书里也讲过。
// Delay line put
delay_line[delay_line_index & 0x3F] = current_sample;
// Delay line get, and LPF
sample_mixed = (delay_line[(delay_line_index - (samples_per_bit/2)) & 0x3F] * current_sample) / 4;
sample_filtered = prev_mixed + sample_mixed + (prev_filtered / 2);
delay_line_index++;
prev_filtered = sample_filtered;
prev_mixed = sample_mixed;
// Slice
sample_bits <<= 1;
sample_bits |= (sample_filtered < -20) ? 1 : 0;
// Check for "clean" transition: either 0011 or 1100
if ((((sample_bits >> 2) ^ sample_bits) & 3) == 3) {
// Adjust phase
if (phase < 0x8000)
phase += 0x800; // Is this a proper value ?
else
phase -= 0x800;
}
phase += phase_inc;
sample_filtered里面是解调出来的音频频率,照道理这里要根据设置的mark和space频率值来灵活配置,不知道为啥只是以-20作为阈值来判断当前采样对应1或0。
下面这段根据不同的协议来把sample_bits转为word_bits然后传送给程序的其他部分。
if (trigger_word) {
// Continuous-stream value-triggered mode (AX.25) - UNTESTED
word_bits <<= 1;
word_bits |= (sample_bits & 1);
bit_counter++;
if (triggered) {
if (bit_counter == word_length) {
bit_counter = 0;
data_message.is_data = true;
data_message.value = word_bits & word_mask;
shared_memory.application_queue.push(data_message);
}
} else {
if ((word_bits & word_mask) == trigger_value) {
triggered = !triggered;
bit_counter = 0;
data_message.is_data = true;
data_message.value = trigger_value;
shared_memory.application_queue.push(data_message);
}
}
} else {
// RS232-like modem mode
if (state == WAIT_START) {
if (!(sample_bits & 1)) {
// Got start bit
state = RECEIVE;
bit_counter = 0;
}
} else if (state == WAIT_STOP) {
if (sample_bits & 1) {
// Got stop bit
state = WAIT_START;
}
} else {
word_bits <<= 1;
word_bits |= (sample_bits & 1);
bit_counter++;
}
if (bit_counter == word_length) {
bit_counter = 0;
state = WAIT_STOP;
data_message.is_data = true;
data_message.value = word_bits;
shared_memory.application_queue.push(data_message);
}
}
aprs的编码是ax.25,根据上面代码的注释,接收程序中还没完全实现解码,所以afsk rx那个app还不能直接显示aprs tx的数据。
我们来比较一下发射和接收里的设置是不是完全一样。
我查了资料aprs的常用制式之一是bel202。
下面是代码里的设置。
ui_aprs_tx.cpp
baseband::set_afsk_data(
AFSK_TX_SAMPLERATE / 1200,
1200,
2200,
1,
10000, //transmitter_model.channel_bandwidth(),
8
);
开头的1200和后面的1200 2200在下面的代码里有解释。
baseband_api.cpp
void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space,
const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count) {
const AFSKTxConfigureMessage message {
afsk_samples_per_bit,
afsk_phase_inc_mark,
afsk_phase_inc_space,
afsk_repeat,
afsk_bw,
symbol_count
};
send_message(&message);
}
根据这两段代码,可以知道目前aprs tx app里,mark对应1200,space对应2200,也可以根据samples_per_bit=sample_rate/1200来算波特率。
下面是接收部分
ui_afsk_rx.cpp
auto def_bell202 = &modem_defs[0];
persistent_memory::set_modem_baudrate(def_bell202->baudrate);
接收在变量名里体现了默认使用bell202。
application/protocols/modems.hpp
struct modem_def_t {
char name[16];
ModemModulation modulation;
uint16_t mark_freq;
uint16_t space_freq;
uint16_t baudrate;
};
constexpr modem_def_t modem_defs[MODEM_DEF_COUNT] = {
{ "Bell202", AFSK, 1200, 2200, 1200 },
{ "Bell103", AFSK, 1270, 1070, 300 },
{ "V21", AFSK, 980, 1180, 300 },
{ "V23 M1", AFSK, 1300, 1700, 600 },
{ "V23 M2", AFSK, 1300, 2100, 1200 },
{ "RTTY US", AM, 2295, 2125, 45 },
{ "RTTY EU", AM, 2125, 1955, 45 }
};
而根据这个代码,Bell202的mark就是1200,space是2200,波特率是1200。
可以说默认情况下aprs发射和afsk接收已经在使用相同的参数了。
不过我始终有个疑问,接收的mark_freq和space_freq没有被任何地方调用到,也就是白指定了,肯定会有问题,因为如果切换制式的话,没有实际的更改效果的。
另外modem.cpp也可以好好看看。
找到两个电脑上开源项目。
发射: https://github.com/casebeer/afsk
接收: https://github.com/wb2osz/direwolf
发射程序可以直接运行,不需要安装
python ./ax25.py -c BGXXX -o new.wav ":EMAIL shao@139.com"
如果更改发射程序代码,可以把log改为print,直接打印在终端窗口上。
print (r"Sending packet: '{0}'".format(packet))
print (r"Packet bits:\n{0!r}".format(packet.unparse()))
这是打印出来的信息,包含我要发的数据包以及对应的二进制数据,这个程序还会生成一个wav文件,里面就是用这个数据包调制出的声音。
Sending packet: 'BG4QL>APRS,WIDE1-1,WIDE2-1::EMAIL shao@139.com'
Packet bits:\nbitarray('01111110010000010000010100100101011001010000001000000010000001100010000101110001000101100100010100011001000000100000011001110101010010010001000101010001010001100000001001000110011101010100100100010001010100010010011000000010110001101100000000001111010111001010001010110010100000101001001000110010000001001100111000010110100001101111011000000010100011001100110010011100011101001100011011110110101101100111101111101110001111110')
接下来我们安装direwolf,然后试试能否从刚刚生成的wav文件里解出信息。
atest new.wav
44100 samples per second. 16 bits per sample. 1 audio channels.
287606 audio bytes in file. Duration = 3.3 seconds.
Fix Bits level = 0
Channel 0: 1200 baud, AFSK 1200 & 2200 Hz, E, 44100 sample rate.
DECODED[1] 0:02.207 BG4QL audio level = 99(53/50)
[0] BG4QL>APRS,WIDE1-1,WIDE2-1::EMAIL shao@139.com
APRS Message must begin with : 9 character addressee :
1 from new.wav
1 packets decoded in 0.050 seconds. 65.1 x realtime
direwolf除了解调其他程序生成的文件以外,也可以自己生成wav,也就是说它不只是接收也能发射。
gen_packets -o test1.wav
Output file set to test1.wav
built in message...
atest test1.wav
44100 samples per second. 16 bits per sample. 1 audio channels.
261650 audio bytes in file. Duration = 3.0 seconds.
Fix Bits level = 0
Channel 0: 1200 baud, AFSK 1200 & 2200 Hz, E, 44100 sample rate.
DECODED[1] 0:00.731 WB2OSZ-15 audio level = 49(25/26)
[0] WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 1 of 4
DECODED[2] 0:01.472 WB2OSZ-15 audio level = 50(24/26)
[0] WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 2 of 4
DECODED[3] 0:02.215 WB2OSZ-15 audio level = 50(26/25)
[0] WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 3 of 4
DECODED[4] 0:02.956 WB2OSZ-15 audio level = 50(26/24)
[0] WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 4 of 4
4 from test1.wav
4 packets decoded in 0.051 seconds. 57.6 x realtime
在电脑上用音频的方式发射和接收aprs数据我们都试过了。接下来是时候用无线电信号试一下了。
1.gnuradio+hackrf发射new.wav,portapack sstv rx接收。
这是发射流图
下面是接收的结果,可以看到屏幕上有画面,它会根据音频频率变化有不同的图像。
如果用portapack aprs tx来发射给portapack sstv rx,我没看到什么特别的图像。
2.portapack aprs tx发射,gnuradio+hackrf接收,并存入tmp.wav
这是接收流图
这是portapack aprs tx发射界面。
接下来把录制好的tmp.wav用atest来解码,确实能解出正确的数据,与Portapack aprs tx程序界面一致,说明portapack aprs tx程序没有问题。
atest tmp.wav
44100 samples per second. 8 bits per sample. 1 audio channels.
372857 audio bytes in file. Duration = 8.5 seconds.
Fix Bits level = 0
Channel 0: 1200 baud, AFSK 1200 & 2200 Hz, E, 44100 sample rate.
DECODED[1] 0:03.855 432312-2 audio level = 16(6/5)
[0] 432312-2>132321-6:FGGHCVVVRWVVMIMGFKKGNSFJIMQQNL
DECODED[2] 0:04.763 432312-2 audio level = 16(6/5)
[0] 432312-2>132321-6:FGGHCVVVRWVVMIMGFKKGNSFJIMQQNL
DECODED[3] 0:05.600 432312-2 audio level = 16(6/5)
[0] 432312-2>132321-6:FGGHCVVVRWVVMIMGFKKGNSFJIMQQNL
3 from tmp.wav
3 packets decoded in 0.135 seconds. 62.5 x realtime
3. 用gnuradio+hackrf发射new.wav,用portapack afsk rx接收
可以看到portapack afsk rx程序里每次解码出的数据都相同,并且gnuradio每发射一次,afsk就解出一条有规律的数据,说明解码有点问题,但是解调应该已经可以了。
4. portapack aprs tx发射,portapack afsk rx接收。
我最后又尝试了直接用两个portapack互相收发,也能在afsk rx里看到有规律的数据了,说明离成功不远了。
https://www.bilibili.com/video/BV1xz4y1C74y/
现在要做的就是做好squelch,这样afsk rx在没信号的时候不会把噪声解出来的数据也打印出来,另外就是要改解码程序。
https://www.bilibili.com/video/BV1Xy4y1v7ny/
经过前面的测试,至少正面portapack内部的aprs发射是没问题的,接收解调基本也可以,但是解码有问题。
所以我要着重观察portapack的aprs tx代码和afsk rx解调部分代码。
aprs tx的编码部分都在ax25.cpp里实现,调制是在proc_afsk.cpp里实现。
编码比较简单,主要是调制可以多看看。
解调我在考虑要不要用sstv解调代替,毕竟那个代码我更熟悉,但是他有采样率限制。而furrtek写的代码也能解调,只是我不熟悉,好处是参数已经和aprs匹配了。
根据这个网页说的,音频频率1200Hz叫做mark,2200Hz叫做space,只是一个名称,没什么实际意义,它们单独出现的时候无法表示0或者1,要表示0和1取决于它们是否发生变化。音频频率变化的时候代表0,音频频率不变的时候代表1。
比特顺序是把低位的先发出来。
由于1对应于音频频率不变,那么如果数据是一连串1,信号就是一个持续的没变化的信号了,这样会导致接收机无法保持同步。因此需要在一连串1(5个1)之间插入一个0。这个0需要接收机自己探测到,然后在解码过程中把它去掉。
AX.25协议规定了数据包格式,基于HDLC(High Level Data Link Control protocol)协议。这样一个包的开头和结尾都会有01111110的标志位。这个包里面有6个1,中间不会像上一段说到的插入0。也就是说如果你看到6个连着的1,那肯定是包的开头或者结尾。
AX.25还是用了CRC-CCITT来纠错,它占用了2个byte(16个bit)。并且传输的时候先把低位的byte发出来。
昨天我把portapack aprs tx发射部分代码又仔细看了一遍,总算把编码和调制都看懂了。
不需要看很多代码,只要看application/protocols/ax25.cpp和baseband/proc_afsk.cpp即可。
先看一下ax25.cpp,它负责编码。
void AX25Frame::make_ui_frame(char * const address, const uint8_t control,
const uint8_t protocol, const std::string& info) {
size_t i;
bb_data_ptr = (uint16_t*)shared_memory.bb_data.data;
memset(bb_data_ptr, 0, sizeof(shared_memory.bb_data.data));
bit_counter = 0;
current_bit = 0;
current_byte = 0;
ones_counter = 0;
crc_ccitt.reset();
add_flag();
add_flag();
add_flag();
add_flag();
make_extended_field(address, 14);
add_data(control);
add_data(protocol);
for (i = 0; i < info.size(); i++)
add_data(info[i]);
add_checksum();
add_flag();
add_flag();
flush();
}
这个make_ui_frame是打包的主要函数。
一开始几行都是在初始化。
接下来add_flag()其实是在加包头了和最后的add_flag()加包尾是一样的。
address包含收发呼号和收发ssid。info里就是那一长串payload,都用add_data加入包里。
在结尾前还有个add_checksum()加的就是crc结果。
现在看看add_flag()函数
void AX25Frame::add_flag() {
add_byte(AX25_FLAG, true, false);
};
这里AX25_FLAG在头文件里定义就是0x7E,01111110,这个就是我们从资料文章里找到的开头和结尾的标志位。
所以add_flag()就是用来加这个标志位的。至于为啥开头要加四遍,结尾要加两遍我还不明白。
void AX25Frame::add_data(uint8_t byte) {
add_byte(byte, false, true);
};
void AX25Frame::add_checksum() {
auto checksum = crc_ccitt.checksum();
add_byte(checksum, false, false);
add_byte(checksum >> 8, false, false);
}
add_data和add_checksum都和add_flag差不多,都在调用add_byte函数。
void AX25Frame::add_byte(uint8_t byte, bool is_flag, bool is_data) {
uint32_t bit;
if (is_data)
crc_ccitt.process_byte(byte);
for (uint32_t i = 0; i < 8; i++) {
bit = (byte >> i) & 1;
if (bit)
ones_counter++;
else
ones_counter = 0;
if ((ones_counter == 6) && (!is_flag)) {
NRZI_add_bit(0);
ones_counter = 0;
}
NRZI_add_bit(bit);
}
}
这个add_byte函数的参数除了要传入的数据以外,还需要告诉它是标志位is_flag,还是普通数据is_data。
如果是普通数据,那么它要先用crc_ccitt来算一下crc。
往下看,把传入数据byte里的每一位取出来,判断一下这个位是不是1,如果是1就要记数,如果是0,就不用记数,且要把记数清空。如果记数到达6,而且不是标志位,就要用NRZI_add_bit函数插入一个0。
这个判断做完了以后就可以把这一位也用NRZI_add_bit加入缓存中了。
接下来看看NRZI_add_bit函数
void AX25Frame::NRZI_add_bit(const uint32_t bit) {
if (!bit)
current_bit ^= 1; // Zero: flip
current_byte <<= 1;
current_byte |= current_bit;
bit_counter++;
if (bit_counter == 8) {
bit_counter = 0;
*bb_data_ptr = current_byte;
bb_data_ptr++;
}
}
它实际上不是把ax25包的bit直接加入缓存中的,而是做了一个转换。如果位是0,要做变频flip,如果位是1不需要做flip。
这个函数的做法是如果输入的位是0,那么current_bit就flip一下,否则就不flip,具体做法就是current_bit与1按位异或,然后存入current_bit就能达到这个效果。
这样其实current_bit的0和1就跟编码已经没什么关系了,它直接就对应了音频频率值1200Hz和2200Hz。
bit数量达到8个后就作为一个byte传给proc代码。
proc代码只需要把这个byte读出来,然后如果其中一个bit是1,就发射2200Hz的音频信号,如果bit是0就发射1200Hz的音频就行。
接下来我们看看proc代码。
void AFSKProcessor::execute(const buffer_c8_t& buffer) {
// This is called at 2.28M/2048 = 1113Hz
if (!configured) return;
for (size_t i = 0; i<buffer.count; i++) {
if (sample_count >= afsk_samples_per_bit) {
if (configured) {
cur_word = *word_ptr;
if (!cur_word) {
// End of data
if (repeat_counter < afsk_repeat) {
// Repeat
bit_pos = 0;
word_ptr = (uint16_t*)shared_memory.bb_data.data;
cur_word = *word_ptr;
txprogress_message.done = false;
txprogress_message.progress = repeat_counter + 1;
shared_memory.application_queue.push(txprogress_message);
repeat_counter++;
} else {
// Stop
cur_word = 0;
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
configured = false;
}
}
}
cur_bit = (cur_word >> (symbol_count - bit_pos)) & 1;
if (bit_pos >= symbol_count) {
bit_pos = 0;
word_ptr++;
} else {
bit_pos++;
}
sample_count = 0;
} else {
sample_count++;
}
if (cur_bit)
tone_phase += afsk_phase_inc_mark;
else
tone_phase += afsk_phase_inc_space;
tone_sample = sine_table_i8[(tone_phase & 0xFF000000U) >> 24];
delta = tone_sample * fm_delta;
phase += delta;
sphase = phase + (64 << 24);
re = (sine_table_i8[(sphase & 0xFF000000U) >> 24]);
im = (sine_table_i8[(phase & 0xFF000000U) >> 24]);
buffer.p[i] = {re, im};
}
}
configured基本上都是1,所以这个判断不用看。
传进来的数据在cur_word里面。
cur_bit = (cur_word >> (symbol_count - bit_pos)) & 1;
这里symbol_count是8-1=7。然后根据bit_pos不断增加,相当于cur_word右移7位,然后取出最低位。下次循环,右移6位,取出最低位。
总之这个操作会把cur_word里的每一位都依次存入cur_bit里。然后根据cur_bit是0还是1决定发射1200Hz还是2200Hz就行。
if (cur_bit)
tone_phase += afsk_phase_inc_mark;
else
tone_phase += afsk_phase_inc_space;
这一段代码就在做这个,它不是直接改频率,而是改的是每次调用的相位增加值,相位增加多少,其实就代表频率是多少。
还有一点很重要,就是说前面得到的1010101的数据,每一位到底需要用某个音调发射多久的问题。
bel202规定了波特率是1200,也就是说1200位/秒。而我们已经知道无线电的采样率是多少了,采样率单位是采样点/秒。
我们可以用采样率除以波特率,得到采样点/位,也就是下面这个afsk_samples_per_bit。
这个值是在ui_aprs_tx.cpp里的set_afsk_data里的第一个参数设定的
baseband::set_afsk_data(
AFSK_TX_SAMPLERATE / 1200,
1200,
2200,
1,
10000, //transmitter_model.channel_bandwidth(),
8
);
AFSK_TX_SAMPLERATE/1200,就是我前面说的采样率除以波特率。
这样我就能知道为了每1位,我需要生成多少个正弦波的采样点了。如果采样点数量达到这个数量,我就继续发射一模一样频率的信号,达到这个数量我才去获取新1位数据,然后重新计算要发射的频率
if (sample_count >= afsk_samples_per_bit) {
//获取新1位的数据
sample_count = 0;
} else {
//还没到达上次要发送的采样点数量,就不获取新数据,继续发射上次的频率,采样点计数+1
sample_count++;
}