目录
1.项目描述
1.1 项目介绍
1.2 设计思路
1.3 框图和流程图
1.4 硬件介绍
1.5 实现的功能
1.6 遇到的主要难题及解决方法:旋转编码器消抖、SPI库无法调用、没有ST-Link无法调试
1.7 未来的计划和建议
2.主要代码片段及原理说明
2.1FPGA部分
2.1.1 DDS信号产生
2.1.2 使用相位累加器实现频率控制
2.1.3 幅度控制
2.1.4 从机SPI协议
2.2 STM32部分
2.2.1 OLED屏幕控制
2.2.2 按键与旋转编码器
2.2.3 参数通信编码
2.2.4 主机SPI协议
3.结语
1.项目描述
1.1 项目介绍
本项目是基于STM32+ICE40电赛训练平台实现的由本地控制的DDS任意波形发生器,可以产生四种不同的波形,并且频率可调,幅度可调,同时在OLED屏幕上面能够显示产生波形的相关参数。
1.2 设计思路
由MCU作为主机,接收按键K1、K2和旋转编码器的命令,控制OLED屏幕显示指定的信息;MCU将收到的命令通过SPI协议发送至FPGA,控制FPGA产生指定幅度、频率、波形的信号。
1.3 框图和流程图
1.4 硬件介绍
本项目使用的硬件包括:STM32+ICE40核心板、电赛扩展板、ADC模块、示波器。
1.5 实现的功能
通过按键K1、K2、旋转编码器控制幅度、频率、波形,实现任意频率、幅度的信号发生器,并在OLED屏幕上显示相关参数。下图分别展示了生成方波、锯齿波、三角波、正弦波信号。
1.6 遇到的主要难题及解决方法
无法调用HAL_SPI_Transmit(hspi, pData, Size, Timeout)函数实现SPI通信。解决方法为,编写程序通过HAL_GPIO_WritePin(GPIOx, GPIO_Pin, PinState)函数按照SPI通信的规则对引脚高低电平进行操作,实现SPI的数据传输。
MCU没有配置ST-Link,无法对代码进行Debug。解决方法为,通过MCU所连接的LED灯,将LED灯电平的控制穿插在程序中,以LED灯的亮灭作为指示辅助代码的调试。这种方法的优点是不需要对硬件进行更改,缺点是无法查看变量的更新情况,并且调试效率极其低下。更好的办法是通过焊接的方式,给MCU配置一个ST-Link接口,这样的优点是代码的调试变的十分简单高效,缺点是在对硬件进行修改的过程中,可能会损坏芯片。交流群中有同学采用了这种焊接的方法,并且取得了成功,焊接后的效果图如下。
1.7 未来的计划和建议
在完成这个项目后,我对于FPGA和MCU的了解又加深了,之后我将尝试完成其他的项目,挑战更高难度的开发工作。
关于这块开发板,我希望官方可以在核心板的MCU上面增加一个ST-Link接口便于MCU编程的调试工作,能够直接对程序进行Debug会极大的提高开发的效率。
2.主要代码片段及原理说明
2.1 FPGA部分
资源占用报告如下
#Synthesis Resource Utilization Report file generated by Lattice Radiant Software (64-bit) 2022.1.0.52.3
#Generated on 03/12/23 14:12:04
#DESIGN = test_ALL
#DEVICE = iCE40UP5K
#PACKAGE = SG48
#OPERATING = Industrial
#PERFORMANCEGRADE = High-Performance_1.2V
LUT4 PFU Registers DSP MULT Carry Cells
top 197(121) 118(91) 1(1) 38(33)
u_SPI 18(18) 27(27) 0(0) 0(0)
u_clk_pll 0(0) 0(0) 0(0) 0(0)
lscc_pll_inst 0(0) 0(0) 0(0) 0(0)
u_lookup_table 58(15) 0(0) 0(0) 5(5)
u_sin_table 43(43) 0(0) 0(0) 0(0)
2.1.1 DDS信号产生
DDS常被用来产生周期性的信号,通过计数器与内部时钟的配合,可以直接产生方波、锯齿波、三角波这三种简单波形,下面展示了这三种简单波形在FPGA中的生成代码
always @(posedge clk_120m) phase_acc <= phase_acc + set_frequency;//相位累加器,通过改变set_frequency的值即可改变生成信号的波形
wire phase_acc_tap = phase_acc[27]; // 取出计数器的最高位
assign square_dat = {10{phase_acc_tap}}; // 重复10次作为10位DAC的值,即可生成方波
assign saw_dat = phase_acc[27:18]; //锯齿波
assign tri_dat = phase_acc[27] ? ~phase_acc[26:18]-9'h1FF : phase_acc[26:18];//三角波
对于正弦波信号,需要通过查找表的方式产生,预先在表中储存好正弦信号的数值,通过计数器查找对应位次的数值并输出,即可产生正弦波。
以下这段代码储存了1/4个周期的正弦波的数值信息,另外3/4个周期可以通过正弦函数的对称性直接生成。
always @(address)begin
case(address)
6'h0: sin=9'h0;
6'h1: sin=9'hC;
6'h2: sin=9'h19;
6'h3: sin=9'h25;
6'h4: sin=9'h32;
6'h5: sin=9'h3E;
6'h6: sin=9'h4B;
6'h7: sin=9'h57;
6'h8: sin=9'h63;
6'h9: sin=9'h70;
6'ha: sin=9'h7C;
6'hb: sin=9'h88;
6'hc: sin=9'h94;
6'hd: sin=9'hA0;
6'he: sin=9'hAC;
6'hf: sin=9'hB8;
6'h10: sin=9'hC3;
6'h11: sin=9'hCF;
6'h12: sin=9'hDA;
6'h13: sin=9'hE6;
6'h14: sin=9'hF1;
6'h15: sin=9'hFC;
6'h16: sin=9'h107;
6'h17: sin=9'h111;
6'h18: sin=9'h11C;
6'h19: sin=9'h126;
6'h1a: sin=9'h130;
6'h1b: sin=9'h13A;
6'h1c: sin=9'h144;
6'h1d: sin=9'h14E;
6'h1e: sin=9'h157;
6'h1f: sin=9'h161;
6'h20: sin=9'h16A;
6'h21: sin=9'h172;
6'h22: sin=9'h17B;
6'h23: sin=9'h183;
6'h24: sin=9'h18B;
6'h25: sin=9'h193;
6'h26: sin=9'h19B;
6'h27: sin=9'h1A2;
6'h28: sin=9'h1A9;
6'h29: sin=9'h1B0;
6'h2a: sin=9'h1B7;
6'h2b: sin=9'h1BD;
6'h2c: sin=9'h1C3;
6'h2d: sin=9'h1C9;
6'h2e: sin=9'h1CE;
6'h2f: sin=9'h1D4;
6'h30: sin=9'h1D9;
6'h31: sin=9'h1DD;
6'h32: sin=9'h1E2;
6'h33: sin=9'h1E6;
6'h34: sin=9'h1E9;
6'h35: sin=9'h1ED;
6'h36: sin=9'h1F0;
6'h37: sin=9'h1F3;
6'h38: sin=9'h1F6;
6'h39: sin=9'h1F8;
6'h3a: sin=9'h1FA;
6'h3b: sin=9'h1FC;
6'h3c: sin=9'h1FD;
6'h3d: sin=9'h1FE;
6'h3e: sin=9'h1FF;
6'h3f: sin=9'h1FF;
endcase
end
以下这部分代码描述了查找表的功能函数,输入当前的相位,即可输出对应的数值
module lookup_tables(phase, sin_out);
input [7:0] phase;
output [9:0] sin_out;
wire [9:0] sin_out;
reg [5:0] address;
wire [1:0] sel;
wire [8:0] sine_table_out;
reg [9:0] sine_onecycle_amp;
//assign sin_out = {4'b0, sine_onecycle_amp[9:4]} + 9'hff; // 可以调节输出信号的幅度
assign sin_out = sine_onecycle_amp[9:0];
assign sel = phase[7:6];
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[5:0];
end
2'b01: begin
sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];
address = ~phase[5:0];
end
2'b10: begin
sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];
address = phase[5:0];
end
2'b11: begin
sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];
address = ~ phase[5:0];
end
endcase
end
endmodule
在top文件中对查找表函数进行例化即可获得当前相位所对应的正弦信号幅值。
2.1.2 使用相位累加器实现频率控制
在本项目中,FPGA内部的时钟信号频率为12Hz,要求产生的信号频率为DC-5MHz,为了保证一个周期至少5个以上的采样点,通过PLL锁相环产生一个频率为120MHz的时钟信号,并且使用28位相位累加器作为计数,这样就使得产生的信号频率范围可以达到0.45Hz-20MHz,同时通过改变相位累加器在每个上升沿增加的数值,即可实现对频率的控制。
在本项目中,采用120MHz的时钟信号和28位相位累加器,调节精度为:
120e6/(2^28)=0.447
即参数set_frequency每增加1,产生信号的频率增加0.447Hz.
以下是关于相位累加器的代码实现。
wire clk_120m;
reg [23:0] set_frequency;
reg [27:0] phase_acc;//为了使调节范围在0.45Hz ~ 20MHz,采用28位相位累加器
clk_pll u_clk_pll(.ref_clk_i(clk), .rst_n_i(1'b1), .outcore_o(clk_120m));//调用锁相环
always @(posedge clk_120m) phase_acc <= phase_acc + set_frequency;//相位累加器计数器,通过改变set_frequency的值即可控制频率
2.1.3 幅度控制
在完成波形信号的产生后,只需要对产生的信号进行乘法和除法运算,即可实现对波形幅度的控制,如以下代码所示。
经过测量,将高速DAC所产生的电压最大视为2.7V,为了实现电压幅度0.1V-1V的控制,在生成数据与调幅因数相乘后,将所得结果右移5位,即所得结果除以32,当调幅因数为1时,产生的波形最大值为:
2.7/32 = 0.084375
即产生信号的幅值最小为0.084375V,最大为2.7V,调节精度为0.084375V。
reg [14:0] sin_amp;//调幅后的波形数据
always @(posedge clk_120m) sin_amp = dac_dat * amp_ctl; //波形数据乘以调幅因数
assign dac_data = sin_amp[14:5]; //取高十位输出,相当于右移5位,也就是除32
2.1.4 从机SPI协议
为了实现MCU对FPGA生成波形参数的控制,需要在MCU和FPGA之间进行通信,在本项目中,由MCU作为主机(Master),FPGA作为从机(Slave),使用模式0的SPI协议进行通信。
上图是SPI通信协议的时序图,当片选信号CS(在STM32中常将片选信号命名为NSS)使能后,从机开始工作,其工作时钟由主机提供,即图中所示的CLK信号(也可命名为SCK)。
由于工作模式为模式0,所以在每个时钟的上升沿,从机FPGA采集1bit由主机MCU发送的数据,在每个时钟的下降沿,从机FPGA向主机MCU发送1bit的数据,这就完成了FPGA与MCU的通信。
以下展示了SPI协议的从机控制模块的代码,由于本项目中从机并不需要向主机发送数据,所以没有使用MISO引脚。
由于主机MCU需要向从机发送频率、幅度、波形、变化步长四个信息,经过编码,将他们用8bit数据表示,因此在本项目中的SPI协议每一次使能需要传输8bit的数据。
module SPI_self_programmed (
input F_SPI_CS,//片选信号,低有效
input F_SPI_SCK,
//input F_SPI_MISO,//不向主机发送数据,这句用不到
input F_SPI_MOSI,
input clk,//FPGA的时钟
output reg Rx_DV,//收到了一次数据,data valid每变化一次就说明收到了一次信号
output reg [7:0] Rx_data
//output test_done
);
reg [7:0] save_Rx_data;
reg [7:0] temp_Rx_data=8'b0;
reg [3:0] counter = 4'd0;
reg Rx_done;
always @(posedge F_SPI_SCK or posedge F_SPI_CS)begin
if (F_SPI_CS)begin
counter <= 0;
Rx_done <= 1'b0;
end
else begin//片选使能,数据传输
counter <= counter + 4'd1;
temp_Rx_data <= {temp_Rx_data[6:0],F_SPI_MOSI};
//Rx_data <= Rx_data;
if (counter == 3'b111)begin//传输了八次,数据传输完成了
Rx_done <= 1'b1;
save_Rx_data <= {temp_Rx_data[6:0],F_SPI_MOSI};
//Rx_DV <= ~Rx_DV;
end
else if (counter == 3'b010)begin
Rx_done <= 1'b0;
end
end
//text_done <= Rx_done;
end
always @(posedge clk)begin
if (Rx_done == 1'b1)begin
Rx_data <= save_Rx_data;
Rx_DV <= 1'b1;
end
else begin
Rx_DV <= 1'b0;
Rx_data <= 8'b0;
end
end
endmodule
信息接收完成后,需要对接收到的8bit信息进行解码,本项目中的编码规则为:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
频率 | 频率 | 幅度 | 幅度 | 波形 | 步长 | 步长 | 步长 |
频率:00表示频率不变,01表示频率增加,10表示频率减少
幅度:00表示幅度不变,01表示幅度增加,10表示幅度减少
波形:0表示波形不变,1表示切换至下一个波形
步长:用三位二进制数表示一个十进制数n,变化的步长为10^n
例如,当从机FPGA收到的信息为01000100,即波形不变,幅度不变,频率增加,增加值为10^4,这就完成了主机对从机的信号参数控制。
信息接收完成后,对其进行解码,根据解码得到的信息对所产生信号的参数进行控制,其代码描述如下,其中包含了一部分对RGB和LED的操作以方便调试时查看程序运行的情况。
//根据spi收到的信息对参数进行操作
always @(posedge Rx_DV)begin
//step decode begin
if (spi_step == 3'b000)begin
decode_step <= 24'd1;
//RGB <= 3'b110;//红
//RGB = ~RGB;
//LED = 2'b11;
end
else if (spi_step == 3'b001)begin
decode_step <= 24'd10;
//RGB <= 3'b101;//蓝
//LED = 2'b11;
end
else if (spi_step == 3'b010)begin
decode_step <= 24'd100;
//RGB <= 3'b011;//绿
//LED = 2'b10;
end
else if (spi_step == 3'b011)begin
decode_step <= 24'd1000;
//RGB <= 3'b001;//蓝绿
//LED = 2'b01;
end
else if (spi_step == 3'b100)begin
decode_step <= 24'd10000;
//LED = 2'b11;
end
else if (spi_step == 3'b101)begin
decode_step <= 24'd100000;
//LED = 2'b11;
end
else if (spi_step == 3'b110)begin
decode_step <= 24'd1000000;
//LED = 2'b11;
end
else begin
decode_step <= decode_step;
//RGB <= 3'b111;
end
//step decode end
//wave decode begin
if (spi_wave == 1'b1)begin
wave <= wave + 2'd1;
//RGB = ~RGB;
end
else begin
wave <= wave;
//RGB = ~RGB;
end
//wave decode end
//ampulitude decode begin
if (spi_amp == 2'b00)begin
amp_ctl <= amp_ctl;
end
else if (spi_amp == 2'b01)begin
amp_ctl <= amp_ctl + decode_step;
if(amp_ctl > 8'd32)begin
amp_ctl <= 8'd32;
end
end
else if (spi_amp == 2'b10)begin
amp_ctl <= amp_ctl - decode_step;
if(amp_ctl < 8'd1)begin
amp_ctl <= 8'd1;
end
end
else begin
amp_ctl <= amp_ctl;
end
//ampulitude decode end
//fre decode begin
if (spi_fre == 2'b00)begin
set_frequency <= set_frequency;
end
else if (spi_fre == 2'b01)begin
set_frequency <= set_frequency + decode_step;
//RGB <= 3'b110;//红
//LED <= 2'b11;
if (set_frequency > 24'd2237136)begin
set_frequency <= 24'd10000;
//LED <= 2'b10;//上
end
end
else if (spi_fre == 2'b10)begin
set_frequency <= set_frequency - decode_step;
//RGB <= 3'b011;//绿
//LED <= 2'b11;
if (set_frequency > 24'd2237136)begin
set_frequency <= 24'd10000;
//LED <= 2'b10;//上
end
end
else begin
frequency_ctl <= frequency_ctl;
end
//fre decode end
2.2 STM32部分
2.2.1 OLED屏幕控制
在OLED屏幕控制中,同样使用了SPI通信协议来向OLED屏幕发送信息,和产生正弦信号类似,先在查找表中按照ASCII表编码顺序保存需要使用的字符,再通过序号地址来选择想要用到的字符。
以下代码是在查找表中保存的字符信息,由于篇幅有限,只展示一部分
const unsigned char asc2_1608[95][16]={
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/
{0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xCC,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00},/*"!",1*/
{0x00,0x00,0x08,0x00,0x30,0x00,0x60,0x00,0x08,0x00,0x30,0x00,0x60,0x00,0x00,0x00},/*""",2*/
{0x02,0x20,0x03,0xFC,0x1E,0x20,0x02,0x20,0x03,0xFC,0x1E,0x20,0x02,0x20,0x00,0x00},/*"#",3*/
{0x00,0x00,0x0E,0x18,0x11,0x04,0x3F,0xFF,0x10,0x84,0x0C,0x78,0x00,0x00,0x00,0x00},/*"$",4*/
{0x0F,0x00,0x10,0x84,0x0F,0x38,0x00,0xC0,0x07,0x78,0x18,0x84,0x00,0x78,0x00,0x00},/*"%",5*/
{0x00,0x78,0x0F,0x84,0x10,0xC4,0x11,0x24,0x0E,0x98,0x00,0xE4,0x00,0x84,0x00,0x08},/*"&",6*/
{0x08,0x00,0x68,0x00,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"'",7*/
{0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x18,0x18,0x20,0x04,0x40,0x02,0x00,0x00},/*"(",8*/
{0x00,0x00,0x40,0x02,0x20,0x04,0x18,0x18,0x07,0xE0,0x00,0x00,0x00,0x00,0x00,0x00},/*")",9*/
{0x02,0x40,0x02,0x40,0x01,0x80,0x0F,0xF0,0x01,0x80,0x02,0x40,0x02,0x40,0x00,0x00},/*"*",10*/
{0x00,0x80,0x00,0x80,0x00,0x80,0x0F,0xF8,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x00},/*"+",11*/
{0x00,0x01,0x00,0x0D,0x00,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*",",12*/
{0x00,0x00,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80},/*"-",13*/
{0x00,0x00,0x00,0x0C,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*".",14*/
{0x00,0x00,0x00,0x06,0x00,0x18,0x00,0x60,0x01,0x80,0x06,0x00,0x18,0x00,0x20,0x00},/*"/",15*/
{0x00,0x00,0x07,0xF0,0x08,0x08,0x10,0x04,0x10,0x04,0x08,0x08,0x07,0xF0,0x00,0x00},/*"0",16*/
{0x00,0x00,0x08,0x04,0x08,0x04,0x1F,0xFC,0x00,0x04,0x00,0x04,0x00,0x00,0x00,0x00},/*"1",17*/
{0x00,0x00,0x0E,0x0C,0x10,0x14,0x10,0x24,0x10,0x44,0x11,0x84,0x0E,0x0C,0x00,0x00},/*"2",18*/
{0x00,0x00,0x0C,0x18,0x10,0x04,0x11,0x04,0x11,0x04,0x12,0x88,0x0C,0x70,0x00,0x00},/*"3",19*/
{0x00,0x00,0x00,0xE0,0x03,0x20,0x04,0x24,0x08,0x24,0x1F,0xFC,0x00,0x24,0x00,0x00},/*"4",20*/
{0x00,0x00,0x1F,0x98,0x10,0x84,0x11,0x04,0x11,0x04,0x10,0x88,0x10,0x70,0x00,0x00},/*"5",21*/
{0x00,0x00,0x07,0xF0,0x08,0x88,0x11,0x04,0x11,0x04,0x18,0x88,0x00,0x70,0x00,0x00},/*"6",22*/
{0x00,0x00,0x1C,0x00,0x10,0x00,0x10,0xFC,0x13,0x00,0x1C,0x00,0x10,0x00,0x00,0x00},/*"7",23*/
{0x00,0x00,0x0E,0x38,0x11,0x44,0x10,0x84,0x10,0x84,0x11,0x44,0x0E,0x38,0x00,0x00},/*"8",24*/
{0x00,0x00,0x07,0x00,0x08,0x8C,0x10,0x44,0x10,0x44,0x08,0x88,0x07,0xF0,0x00,0x00},/*"9",25*/
{0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x0C,0x03,0x0C,0x00,0x00,0x00,0x00,0x00,0x00},/*":",26*/
{0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*";",27*/
{0x00,0x00,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x10,0x08,0x08,0x10,0x04,0x00,0x00},/*"<",28*/
{0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x00,0x00},/*"=",29*/
{0x00,0x00,0x10,0x04,0x08,0x08,0x04,0x10,0x02,0x20,0x01,0x40,0x00,0x80,0x00,0x00},/*">",30*/
{0x00,0x00,0x0E,0x00,0x12,0x00,0x10,0x0C,0x10,0x6C,0x10,0x80,0x0F,0x00,0x00,0x00},/*"?",31*/
{0x03,0xE0,0x0C,0x18,0x13,0xE4,0x14,0x24,0x17,0xC4,0x08,0x28,0x07,0xD0,0x00,0x00},/*"@",32*/
{0x00,0x04,0x00,0x3C,0x03,0xC4,0x1C,0x40,0x07,0x40,0x00,0xE4,0x00,0x1C,0x00,0x04},/*"A",33*/
{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x04,0x11,0x04,0x0E,0x88,0x00,0x70,0x00,0x00},/*"B",34*/
{0x03,0xE0,0x0C,0x18,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x08,0x1C,0x10,0x00,0x00},/*"C",35*/
{0x10,0x04,0x1F,0xFC,0x10,0x04,0x10,0x04,0x10,0x04,0x08,0x08,0x07,0xF0,0x00,0x00},/*"D",36*/
{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x04,0x17,0xC4,0x10,0x04,0x08,0x18,0x00,0x00},/*"E",37*/
{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x00,0x17,0xC0,0x10,0x00,0x08,0x00,0x00,0x00},/*"F",38*/
{0x03,0xE0,0x0C,0x18,0x10,0x04,0x10,0x04,0x10,0x44,0x1C,0x78,0x00,0x40,0x00,0x00},/*"G",39*/
{0x10,0x04,0x1F,0xFC,0x10,0x84,0x00,0x80,0x00,0x80,0x10,0x84,0x1F,0xFC,0x10,0x04},/*"H",40*/
{0x00,0x00,0x10,0x04,0x10,0x04,0x1F,0xFC,0x10,0x04,0x10,0x04,0x00,0x00,0x00,0x00},/*"I",41*/
{0x00,0x03,0x00,0x01,0x10,0x01,0x10,0x01,0x1F,0xFE,0x10,0x00,0x10,0x00,0x00,0x00},/*"J",42*/
{0x10,0x04,0x1F,0xFC,0x11,0x04,0x03,0x80,0x14,0x64,0x18,0x1C,0x10,0x04,0x00,0x00},/*"K",43*/
{0x10,0x04,0x1F,0xFC,0x10,0x04,0x00,0x04,0x00,0x04,0x00,0x04,0x00,0x0C,0x00,0x00},/*"L",44*/
{0x10,0x04,0x1F,0xFC,0x1F,0x00,0x00,0xFC,0x1F,0x00,0x1F,0xFC,0x10,0x04,0x00,0x00},/*"M",45*/
{0x10,0x04,0x1F,0xFC,0x0C,0x04,0x03,0x00,0x00,0xE0,0x10,0x18,0x1F,0xFC,0x10,0x00},/*"N",46*/
{0x07,0xF0,0x08,0x08,0x10,0x04,0x10,0x04,0x10,0x04,0x08,0x08,0x07,0xF0,0x00,0x00},/*"O",47*/
{0x10,0x04,0x1F,0xFC,0x10,0x84,0x10,0x80,0x10,0x80,0x10,0x80,0x0F,0x00,0x00,0x00},/*"P",48*/
{0x07,0xF0,0x08,0x18,0x10,0x24,0x10,0x24,0x10,0x1C,0x08,0x0A,0x07,0xF2,0x00,0x00},/*"Q",49*/
{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x00,0x11,0xC0,0x11,0x30,0x0E,0x0C,0x00,0x04},/*"R",50*/
{0x00,0x00,0x0E,0x1C,0x11,0x04,0x10,0x84,0x10,0x84,0x10,0x44,0x1C,0x38,0x00,0x00},/*"S",51*/
{0x18,0x00,0x10,0x00,0x10,0x04,0x1F,0xFC,0x10,0x04,0x10,0x00,0x18,0x00,0x00,0x00},/*"T",52*/
{0x10,0x00,0x1F,0xF8,0x10,0x04,0x00,0x04,0x00,0x04,0x10,0x04,0x1F,0xF8,0x10,0x00},/*"U",53*/
{0x10,0x00,0x1E,0x00,0x11,0xE0,0x00,0x1C,0x00,0x70,0x13,0x80,0x1C,0x00,0x10,0x00},/*"V",54*/
{0x1F,0xC0,0x10,0x3C,0x00,0xE0,0x1F,0x00,0x00,0xE0,0x10,0x3C,0x1F,0xC0,0x00,0x00},/*"W",55*/
{0x10,0x04,0x18,0x0C,0x16,0x34,0x01,0xC0,0x01,0xC0,0x16,0x34,0x18,0x0C,0x10,0x04},/*"X",56*/
{0x10,0x00,0x1C,0x00,0x13,0x04,0x00,0xFC,0x13,0x04,0x1C,0x00,0x10,0x00,0x00,0x00},/*"Y",57*/
{0x08,0x04,0x10,0x1C,0x10,0x64,0x10,0x84,0x13,0x04,0x1C,0x04,0x10,0x18,0x00,0x00},/*"Z",58*/
};
通过不同的函数,可以控制OLED屏幕在指定的位置显示对应的字符
每次向OLED中写入8bit信息,其代码描述如下
void OLED_WR_Byte(u8 dat, u8 mode)
{
u8 i;
if(mode)
OLED_DC_Set();
else
OLED_DC_Clr();
for(i=0;i<8;i++)
{
OLED_SCL_Clr();
if(dat&0x80)
OLED_SDA_Set();
else
OLED_SDA_Clr();
OLED_SCL_Set();
dat<<=1;
}
OLED_DC_Set();
}
显示符号、字母、数字的函数描述如下
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size1)
{
u8 i,m,temp,size2,chr1;
u8 y0=y;
size2=(size1/8+((size1%8)?1:0))*(size1/2); //得到字体一个字符对应点阵集所占的字节数
chr1=chr-' '; //计算偏移后的值
for(i=0;i<size2;i++)
{
if(size1==16)
{temp=asc2_1608[chr1][i];}
else return;
for(m=0;m<8;m++) //写入数据
{
if(temp&0x80)OLED_DrawPoint(x,y);
else OLED_ClearPoint(x,y);
temp<<=1;
y++;
if((y-y0)==size1)
{
y=y0;
x++;
break;
}
}
}
}
以显示频率为例,由于频率的数据范围很大,且OLED屏幕大小有限,因此只显示其前三位数字,并通过科学计数法在数字后面进行单位的表示。
如,频率为123Hz,则显示为123;
频率为123456Hz,则显示为123k;
频率为123456789Hz,则显示为123M;
其函数的代码描述如下
void SHOW_frequency_figure(int figure){
int digit1 = 0; //个位
int digit2 = 0; //十位
int digit3 = 0; //百位
int remainder = 0;
int freUnit = 0; //单位为1
if (figure > 999 && figure <1000000){
freUnit = 1; //单位为1e3
remainder = figure/1000;
}
else if (figure > 999999){
freUnit = 2; //单位为1e6
remainder = figure/1000000;
}
else{
remainder = figure;
}
digit3 = remainder/100;//取出前三位数字
remainder = remainder - 100*digit3;
digit2 = remainder/10;
remainder = remainder - 10*digit2;
digit1 = remainder/1;
remainder = remainder - 1*digit1;
OLED_ShowChar(16,16,digit3+48,16);
OLED_ShowChar(24,16,digit2+48,16);
OLED_ShowChar(32,16,digit1+48,16);
if (freUnit == 0)OLED_ShowChar(80,16,' ',16);
else if (freUnit == 1)OLED_ShowChar(80,16,'k',16);
else if (freUnit == 2)OLED_ShowChar(80,16,'M',16);
}
对于波形,由变量wave来表示,通过三个字母在OLED上显示当前波形。
wave=0,则显示squ,表示当前波形为方波;wave=1,则显示saw,表示当前波形为锯齿波;wave=2,则显示tri,表示当前波形为三角波;wave=3,则显示sin,表示当前波形为正弦波。
2.2.2 按键与旋转编码器
在本项目中,开发板上有两个按键和一个旋转编码器与MCU相连,通过他们可以实现对波形,频率,幅度的控制。
为了保证在按键触发的第一时间进入相关程序,将按键K1、K2、旋转编码器设置为外部中断触发模式。
旋转编码器的原理十分简单,其内部相当于两个按键A、B。当旋转编码器顺时针旋转时,A先触发,B后触发;当旋转编码器逆时针旋转时,B先触发,A后触发。通过这两个按键触发的先后顺序,即可判断出旋转编码器是顺时针旋转还是逆时针旋转,旋转编码器的时序图如下所示
设置顺时针旋转表示增大参数,逆时针旋转表示减小参数,在每次参数改变后,通过SPI协议向从机FPGA发送控制信息,关于主机SPI协议和发送信息的编码过程将在2.3和2.4节中详细介绍。
按键K1控制波形的切换,按键K2选择要进行操作的参数,他们的程序描述与旋转编码器类似并且更简单,在这里不再赘述。
以下是旋转编码器的代码描述,USER CODE BEGIN - USER CODE END之外的部分由cubeMX自动生成。
void EXTI0_1_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_1_IRQn 0 */
/*************encoder control begin**********/
if (HAL_GPIO_ReadPin(encoderB_GPIO_Port, encoderB_Pin)){//顺时针,值增大
if (select_mode == 0){//控制频率
if (step == 0) frequency = frequency + 1;
else if (step == 1) frequency = frequency + 10;
else if (step == 2) frequency = frequency + 100;
else if (step == 3) frequency = frequency + 1000;
else if (step == 4) frequency = frequency + 10000;
else if (step == 5) frequency = frequency + 100000;
else if (step == 6) frequency = frequency + 1000000;
spi_frequency = 1;
spi_amplitude = 0;
spi_wave = 0;
spi_step = step;
SEND_FPGA(generate_message(spi_frequency, spi_amplitude, spi_wave, spi_step));//向FPGA发送数据
if (frequency > 11111113){
frequency = 11111113;
}
SHOW_frequency_figure(frequency);
}
else if (select_mode == 1){ //控制幅值
if (step == 0) amplitude = amplitude + 1;
else if (step == 1) amplitude = amplitude + 10;
else if (step == 2) amplitude = amplitude + 100;
else if (step == 3) amplitude = amplitude + 1000;
else if (step == 4) amplitude = amplitude + 10000;
else if (step == 5) amplitude = amplitude + 100000;
else if (step == 6) amplitude = amplitude + 1000000;
spi_frequency = 0;
spi_amplitude = 1;
spi_wave = 0;
spi_step = step;
SEND_FPGA(generate_message(spi_frequency, spi_amplitude, spi_wave, spi_step));
if (amplitude > 32){
amplitude = 32;
}
SHOW_amplitude_figure(amplitude);
}
else if (select_mode == 2){ //cotrol step
step = step + 1;
if (step > 6)step = 6;
spi_frequency = 0;
spi_amplitude = 0;
spi_wave = 0;
spi_step = step;
SEND_FPGA(generate_message(spi_frequency, spi_amplitude, spi_wave, spi_step));
SHOW_step_figure(step);
}
else{}
}
else if (!HAL_GPIO_ReadPin(encoderB_GPIO_Port, encoderB_Pin)){//逆时针,值减小
if (select_mode == 0){//control frequency
if (step == 0) frequency = frequency - 1;
else if (step == 1) frequency = frequency - 10;
else if (step == 2) frequency = frequency - 100;
else if (step == 3) frequency = frequency - 1000;
else if (step == 4) frequency = frequency - 10000;
else if (step == 5) frequency = frequency - 100000;
else if (step == 6) frequency = frequency - 1000000;
spi_frequency = 2;
spi_amplitude = 0;
spi_wave = 0;
spi_step = step;
SEND_FPGA(generate_message(spi_frequency, spi_amplitude, spi_wave, spi_step));
if (frequency < 1){
frequency = 1;
}
SHOW_frequency_figure(frequency);
}
else if (select_mode == 1){ //control amplitude
if (step == 0) amplitude = amplitude - 1;
else if (step == 1) amplitude = amplitude - 10;
else if (step == 2) amplitude = amplitude - 100;
else if (step == 3) amplitude = amplitude - 1000;
else if (step == 4) amplitude = amplitude - 10000;
else if (step == 5) amplitude = amplitude - 100000;
else if (step == 6) amplitude = amplitude - 1000000;
spi_frequency = 0;
spi_amplitude = 2;
spi_wave = 0;
spi_step = step;
SEND_FPGA(generate_message(spi_frequency, spi_amplitude, spi_wave, spi_step));
if (amplitude < 1){
amplitude = 1;
}
SHOW_amplitude_figure(amplitude);
}
else if (select_mode == 2){ //control step
step = step -1;
if (step < 0)step = 0;
spi_frequency = 0;
spi_amplitude = 0;
spi_wave = 0;
spi_step = step;
SEND_FPGA(generate_message(spi_frequency, spi_amplitude, spi_wave, spi_step));
SHOW_step_figure(step);
}
else{}
}
else{}
/*************encoder control end**********/
/*************SPI_with_FPGA begin**********/
/*************SPI_with_FPGA end**********/
/* USER CODE END EXTI0_1_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(encoderA_Pin);
/* USER CODE BEGIN EXTI0_1_IRQn 1 */
/* USER CODE END EXTI0_1_IRQn 1 */
}
2.2.3 参数通信编码
参数通信的编码方式已在1.4节中进行过陈述,本节中只对其代码实现进行讨论。
MCU发送给FPGA的信息共有8bit,其中包含了四个信息,频率、幅值、波形、步长,将他们分别定义为fre、amp、wave、step。将这四个变量输入编码函数,即可获得需要发送的8bit信息,其代码描述如下
int generate_message(int fre, int amp, int wave, int step){
uint8_t message = 0;
int i = 0;
for (i = 0; i<4; i++){
if (i == 0){
message <<= 2;
message = message + fre;
}
else if (i == 1){
message <<= 2;
message = message + amp;
}
else if (i == 2){
message <<= 1;
message = message + wave;
}
else if (i == 3){
message <<= 3;
message = message + step;
}
else{}
}
return message;
}
2.2.4 主机SPI协议
在2.3节中,我们得到了通过四个参数编码产生的8bit信息,得到了这8bit信息后,还要将其转化为电平信号,并且按照SPI协议的规则配置时钟信号和片选信号,FPGA才能顺利的接收到这些信息并将其解码。MCU的22-25引脚与FPGA相连接,通过这四根引脚,即可按照SPI的规则发送信息。将要发送的8bit信息输入SPI函数,即可将这些信息转化为电平信号发送至FPGA,其代码描述如下
void SEND_FPGA (uint8_t Txdata){
HAL_GPIO_WritePin(F_SPI_CS_GPIO_Port, F_SPI_CS_Pin, GPIO_PIN_RESET);//片选使能
//HAL_SPI_TransmitReceive(&hspi1, Txdata, Rxdata, 8, 100);
int i;
for(i=0;i<8;i++){
HAL_GPIO_WritePin(F_SPI_SCK_GPIO_Port, F_SPI_SCK_Pin, GPIO_PIN_RESET);
if(Txdata&0x80){
HAL_GPIO_WritePin(F_SPI_MOSI_GPIO_Port, F_SPI_MOSI_Pin, GPIO_PIN_SET);
}
else{
HAL_GPIO_WritePin(F_SPI_MOSI_GPIO_Port, F_SPI_MOSI_Pin, GPIO_PIN_RESET);
}//信息已经准备好了
HAL_GPIO_WritePin(F_SPI_SCK_GPIO_Port, F_SPI_SCK_Pin, GPIO_PIN_SET);//主机时钟上升沿,从机开始接收信息
Txdata<<=1;
}
HAL_GPIO_WritePin(F_SPI_CS_GPIO_Port, F_SPI_CS_Pin, GPIO_PIN_SET);//片选关闭
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);//LED翻转,表示传输了一次信号
}
3.结语
在这次寒假在家练的项目中,我收获良多,由于此前从未接触过MCU开发,并且对FPGA开发的了解也极为有限,本次项目对我而言是一个巨大的挑战。从零开始学习FPGA、STM32,在此过程中我遇到了许多困难,不理解Verilog的并行逻辑,程序结构漏洞百出;误删代码,导致一天的工作白费;下载了旧版的原理图,使用了错误的芯片编号导致MCU的开发无法展开;没有ST-Link,调试代码只能一行一行手动计算变量的变更;没接触过SPI,只能从时序图开始一点点学起。在这过程中我无数次想过放弃,又无数次告诉自己既然已经做了这么多,不如坚持到底把它完成,要不然可是没法退款了^V^。幸运的是,我一路磕磕绊绊,最终完成了本次项目,当看到设定的波形在示波器上闪烁时,我有了一种前所未有的成就感。在本次项目的开发过程中,我学习到了很多新的知识,同时也有了许多新的问题,有了这次项目的历练,在以后的学习和工作中,我有足够的信心继续坚持下去,直到完成目标。
感谢硬禾学堂的老师在直播中尽心的讲解,感谢微信交流群中的同学和老师对我问题的无私解答!