1. 项目概述
这个基于FPGA的温度采集系统项目,是我去年为一个工业烤箱温度监控需求开发的完整解决方案。核心功能是通过Max6675热电偶转换器采集温度数据,通过FPGA进行数据处理,最终在上位机软件中实时显示温度曲线并实现远程控制。整套系统实现了从传感器到控制终端的完整链路,特别适合需要高精度温度监控的工业场景。
在实际项目中,我发现很多工程师在搭建类似系统时,常常面临FPGA驱动开发效率低、上位机软件与硬件通信不稳定、温度曲线绘制卡顿等问题。这个项目通过优化SPI通信协议、采用双缓冲绘图技术等方案,成功解决了这些痛点。系统实测温度采集精度达到±2℃,刷新率最高可达20Hz,完全满足大多数工业场景的需求。
2. 硬件系统设计
2.1 核心器件选型
FPGA选型:最终选用Xilinx Artix-7系列XC7A35T芯片,主要考虑因素包括:
- 内置硬件SPI控制器,可降低驱动开发难度
- 足够的逻辑单元(33,280个LUT)应对数据处理需求
- 性价比高,批量采购单价控制在200元以内
温度传感器:Max6675是系统的关键部件,其特点包括:
- 直接支持K型热电偶输入
- 内置冷端补偿电路
- 12位分辨率(0.25℃)
- SPI接口输出
- 工作温度范围0-1024℃
注意:Max6675的SPI时钟最高5MHz,过高的时钟频率会导致数据读取错误。实际项目中我设置为4MHz,在稳定性和速度间取得平衡。
2.2 硬件连接设计
FPGA与Max6675的典型连接方式:
code复制FPGA Max6675
MOSI --> SO (数据输出)
MISO <-- CS (片选)
SCK --> SCK (时钟)
GND --> GND
3.3V --> VCC
硬件设计中的几个关键点:
- 必须为每路Max6675添加0.1μF去耦电容
- 热电偶导线需使用双绞线,长度不超过1米
- 避免将Max6675放置在高温区域(>85℃)
3. FPGA驱动开发
3.1 SPI通信协议实现
Max6675的SPI时序有特殊要求:
- CS拉低后需等待至少100ns才能发送时钟
- 数据在时钟下降沿有效
- 完整读取周期需要16个时钟脉冲
Verilog核心代码片段:
verilog复制module max6675_driver (
input wire clk,
input wire reset,
output reg cs_n,
output reg sck,
input wire so,
output reg [15:0] temp_data,
output reg data_valid
);
// 状态机定义
parameter IDLE = 2'b00;
parameter START = 2'b01;
parameter SHIFT = 2'b10;
parameter DONE = 2'b11;
reg [1:0] state;
reg [4:0] bit_count;
reg [15:0] shift_reg;
always @(posedge clk or posedge reset) begin
if (reset) begin
state <= IDLE;
cs_n <= 1'b1;
sck <= 1'b0;
bit_count <= 5'd0;
shift_reg <= 16'd0;
data_valid <= 1'b0;
end else begin
case (state)
IDLE: begin
cs_n <= 1'b1;
sck <= 1'b0;
if (start_conversion) begin
state <= START;
cs_n <= 1'b0;
end
end
START: begin
if (delay_counter == DELAY_VALUE) begin
state <= SHIFT;
sck <= 1'b1;
end
end
SHIFT: begin
sck <= ~sck;
if (sck) begin // 下降沿采样
shift_reg <= {shift_reg[14:0], so};
bit_count <= bit_count + 1;
if (bit_count == 15) begin
state <= DONE;
end
end
end
DONE: begin
cs_n <= 1'b1;
temp_data <= shift_reg;
data_valid <= 1'b1;
state <= IDLE;
end
endcase
end
end
endmodule
3.2 温度数据处理
从Max6675读取的16位数据中:
- D15为无效位(恒为0)
- D14为热电偶开路检测位(1表示开路)
- D13-D2为温度数据(单位0.25℃)
- D1-D0为器件识别位
温度转换公式:
code复制实际温度 = (raw_data >> 3) * 0.25
FPGA中实现温度转换的代码:
verilog复制wire [12:0] temp_raw = temp_data[15:3];
wire thermocouple_open = temp_data[14];
reg [15:0] temperature;
always @(posedge clk) begin
if (data_valid) begin
if (thermocouple_open) begin
temperature <= 16'hFFFF; // 错误标志
end else begin
temperature <= (temp_raw * 25) / 100; // 转换为0.1℃分辨率
end
end
end
4. 上位机软件设计
4.1 QT框架选择
选用QT5.15 LTS版本,主要优势:
- 跨平台特性(Windows/Linux均可运行)
- 强大的图表库(QCustomPlot)
- 完善的串口通信支持(QSerialPort)
- 丰富的UI控件库
项目文件结构:
code复制TemperatureMonitor/
├── mainwindow.h
├── mainwindow.cpp
├── main.cpp
├── serialprotocol.h
├── chartwidget.h
├── chartwidget.cpp
└── resources/
├── styles.qss
└── icons/
4.2 通信协议设计
自定义的串行通信协议格式:
code复制帧头(0xAA) | 长度(1字节) | 命令字(1字节) | 数据(N字节) | CRC8校验 | 帧尾(0x55)
典型温度数据帧示例:
code复制AA 08 01 01 04 00 13 88 2F 55
解释:
- 08: 数据长度
- 01: 温度数据命令
- 01: 通道号
- 04 00: 时间戳(1024ms)
- 13 88: 温度数据(50.00℃)
- 2F: CRC8校验
QT中实现协议解析的关键代码:
cpp复制void SerialPortThread::processData(const QByteArray &data)
{
static QByteArray buffer;
buffer.append(data);
while(buffer.size() >= 5) { // 最小帧长度
int startPos = buffer.indexOf(0xAA);
if(startPos < 0) {
buffer.clear();
break;
}
if(startPos > 0) {
buffer = buffer.mid(startPos);
}
if(buffer.size() < 5) break;
quint8 length = static_cast<quint8>(buffer[1]);
if(buffer.size() < length + 4) break;
QByteArray frame = buffer.mid(0, length + 4);
quint8 crc = calculateCRC(frame.mid(1, length + 2));
if(static_cast<quint8>(frame[length+2]) == crc &&
static_cast<quint8>(frame[length+3]) == 0x55) {
parseFrame(frame);
buffer = buffer.mid(length + 4);
} else {
buffer = buffer.mid(1); // 校验失败,跳过1字节
}
}
}
4.3 温度曲线绘制优化
采用QCustomPlot库实现高性能绘图,关键优化点:
- 双缓冲技术:
cpp复制// 在chartwidget.cpp中
void ChartWidget::addDataPoint(double time, double temp)
{
static QVector<double> timeBuffer, tempBuffer;
static const int BUFFER_SIZE = 100;
timeBuffer.append(time);
tempBuffer.append(temp);
if(timeBuffer.size() >= BUFFER_SIZE) {
// 批量添加数据,减少重绘次数
ui->customPlot->graph(0)->addData(timeBuffer, tempBuffer);
timeBuffer.clear();
tempBuffer.clear();
}
// 自动调整X轴范围
ui->customPlot->xAxis->setRange(time, 60, Qt::AlignRight);
ui->customPlot->replot(QCustomPlot::rpQueuedReplot);
}
- 性能优化参数:
cpp复制// 在初始化代码中设置
ui->customPlot->setNotAntialiasedElements(QCP::aeAll);
ui->customPlot->setNoAntialiasingOnDrag(true);
ui->customPlot->setPlottingHints(QCP::phFastPolylines);
ui->customPlot->graph(0)->setAdaptiveSampling(true);
- 动态分辨率调整:
cpp复制void ChartWidget::onResolutionChanged(int level)
{
double pixelPerPoint = 1.0 / (1 << level); // 2^level
ui->customPlot->graph(0)->setAdaptiveSamplingTolerance(pixelPerPoint);
}
5. 系统集成与调试
5.1 硬件软件协同测试
典型的测试流程:
- 使用信号发生器模拟热电偶输出
- 通过逻辑分析仪验证SPI时序
- 使用串口调试工具验证通信协议
- 实际上电测试温度采集精度
测试中发现的一个典型问题及解决方案:
问题:当多个Max6675同时工作时,温度读数偶尔出现跳变
原因分析:电源噪声导致SPI通信错误
解决方案:
- 为每个Max6675增加LC滤波电路
- 在FPGA代码中添加SPI重试机制
- 软件端实现数据平滑滤波
5.2 系统校准方法
温度校准步骤:
- 准备标准温度源(如恒温油槽)
- 在0℃、100℃、300℃三个点进行校准
- 记录实际温度与读数偏差
- 在软件中实现分段线性补偿
QT中实现的校准算法:
cpp复制double CalibrationManager::applyCalibration(double rawTemp, int channel)
{
const CalibrationPoint &p1 = calPoints[channel][0];
const CalibrationPoint &p2 = calPoints[channel][1];
const CalibrationPoint &p3 = calPoints[channel][2];
if(rawTemp <= p2.raw) {
return p1.actual + (rawTemp - p1.raw) *
(p2.actual - p1.actual) / (p2.raw - p1.raw);
} else {
return p2.actual + (rawTemp - p2.raw) *
(p3.actual - p2.actual) / (p3.raw - p2.raw);
}
}
6. 项目扩展与优化
6.1 多通道扩展方案
对于需要监测多路温度的场景,系统可通过以下方式扩展:
- 使用FPGA的多SPI控制器并行采集
- 采用多路复用器切换热电偶输入
- 在QT界面中实现多曲线同屏显示
多通道配置示例代码:
verilog复制// FPGA端多通道控制
genvar i;
generate
for(i=0; i<8; i=i+1) begin: channel_gen
max6675_driver driver_inst (
.clk(sys_clk),
.reset(sys_reset),
.cs_n(cs_n[i]),
.sck(sck_shared),
.so(so[i]),
.temp_data(temp_data[i]),
.data_valid(data_valid[i])
);
end
endgenerate
6.2 云端数据存储与分析
系统可通过添加以下功能实现远程监控:
- 使用MQTT协议上传数据到云服务器
- 在QT中集成WebSocket客户端
- 实现历史数据查询和导出功能
QT中实现数据上传的关键代码:
cpp复制void CloudUploader::uploadTemperatureData(const QVector<TempData> &data)
{
QJsonArray jsonArray;
for(const auto &item : data) {
QJsonObject obj;
obj["timestamp"] = item.timestamp;
obj["temperature"] = item.temperature;
obj["channel"] = item.channel;
jsonArray.append(obj);
}
QNetworkRequest request(QUrl("https://api.example.com/tempdata"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = manager->post(request, QJsonDocument(jsonArray).toJson());
connect(reply, &QNetworkReply::finished, [=]() {
if(reply->error() != QNetworkReply::NoError) {
qWarning() << "Upload failed:" << reply->errorString();
}
reply->deleteLater();
});
}
6.3 控制功能实现
在QT中实现温度控制逻辑:
cpp复制void TemperatureController::updateControlOutput()
{
double error = setpoint - currentTemp;
integral += error * dt;
derivative = (error - lastError) / dt;
lastError = error;
double output = Kp * error + Ki * integral + Kd * derivative;
output = qBound(0.0, output, 100.0); // 限制输出0-100%
if(serialPort->isOpen()) {
QByteArray cmd;
cmd.append(static_cast<char>(0xAA));
cmd.append(0x04); // 长度
cmd.append(0x02); // 控制命令
cmd.append(static_cast<char>(channel));
cmd.append(static_cast<char>(output));
cmd.append(calculateCRC(cmd.mid(1)));
cmd.append(0x55);
serialPort->write(cmd);
}
}
在实际部署这个系统时,我发现将控制周期设置为200-500ms能在响应速度和系统稳定性间取得良好平衡。对于温度变化较慢的烘箱类设备,甚至可以降低到1秒一次控制输出。
