尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+FPGA协同实现高精度数字频率计:等精度测量与双芯协作

STM32+FPGA协同实现高精度数字频率计:等精度测量与双芯协作 简介基于STM32与FPGA的数字频率计完整源码包面向嵌入式学习者与硬件开发者演示ARM Cortex-M3微控制器和FPGA协同实现信号频率测量。项目以STM32F103完成数据采集、定时捕获与结果输出Altera EP2C5T144C8N负责边沿检测、多通道预处理源码涵盖Keil与Quartus II工程文件。压缩包共345个文件大小8.53MB主要包括C语言固件、VHDL/Verilog硬件描述、Quartus配置与综合报告、链接脚本、烧录文件及说明文档可直接对照工程学习定时器捕获、串口通信和FPGA逻辑设计。目前已有463人学习下载适合需要参考完整软硬件协同方案、理解数字频率计实现细节的读者。通过该源码可提取STM32端底层驱动与算法、FPGA端信号处理逻辑梳理整体设计思路也可基于提供的构建脚本与实际配置文件快速复现项目流程。1. 为什么数字频率计要同时用 STM32 和 FPGA做频率测量方案很多纯单片机定时器计数、纯 FPGA 计数、或者 STM32 加 FPGA 双芯协同。纯单片机的上限通常在几 MHz 到几十 MHz受定时器分辨率和中断响应限制测高频时误差会明显放大纯 FPGA 能跑很高频率但做浮点运算、驱动 LCD 显示、扩展按键交互并不顺手。这个项目把 STM32F103 和 Altera EP2C5T144C8N 拼在一起恰好互补各自短板。源码包里几个文件的命名已经能看出整体思路exti1.__i对应外部中断通道 EXTI1tlc5540.__i指向 TLC5540 高速 ADCsendData.vhd和deltat.vhd是 FPGA 侧的 VHDL 逻辑前者负责把测量结果发送给 STM32后者处理时间差测量。整体架构是FPGA 做高频信号的捕获和预处理STM32 做控制、计算和显示。适合的人群有两类一类是做嵌入式毕业设计或课程设计的学生另一类是在做仪器仪表类产品选型评估的工程师。读完这篇文章你能理解测频误差从哪来、双芯之间怎么通信、以及源码里那些文件各自承担什么角色。2. STM32F103 侧的频率计算与显示链路2.1 为什么用定时器输入捕获而不是外部中断计数STM32F103 的定时器输入捕获模式可以记录上升沿或下降沿到来时的计数器值通过两次相邻上升沿的计数值差换算周期。相比单纯的外部中断加变量累加输入捕获由硬件完成时间戳记录不受中断延迟抖动影响测量低频信号时优势很明显。在源码中pinlvji_Uv2.Bak是 Keil uVision 工程备份文件工程里对 TIM 的初始化通常会这样配置void TIM2_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 使能时钟TIM2 挂在 APB1GPIOA 挂在 APB2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0 复用为 TIM2_CH1输入捕获通道 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 预分频 7172MHz / 72 1MHz计数分辨率 1us TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 上升沿捕获不滤波直接映射 CH1 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0; TIM_ICInit(TIM2, TIM_ICInitStructure); // 使能捕获中断开启定时器 TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }这段代码把 TIM2_CH1 配置为上升沿捕获预分频设为 71配合 72MHz 系统时钟得到 1MHz 的计数频率。TIM_Period设为 0xFFFF 意味着计数器在 65535us 后溢出如果被测信号周期超过约 65ms对应频率低于约 15Hz需要在中断里额外处理溢出次数。输入捕获中断服务函数中读取TIM_GetCapture1(TIM2)拿到捕获时刻的计数值连续两次捕获值的差就是信号的周期单位为微秒频率就是周期的倒数。2.2 频率计算与 LCD 显示的数据流LCD.axf是编译链接后的可执行文件工程中通常使用 LCD 模块直接显示频率结果。计算频率时需要考虑单位换算和显示刷新率之间的平衡如果每次捕获都刷新 LCD刷新时间会占用相当多的 CPU 时间且显示会闪烁。常见做法是每秒累积若干个周期后取平均或者做滑动平均滤波。volatile uint32_t cap_val 0, cap_last 0, period_sum 0; volatile uint16_t period_cnt 0; volatile float freq_hz 0.0f; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); cap_val TIM_GetCapture1(TIM2); // 两次捕获的差值即信号周期us period_sum (cap_val cap_last) ? (cap_val - cap_last) : (0xFFFF - cap_last cap_val); period_cnt; cap_last cap_val; } } void LCD_ShowFreq(void) { if (period_cnt 50) { // 取 50 个周期的平均值换算成 Hz float period_avg_us (float)period_sum / period_cnt; freq_hz 1000000.0f / period_avg_us; period_sum 0; period_cnt 0; // 调用 LCD 驱动显示 freq_hz } }period_sum累加的是相邻捕获计数值的差值period_cnt记录累加次数。用 50 个周期取平均的方式可以有效降低单次捕获抖动带来的误差。注意处理了计数器回绕的情况当cap_val小于cap_last时说明计数器从 0xFFFF 翻回 0真实差值要用0xFFFF - cap_last cap_val计算。参数项推荐值说明TIM 预分频71计数频率 1MHz分辨率 1usTIM 周期0xFFFF溢出周期约 65.5ms平均周期数50兼顾刷新率和稳定性捕获极性上升沿可改为双边沿测占空比3. FPGA 侧的高频捕获与 TLC5540 信号链路3.1 EP2C5T144C8N 的资源和选型理由FPGA 选用 Altera Cyclone II 家族的 EP2C5T144C8N逻辑单元约 4608 个内置 26 个 M4K 存储器块共 119808 bit。这个资源量对于频率计场景完全够用需要实现的逻辑无非是同步器、计数器、寄存器组以及简单的状态机逻辑单元消耗通常在 30% 以内。选择这款 FPGA 更现实的理由是开发板普及度高、Quartus II 支持完善、5V 和 3.3V 电平兼容性好。tlc5540.__i是 TLC5540 的初始化或接口相关文件。TLC5540 是 TI 的 8 位 40MSPS 高速 ADC它在这个项目中的角色是把待测模拟信号数字化。FPGA 从 TLC5540 读入数字信号然后做边沿检测。频率计的输入信号经过前级整形后送入 TLC5540 采样由 FPGA 内部逻辑完成上升沿提取和门控计数。3.2 deltat.vhd 与 sendData.vhd 的协作逻辑deltat.vhd处理的是时间差测量这是等精度测频法的核心。等精度测频的思路是用一个高频基准时钟作为计数脉冲用一个与被测信号同步的闸门信号控制计数时间。无论被测信号频率高低实际测量精度都取决于基准时钟的频率而不是待测频率本身。-- deltat.vhd 的核心计数器结构 library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity deltat is port ( clk_ref : in std_logic; -- 高频基准时钟 gate_in : in std_logic; -- 与被测信号同步的闸门 rst_n : in std_logic; -- 异步复位 cnt_period : out std_logic_vector(31 downto 0) -- 闸门内的实际时间 ); end deltat; architecture rtl of deltat is signal cnt_reg : std_logic_vector(31 downto 0); begin process(clk_ref, rst_n) begin if rst_n 0 then cnt_reg (others 0); elsif rising_edge(clk_ref) then if gate_in 1 then cnt_reg cnt_reg 1; else cnt_reg (others 0); end if; end if; end process; cnt_period cnt_reg; end rtl;这段代码定义了一个闸门控制的计数器gate_in为高电平时对clk_ref计数闸门关闭后输出计数值。cnt_period的值乘以基准时钟周期就是闸门的实际宽度。这样做的好处是闸门时间不用精确等于某个整数值实际闸门宽度通过计数结果反推消除了闸门信号与被测信号不同步导致的量化误差。sendData.vhd则负责把测量结果传给 STM32通常采用串行方式降低 IO 占用-- sendData.vhd串行发送测量结果 entity sendData is port ( clk : in std_logic; data_in : in std_logic_vector(31 downto 0); -- 计数结果 send_en : in std_logic; -- 发送使能 txd_out : out std_logic -- 串行输出 ); end sendData;3.3 等精度测量为什么比直接计数更准直接计数法是用固定闸门时间 T 去数被测信号的上升沿个数 N结果 fx N / T。误差来源在于闸门开关时刻与信号上升沿之间的关系闸门开早或关晚半个周期计数就多一个或少一个相对误差为 1/N。被测频率越低N 越小误差越大。等精度测量法把闸门信号改为由被测信号上升沿触发闸门的实际宽度是整数个被测周期。用基准时钟测出闸门的精确宽度后被测频率的计算公式为 fx Nx / (Ns * fs)其中 Nx 是被测信号的上升沿个数Ns 是基准时钟的计数值fs 是基准时钟频率。用表格对比两种方法的误差特征测量方式闸门同步方式误差主来源低频表现高频表现直接计数固定时间量化误差 1/N差良好等精度测量信号上升沿同步基准时钟量化良好良好量化误差在等精度测量中变成基准时钟的一个周期与被测频率无关。基准时钟用 50MHz 晶振时最大量化误差就是 20ns换算成 1MHz 被测信号的相对误差约为 2%但是通过多次测量求平均可以进一步压低这个误差。4. 双芯协作的工程构建与联调要点4.1 源码包中 Keil 与 Quartus 工程的联动方式这份源码包里的文件分为两个独立工程STM32 侧对应 Keil uVision 工程pinlvji_Uv2.Bak是工程备份文件pinlvji_Opt.Bak是编译选项备份FPGA 侧对应 Quartus II 工程sendData.vhd.bak和deltat.vhd.bak是 VHDL 源文件的备份版本。LCD.axf是 ARM 编译生成的最终镜像可以直接烧录到 STM32F103。两个工程之间通过约定的 GPIO 管脚对接。STM32 的exti1文件表明使用了 EXTI1 外部中断通道这个引脚连接的正是 FPGA 发送数据用的输出线。FPGA 完成一次测量后把数据逐位发送到该引脚STM32 通过 EXTI1 中断逐位接收按约定的数据帧格式拼接成完整测量结果。数据帧格式常见定义是起始位 32 位计数值 校验位。起始位用一低电平表示32 位数据按高到低顺序发送最后是奇偶校验。接收端在检测到下降沿后开启接收流程按照约定的波特率采样。4.2 编译时常见的引脚约束与时钟约束问题FPGA 工程在 Quartus II 中综合时最容易出错的不是逻辑本身而是引脚分配和时序约束。EP2C5T144C8N 是 144 脚封装IO 资源有限需要仔细分配。TLC5540 的 8 位数据线加时钟线占用 9 个引脚与 STM32 通信的串行接口占用 2 到 3 个引脚再加上系统复位、基准时钟等引脚规划必须提前做好。时钟约束方面如果基准时钟为 50MHz需要在 Quartus II 中创建时钟约束create_clock -name clk_ref -period 20.0 [get_ports {clk_ref}]-period 20.0对应 50MHz 频率如果不加这个约束Quartus 默认按理想时钟分析综合后的实际时序可能在高温或电压波动下不满足导致计数器出现亚稳态。STM32 侧遇到error: no stm32 target found时先查 ST-Link 驱动和接线再查 Keil 的 Debug 设置里是否选中了正确的下载器。这个报错和工程本身无关多数是环境问题。ST-Link 的 SWDIO 和 SWCLK 两根线务必短接稳定目标板供电不足时也会出现同样提示。4.3 软硬件联调时的信号完整性与误触发排查联调时最容易出现的现象是 FPGA 计数结果正确但 STM32 收到的数据偶尔错乱。这类问题端口因素很多不一定是逻辑问题。一个重要嫌疑是 FPGA 输出信号的边沿不够陡峭导致 STM32 的 EXTI1 反复触发。常见对策是在 STM32 的 EXTI 配置中打开输入滤波。EXTI_InitTypeDef EXTI_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // PA1 配置为上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // EXTI1 上升沿触发使能中断 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1); EXTI_InitStructure.EXTI_Line EXTI_Line1; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure);如果 FPGA 输出频率较高或信号质量差EXTI_Trigger_Rising可以改为EXTI_Trigger_Falling配合下降沿数据有效同时在 FPGA 侧把输出模式配置为推挽、提高驱动能力。FPGA 内部还可以加一级同步器消除跨时钟域的亚稳态风险。5. 高频测量时为 STM32 分担负载的几条思路5.1 把部分计算下沉到 FPGA源码包中只看到 FPGA 做计数和时间差测量但如果你要测更高频率或更多通道完全可以把一些简单的换算逻辑也做到 FPGA 里面。FPGA 内部实现乘以固定系数再移位代替 STM32 的浮点运算可以显著降低 STM32 的 CPU 负载。一个实用技巧是周期累加值除以被测信号周期个数得到平均周期这个除法如果放在 FPGA 里做可以利用移位寄存器近似处理前提是除数固定或变化缓慢。比如平均周期个数固定为 50那么除以 50 可以近似为乘以 13107 再右移 16 位13107 ≈ 65536/50 取整。代价是约 0.05% 的额外误差换来 STM32 侧省掉一次整数除法。5.2 用 OCXO 或 TCXO 做基准时钟提升测量精度等精度测量的精度上限取决于基准时钟的稳定性普通晶振的温漂通常在 20ppm 到 50ppm 之间环境温度变化大会直接反映为测量误差。如果你需要把频率计做到参考源级别把 FPGA 的基准时钟从普通无源晶振换成 OCXO 恒温晶振测量稳定性会有一个量级的提升。换时钟后需要注意两个工程侧的修改FPGA 工程中 Quartus 时钟约束的-period要按新时钟频率修改STM32 侧的定时器预分频值也要按新的计数频率调整。比如基准时钟从 50MHz 换成 100MHz 后create_clock的周期从 20ns 改为 10ns而 STM32 侧如果仍然使用片内 72MHz 时钟则不需要改定时器配置。5.3 加入占空比和脉宽测量deltat.vhd这个文件名暗示了它具备测时间差的能力这也意味着同样的硬件可以扩展出占空比测量功能。实现方法是在 FPGA 中增加一个对高电平持续时间的计数器同时保留原有对完整周期的计数。这样一次测量可以输出周期值和高电平时间两个参数STM32 侧做一次除法即可得到占空比。-- 扩展高电平时间计数 process(clk_ref, rst_n) begin if rst_n 0 then high_cnt (others 0); elsif rising_edge(clk_ref) then if sig_in 1 then high_cnt high_cnt 1; else high_cnt (others 0); end if; end if; end process;这段代码与 deltat.vhd 的区别在于计数条件从gate_in 1换成了sig_in 1其输出high_cnt表示高电平持续时间内基准时钟的计数值。把这个值和周期计数值一起通过sendData.vhd发给 STM32STM32 侧计算占空比的公式就是high_cnt * 100.0 / period_cnt。上述扩展都建立在源码包现有架构之上。sendData.vhd的发送格式如果需要增加字段注意 STM32 侧的帧解析逻辑要同步修改否则会出现数据错位的现象。建议在帧定义时预留若干保留位方便后续扩展协议时保持向后兼容。本文还有配套的精品资源点击获取
返回列表