项目描述
本项目实现了基于ICE40UP5K平台的数字电压表设计,实现了测量旋转电位计产生的电压并将测量结果显示在OLED屏幕上的功能。
设计思路
硬件部分,旋转电位计的输出已经连接到比较器的同相输入端。我们所要做的,只有软件部分即(图中黄色部分)
- 驱动这个ADC电路,测量电位计产生的点位大小。
- 将测量结果映射到0-3.3V。
- 将测量结果显示到OLED屏幕
硬件介绍
ADC部分主要电路如图所示,其中Ain2端口已经默认连接至滑动变阻器,如果在比较器的另一端口输入一PWM波,经滤波器滤波后同Ain2比较,结果可以读取C_OUT2获得。通过不断增大PWM占空比,最终可以逼近 Ain2的真实值。
实现的功能
项目实现的功能为测量旋转电位计产生的电压并将测量结果显示在OLED屏幕上。
主要代码片段
1.ADC测量模块
ADC部分程序由Lattice官网提供的Simple Sigma-Delta ADC例程修改而来,ADC_top模块为ADC部分的顶层调用模块,sigmadelta_adc模块负责获取采样值,这里展示部分核心代码,完整代码可以查看文末工程文件。
module sigmadelta_adc (
clk,
rstn,
digital_out,
analog_cmp,
analog_out,
sample_rdy);
parameter
ADC_WIDTH = 8, // ADC Convertor Bit Precision
ACCUM_BITS = 10, // 2^ACCUM_BITS is decimation rate of accumulator
LPF_DEPTH_BITS = 3; // 2^LPF_DEPTH_BITS is decimation rate of averager
//input ports
input clk; // sample rate clock
input rstn; // async reset, asserted low
input analog_cmp ; // input from LVDS buffer (comparitor)
//output ports
output analog_out; // feedback to comparitor input RC circuit
output sample_rdy; // digital_out is ready
output [ADC_WIDTH-1:0] digital_out; // digital output word of ADC
reg delta; // captured comparitor output
reg [ACCUM_BITS-1:0] sigma; // running accumulator value
reg [ADC_WIDTH-1:0] accum; // latched accumulator value
reg [ACCUM_BITS-1:0] counter; // decimation counter for accumulator
reg rollover; // decimation counter terminal count
reg accum_rdy; // latched accumulator value 'ready'
always @ (posedge clk)
begin
delta <= analog_cmp; // capture comparitor output
end
assign analog_out = delta; // feedback to comparitor LPF
always @ (posedge clk or negedge rstn)
begin
if( ~rstn )
begin
sigma <= 0;
accum <= 0;
accum_rdy <= 0;
end else begin
if (rollover) begin
// latch top ADC_WIDTH bits of sigma accumulator (drop LSBs)
accum <= sigma[ACCUM_BITS-1:ACCUM_BITS-ADC_WIDTH];
sigma <= delta; // reset accumulator, prime with current delta value
end else begin
if (&sigma != 1'b1) // if not saturated
sigma <= sigma + delta; // accumulate
end
accum_rdy <= rollover; // latch 'rdy' (to align with accum)
end
end
box_ave #(
.ADC_WIDTH(ADC_WIDTH),
.LPF_DEPTH_BITS(LPF_DEPTH_BITS))
box_ave (
.clk(clk),
.rstn(rstn),
.sample(accum_rdy),
.raw_data_in(accum),
.ave_data_out(digital_out),
.data_out_valid(sample_rdy)
);
always @(posedge clk or negedge rstn)
begin
if( ~rstn ) begin
counter <= 0;
rollover <= 0;
end
else begin
counter <= counter + 1; // running count
rollover <= &counter; // assert 'rollover' when counter is all 1's
end
end
endmodule
box_ave 模块则实现了对采样结果的滤波,方法是对连续采集到的N个采样值取平均值。实现了低通滤波效果。
module box_ave (
clk,
rstn,
sample,
raw_data_in,
ave_data_out,
data_out_valid);
parameter
ADC_WIDTH = 8, // ADC Convertor Bit Precision
LPF_DEPTH_BITS = 4; // 2^LPF_DEPTH_BITS is decimation rate of averager
//input ports
input clk; // sample rate clock
input rstn; // async reset, asserted low
input sample; // raw_data_in is good on rising edge,
input [ADC_WIDTH-1:0] raw_data_in; // raw_data input
//output ports
output [ADC_WIDTH-1:0] ave_data_out; // ave data output
output data_out_valid; // ave_data_out is valid, single pulse
reg [ADC_WIDTH-1:0] ave_data_out;
reg [ADC_WIDTH+LPF_DEPTH_BITS-1:0] accum; // accumulator
reg [LPF_DEPTH_BITS-1:0] count; // decimation count
reg [ADC_WIDTH-1:0] raw_data_d1; // pipeline register
reg sample_d1, sample_d2; // pipeline registers
reg result_valid; // accumulator result 'valid'
wire accumulate; // sample rising edge detected
wire latch_result; // latch accumulator result
always @(posedge clk or negedge rstn)
begin
if( ~rstn ) begin
sample_d1 <= 0;
sample_d2 <= 0;
raw_data_d1 <= 0;
result_valid <= 0;
end else begin
sample_d1 <= sample; // capture 'sample' input
sample_d2 <= sample_d1; // delay for edge detection
raw_data_d1 <= raw_data_in; // pipeline
result_valid <= latch_result; // pipeline for alignment with result
end
end
assign accumulate = sample_d1 && !sample_d2; // 'sample' rising_edge detect
assign latch_result = accumulate && (count == 0); // latch accum. per decimation count
always @(posedge clk or negedge rstn)
begin
if( ~rstn ) begin
count <= 0;
end else begin
if (accumulate) count <= count + 1; // incr. count per each sample
end
end
always @(posedge clk or negedge rstn)
begin
if( ~rstn ) begin
accum <= 0;
end else begin
if (accumulate)
if(count == 0) // reset accumulator
accum <= raw_data_d1; // prime with first value
else
accum <= accum + raw_data_d1; // accumulate
end
end
always @(posedge clk or negedge rstn)
begin
if( ~rstn ) begin
ave_data_out <= 0;
end else if (latch_result) begin // at end of decimation period...
ave_data_out <= accum >> LPF_DEPTH_BITS; // ... save accumulator/n result
end
end
assign data_out_valid = result_valid; // output assignment
endmodule
2. ADC采样得到的采样值为8位二进制数,为了阅读的方便,我将采样值映射到0~3.30。映射采用的方法是依次获取个位,十分位和百分位的数值,个位和百分位的获取是通过直接比较的方法,而十分位的获取是通过累加的方法。
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_main <= 3'd1;
cnt_tens <= 3'd0;
cnt_ones <= 3'd0;
temp_A <= A;
temp_ones <= 4'd0;
final_hundreds <= 2'd0;
temp_tens <= 4'd0;
final_ones <= 4'd0;
final_tens <= 4'd0;
rdy <= 0;
end else begin
case (cnt_main)
INIT1:begin
cnt_main <= 3'd1;
cnt_tens <= 3'd0;
cnt_ones <= 3'd0;
temp_A <= A;
temp_ones <= 4'd0;
final_hundreds <= 2'd0;
temp_tens <= 4'd0;
final_ones <= 4'd0;
final_tens <= 4'd0;
rdy <= 0;
end
HUNDREDS1:begin
cnt_main <= 3'd02;
if(temp_A > 8'd231) begin final_hundreds <= 2'd03; temp_A <= temp_A - 8'd231;end
else if(temp_A > 8'd154) begin final_hundreds <= 2'd02; temp_A <= temp_A - 8'd154;end
else if(temp_A > 8'd77) begin final_hundreds <= 2'd01; temp_A <= temp_A - 8'd77;end
end
TENS1:begin
case(cnt_tens)
3'd00: begin if(temp_A > 8'd62) begin temp_A <= temp_A - 8'd62; temp_tens <= temp_tens + 8'd08; end cnt_tens <= cnt_tens + 3'd01; end
3'd01: begin if(temp_A > 8'd31) begin temp_A <= temp_A - 8'd31; temp_tens <= temp_tens + 8'd04; end cnt_tens <= cnt_tens + 3'd01; end
3'd02: begin if(temp_A > 8'd15) begin temp_A <= temp_A - 8'd15; temp_tens <= temp_tens + 8'd02; end cnt_tens <= cnt_tens + 3'd01; end
3'd03: begin if(temp_A > 8'd07) begin temp_A <= temp_A - 8'd07; temp_tens <= temp_tens + 8'd01; end cnt_tens <= cnt_tens + 3'd01; end
3'd04: begin cnt_main <= 3'd03; final_tens <= temp_tens; cnt_tens <= 3'd0; end
default: ;
endcase
end
ONES1:begin
case(temp_A)
8'd01: final_ones <= 8'd01;
8'd02: final_ones <= 8'd03;
8'd03: final_ones <= 8'd04;
8'd04: final_ones <= 8'd05;
8'd05: final_ones <= 8'd06;
8'd06: final_ones <= 8'd08;
8'd07: final_ones <= 8'd09;
default: final_ones <= 8'd00;
endcase
rdy <= 1;
cnt_main <= 3'd04;
end
default: cnt_main <= 3'd00;
endcase
end
end
3.最终,将获得的电压数值显示到OLED屏幕上,为了便于观看,我修改了电子森林所提供的例程,对字体进行了放大,此外修改了部分代码,实现了电压显示部分的不断刷新。这里展示程序的主要循环流程。
if(!rst_n) begin
cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;cnt_chinese <= 1'b0;
y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
num <= 1'b0; char <= " "; char_reg <= 1'b0;
num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;
oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
state <= IDLE; state_back <= IDLE;
end else begin
case(state)
IDLE:begin
cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;
y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
num <= 1'b0; char <= " "; char_reg <= 1'b0;
num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;mem_hanzi_num<=8'd0;
oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
state <= MAIN; state_back <= MAIN;
end
MAIN:begin
if(cnt_main >= 5'd18) cnt_main <= 5'd17;//接下来执行空操作,实现数据只刷新一次
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 <= " ";state <= SCAN; end
5'd2 : begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";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 <= " ";state <= SCAN; end
5'd5 : begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd6 : begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd7 : begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd8 : begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd9 : begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd0; state <= CHINESE; end
5'd10: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd2; state <= CHINESE; end
5'd11: begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd4; state <= CHINESE; end
5'd12: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd6; state <= CHINESE; end
5'd13: begin y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 8'd8; state <= CHINESE; end
5'd14: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 8'd10; state <= CHINESE; end
5'd15: begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_hanzi_num <= 8'd12; state <= CHINESE; end
5'd16: begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; mem_hanzi_num <= 8'd14; state <= CHINESE; end
5'd17: begin y_p <= 8'hb6; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd6; char <= str[48:0] ;state <= SCAN; end
5'd18: ;//begin y_p <= 8'hb4; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd6; char <= "World!";state <= SCAN; end
default: state <= IDLE; //如果你需要动态刷新一些信息,此行应该取消注释
endcase
end
INIT:begin //初始化状态
case(cnt_init)
5'd0: begin oled_rst <= LOW; cnt_init <= cnt_init + 1'b1; end //复位有效
5'd1: begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end //延时大于3us
5'd2: begin oled_rst <= HIGH; cnt_init <= cnt_init + 1'b1; end //复位恢复
5'd3: begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end //延时大于220us
5'd4: begin
if(cnt>=INIT_DEPTH) begin //当25条指令及数据发出后,配置完成
cnt <= 1'b0;
cnt_init <= cnt_init + 1'b1;
end else begin
cnt <= cnt + 1'b1; num_delay <= 16'd5;
oled_dcn <= CMD; char_reg <= cmd[cnt]; state <= WRITE; state_back <= INIT;
end
end
5'd5: begin cnt_init <= 1'b0; state <= MAIN; end //初始化完成,返回MAIN状态
default: state <= IDLE;
endcase
end
遇到的主要难题
在实现的过程中,我在放大汉字的过程中遇到了一些困难。因为起初不是很熟悉屏幕地址写入的规则 ,所以显示的汉字总是出现一些形变和杂点。
最终我重新设置汉字的拆分规则,将汉字拆分为左上,右上,左下,右下4个部分,解决了汉字的正常显示问题。
同时,如何将采样得到的八位二进制数,转化为0-3.3V电压值也曾给我困惑。我学习了如何将二进制码转化为十进制码,实现了在屏幕上显示0-255十进制数。但是进一步的转化反而更加困难。因此我采用了一种比较原始但十分有效的方法,即通过直接比较的方式,分别获取电压值的个位,十分位和百分位(具体过程可以浏览上面展示的代码)。
此外,我也遇到了一个至今也不能解释的问题:即其中用到的一个计数器尽管已经有了响应的初始化代码,系统依然不能正常初始化,表现为计数器计数频率明显偏高。只有在手动按下rst键进行初始化之后才能正常运行。对此,我参考了如下内容,设计了一个开机自动触发的信号对程序进行初始化操作,最终解决了这个问题。
未来可以改进的地方
通过波形图展示旋转电位计产生电压的变化
项目资源使用报告
Number of slice registers: 386 out of 5280 (7%)
Number of I/O registers: 5 out of 117 (4%)
Number of LUT4s: 1340 out of 5280 (25%)
Number of logic LUT4s: 1032
Number of inserted feedthru LUT4s: 68
Number of replicated LUT4s: 32
Number of ripple logic: 104 (208 LUT4s)
Number of IO sites used: 12 out of 39 (31%)
Number of IO sites used for general PIO: 12
Number of IO sites used for I3C: 0 out of 2 (0%)
(note: If I3C is not used, its site can be used as general PIO)
Number of IO sites used for PIO+I3C: 12 out of 36 (33%)
Number of IO sites used for OD+RGB IO buffers: 0 out of 3 (0%)
(note: If RGB LED drivers are not used, sites can be used as OD outputs,
see TN1288 iCE40 LED Driver Usage Guide)
Number of IO sites used for PIO+I3C+OD+RGB: 12 out of 39 (31%)
Number of DSPs: 0 out of 8 (0%)
Number of I2Cs: 0 out of 2 (0%)
Number of High Speed OSCs: 0 out of 1 (0%)
Number of Low Speed OSCs: 0 out of 1 (0%)
Number of RGB PWM: 0 out of 1 (0%)
Number of RGB Drivers: 0 out of 1 (0%)
Number of SCL FILTERs: 0 out of 2 (0%)
Number of SRAMs: 0 out of 4 (0%)
Number of WARMBOOTs: 0 out of 1 (0%)
Number of SPIs: 0 out of 2 (0%)
Number of EBRs: 6 out of 30 (20%)
Number of PLLs: 0 out of 1 (0%)
Number of Clocks: 2
引脚分配
(yrgb引脚系学习过程中使用,可以不分配引脚)