一、项目具体要求
-
通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调、幅度可调的波形
- 生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
- 通过UART同PC连接,在PC上可以使用Matlab、Labview或其它调试工具来控制波形的切换、参数的改变
二、设计思路
通过MCU、FPGA外设(旋转编码器、按键)调整波形参数(波形、频率、幅值),单片机将波形频率控制字通过SPI总线发送给FPGA,同时将波形频率及频率控制字显示在OLED屏幕上。FPGA根据MCU发送的数据以及外部按键的输入,结合DDS原理,控制DAC并行数模转换芯片,从而输出符合要求的波形。
三、开发工具配置
1、FPGA开发工具使用及下载
使用LATTICE Radiant开发FPGA程序,需要注意工程路径不允许存在中文。
(1)综合分析
(2)分配管脚
(3)设置输出文件格式为rbt
(4)下载方式
2、MCU开发工具STM32CubeMX
3、MCU下载工具STM32CubeProgrammer
按住BOOT0(将此管脚拉高),再上电,连接USART
打开HEX文件,下载
四、程序实现
1、MCU功能:OLED显示、按键及旋转编码器检测、串口通信、SPI通信
(1)OLED初始化
//OLED初始化
void OLED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin : oled_Pin */
GPIO_InitStruct.Pin = SCL_Pin|SDA_Pin|RES_Pin|DC_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(SCL_GPIO_Port, &GPIO_InitStruct);
HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin|SDA_Pin|RES_Pin|DC_Pin, GPIO_PIN_SET);
OLED_RES_Clr();
HAL_Delay(200);
OLED_RES_Set();
OLED_WR_Byte(0xAE,OLED_CMD);//--turn off oled panel
OLED_WR_Byte(0x00,OLED_CMD);//---set low column address
OLED_WR_Byte(0x10,OLED_CMD);//---set high column address
OLED_WR_Byte(0x40,OLED_CMD);//--set start line address Set Mapping RAM Display Start Line (0x00~0x3F)
OLED_WR_Byte(0x81,OLED_CMD);//--set contrast control register
OLED_WR_Byte(0xCF,OLED_CMD);// Set SEG Output Current Brightness
OLED_WR_Byte(0xA1,OLED_CMD);//--Set SEG/Column Mapping 0xa0左右反置 0xa1正常
OLED_WR_Byte(0xC8,OLED_CMD);//Set COM/Row Scan Direction 0xc0上下反置 0xc8正常
OLED_WR_Byte(0xA6,OLED_CMD);//--set normal display
OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
OLED_WR_Byte(0x3f,OLED_CMD);//--1/64 duty
OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
OLED_WR_Byte(0x00,OLED_CMD);//-not offset
OLED_WR_Byte(0xd5,OLED_CMD);//--set display clock divide ratio/oscillator frequency
OLED_WR_Byte(0x80,OLED_CMD);//--set divide ratio, Set Clock as 100 Frames/Sec
OLED_WR_Byte(0xD9,OLED_CMD);//--set pre-charge period
OLED_WR_Byte(0xF1,OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
OLED_WR_Byte(0xDA,OLED_CMD);//--set com pins hardware configuration
OLED_WR_Byte(0x12,OLED_CMD);
OLED_WR_Byte(0xDB,OLED_CMD);//--set vcomh
OLED_WR_Byte(0x40,OLED_CMD);//Set VCOM Deselect Level
OLED_WR_Byte(0x20,OLED_CMD);//-Set Page Addressing Mode (0x00/0x01/0x02)
OLED_WR_Byte(0x02,OLED_CMD);//
OLED_WR_Byte(0x8D,OLED_CMD);//--set Charge Pump enable/disable
OLED_WR_Byte(0x14,OLED_CMD);//--set(0x10) disable
OLED_WR_Byte(0xA4,OLED_CMD);// Disable Entire Display On (0xa4/0xa5)
OLED_WR_Byte(0xA6,OLED_CMD);// Disable Inverse Display On (0xa6/a7)
OLED_WR_Byte(0xAF,OLED_CMD);
OLED_Clear();
OLED_WR_Byte(0xAF,OLED_CMD);
}
(2)外部中断初始化EXTI_Init,设置中断线优先级
//中断线0-PB0
HAL_NVIC_SetPriority(EXTI0_1_IRQn,1,0); //抢占优先级为1,子优先级为0
HAL_NVIC_EnableIRQ(EXTI0_1_IRQn); //使能中断线2
//中断线11-PA11
HAL_NVIC_SetPriority(EXTI4_15_IRQn,2,0); //抢占优先级为2,子优先级为0
HAL_NVIC_EnableIRQ(EXTI4_15_IRQn); //使能中断线2
HAL库中可以通过HAL_NVIC_SetPriority函数来设置中断的优先级,决定中断是否能够被抢占。第一个参数为要设置的中断号,第二个参数为抢占优先级,有0~3个四个等级,值越小表示的优先级越高,即抢占优先级0的中断的优先级高于抢占优先级1的中断。第三个参数为响应优先级,从底层代码注释可以看出对于Cortex M0+的产品不支持该参数,该参数不用设置。
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(SubPriority);
/* Check the parameters */
assert_param(IS_NVIC_PREEMPTION_PRIORITY(PreemptPriority));
NVIC_SetPriority(IRQn, PreemptPriority);
}
中断服务函数EXTI0_1_IRQHandler处理PB0的外部中断
中断服务函数EXTI4_15_IRQHandler处理PA11、PA12、PC6的外部中断
中断服务函数会调用HAL_GPIO_EXTI_IRQHandler,在其中处理上升沿中断和下降沿中断
在上升沿中断和下降沿中断中检测按键及旋转编码器,修改波形参数并将同步更新到OLED
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case key1_Pin: //复位按键
{
freq = 0; //正弦波频率
freq_step = 0; //正弦波调节步进,送到fpga
step_flag = 0; //正弦波步进标志,为0步进10Hz,为1步进100Hz
OLED_Clear1();
SetCursor(40,16);
printf("%.1fkHz",freq/1000.0);
SetCursor(40,32);
printf("1Hz ");
//SetCursor(0,48);
//printf("%10d",freq_step);
OLED_Refresh();
break;
}
case key2_Pin: //切换频率步进按键
{
switch(step_flag)
{
case 0: step_flag = 1; //10Hz步进
SetCursor(40,32);
printf("10Hz ");
break;
case 1: step_flag = 2; //100Hz步进
SetCursor(40,32);
printf("100Hz ");
break;
case 2: step_flag = 3; //1kHz步进
SetCursor(40,32);
printf("1kHz ");
break;
case 3: step_flag = 4; //10kHz步进
SetCursor(40,32);
printf("10kHz ");
break;
case 4: step_flag = 5; //100kHz步进
SetCursor(40,32);
printf("100kHz");
break;
case 5: step_flag = 6; //1MHz步进
SetCursor(40,32);
printf("1MHz ");
break;
case 6: step_flag = 0; //10Hz步进
SetCursor(40,32);
printf("1Hz ");
break;
default: step_flag = 0; //10Hz步进
SetCursor(40,32);
printf("1Hz ");
break;
}
OLED_Refresh();
break;
}
case encoderA_Pin: //编码器旋钮,调节频率
{
if(HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A下降沿,B高电平,顺时针
{
switch(step_flag)
{
case 0: freq += 1; break;
case 1: freq += 10; break;
case 2: freq += 100; break;
case 3: freq += 1000; break;
case 4: freq += 10000; break;
case 5: freq += 100000; break;
case 6: freq += 1000000; break;
default: freq += 1; break;
}
if(freq>10000000) freq=10000000;
freq_step = freq*71.5827883;
}
else if(!HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A下降沿,B低电平,逆时针
{
switch(step_flag)
{
case 0: freq -= 1; break;
case 1: freq -= 10; break;
case 2: freq -= 100; break;
case 3: freq -= 1000; break;
case 4: freq -= 10000; break;
case 5: freq -= 100000; break;
case 6: freq -= 1000000; break;
default: freq -= 1; break;
}
if(freq>10000000) freq=0;
freq_step = freq*71.5827883;
}
OLED_Clear1();
SetCursor(40,16);
if(freq<1000000)
{
printf("%.3fkHz",freq/1000.0);
}
else
{
printf("%.6fMHz",freq/1000000.0);
}
//SetCursor(0,48);
//printf("%10d",freq_step);
//OLED_Refresh();
break;
}
case encoder_key_Pin: //编码器按键,改变频率
{
//显示发送给FPGA的频率
SetCursor(0,48);
printf("%10d",freq_step);
OLED_Refresh();
spi_trans = freq_step>>24;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
spi_trans = freq_step>>16;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
spi_trans = freq_step>>8;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
spi_trans = freq_step;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
break;
}
default: break;
}
}
(3)串口通信
通过STM32CubeMx配置串口通信功能
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart ->Instance == USART2)
{
if((USART2_RX_STA&0x8000)==0)
{
if(Res==0x0D)
{
USART2_RX_STA|=0x8000;
HAL_UART_Receive_IT(&huart2, &Res, 1);
}
else
{
USART2_RX_BUF[USART2_RX_STA&0X3FFF]=Res ;
USART2_RX_STA++;
if(USART2_RX_STA>(USART_REC_LEN-1))
USART2_RX_STA=0;
}
}
HAL_UART_Receive_IT(huart,&Res,1);
}
}
(4)SPI通信
通过STM32CubeMx配置SPI通信功能
SCLK: 串行时钟(由主设备输出).
MOSI: 主输出、从输入(由主设备输出).
MISO: 主输入、从输出(由从设备输出).
NSS: 从设备选中(低有效, 由主设备输出).
spi_trans = freq_step>>24;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
spi_trans = freq_step>>16;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
spi_trans = freq_step>>8;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
spi_trans = freq_step;
HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
2、FPGA核心功能:DDS波形发生器
将外部12MHz晶振5倍频后作为系统时钟
(1)频率控制
通过相位累加器实现频率的控制,系统时钟为60MHz,选取相位累加器位数为32位,根据公式可以计算得到1Hz时,频率控制字为232/(60×106)≈71.58。由此可知,为了实现1Hz的控制精度,频率控制字的步进值为71.58。
正弦波通过查表法实现,使用的rom是宽度为10,深度为1024的数据,所以相位控制字根据rom的深度选择了10位宽。根据相位累加器的高10位,进行波形查找。同理,方波、三角波可以利用相位累加器实现频率的控制。具体代码如下
//0:方波
wire cnt_tap = phase_acc[31]; // 取出计数器的其中1位(bit 7 = 第8位)
assign square_dac = {10{cnt_tap}}; // 重复10次作为10位DAC的值
//1:三角波
assign trig_dac = phase_acc[31] ? ~phase_acc[30:21] : phase_acc[30:21];
//2:正弦波
assign sin_dac = rd_data_i; //将读到的ROM数据赋值给DA数据端口
(2)波形控制
方波:
module dds_main(clk, dac_data, dac_clk);
input clk; //12MHz的外部时钟送给FPGA;
output [9:0] dac_data; //10位的并行数据输出到R-2R DAC;
output dac_clk; //输出给DAC的并行时钟,在R-2R的DAC中不需要这个时钟信号
//创建一个24位的自由运行的二进制计数器,方便取出最高位(0.7秒一个周期)送给LED用作心跳灯指示用, 在此程序中不列出LED的部分
//后面也会讲到为什么DDS波表的地址只有8~12位,而我们选用24位或32位相位累加器的原因
reg [23:0] cnt;
always @(posedge clk) cnt <= cnt + 24'h1;
//用它来产生DAC的信号输出
wire cnt_tap = cnt[7]; // 取出计数器的其中1位(bit 7 = 第8位)
assign dac_data = {10{cnt_tap}}; // 重复10次作为10位DAC的值
assign dac_clk= clk;
endmodule
三角波
assign dac_data = cnt[10] ? ~cnt[9:0] : cnt[9:0];
(3)幅值控制
信号发生器通常采用“DAC参考电压”配合“模拟通道信号调理”进行信号幅值的调节,市面上大多数信号发生器产品都是采用这种调幅方案。这样在调节的过程中信号的分辨率不会受影响,最大程度保证信号的性能指标,同时也需要额外的DAC等电路控制参考电压。
另外我们也可以在FPGA中增加对信号幅值调节的设计,例如我们将信号的数据乘以一个8bit的因数,然后再将结果右移8位(相当于除以256),然后再把结果输出给DAC电路。将8bit的因数作为变量可调,最后就实现了在FPGA中调节幅值的功能。这种方法不需要额外的DAC改变参考电压,即可实现对幅值的调节,但是由于采用量化后的数据进行,会造成信号数据分辨率的降低。
wire [9:0] signal_dat; //未调幅的波形数据
wire [7:0] a_ver; //用于调幅的因数
reg [17:0] amp_dat; //调幅后的波形数据
always @(posedge clk) amp_dat = signal_dat * a_ver; //波形数据乘以调幅因数
wire [9:0] dac_dat; //输出给DAC电路的数据端口
assign dac_dat = amp_dat[17:8]; //取高十位输出,相当于右移8位
根据上述DDS波形发生器原理,结合自己的业务逻辑(接收单片机发送的频率字、通过按键控制波形、幅值),输出对应的波形,具体代码如下
module dds_output(
input clk_i, //时钟
input rst_n_i, //复位信号,低电平有效
input [1:0] mode, //波形
input [7:0] amp, //幅值
input [31:0] freq_set, //频率设置
input [9:0] rd_data_i, //ROM读出的数据
output reg led11,
output reg led22,
output [9:0] rd_addr_o, //读ROM地址
//DA芯片接口
output da_clk_o, //DA驱动时钟
output [9:0] da_data_o //输出给DA的数据
);
//reg define
reg [31:0] phase_acc; //相位累加器
wire [9:0] square_dac;
wire [9:0] trig_dac;
wire [9:0] sin_dac;
reg [9:0] signal_dat; //未调幅的波形数据
reg [17:0] amp_dat; //调幅后的波形数据
//*****************************************************
//** main code
//*****************************************************
//数据rd_data是在clk的上升沿更新的,所以DA芯片在clk的下降沿锁存数据是稳定的时刻
//而DA实际上在da_clk的上升沿锁存数据,所以时钟取反,这样clk的下降沿相当于da_clk的上升沿
assign da_clk_o = ~clk_i;
//0:方波
wire cnt_tap = phase_acc[31]; // 取出计数器的其中1位(bit 7 = 第8位)
assign square_dac = {10{cnt_tap}}; // 重复10次作为10位DAC的值
//1:三角波
assign trig_dac = phase_acc[31] ? ~phase_acc[30:21] : phase_acc[30:21];
//2:正弦波
assign sin_dac = rd_data_i; //将读到的ROM数据赋值给DA数据端口
//显示波形、幅值的变化状态
always @(posedge clk_i) begin
if(mode%2==0)
led11=1'b0;
else
led11=1'b1;
if(amp%20==0)
led22=1'b0;
else
led22=1'b1;
end
//相位累加器累加
//波形选择+幅值调整
always @(posedge clk_i or negedge rst_n_i) begin
if(rst_n_i == 1'b0) begin
phase_acc <= 32'b0;
end
else begin
phase_acc <= phase_acc + freq_set;
case(mode)
2'b00: amp_dat = trig_dac * amp;
2'b01: amp_dat = sin_dac * amp;
2'b10: amp_dat = square_dac * amp;
2'b11: amp_dat = 18'd0;
endcase
end
end
//读ROM地址
assign rd_addr_o = phase_acc[31:22];
assign da_data_o = amp_dat[17:8]; //取高十位输出,相当于右移8位
endmodule
五、遇到的难题
1、不同尺寸OLED驱动代码不同,需要移植对应驱动代码,在这一方面走了一些弯路
2、不同系列单片机提供外部中断服务函数可能不同
STM32G0包括如下三个外部中断服务函数
EXTI0_1_IRQn EXTI0_1_IRQHandler
EXTI2_3_IRQn EXTI2_3_IRQHandler
EXTI4_15_IRQn EXTI4_15_IRQHandler
即外部中断线0-1分配一个中断向量,共用一个中断服务函数
外部中断线2-3分配一个中断向量,共用一个中断服务函数
外部中断线4-15分配一个中断向量,共用一个中断服务函数
因此EXTI4_15_IRQHandler可以处理PA11、PA12、PC6的下降中断
3、以前都是使用标准库开发MCU程序,第一次使用HAL进行开发,很多函数都是现查现用;
同时也是第一次接触FPGA,通过这次项目也算正式入门了。
4、首次使用接触FPGA,在学习Verilog语言的过程。了解到模块的输入、输出必须是wire类型
可是根据按键上升沿改变值必须是reg类型,之前一直在这个矛盾中循环,其实只需要在模块内部定义一个reg类型的临时变量,再通过assign语句将其赋值给输出的wire变量。
5、SPI通信速率
按说可以采样到18Mhz的数据,但是,当STM32的SPI设置为18Mhz时会出错,接收到的数据总会出错。降低通信速率,顺利解决该问题。
六、未来的计划
已经初步实现通过串口控制波形频率,下一步完善MCU与FPGA之间的SPI通信,实现波形、幅值信息的传递,进而实现DDS任意波形发生器/PC远程控制。此外,利用开发板进一步实现单通道示波器的功能,不断提升自己。
七、工程文件
链接:https://pan.baidu.com/s/1s2bHOufqqirEKNofpeHNjg
提取码:3cda