一、项目需求
2、生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
3、生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
4、在OLED上显示当前波形的形状、波形的频率以及幅度
5、利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
二、完成的功能及达到的性能
1、选择模式
可选择波形选择、幅度控制、频率控制三种模式。在板卡上通过mode按键选择,按下后光标自动跳转到下一个模式,默认处于波形选择模式。
2、选择波形
在波形选择模式下,可以选择正弦波、三角波、方波三种波形,每按下一次select按键跳转到下一个波形,默认为正弦波
3、幅度控制
在幅度控制模式下,我们可以调节幅度大小和选择单位步进幅度大小。首先选择调节幅度步进大小,当步进0.1V时,OLED屏幕上会显示mv单位;当步进1V时,OLED屏幕上显示的是V单位。通过旋转编码器来控制数值大小,顺时针旋转时幅值增大,反之减小。
4、频率控制
控制方法与幅度控制相同,这里将单位改为hz、khz与Mhz,步进大小改为1hz、1khz和1Mhz。
5、输入输出
按下旋转编码器按键即可选择输入输出状态,状态会在屏幕上显示。
三、实现思路
板卡主要分为单片机部分和FPGA部分。
1、单片机部分
单片机主控芯片为stm32g031g8u6,是一款性价比较高的芯片。它主要负责的功能有外界交互、OLED显示、复杂运算和充当与FPGA通信的主机。
2、FPGA部分
主要负责DDS波形生成和充当通信的从机。能够对单片机的指令进行翻译并进行反馈。
四、实现过程
1、按键中断
对于按键消抖,我采用了定时器消抖的方法,这样的方法可以避免中断长时间占用cpu,达到快进快出的目的。当按键触发了中断回调函数,我便开启定时器中断,产生定时器中断后再次检测按键电平。
//定时器状态机消抖
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM14)
{
switch(Key.keyState)
{
case KEY_CHECK:
{
// 读到低电平,进入按键确认状态
if((HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_11) == GPIO_PIN_RESET)||(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_12) == GPIO_PIN_RESET) ||
(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6) == GPIO_PIN_SET))
{
Key.keyState = KEY_COMFIRM;
}
break;
}
case KEY_COMFIRM:
{
if((HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_11) == GPIO_PIN_RESET)||(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_12) == GPIO_PIN_RESET) ||
(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6) == GPIO_PIN_SET))
{
//读到低电平,按键确实按下,按键标志位置1,并进入按键释放状态
Key.keyFlag = 1;
Key.keyState = KEY_RELEASE;
}
//读到高电平,可能是干扰信号,返回初始状态
else
{
Key.keyState = KEY_CHECK;
}
break;
}
case KEY_RELEASE:
{
if((HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_11) == GPIO_PIN_RESET)||(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_12) == GPIO_PIN_RESET) ||
(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6) == GPIO_PIN_SET))
{
// 读到高电平,说明按键释放,返回初始状态
Key.keyState = KEY_CHECK;
HAL_TIM_Base_Stop_IT(&htim14);//关闭中断,下一次按键触发后开启
}
break;
}
default: break;
}
}
}
编码器部分设置A和B引脚都由下降沿触发中断,此时可根据旋转方向分为两种情况。A引脚触发中断,此时若B引脚为高电平,则是一个方向的旋转周期,此时cnt改变;B引脚触发中断,此时A引脚若为高电平,则是相反方向的旋转,此时cnt按照相反方向改变。
//由两个按键和旋转编码器引脚引起的中断
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
//旋转编码器引脚
if(GPIO_Pin == GPIO_PIN_0)
{
if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET))
{
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == GPIO_PIN_SET)
{
cnt++;
}
}
}
else if(GPIO_Pin == GPIO_PIN_1)
{
if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == GPIO_PIN_RESET))
{
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_SET)
{
cnt--;
}
}
}
//模式和选择按键下降沿触发
else
{
HAL_TIM_Base_Start_IT(&htim14);//打开定时器进行消抖
}
}
除状态输出按键外均设置为下降沿触发
/*Configure GPIO pins : PB0 PB1 */
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/*Configure GPIO pins : PA11 PA12 */
GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PC6 */
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI0_1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_1_IRQn);
HAL_NVIC_SetPriority(EXTI4_15_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);
2、OLED屏幕
SSD1306是屏幕的驱动芯片,任何屏幕都需要驱动芯片。这里因为有驱动芯片了,我们不需要研究屏幕的驱动电路,只需与SSD1306芯片通信即可。我移植了u8g2库,由于stm32g031g8u6仅有64kflash,而绘制显示需要微控制提供一定的内存。故需要对特别注意以下两个函数。若将构造函数(第一个)的末尾的数字改成f,单片机的flash内存将无法支持。
void u8g2_Setup_ssd1306_128x64_noname_2(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb);
uint8_t u8x8_byte_3wire_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int,void *arg_ptr) ;
3、DDS波形生成
通过matlab生成三种波形的数据供读取,我们设置波形rom表的depth为8,最大值为2^10-1,那么就可以存储2^8个0~2^10-1的数据。这里路径需要改一下,改成波表文件所在路径即可。
reg [9:0] sine[0:255];//申请256个10位的存储单元
reg [9:0] square[0:255];//申请256个10位的存储单元
reg [9:0] sanjiao[0:255];//申请256个10位的存储单元
initial
begin
$readmemh("D:/FPGA/ICE40/DDS_fpga/source/impl_1/sine.txt",sine); //sine.txt中的数字到memory
$readmemh("D:/FPGA/ICE40/DDS_fpga/source/impl_1/sanjiao.txt",sanjiao);
$readmemh("D:/FPGA/ICE40/DDS_fpga/source/impl_1/square.txt",square);
end
根据题目要求,需要实现固定幅度和频率的步进。对于频率的步进我采用相位累加器的方法。相位累加器本质上是一个计数器,这里起一个形象的名字phase_acc。对于幅度的控制,我将幅度先乘以调幅因子,再进行移位,可实现高精度的除法。
//生成波形
reg [41:0] phase_acc;
always @(posedge clk or negedge rst) begin
if(!rst)
phase_acc<=0;
else
phase_acc <= phase_acc + frezi[37:0];
end
always@(posedge clk or negedge rst)
if(!rst)
sin_out<=0;
else if(wave_state == 0)
sin_out<=sine[phase_acc[41:34]]*vppzi;
else if(wave_state == 1)
sin_out<=sanjiao[phase_acc[41:34]]*vppzi;
else
sin_out<=square[phase_acc[41:34]]*vppzi;
always @(posedge clk or negedge rst) begin
if(!rst)
waveout<=0;
else if(enable)
waveout<=sin_out[25:16];
else
waveout<=0;
end
4、SPI通信
对于SPI的通信原理不多做阐述,网上有很多很好的资料。NSS作用:用来选择主从设备。SPI_NSS有两种模式,SPI_NSS_Hard和SPI_NSS_Soft。SPI_NSS_Hard,硬件自动拉高拉低片选,在速率上是远比软件方式控制要高的。我设置FPGA为从机,单片机为主机。所有的指令由单片机发起,FPGA翻译。
单片机代码
设置为主机模式,SPI_NSS_HARD模式,极性为低,相位为1,分频为256,MSB优先。
/* SPI1 init function */
void MX_SPI1_Init(void)
{
/* USER CODE BEGIN SPI1_Init 0 */
/* USER CODE END SPI1_Init 0 */
/* USER CODE BEGIN SPI1_Init 1 */
/* USER CODE END SPI1_Init 1 */
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_HARD_OUTPUT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 7;
hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
hspi1.Init.NSSPMode = SPI_NSS_PULSE_ENABLE;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SPI1_Init 2 */
/* USER CODE END SPI1_Init 2 */
}
FPGA代码
按照配合单片机时序移植的时序,这一块的代码移植的以下文章【原创】详细解析FPGA与STM32的SPI通信(二) -LinCoding-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台 (chinaaet.com)
module spi_receiver
(
input clk, //全局时钟信号
input rst_n, //低电平有效复位
input spi_cs, //片选信号
input spi_sck, //spi时钟信号
input spi_mosi, //主机输出从机接收线
output reg [7:0] rxd_data, //接受数据
output reg rxd_flag
);
//-----------------------------------
//对异步时钟域信号进行处理,得到同步信号
reg spi_cs_r0, spi_cs_r1;
reg spi_sck_r0, spi_sck_r1;
reg spi_mosi_r0,spi_mosi_r1;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
begin
spi_cs_r0 <= 1'b1; spi_cs_r1 <= 1'b1;
spi_sck_r0 <= 1'b0; spi_sck_r1 <= 1'b0;
spi_mosi_r0 <= 1'b0; spi_mosi_r1 <= 1'b0;
end
else
begin
spi_cs_r0 <= spi_cs; spi_cs_r1 <= spi_cs_r0;
spi_sck_r0 <= spi_sck; spi_sck_r1 <= spi_sck_r0;
spi_mosi_r0 <= spi_mosi; spi_mosi_r1 <= spi_mosi_r0;
end
end
reg [3:0] rxd_cnt /*synthesis noprune*/; // 避免Quartus II优化掉没output的reg,这个在Radiant里不知道效果如何,待实验
wire mcu_cs = spi_cs_r1;
wire mcu_data= spi_mosi_r1;
wire mcu_read_flag = ( spi_sck_r0 & ~spi_sck_r1) ? 1'b1 : 1'b0; //捕捉spi主机时钟信号上升沿
wire mcu_read_done = ( spi_cs_r0 & ~spi_cs_r1 & (rxd_cnt == 4'd8) ) ? 1'b1 : 1'b0; //接受信号完成标志
//-----------------------------------
//MOSI线信号采样
reg [7:0] rxd_data_r;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
begin
rxd_cnt <= 4'd0;
rxd_data_r <= 8'd0;
end
else if ( ! mcu_cs )
if ( mcu_read_flag )
begin
rxd_data_r[3'd7-rxd_cnt] <= mcu_data; //32单片机设置的是MSB,先接收最高位
rxd_cnt <= rxd_cnt + 1'b1;
end
else
begin
rxd_data_r <= rxd_data_r;
rxd_cnt <= rxd_cnt;
end
else
begin
rxd_data_r <= rxd_data_r;
rxd_cnt <= 4'd0;
end
end
//-----------------------------------
//输出信号
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
begin
rxd_data <= 8'd0;
rxd_flag <= 1'b0;
end
else if ( mcu_read_done )
begin
rxd_data <= rxd_data_r;
rxd_flag <= 1'b1;
end
else
begin
rxd_data <= rxd_data;
rxd_flag <= 1'b0;
end
end
endmodule
5、FPGA状态机翻译数据
这里我写得比较粗糙,好在做dds对数据的传输速率要求不高。我的大致思路是先接收到单片机发送的0和1,这样的话FPGA下一个八位接受到的就是波形的选择数据;单片机再发送2和3,FPGA下一个接收的是输出状态;单片机再发送4和5,FPGA下八次接收到的是频率累加值,这八个八位再通过位拼接组合成完整的频率累加值;单片机再发送6和7,FPGA接下来两个八位是调幅因子,同样需要进行位拼接操作得到完整值。这里接收到的数据将直接用于dds波形生成部分。这里大家只要知道是这么个功能就行了不需要细纠我的代码。首先是因为这是我自己在SPI的基础上自创的一个协议,大家完全可以自己想到更好的协议;其次是因为这个部分仅适合本工程,其它应用需要做出较大改动。
单片机代码
void arm2fpga()
{
frequentzi();
frequentzi2byte();
vppzi();
vppzi2byte();
HAL_SPI_TransmitReceive(&hspi1,&armfpga[0],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[1],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&Wave_state,&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[2],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[3],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&On_state,&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[4],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[5],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[0],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[1],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[2],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[3],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[4],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[5],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[6],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[7],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[6],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[7],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&vzibyte[0],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&vzibyte[1],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
HAL_SPI_TransmitReceive(&hspi1,&armfpga[8],&RX,sizeof(i),0xFF);
printf("%d\r\n",RX);
}
五、遇到的主要难题
1、直接打开spi1的话不会是用和fpga相连的那几个引脚,要用那几个脚的话要自己设置,这是一个值得注意的点,不然会耗费大量时间。
2、Spi通信中寄存器综合时被优化。MISO引脚在行为级仿真表现正常,但是不能通过时序级仿真。
下面是我尝试的解决方法以及结果:
方法1. Synthesis_Options中的-keep hierarchy设置为YES或soft,zhe
在ISE中的综合(XST)选项上右键选择process properties,弹出的对话框里面Synthesis_Options中的-keep hierarchy是设置综合后层次结构的。设置为YES后,用CHIPSCOPE调试时看到的层次结构跟你的设计是一样的,找信号很方便。缺点在于,xilinx 的工具就不能在设计层次间进行设计优化了。所以,建议你设成 “soft”,意思就是综合后保持层次结构,但是P&R的时候可以打破层次结构进行优化。
结果:没有找到radiant内的这个设置,比较了解的同学可以试试看。
方法2. 这种方法简单,但是偶尔寄存器也可能被优化掉
Place the Verilog constraint immediately before the module or instantiation . Specify the Verilog constraint as follows:
(* KEEP = “{TRUE|FALSE |SOFT}” *)
例如:(*KEEP = "TRUE"*) reg [15:0] cnt1;//就可以防止cnt1被优化
结果:无效
方法3.把要查看的信号引出到模块的输出端,一般不会被优化掉
结果:有效,将信号引出到输出端后miso工作也变得正常。
这里我列出了试验成功和失败的方法供大家参考,最终是方法3成功了,这也是我会把一些无意义的寄存器设置为输出引脚的原因。
3、在解决后仿真问题之后我的miso依然无法正常工作,耗费大量时间后,发现FPGA需要我按下复位键之后才能开始正常工作。原因是FPGA部分寄存器没有复位处于亚稳态所以不能正常工作。这条就作为经验之谈吧,以后注意。
六、 未来的计划建议
1、对于这个项目指标而言,我认为自己是做到足够好的,希望未来能够精简自己的代码,让代码更加健壮。
2、加一些修饰,UI界面和开机动画还有很大提升空间。
七、附录
1、单片机资源使用情况
2、FPGA资源占用分析