STM32F103ZE电力数据采集:ADC同步采样、NAND存储与RS485通信
2026/9/17 14:18:32 网站建设 项目流程

简介:围绕STM32电力数据采集系统的设计与实现展开,面向嵌入式开发学习者、自动化及电气专业师生与工程技术人员,可用于课程设计、毕业设计或电力监测项目方案参考。内容分硬件与软件两条主线:硬件部分梳理STM32F103ZE片上资源、Cortex-M3内核特性、12位ADC多通道同步采样、定时器触发注入模式、FSMC扩展NAND FLASH,以及电压电流信号调理、霍尔传感器与CT应用、GPIO开关量读取;软件部分涵盖数据滤波与功率电能计算、阈值异常检测、RS485/CAN/以太网通信协议及上位机GUI交互。文档仅含1个PDF文件,压缩包约45KB,已有127人学习。读者可借此掌握采集系统架构、关键器件选型与软硬件协同设计思路,并对照高精度、大存储、实时性、低成本等优势,为实际项目搭建提供可落地参考。

1. 从一块配电监测板说起:STM32F103ZE 电力数据采集系统在采什么

调试台上这类板子长得都差不多:三路相电压、三路相电流从端子排进来,旁边一排 24 路开关量光耦,板子背面挂一片 NAND FLASH,对外只有一路 RS485 和一路以太网。很多人的第一反应是外挂专用计量芯片或者高速并行 ADC,但用 STM32F103ZE 自带的 12 位逐次逼近型 ADC,配合定时器触发注入组做同步采样,把 0.5 级以内的电力参数采集做出来是完全可行的,BOM 成本能压下一大截。这套方案要解决的核心问题就三个:把 220V/380V 和几十安培的强电安全地搬进 0~3.3V 的 ADC 输入窗口;在多通道同时转换的前提下保住电压与电流之间的相位关系;在无人值守的现场把数据可靠地存下来、传出去。下面按调理电路、固件采样、存储通信、现场校准排错几层拆开讲。

2. 强电搬进 3.3V:信号调理电路与 ADC 通道选型

2.1 电压通道:电阻分压网络与电压互感器的取舍

现场进来的工频强电,峰值能到 311V(相电压 220V)甚至 537V(线电压 380V),而 STM32 的 ADC 引脚绝对最大耐压只有 VDD+0.3V,超一点点就是锁死或者直接烧脚。所以第一级一定是降压,而且要先算功耗和耐压再选器件,不能拿一个 1MΩ 的 0805 电阻直接顶上去。

直接电阻分压适合 400V 以下的低压配电场景,成本最低。算例:相电压有效值 220V,峰值约 311V,想在 ADC 端拿到 1.55V 峰值,分压比取 1:201。上臂用 4 个 51kΩ/0.25W 金属膜电阻串联成 204kΩ,下臂取 1kΩ。流过支路的峰值电流约 311V/205kΩ ≈ 1.5mA,上臂总功耗约 0.33W,摊到 4 个电阻上每个约 0.083W,1/4W 封装留了 3 倍余量。下臂并联一个 100nF 电容和 1kΩ 组成 RC 低通,截止频率约 1.6kHz,用来压掉高频干扰和 ADC 采样保持回路的电荷注入。

交流采样必须保留瞬时波形才能算有功功率,所以 ADC 端不能只看到正半周。常见做法是给下臂串一个 1.65V 的直流偏置,把 ±1.55V 的交流信号抬到 0.1~3.2V 之间,正好落在 ADC 的线性区间内。偏置电压从 TL431 或者 REF3033 取,千万别用 MCU 的 3.3V 数字电源分压,那个纹波会直接进采样结果。

方案适用电压精度隔离成本备注
电阻分压≤400V0.2%极低需要额外做隔离和爬电距离设计
电压互感器 PT100V~1kV0.1%天然隔离有相位误差,需软件补偿
霍尔电压传感器任意0.5%天然隔离温漂较大

