《STM32F103RCT6 14 路 ADC 采集器实战指南:从硬件原理图到 HAL 库固件全流程落地》

目录

  1. 引言 
  2. STM32F103RCT6 ADC 资源深度解析
  3. 14 路 ADC 采集器硬件系统设计

  4. 软件系统设计与实现 
  5. 系统测试与验证
  6. 常见问题与注意事项 
  7. 总结与展望
  8. 参考文献

1. 引言 

在现代工业控制、物联网 (IoT)、智能仪器仪表等领域,对物理世界的模拟信号进行精确、快速的采集是核心任务之一。模拟 - to - 数字转换器 (ADC) 是实现这一任务的关键组件。STM32 系列微控制器以其强大的性能、丰富的外设和相对低廉的成本,成为嵌入式 ADC 采集系统设计的首选平台。

本项目旨在设计一个基于 STM32F103RCT6 微控制器的 14 通道 ADC 采集系统。该系统不仅能够实现多通道数据的循环采集,还将重点关注采集精度、系统稳定性、抗干扰能力以及软件开发的规范性和高效性。通过本项目的深入剖析,将能够掌握从硬件原理图设计、PCB Layout 到基于 HAL 库的固件开发的完整流程,并理解在实际工程中需要注意的关键技术点。

项目主要技术指标:

  • 采集通道数: 14 路单端模拟输入。
  • ADC 分辨率: 12 位。
  • 输入电压范围: 0V ~ 3.3V 。
  • 采样率: 可配置,目标单通道采样率不低于 1kHz。
  • 数据输出: 通过 UART 串口输出原始 ADC 值及转换后的电压值。
  • 数据处理: 集成滑动平均滤波算法。

2. STM32F103RCT6 ADC 资源深度解析

STM32F103RCT6 内置了 3 个独立的 12 位逐次逼近型 ADC(ADC1, ADC2, ADC3),这为多通道、高并发的数据采集提供了硬件基础。

2.1 ADC 核心特性

特性参数 / 描述
分辨率12 位,可配置为 10/8/6 位以提高转换速度。
转换时间最快 1μs(在 ADCCLK=14MHz,采样时间 = 1.5 周期时)。
输入电压范围VREF- ≤ VIN ≤ VREF+。通常 VREF + 连接到 VDD(3.3V),VREF - 连接到 GND。
参考电压外部 VREF + 引脚或内部参考电压(VREFINT,约 1.2V)。
转换模式单次转换模式:触发一次,转换一个通道后停止。连续转换模式:触发一次后,连续不断地转换同一通道。扫描模式:对 ADC_SQRx 寄存器中配置的所有通道进行依次转换。
触发方式软件触发:通过ADC_SoftwareStartConvCmd()启动。硬件触发:定时器捕获 / 比较、外部中断等。
数据对齐右对齐:12 位转换结果存储在ADC_DR寄存器的低 12 位。左对齐:12 位转换结果存储在ADC_DR寄存器的高 12 位。
通道数量16 个外部通道(PA0-PA7, PB0-PB1, PC0-PC5)和 2 个内部通道(温度传感器、内部参考电压 VREFINT)。
转换序列规则通道:最多 16 个通道,常规转换。注入通道:最多 4 个通道,具有更高优先级,可 “注入” 到规则通道转换过程中。
中断可在转换结束(EOC)、注入转换结束(JEOC)、数据溢出(OVR)时产生中断。
DMA 请求支持将转换结果直接传输到内存(DMA),无需 CPU 干预。

2.2 ADC 时钟配置

ADC 的时钟(ADCCLK)不是直接由系统时钟提供,而是由 PCLK2 经过一个预分频器产生。

  • 时钟源: PCLK2 (APB2 总线时钟)。对于 STM32F103RCT6,当系统时钟为 72MHz 时,PCLK2 也为 72MHz。
  • 预分频器: ADC1, ADC2, ADC3 共享一个预分频器,可配置为 2/4/6/8 分频。
  • ADCCLK 频率: ADCCLK = PCLK2 / 预分频系数。
  • 重要限制: ADCCLK 的频率不得超过 14MHz,否则会导致转换结果不准确。

示例配置:若 PCLK2 = 72MHz,为了使 ADCCLK = 9MHz(≤14MHz),预分频系数应设为 8 (72MHz / 8 = 9MHz)。

2.3 14 路 ADC 通道分配方案

我们将利用 ADC1 的扫描模式和 DMA 功能来实现 14 路通道的高效采集。选择以下 14 个外部通道:

采集通道序号STM32 ADC 通道对应 GPIO 引脚用途
CH1ADC_Channel_0PA0外部模拟输入 1
CH2ADC_Channel_1PA1外部模拟输入 2
CH3ADC_Channel_2PA2外部模拟输入 3
CH4ADC_Channel_3PA3外部模拟输入 4
CH5ADC_Channel_4PA4外部模拟输入 5
CH6ADC_Channel_5PA5外部模拟输入 6
CH7ADC_Channel_6PA6外部模拟输入 7
CH8ADC_Channel_7PA7外部模拟输入 8
CH9ADC_Channel_8PB0外部模拟输入 9
CH10ADC_Channel_9PB1外部模拟输入 10
CH11ADC_Channel_10PC0外部模拟输入 11
CH12ADC_Channel_11PC1外部模拟输入 12
CH13ADC_Channel_12PC2外部模拟输入 13
CH14ADC_Channel_13PC3外部模拟输入 14

选择理由:

  • 这些引脚分布在 PA、PB、PC 三个端口,便于在 PCB 上进行布局布线。
  • 避开了一些常用的调试和通信引脚(如 PA9/TX, PA10/RX, PA13/SWDIO, PA14/SWCLK)。
  • 保留了 PC4, PC5 作为备用通道或用于其他功能。

3. 14 路 ADC 采集器硬件系统设计

硬件设计是整个项目的基石,其合理性直接决定了系统的性能和稳定性,尤其是对于对噪声敏感的模拟电路部分。

3.1 总体硬件架构

