> 技术文档 > 一种简单的stm32调制解调2fsk信号的方式_stm32 qpsk调制解调器

一种简单的stm32调制解调2fsk信号的方式_stm32 qpsk调制解调器


2FSK的介绍

2FSK(Binary Frequency Shift Keying)是一种数字调制技术,通过改变载波频率来传输二进制数据。其核心是用两个不同的频率分别表示二进制符号“0”和“1”。例如:“0”对应频率f1​,“1”对应频率f2。

调制原理:

  • 频率切换:根据输入比特流切换两个独立的振荡器(或单个压控振荡器),生成对应频率的载波。

解调方法:

  • 非相干解调:无需相位同步,常用方法包括:

    • 带通滤波器+包络检波:通过两个滤波器分离频率,检测幅度判断比特。

    • 过零检测:根据单位时间内信号过零点数判断频率。

  • 相干解调:需同步载波相位,误码率更低但实现复杂。

性能特点

  • 抗干扰性:频率变化对幅度衰减不敏感,适合噪声信道(如无线传感网络、RFID)。

  • 带宽:约为频率间隔Δf与符号速率Rs​之和(Δf+2Rs),连续相位可优化带宽。

  • 调制指数h:定义为h=Δf/Rsh=Δf/Rs​,影响频谱效率,MSK中h=0.5可以实现高效传输。

应用场景

  • 低速通信:早期调制解调器、遥测系统。

  • 低功耗场景:物联网(IoT)、传感器网络。

对比其他调制技术

  • 对比ASK/PSK:FSK抗噪声更优,但频谱效率较低。

  • 进阶变体:GFSK(高斯滤波FSK)用于蓝牙,进一步提升抗干扰性。

思路

本文章将使用stm32的dsp库实现2fsk的调制与非相干解调,思路为:

调制:通过按键输出想要传输的码元,并加上固定的包头包尾形成想要传输的send数组然后根据二进制码元的0,1值通过stm32f4的DAC输出相应频率的波(其中1为1000Hz,0为500Hz)。

解调:使用stm32f4的ADC采集输出的2fsk信号,然后通过dsp自带的fir低通滤波器滤除高频信号部分留下低频信号部分,使2fsk信号变成2ask信号,这时再让2ask信号取绝对值使其包络变为只有0,1两种电平,最后进行包络检波并判决便可将2ask信号转化为方波信号,通过ADC的采样频率与码元的传输速率进行对比便可将方波信号转化为接收到的数组,如果包头包尾正确便可读出数据点亮相应的LED灯。

代码部分

调制

首先进行stm32cubemx的配置:

1.打开并配置相应的时钟(不做演示)

2.打开相应的DAC,使用DAC+TIM+DMA控制,打开DAC的通道,并选择使用TIM2触发,DMA打开Circular模式:

3.配置定时器2,打开时钟源配置PSC为1-1,ARR任意(不要太大,否则第一次进入中断会很慢),后续将在keil中修改,auto-reload preload为enable,防止修改ARR时定时器卡住,最后务必打开Updata Event,否则DAC无法使用TIM2,打开TIM2的中断:

4.打开TIM7,设置为6ms中断,用来输出码元(即一个码元持续6ms):

5.配置好后进入keil:

 在开头定义包头包尾与按键值和发送数组

uint8_t head[4]={1,0,1,0};//包头为1010uint8_t tail[4]={1,0,1,0};//包尾为1010uint8_t send[12];uint8_t Key[4]={1,1,1,0};//初始值为1110uint8_t signal; //存储二进制数

在main函数的开头定义一个120个点的正弦波数组

 uint16_t sinx[120]; for (int i = 0; i < 120; i++) { sinx[i] =(sin(i * 2 * 3.14159265358 / 120)+1.2)*1700; }