提示:分压电阻要选温漂 25ppm/℃ 以内的金属膜电阻,普通碳膜电阻温漂能到 300ppm,夏天柜内温度上到 60℃,增益直接飘掉 1%。

2.2 电流通道:CT、霍尔与采样电阻的组合

电流通道的选型比电压通道更纠结,因为要区分测的是交流还是直流。CT(电流互感器)只能耦合交流,变比常见 1000:1 或 2000:1,二次侧接一个采样电阻(burden resistor)转成电压。比如 1000:1 的 CT,一次侧 5A 时二次侧 5mA,挂 100Ω 采样电阻得到 0.5V,再经一级同相放大器放大到 1.5V 送 ADC。CT 的优点是便宜、隔离好、线性度高;缺点是励磁电流会带来相位超前误差,在功率因数接近 1 的场合影响不大,但在 0.5L 以下会明显拉低有功功率的计算精度。

霍尔电流传感器(开环 ACS712、闭环 LEM 系列)能测直流,带宽也高,适合光伏、储能这类有直流分量的场景。开环霍尔零点漂移大,闭环霍尔精度好但单价能到 CT 的 5 倍以上。做纯交流配电监测,CT 是性价比最高的选择。

方案测直流带宽相位误差成本
CT + 采样电阻50kHz0.5°~2°
开环霍尔80kHz
闭环霍尔200kHz0.1°
分流电阻 + 隔离运放100kHz极小

CT 二次侧绝对不能开路,开路瞬间二次侧会感应出上千伏电压,能把放大器和 MCU 一起带走。采样电阻虚焊是现场最常见的炸板原因,装配时优先用带保护的接线端子。

2.3 STM32F103ZE 的 ADC 注入组与定时器同步触发

STM32F103ZE 内部有 3 个 12 位逐次逼近型 ADC,ADCCLK 最高 14MHz(PCLK2 六分频后 12MHz),采样时间可编程为 1.5、7.5、41.5、55.5、71.5、239.5 个周期,转换时间 =(采样周期 + 12.5)/ ADCCLK。取 41.5 周期时单次转换约 4.5μs,对应约 222kSPS,做 64 点/周期的工频采样绰绰有余。

同步采样的关键在于用定时器的 TRGO 事件同时触发多个 ADC,而不是让 CPU 挨个去启动。规则组通道多、支持 DMA,但每个 ADC 同一时刻只能有一个规则组在转换;注入组每路有独立的 JDR1~JDR4 寄存器,四路注入通道的转换结果能锁存在同一时刻,相位一致性更好。所以三相电压电流这类必须同相采集的信号,走注入组;慢速的温度、直流母线电压这类,走规则组加 DMA。

/* 1. 关闭 JTAG,保留 SWD,释放 PA15/PB3/PB4 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); /* 2. ADCCLK = PCLK2 / 6 = 12MHz */ RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* 3. 注入组配置:4 路,顺序 IA -> IB -> IC -> UA */ ADC_InitTypeDef adc; ADC_StructInit(&adc); adc.ADC_Mode = ADC_Mode_Independent; adc.ADC_ScanConvMode = DISABLE; /* 注入组按序列自动扫描 */ adc.ADC_ContinuousConvMode = DISABLE; /* 由定时器触发,单次 */ adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &adc); ADC_InjectedSequencerLengthConfig(ADC1, 4); ADC_InjectedChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_41Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_41Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_41Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_41Cycles5); /* 4. TIM1 更新事件作为 TRGO,触发注入转换 */ ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_T1_TRGO); ADC_ExternalTrigInjectedConvCmd(ADC1, ENABLE); ADC_ITConfig(ADC1, ADC_IT_JEOC, ENABLE); /* 注入组只能用中断读,没有 DMA */ ADC_Cmd(ADC1, ENABLE);