整个硬件系统可以分为以下几个功能模块:

  1. 电源模块:为整个系统提供稳定、干净的 3.3V 和 5V 电源。
  2. STM32F103RCT6 最小系统:包括 MCU、晶振、复位、调试接口。
  3. 14 路 ADC 输入调理与保护电路:对外部输入的模拟信号进行滤波、限幅等处理。
  4. 通信接口模块:主要是 UART 串口,用于与 PC 机或其他设备通信。
  5. 用户接口 :如 LED 指示灯、按键。

3.2 电源系统设计

一个稳定可靠的电源系统对于 ADC 采集的精度至关重要。

3.2.1 电源架构
  • 输入: 建议使用 5V 直流电源输入(如 USB 5V 或外部 5V 适配器)。
  • 转换: 使用一个低压差线性稳压器(LDO)将 5V 转换为 3.3V,为 STM32 和其他 3.3V 外设供电。
3.2.2 LDO 选型
特性推荐型号举例说明
输出电压3.3V固定输出。
输出电流≥100mASTM32F103RCT6 的典型工作电流约为 20-50mA,留有足够余量。
压差 (V dropout)≤0.5V @ 100mA在 5V 输入下,能稳定输出 3.3V。
噪声低噪声如 TI 的 TPS79333、ADI 的 ADP3338 或国产的 AMS1117-3.3。
3.2.3 电源滤波与去耦

电源滤波是抑制噪声的关键。

  • 输入滤波: 在 LDO 的输入端(5V 侧)放置一个大容量的电解电容(如 10μF)和一个小容量的陶瓷电容(如 0.1μF/100nF),用于滤除低频和高频噪声。
  • 输出滤波: 在 LDO 的输出端(3.3V 侧)同样放置一个电解电容(如 10μF)和一个陶瓷电容(如 0.1μF),以稳定输出电压并滤除 LDO 本身产生的噪声。
  • 去耦电容: 在 STM32 芯片的每一对 VDD/VSS 引脚旁边,都必须放置一个 0.1μF 的陶瓷去耦电容,且电容要尽可能靠近引脚,以抑制电源线上的瞬态噪声,保证芯片内部各模块的稳定工作。
3.2.4 电源电路原理图

plaintext

[5V Input] ----+---- [10μF Electrolytic Cap] ---- GND
                |
                +---- [0.1μF Ceramic Cap] ---- GND
                |
                +---- [LDO (e.g., AMS1117-3.3)] Vin
                                                   |
                                                   Vout ----+---- [10μF Electrolytic Cap] ---- GND
                                                             |
                                                             +---- [0.1μF Ceramic Cap] ---- GND
                                                             |
                                                             +---- [To STM32 VDD Pins and other 3.3V peripherals]

3.3 ADC 输入通道设计与信号调理

为了保证 ADC 采集的精度和可靠性,对外部输入信号进行适当的调理是必要的。

3.3.1 基本输入保护与滤波电路

对于每个 ADC 输入通道,推荐使用以下电路:

  • 限流电阻 (R1): 作用是限制流入 ADC 引脚的电流,防止因外部信号源故障(如短路到电源)而损坏 MCU 内部的 ESD 保护二极管。通常取值为 1kΩ ~ 10kΩ。
  • RC 低通滤波器 (R2, C1): 用于滤除高于 ADC Nyquist 频率的噪声,避免混叠。R2 和 C1 构成一个简单的一阶低通滤波器。
    • 截止频率 (fc): fc = 1 / (2 * π * R2 * C1)
    • 选择原则: 截止频率应高于信号中最高频率成分,同时低于 ADC 采样频率的一半。例如,如果信号带宽是 100Hz,采样率是 1kHz,可以选择 fc 约为 200Hz。
    • 典型值: R2 = 1kΩ, C1 = 100nF → fc ≈ 159Hz。

注意: R1 和 R2 的总和不应过大,否则会与 ADC 输入引脚的寄生电容形成新的 RC 网络,影响采样保持时间。通常,总电阻应小于 10kΩ。

3.3.2 单端输入通道原理图

plaintext

[External Analog Signal Source]
       |
       +---- [R1 (e.g., 1kΩ)] ----+
                                  |
                                  +---- [R2 (e.g., 1kΩ)] ----+
                                                             |
                                                             +---- [C1 (e.g., 100nF)] ---- GND
                                                             |
                                                             +---- [STM32 ADC Input Pin (e.g., PA0)]

注:R1 和 R2 可以合并为一个电阻,如果只需要限流和滤波功能,可以简化为 R = R1+R2, C = C1。分开设计则更灵活。

3.3.3 扩展:测量超出 0-3.3V 范围的信号

如果需要测量的信号电压范围超出了 ADC 的输入范围(如 0-10V 或 - 5V 到 + 5V),则需要增加额外的信号调理电路。

  • 测量高电压 (如 0-10V): 使用电阻分压网络

    • 原理:通过两个电阻将高电压按比例衰减到 0-3.3V。
    • 公式:V_ADC = V_IN * (R2 / (R1 + R2))
    • 设计要点:
      1. 确保 R1 + R2 的值足够大,以减少对信号源的负载效应(通常为几十 kΩ 到几百 kΩ)。
      2. 电阻的精度要高,推荐使用 1% 或 0.1% 精度的金属膜电阻。
      3. 在 ADC 输入引脚前仍需加入 RC 低通滤波器。
      4. 软件中需要根据分压比进行反推计算。
  • 测量双极性电压 (如 - 5V 到 + 5V): 使用电平抬升电路

    • 原理:将双极性信号叠加一个固定的偏移电压,使其整体上移到 0-3.3V 范围内。
    • 常用电路:使用一个运算放大器构成的加法器电路,将输入信号与一个由 3.3V 电源分压得到的参考电压(如 1.65V)相加。
    • 设计要点:对运放的选型(如输入偏置电流、失调电压)有一定要求,电路相对复杂。

3.4 MCU 最小系统设计

