ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA嵌入式数据处理实战:从UART接收到均值滤波的完整设计

FPGA嵌入式数据处理实战:从UART接收到均值滤波的完整设计 简介这份PDF文献围绕FPGA嵌入式数据处理技术展开适合从事数字信号处理、硬件开发及嵌入式系统研究的工程师和研究生参考。资源为单独1个PDF文件大小约1.48MB目前已有85人浏览学习。文中以Xilinx XC5VFX70T为处理器核心针对传统DSP在I/O能力、扩展性与开发环境上的局限对比分析了FPGA在高速运算、接口支持与可编程特性上的优势给出完整的数据处理系统方案设计包括中频信号接收、A/D转换、串/并转换、解调及基带信号恢复流程并详细说明了FPGA核心处理电路、AD9430转换电路、电压/时钟/配置电路以及RS-422串口和千兆以太网接口的实现同时介绍了基于EDK和PowerPC440硬核的可编程片上系统构建与调试方法内容还涵盖DDR2存储扩展、Flash存储等外围设计。对于需要开展FPGA项目预研、毕业设计或了解高可靠性嵌入式数据处理架构的读者该文档可提供系统化的设计路径和可直接借鉴的电路参考。1. 拿到基于FPGA的嵌入式数据处理这个题目先别急着写代码做嵌入式的人看到FPGA第一反应通常是这玩意儿是硬件工程师玩的做FPGA的人看到嵌入式又觉得那是跑Linux的事。但实际工程项目里FPGA做数据处理恰恰落在两者交界处接口要对接传感器、ADC、通信芯片逻辑要完成滤波、拼接、格式转换最后还要把结果交给MCU或上位机。这套链路里FPGA吃的是硬实时、低延迟、确定性的活儿——数据来了就是来了没有操作系统调度延迟也没有Cache命中率的玄学。这篇笔记就按先定架构再写RTL最后上板调的顺序把FPGA在嵌入式场景里做数据处理的最小闭环拆开讲。适合三类人看刚入门想找实战方向的FPGA开发者做嵌入式单片机想往高速采集方向扩边界的人以及准备毕业设计或工程预研、需要快速验证方案可行性的人。跟着做一遍你会得到一个能跑通UART接收、数据拼接、滤波处理和回传的完整数据通路。2. 嵌入式场景下FPGA数据处理的架构先画数据流再谈代码FPGA开发最忌讳一上来就写代码。不管是低速传感器还是高速ADC先想清楚数据从哪来、到哪去、中间要做什么把数据流图在纸上画出来RTL写起来才有底。嵌入式数据处理的典型痛点不是算不过来而是接口协议各说各话、跨时钟域乱传数据、处理延迟不可控。这三个问题恰恰是FPGA擅长的方向。2.1 数据通路怎么搭接口、缓存、处理、回传四段式我的习惯是把整个数据处理链路拆成四段接口接收、数据缓存、核心处理、结果回传。接口接收负责把外部数据按协议拿进来常见的有UART、SPI、I2C再往上有LVDS、EMMC、QSPI这类高速接口数据缓存解决的是两个时钟域之间数据来得快、处理得慢的匹配问题核心处理是真正干活的模块滤波、均值、FFT、格式转换都在这里结果回传把处理后的数据送出去可能走UART给调试助手也可能走双口RAM让MCU来读。2.2 采样率决定设计方式低速用状态机高速用流水线数据量小比如每秒几万个点、接口是UART或低速SPI用状态机逐个字节处理就够了资源占用低、代码直观。数据量大传感器输出几十MHz的并行数据或者图像传感器的像素流就必须上流水线结构让每个处理级只做一次简单运算数据像传送带一样流过。判断标准很简单一个数据在两个处理节拍之间需要等待就考虑流水线如果完全不等待、来一个处理一个状态机更省事。FPGA定点数在这个阶段就要想清楚。嵌入式数据处理里很少用浮点——浮点IP核占DSP资源多、延迟高数据量一大资源就报警。常见的做法是把浮点系数定点化比如把浮点放大128倍再取整处理完再缩回来。后面滤波模块里我会给出具体参数怎么定。3. 数据处理核心模块的实现UART接收、数据拼接与FIR滤波实测架构定了就动手写模块。这一章按接收—处理—发送完整走一遍每个模块给出可综合的RTL代码附上参数说明和综合时要注意的点。下面代码用的是Verilog开发环境可以是Vivado或Quartus差别不大。3.1 UART_RX接收模块带仿真验证的最小实现UART是嵌入式系统里最常用的低速接口也是FPGA入门绕不开的第一个实战点。写UART_RX的难点不在协议本身而在采样点的计算和抗抖动处理。常见做法是16倍过采样系统时钟跑波特率的16倍在每一位中间采样能有效避开信号跳变沿的毛刺。module uart_rx #( parameter CLK_FREQ 50_000_000, // 系统时钟 50MHz parameter BAUD_RATE 115200 // 波特率 115200 )( input wire clk, input wire rst_n, input wire rx, output reg [7:0] data_out, output reg data_valid ); // 每个bit的时钟周期数 50MHz / 115200 ≈ 434 localparam BIT_CYCLE CLK_FREQ / BAUD_RATE; localparam HALF_BIT BIT_CYCLE / 2; reg [2:0] state; reg [3:0] bit_cnt; reg [15:0] cnt; reg [7:0] shift_reg; reg rx_d0, rx_d1; wire rx_negedge rx_d1 ~rx_d0; // 下降沿检测 // 两级同步消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_d0 1b1; rx_d1 1b1; end else begin rx_d0 rx; rx_d1 rx_d0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; data_valid 1b0; data_out 8d0; end else begin case (state) IDLE: begin data_valid 1b0; if (rx_negedge) begin state START_BIT; cnt HALF_BIT; // 从起始位中间开始计时 end end START_BIT: begin if (cnt 0) begin state DATA_BITS; bit_cnt 4d0; cnt BIT_CYCLE; end else cnt cnt - 1; end DATA_BITS: begin if (cnt 0) begin if (bit_cnt 8) begin shift_reg[bit_cnt] rx_d1; // 中间点采样 bit_cnt bit_cnt 1; cnt BIT_CYCLE; end else begin state STOP_BIT; cnt BIT_CYCLE; end end else cnt cnt - 1; end STOP_BIT: begin if (cnt 0) begin state IDLE; data_out shift_reg; data_valid 1b1; end else cnt cnt - 1; end default: state IDLE; endcase end end endmodule代码逻辑说明模块内部用16倍过采样思路但由于把BIT_CYCLE算成了整数周期实际效果等价于逐bit中点采样。rx_d0/rx_d1两级寄存器的存在是为了消除跨时钟域的亚稳态——外部UART信号和系统时钟不同源必须同步。IDLE态检测下降沿进入起始位等到半个bit周期后确认是真正的起始位再从数据位的中点采样每个数据位采一次。注意参数设置50MHz时钟配115200波特率时434个时钟采样一位是整数没有误差。如果你的系统时钟是27MHz或者12MHz要先算一下整除关系再选波特率选不了整数的要选误差小于1%的。仿真激励也很简单想验证UART_RX能不能用不用写复杂的testbench用一个循环发一串0x55再发0xAA波形里能看到data_valid在每个字节结束后拉高一次就算过。我做仿真时习惯把data_valid输出去点亮一个LED上板后看到LED按固定节奏闪烁心里就有底了。3.2 数据拼接把8bit数据合成16bit有效值很多传感器输出的不是完整字节比如12位的ADC低两位是无效值或者编码器数据分两帧传一帧高8位一帧低8位。上位机拿到这种数据是不能直接解读的得在FPGA里先把数据拼成完整的有效值再送给下游。module data_merge #( parameter DATA_WIDTH 12 )( input wire clk, input wire rst_n, input wire [7:0] rx_data, input wire rx_valid, output reg [DATA_WIDTH-1:0] merged_data, output reg merge_valid ); reg [1:0] state; reg [7:0] high_byte; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state HIGH; merge_valid 1b0; high_byte 8d0; end else begin merge_valid 1b0; // 默认不拉高 case (state) HIGH: begin if (rx_valid) begin high_byte rx_data; state LOW; end end LOW: begin if (rx_valid) begin // 高8位左移加上低4位有效数据 merged_data {high_byte[7:0], rx_data[7:4]}; merge_valid 1b1; state HIGH; end end default: state HIGH; endcase end end endmodule代码逻辑说明接收端先来高8位后来低8位拼接时高8位直接保留低字节只取高4位假设12位ADC正好占满。这里的参数DATA_WIDTH是方便复用如果换16位数据拼接把LOW分支改成{high_byte, rx_data}即可。核心设计在于merge_valid 1b0这个默认赋值它保证了拼接完成后valid只拉高一个时钟周期下游模块不会重复读到同一个数据。这个模块看着简单但它是嵌入式数据处理里最有代表性的格式转换操作。工程上我遇到过不少翻车案例有人拼接时不屏蔽无效位直接把rx_data[7:0]拼进去结果采集到的信号噪声大得离谱波形像锯齿一样。数据拼接的关键原则是只保留有效位宁可在FPGA里多花两个寄存器做移位也不要把噪声带进运算链路。3.3 均值滤波在FPGA里的实现滑动窗口与定点移位数据处理里最常用的是均值滤波去掉传感器的高频噪声实现代价也最低。FPGA里做均值滤波最大的优势是并行度可以同时采样N个点做一个周期就算完一次平均。module moving_avg #( parameter DATA_WIDTH 12, parameter AVG_SIZE 8 // 窗口大小必为2的幂次 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] data_in, input wire data_valid, output reg [DATA_WIDTH3-1:0] avg_out, output reg avg_valid ); // 累加器和求和寄存器组 reg [DATA_WIDTH10-1:0] sum; reg [DATA_WIDTH-1:0] shift_reg [0:AVG_SIZE-1]; reg [$clog2(AVG_SIZE)-1:0] index; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sum 0; index 0; avg_valid 1b0; avg_out 0; end else begin avg_valid 1b0; if (data_valid) begin // 旧值减掉新值加进来 sum sum - shift_reg[index] data_in; shift_reg[index] data_in; index index 1; avg_valid 1b1; end end end // 除以窗口大小AVG_SIZE是2的幂次直接右移 always (*) begin avg_out sum $clog2(AVG_SIZE); end endmodule代码逻辑说明这是滑动均值滤波的标准写法每个新数据到来时用累加器减去窗口中最旧的值、加上最新值等于只做一次加减法就完成了窗口内8个点的求和然后用右移实现除法。注意两个append一是不能把avg_out和sum位宽写死如果数据源是12位8个点求和最大需要15位必须预留位宽二是求和寄存器加了10位扩展是因为实际工程里SUM还要往下游继续累加宁多勿少。参数设定很关键AVG_SIZE必须是2的次幂否则不能用移位代替除法窗口越大滤波越平滑但延迟越明显。逐点滤波时AVG_SIZE取8或16通常够用。如果只是去毛刺不是做平滑均值滤波可以换成一阶低通out out (data - out) K系数K越大滤波越重这个式子用一个加法器和一个移位器就能实现。4. 系统集成与上板验证从仿真到实测的距离没那么远模块单测各跑各的没问题拼到一起经常出问题。这一章讲整个系统怎么搭起来以及如何不靠示波器也能判断系统跑没跑对。系统集成这一层数据打包、地址规划、回传参数都是容易忽略的细节。4.1 数据打包与下传处理完的数据怎么送出去均值滤波计算出的数据要送给上位机查看最直接的方式还是UART发送。上一个模块出来的数据是12位而UART一次只能发8位所以发送端要拆包还要加上帧头帧尾方便上位机辨认。module data_packetizer #( parameter DATA_WIDTH 12 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] data_in, input wire data_valid, output reg [7:0] tx_data, output reg tx_valid ); // 帧格式: [0xAA] [data[11:4]] [data[3:0]4] [0x55] localparam FRAME_HEAD 8hAA; localparam FRAME_TAIL 8h55; reg [1:0] state; reg [DATA_WIDTH-1:0] cur_data; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state WAIT_DATA; tx_valid 1b0; end else begin tx_valid 1b0; case (state) WAIT_DATA: begin if (data_valid) begin cur_data data_in; state SEND_HEAD; tx_data FRAME_HEAD; tx_valid 1b1; end end SEND_HEAD: begin state SEND_HIGH; tx_data cur_data[DATA_WIDTH-1:DATA_WIDTH-8]; tx_valid 1b1; end SEND_HIGH: begin state SEND_LOW; tx_data {cur_data[3:0], 4b0000}; tx_valid 1b1; end SEND_LOW: begin state WAIT_DATA; tx_data FRAME_TAIL; tx_valid 1b1; end default: state WAIT_DATA; endcase end end endmodule代码逻辑说明这个打包模块把处理后的12位数值封装成4字节帧——帧头0xAA、数据高8位、数据低4位放到一个字节的高半字节、帧尾0x55。上位机解析时看到0xAA开始读到0x55结束中间两个字节拼回来就是完整的12位数据。低4位补零是为了保持帧长度固定方便上位机用状态机处理。工程上实际的痛点就在这里很多人调通了接收和滤波下传时直接把裸数据丢给UART发送上位机拿到后根本不知道哪两个字节是一组的。加上帧头帧尾是嵌入式通信的基本素养代价只有两个字节的带宽开销换来的是可靠的协议对齐。4.2 上板验证手段除了示波器还能看什么没有逻辑分析仪和示波器的时候验证数据通路的常用方法是回环测试在FPGA内部把UART接收到的数据原封不动送进发送模块上位机发什么就回什么。回环测试通过了说明接收和发送两端都正常问题范围缩小到中间的处理逻辑。// 顶层测试连接示意 wire [7:0] rx_data; wire rx_valid; wire [7:0] tx_data; wire tx_valid; uart_rx u_uart_rx ( .clk (clk), .rst_n (rst_n), .rx (uart_rx_pin), .data_out (rx_data), .data_valid(rx_valid) ); // 回环测试直接发包不经过处理 assign tx_data rx_data; assign tx_valid rx_valid; // 正常模式经过拼接→滤波→打包 data_merge u_merge ( .rx_data (rx_data), .rx_valid (rx_valid), .merged_data (adc_data), .merge_valid (adc_valid) );注意这里有一个工程习惯把回环测试和处理通路用宏或寄存器选择开关包起来上板调试时先跑回环确认链路通再切换处理模式看数据。不要每次改代码都全量编译几分钟调试期的时间省下来都够多跑几轮仿真了。上板验证时还可能遇到一个问题仿真全对上板数据全错。排除代码问题后优先查时钟——用过的板子如果晶振停振或者频率不对波特率会完全错乱。另一个问题是复位很多板子的复位按键是按下低电平但有些是反的复位极性搞反了模块会一直处于复位状态现象就是所有信号都不动。5. 连带踩坑FPGA数据处理的5个高频翻车现场写RTL做上板验证这么多年把常见的坑集中列出来。这些坑大多不是逻辑复杂造成的而是边界条件、位宽、跨时钟域这类细节问题每条都按现象、原因、解决来写拿去排查路径很直接。1. 仿真波形正确上板后数据偶发错乱现象UART接收的数据在仿真里完美上板调试时大约每几百个字节出现一次乱码。原因这是经典的跨时钟域问题。外部UART信号与系统时钟不同源rx信号进入FPGA时如果不做同步处理可能采样到跳变沿中间的亚稳态导致数据位错位。仿真里激励信号是理想的不会出现亚稳态所以仿真全对、上板翻车。解决rx信号必须经过两级寄存器同步后才能进入状态机判断也就是模块代码里的rx_d0和rx_d1两级触发器的处理。这个处理是最高优先级的任何外部输入信号都要走这道工序。2. 均值滤波后数据反而出现尖刺现象输入信号是干净的直流电平均值滤波后输出却时不时出现尖刺持续时间只有一个周期。原因累加器减旧值加新值时没有考虑数据的边界情况。比如窗口内数据从大的跳变到小的旧的减值和新的加值同时发生时sum的中间结果可能瞬间超过或低于正常范围。另外判决位宽不够累加器溢出了。解决sum的位宽要按最大值×窗口大小来算留足冗余12位数据×8个点至少要15位。同时把sum的更新写上reg类型并完整复位清零不要图省事让综合器自动推断位宽位宽少一位就可能溢出。3. 系统跑起来后上位机收到的数据频率比预期慢一半现象数据处理通路上没有报错但上位机曲线更新速度明显比设计值慢。原因这个坑多半出在数据valid信号的时序上。比如merge模块里merge_valid拉高时数据在同一个时钟边沿更新但下游模块的采样逻辑看的却是旧数据。数据本身对但valid和数据不同步导致一组数据要两个周期才被识别。解决调试时先看valid和数据信号是否在同一拍对齐把两个信号同时引到两个GPIO上用示波器观察它们的对齐关系。经验法则是valid在数据稳定的同一个边沿拉高并且只维持一个周期。4. 综合后逻辑单元占用暴增时序跑不过现象代码里就一个状态机加一个滤波器综合出来LUT占用比预期多了三倍时序报告一堆违例。原因往往是代码风格问题——在always块里混用了阻塞赋值和非阻塞赋值或者在组合逻辑里写了不完整的if-else导致综合出锁存器。综合器为每个锁存器分配寄存器资源整体占用自然就上去了。解决时序逻辑只用非阻塞赋值组合逻辑只用阻塞赋值状态机里不要混用。if-else在时序always块里必须写全该有默认输出的给default分支就没有意外锁存器了。5. 换一块同型号板子同样的代码采集效果差很多现象两块板子理论上硬件相同一块采集曲线平滑另一块噪声明显偏大。原因电源质量、参考电压波动、PCB布线差异都会影响数据采集FPGA本身是确定性的但模拟前端的差异会被数据处理链路如实传输。这个问题在FPGA里没有完全解法只能通过校准。解决处理链路里预留一个校准通道采集前先采集已知参考电压把增益误差和偏移误差在后端做补偿。均值滤波、去毛刺算法也在一定程度上能压住这种底噪。嵌入式系统调试到最后一半时间花在FPGA逻辑上另一半花在理解模拟前端和电路板的特性上。6. 把FPGA数据处理做成可复用的模块库寄存器参数化与系统自检做到这里你已经完成了一条完整的数据处理通路但工程化还没完。真正落地的系统还要考虑可维护性——过三个月再拿起这份代码能不能一眼看懂换一块传感器改一下位宽要不要动状态机这块讲两个实用技巧让代码能跨项目复用。寄存器参数化是FPGA工程的基本功。推荐的做法是把关键参数——滤波窗口大小、数据位宽、帧格式、累加器使能——全部映射为内部寄存器通过UART指令现场改写而不是改RTL再重新综合。UART指令本身只需要一个状态机格式定为两位操作码加十六位数据写滤波系数、写切换通道、读当前累加值三件事就够了。这样同一份bit文件可以适配多种传感器和多种工况。// 寄存器参数化的一个示例用寄存器控制滤波窗口 reg [2:0] filter_cfg; // 通过UART写命令设置 0~7 always (posedge clk) begin if (cmd_valid cm p a hrefhttps://download.csdn.net/download/u013883025/21087332 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表