然后在初始化后打开定时器与DAC,并将TIM2的ARR修改为700-1(因为TIM2的CNT每自增到ARR时DAC便输出一次,所以正弦波的频率为84M/((PSC+1)*(ARR+1)*正弦波点数),即1000Hz,写完之后DAC就可以自动输出正弦波了

 HAL_TIM_Base_Start_IT(&htim2); HAL_TIM_Base_Start_IT(&htim7);TIM2->ARR=700-1;HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_2,(uint32_t *)sinx,120,DAC_ALIGN_12B_R);

在while(1)中将数据赋入send数组

 Keynum=Key_GetNum();switch(Keynum) //根据按下的按键修改要输出的数组{case 1:mode=1;break;case 2:mode=2;break;case 3:mode=3;break;case 4:mode=4;break;}if(mode==1) {Key[0]=0;Key[1]=0;Key[2]=0;Key[3]=1; //按下按键1时值为0001}if(mode==2) {Key[0]=0;Key[1]=0;Key[2]=1;Key[3]=1; //按下按键2时值为0011}if(mode==3) {Key[0]=0;Key[1]=1;Key[2]=1;Key[3]=1; //按下按键3时值为0111}if(mode==4) {Key[0]=1;Key[1]=1;Key[2]=1;Key[3]=1; //按下按键4时值为1111}for(uint8_t i=0;i<4;i++) //将数据赋入send{send[i]=head[i];}for(uint8_t i=0;i<4;i++){send[i+4]=Key[i];}for(uint8_t i=0;i<4;i++){send[i+8]=tail[i];}