STM32F103RCT6 的最小系统包括以下几个部分:

  1. 电源引脚 (VDD, VSS, VDDA, VSSA):

    • VDD 和 VSS 是数字电源和地。
    • VDDA 和 VSSA 是模拟电源和地,必须连接到干净的 3.3V 和 GND。
    • 关键原则: 为了减少数字电路对模拟电路的干扰,VDDA 应通过一个铁氧体磁珠或一个小电阻(如 0Ω 电阻,用于测试时断开)连接到 VDD,VSSA 直接连接到 VSS。在 PCB 上,模拟地和数字地应单点连接。
  2. 晶振电路 (HSE):

    • 推荐使用 8MHz 的外部高速晶振(HSE)。
    • 匹配电容:在晶振的两个引脚和地之间各接一个电容,通常为 18pF ~ 22pF,具体值需参考晶振 datasheet。
    • 芯片:选择无源晶振。
  3. 复位电路 (NRST):

    • 推荐使用外部复位电路,以提高复位的可靠性。
    • 一个简单的上电复位电路由一个电阻和一个电容组成。
    • 也可以使用专用的复位芯片。
    • 典型值: R = 10kΩ (上拉到 VDD),C = 1μF (接地)。
  4. 调试接口 (SWD):

    • STM32 支持 JTAG 和 SWD 两种调试接口。SWD 接口仅需两根线(SWDIO 和 SWCLK),更为简洁。
    • 引脚:PA13 (SWDIO), PA14 (SWCLK)。
    • 必须提供 GND 连接。
    • 建议在 SWDIO 和 SWCLK 引脚上添加 10kΩ 的上拉电阻,增强信号稳定性。

3.5 通信接口设计

本项目使用 UART1 进行串口通信。

  • 引脚: PA9 (USART1_TX), PA10 (USART1_RX)。
  • 电平: TTL 电平。如果需要与 PC 机的 RS-232 接口通信,需要使用 MAX232 等电平转换芯片。如果通过 USB 转 TTL 模块连接 PC,则可直接连接。
  • 电路: TX 和 RX 引脚可以直接连接到通信伙伴的 RX 和 TX 引脚(注意交叉连接:MCU_TX -> 对方_RX, MCU_RX -> 对方_TX)。为增强抗干扰能力,可以在引脚上串联一个小电阻(如 100Ω)。

3.6 PCB 布局与布线原则

PCB 设计是硬件开发中至关重要的一环,尤其是对于包含模拟和数字混合信号的系统。

3.6.1 布局 (Layout)
  1. 分区布局:

    • 将电路板清晰地划分为模拟区数字区
    • 模拟区包括:ADC 输入连接器、信号调理电路、VDDA、VSSA。
    • 数字区包括:MCU 数字部分、晶振、UART 接口、电源转换电路。
    • 将 LDO 放置在靠近模拟区和数字区的边界,以便其输出能方便地为两个区域供电。
  2. 关键元件放置:

    • 去耦电容: 尽可能靠近 STM32 的电源引脚。
    • 晶振: 尽可能靠近 MCU 的 OSC_IN 和 OSC_OUT 引脚,减少引线长度。晶振和匹配电容应放在同一侧。
    • LDO: 输入输出电容应尽可能靠近 LDO 本身。
    • ADC 输入连接器: 尽量靠近 ADC 输入调理电路,缩短模拟信号在 PCB 上的路径。
3.6.2 布线 (Routing)
  1. 地线处理 (最关键):

    • 使用星形接地或单点接地: 所有模拟地(AGND)和数字地(DGND)最终在一个点连接到电源地。这个点通常选择在电源入口处或 MCU 的 GND 引脚附近。
    • 加粗地线: 主地线应尽可能粗,以降低地线阻抗,提供稳定的参考电位。
    • 模拟地平面: 如果可能,为模拟区创建一个独立的地平面,然后通过一个小的 “桥” 或一个 0Ω 电阻与数字地平面连接。这能有效隔离数字噪声。
  2. 电源线:

    • 主电源线也应适当加粗。
    • 在模拟电源(VDDA)和数字电源(VDD)之间使用铁氧体磁珠或小电阻进行隔离。
  3. 模拟信号线:

    • 短而直: ADC 输入信号线应尽可能短,避免长距离平行布线。
    • 远离数字信号线: 特别是高速切换的时钟线(如晶振、SPI、I2C 时钟)和 PWM 输出。
    • 避免直角: 布线时使用 45 度角或圆弧,避免 90 度直角,以减少信号反射和 EMI。
    • 差分走线 (如适用): 如果使用差分输入,确保两根信号线长度一致、紧密耦合。
  4. 数字信号线:

    • 时钟线和高速信号线应进行阻抗匹配,并远离模拟区域。

3.7 硬件清单 (BOM)

序号元件名称规格 / 型号数量备注
1微控制器STM32F103RCT61LQFP-64 封装
2LDO 稳压器AMS1117-3.31或其他等效型号
3外部晶振8MHz1无源晶振
4电容10μF/16V (电解)2电源滤波
5电容0.1μF (100nF)若干去耦、滤波
6电容18pF ~ 22pF2晶振匹配电容
7电容1μF1复位电路
8电阻1kΩ14+ADC 输入限流 / 滤波
9电阻10kΩ若干上拉 / 下拉,复位电路
10电阻100nF2ADC 输入滤波
11电阻100Ω2UART 接口限流 (可选)
12LED任意颜色1电源指示 (可选)
13按键轻触开关1复位按键 (可选)
14接插件排针 / 排母若干电源、UART、ADC 输入
15印刷电路板PCB1自定义设计

4. 软件系统设计与实现

本章节将详细介绍基于 STM32CubeMX 和 HAL 库的软件实现。HAL (Hardware Abstraction Layer) 库是 ST 官方推荐的、跨 STM32 系列的抽象层库,它提供了一套统一的 API,简化了外设的配置和使用。

4.1 开发环境与工具链

  1. STM32CubeMX:图形化配置工具,用于生成初始化代码。下载地址
  2. Keil MDK-ARM 或 STM32CubeIDE:集成开发环境 (IDE),用于编写、编译和调试代码。
    • Keil MDK-ARM:功能强大,使用广泛。
    • STM32CubeIDE:免费,基于 Eclipse,与 STM32CubeMX 深度集成。
  3. ST-Link/V2 或 J-Link:调试器 / 编程器,用于将程序下载到 MCU 并进行在线调试。
  4. 串口助手 (如 SecureCRT, PuTTY, SSCOM):用于在 PC 上接收和显示 MCU 通过串口发送的数据。