逻辑说明:先做一次ADC_ResetCalibration/ADC_StartCalibration校准,再使能 ADC。参数上,采样时间选 41.5 周期是精度和速度的折中,信号源内阻越大(比如分压网络等效输出阻抗高),采样时间就得越长,否则采样保持电容充不满,读数会偏低。注入组在 F1 系列上不支持 DMA,只能在 JEOC 中断里依次读ADC_GetInjectedConversionValue(),所以中断频率等于采样率,4kHz 中断对 72MHz 的 M3 来说压力不大,但中断服务程序里别做浮点运算。

2.4 开关量输入与 JTAG 引脚复用、外设 CS 管理

开关量信号代表继电器、接触器的分合状态,现场一般用光耦隔离后再进 GPIO,配合 10kΩ 上拉和 100nF 滤波。软件上不能只靠轮询,否则高频抖动能把计数器刷爆,常见做法是 1ms 定时器里做状态机去抖,连续 5 次读到同一状态才更新,同时开 EXTI 下降沿中断做快速响应。

这里有个新手极易踩的坑:PA15、PB3、PB4 默认是 JTAG 的 JTDI、JTDO、NJTRST 复用功能,直接配成 GPIO 输入是不生效的,必须调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)关掉 JTAG、保留 SWD。同理,用 SPI 挂外部 EEPROM 或者 Flash 时,片选 CS 引脚如果落在这几个脚上,也会出现「IO 翻转正常但外设毫无反应」的现象——这就是常说的 stm32 cs 管理问题,排错时先查引脚复用表,再看 CS 时序,别一上来就怀疑芯片。

/* 开关量输入初始化:PB3 作为 DI1,需先释放 JTAG */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitTypeDef io; io.GPIO_Pin = GPIO_Pin_3; io.GPIO_Mode = GPIO_Mode_IPU; /* 上拉输入,光耦导通时拉低 */ io.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &io); /* EXTI3 下降沿触发 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource3); EXTI_InitTypeDef exti; exti.EXTI_Line = EXTI_Line3; exti.EXTI_Mode = EXTI_Mode_Interrupt; exti.EXTI_Trigger = EXTI_Trigger_Falling; exti.EXTI_LineCmd = ENABLE; EXTI_Init(&exti);

参数说明:GPIO_Mode_IPU是上拉输入,对应光耦集电极开路输出;如果前端是推挽输出,改成GPIO_Mode_IN_FLOATING并外置下拉。EXTI 中断里只置标志位,实际状态判定放到主循环或定时器任务里做,避免中断里做长延时。

3. 定时器触发 + 三重同步采样:固件层的数据搬运与功率计算

3.1 规则组 DMA 搬运与 ADC 三重同步模式

注入组解决了同相采样,但采样点数一多,中断开销就上来了。做谐波分析要 128 点甚至 256 点每周期,这时候换个思路:把 ADC1 配成主、ADC2 和 ADC3 配成从,工作在ADC_Mode_RegSimult三重同步模式,三个 ADC 的规则通道由同一个 TIM2 TRGO 同时触发,转换结果分别落到各自的 DR 寄存器,只有 ADC1 的 DR 请求 DMA。这样一次搬运就能拿到三路同相数据,CPU 完全不参与。

采样周期数ADCCLK=12MHz 转换时间适用场景
1.51.17μs高速但要求源阻抗极低
7.51.67μs运放直驱
41.54.5μs分压网络、CT 采样电阻
239.521μs高阻信号源、内部温度通道
/* ADC1 规则组 + DMA,三重同步模式 */ ADC_InitTypeDef adc; adc.ADC_Mode = ADC_Mode_RegSimult; /* 三重同步 */ adc.ADC_ScanConvMode = ENABLE; adc.ADC_ContinuousConvMode = DISABLE; adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfChannel = 3; ADC_Init(ADC1, &adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_41Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_41Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 3, ADC_SampleTime_41Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); /* DMA1 通道1,循环模式,目标缓冲区 3*N 个半字 */ DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_MemoryBaseAddr = (uint32_t)adc_buf; dma.DMA_DIR = DMA_DIR_PeripheralSRC; dma.DMA_BufferSize = 3 * SAMPLE_N; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; dma.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &dma); DMA_Cmd(DMA1_Channel1, ENABLE);

