基于STM32与FPGA实现本地控制DDS任意波形发生器
本项目是在2023年寒假在家一起练的电赛训练平台实现的项目3,基于STM32与FPGA实现本地控制DDS任意波形发生器。
标签
STM32
FPGA
DDS
2023寒假在家练
山与风
更新2023-03-28
成都大学
773

一、项目需求

  1. 通过板上的高速DAC配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
  2. 生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
  3. 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
  4. 在OLED上显示当前波形的形状、波形的频率以及幅度
  5. 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节

二、设计思路

     1.硬件和编译环境

      本项目用到了Lattice的ICE40UP5K FPGA和STM32G031 MCU,以及与stm32连接的按键,旋转编码器和OLED显示屏,与FPGA连接的高速DAC。主要是用stm32CubeMX,keil5和Radiant作为开发环境。

     2.设计框图

FrREMqELr8eEHdzTWKPoH46Oq6Nb

       如图所示,按键与旋转编码器与stm32连接,实现波信号的输入,OLED屏幕进行信息的显示,FPGA与高速DAC相连接,实现波信号的发生。stm32与FPGA则通过spi相连接实现信号的传输。

三、代码实现

     1.按键输入部分

        这里一共用到两个按键和旋转编码器,由于按键资源比较少,所以在设计的时候尽量让每个按键功能单一且分隔开一点。这里主要需要输入的信息有波形号的频率,幅度,波形,频率的范围是0到5000000HZ,精度是1HZ,幅度的范围是0到1V,精度是0.1V,波形的选择则有三角波,正弦波和方波。按键1负责对波形号的幅度,频率,波形进行模式切换,按键2负责选中频率的各个位,旋转编码器则负责对频率,幅度进行数字的输入,并选择波形。当按键1按到第四下的时候,32部分将所有的数据一并送给FPGA,并显示波形。

  按键1:

	if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_12)==0){
						      key1_num++;

 按键2:

 if(GPIO_Pin == KEY2_Pin)//第二层按键频率位选择
	{
			 delay(10);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11)==0&&key1_num==2){
					 num[2]=0;
						if(wei>=7)
							wei=0;
						else 
							wei++;
			}	

旋转编码器:

if(GPIO_Pin == A_state_Pin)
	{	
			HAL_TIM_Base_Start_IT(&htim2);
			if((wei==7&&num[2]>=5)||num[2]>=9) num[2]=0;
			if(num[3]>=3) num[3]=0;
			if(num[1]>=10) num[1]=0;
			
							if(flag==1&&num[key1_num]>=1)
							{
									num[key1_num]--;
							}
							else if(flag==2)
							{
								num[key1_num]++;
							 }
				      switch(key1_num){
									   case 1:amp=num[1];	break;										          
									   case 2:freq[wei]=num[2];
				                    freq_sum=freq[7]*1000000+freq[6]*100000+freq[5]*10000+freq[4]*1000+freq[3]*100+freq[2]*10+freq[1];	
							            	data1=freq[2]*10+freq[1];//将要发送的频率数据在结束按键2之后做一个整合
								            data2=freq[4]*10+freq[3];
								            data3=freq[6]*10+freq[5];
								            data4=freq[7];
									            break;
									   case 3:wave=num[3];										             
								            break;
                     default: break;								
									}
						flag=0;					 
							 
    }	

       关于消抖,两个按键消抖比较简单,直接用了一个自定义的延时函数,旋转编码器的消抖则是用了一个定时器,并在定时器中断中判断旋转的方向。这里旋转编码器的顺逆方向主要是通过AB端产生低电平信号的时间先后来判断的。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim==(&htim2))
  { 
       if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)==0)
			 {
				  if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1)==0)
					 flag=1;
                  else	
                     flag=2; 		 					 
		 }
    	 HAL_TIM_Base_Stop_IT(&htim2);
  }   
} 