4.2 基于 HAL 库的项目结构

使用 STM32CubeMX 生成的 HAL 库项目结构清晰,主要包含以下部分:

plaintext

Project_Name/
├── Drivers/
│   ├── CMSIS/             // Cortex-M内核相关文件
│   └── STM32F1xx_HAL_Driver/ // STM32F1系列HAL库文件
│       ├── Inc/           // 头文件
│       └── Src/           // 源文件
├── Inc/                   // 用户头文件
│   ├── main.h
│   ├── stm32f1xx_hal_conf.h // HAL库配置文件
│   └── ...
├── Src/                   // 用户源文件
│   ├── main.c
│   ├── stm32f1xx_hal_msp.c // 硬件抽象层配置文件 (MCU Specific)
│   ├── stm32f1xx_it.c    // 中断服务程序
│   ├── system_stm32f1xx.c // 系统初始化文件
│   └── ...
├── Project_Name.uvprojx   // Keil MDK项目文件 (或 .project for CubeIDE)
└── ...
  • main.c: 程序主函数,包含main()入口,以及while(1)主循环。
  • stm32f1xx_hal_msp.c: 此文件非常重要,用于实现 HAL 库中与硬件相关的底层函数,如外设时钟使能、GPIO 配置、中断优先级配置等。用户可以在这里重写HAL_ADC_MspInit()HAL_UART_MspInit()等函数。
  • stm32f1xx_it.c: 包含所有中断服务程序 (ISR) 的模板。当外设产生中断时,程序会跳转到这里对应的函数执行。HAL 库通常会在 ISR 中调用一个回调函数(Callback)。

4.3 ADC 外设初始化详解

我们将使用 ADC1 的扫描模式连续转换模式,并结合DMA来实现 14 路通道的无缝采集。

4.3.1 使用 STM32CubeMX 进行配置
  1. 创建新项目:选择 STM32F103RCT6 芯片。
  2. 配置时钟
    • 在 Pinout & Configuration -> RCC 中,将 HSE 设置为 Crystal/Ceramic Resonator
    • 切换到 Clock Configuration 标签页,配置系统时钟为 72MHz。确保 APB2 Prescaler 设置为 /1 (PCLK2 = 72MHz)。
  3. 配置 ADC1
    • 在 Pinout & Configuration -> ADC1 中,勾选需要使用的 14 个通道 (IN0 to IN13)。STM32CubeMX 会自动将对应的 GPIO 引脚配置为Analog模式。
    • 在 Parameter Settings 中:
      • INx Configuration: 保持默认。
      • ADC_Settings:
        • ModeIndependent ADC mode
        • Data AlignmentRight alignment (推荐)
        • Scan Conversion ModeEnabled (关键!)
        • Continuous Conversion ModeEnabled (关键!)
        • Discontinuous Conversion ModeDisabled
        • Number Of Conversion14 (关键!)
        • External Trigger Conversion SourceNone (我们将使用软件触发启动第一次转换)
        • Rank: 在这里可以看到 14 个通道被自动添加。可以调整它们的转换顺序。
        • Sampling Time: 为每个通道选择采样时间。采样时间越长,对噪声的抑制越好,但采样率越低。推荐选择 1.5 Cycles 到 239.5 Cycles 之间的值。例如,选择 11.5 Cycles
      • ADC_ClockPrescalerPCLK2 divided by 8 (因为 PCLK2=72MHz, 72/8=9MHz ≤ 14MHz)。
  4. 配置 DMA
    • 在 Pinout & Configuration -> DMA 中,点击 Add 按钮添加一个 DMA 请求。
    • Channel: 选择 DMA1_Channel1 (ADC1 的 DMA 请求映射到这个通道)。
    • DirectionPeripheral To Memory
    • Peripheral address: 选择 ADC1_DR (ADC1 的数据寄存器地址)。
    • Memory address: 这里暂时不填,我们将在代码中指定一个数组作为目标缓冲区。
    • Data Width: 两边都选择 HalfWord (16-bit),因为 ADC 转换结果是 12 位,存储在 16 位的变量中。
    • ModeCircular (关键!循环模式,当缓冲区满后,会自动从头开始覆盖,适合连续采集)。
    • Increment Memory AddressEnabled (关键!内存地址自动递增)。
    • Increment Peripheral AddressDisabled (外设地址固定)。
  5. 配置 UART1
    • 在 Pinout & Configuration -> USART1 中,将 Mode 设置为 Asynchronous (异步模式)。
    • 在 Configuration -> Parameter Settings 中,设置 Baud Rate 为 115200,其他参数默认 (8N1)。
  6. 生成代码
    • 点击右上角 GENERATE CODE 按钮,选择你的 IDE (如 MDK-ARM),生成初始化代码。
4.3.2 代码实现与分析

STM32CubeMX 生成的代码已经完成了大部分初始化工作。我们需要在main.c中添加一些用户代码来完成整个功能。

1. 定义 ADC 数据缓冲区和滤波参数

main.c/* USER CODE BEGIN PV */ (Private variables) 区域,定义一个数组来存储 DMA 传输过来的 ADC 原始数据,以及一个用于滑动平均滤波的数组和参数。

c

运行

/* USER CODE BEGIN PV */
// ADC数据缓冲区,用于存储14路通道的原始转换结果
uint32_t adc_raw_data[14]; 

// 滑动平均滤波参数
#define FILTER_WINDOW_SIZE 10 // 滤波窗口大小,可调整
uint32_t adc_filter_buffers[14][FILTER_WINDOW_SIZE]; // 14个通道的滤波缓冲区
uint8_t adc_filter_index[14] = {0}; // 每个缓冲区的当前索引
float adc_filtered_values[14]; // 滤波后的结果 (电压值)

// UART发送缓冲区
char uart_tx_buffer[200];
/* USER CODE END PV */

注意:使用uint32_t是为了方便后续计算平均值,避免溢出。虽然 ADC 结果是 12 位,但 HAL 库的 DMA 接收函数有时会使用 32 位变量来简化处理。

