这个基于FPGA的电压表项目实现了一个完整的模拟信号采集与显示系统。系统采用FPGA作为核心控制器,通过TLC549 ADC芯片采集模拟电压信号,将采集到的数字量通过LC1602液晶屏实时显示,同时支持通过串口将数据传输到上位机。整个系统展示了FPGA在嵌入式测量系统中的典型应用场景。
提示:对于FPGA初学者来说,这个项目涵盖了数字系统设计的多个关键环节,包括外设控制、时序设计、数据处理和通信协议实现,是非常好的综合实践案例。
在本项目中,我们选择Xilinx Spartan-6系列XC6SLX9作为主控芯片,主要基于以下考虑:
TLC549是8位串行ADC,其优势在于:
LC1602字符型LCD具有以下特点:
整个系统的硬件架构如下图所示:
code复制模拟信号 → TLC549(ADC) → FPGA → LC1602显示
↓
串口 → PC
FPGA需要实现以下功能模块:
TLC549采用三线制串行接口:
典型工作时序:
verilog复制module adc_controller(
input clk, // 系统时钟(50MHz)
input reset, // 系统复位
output reg cs_n, // 片选信号
output reg sclk, // 串行时钟
input dout, // ADC数据输出
output reg [7:0] adc_data, // 采集数据
output reg data_valid // 数据有效标志
);
// 状态定义
localparam IDLE = 2'b00;
localparam CONVERT = 2'b01;
localparam READ = 2'b10;
reg [1:0] state;
reg [3:0] bit_cnt;
reg [15:0] timer;
always @(posedge clk or posedge reset) begin
if(reset) begin
state <= IDLE;
cs_n <= 1'b1;
sclk <= 1'b0;
bit_cnt <= 0;
adc_data <= 0;
data_valid <= 0;
timer <= 0;
end else begin
case(state)
IDLE: begin
cs_n <= 1'b0; // 启动转换
timer <= 0;
state <= CONVERT;
end
CONVERT: begin
if(timer >= 850) begin // 17μs@50MHz
state <= READ;
bit_cnt <= 0;
end else
timer <= timer + 1;
end
READ: begin
if(bit_cnt < 8) begin
if(sclk) begin // 下降沿采样
adc_data[7-bit_cnt] <= dout;
bit_cnt <= bit_cnt + 1;
end
sclk <= ~sclk;
end else begin
cs_n <= 1'b1;
data_valid <= 1'b1;
state <= IDLE;
end
end
endcase
end
end
endmodule
注意事项:TLC549的SCLK最高频率为1.1MHz,在50MHz系统时钟下需要适当分频。实际调试时建议用示波器观察时序是否符合芯片规格要求。
ADC采集到的8位数字量需要转换为实际电压值。假设参考电压Vref=5V:
code复制电压值(V) = ADC值 × Vref / 255
为避免浮点运算,可采用定点数处理:
verilog复制// 假设Vref=5000mV (5V)
wire [15:0] voltage_mv = adc_data * 5000 / 255;
为消除噪声干扰,可添加简单的移动平均滤波:
verilog复制reg [7:0] adc_buffer [0:7];
reg [2:0] buf_ptr;
reg [10:0] adc_sum; // 8个8位数据求和需要11位
always @(posedge clk) begin
if(data_valid) begin
adc_buffer[buf_ptr] <= adc_data;
buf_ptr <= buf_ptr + 1;
adc_sum <= adc_sum + adc_data - adc_buffer[buf_ptr];
end
end
wire [7:0] adc_filtered = adc_sum >> 3; // 除以8
LC1602采用并行接口,关键信号:
初始化序列:
verilog复制module lcd_driver(
input clk,
input reset,
input [15:0] voltage_mv,
output reg rs,
output reg rw,
output reg e,
output reg [7:0] db
);
// 状态定义
localparam INIT = 0;
localparam IDLE = 1;
localparam WRITE_CMD = 2;
localparam WRITE_DATA = 3;
reg [2:0] state;
reg [4:0] init_step;
reg [19:0] delay_cnt;
reg [3:0] char_pos;
reg [7:0] display_buf [0:31];
// 初始化指令序列
wire [7:0] init_cmds [0:3] = {
8'h38, // 功能设置
8'h0C, // 显示开关
8'h06, // 输入模式
8'h01 // 清屏
};
always @(posedge clk or posedge reset) begin
if(reset) begin
state <= INIT;
init_step <= 0;
e <= 0;
rs <= 0;
rw <= 0;
delay_cnt <= 0;
end else begin
case(state)
INIT: begin
if(delay_cnt == 20'd100000) begin // 等待LCD上电稳定
state <= WRITE_CMD;
db <= init_cmds[init_step];
delay_cnt <= 0;
end else
delay_cnt <= delay_cnt + 1;
end
WRITE_CMD: begin
e <= 1;
delay_cnt <= delay_cnt + 1;
if(delay_cnt == 20'd10) begin
e <= 0;
delay_cnt <= 0;
if(init_step == 4) begin
state <= IDLE;
// 更新显示缓冲区
display_buf[0] <= "V";
display_buf[1] <= "o";
display_buf[2] <= "l";
display_buf[3] <= "t";
display_buf[4] <= "a";
display_buf[5] <= "g";
display_buf[6] <= "e";
display_buf[7] <= ":";
display_buf[8] <= " ";
// 电压值转换
display_buf[9] <= (voltage_mv/1000) + "0";
display_buf[10] <= ".";
display_buf[11] <= ((voltage_mv%1000)/100) + "0";
display_buf[12] <= ((voltage_mv%100)/10) + "0";
display_buf[13] <= "V";
display_buf[14] <= " ";
display_buf[15] <= " ";
end else begin
init_step <= init_step + 1;
state <= WRITE_CMD;
db <= init_cmds[init_step];
end
end
end
IDLE: begin
// 定期刷新显示
if(delay_cnt == 20'd500000) begin
state <= WRITE_DATA;
char_pos <= 0;
delay_cnt <= 0;
end else
delay_cnt <= delay_cnt + 1;
end
WRITE_DATA: begin
e <= 1;
rs <= 1;
db <= display_buf[char_pos];
delay_cnt <= delay_cnt + 1;
if(delay_cnt == 20'd10) begin
e <= 0;
delay_cnt <= 0;
if(char_pos == 15) begin
state <= IDLE;
end else begin
char_pos <= char_pos + 1;
state <= WRITE_DATA;
end
end
end
endcase
end
end
endmodule
实操心得:LCD初始化时序非常关键,必须严格按照数据手册要求的时间参数操作。实际调试时发现,上电后需要至少15ms的等待时间才能发送第一条指令,否则可能导致初始化失败。
采用9600波特率,8数据位,无校验,1停止位:
verilog复制module uart_tx(
input clk,
input reset,
input [7:0] tx_data,
input tx_start,
output reg txd,
output reg tx_busy
);
reg [12:0] baud_cnt;
reg [3:0] bit_cnt;
reg [7:0] tx_reg;
always @(posedge clk or posedge reset) begin
if(reset) begin
txd <= 1'b1;
tx_busy <= 1'b0;
baud_cnt <= 0;
bit_cnt <= 0;
end else begin
if(tx_busy) begin
if(baud_cnt == 5207) begin // 50MHz/9600≈5208
baud_cnt <= 0;
case(bit_cnt)
0: txd <= 1'b0; // 起始位
1: txd <= tx_reg[0];
2: txd <= tx_reg[1];
3: txd <= tx_reg[2];
4: txd <= tx_reg[3];
5: txd <= tx_reg[4];
6: txd <= tx_reg[5];
7: txd <= tx_reg[6];
8: txd <= tx_reg[7];
9: begin
txd <= 1'b1; // 停止位
tx_busy <= 1'b0;
end
endcase
if(bit_cnt < 9)
bit_cnt <= bit_cnt + 1;
end else
baud_cnt <= baud_cnt + 1;
end else if(tx_start) begin
tx_reg <= tx_data;
tx_busy <= 1'b1;
bit_cnt <= 0;
baud_cnt <= 0;
end
end
end
endmodule
发送到上位机的数据格式:
verilog复制// 在顶层模块中实例化
uart_tx uart(
.clk(clk_50m),
.reset(reset),
.tx_data(tx_byte),
.tx_start(tx_start),
.txd(uart_txd),
.tx_busy(tx_busy)
);
// 数据打包状态机
reg [1:0] tx_state;
reg [15:0] tx_voltage;
reg [7:0] tx_byte;
reg tx_start;
always @(posedge clk_50m) begin
if(adc_valid && !tx_busy) begin
case(tx_state)
0: begin // 发送起始字符
tx_byte <= "V";
tx_start <= 1'b1;
tx_voltage <= voltage_mv;
tx_state <= 1;
end
1: begin // 发送高字节
if(!tx_start) begin
tx_byte <= tx_voltage[15:8];
tx_start <= 1'b1;
tx_state <= 2;
end
end
2: begin // 发送低字节
if(!tx_start) begin
tx_byte <= tx_voltage[7:0];
tx_start <= 1'b1;
tx_state <= 3;
end
end
3: begin // 发送结束符
if(!tx_start) begin
tx_byte <= "\n";
tx_start <= 1'b1;
tx_state <= 0;
end
end
endcase
end else
tx_start <= 1'b0;
end
verilog复制module voltage_meter(
input clk_50m,
input reset_n,
// ADC接口
output adc_cs_n,
output adc_sclk,
input adc_dout,
// LCD接口
output lcd_rs,
output lcd_rw,
output lcd_e,
output [7:0] lcd_db,
// UART接口
output uart_txd
);
wire reset = ~reset_n;
// ADC控制
wire [7:0] adc_data;
wire adc_valid;
adc_controller adc(
.clk(clk_50m),
.reset(reset),
.cs_n(adc_cs_n),
.sclk(adc_sclk),
.dout(adc_dout),
.adc_data(adc_data),
.data_valid(adc_valid)
);
// 数据处理
wire [15:0] voltage_mv;
data_processor data(
.clk(clk_50m),
.reset(reset),
.adc_data(adc_data),
.adc_valid(adc_valid),
.voltage_mv(voltage_mv)
);
// LCD显示
lcd_driver lcd(
.clk(clk_50m),
.reset(reset),
.voltage_mv(voltage_mv),
.rs(lcd_rs),
.rw(lcd_rw),
.e(lcd_e),
.db(lcd_db)
);
// UART发送
uart_tx uart(
.clk(clk_50m),
.reset(reset),
.tx_data(tx_byte),
.tx_start(tx_start),
.txd(uart_txd),
.tx_busy(tx_busy)
);
// UART数据打包
// ... (前面uart部分代码)
endmodule
以Xilinx ISE为例,UCF文件关键内容:
code复制NET "clk_50m" LOC = "P56" | IOSTANDARD = LVCMOS33;
NET "reset_n" LOC = "P38" | IOSTANDARD = LVCMOS33 | PULLUP;
# ADC接口
NET "adc_cs_n" LOC = "P45" | IOSTANDARD = LVCMOS33;
NET "adc_sclk" LOC = "P44" | IOSTANDARD = LVCMOS33;
NET "adc_dout" LOC = "P43" | IOSTANDARD = LVCMOS33 | PULLUP;
# LCD接口
NET "lcd_rs" LOC = "P70" | IOSTANDARD = LVCMOS33;
NET "lcd_rw" LOC = "P69" | IOSTANDARD = LVCMOS33;
NET "lcd_e" LOC = "P68" | IOSTANDARD = LVCMOS33;
NET "lcd_db[0]" LOC = "P67" | IOSTANDARD = LVCMOS33;
...
NET "lcd_db[7]" LOC = "P60" | IOSTANDARD = LVCMOS33;
# UART接口
NET "uart_txd" LOC = "P59" | IOSTANDARD = LVCMOS33;
无数据输出
数据跳动严重
实测发现:TLC549对电源噪声敏感,建议在VCC和GND之间加0.1μF去耦电容,尽量靠近芯片引脚。
无显示
显示乱码
上位机收不到数据
数据错误
多通道采集
数据记录功能
报警功能
提高测量精度
优化显示效果
增强通信能力
电源监控系统
实验测量设备
教学演示平台
这个基于FPGA的电压表项目虽然基础,但涵盖了数字系统设计的多个关键环节。在实际开发过程中,我深刻体会到良好的模块划分和严谨的时序设计对系统稳定性的重要性。特别是在混合信号系统中,处理好模拟和数字部分的干扰隔离是保证测量精度的关键。建议初学者可以从此项目入手,逐步扩展功能,最终打造出一个实用的测量仪器。