一、项目需求
-
通过板上的高速DAC配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
- 生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
- 在OLED上显示当前波形的形状、波形的频率以及幅度
- 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
二、设计思路
1.硬件和编译环境
本项目用到了Lattice的ICE40UP5K FPGA和STM32G031 MCU,以及与stm32连接的按键,旋转编码器和OLED显示屏,与FPGA连接的高速DAC。主要是用stm32CubeMX,keil5和Radiant作为开发环境。
2.设计框图
如图所示,按键与旋转编码器与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, &, 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
四、结果及现象
波形如下图:
资源占用:
值得改进的部分:
- 旋转编码器判断旋转方向的方法比较简单粗暴,实际的输入会有点不稳定。之后可以尝试定时器编码器模式进行解码。
- 波信息总体输入需要输入所有信息后才进行传递信号使产生波形,之后可以尝试各种信息可以实时输入。
- 最终的波形效果不是特别好,在高频情况会出现一些失真,可以尝试一些滤波算法提高精度。
五、心得体会
这个项目总体来说比较简单,但实际在做的过程中出了很多的问题,各种各样的问题,也由于水平有限,很多地方代码编写的都比较简单,也使得最后结果有很多不完美的地方。
通过这次活动,我觉得最大的收获是锻炼到了我解决问题的能力,从网上找相关资料,和通过一步一步调试代码,确定问题出在哪里,然后进行修改。stm32和FPGA通信这部分我是最后做的,本来觉得挺简单,但意外卡了很久,当时不知道问题出在STM32部分还是FPGA部分,利用调试灯和仿真调了很久慢慢缩小范围,最后成功通信。
基于本次项目的不足,之后我还会继续完善,多加学习。