2. 在main()函数中启动 ADC 和 DMA

main()函数的while(1)循环之前,/* USER CODE BEGIN 2 */区域,添加启动 ADC 转换和 DMA 传输的代码。

c

运行

/* USER CODE BEGIN 2 */
  // 启动ADC1的DMA请求,将转换结果传输到adc_raw_data数组
  if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw_data, 14) != HAL_OK)
  {
    Error_Handler(); // 如果启动失败,调用错误处理函数
  }
/* USER CODE END 2 */
  • &hadc1: ADC_HandleTypeDef 结构体指针,由 CubeMX 自动生成,包含了 ADC1 的所有配置信息。
  • (uint32_t*)adc_raw_data: 目标内存缓冲区的地址。
  • 14: 要传输的数据量(即通道数)。

3. 实现滑动平均滤波函数

main.c/* USER CODE BEGIN 4 */ (Private function prototypes) 区域,声明并实现滤波函数。

c

运行

/* USER CODE BEGIN 4 */

/**
  * @brief  滑动平均滤波函数
  * @param  channel: ADC通道号 (0-13)
  * @param  new_value: 新采样到的ADC原始值
  * @retval 滤波后的电压值
  */
float滑动_average_filter(uint8_t channel, uint32_t new_value)
{
  uint32_t sum = 0;
  uint8_t i;

  // 将新值存入缓冲区,覆盖最旧的值
  adc_filter_buffers[channel][adc_filter_index[channel]] = new_value;
  
  // 更新索引,循环使用缓冲区
  adc_filter_index[channel] = (adc_filter_index[channel] + 1) % FILTER_WINDOW_SIZE;
  
  // 计算窗口内所有值的总和
  for (i = 0; i < FILTER_WINDOW_SIZE; i++)
  {
    sum += adc_filter_buffers[channel][i];
  }
  
  // 计算平均值并转换为电压值
  // 假设VREF+ = 3.3V, ADC分辨率为12位 (4096)
  adc_filtered_values[channel] = (float)sum / FILTER_WINDOW_SIZE / 4095.0f * 3.3f;
  
  return adc_filtered_values[channel];
}

/**
  * @brief  通过UART发送所有通道的ADC数据
  * @param  None
  * @retval None
  */
void send_adc_data_via_uart(void)
{
  uint8_t i;
  float voltage;
  
  // 使用sprintf格式化数据
  sprintf(uart_tx_buffer, "--- ADC Data ---\r\n");
  HAL_UART_Transmit(&huart1, (uint8_t*)uart_tx_buffer, strlen(uart_tx_buffer), HAL_MAX_DELAY);
  
  for (i = 0; i < 14; i++)
  {
    // 对每个通道的数据进行滤波
    voltage = sliding_average_filter(i, adc_raw_data[i]);
    
    // 格式化输出:通道号,原始ADC值,滤波后的电压值
    sprintf(uart_tx_buffer, "CH%d: Raw=%.4lu, Voltage=%.2fV\r\n", 
            i+1, adc_raw_data[i], voltage);
    
    // 通过UART发送数据
    HAL_UART_Transmit(&huart1, (uint8_t*)uart_tx_buffer, strlen(uart_tx_buffer), HAL_MAX_DELAY);
  }
  sprintf(uart_tx_buffer, "----------------\r\n\r\n");
  HAL_UART_Transmit(&huart1, (uint8_t*)uart_tx_buffer, strlen(uart_tx_buffer), HAL_MAX_DELAY);
}

/* USER CODE END 4 */

4. 在while(1)主循环中处理和发送数据

c

运行

/* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */

    // 每隔一段时间发送一次数据,例如1秒
    send_adc_data_via_uart();
    
    // 使用HAL库的延时函数
    HAL_Delay(1000); 
  }
  /* USER CODE END 3 */

4.4 DMA 配置与数据传输

在 4.3.1 节中,我们已经在 STM32CubeMX 中配置了 DMA。生成的代码会自动初始化 DMA 控制器。

  • DMA 的配置细节可以在stm32f1xx_hal_msp.c文件中的HAL_ADC_MspInit()函数中找到:

    c

    运行

    void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
    {
      GPIO_InitTypeDef GPIO_InitStruct = {0};
      if(hadc->Instance==ADC1)
      {
      /* USER CODE BEGIN ADC1_MspInit 0 */
    
      /* USER CODE END ADC1_MspInit 0 */
        /* Peripheral clock enable */
        __HAL_RCC_ADC1_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_GPIOB_CLK_ENABLE();
        __HAL_RCC_GPIOC_CLK_ENABLE();
        /**ADC1 GPIO Configuration
        PA0-WKUP     ------> ADC1_IN0
        PA1     ------> ADC1_IN1
        ... (其他GPIO配置)
        PC3     ------> ADC1_IN13
        */
        GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|...|GPIO_PIN_7;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
        GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
        GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
    
        /* ADC1 DMA Init */
        /* ADC1_RX Init */
        hdma_adc1.Instance = DMA1_Channel1;
        hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
        hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
        hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
        hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 注意这里是WORD (32-bit)
        hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;   // 注意这里是WORD (32-bit)
        hdma_adc1.Init.Mode = DMA_CIRCULAR;
        hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
        if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
        {
          Error_Handler();
        }
    
        __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc1);
    
      /* USER CODE BEGIN ADC1_MspInit 1 */
    
      /* USER CODE END ADC1_MspInit 1 */
      }
    
    }
    
    注意:CubeMX 生成的代码中,PeriphDataAlignment 和 MemDataAlignment 可能被设置为 WORD (32-bit)。这与我们在main.c中使用uint32_t adc_raw_data[14]是匹配的。如果使用uint16_t,则需要将其改为HALFWORD (16-bit)

HAL_ADC_Start_DMA()被调用后,ADC1 会开始按照配置的序列(14 个通道)进行连续扫描转换。每当一个通道转换完成后,ADC 会产生一个 DMA 请求,DMA 控制器会自动将ADC_DR寄存器中的数据(12 位,右对齐)传输到adc_raw_data数组的下一个位置。当 14 个通道都转换完毕后,由于 DMA 工作在循环模式,它会自动回到数组的起始位置,覆盖旧的数据,开始新一轮的传输。

