Portapack应用开发教程(十五) APRS接收

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匹配了。

 

http://n1vg.net/packet/

根据这个网页说的,音频频率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++;
		}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值