逻辑说明:DMA_Mode_Circular让缓冲区自动回卷,主循环只需在半传输和传输完成中断里翻页处理,省掉一次内存拷贝。参数上DMA_PeripheralDataSize必须是半字,因为 ADC 的 DR 是 16 位;DMA_BufferSize要等于通道数乘以每周期采样点数,改采样率时这个值要同步改,否则会出现通道错位——表现是 A 相电压的数据跑到 B 相电流里去了。

3.2 采样值标定与真有效值计算

ADC 拿到的是 0~4095 的原始码值,先减掉偏置对应的码值再乘标定系数,才是真实的电压电流瞬时值。三点必须注意:偏置码值要在零输入时实测,不能拍脑袋用 2048;标定系数用两点法算,比单点缩放准得多;所有中间运算用int32_tfloat,别用int16_t硬扛,平方运算很容易溢出。

真有效值的做法是每周期采 N 点,求平方和再开方。电压电流共用一套:

#define SAMPLE_N 64 /* u_buf / i_buf 为已扣除偏置、折算成实际工程量的浮点数组 */ float calc_rms(const float *buf, uint16_t n) { float sum = 0.0f; for (uint16_t k = 0; k < n; k++) { sum += buf[k] * buf[k]; /* 累加平方,注意用 float 防溢出 */ } return sqrtf(sum / n); /* 均方根 */ } /* 有功功率:同相瞬时值乘积的平均 */ float calc_active_power(const float *u, const float *i, uint16_t n) { float sum = 0.0f; for (uint16_t k = 0; k < n; k++) { sum += u[k] * i[k]; /* 电压电流必须同相采样,否则功率偏小 */ } return sum / n; }

参数说明:SAMPLE_N取 64 是 50Hz 下每周波 64 点,采样率 3.2kHz,对 20 次以内的谐波(1kHz)满足奈奎斯特定理。如果要测到 50 次谐波,SAMPLE_N要提到 256,采样率 12.8kHz,此时 ADC 转换时间必须压到 7.5 周期以内,DMA 缓冲区也要同步扩大 4 倍。相位偏差会直接体现成功率误差:电压电流之间差 1° 相位,功率因数 0.9 时功率误差约 0.5%,所以 CT 的相位补偿系数必须实测标定。

3.3 有功功率与电能累计

瞬时功率算出来以后,电能就是功率对时间的积分,离散化之后就是累加:

/* 每 20ms 调用一次(一个工频周期) */ void energy_accumulate(float p_watt) { static float energy_wh = 0.0f; /* 20ms = 20/3600/1000 小时 */ energy_wh += p_watt * (20.0f / 3600000.0f); if (energy_wh >= 0.01f) { /* 每 0.01Wh 存入一次,减少擦写 */ eeprom_save_energy(energy_wh); energy_wh = 0.0f; } }

参数说明:累加周期必须和实际采样窗口对齐,如果采样窗口是 20ms 但定时器实际周期是 20.5ms,长时间跑下来电能会有 2.5% 的系统误差。稳妥做法是用定时器的实际计数值反推时间,或者直接用 RTC 的秒脉冲做长时间基准。掉电保存不能在主循环里直接写 Flash,要开 PVD 电压监测中断,检测到掉电后靠电容储能完成最后一次写入。

4. FSMC 挂 NAND FLASH 与 RS485 上报:存储和通信的落地细节

4.1 FSMC 地址映射与 NAND FLASH 片选时序

STM32F103ZE 的 FSMC 可以把 NAND FLASH 直接映射到内存空间,CPU 读写就像访问数组一样。NAND 挂在 Bank2 时基址是 0x70000000,A16 接 CLE(命令锁存)、A17 接 ALE(地址锁存)、NE2 接片选 NCE,这三个信号决定了三个不同的访问地址。理解这个映射是写驱动的前提,也解释了为什么 stm32 cs 在 FSMC 场景下不是普通 GPIO,而是由硬件自动拉低的。