这个过程完全由硬件完成,CPU 可以同时执行其他任务(如数据处理、UART 发送等),极大地提高了系统效率。

4.5 中断处理机制

虽然在本设计中我们主要依赖 DMA 来传输数据,但了解 ADC 的中断机制仍然很重要。

  1. ADC 转换结束中断 (EOC):

    • 当一个规则通道的转换完成后,会触发 EOC 中断。
    • 在扫描模式下,如果CONTINUOUS Conversion ModeDISABLE,则在所有通道转换完毕后触发一次 EOC 中断。如果为ENABLE,则每个通道转换完毕后都会触发一次 EOC 中断。
    • 在 HAL 库中,中断服务程序ADC1_2_IRQHandler会被调用,它内部会调用HAL_ADC_IRQHandler(&hadc1)
    • HAL_ADC_IRQHandler会检查中断标志位,并调用相应的回调函数。对于 EOC 中断,回调函数是HAL_ADC_ConvCpltCallback(&hadc1)
    • 用户可以在stm32f1xx_it.cmain.c中重写HAL_ADC_ConvCpltCallback函数,在每次转换完成后执行特定操作。
  2. DMA 传输完成中断 (TC):

    • 当 DMA 传输完成预设的数据量(在我们的例子中是 14 个)后,会触发传输完成中断。
    • 中断服务程序DMA1_Channel1_IRQHandler会被调用,它会调用HAL_DMA_IRQHandler(&hdma_adc1)
    • 相应的回调函数是HAL_DMA_ConvCpltCallback(&hdma_adc1)
    • 在循环模式下,这个中断会在每填满一次缓冲区后触发一次。

在我们的主循环设计中,我们没有使用中断来触发数据处理,而是周期性地读取adc_raw_data数组。这种方式简单直接。另一种更高效的设计是:

  • 开启 DMA 传输完成中断。
  • HAL_DMA_ConvCpltCallback回调函数中设置一个标志位(如data_ready_flag = 1)。
  • while(1)循环中检查这个标志位,当标志位为 1 时,处理数据、发送 UART,并清除标志位。

这种中断驱动的方式可以更精确地控制数据处理的时机,避免了HAL_Delay()带来的不确定性。

4.6 数据处理与滤波算法

原始的 ADC 采样值往往包含各种噪声,影响测量精度。因此,数据滤波是 ADC 采集系统中常见的步骤。

4.6.1 滑动平均滤波 (Moving Average Filter)

我们在 4.3.2 节中已经实现了滑动平均滤波。

  • 原理:将最近 N 次的采样值相加,然后除以 N,得到当前的滤波输出值。N 称为窗口大小。
  • 优点:算法简单,易于实现,对抑制随机噪声(白噪声)效果较好。
  • 缺点:对信号的阶跃变化响应较慢,窗口越大,响应越慢,但滤波效果越好。
  • 适用场景:适用于变化缓慢的信号,如温度、湿度、光照强度等。
4.6.2 其他常用滤波算法
  • 中值滤波 (Median Filter)

    • 原理:将最近 N 次的采样值排序,取中间值作为当前的输出值。
    • 优点:对抑制脉冲噪声(椒盐噪声)非常有效。
    • 缺点:计算量比滑动平均略大,同样对快速变化的信号响应较慢。
  • 一阶滞后滤波 (First-Order IIR Filter / Low-Pass Filter)

    • 原理Y(n) = α * X(n) + (1 - α) * Y(n-1),其中X(n)是当前采样值,Y(n)是当前滤波输出,Y(n-1)是上一次滤波输出,α是滤波系数(0 < α < 1)。
    • 优点:只需要一个存储单元保存上一次的输出值,内存占用小。α值越小,滤波效果越平滑,但响应速度越慢。
    • 缺点:滤波效果受α值选择影响较大。
  • 卡尔曼滤波 (Kalman Filter)

    • 原理:一种基于状态空间模型的递归滤波算法,能够在存在噪声的情况下,最优地估计系统的状态。
    • 优点:滤波效果极佳,尤其适用于多变量、动态系统。
    • 缺点:算法复杂,计算量大,需要建立系统模型,对于简单应用来说过于复杂。

4.7 串口通信与数据格式化

UART 通信是嵌入式系统中最常用的数据输出方式之一。

  • HAL_UART_Transmit(): 阻塞式发送函数,直到所有数据发送完毕或超时才返回。
  • HAL_UART_Transmit_IT(): 非阻塞式(中断)发送函数,调用后立即返回,数据在中断中后台发送。
  • HAL_UART_Receive(): 阻塞式接收函数。
  • HAL_UART_Receive_IT(): 非阻塞式(中断)接收函数。

在我们的send_adc_data_via_uart()函数中,使用了sprintf()来格式化字符串。sprintf()功能强大,但需要注意:

  1. 缓冲区溢出:确保uart_tx_buffer的大小足够容纳格式化后的字符串,否则会导致内存 corruption。
  2. 浮点数支持:使用%f格式化浮点数会增加代码体积。如果对代码大小有严格要求,可以考虑将浮点数转换为整数后再发送(例如,将电压值乘以 100,发送整数部分和小数部分)。

    c

    运行

    // 不使用浮点数的sprintf示例
    uint16_t integer_part = (uint16_t)voltage;
    uint16_t fractional_part = (uint16_t)((voltage - integer_part) * 100);
    sprintf(uart_tx_buffer, "CH%d: Raw=%.4lu, Voltage=%d.%02dV\r\n", 
            i+1, adc_raw_data[i], integer_part, fractional_part);
    

4.8 主函数与程序流程

