github/28hua
摘 要
本文介绍采用标准C语言搭建BPSK、QPSK、16QAM、OFDM基带移动通信仿真系统的原理和实现,深刻揭示了使用标准C语言进行移动通信系统仿真的优势和弊端,认为虽然标准C具有运行速度快和占用内存小的优点,但是缺乏科学计算库,抽象层次低,内存管理复杂,并发严重依赖操作系统,在语言层面和标准库没有对多线程的直接支持,不适合进行模型的快速构建和理论验证。本系统除C语言标准库外仅仅依赖第三方库FFTW3,程序采用高内聚、低耦合的模块化程序设计思想进行设计,代码重用率高,具有较强的可扩展性和可重用性。 本文所有仿真系统模仿香农经典通信系统模型,即点对点通信,不涉及网络通信。信道模型包含高斯噪声信道和瑞利信道。系统仿真的结果是误比特率曲线,通过比较不同调制方法的误比特率曲线,分析其抗噪声、抗衰落性能。
关键字:BPSK QPSK 16QAM OFDM 标准C
电汽锅一、 简介
1. 各种调制方法简介
BPSK
BPSK即二进制相移键控,采用相位相反的正弦载波表示0和1。
QPSK
BPSK的一个符号代表一个比特,即,在QPSK中,一个符号代表两个比特,即。QPSK的编码采用格雷码,在星座图中某点与距离其最近的点的汉明距离最小。
16QAM
对于BPSK和QPSK,每种信号的能量或者振幅是相同的,对于16QAM可以有三种能量或振幅的信号。OFDM星座图中的点也采用格雷码表示,某点与距离其最近的点的汉明距离最小。
OFDM
BPSK、QPSK、16QAM都只有一路载波,而OFDM采用多路相互正交的子载波,子载波的频率间隔,为OFDM信号的符号周期,其调制解调框图如下所示。
2. BER、SNR和
BER:误比特率,即
SNR:信噪比
:信噪比的分贝形式:
: 功率效率
:带宽效率
: 的分贝形式:
在编程实现中,假设各种调制系统的发送信号平均每比特能量相同且为,假设码元发送速率已知且,通过改变的值测试不同功率效率下的误比特率,待求变量是,由上述方程和假设已知条件可得,
假设,则
3. 标准C回顾
为使用C语言进行系统仿真,必须深入理解C语言语法,其中最重要的是数组与指针。 在C的世界里,数组和指针都是基本数据类型。指针变量中存放的是某段内存空间的起始地址,指针的类型暗示指针做自加运算时偏移的字节数。当数组作为函数参数或者函数返 回值或者赋值给指针时,数组被隐式转换为指针,丢失数组大小信息。
局部变量存储在栈上,局部变量离开其作用域时,内存不能再被访问。举例,函数体内不加static关键字定义的数组作为返回值返回,此返回值赋值给指针,试图使用指针访问之前在函数体内定义的数组将会发生错误,因为此数组在栈上分配,栈上内存在函数结束时被计算机回收。为使函数体内分配的内存具有跨函数的生命周期,采用动态内存分配或者static关键字修饰,前者灵活但不易编程,后者编程方便但所分配内存直到程序结束才释放。
二、 系统模型
对于BPSK、QPSK、16QAM、OFDM仿真系统,编程实现由易到难,但简单仿真系统的子模块经常可以复用到复杂仿真系统,并且简单仿真系统可以为复杂仿真系统提供设计思路。
本文从最简单的BPSK仿真系统开始,规定各种仿真系统平均每比特能量。首先生成0/1伪随机序列,对于基带仿真系统,不考虑高频正弦载波,因此使用1表示比特1,使用-1表示比特0。然后模拟信道传输,添加噪声(和衰落),如果发送x,噪声,则接收,如果衰落,则接收。最后判决,计算误码率。
对于QPSK仿真系统,由于QPSK系统的星座图有四个点,每个点离原点距离相同,故能量相同,又因为,因此星座图中每个点的模均为。本系统使用的映射如下
并且采用格雷编码使汉明距离最小的码相邻。
对于16QAM仿真系统,,星座图包括十六个点,离远点距离有三类,分别用a,b,c表示,令则,所以
由方形QAM系统星座图的特点易得,因此x轴和y轴的投影长度只有两种。本系统采用映射如下,依然采用格雷编码:
0, 0, 0, 0->x1, y1 0, 0, 0, 1->x2, y1 0, 0, 1, 0->x1, y2 0, 0, 1, 1->x2, y2
0, 1, 0, 0->x1, -y1 0, 1, 1, 0->x1, -y2 0, 1, 0, 1->x2, -y1 0, 1, 1, 1->x2, -y2
1, 0, 0, 0->-x1, y1 1, 0, 1, 0->-x1, y2 1, 0, 0, 1->-x2, y1 1, 0, 1, 1->-x2, y2
1, 1, 0, 0->-x1,-y1 1, 1, 0, 1->-x2,-y1 1, 1, 1, 0->-x1,-y2 1, 1, 1, 1->-x2,-y2
其中x1=y1=,x2=y2=。
对于OFDM系统,本次仿真产生伪随机0/1序列后立刻做16QAM映射,然后串并变换,快速傅里叶反变换,添加循环前缀,并串变换,模拟噪声(和衰落)信道,接收端采用与发送端相逆的过程,最后统计误码率。
各种调制系统流程图如下图所示:
三、 编程实现
1. 瑞利随机数与高斯随机数的产生
瑞利随机数的分布函数为:
在概率论中,根据给定的随机变量x的分布和函数g(x)容易得到y=g(x)的分布。其逆问题是已知随机变量x的分布和y的分布,确定g(x),使得g(x)服从y的分布。根据概率论理论:
产生瑞利随机数的思想是由[0-1]均匀分布的随机变量u和瑞利分布函数求解g(u),
令:
解得
根据概率论理论,如果x和y是相互独立的零均值方差为的正太随机变量,那么服从瑞利分布,服从的均匀分布。因此,可以由
产生零均值,方差为的高斯随机数。
2. 动态分配二维矩阵的两种方法
方法一:
方法二:
方法一的优点是需要调整数组维数只需realloc,缺点是malloc和free的复杂度都是o(n),并且访问每一个元素需要两次寻址;方法二的优点是malloc和free的复杂度都是o(1),访问每个元素只需要一次寻址,缺点是调整数组维数非常不便,比如OFDM仿真系统需要添加循环前缀,使用第二种方法需要对整个数组做拷贝。
3. 内存管理
使用MatLab、Java等带有垃圾回收机制的编程语言很少操心内存管理,但是在C语言给予用户直接操纵内存的自由,而用户需要为这种自由承担内存管理的责任,为此需要深刻理解操作系统内存布局。
一种典型的操作系统内存由低字节到高字节包括:文本段(代码段)、已初始化数据段、未初始化数据段、栈、堆。
文本段存储将要执行的代码,多个程序实例可以共享文本段,当多个程序实例链接相同动态库时,内存只载入一份此库的副本。文本段还是只读的,程序不能改变自身。
已初始化数据段存储已经初始化的全局变量。
未初始化数据段存储尚未初始化的全局变量,存储在此段中的变量,即所有未初始化的全局变量在程序执行前自动被初始化为0或者NULL。
栈是栈帧的集合,每当一个函数被调用时会为它分配一个栈帧,栈帧中存放被调用函数所有局部变量,传递给被调用函数的参数,被调用函数返回后将要执行的下一条指令的地址。
堆,是C调用malloc或者C++调用new分配内存的区域。堆和栈的生长方向相反。
C语言的内存管理是一项极易犯错的编程任务,培养好的编程习惯可以降低错误发生率
● 为函数编写清晰的注释,尤其涉及内存分配与释放的函数
● 尽力把内存分配逻辑和释放逻辑写到同一作用域内
家庭视频电话● 代码检查,调用malloc的次数等于调用free的次数
红外线烘干箱● 检查malloc返回值,判断内存是否分配成功
● free指针指向的内存后,将指针赋值为NULL
● 浅复制时,当内存被释放后所有指向此处内存区的指针应赋值为NULL;深复制时,复制后的内存也要释放。
4. 头文件一览
#ifndef SIMULAT_H #define SIMULAT_H #include <stdbool.h> #include <fftw3.h> #include <math.h> typedef unsigned long ulong; typedef struct Point { double SNR_db; double BER; } Point; /*串行序列*/ typedef struct Serial { double * signal; ulong N; } Serial; /*串行序列, but存储的是复数*/ typedef struct Serial_C { fftw_complex * signal; ulong N; } Serial_C; /*m路并行*/ typedef struct Parallel { double ** signal; ulong M; ulong N; } Parallel; /*m路并行, but存储的是复数*/ typedef struct Parallel_C { fftw_complex ** signal; ulong M; ulong N; } Parallel_C; double gen_uniform_random(void); double gen_rayleigh_random(double sigma); double gen_standard_normal_random(void); double gen_normal_random(double mean, double sigma); int gen_binomial_random(double p); Serial* gen_signal(ulong N); void bipolar(Serial* serial, double scale); void add_noise(Serial * serial, double mean, double sigma); void add_noise_c(Serial_C * serial_c, double mean, double sigma); void add_noise_and_fading(Serial * serial, double n_mean, double n_sigma, double r_sigma); void add_noise_and_fading_c(Serial_C * serial, double n_mean, double n_sigma, double r_sigma); Parallel* serial_to_parallel(Serial * serial, ulong M); 荸荠削皮机Parallel_C* serial_to_parallel_c(Serial_C* serial, ulong M); Serial_C* parallel_to_serial_c(Parallel_C* parallel_c); void add_cycle_prefix(Parallel_C* parallel_c); void del_cycle_prefix(Parallel_C* parallel_c); Serial* BPSK(Serial* serial); Serial* rBPSK(Serial* serial); Serial_C* QPSK(Serial* serial); Serial* rQPSK(Serial_C* serial_c); Serial_C* QAM16(Serial* serial); Serial* rQAM16(Serial_C* serial_c); void judge4QAM16(Serial_C* serial_c); void judge4QPSK(Serial_C* serial_c); void judge4BPSK(Serial* serial); void fft(Parallel_C* parallel_c); void ifft(Parallel_C* parallel_c); void transpose_array(Parallel* parallel); #endif |
|
5. 绘图脚本
啪啪圈""" 绘出误码率/信噪比曲线 """ import sys import csv import matplotlib.pyplot as plt import matplotlib.pylab as plb from collections import namedtuple def get_x_y(result_file): """ 绘出误码率/信噪比曲线 """ with open(file=result_file, mode='r') as data: data_csv = ader(data) headings = next(data) SNR_BER = namedtuple("SNR_BER", headings) SNR_db, BER = [], [] for line in data_csv: row = SNR_BER(*line)磁流体发电机 SNR_db.append(float(row.SNR_db)) BER.append(float(row.BER)) return SNR_db, BER if __name__ == '__main__': files = sys.argv[1:] for file in files: SNR_db, BER = get_x_y(file) plt.title("") plt.yscale("log") plt.plot(SNR_db, BER, 'r^-') id(True) plt.show() |
|
四、 仿真结果及讨论
所有仿真系统的产生一千万个伪随机序比特,噪声服从零均值,方差为1的高斯分布,瑞利衰落的参数取0.5,OFDM为16路复用。仿真系统的所有误码率数据保存为.csv格式的文件,读者可以使用最擅长的绘图工具绘制误码率曲线对比研究。笔者使用Python作图,命令行参数为需要比较的误码率文件。