2.OLED界面

   OLED部分我是直接用的现成的驱动代码,稍微修改了一点,配置spi2中的MOSI和sck将数据发送出去,软件模拟片选信号。界面的话加入了下滑线,使更好定位按键输入所在位置。

void show(void)
{
           OLED_ShowString(8,32,(uint8_t *)"WARE:",16,1);
		   OLED_ShowString(8,0,(uint8_t *)"AMP:",16,1);
		   OLED_ShowString(8,16,(uint8_t *)"freq:",16,1);
	
		   OLED_ShowNum(40,0,amp/10,1,16,1);
           OLED_ShowString(48,0,(uint8_t *)".",16,1);			 
		   OLED_ShowNum(54,0,amp%10,1,16,1);
		   OLED_ShowString(104,0,(uint8_t *)"V",16,1);			 
		   OLED_ShowNum(48,16,freq_sum,7,16,1);
		   OLED_ShowString(104,16,(uint8_t *)"HZ",16,1);
		   switch(wave){
		     case 0:OLED_ShowString(56,32,(uint8_t *)"DC      ",16,1);break;
			 case 1:OLED_ShowString(56,32,(uint8_t *)"square  ",16,1);break;
	         case 2:OLED_ShowString(56,32,(uint8_t *)"triangle",16,1);break;
			 case 3:OLED_ShowString(56,32,(uint8_t *)"sine    ",16,1);break;
		     default:break;
			 }			 	     
       switch(key1_num){
			   case 1:OLED_DrawLine(8,15,31,15,1);break;
			   case 2:{OLED_DrawLine(8,31,39,31,1);OLED_DrawLine(8,15,31,15,0);break;}
			   case 3:{OLED_DrawLine(8,31,39,31,0);OLED_DrawLine(8,47,39,47,1);break;}			   
		       default:{OLED_DrawLine(8,15,31,15,0);OLED_DrawLine(8,31,39,31,0);OLED_DrawLine(8,47,39,47,0);break;}
			 }	
	     if(key1_num==2){
			    switch(wei){
							 case 7:{OLED_DrawLine(56,31,63,31,0);OLED_DrawLine(48,31,55,31,1);break;}
							 case 6:{OLED_DrawLine(64,31,71,31,0);OLED_DrawLine(56,31,63,31,1);break;}
							 case 5:{OLED_DrawLine(72,31,79,31,0);OLED_DrawLine(64,31,71,31,1);break;}
                             case 4:{OLED_DrawLine(80,31,87,31,0);OLED_DrawLine(72,31,79,31,1);break;}
                             case 3:{OLED_DrawLine(88,31,95,31,0);OLED_DrawLine(80,31,87,31,1);break;}
                             case 2:{OLED_DrawLine(96,31,103,31,0);OLED_DrawLine(88,31,95,31,1);break;}
                             case 1:{OLED_DrawLine(96,31,103,31,1);break;} 
					         default:{OLED_DrawLine(48,31,55,31,0);break;}
						 }	
		 }
			 
       OLED_Refresh();
}

3.stm32与FPGA通信

     stm32部分我也是用OLED屏spi通信的原理,利用软件控制片选,配置spi2中的MOSI和sck将数据发送出去。将波信息分成6个字节依次发送,为了对齐这里将幅度和波的数据各设置成一个字节,频率数据分成4个字节。

	 if(key1_num==4)
			{											
			//一次性发送6位数据
	        spi1_cs_set();//幅度数据  	
		    HAL_SPI_Transmit(&hspi1, &amp, 1, 1000);
			spi1_cs_res();

			spi1_cs_set();//波形
			HAL_SPI_Transmit(&hspi1, &wave, 1, 1000);
			spi1_cs_res();
													
			spi1_cs_set();//频率数据1
			HAL_SPI_Transmit(&hspi1, &data1, 1, 1000);
			spi1_cs_res();
													
			spi1_cs_set();//频率数据2
			HAL_SPI_Transmit(&hspi1, &data2, 1, 1000); 
			spi1_cs_res();
													
			spi1_cs_set();//频率数据3
			HAL_SPI_Transmit(&hspi1, &data3, 1, 1000);
			spi1_cs_res();
													
			spi1_cs_set();//频率数据4
			HAL_SPI_Transmit(&hspi1, &data4, 1, 1000);
			spi1_cs_res();																																		
			key1_num=0;												
												 }						

        FPGA部分我参考了电子森林中的代码https://www.eetree.cn/wiki/spi_verilog,对接收的数据进行处理。调整stm32部分的SCK速率,使和FPGA这边12M的时钟频率相匹配,通过测试,stm32主机的spi大概1000kBits/s时FPGA接收到的数据比较稳定。

