发新帖本帖赏金 3.00元(功能说明)我要提问
12下一页
返回列表
打印
[STM32F1]

【分享】基于stm32的2FSK调制解调器设计

[复制链接]
4076|36
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
        笔者的专业是通信工程, 这是笔者在校的一个综合设计。大致要求:设计一个FSK调制解调器,基带信号码速率为2000B/s,载波速率为4khz和8khz,解调信号要能完整还原基带信号。实现方法多种多样,通信领域内调制解调器的设计大多数用的都是硬件电路,鉴于笔者对编程情有独钟(其实笔者还是懂一点电路设计知识的~),所以最终决定用stm32来设计,纯编程实现。看起来高大上,但实际做起来不难,不过有挺多东西要考虑的,所以还是花了笔者一个星期的时间。
         总的设计思路如下:

         首先是基带信号的产生,它也是我们要调制和解调的目标。基带信号由一连串随机的码元序列构成,为了模拟随机的码元序列,笔者用定时器设计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;        //防止出现底部失真
  }
  ...                                          //省去无关代码
}

打赏榜单

21ic小能手 打赏了 3.00 元 2016-06-07
理由:编辑推荐帖

沙发
风中的狙击手|  楼主 | 2016-6-7 16:41 | 只看该作者
       为什么要这么处理呢?在讲到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信号为一条直线。把计数模式改为向下计数,问题解决。
       经过上述一番折腾,调制总算是搞定了。

使用特权

评论回复
板凳
风中的狙击手|  楼主 | 2016-6-7 16:43 | 只看该作者
       接下来就是解调。笔者用了两次解调才把基带信号完整复现出来。先来看看初步解调代码,用的是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,所以我们需要进行二次解调。

使用特权

评论回复
地板
风中的狙击手|  楼主 | 2016-6-7 16:47 | 只看该作者
        二次解调的关键在于定时器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  );  
                }
}
       由于TIM5的2khz时钟的同步作用,得到的二次解调信号能完整地复现基带信号,实现解调。至此,整个2FSK调制解调系统设计完成。

使用特权

评论回复
5
风中的狙击手|  楼主 | 2016-6-7 16:52 | 只看该作者
欢迎提出各种建议~

使用特权

评论回复
6
longquanshuang| | 2016-6-10 10:27 | 只看该作者
本帖最后由 longquanshuang 于 2016-6-10 10:32 编辑

整个通信过程没见到计算依据,望补充

我会用SPI加DMA来做这个东西,就是个发送1010和1100的事情
接收也用SPI接收,建个表直接把接收的16bit翻译成2位或者1位,SPI使用两倍或者4倍发送速率过采样,避免传输干扰和采样泄漏

使用特权

评论回复
7
keigo2008| | 2016-6-11 08:42 | 只看该作者
太强大了。

使用特权

评论回复
8
风中的狙击手|  楼主 | 2016-6-11 10:23 | 只看该作者
longquanshuang 发表于 2016-6-10 10:27
整个通信过程没见到计算依据,望补充

我会用SPI加DMA来做这个东西,就是个发送1010和1100的事情

不知道你指的是哪方面的计算依据呢?

使用特权

评论回复
9
wfmartin28| | 2016-6-11 10:28 | 只看该作者
楼主能否共享一份源程序呢!!万分感谢!!!

使用特权

评论回复
10
风中的狙击手|  楼主 | 2016-6-11 11:09 | 只看该作者
wfmartin28 发表于 2016-6-11 10:28
楼主能否共享一份源程序呢!!万分感谢!!!

可以,等我上传一份

使用特权

评论回复
11
风中的狙击手|  楼主 | 2016-6-11 11:14 | 只看该作者
~

基于stm32的2FSK调制解调器源程序.rar

2.22 MB

使用特权

评论回复
12
wfmartin28| | 2016-6-11 16:24 | 只看该作者
谢谢!

使用特权

评论回复
13
ckr| | 2016-6-11 18:45 | 只看该作者
好牛,非常好很有参考价值

使用特权

评论回复
14
薨莀| | 2016-6-15 08:26 | 只看该作者
好厉害!学习学习

使用特权

评论回复
15
haov000| | 2016-6-16 13:51 | 只看该作者
mark ,赞一个

使用特权

评论回复
16
huangqi412| | 2016-6-16 15:59 | 只看该作者
虽然用了DA搞成了正弦波,其实就是当方波处理的。

使用特权

评论回复
17
huangqi412| | 2016-6-16 16:01 | 只看该作者
有个疑问,忘记手册怎么写的了,捕捉高低触发电平是多少,一个0V--3.3V纯正弦波送到捕捉口,上下触发分别会发生在多少电压处。

使用特权

评论回复
18
风中的狙击手|  楼主 | 2016-6-17 09:34 | 只看该作者
huangqi412 发表于 2016-6-16 16:01
有个疑问,忘记手册怎么写的了,捕捉高低触发电平是多少,一个0V--3.3V纯正弦波送到捕捉口,上下触发分别会 ...

是的,捕获时是把正弦波当成方波处理的,如果要精确测量的话这样做误差蛮大,不过在这里只是做个比较,不需要太精确,所以这样做也是可以的。
对于stm32来说,捕获时高电平的触发电平是2v以上,低电平是0.8v以下

使用特权

评论回复
19
bobby567| | 2020-4-17 13:54 | 只看该作者
Mark一个,虽然是旧贴子,但是对我学习工作有指导作用,谢谢斑竹

使用特权

评论回复
20
xiangqide| | 2020-9-25 17:52 | 只看该作者
这个输入接口是怎样一个形式,有哪位能提供一份参考原理图吗,用于解调FSK的,谢谢。

使用特权

评论回复
发新帖 本帖赏金 3.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2

主题

20

帖子

1

粉丝