整个程序的执行流程如下:

  1. SystemInit(): 系统初始化,设置时钟等(由启动文件调用)。
  2. main():a. HAL_Init(): HAL 库初始化,配置系统滴答定时器(用于延时)。b. SystemClock_Config(): 配置系统时钟为 72MHz(由 CubeMX 生成)。c. MX_GPIO_Init()MX_ADC1_Init()MX_DMA_Init()MX_USART1_UART_Init(): 初始化各个外设(由 CubeMX 生成)。d. HAL_ADC_Start_DMA(): 启动 ADC 转换和 DMA 数据传输。e. while(1)循环:i. send_adc_data_via_uart(): 读取adc_raw_data数组,进行滤波,格式化并通过 UART 发送。ii. HAL_Delay(1000): 延时 1 秒。iii. 重复步骤 i 和 ii。

4.9 完整代码附录

由于代码较长,这里仅提供main.c的完整代码作为示例。其他文件(如stm32f1xx_hal_msp.c)由 STM32CubeMX 生成,通常无需大幅修改。

main.c 完整代码:

c

运行

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define FILTER_WINDOW_SIZE 10
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint32_t adc_raw_data[14];
uint32_t adc_filter_buffers[14][FILTER_WINDOW_SIZE];
uint8_t adc_filter_index[14] = {0};
float adc_filtered_values[14];
char uart_tx_buffer[200];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
float sliding_average_filter(uint8_t channel, uint32_t new_value);
void send_adc_data_via_uart(void);
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw_data, 14) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    send_adc_data_via_uart();
    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
  PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV8;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
float sliding_average_filter(uint8_t channel, uint32_t new_value)
{
  uint32_t sum = 0;
  uint8_t i;

  adc_filter_buffers[channel][adc_filter_index[channel]] = new_value;
  adc_filter_index[channel] = (adc_filter_index[channel] + 1) % FILTER_WINDOW_SIZE;
  
  for (i = 0; i < FILTER_WINDOW_SIZE; i++)
  {
    sum += adc_filter_buffers[channel][i];
  }
  
  adc_filtered_values[channel] = (float)sum / FILTER_WINDOW_SIZE / 4095.0f * 3.3f;
  
  return adc_filtered_values[channel];
}

void send_adc_data_via_uart(void)
{
  uint8_t i;
  float voltage;
  
  sprintf(uart_tx_buffer, "--- ADC Data ---\r\n");
  HAL_UART_Transmit(&huart1, (uint8_t*)uart_tx_buffer, strlen(uart_tx_buffer), HAL_MAX_DELAY);
  
  for (i = 0; i < 14; i++)
  {
    voltage = sliding_average_filter(i, adc_raw_data[i]);
    
    sprintf(uart_tx_buffer, "CH%d: Raw=%lu, Voltage=%.2fV\r\n", 
            i+1, adc_raw_data[i], voltage);
    
    HAL_UART_Transmit(&huart1, (uint8_t*)uart_tx_buffer, strlen(uart_tx_buffer), HAL_MAX_DELAY);
  }
  sprintf(uart_tx_buffer, "----------------\r\n\r\n");
  HAL_UART_Transmit(&huart1, (uint8_t*)uart_tx_buffer, strlen(uart_tx_buffer), HAL_MAX_DELAY);
}
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif

5. 系统测试与验证

完成软硬件设计后,必须进行充分的测试与验证,以确保系统功能正常且性能达标。

5.1 硬件调试

  1. 视觉检查 (Visual Inspection):

    • 检查 PCB 上是否有明显的短路、断路、虚焊、漏焊。
    • 确认所有元件的型号、封装和极性是否正确。
    • 检查电源、地线等关键网络的走线是否符合设计要求。
  2. 电源测试:

    • 在不通电的情况下,使用万用表测量 VDD/GND、VDDA/VSSA 之间是否有短路。
    • 接入电源,测量 LDO 输入端和输出端的电压是否正常(分别为 5V 和 3.3V 左右)。
    • 检查电源指示灯(如果设计了)是否正常点亮。
  3. MCU 最小系统测试:

    • 连接 ST-Link 调试器。
    • 尝试通过 IDE(如 Keil)连接目标 MCU。如果能成功连接并读取芯片 ID,说明 MCU 基本工作正常。
    • 烧录一个简单的测试程序(如点亮 LED),验证复位、时钟和 GPIO 功能。
  4. 外设功能测试:

    • UART 测试: 烧录一个简单的 UART 发送程序,通过串口助手观察是否能接收到正确的数据。
    • ADC 测试: 将一个已知的稳定电压(如通过电位器分压得到的 1.0V)接入其中一个 ADC 通道。烧录测试程序,读取该通道的 ADC 值,并计算出电压值,与实际测量值进行比较,验证 ADC 采集的准确性。

5.2 软件调试

  1. 单步调试 (Step-by-Step Debugging):

    • 使用 IDE 的调试功能,设置断点,单步执行代码。
    • 观察关键变量(如adc_raw_data数组)的值是否符合预期。
    • 检查函数调用是否正确,寄存器配置是否符合预期。
  2. printf 调试 (printf Debugging):

    • 在代码的关键位置使用printfHAL_UART_Transmit输出调试信息,如变量值、函数执行状态等。
    • 通过串口助手观察输出,判断程序的执行流程和数据处理是否正确。
  3. 逻辑分析仪 / 示波器辅助调试:

    • ADC 时钟: 使用示波器测量 ADC 时钟相关引脚,确认时钟频率是否正确。
    • 转换时序: 在 ADC 转换开始和结束时翻转一个 GPIO 引脚,用示波器观察转换周期,估算采样率。
    • UART 信号: 用示波器或逻辑分析仪观察 UART 的 TX 引脚,验证波特率、数据位、停止位是否正确。

5.3 功能测试

  1. 14 通道采集功能验证:

    • 将 14 路 ADC 输入通道分别连接到不同的电压源(可以是多个电位器)。
    • 运行完整的 ADC 采集程序。
    • 在串口助手上观察输出,确认 14 个通道都能正确采集到数据,并且数据能正确对应到各个通道。
  2. 数据准确性验证:

    • 对每个通道,输入一个精确已知的电压(如使用高精度电源供应器提供 0.5V, 1.0V, 2.0V, 3.0V)。
    • 记录串口输出的电压值。
    • 计算绝对误差 (| 测量值 - 真实值 |) 和相对误差 (绝对误差 / 真实值 * 100%)。
    • 分析误差来源:可能包括 ADC 本身的误差、电阻精度、电源噪声、PCB 布局等。
  3. 滤波效果验证:

    • 在一个通道上输入一个带有噪声的信号(例如,通过一个简单的 RC 网络将方波信号滤波成带有纹波的信号)。
    • 分别运行开启滤波和关闭滤波的程序。
    • 观察串口输出数据的波动情况,验证滤波算法是否有效。