/* Bank2 地址映射:A16=CLE, A17=ALE, NE2=片选 */ #define NAND_CMD_AREA ((uint32_t)0x70000000) /* A16=1 */ #define NAND_ADDR_AREA ((uint32_t)0x70020000) /* A17=1 */ #define NAND_DATA_AREA ((uint32_t)0x70040000) /* 均不置位,读写数据 */ static void nand_write_cmd(uint8_t cmd) { *(volatile uint8_t *)NAND_CMD_AREA = cmd; } static void nand_write_addr(uint8_t addr) { *(volatile uint8_t *)NAND_ADDR_AREA = addr; } /* 读 ID:0x90 命令 + 0x00 地址,然后连读 5 字节 */ uint32_t nand_read_id(void) { nand_write_cmd(0x90); nand_write_addr(0x00); uint8_t id[5]; for (int k = 0; k < 5; k++) { id[k] = *(volatile uint8_t *)NAND_DATA_AREA; } return (id[0] << 24) | (id[1] << 16) | (id[2] << 8) | id[3]; }

逻辑说明:时序参数在FSMC_NANDInitTypeDef里的FSMC_TCLRFSMC_TARFSMC_THOLDFSMC_THIZ四个字段,单位是 HCLK 周期。K9F1G08 这类常见 NAND,TCLR 取 2、TAR 取 2、THOLD 取 2 基本能跑通;如果读 ID 返回全 0xFF 或者全 0x00,八成是片选没选中或者时序太快,先把FSMC_Bank2_NANDFSMC_SetupTime调大试试。硬件 ECC 记得开,FSMC_NANDInitStructure.FSMC_ECC设为 ENABLE,每 512 字节自动算一次,省掉软件 ECC 的 CPU 开销。

4.2 数据记录格式与坏块管理

电力数据的存储策略是「循环覆盖 + 定长记录」。每条记录固定 32 字节:时间戳 4 字节、三相电压电流各 2 字节共 12 字节、有功功率 4 字节、电能 4 字节、开关量状态 4 字节、CRC16 2 字节、预留 2 字节。按每秒一条算,一天 86400 条,不到 3MB,1Gb 的 NAND 能存小半年。

坏块管理用最简单的方式就够:NAND 的每个块第 1 页 spare 区的第 1 个字节是坏块标记(非 0xFF 即为坏块),上电时扫一遍建表,写满一块就换下一块。擦除是 NAND 的最小单位,512 页一块的 K9F1G08 每块 128KB,擦除时间约 2ms,写入前必须先擦除,别指望能原地改写。

4.3 Modbus RTU over RS485 的收发切换

对外通信走 RS485 半双工,用 Modbus RTU 从站协议最省事,上位机侧几乎所有组态软件都支持。帧格式:地址 1 字节 + 功能码 1 字节 + 数据 N 字节 + CRC16 2 字节,帧间至少 3.5 个字符时间的静默作为分隔。

功能码含义本项目用途
0x03读保持寄存器读实时电压电流、功率
0x04读输入寄存器读电能累积值
0x06写单个寄存器设置 CT 变比、报警阈值
0x10写多个寄存器批量下装参数

驱动上最容易翻车的是收发切换时刻。发送完最后一个字节后立刻拉低 DE,最后一个字节会因为移位寄存器还没发完而被截断,上位机看到的就是 CRC 错误。正确做法是等 USART 的 TC 标志置位再切。

/* RS485 发送:拉高 DE,发完等 TC 再拉低 */ #define RS485_DE_H() GPIO_SetBits(GPIOB, GPIO_Pin_9) #define RS485_DE_L() GPIO_ResetBits(GPIOB, GPIO_Pin_9) void rs485_send(const uint8_t *buf, uint16_t len) { RS485_DE_H(); for (uint16_t k = 0; k < len; k++) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); USART_SendData(USART2, buf[k]); } while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); RS485_DE_L(); /* 必须等 TC,不是 TXE */ }

