在此感谢原文作者提供的思路,已经在STM32L4和STM32F0成功实现
笔者的专业是通信工程,通信领域内调制解调器的设计大多数用的都是硬件电路,但是鉴于笔者对编程情有独钟(其实笔者还是懂一点电路设计知识的~),所以最终决定用stm32来设计,纯编程实现。看起来高大上,但实际做起来不难,不过有挺多东西要考虑的,所以还是花了笔者一个星期的时间。
废话不多说,先来介绍下什么是调制解调,什么是2FSK。
在通信系统中,包含有丰富的低频分量的信号称为基带信号,在传输距离较近时,基带信号可以直接传输。但是如果要实现较远距离的传输时,需要用到调制技术,也就是用较高频率的载波与基带信号进行结合,然后发射出去,接收方接收到信号后,再通过一系列方法把基带信号还原出来。
上面只是一个简单地介绍,实际上调制解调过程要更复杂得多,涉及到模拟调制和数字调制,AM,FM,ASK,FSK,PSK,QAM等调制方式和相干解调,非相干解调等解调方式,还要考虑码间串扰,误码率等传输性能的问题。在这里笔者不打算一一介绍,毕竟以编程为主。笔者仅对数字调制中的2FSK调制原理做一个简单的说明。
2FSK是利用载波的频率变化来传递数字信息。在基带信号的控制下,2FSK由两个不同频率的正弦波组合而成,具体的调制过程如下:
好了,接下来开始进入调制解调器的设计阶段。
总的设计思路如下:
首先是基带信号的产生,它也是我们要调制和解调的目标。基带信号由一连串随机的码元序列构成,为了模拟随机的码元序列,笔者用定时器设计8位的PN码序列,码元速率为2000B/s。定时器3定时0.5ms,每进入一次中断,变量num加一,设置一次IO引脚电平,8位PN码只需设置8次,然后num清零。
TIM3_Init(499,71); //基带信号
u8 num=0;
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
num++;
switch (num)
{
case 1: Base_Signal = 1; break;
case 2: Base_Signal = 0; break;
case 3: Base_Signal = 0; break;
case 4: Base_Signal = 0; break;
case 5: Base_Signal = 1; break;
case 6: Base_Signal = 0; break;
case 7: Base_Signal = 1; break;
case 8: Base_Signal = 0; break; //pn码序列
}
if(num == 8)
num = 0;
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
接下来要产生载波,载波就是正弦波无疑。这里笔者的载波频率要求是4khz和8khz。正弦波的产生用的是stm32的DMA+DAC+TIM2。
正弦波的数据用正弦波数据发生器产生,采样点数64,精度12位,保存在Sine12bit[]数组,但是传送给DMA的正弦波数据不是这些原始的数据,而是将这些数据进行了进一步的处理:
uint16_t Sine12bit[64] = {
0x7FF,0x8C8,0x98E,0xA51,0xB0F,0xBC4,0xC71,0xD12,0xDA7,0xE2E,0xEA5,0xF0D,0xF63,0xFA6,0xFD7,0xFF5
,0xFFE,0xFF5,0xFD7,0xFA6,0xF63,0xF0D,0xEA5,0xE2E,0xDA7,0xD12,0xC71,0xBC4,0xB0F,0xA51,0x98E,0x8C8
,0x7FF,0x736,0x670,0x5AD,0x4EF,0x43A,0x38D,0x2EC,0x257,0x1D0,0x159,0x0F1,0x09B,0x058,0x027,0x009
,0x000,0x009,0x027,0x058,0x09B,0x0F1,0x159,0x1D0,0x257,0x2EC,0x38D,0x43A,0x4EF,0x5AD,0x670,0x736
};
uint32_t Idx = 0;
int main(void)
{
... //省去无关代码
for (Idx = 0; Idx < 64; Idx++)
{
Sine12bit[Idx] = Sine12bit[Idx]*8/10+500; //防止出现底部失真
}
... //省去无关代码
}
为什么要这么处理呢?在讲到DAC的配置时还会再提到这一点,在这里先不做解释。经过处理后的正弦波数据可以直接传送到DMA通道,等TIM2的触发时间一到,就可以依次把数据给到DAC,转换成正弦波输出。笔者用DAC通道2(对应PA5引脚)输出波形,所以需要使能和配置DMA2通道4,DMA的配置如下:
#define DAC_DHR12R2_Address 0x40007414
void DMAx_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* DMA1 clock enable */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);
/* GPIOA Periph clock enable */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* DAC Periph clock enable */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
/* Once the DAC channel is enabled, the corresponding GPIO pin is automatically
connected to the DAC converter. In order to avoid parasitic consumption,
the GPIO pin should be configured in analog */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//配置为模拟输入,抗噪声干扰
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* DMA1 channel4 configuration */
DMA_DeInit(DMA2_Channel4);
DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12R2_Address;//DAC通道2的12位右对齐寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&Sine12bit;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 64;//采样64点,故缓存大小为64
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA2_Channel4,&DMA_InitStructure);
DMA_Cmd(DMA2_Channel4, ENABLE);
}
TIM2和DAC的配置如下:
void TIM2_DAC_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
DAC_InitTypeDef DAC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler = psc;
TIM_TimeBaseStructure.TIM_ClockDivision = 0x0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Down; //设为向下计数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
DAC_InitStructure.DAC_Trigger = DAC_Trigger_T2_TRGO;
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; //使能输出缓存
DAC_Init(DAC_Channel_2, &DAC_InitStructure);
DAC_Cmd(DAC_Channel_2, ENABLE);
DAC_DMACmd(DAC_Channel_2, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
这里笔者把两个模块的配置同时放在一个初始化函数里面,只是图个方便,在官方例程里是将DAC和DMA的配置放在一起。这段代码有两个地方需要注意:一是TIM2计数模式设为向下计数,二是使能了DAC的输出缓存。设为向下计数是为了在两个正弦波频率切换时不会因为计数溢出而出现问题,在FSK产生环节里还会详细说到这一点;使能输出缓存是因为stm32的DAC在输出缓存关闭时输出阻抗太大,带负载能力弱,在输入捕获时正弦波严重失真,故需要开启输出缓存,但同时也存在一个问题:使能输出缓存后,DAC没办法使输出达到0,这就使得原始正弦波的峰值数据丢失,导致底部失真。于是我们需要用上面的代码对原始正弦波数据做一个处理——先乘上8除以10防止峰值超过12位精度的最大值4096(不能直接乘上0.8,因为数组存储的数据必须是整形),然后再加上500,将正弦波数据整体抬高。
生成正弦波后自然是要把两个正弦波组合在一起形成FSK信号,这个组合当然不是随意组合,是要在基带信号的控制下进行。代码在主函数执行,如下:
int main(void)
{
... //初始化代码
while(1)
{
if(Base_Signal == 1)
{
TIM2->ARR = 140;;
}
if(Base_Signal == 0)
{
TIM2->ARR = 280;
}
}
}
while(1)循环里if语句判断基带信号的码元序列,“1”对应8khz载波,“0”对应4khz载波。通过改变TIM2的自动重装载寄存器(ARR)的值实现两个载波的频率切换。解释一下这里为什么选择140和280:采样64个点,8khz对应的DAC转换速率为8000*64hz,那么TIM2就要每隔8000/64/72 000 000 = 1/140s触发一次DAC,故TIM2的ARR值为140;同样的,4khz对应的ARR值为280。在这里还要注意:TIM2的计数模式应配置为向下计数。一般例程都会把定时器配置为向上计数,但用在这里会出现一个问题:在基带信号由0变为1时,FSK信号也要相应的从4khz正弦波跳变到8khz正弦波。我们知道向上计数模式是TIM2->CNT寄存器从0开始计数,一直计到ARR的值,进入中断,然后重新清零,继续计数直到又达到ARR设定的值。。。假设FSK信号在4khz正弦波时TIM2->CNT一度计数到140以上(此时ARR的值为280),突然基带信号变为1,FSK信号由4khz正弦波变为8khz,ARR值被设定为140,这时候CNT寄存器将一直往上计数,永远不会停止,直到溢出(ARR寄存器为16位)。实际上笔者在调试时,当基带信号为“1“,输出的FSK信号为一条直线。把计数模式改为向下计数,问题解决。
经过上述一番折腾,调制总算是搞定了,来看看效果:
接下来就是解调。笔者用了两次解调才把基带信号完整复现出来。先来看看初步解调代码,用的是TIM1的输入捕获模块,TIM1属于高级定时器,和通用定时器的代码还是有些地方不一样的,比如输入捕获中断函数名为TIM1_CC_IRQHandler()
void TIM1_Cap_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM1_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_8);
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler =psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
TIM1_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01
TIM1_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM1_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM1_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM1_ICInitStructure.TIM_ICFilter = 0x00;
TIM_ICInit(TIM1, &TIM1_ICInitStructure);
NVIC_InitStructure.NVIC_IRQChannel = TIM1_CC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_ITConfig(TIM1,TIM_IT_CC1,ENABLE);
TIM_Cmd(TIM1,ENABLE );
}
选择输入捕获是因为对于FSK信号来说,它由两个不同频率的正弦波组成,stm32默认的高电平在2V以上,低电平在0.8V以下。通过测量从上升沿到下降沿这段时间,与阈值100us比较(4khz的正弦波半个周期为125us,8khz的正弦波半个周期为62.5us),大于100者码元即为“0”,反之则为“1”。
u8 flag_falling;
int TIM1CH1_CAPTURE_VAL;
void TIM1_CC_IRQHandler(void)
{
if(flag_falling == 0) //检测到上升沿
{
TIM_OC1PolarityConfig(TIM1,TIM_ICPolarity_Falling);//设置下一次触发为下降沿触发
TIM_SetCounter(TIM1,0);//清空TIM1->CCR1寄存器的值
TIM1CH1_CAPTURE_VAL = 0;//变量TIM1CH1_CAPTURE_VAL用于存储TIM1->CCR1寄存器的值
flag_falling = 1;//置位标志位,标志下一次进入中断后检测到下降沿
}
else //检测到下降沿
{
TIM_OC1PolarityConfig(TIM1,TIM_ICPolarity_Rising);//设置下一次触发为上升沿触发
TIM1CH1_CAPTURE_VAL=TIM_GetCapture1(TIM1);//读取TIM1->CCR1寄存器的值
flag_falling = 0;//清除标志位,标志下一次进入中断后检测到上升沿
if(TIM1CH1_CAPTURE_VAL >= 100)//设定阈值,与TIM1CH1_CAPTURE_VAL进行比较
{
First_jietiao = 0;
}
else
{
First_jietiao = 1;
}
}
TIM_ClearITPendingBit(TIM1, TIM_IT_CC1);
}
在这里笔者小小地偷了个懒——没有配置TIM1的更新中断,而只是配置了捕获中断。这是鉴于笔者的TIM1初始化为:
TIM1_Cap_Init(0XFFFF,71); //以1MHZ的频率计数
看到了吧,0xFFFF,多大的数~其实也不大,只不过对于我们要捕获的FSK信号来说它避免了更新中断对捕获造成的影响,也就是说当我们捕获到下降沿时得到的TIM1->CCR1寄存器的值就是我们想得到的时间,与计数值溢出多少次并无关系。注意:当捕获的波形频率较高时可以这么做,但是如果波形频率较低时最好使能更新中断,在更新中断里保存中断次数,得到的结果更准确。
然而这只是我们初步解调出来的结果,由于4khz与8khz之间的过渡带影响,最终得到的码元序列“1”的持续时间长于码元为“0”的持续时间,信号的码速率不是2000B/s,所以我们需要进行二次解调。
二次解调的关键在于定时器TIM5的同步作用。笔者用TIM5定时2khz,在初步解调信号的边沿处先延时150us,然后开始同步,通过判断初步解调信号的码元序列,得到二次解调信号的码元。
在TIM1中断函数里面:
u8 a=1; //a为全局变量
if(flag_falling == 0 && a == 1)//捕获到下降沿时开始同步(下降沿亦即初步解调信号的边沿)
{
delay_us(150);
TIM_Cmd(TIM5, ENABLE); //只需要执行一次
a = 0;
}
在这里为什么要延时150us呢?为何不在初步解调信号的边沿处就开始同步呢?这是考虑到初步解调信号高电平持续的时间比低电平的长,如果不延时,则可能出现低电平码元误判。
接下来就是定时器5的中断服务函数:
void TIM5_IRQHandler(void)
{
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
{
if(First_jietiao == 1)
out_put = 1;
else
out_put = 0;
TIM_ClearITPendingBit(TIM5, TIM_IT_Update );
}
}