最后写入定时器的中断回调函数

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){if(htim==&htim2){if(signal==1)TIM2->ARR=700-1; //修改正弦波频率为500HzelseTIM2->ARR=1400-1; //修改正弦波频率为1000Hz}if(htim==&htim7)//6ms{signal=send[num1];  //将数组赋值给signanum1++; if(num1 >= 12) num1 = 0; //加上包头包尾和数据共有12个码元}}

6.至此便完成了stm32的2fsk调制,输出的波形如图所示:

解调

首先进行stm32cubemx的配置:

1.打开并配置相应的时钟(不做演示)

2.打开相应的ADC,使用ADC+TIM+DMA控制,打开ADC的通道,并选择使用TIM3触发,DMA打开Circular模式:

3.配置定时器3,打开时钟源配置PSC为84-1,ARR为100-1(此时的采样频率为10Khz),最后务必打开Updata Event,否则ADC无法使用TIM3:

4.打开串口1,可以将采集到的数据打印到VOFA上便于观察:

5.配置好后进入keil:

首先对printf函数进行重定向,加上以下代码并勾上Use microLIB

#include int fputc(int ch, FILE *f){ HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff); return ch;}int fgetc(FILE *f){ uint8_t ch = 0; HAL_UART_Receive(&huart1, &ch, 1, 0xffff); return ch;}

定义ADC采集的数组(10Khz采样频率要采集至少144ms所以至少采集1440个点,但是dsp库要求采集的点为2^N,故选择2048)

uint16_t adc_buff[2048];//存放ADC采集的数据

打开定时器3

HAL_TIM_Base_Start(&htim3);

打开ADC并延时200ms(6ms一个码元,一个周期有12个码元,所以两个周期一个要144ms才能采完)保证能够采集完毕,之后关闭ADC进行数据处理

HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_buff,2048);HAL_Delay(200);HAL_ADC_Stop_DMA(&hadc1);

采集后可以串口打印给vofa看看波形,确实是有2fsk的波形

for(uint16_t i=0;i<2048;i++){printf(\"%.3f\\n\",adc_buff[i]*3.3/4096);}

接着添加DSP库,然后用matlab设计一个低通滤波器,导入代码

uint16_t BL = 46;float32_t TB[46] = {//500-1k 低通-0.00192302686307542,-0.00179186182294684,-0.000828174329093916,0.00104096891537065,0.00357678176218631,0.00619782240763762,0.00805660256598988,0.00822795688466835,0.00597902573319143,0.00105907891428945,-0.00607131593929422,-0.0141504401553351,-0.0212186454280924,-0.0249244480145768,-0.0229903049074396,-0.0137466035336783,0.00338734878600169,0.0275978052390779,0.0566387991445388,0.0870980799701347,0.114933113960468,0.136177607324765,0.147673829385213,0.147673829385213,0.136177607324765,0.114933113960468,0.0870980799701347,0.0566387991445388,0.0275978052390779,0.00338734878600169,-0.0137466035336783,-0.0229903049074396,-0.0249244480145768,-0.0212186454280924,-0.0141504401553351,-0.00607131593929422,0.00105907891428945,0.00597902573319143,0.00822795688466835,0.00805660256598988,0.00619782240763762,0.00357678176218631,0.00104096891537065,-0.000828174329093916,-0.00179186182294684,-0.00192302686307542};uint32_t blockSize = 2048; //块处理大小,即ADC采样的数据个数 float32_t pState[2093]={0.0f}; //FIR滤波器状态变量暂存:数组的大小=BL+blockSize-1 = 2048 + 46 -1 = 2093

定义fir滤波器的数组

float fir_inputbuf[2048]={0};float fir_outputbuf[2048]={0};

在主函数中初始化fir滤波器

arm_fir_instance_f32 *S;//FIR实例化结构体 S = (arm_fir_instance_f32 *)malloc(sizeof(arm_fir_instance_f32)); //开辟一个空间Sarm_fir_init_f32(S,BL,TB,pState,blockSize);

随后可在ADC采集完后调用fir滤波器

for(uint16_t i=0;i<2048;i++){fir_inputbuf[i]=(float)adc_buff[i]*3.3/4096; //将ADC采样数据化为真实值传入fir_inputbuf中等待处理}arm_fir_f32(S,fir_inputbuf,fir_outputbuf,blockSize);

随后可以串口打印在vofa上看看波形,可以看出1Khz的信号基本被滤干净了,但500Hz的信号完整,从2fsk信号变为了2ask信号

for(uint16_t i=0;i<2048;i++){printf(\"%.3f,%.3f\\n\",adc_buff[i]*3.3/4096,fir_outputbuf[i]);}

随后可将2ask信号反转使其包络只有0,1两种状态,此时信号峰值为1.6,谷值为0

for(uint16_t i=0;i<2048;i++){if(fir_outputbuf[i]<1.6)fir_outputbuf[i]=3.2-fir_outputbuf[i];fir_outputbuf[i]-=1.6;}

然后便可以进行模拟包络检波

#define V_MAX 21 //用10Khz采样频率去采500Hz波一个周期有20个点 故设置为20+1for(uint16_t i=0;i0.8)//当前值大于最大值的一半v=V_MAX ; //模拟电容充能至最大值else{if(v-1>0) v=v-1;  //电容电压衰减elsev=0; //电容能量衰减完} if (v>V_MAX/4 ) //电压大于阈值fir_outputbuf[i]=1; //为1elsefir_outputbuf[i]=0; //为0}

随后可将波形打印出来看看,可以看出除去滤波器导致的相位后移基本将方波解调出来

然后将方波处理为数组

for(uint16_t i=0;i=30) low_count++; //四舍五入low_time=0;for(int i=0;i=30) high_count++;high_time=0;for(int i=0;i<high_count;i++){recieve[num]=0;num++;}high_count=0;}flag=0;}}

随后便可判断包头包尾并赋值,然后清空数组了

for(uint8_t i=0;i<24;i++){if(recieve[i]==1 &&recieve[i+1]==0 &&recieve[i+2]==1 &&recieve[i+3]==0 &&recieve[i+8]==1 &&recieve[i+9]==0 &&recieve[i+10]==1 &&recieve[i+11]==0){led[0]=recieve[i+4];led[1]=recieve[i+5];led[2]=recieve[i+6];led[3]=recieve[i+7];}}num=0;for(uint8_t i=0;i<35;i++){recieve[i]=0;}

至此解调结束

总结

我的发送接收写在同一块stm32中,以下是我完整的main.c代码

/* USER CODE BEGIN Header *//** ****************************************************************************** * @file  : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2024 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** *//* USER CODE END Header *//* Includes ------------------------------------------------------------------*/#include \"main.h\"#include \"adc.h\"#include \"dac.h\"#include \"dma.h\"#include \"i2c.h\"#include \"tim.h\"#include \"usart.h\"#include \"gpio.h\"/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes */#include \"oled.h\"#include \"stdio.h\"#include \"key.h\"#include \"LED.h\"#include \"math.h\"#include \"arm_math.h\"#include \"arm_const_structs.h\"#include /* USER CODE END Includes *//* Private typedef -----------------------------------------------------------*//* USER CODE BEGIN PTD */uint16_t adc_buff[2048];//存放ADC采集的数据uint8_t recieve[35];uint16_t high_time,low_time;uint8_t flag,high_count,low_count,num;/* USER CODE END PTD *//* Private define ------------------------------------------------------------*//* USER CODE BEGIN PD */#define V_MAX 21uint8_t v=0;uint16_t BL = 46;float32_t TB[46] = {//500-1k 低通-0.00192302686307542,-0.00179186182294684,-0.000828174329093916,0.00104096891537065,0.00357678176218631,0.00619782240763762,0.00805660256598988,0.00822795688466835,0.00597902573319143,0.00105907891428945,-0.00607131593929422,-0.0141504401553351,-0.0212186454280924,-0.0249244480145768,-0.0229903049074396,-0.0137466035336783,0.00338734878600169,0.0275978052390779,0.0566387991445388,0.0870980799701347,0.114933113960468,0.136177607324765,0.147673829385213,0.147673829385213,0.136177607324765,0.114933113960468,0.0870980799701347,0.0566387991445388,0.0275978052390779,0.00338734878600169,-0.0137466035336783,-0.0229903049074396,-0.0249244480145768,-0.0212186454280924,-0.0141504401553351,-0.00607131593929422,0.00105907891428945,0.00597902573319143,0.00822795688466835,0.00805660256598988,0.00619782240763762,0.00357678176218631,0.00104096891537065,-0.000828174329093916,-0.00179186182294684,-0.00192302686307542};uint32_t blockSize = 2048; //块处理大小,即ADC采样的数据个数 float32_t pState[2093]={0.0f}; //FIR滤波器状态变量暂存:数组的大小=BL+blockSize-1 = 2048 + 46 -1 = 2093#define SAM_FRE 10000 //采样率float fir_inputbuf[2048]={0};float fir_outputbuf[2048]={0};uint8_t Keynum,mode,time;uint8_t head[4]={1,0,1,0};uint8_t tail[4]={1,0,1,0};uint8_t send[12];uint8_t Key[4]={1,1,1,0};uint8_t led[4]={1,1,1,1};uint16_t sinx[120];uint8_t signal;/* USER CODE END PD *//* Private macro -------------------------------------------------------------*//* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */char message[50];/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/void SystemClock_Config(void);/* USER CODE BEGIN PFP *//* USER CODE END PFP *//* Private user code ---------------------------------------------------------*//* USER CODE BEGIN 0 *//* USER CODE END 0 *//** * @brief The application entry point. * @retval int */int main(void){ /* USER CODE BEGIN 1 */for (int i = 0; i ARR=700-1;HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_2,(uint32_t *)sinx,120,DAC_ALIGN_12B_R);arm_fir_instance_f32 *S;//FIR实例化结构体 S = (arm_fir_instance_f32 *)malloc(sizeof(arm_fir_instance_f32)); //开辟一个空间Sarm_fir_init_f32(S,BL,TB,pState,blockSize);OLED_Init();OLED_NewFrame();OLED_PrintString(0,0,\"Init Sucess\",&font16x16,OLED_COLOR_NORMAL);OLED_ShowFrame();HAL_Delay(500);//保证模块先上电OLED_NewFrame(); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) {OLED_NewFrame();Keynum=Key_GetNum();switch(Keynum){case 1:mode=1;break;case 2:mode=2;break;case 3:mode=3;break;case 4:mode=4;break;}if(mode==1) {Key[0]=0;Key[1]=0;Key[2]=0;Key[3]=1;}if(mode==2) {Key[0]=0;Key[1]=0;Key[2]=1;Key[3]=1;}if(mode==3) {Key[0]=0;Key[1]=1;Key[2]=1;Key[3]=1;}if(mode==4) {Key[0]=1;Key[1]=1;Key[2]=1;Key[3]=1;}for(uint8_t i=0;i<4;i++){send[i]=head[i];}for(uint8_t i=0;i<4;i++){send[i+4]=Key[i];}for(uint8_t i=0;i<4;i++){send[i+8]=tail[i];}HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_buff,2048);HAL_Delay(200);HAL_ADC_Stop_DMA(&hadc1);for(uint16_t i=0;i<2048;i++){fir_inputbuf[i]=(float)adc_buff[i]*3.3/4096; //将ADC采样数据化为真实值传入fir_inputbuf中等待处理}arm_fir_f32(S,fir_inputbuf,fir_outputbuf,blockSize);for(uint16_t i=0;i<2048;i++){if(fir_outputbuf[i]<1.6)fir_outputbuf[i]=3.2-fir_outputbuf[i];fir_outputbuf[i]-=1.6;}for(uint16_t i=0;i0.8)//假设绝对值信号最大值为1,当前值大于最大值的一半v=V_MAX ;//电容充能至最大值else{if((v-1)>0)//防止减成负数v=v-1;//相当于电容电压衰减elsev=0;//电容能量衰减完后电压持续为0} if (v>V_MAX/4 )//电压大于阈值fir_outputbuf[i]=1;//为逻辑真elsefir_outputbuf[i]=0;//为逻辑假}for(uint16_t i=0;i=30) low_count++;low_time=0;for(int i=0;i=30) high_count++;high_time=0;for(int i=0;i<high_count;i++){recieve[num]=0;num++;}high_count=0;}flag=0;}}for(uint8_t i=0;i<24;i++){if(recieve[i]==1 &&recieve[i+1]==0 &&recieve[i+2]==1 &&recieve[i+3]==0 &&recieve[i+8]==1 &&recieve[i+9]==0 &&recieve[i+10]==1 &&recieve[i+11]==0){led[0]=recieve[i+4];led[1]=recieve[i+5];led[2]=recieve[i+6];led[3]=recieve[i+7];}}num=0;for(uint8_t i=0;iARR=700-1;elseTIM2->ARR=1400-1;}if(htim==&htim4){Key_Loop();}if(htim==&htim7)//6ms{signal=send[num1];num1++; if(num1 >= 12) num1 = 0; }}/* USER CODE END 4 *//** * @brief This function is executed in case of error occurrence. * @retval None */void Error_Handler(void){ /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { } /* USER CODE END Error_Handler_Debug */}#ifdef USE_FULL_ASSERT/** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */void assert_failed(uint8_t *file, uint32_t line){ /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf(\"Wrong parameters value: file %s on line %d\\r\\n\", file, line) */ /* USER CODE END 6 */}#endif /* USE_FULL_ASSERT */

完整工程:

通过网盘分享的文件:2fsk.zip
链接: https://pan.baidu.com/s/1W3_B-3iy5g9qWLuDGS3AqA?pwd=3663 提取码: 3663

stm32添加DSP库

在manage run-time environment中把dsp勾上

然后在C/C++中的define加入(F4为CM4 F1为CM1 H7为CM7)

,ARM_MATH_CM4

并且忽略特点警告

--diag_suppress=2803,1,1035

在主函数中加入头文件

#include \"arm_math.h\"#include \"arm_const_structs.h\"

至此DSP库添加完毕

matlab设计fir滤波器

打开matlab,在命令行中输入fdatool,进入滤波器设计器(Fs为采样频率,Fpass为截止频率,Fstop为截止到Astop时的频率),这就是一个45阶的fir低通滤波器

设计完成后点击文件->导出->导出,进入工作区将num复制下来粘贴到代码中

至此设计完毕

火车头伪原创插件