module stm_fpga_spi(clk, SCK, MOSI, SSEL,freq,ware, amp,LED1);
input clk;
input SCK, SSEL, MOSI;

output reg LED1;
output reg [7:0] amp;
output reg [7:0] ware;
output reg [23:0]freq;  

// sync SCK to the FPGA clock using a 3-bits shift register
reg [2:0] SCKr;  always @(posedge clk) SCKr <= {SCKr[1:0], SCK};
wire SCK_risingedge = (SCKr[2:1]==2'b01);  // now we can detect SCK rising edges
wire SCK_fallingedge = (SCKr[2:1]==2'b10);  // and falling edges
 
// same thing for SSEL
reg [2:0] SSELr;  always @(posedge clk) SSELr <= {SSELr[1:0], SSEL};
wire SSEL_active = ~SSELr[1];  // SSEL is active low
wire SSEL_startmessage = (SSELr[2:1]==2'b10);  // message starts at falling edge
wire SSEL_endmessage = (SSELr[2:1]==2'b01);  // message stops at rising edge
 
// and for MOSI
reg [1:0] MOSIr;  always @(posedge clk) MOSIr <= {MOSIr[0], MOSI};
wire MOSI_data = MOSIr[1];

//接收部分
// we handle SPI in 8-bits format, so we need a 3 bits counter to count the bits as they come in
reg [2:0] bitcnt;

reg byte_received;  // high when a byte has been received
reg [7:0] byte_data_received;
 
always @(posedge clk)
begin
  if(~SSEL_active)
    bitcnt <= 3'b000;
  else
  if(SCK_risingedge)//上升沿采样
  begin
    bitcnt <= bitcnt + 3'b001;
 
    // implement a shift-left register (since we receive the data MSB first)
    byte_data_received <= {byte_data_received[6:0], MOSI_data};
  end
end

//表示已经接收完成
always @(posedge clk) 
	byte_received <= SSEL_active && SCK_risingedge && (bitcnt==3'b111);
	
//用count来计数传的6组数据
reg [7:0] count;
always @(posedge clk) begin
	if(byte_received)begin
		if(count>=5)begin
		   count<=0;		   
	       end
		else
		count<=count+8'h1;  // count the messages
    end
end		
 reg [31:0] freq_temp;
 reg [7:0]ware_temp;
 reg [7:0]amp_temp;
 reg flag;
 //分成六个状态,分别接收不同的数据
always @(posedge clk)   
	if(byte_received) begin 
		case(count)
		   0:amp_temp= byte_data_received;
	       1:ware_temp= byte_data_received;
		   2:freq_temp[7:0]= byte_data_received;
		   3:freq_temp[15:8]= byte_data_received;
		   4:freq_temp[23:16]= byte_data_received;
		   5:begin freq_temp[31:24]= byte_data_received;end 			       				   
		   default:begin  ware_temp =0;     
			              amp_temp  =0;
						   freq_temp =0;                       				   
			   end
        endcase 
end		
//当count=5时将接收到的数据做整合
always @(posedge clk)   
	if(byte_received) begin 
     if(count==5)begin
		freq<=freq_temp[7:0]+freq_temp[15:8]*100+freq_temp[23:16]*10000+freq_temp[31:24]*1000000;
		ware<=ware_temp;
		amp<=amp_temp;
	end
	end	
 endmodule

4.DDS部分

     这部分我参考了电子森林的关于DDS的资料https://www.eetree.cn/wiki/dds_verilog和其他一些资料,首先通过PLL将时钟频率提高到120MHZ,这里是直接使用Radiant软件自带的IP核。这里采用了41位的相位累加器,可以达到0.0000546HZ的精度。将传过来的频率数据乘18325,则使精度变为1HZ,实现频率可调。

wire            clk_120M;
pll_120M u_pll_120(
        .ref_clk_i (clk),
		.rst_n_i(rst_n),
		.outcore_o(clk_120M),
        .outglobal_o()		
    );
   	reg [40:0] phase_add;
    always @(posedge clk_120M or negedge rst_n) begin
        if(!rst_n) begin
            phase_add <=0;
        end
        else
            phase_add <= phase_add + freq_input_dds*18325;
    end

        方波,三角波的实现:

	assign  square_data = phase_add[40] ? 10'd1023:10'd0;
	assign  triangle_data=phase_add[40] ? ~phase_add[39:30]:phase_add[39:30];
	assign  DC=10'h3FF;

      正弦波的实现,主要是利用了查找表。我是利用MATLAB生成波形信息,保留四分之一的波形数据,利用对称性实现整个波形的映射。

lookup_tables u_lookup_tables(.phase(phase_add[40:31]),.sin_out(sin_data));

 

assign sin_out = sine_onecycle_amp[9:0];
assign sel = phase[9:8];
sin_table u_sin_table(address,sine_table_out);
 
always @(sel or sine_table_out)
begin
	case(sel)
	2'b00: 	begin
			sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];
			address = phase[7:0];
	     	end
  	2'b01: 	begin
			sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];
			address = ~phase[7:0];
	     	end
  	2'b10: 	begin
			sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];
			address = phase[7:0];
     		end
  	2'b11: 	begin
			sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];
			address = ~ phase[7:0];
     		end
	endcase
end

四、结果及现象

波形如下图:

FnVsFeH3kwGWoBDBgONkrE5VHn5pFg_Z1waoUtn1bKMfArY9KTDQ_6tjFsOkKUIpDI3yu3bkL6ZYrkIAD_o7

 

资源占用:

FitolL6K79W4umvEtp1iOsKtr3Yk

值得改进的部分:

  • 旋转编码器判断旋转方向的方法比较简单粗暴,实际的输入会有点不稳定。之后可以尝试定时器编码器模式进行解码。
  • 波信息总体输入需要输入所有信息后才进行传递信号使产生波形,之后可以尝试各种信息可以实时输入。
  • 最终的波形效果不是特别好,在高频情况会出现一些失真,可以尝试一些滤波算法提高精度。

五、心得体会

      这个项目总体来说比较简单,但实际在做的过程中出了很多的问题,各种各样的问题,也由于水平有限,很多地方代码编写的都比较简单,也使得最后结果有很多不完美的地方。 

      通过这次活动,我觉得最大的收获是锻炼到了我解决问题的能力,从网上找相关资料,和通过一步一步调试代码,确定问题出在哪里,然后进行修改。stm32和FPGA通信这部分我是最后做的,本来觉得挺简单,但意外卡了很久,当时不知道问题出在STM32部分还是FPGA部分,利用调试灯和仿真调了很久慢慢缩小范围,最后成功通信。

      基于本次项目的不足,之后我还会继续完善,多加学习。

    

附件下载
my_dds1.zip
32:链接:https://pan.baidu.com/s/1u72eOnxGSbcvt1xzjJ_E6w 提取码:oh1i
团队介绍
成都大学 张岚华
团队成员
山与风
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号