一.项目需求:
-
通过板上的高速DAC(10bits/最高125Msps)配合FPGA内部DDS的逻辑(最高48Msps),生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
- 生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
- 在OLED上显示当前波形的形状、波形的频率以及幅度
- 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
本人已全部实现,并且增加了锯齿波发生。
二.原理及实现方法:
1.DDS原理
DDS是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术。与传统的频率合成器相比,DDS具有低成本、低功耗、高分辨率和快速转换时间等优点。
信号发生器发出的是模拟信号,而数字系统内部是数字信号,所以需要先将想要发出的信号波形进行离散化,用若干个点表示,假设用1024个点来表示一个正弦波周期的波形。如果我1/1024秒让dac改变一下数值,那么一秒钟改变1024次正好发出一个正弦波,就是1Hz。同理,如果我在1/1024s内改变的数据跳一个,这样我就可以一个波取512个点,1s就能发出2Hz的正弦波。如果我2/1024s改变一个点,那么就可以发出0.5Hz个正弦波了。这个就是DDS最通俗的理解。
如上图所示,ROM就是放我们离散化的波形数据,而相位累加器随着时间不断累加,从而来改变相位,使得发出我们想要的波。我们程序只要控制相位累加器的频率控制字即可控制输出信号的频率。
2.设计思路
由于这次板卡配的是lattice 的ice40 fpga和stm32g0单片机,所以就采用fpga+arm的架构进行控制。arm控制oled、编码器,进行频率、幅值、波形的设置,并通过spi将控制数据发送给fpga。
3.框图和软件流程图
↑是stm32内部程序的流程图,在while里判断是否有参数改变标志,若有就发送新参数加上刷屏,通过中断来识别是否有编码器动作,有就改变参数。
↑是fpga的rtl视图。通过锁相环变为48M作为系统时钟。spi模块负责接收stm32的指令,从而控制DDS模块。
↑fpga的资源使用报告,从上表可见使用的资源还是比较少的。
4.硬件介绍
↑电赛训练板的电路图如图所示,本次主要使用OLED、编码器、fpga+stm32核心板、DAC部分。各部分连线相对比较简单,都是直接相连。值得注意的是OLED由于内部把片选直接接好了,所以只有六根线,spi通信的时候是不需要在用片选了,直接通信。
↑核心板的电路图,其中LPC是作为下载器,通过U盘的方式给fpga下载,通过串口给单片机下载,相比jtag有点不方便,每次给单片机下载都要拔线,按下boot键,再上电。由于stm32实际上pc14和pc15与电路图不符,是没有接线的,所以stm32只能使用内部时钟,不能通过fpga作为有源时钟输入。
↑是dac部分的电路图。采用3PD的dac芯片,差分输出,经过一个运放差分转单端同时也增加输出能力。由于没有负电源,所以运放实际输出是有直流量的,是一个脉动的直流,只能自己后期加电容隔直或者再来一级运放利用减法器减去直流量。
三、实现功能展示
↑ 1Hz 正弦波
↑1801Hz 正弦波,波形形态良好,频率较准
↑2701Hz方波,频率较准,因未使用同轴线,以及示波器内部信号调理电路不佳,波形平台可能略有抖动
↑4001Hz三角波
↑33kHz锯齿波
↑使用FFT,821Hz频率准确
↑2.941kHz频率准确
↑ 241.941kHz频率
3MHz、4MHz、5Mhz部分在上方视频中有展示
四、主要代码
1.stm32部分
这次用的stm32是一个比较新的g031系列,以前也没有用过。由于我原来用的是f103、f407、f334等,都是基于标准库开发的,这次的g0系列没有标准库,所以有点不好下手。还好硬禾学堂以前有一个案例www.eetree.cn/project/detail/361 也是使用这个板子,所以就用他的stm32工程,之后像采用标准库一样使用hal库,纯手改的形式进行配置。
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_SPI1_Init();
KEY_Init();
ENCODER_Init();
EXTI_Init();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
OLED_ShowChinese(24,0,2,16);//信
OLED_ShowChinese(40,0,3,16);//号
OLED_ShowChinese(56,0,4,16);//发
OLED_ShowChinese(72,0,5,16);//生
OLED_ShowChinese(90,0,6,16);//器
SetCursor(0,16);printf("Freq:");
SetCursor(112,16);printf("Hz");
SetCursor(0,32);printf("Vpp : V");
SetCursor(0,48);printf("Wave:");
OLED_Refresh();
while (1)
{
if(trans==0x01){
OLED_DISPLAY_8x16(2,40,48+set_freq/1000000,set==6);
OLED_DISPLAY_8x16(2,53,48+set_freq/100000%10,set==5);
OLED_DISPLAY_8x16(2,61,48+set_freq/10000%10,set==4);
OLED_DISPLAY_8x16(2,69,48+set_freq/1000%10,set==3);
OLED_DISPLAY_8x16(2,82,48+set_freq/100%10,set==2);
OLED_DISPLAY_8x16(2,90,48+set_freq/10%10,set==1);
OLED_DISPLAY_8x16(2,98,48+set_freq%10,set==0);
OLED_DISPLAY_8x16(4,40,48+set_vpp/100,0);
OLED_DISPLAY_8x16(4,48,'.',0);
OLED_DISPLAY_8x16(4,56,48+set_vpp/10%10,set==8);
OLED_DISPLAY_8x16(4,64,48+set_vpp%10,set==7);
if(set_wave==0){//正弦波
OLED_DISPLAY_8x16(6,40,'S',set==9);
OLED_DISPLAY_8x16(6,48,'i',set==9);
OLED_DISPLAY_8x16(6,56,'n',set==9);
}
if(set_wave==1){//方波
OLED_DISPLAY_8x16(6,40,'S',set==9);
OLED_DISPLAY_8x16(6,48,'q',set==9);
OLED_DISPLAY_8x16(6,56,'u',set==9);
}
if(set_wave==2){//三角波
OLED_DISPLAY_8x16(6,40,'T',set==9);
OLED_DISPLAY_8x16(6,48,'r',set==9);
OLED_DISPLAY_8x16(6,56,'i',set==9);
}
if(set_wave==3){//锯齿
OLED_DISPLAY_8x16(6,40,'S',set==9);
OLED_DISPLAY_8x16(6,48,'a',set==9);
OLED_DISPLAY_8x16(6,56,'w',set==9);
}
vpp=(int)((float)set_vpp*4.34);
freq=(u32)((float)set_freq/0.0111758709);
spi = (set_wave<<2)+(vpp>>8);
HAL_SPI_Transmit(&hspi1,&spi,1,1000);
spi =(u8)(vpp&0x00ff);
HAL_SPI_Transmit(&hspi1,&spi,1,1000);
spi = freq>>24;
HAL_SPI_Transmit(&hspi1,&spi,1,1000);
spi = freq>>16;
HAL_SPI_Transmit(&hspi1,&spi,1,1000);
spi = freq>>8;
HAL_SPI_Transmit(&hspi1,&spi,1,1000);
spi = freq;
HAL_SPI_Transmit(&hspi1,&spi,1,1000);
trans=0x00;
}
}
}
↑主程序就是一个循环,循环判断是否有参数改变,若有就更新oled屏幕并发送,没有就不更新不发送。
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case key1_Pin: //复位按键
{
break;
}
case key2_Pin: //切换频率步进按键
{
break;
}
case encoderA_Pin: //编码器旋钮,调节频率
{
if(HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A下降沿,B高电平,顺时针
{
switch(set)
{
case 0: set_freq += 1; break;
case 1: set_freq += 10; break;
case 2: set_freq += 100; break;
case 3: set_freq += 1000; break;
case 4: set_freq += 10000; break;
case 5: set_freq += 100000; break;
case 6: set_freq += 1000000; break;
case 7: set_vpp += 1; break;
case 8: set_vpp += 10; break;
case 9: set_wave += 1; break;
default: set_freq += 1; break;
}
if(set_freq>5000000) set_freq=5000000;
if(set_vpp>=100) set_vpp=100;
if(set_wave>=3) set_wave=3;
}
else if(!HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A下降沿,B低电平,逆时针
{
switch(set)
{
case 0: set_freq -= 1; break;
case 1: set_freq -= 10; break;
case 2: set_freq -= 100; break;
case 3: set_freq -= 1000; break;
case 4: set_freq -= 10000; break;
case 5: set_freq -= 100000; break;
case 6: set_freq -= 1000000; break;
case 7: set_vpp -= 1; break;
case 8: set_vpp -= 10; break;
case 9: set_wave -= 1; break;
default: set_freq -= 1; break;
}
if(set_freq<1) set_freq=1;
if(set_vpp<=10) set_vpp=10;
if(set_wave<=0) set_wave=0;
}
trans=0x01;
break;
}
case encoder_key_Pin: //编码器按键
{
if(set<9)set++;
else set=0;
trans=0x01;
break;
}
default: break;
}
}
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case encoderA_Pin:
{
if(!HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A上升沿,B低电平,下降沿
{
switch(set)
{
case 0: set_freq += 1; break;
case 1: set_freq += 10; break;
case 2: set_freq += 100; break;
case 3: set_freq += 1000; break;
case 4: set_freq += 10000; break;
case 5: set_freq += 100000; break;
case 6: set_freq += 1000000; break;
case 7: set_vpp += 1; break;
case 8: set_vpp += 10; break;
case 9: set_wave += 1; break;
default: set_freq += 1; break;
}
if(set_freq>5000000) set_freq=5000000;
if(set_vpp>100) set_vpp=100;
if(set_wave>=3) set_wave=3;
}
else if(HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A上升沿,B高电平,逆时针
{
switch(set)
{
case 0: set_freq -= 1; break;
case 1: set_freq -= 10; break;
case 2: set_freq -= 100; break;
case 3: set_freq -= 1000; break;
case 4: set_freq -= 10000; break;
case 5: set_freq -= 100000; break;
case 6: set_freq -= 1000000; break;
case 7: set_vpp -= 1; break;
case 8: set_vpp -= 10; break;
case 9: set_wave -= 1; break;
default: set_freq -= 1; break;
}
if(set_freq<=1) set_freq=1;
if(set_vpp<=10) set_vpp=10;
if(set_wave<=0) set_wave=0;
}
trans=0x01;
break;
}
default: break;
}
}
↑中断部分就是判断编码器左右转和按键,参数改变就把参数改变标志置1,这样主循环就会刷新OLED并发送。
void OLED_DISPLAY_8x16(u8 x, //显示汉字的页坐标(从0到7)(此处不可修改)
u8 y, //显示字的列坐标(从0到127)
int w,
u8 z//){//0正常 1反白
){ //要显示汉字的编号
u8 j,t,c=0;
y=y+2; //因OLED屏的内置驱动芯片是从0x02列作为屏上最左一列,所以要加上偏移量
if(z==0){
for(t=0;t<2;t++){
OLED_WR_Byte(0xb0+x,OLED_CMD); //页地址(从0xB0到0xB7)
OLED_WR_Byte(y%16,OLED_CMD); //设置低列起始地址
OLED_WR_Byte(y/16+0x10,OLED_CMD); //设置高列起始地址
for(j=0;j<8;j++){ //整页内容填充
OLED_WR_Byte(ASCII_8x16[(w*16)+c-512],OLED_DATA);//为了和ASII表对应要减512
c++;}x++; //页地址加1
}
}
if(z==1){
for(t=0;t<2;t++){
OLED_WR_Byte(0xb0+x,OLED_CMD); //页地址(从0xB0到0xB7)
OLED_WR_Byte(y%16,OLED_CMD); //设置低列起始地址
OLED_WR_Byte(y/16+0x10,OLED_CMD); //设置高列起始地址
for(j=0;j<8;j++){ //整页内容填充
OLED_WR_Byte(~ASCII_8x16[(w*16)+c-512],OLED_DATA);//为了和ASII表对应要减512
c++;}x++; //页地址加1
}
}
}
OLED部分我采用了一个变量,来决定是否反显,这样正在设置的那一位OLED就会反显,变成白色底色,效果比较好。
2.fpga部分
module stepice(
input clk_12M,
input sys_rst_n,
output wire [9:0] da_out,
output wire da_clk,
input wire spi_mosi,
input wire spi_cs ,
input wire spi_sclk
);
wire clk_48M;
//锁相环例化
pll_48M u_pll_48M(
.ref_clk_i(clk_12M),
.rst_n_i(1),
.outcore_o(clk_48M),
.outglobal_o( )
);
//DDS例化
DDS u1(
.clk (clk_48M),
.rst (1),
.wave (set_out[43:42]),
.Fword (set_out[31:0]),
.Pword (set_P),
.Aword (set_out[41:32]),
.clkout (da_clk),
.dataout(da_out)
);
//spi_slave模块例化
wire [47:0]set_out;
spi_slave u2(
.clk(clk_48M),
.rst_n(sys_rst_n),
.SCK(spi_sclk),
.SSEL(spi_cs),
.MOSI(spi_mosi),
.set_out(set_out)
);
endmodule;
↑是fpga的顶层模块,其中比较重要的就是对接收到的set_out数据进行拆分。,由于spi一次只能穿8位,所以总共穿48位,0-31位为频率控制字,32-41位为幅值控制字,42-43位为波形选择。
//相位寄存器:
reg [31:0]frechange;
always @(posedge clk or negedge rst)
begin
if(!rst)
frechange <= 32'd0;
else
frechange <= frechange + Fword;
end//相位累加器:
reg [9:0]romaddr;
always @(posedge clk or negedge rst)
begin
if(!rst)
romaddr <= 10'd0;
else
romaddr <= frechange[31:22] + Pword;
end//正弦波表:
rom_sin romsin (.clka(clk), // input wire clka
.addra(romaddr), // input wire [9 : 0] addra
.douta(sindata) // output wire [9 : 0] douta
);
↑DDS的核心部分就是这一段代码。通过控制频率控制字来控制每次加的速度,频率控制字小的时候就是同一个点发好几个时钟周期,而频率控制字大的时候,甚至取样的点还是跳着发的,并不能完整一个周期1024个点。
//方波:
assign squdata = phase[31] ? 10'd1023:10'd0;
//三角波:
assign tridata = phase[31]? (~phase[30:21]): phase[30:21];
//锯齿波:
assign sawdata = phase[31:22];
↑就是方波、三角波、锯齿波生成的算法,这样可以省rom空间,正弦波因为fpga没法生成sin,所以采用查表法。
//调幅:
wire [9:0] data;
assign data = wavedata;
reg [19:0] AMdata;
always@(posedge clk)
if(!rst)
AMdata<=1'd0;
else
AMdata<=data*Aword;
assign dataout = AMdata[19:10];
↑这个就是调幅部分,本质上就是乘以一个小数,但是fpga除法很耗LUT,故采用乘一个数再移位。
module spi_slave(
input clk,
input rst_n,
input SCK,
input SSEL,
input MOSI,
output [47:0] set_out
);
//同步信号
//----------------------------------------------------------------------------------------
// 使用3位移位寄存器将SCK与时钟信号同步
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
// 同步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
// 同步MOSI
reg [1:0] MOSIr; always @(posedge clk) MOSIr <= {MOSIr[0], MOSI};
wire MOSI_data = MOSIr[1];
//----------------------------------------------------------------------------------------
//接收部分
//----------------------------------------------------------------------------------------
// 3位计数器,计数接收比特
reg [2:0] bitcnt;
reg byte_received; // high when a byte has been received
reg [47:0] byte_data_received; //接收数据寄存器,48bit
always @(posedge clk or negedge rst_n)
begin
if(~rst_n)
byte_data_received <= 48'd0;
else
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[47:0], MOSI_data};
end
end
// 频率控制字步进
assign set_out = byte_data_received;
//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------
endmodule
↑是spi接收部分。也是参考硬禾学堂的案例spi_verilog [电子森林] (eetree.cn),我把接收位宽给扩大到了48位。
五、遇到的难题
由于采用了LPC芯片下载,fpga的逻辑分析仪等功能用不了了,并且stm32的片上仿真也用不了了,遇到问题只能靠一遍遍下载,调试。由于fpga和stm32都处于不确定,很多问题都是靠现象去猜问题,调试起来有点不方便。最后只能靠耐心来解决问题。
六、未来的计划和建议
其实这次寒假一起练我原先想做示波器的,并且做了一周多,主要功能基本都做好了,但是bug也很多。由于没有fpga逻辑分析和stm32的片上仿真,所以很多问题查不出来,后面期待有时间再用其他fpga把示波器项目给做了。同时因为我自己水平不扎实,ice40 fpga上会有出现很多其他fpga没有的问题,很多一直难以解决,最后不得不放弃。不知道是不是综合软件的问题,希望后期能再完善一下我的fpga示波器。