一、项目要求
利用ADC制作一个数字电压表
1.旋转电位计可以产生0-3.3V的电压
2.利用板上的串行ADC对电压进行转换
3.将电压值在板上的OLED屏幕上显示出来
二、设计思路
1.基本功能模块
ADC驱动:利用板上ADS7868芯片对电位计电压进行采集,并得到二进制电压倍数传给数据模块进行处理。
数据转换模块:将ADC传来数据先转换为二进制电压数据,再变为BCD码表示的电压值。
OLED驱动:由BCD码电压值在OLED屏幕上显示出来。
2.模块框图
图1 模块框图
如图所示,本项目的基本模块连接情况。芯片产生12MHz的晶振,经分频模块产生6MHz的时钟供ADC驱动采样使用。ADC驱动在6MHz的时钟频率下,驱动ADS7868芯片工作,以SPI通信方式对芯片输出时钟信号(adc_clk)与片选信号(adc_cs),接收到二进制形式的采样电压倍数(adc_in),并输出每次采样电压(adc_data)。此电压经数值转换模块得到BCD码电压(bcd_code),送给OLED驱动模块使用。OLED驱动模块得到要显示的BCD码后,同样以SPI的通信方式对芯片SSD1306输出指令与数据,从而实现OLED显示。详细的接口信息在下一部分说明。
3.RTL级电路图
图2 RTL级电路
三、各模块代码及介绍
1 ADC驱动模块(ADC_driver.v)
本模块的编写参考了简易电压表设计项目中ADC081S101驱动的编写思路。
1.1 时序分析
图3 芯片采样时序图
时序图信息:①三个时钟周期的采样保持时间,输出数据无效;
②时钟下降沿输出数据变化,在时钟高电平采集数据;
③本芯片共输出8位有效数据;
④芯片采样至少需要12个时钟周期,添加几个延时周期方便数据传输。这里选择使用14个时钟周期。
1.2 编程思路
FPGA芯片与ADS7868芯片采用SPI通信协议,时钟信号由FPGA设计一个计数器控制输出,按时序图实现对控制芯片的采样。
图4 ADS7868采样参数
由其采样频率的参数可知,ADC的3MHz的时钟频率即可,故驱动应有6MHz的系统时钟,添加一个分频模块(clock_divide.v)。
1.3 接口
input clk, //系统时钟
input rst_n, //系统复位,低有效
output reg adc_cs, //SPI总线CS
output reg adc_clk, //SPI总线SCK
input adc_in, //SPI总线SDA
output reg [7:0] adc_data //ADC采样数据
1.4 代码
module ADC_driver
(
input clk, //系统时钟
input rst_n, //复位信号
output reg adc_cs, //SPI总线CS
output reg adc_clk, //SPI总线SCK
input adc_in, //SPI总线SDA
output reg [7:0] adc_data //ADC采样数据
);
parameter HIGH = 1;
parameter LOW = 0;
reg [7:0] cnt; //计数器
always @(posedge clk or negedge rst_n)
if(!rst_n) cnt <= 1'b0;
else if(cnt >= 8'd28) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
reg [7:0] data;
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
adc_cs <= HIGH; adc_clk <= HIGH;
end else case(cnt)
8'd0 : begin adc_cs <= HIGH; adc_clk <= HIGH; end
8'd1 : begin adc_cs <= LOW; adc_clk <= HIGH; end //CS信号片选 进入采样周期
8'd2,8'd4,8'd6,8'd8,8'd10,8'd12,8'd14,8'd16,
8'd18,8'd20,8'd22,8'd24,8'd26:
begin adc_cs <= LOW; adc_clk <= LOW; end
8'd3 : begin adc_cs <= LOW; adc_clk <= HIGH; end //0 三个无效数据周期
8'd5 : begin adc_cs <= LOW; adc_clk <= HIGH; end //1
8'd7 : begin adc_cs <= LOW; adc_clk <= HIGH; end //2
8'd9 : begin adc_cs <= LOW; adc_clk <= HIGH; data[7] <= adc_in; end //3 按MSB顺序采集8个有效数据
8'd11 : begin adc_cs <= LOW; adc_clk <= HIGH; data[6] <= adc_in; end //4
8'd13 : begin adc_cs <= LOW; adc_clk <= HIGH; data[5] <= adc_in; end //5
8'd15 : begin adc_cs <= LOW; adc_clk <= HIGH; data[4] <= adc_in; end //6
8'd17 : begin adc_cs <= LOW; adc_clk <= HIGH; data[3] <= adc_in; end //7
8'd19 : begin adc_cs <= LOW; adc_clk <= HIGH; data[2] <= adc_in; end //8
8'd21 : begin adc_cs <= LOW; adc_clk <= HIGH; data[1] <= adc_in; end //9
8'd23 : begin adc_cs <= LOW; adc_clk <= HIGH; data[0] <= adc_in; end //10
8'd25 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_data <= data; end //11 结束采样 拉高CS信号
8'd27 : begin adc_cs <= LOW; adc_clk <= HIGH; end //12
8'd28 : begin adc_cs <= HIGH; adc_clk <= HIGH; end
default : begin adc_cs <= HIGH; adc_clk <= HIGH; end
endcase
endmodule
2.十进制码转换模块(bin2num.v)
将二进制数转换成BCD码的形式,采用左移加三的算法。
2.1 接口
input [15:0]bin_code, //二进制电压
input rst_n, //复位信号
output reg[19:0] bcd_code //BCD码电压
2.2 代码
module bin2num(
input [15:0]bin_code,
input rst_n,
output reg[19:0] bcd_code
);
reg[35:0] shift_reg;
always@(bin_code or rst_n)begin
shift_reg = {20'h0,bin_code};
if(!rst_n) bcd_code = 0;
else begin
repeat(16) begin //循环16次
//BCD码各位数据作满5加3操作,
if (shift_reg[19:16] >= 5) shift_reg[19:16] = shift_reg[19:16] + 2'b11;
if (shift_reg[23:20] >= 5) shift_reg[23:20] = shift_reg[23:20] + 2'b11;
if (shift_reg[27:24] >= 5) shift_reg[27:24] = shift_reg[27:24] + 2'b11;
if (shift_reg[31:28] >= 5) shift_reg[31:28] = shift_reg[31:28] + 2'b11;
if (shift_reg[35:32] >= 5) shift_reg[35:32] = shift_reg[35:32] + 2'b11;
shift_reg = shift_reg << 1;
end
bcd_code = shift_reg[35:16];
end
end
endmodule
3.OLED驱动模块(OLED_SSD1306.v)
OLED驱动即是与OLED屏控制芯片SSD1306进行通信,此部分的完整编写难度较大,并未改变寻址方式,这里使用了电子森林OLED驱动说明及Verilog代码实例项目源码进行修改得到了输出。
3.1 编程思路:
①读取cmd_RAM中存储的初始化命令进行寻址方式等相关设置;
②利用状态机讲显示分为五个部分:待机(IDLE)、主要状态(MAIN)、初始化状态(INIT)、刷屏状态(SCAN)、数据发送状态(WRITE)、延时状态(DELAY)。
③MAIN状态:设置每一行输入的字符存储到char寄存器中。在MAIN状态中完成对工作状态的切换,以实现OLED驱动的运转。这也是在相同格式下输出不同内容要修改的地方。
INIT状态:执行预存储的命令完成OLED初始化;
SCAN状态:读取每个字符8*8矩阵中对应的LED显示数组(前3*8矩阵起分割作用),存储到char_reg中;
WRITE状态:SPI时序将数据发送给屏幕点亮对应点位。
④配置命令CMD与字库数据在RAM中以便使用。
3.2 接口
output reg oled_csn, //OLCD液晶屏使能
output reg oled_rst, //OLCD液晶屏复位
output reg oled_dcn, //OLCD数据指令控制 控制写入数据为数据还是指令
output reg oled_clk, //OLCD时钟信号
output reg oled_dat //OLCD数据信号
3.3 修改部分代码
MAIN:begin
if(cnt_main >= 5'd5) cnt_main <= 5'd5;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
5'd0: begin state <= INIT; end
//输出格式编写
5'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Project: ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "ADC_voltmeter ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "V: ";state <= SCAN; end
//一位数字对应4位BCD码,一个char的输出对应8位,故此处用4'b0+bcd_code[]补足一个char型结构
5'd5: begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 5; char <= {bcd_code[19:16],".",4'b0,bcd_code[15:12],4'b0,bcd_code[11:8],"V"}; state <= SCAN; end
default: state <= IDLE;
endcase
end
4.顶层模块(top.v)
按设计思路对各个基本模块进行连接。
module top
(
input clk,
input rst_n,
//ADC管脚
input adc_in,
output adc_clk,
output adc_cs,
//OLED驱动管脚
output oled_csn,
output oled_rst,
output oled_dcn,
output oled_clk,
output oled_dat
);
wire clk_6M;
//时钟分频
clock_divide u4
(
.clk(clk),
.rst_n(rst_n),
.clk_6M(clk_6M)
);
//ADC采样
wire[7:0] adc_data;
ADC_driver u1
(
.clk(clk_6M), //系统时钟
.rst_n(rst_n), //系统复位,低有效
.adc_cs(adc_cs), //SPI总线CS
.adc_clk(adc_clk), //SPI总线SCK
.adc_in(adc_in), //SPI总线SDA
.adc_data(adc_data) //ADC采样数据
);
//ADC采样二进制码转换电压BCD码
wire [15:0] bin_code = adc_data * 16'd129;
wire[19:0] bcd_code;
bin2num u2(
.bin_code(bin_code),
.rst_n(rst_n),
.bcd_code(bcd_code)
);
//BCD码显示
OLED_SSD1306 u3
(
.clk(clk),
.rst_n(rst_n),
.bcd_code(bcd_code),
.oled_csn(oled_csn),
.oled_rst(oled_rst),
.oled_dcn(oled_dcn),
.oled_clk(oled_clk),
.oled_dat(oled_dat)
);
endmodule
四、项目总结与心得
本次项目是我第一次完整的接触到FPGA项目的实战,小脚丫开发板虽然体积不大,但功能对我这样的初学者也十分足够,小小一块板子也有足够的东西去学习。
在本次项目中,让我学到了许多。我第一次明白了如何用Verilog描述芯片的时序图,在以前总是对时序的学习是纸上谈兵的感觉,这次终于理解了它工作起来的面貌;其次,芯片通信协议其实就藏在芯片使用之中,之前学习通信时序时,总觉得时序十分玄妙,这次在编写两个驱动时真切感受到了了SPI时序并不如我所想的一样;最后则是各种硬件电路与芯片功能上知识的积累。
同样本次实习中也还存在一些遗憾,未能自己改变OLED的驱动方式,自己写出一套命令对OLED进行配置,希望以后能有所长进。
总之,在本次“暑期一起练”活动中,FPGA的学习让我乐在其中,受益匪浅!
五、成果演示
本项目完整工程文件附在附件中。