5.4 性能测试与分析

  1. 采样率测试:

    • 方法一 (理论计算): 根据 ADC 时钟频率、采样时间和通道数计算。
      • 单个通道转换时间 (T_conv) = 采样时间 + 12.5 个 ADCCLK 周期。
      • 总转换时间 (T_total) = T_conv * 通道数。
      • 采样率 (Fs) = 1 / T_total。
      • 示例: ADCCLK=9MHz, 采样时间 = 11.5 周期,14 个通道。
        • T_conv = (11.5 + 12.5) * (1/9MHz) = 24 / 9e6 ≈ 2.6667 μs。
        • T_total = 2.6667 μs * 14 ≈ 37.333 μs。
        • Fs ≈ 1 / 37.333e-6 ≈ 26.79 kHz (这是理论上的最高采样率,每个通道的采样率约为 26.79kHz / 14 ≈ 1.91kHz)。
    • 方法二 (实测): 在代码中加入计时逻辑。例如,在HAL_ADC_ConvCpltCallback中翻转一个 GPIO 引脚,用示波器测量该引脚电平翻转的频率,即为总采样率。
  2. 系统稳定性测试:

    • 让系统长时间(如几小时)连续运行。
    • 观察串口输出数据是否持续稳定,有无数据丢失、错乱或程序崩溃的现象。
    • 可以将数据保存到文件中,后期进行离线分析。

6. 常见问题与注意事项

6.1 硬件设计常见陷阱

  1. 电源和地处理不当:

    • 症状: ADC 采集数据波动大、不准确,系统工作不稳定。
    • 原因: 数字电路的开关噪声通过电源和地线耦合到模拟电路。
    • 解决方案: 严格遵守模拟地和数字地单点连接的原则;在所有电源引脚旁放置去耦电容;对模拟电源和数字电源进行适当隔离。
  2. ADC 输入引脚悬空或未正确配置:

    • 症状: 未使用的 ADC 通道采集到随机波动的数据。
    • 原因: 悬空的引脚会拾取环境噪声。
    • 解决方案: 对于不使用的 ADC 通道,要么将其配置为 GPIO 输出并拉低 / 拉高,要么在硬件上将其连接到 GND 或 VDD。
  3. ADC 参考电压不稳定:

    • 症状: 所有通道的采集数据都存在固定的比例误差。
    • 原因: VREF + 引脚电压不稳定或不准确。
    • 解决方案: 确保 VREF + 有良好的去耦电容;如果对精度要求极高,可使用外部高精度基准电压源。
  4. 信号源阻抗过高:

    • 症状: ADC 采样值不准确,特别是在快速采样时。
    • 原因: ADC 输入引脚有寄生电容,高阻抗信号源无法在采样时间内为其充放电到正确电压。
    • 解决方案: 尽量降低信号源的输出阻抗;在 ADC 输入引脚前使用缓冲放大器;适当增加采样时间。

6.2 软件配置常见错误

  1. ADC 时钟配置错误:

    • 症状: ADC 转换结果完全错误或没有结果。
    • 原因: ADCCLK 频率超过 14MHz。
    • 解决方案: 在SystemClock_Config中正确设置 ADC 预分频器。
  2. DMA 配置与缓冲区不匹配:

    • 症状: 数据传输错误,程序崩溃。
    • 原因: DMA 的数据宽度(Word/HalfWord/Byte)与缓冲区变量类型不匹配。
    • 解决方案: 确保 CubeMX 中的PeriphDataAlignmentMemDataAlignmentmain.c中定义的数组类型一致。
  3. 忘记开启时钟:

    • 症状: 外设无法工作。
    • 原因: 在使用任何外设之前,必须先为其使能相应的时钟。
    • 解决方案: 在stm32f1xx_hal_msp.c中,HAL_XXX_MspInit函数会自动使能时钟。如果手动配置,需调用__HAL_RCC_XXX_CLK_ENABLE()宏。
  4. 数据对齐错误:

    • 症状: 读取到的 ADC 值总是看起来 “偏大” 或 “偏小”,且不符合预期。
    • 原因: ADC 数据对齐方式(左 / 右)与软件处理方式不一致。
    • 解决方案: 在 CubeMX 中明确配置对齐方式,并在软件中正确处理。推荐使用右对齐。

6.3 电磁兼容性 (EMC) 设计

  1. 辐射发射 (Radiated Emission):

    • 原因: 高速数字信号(如晶振、时钟)的边沿陡峭,会产生较强的电磁辐射。
    • 对策:
      • 晶振和其匹配电容尽量靠近 MCU。
      • 对时钟线进行阻抗匹配和屏蔽。
      • 使用多层 PCB,利用地平面作为屏蔽。
      • 优化 PCB 布局,减少环路面积。
  2. 传导发射 (Conducted Emission):

    • 原因: 噪声通过电源线传导出去。
    • 对策:
      • 在电源入口处使用 EMI 滤波器。
      • 加强电源滤波和去耦设计。
  3. 静电放电 (ESD):

    • 原因: 人体或设备上的静电可能通过接口或按键释放到 MCU,造成损坏。
    • 对策:
      • 在所有外部接口(如 UART、按键)上使用 ESD 保护器件(如 TVS 管)。
      • 确保 PCB 有良好的接地。

6.4 电源完整性 (PI) 与信号完整性 (SI)

  1. 电源完整性 (Power Integrity):

    • 目标: 确保在所有情况下,提供给芯片的电源电压都在其规格范围内,并且噪声足够小。
    • 关键因素:
      • 去耦电容: 选择合适的电容值和封装,放置在正确的位置。
      • 电源平面: 使用足够宽的电源线或电源平面,降低阻抗。
      • 压降: 考虑大电流下电源线上的压降。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值