参数说明:USART_FLAG_TXE只表示发送数据寄存器空,可以塞下一个字节;USART_FLAG_TC才表示移位寄存器也发完了。很多人用 TXE 关 DE,通信时好时坏,查半天查不出原因。另外 RS485 总线两端要各接一个 120Ω 终端电阻,中间节点不要接,节点数超过 32 个要换低负载的收发器。

5. 两点校准与现场排错:把采样误差压到 0.5 级以内

5.1 两点校准法消除增益与偏移误差

ADC 的偏移误差和增益误差是分开的,单点校准只能修一个。标准做法是给两个已知输入,比如电压通道加 20% 和 80% 额定值,记录原始码值码₁、码₂和对应的标准值标₁、标₂,解出线性系数:

/* 两点标定:x 为 ADC 原始码值,y 为标准表读数 */ void calib_two_point(uint16_t code1, float std1, uint16_t code2, float std2, float *k, float *b) { *k = (std2 - std1) / ((float)code2 - (float)code1); /* 增益 */ *b = std1 - (*k) * (float)code1; /* 偏移 */ } /* 实际使用:y = k * code + b */

参数说明:两个校准点要拉开至少 50% 量程,靠得太近时code2-code1很小,除法会把噪声放大。校准要在常温、额定负载下做,做完把 k、b 存进 Flash 或者 MCU 的选项字节。CT 通道还要额外做相位补偿,方法是加纯阻性负载,观察功率因数的偏差角,在固件里对电流数组做插值移相。

5.2 现场常见故障对照

现象可能原因排查顺序
采样值整体偏低 5%采样时间不足,保持电容没充满增采样周期数 → 查源阻抗
某相数据始终为 0DMA 缓冲区通道错位查 BufferSize → 查序列配置
通信时好时坏DE 切换用 TXE 而非 TC改代码 → 示波器看波形
程序卡在 delay 里SysTick 被高优先级中断抢占查 NVIC 优先级分组
上电后 IO 无反应JTAG 复用引脚未释放加 SWJ_JTAGDisable 重映射
读 NAND 全 0xFF片选或时序配置错误调 TCLR/TAR → 量 NCE 波形

delay函数卡死是个高频问题:如果 ADC 中断的抢占优先级高于 SysTick,而中断服务程序执行时间又超过了 1ms,delay_ms里的计数值永远不会被更新,程序就死在那儿了。解决办法是把 SysTick 的优先级设到最高,或者干脆用 DWT 的周期计数器做延时,不受中断影响。

5.3 用定时器输入捕获测频做电网异常判据

电力数据采集系统除了采数据,还得判断电网是否正常。把一路电压信号经过比较器整形成方波送进 TIM3 的 CH1,用输入捕获测周期,就能实时拿到电网频率。频率超出 49.5~50.5Hz 范围持续 200ms,判为频率异常;同时结合电压有效值低于 80% 额定值判欠压,高于 110% 判过压。这三个判据配合起来,比单纯看某一相电压是否越限要可靠得多,能有效滤掉电机启动瞬间的电压跌落误报。

/* TIM3 CH1 输入捕获:上升沿捕获,两次捕获值相减即周期 */ void TIM3_IRQHandler(void) { static uint16_t last_ccr = 0; if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) { uint16_t ccr = TIM_GetCapture1(TIM3); uint16_t period = ccr - last_ccr; /* 1MHz 计数,周期值即微秒 */ last_ccr = ccr; g_freq_x100 = 100000000UL / period; /* 频率 ×100,整数运算 */ TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } }

参数说明:定时器预分频设成 71,计数时钟 1MHz,一个工频周期 20000 个计数,16 位计数器放得下但接近上限,建议开溢出中断做扩展计数。period直接相减能正确处理计数器回卷,这是无符号减法的特性,不用额外判断。测到的频率值再做 16 点滑动平均,把比较器抖动带来的 ±0.2Hz 波动滤掉,判据才稳。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询