1 项目要求
-
通过板上的高速DAC(10bits/最高125Msps)配合FPGA内部DDS的逻辑(最高48Msps),生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
- 生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
- 在OLED上显示当前波形的形状、波形的频率以及幅度
- 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
2 完成的功能介绍
2.1 OLED屏的驱动
先上图
屏上显示了波形(wave)、频率(freq)、调整时的步长(step)、幅值(ampl),这个步长对于频率和幅值都是有效的,在调节的量名称前面有一个小圆圈,这个小圆圈在哪个名称前面就表示正在调整哪个量。
2.2 按键和旋转编码器
左边的按键调可选择调整哪个量,右边按键调整值,默认增;旋转编码器调整值,顺时针转是增,逆时针转是减。
2.3 fpg控制dac出波
可发生三种波形(正弦、方波、三角波),频率范围DC-5MHz,精度1Hz,幅度范围0.1-1Vpp
3 整体思路
整个项目可以分成以下几部分:
- stm32通过SPI驱动OLED
- stm32实现按键、旋转编码器对参数的调整
- stm32与fpga通过SPI实现通信,以传输各参数
- fpga控制DAC出波
4 实现过程
4.1 实现思路框图
4.2 OLED驱动
为实现SPI驱动OLED并显示信息,项目中移植了u8g2图形库。移植最重要的就是初始化以下两个函数。
uint8_t u8x8_stm32_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
uint8_t u8x8_byte_stm32_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
由于项目只需要单片机到OLED的单项通讯,因此配置好发送数据的部分是最关键的。
case U8X8_MSG_BYTE_SEND:
/* Insert codes to transmit data */
if(HAL_SPI_Transmit(&hspi2, arg_ptr, arg_int, TX_TIMEOUT) != HAL_OK) return 0;
break;
使用的是硬件SPI驱动。
之后就可以顺利发送数据了,我用的最多的就是DrawStr
u8g2_DrawStr(&u8g2, 10, 26, "freq");
4.3 按键与旋转编码器状态获取
本项目中给按键和Encoder_A脚、Encoder_B脚都配置了中断,当遇到下降沿时进行参数的变化操作。
编码器这里进行了一个消抖操作,就是当单片机检测到了连续两次的某个引脚的下降沿,参数才能够变化,这样可以防止编码器出现往同一个方向旋转时连着增一下又减一下的情况。
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case SW2_Pin :
line=(line+1)%4;
break;
case SW1_Pin:
value_change(line,0);
break;
case Encoder_A_Pin://逆时针则减
if (!HAL_GPIO_ReadPin(Encoder_A_GPIO_Port, Encoder_A_Pin)){
if (!HAL_GPIO_ReadPin(Encoder_B_GPIO_Port, Encoder_B_Pin) && (!ifchange) && left){
b=2;
right=0;
}
else if (!left){
left++;
}
}
break;
case Encoder_B_Pin://顺时针则增
if (!HAL_GPIO_ReadPin(Encoder_B_GPIO_Port, Encoder_B_Pin)){
if (!HAL_GPIO_ReadPin(Encoder_A_GPIO_Port, Encoder_A_Pin) && (!ifchange) && right){
b=1;
left=0;
}
else if (!right){
right++;
}
}
break;
}
}
参数改变的函数都比较简单,这里就不赘述了。
4.4 STM32与FPGA的通信
由于本项目做的是一个任意波形发生器,只需要单片机把使用者设置的参数给FPGA就好了,所以只需要做从STM32向FPGA的单向SPI通信。我把STM32设定为主机,FPGA为从机。这和前面驱动OLED就有一些相似之处了,这里也用的硬件驱动。值得注意的是,单片机的时钟最高可以到64MHz,而FPGA的时钟只有12MHz,所以要想FPGA能够接收到稳定的、准确的数据信息,SPI设置时得有一个分频的设置(这里整整困了我一晚上,悲伤)。下面放一些比较关键的设置。
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;
然后就是复杂的FPGA接收数据的代码的编写。这里就真的很需要看懂SPI的时序图了,因为必须得用软件模拟SPI。iCE40好像板子上有硬件SPI来着,但似乎是用于做虚拟U盘了。
因为我在STM32里设置的时钟极性是HIGH,所以要接收数据,首先要捕捉时钟的上升沿(即下图sck_p)。
reg sck_r0,sck_r1;
wire sck_n,sck_p;
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
sck_r0 <= 1'b0; //sck of the idle state is high
sck_r1 <= 1'b0;
end
else
begin
sck_r0 <= SCK;
sck_r1 <= sck_r0;
end
assign sck_n = (~sck_r0 & sck_r1)? 1'b1:1'b0; //capture the sck negedge
assign sck_p = (~sck_r1 & sck_r0)? 1'b1:1'b0; //capture the sck posedge
当检测到时钟上升沿,同时片选信号为低电平时,就可以逐位获取数据了。要传递的参数有很多个,所以我是依次发送,在FPGA代码里加入计数cnt,然后把数据传到对应的数组里。
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
rxd_data <= 1'b0;
rxd_flag_r <= 1'b1;
rxd_state <= 1'b0;
//store<=1'b0;
cnt<=3'd4;
end
else if(sck_p && !CS_N)
begin
case(rxd_state)
3'd0:begin
rxd_data[7] <= MOSI;
rxd_flag_r <= 1'b0; //reset rxd_flag
rxd_state <= 3'd1;
end
3'd1:begin
rxd_data[6] <= MOSI;
rxd_state <= 3'd2;
end
3'd2:begin
rxd_data[5] <= MOSI;
rxd_state <= 3'd3;
end
3'd3:begin
rxd_data[4] <= MOSI;
rxd_state <= 3'd4;
end
3'd4:begin
rxd_data[3] <= MOSI;
rxd_state <= 3'd5;
end
3'd5:begin
rxd_data[2] <= MOSI;
rxd_state <= 3'd6;
end
3'd6:begin
rxd_data[1] <= MOSI;
rxd_state <= 3'd7;
end
3'd7:begin
rxd_data[0] <= MOSI;
rxd_flag_r <= 1'b1; //set rxd_flag
rxd_state <= 3'd0;
if (cnt==3'd4)
cnt<=3'd0;
else
cnt<=cnt+1'b1;
end
default: ;
endcase
end
end
//存下数据
always@(posedge clk or negedge rst_n)
if (!rst_n)
begin
wave<=8'd0;
freq<=24'd0;
ampl<=16'd0;
store<=1'b0;
end
else if (rxd_flag)
begin
case(cnt)
3'd0:begin
wave<=rxd_data;
end
3'd1:begin
freq[7:0]<=rxd_data;
end
3'd2:begin
freq[15:8]<=rxd_data;
end
3'd3:begin
freq[23:16]<=rxd_data;
end
3'd4:begin
ampl<=rxd_data;
end
default: ;
endcase
end
4.5 FPGA控制DAC出波
在这一环节,本项目使用了ROM IP核,根据radient的手册,我先用软件生成了波形数据文件,又将其转换成了.mem格式,删除了多余的参数和字母。参数设置如下图。
之后就是DDS.v的编写,通过相位步进来在IP核中寻址,进行波形选择和幅度调整。最后实例化IP核即可。
//波形选择
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dac_data <= 10'd0;
end
else begin
case(wave_c)
8'd0:dac_data <=dac_data0/10'd25*amplitude;//正弦波
8'd1:dac_data <=dac_data1/10'd25*amplitude;//三角波
8'd2:dac_data <=dac_data2/10'd25*amplitude;//方波
default:dac_data <= dac_data0;
endcase
end
end
//相位累加器
reg [31:0] fre_acc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
fre_acc <= 0;
end
else begin
fre_acc <= fre_acc + tmp_freq;
end
end
//生成查找表地址
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
addr <= 0;
end
else begin
addr <= fre_acc[31:22] + p_word;
end
end
//正弦波IP核实例化
sine inst_sin_rom (
.rd_clk_i(clk) ,
.rst_i(!rst_n) ,
.rd_en_i (1'b1),
.rd_clk_en_i (1'b1),
.rd_addr_i (addr) ,
.rd_data_o (dac_data0)
);
由数据手册上的时序图可以知道,DAC的时钟和FPGA本身的时钟设置相反即可。
assign dac_sclk=~clk;
5 困难和还可以优化的问题
- 按键、编码器的消抖。本项目里做的消抖不多,因为我试验的时候发现只要用的时候不着急,按键和编码器不会有问题,但像我在视频里那样一个手拿手机录视频,又很紧张,这是如果控制不好力度就容易出问题,所以还需要更高明的消抖办法。
- 输出波形的幅度精度不高。这个DAC的正常出波是2.5Vpp(就是不对幅度有任何加减,直接输出),项目里需要0.1-1Vpp就需要进行一些除法,会导致波形毛刺比较多。我还没有想到更好的办法解决。
- 输出波的形状有限,还可以开发更多样的波形。
6 未来计划和建议
- 对于我个人,这是我的第一块板子,一定会好好珍惜的!准备再把其他题目做一做,希望能有更多这样有趣的活动!
- 建议可以多放一些例程和板卡的信息。
- 模拟U盘真的很方便!希望单片机也可以搞个类似的,按boot按的手疼hhh
代码链接:https://pan.baidu.com/s/15xu3u5NlYrvl4242E68WYA
提取码:woex