目录
- 引言
- STM32F103RCT6 ADC 资源深度解析
-
14 路 ADC 采集器硬件系统设计
- 软件系统设计与实现
- 系统测试与验证
- 常见问题与注意事项
- 总结与展望
- 参考文献
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 引脚 | 用途 |
|---|---|---|---|
| CH1 | ADC_Channel_0 | PA0 | 外部模拟输入 1 |
| CH2 | ADC_Channel_1 | PA1 | 外部模拟输入 2 |
| CH3 | ADC_Channel_2 | PA2 | 外部模拟输入 3 |
| CH4 | ADC_Channel_3 | PA3 | 外部模拟输入 4 |
| CH5 | ADC_Channel_4 | PA4 | 外部模拟输入 5 |
| CH6 | ADC_Channel_5 | PA5 | 外部模拟输入 6 |
| CH7 | ADC_Channel_6 | PA6 | 外部模拟输入 7 |
| CH8 | ADC_Channel_7 | PA7 | 外部模拟输入 8 |
| CH9 | ADC_Channel_8 | PB0 | 外部模拟输入 9 |
| CH10 | ADC_Channel_9 | PB1 | 外部模拟输入 10 |
| CH11 | ADC_Channel_10 | PC0 | 外部模拟输入 11 |
| CH12 | ADC_Channel_11 | PC1 | 外部模拟输入 12 |
| CH13 | ADC_Channel_12 | PC2 | 外部模拟输入 13 |
| CH14 | ADC_Channel_13 | PC3 | 外部模拟输入 14 |
选择理由:
- 这些引脚分布在 PA、PB、PC 三个端口,便于在 PCB 上进行布局布线。
- 避开了一些常用的调试和通信引脚(如 PA9/TX, PA10/RX, PA13/SWDIO, PA14/SWCLK)。
- 保留了 PC4, PC5 作为备用通道或用于其他功能。
3. 14 路 ADC 采集器硬件系统设计
硬件设计是整个项目的基石,其合理性直接决定了系统的性能和稳定性,尤其是对于对噪声敏感的模拟电路部分。
3.1 总体硬件架构
整个硬件系统可以分为以下几个功能模块:
- 电源模块:为整个系统提供稳定、干净的 3.3V 和 5V 电源。
- STM32F103RCT6 最小系统:包括 MCU、晶振、复位、调试接口。
- 14 路 ADC 输入调理与保护电路:对外部输入的模拟信号进行滤波、限幅等处理。
- 通信接口模块:主要是 UART 串口,用于与 PC 机或其他设备通信。
- 用户接口 :如 LED 指示灯、按键。
3.2 电源系统设计
一个稳定可靠的电源系统对于 ADC 采集的精度至关重要。
3.2.1 电源架构
- 输入: 建议使用 5V 直流电源输入(如 USB 5V 或外部 5V 适配器)。
- 转换: 使用一个低压差线性稳压器(LDO)将 5V 转换为 3.3V,为 STM32 和其他 3.3V 外设供电。
3.2.2 LDO 选型
| 特性 | 推荐型号举例 | 说明 |
|---|---|---|
| 输出电压 | 3.3V | 固定输出。 |
| 输出电流 | ≥100mA | STM32F103RCT6 的典型工作电流约为 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))
- 设计要点:
- 确保 R1 + R2 的值足够大,以减少对信号源的负载效应(通常为几十 kΩ 到几百 kΩ)。
- 电阻的精度要高,推荐使用 1% 或 0.1% 精度的金属膜电阻。
- 在 ADC 输入引脚前仍需加入 RC 低通滤波器。
- 软件中需要根据分压比进行反推计算。
-
测量双极性电压 (如 - 5V 到 + 5V): 使用电平抬升电路。
- 原理:将双极性信号叠加一个固定的偏移电压,使其整体上移到 0-3.3V 范围内。
- 常用电路:使用一个运算放大器构成的加法器电路,将输入信号与一个由 3.3V 电源分压得到的参考电压(如 1.65V)相加。
- 设计要点:对运放的选型(如输入偏置电流、失调电压)有一定要求,电路相对复杂。
3.4 MCU 最小系统设计
STM32F103RCT6 的最小系统包括以下几个部分:
-
电源引脚 (VDD, VSS, VDDA, VSSA):
- VDD 和 VSS 是数字电源和地。
- VDDA 和 VSSA 是模拟电源和地,必须连接到干净的 3.3V 和 GND。
- 关键原则: 为了减少数字电路对模拟电路的干扰,VDDA 应通过一个铁氧体磁珠或一个小电阻(如 0Ω 电阻,用于测试时断开)连接到 VDD,VSSA 直接连接到 VSS。在 PCB 上,模拟地和数字地应单点连接。
-
晶振电路 (HSE):
- 推荐使用 8MHz 的外部高速晶振(HSE)。
- 匹配电容:在晶振的两个引脚和地之间各接一个电容,通常为 18pF ~ 22pF,具体值需参考晶振 datasheet。
- 芯片:选择无源晶振。
-
复位电路 (NRST):
- 推荐使用外部复位电路,以提高复位的可靠性。
- 一个简单的上电复位电路由一个电阻和一个电容组成。
- 也可以使用专用的复位芯片。
- 典型值: R = 10kΩ (上拉到 VDD),C = 1μF (接地)。
-
调试接口 (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)
-
分区布局:
- 将电路板清晰地划分为模拟区和数字区。
- 模拟区包括:ADC 输入连接器、信号调理电路、VDDA、VSSA。
- 数字区包括:MCU 数字部分、晶振、UART 接口、电源转换电路。
- 将 LDO 放置在靠近模拟区和数字区的边界,以便其输出能方便地为两个区域供电。
-
关键元件放置:
- 去耦电容: 尽可能靠近 STM32 的电源引脚。
- 晶振: 尽可能靠近 MCU 的 OSC_IN 和 OSC_OUT 引脚,减少引线长度。晶振和匹配电容应放在同一侧。
- LDO: 输入输出电容应尽可能靠近 LDO 本身。
- ADC 输入连接器: 尽量靠近 ADC 输入调理电路,缩短模拟信号在 PCB 上的路径。
3.6.2 布线 (Routing)
-
地线处理 (最关键):
- 使用星形接地或单点接地: 所有模拟地(AGND)和数字地(DGND)最终在一个点连接到电源地。这个点通常选择在电源入口处或 MCU 的 GND 引脚附近。
- 加粗地线: 主地线应尽可能粗,以降低地线阻抗,提供稳定的参考电位。
- 模拟地平面: 如果可能,为模拟区创建一个独立的地平面,然后通过一个小的 “桥” 或一个 0Ω 电阻与数字地平面连接。这能有效隔离数字噪声。
-
电源线:
- 主电源线也应适当加粗。
- 在模拟电源(VDDA)和数字电源(VDD)之间使用铁氧体磁珠或小电阻进行隔离。
-
模拟信号线:
- 短而直: ADC 输入信号线应尽可能短,避免长距离平行布线。
- 远离数字信号线: 特别是高速切换的时钟线(如晶振、SPI、I2C 时钟)和 PWM 输出。
- 避免直角: 布线时使用 45 度角或圆弧,避免 90 度直角,以减少信号反射和 EMI。
- 差分走线 (如适用): 如果使用差分输入,确保两根信号线长度一致、紧密耦合。
-
数字信号线:
- 时钟线和高速信号线应进行阻抗匹配,并远离模拟区域。
3.7 硬件清单 (BOM)
| 序号 | 元件名称 | 规格 / 型号 | 数量 | 备注 |
|---|---|---|---|---|
| 1 | 微控制器 | STM32F103RCT6 | 1 | LQFP-64 封装 |
| 2 | LDO 稳压器 | AMS1117-3.3 | 1 | 或其他等效型号 |
| 3 | 外部晶振 | 8MHz | 1 | 无源晶振 |
| 4 | 电容 | 10μF/16V (电解) | 2 | 电源滤波 |
| 5 | 电容 | 0.1μF (100nF) | 若干 | 去耦、滤波 |
| 6 | 电容 | 18pF ~ 22pF | 2 | 晶振匹配电容 |
| 7 | 电容 | 1μF | 1 | 复位电路 |
| 8 | 电阻 | 1kΩ | 14+ | ADC 输入限流 / 滤波 |
| 9 | 电阻 | 10kΩ | 若干 | 上拉 / 下拉,复位电路 |
| 10 | 电阻 | 100nF | 2 | ADC 输入滤波 |
| 11 | 电阻 | 100Ω | 2 | UART 接口限流 (可选) |
| 12 | LED | 任意颜色 | 1 | 电源指示 (可选) |
| 13 | 按键 | 轻触开关 | 1 | 复位按键 (可选) |
| 14 | 接插件 | 排针 / 排母 | 若干 | 电源、UART、ADC 输入 |
| 15 | 印刷电路板 | PCB | 1 | 自定义设计 |
4. 软件系统设计与实现
本章节将详细介绍基于 STM32CubeMX 和 HAL 库的软件实现。HAL (Hardware Abstraction Layer) 库是 ST 官方推荐的、跨 STM32 系列的抽象层库,它提供了一套统一的 API,简化了外设的配置和使用。
4.1 开发环境与工具链
- STM32CubeMX:图形化配置工具,用于生成初始化代码。下载地址
- Keil MDK-ARM 或 STM32CubeIDE:集成开发环境 (IDE),用于编写、编译和调试代码。
- Keil MDK-ARM:功能强大,使用广泛。
- STM32CubeIDE:免费,基于 Eclipse,与 STM32CubeMX 深度集成。
- ST-Link/V2 或 J-Link:调试器 / 编程器,用于将程序下载到 MCU 并进行在线调试。
- 串口助手 (如 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 进行配置
- 创建新项目:选择 STM32F103RCT6 芯片。
- 配置时钟:
- 在
Pinout & Configuration->RCC中,将HSE设置为Crystal/Ceramic Resonator。 - 切换到
Clock Configuration标签页,配置系统时钟为 72MHz。确保APB2 Prescaler设置为/1(PCLK2 = 72MHz)。
- 在
- 配置 ADC1:
- 在
Pinout & Configuration->ADC1中,勾选需要使用的 14 个通道 (IN0 to IN13)。STM32CubeMX 会自动将对应的 GPIO 引脚配置为Analog模式。 - 在
Parameter Settings中:- INx Configuration: 保持默认。
- ADC_Settings:
- Mode:
Independent ADC mode - Data Alignment:
Right alignment(推荐) - Scan Conversion Mode:
Enabled(关键!) - Continuous Conversion Mode:
Enabled(关键!) - Discontinuous Conversion Mode:
Disabled - Number Of Conversion:
14(关键!) - External Trigger Conversion Source:
None(我们将使用软件触发启动第一次转换) - Rank: 在这里可以看到 14 个通道被自动添加。可以调整它们的转换顺序。
- Sampling Time: 为每个通道选择采样时间。采样时间越长,对噪声的抑制越好,但采样率越低。推荐选择
1.5 Cycles到239.5 Cycles之间的值。例如,选择11.5 Cycles。
- Mode:
- ADC_ClockPrescaler:
PCLK2 divided by 8(因为 PCLK2=72MHz, 72/8=9MHz ≤ 14MHz)。
- 在
- 配置 DMA:
- 在
Pinout & Configuration->DMA中,点击Add按钮添加一个 DMA 请求。 - Channel: 选择
DMA1_Channel1(ADC1 的 DMA 请求映射到这个通道)。 - Direction:
Peripheral To Memory。 - Peripheral address: 选择
ADC1_DR(ADC1 的数据寄存器地址)。 - Memory address: 这里暂时不填,我们将在代码中指定一个数组作为目标缓冲区。
- Data Width: 两边都选择
HalfWord (16-bit),因为 ADC 转换结果是 12 位,存储在 16 位的变量中。 - Mode:
Circular(关键!循环模式,当缓冲区满后,会自动从头开始覆盖,适合连续采集)。 - Increment Memory Address:
Enabled(关键!内存地址自动递增)。 - Increment Peripheral Address:
Disabled(外设地址固定)。
- 在
- 配置 UART1:
- 在
Pinout & Configuration->USART1中,将Mode设置为Asynchronous(异步模式)。 - 在
Configuration->Parameter Settings中,设置Baud Rate为115200,其他参数默认 (8N1)。
- 在
- 生成代码:
- 点击右上角
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
运行
注意:CubeMX 生成的代码中,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 */ } }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 的中断机制仍然很重要。
-
ADC 转换结束中断 (EOC):
- 当一个规则通道的转换完成后,会触发 EOC 中断。
- 在扫描模式下,如果
CONTINUOUS Conversion Mode为DISABLE,则在所有通道转换完毕后触发一次 EOC 中断。如果为ENABLE,则每个通道转换完毕后都会触发一次 EOC 中断。 - 在 HAL 库中,中断服务程序
ADC1_2_IRQHandler会被调用,它内部会调用HAL_ADC_IRQHandler(&hadc1)。 HAL_ADC_IRQHandler会检查中断标志位,并调用相应的回调函数。对于 EOC 中断,回调函数是HAL_ADC_ConvCpltCallback(&hadc1)。- 用户可以在
stm32f1xx_it.c或main.c中重写HAL_ADC_ConvCpltCallback函数,在每次转换完成后执行特定操作。
-
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()功能强大,但需要注意:
- 缓冲区溢出:确保
uart_tx_buffer的大小足够容纳格式化后的字符串,否则会导致内存 corruption。 - 浮点数支持:使用
%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 主函数与程序流程
整个程序的执行流程如下:
SystemInit(): 系统初始化,设置时钟等(由启动文件调用)。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 硬件调试
-
视觉检查 (Visual Inspection):
- 检查 PCB 上是否有明显的短路、断路、虚焊、漏焊。
- 确认所有元件的型号、封装和极性是否正确。
- 检查电源、地线等关键网络的走线是否符合设计要求。
-
电源测试:
- 在不通电的情况下,使用万用表测量 VDD/GND、VDDA/VSSA 之间是否有短路。
- 接入电源,测量 LDO 输入端和输出端的电压是否正常(分别为 5V 和 3.3V 左右)。
- 检查电源指示灯(如果设计了)是否正常点亮。
-
MCU 最小系统测试:
- 连接 ST-Link 调试器。
- 尝试通过 IDE(如 Keil)连接目标 MCU。如果能成功连接并读取芯片 ID,说明 MCU 基本工作正常。
- 烧录一个简单的测试程序(如点亮 LED),验证复位、时钟和 GPIO 功能。
-
外设功能测试:
- UART 测试: 烧录一个简单的 UART 发送程序,通过串口助手观察是否能接收到正确的数据。
- ADC 测试: 将一个已知的稳定电压(如通过电位器分压得到的 1.0V)接入其中一个 ADC 通道。烧录测试程序,读取该通道的 ADC 值,并计算出电压值,与实际测量值进行比较,验证 ADC 采集的准确性。
5.2 软件调试
-
单步调试 (Step-by-Step Debugging):
- 使用 IDE 的调试功能,设置断点,单步执行代码。
- 观察关键变量(如
adc_raw_data数组)的值是否符合预期。 - 检查函数调用是否正确,寄存器配置是否符合预期。
-
printf 调试 (printf Debugging):
- 在代码的关键位置使用
printf或HAL_UART_Transmit输出调试信息,如变量值、函数执行状态等。 - 通过串口助手观察输出,判断程序的执行流程和数据处理是否正确。
- 在代码的关键位置使用
-
逻辑分析仪 / 示波器辅助调试:
- ADC 时钟: 使用示波器测量 ADC 时钟相关引脚,确认时钟频率是否正确。
- 转换时序: 在 ADC 转换开始和结束时翻转一个 GPIO 引脚,用示波器观察转换周期,估算采样率。
- UART 信号: 用示波器或逻辑分析仪观察 UART 的 TX 引脚,验证波特率、数据位、停止位是否正确。
5.3 功能测试
-
14 通道采集功能验证:
- 将 14 路 ADC 输入通道分别连接到不同的电压源(可以是多个电位器)。
- 运行完整的 ADC 采集程序。
- 在串口助手上观察输出,确认 14 个通道都能正确采集到数据,并且数据能正确对应到各个通道。
-
数据准确性验证:
- 对每个通道,输入一个精确已知的电压(如使用高精度电源供应器提供 0.5V, 1.0V, 2.0V, 3.0V)。
- 记录串口输出的电压值。
- 计算绝对误差 (| 测量值 - 真实值 |) 和相对误差 (绝对误差 / 真实值 * 100%)。
- 分析误差来源:可能包括 ADC 本身的误差、电阻精度、电源噪声、PCB 布局等。
-
滤波效果验证:
- 在一个通道上输入一个带有噪声的信号(例如,通过一个简单的 RC 网络将方波信号滤波成带有纹波的信号)。
- 分别运行开启滤波和关闭滤波的程序。
- 观察串口输出数据的波动情况,验证滤波算法是否有效。
5.4 性能测试与分析
-
采样率测试:
- 方法一 (理论计算): 根据 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 引脚,用示波器测量该引脚电平翻转的频率,即为总采样率。
- 方法一 (理论计算): 根据 ADC 时钟频率、采样时间和通道数计算。
-
系统稳定性测试:
- 让系统长时间(如几小时)连续运行。
- 观察串口输出数据是否持续稳定,有无数据丢失、错乱或程序崩溃的现象。
- 可以将数据保存到文件中,后期进行离线分析。
6. 常见问题与注意事项
6.1 硬件设计常见陷阱
-
电源和地处理不当:
- 症状: ADC 采集数据波动大、不准确,系统工作不稳定。
- 原因: 数字电路的开关噪声通过电源和地线耦合到模拟电路。
- 解决方案: 严格遵守模拟地和数字地单点连接的原则;在所有电源引脚旁放置去耦电容;对模拟电源和数字电源进行适当隔离。
-
ADC 输入引脚悬空或未正确配置:
- 症状: 未使用的 ADC 通道采集到随机波动的数据。
- 原因: 悬空的引脚会拾取环境噪声。
- 解决方案: 对于不使用的 ADC 通道,要么将其配置为 GPIO 输出并拉低 / 拉高,要么在硬件上将其连接到 GND 或 VDD。
-
ADC 参考电压不稳定:
- 症状: 所有通道的采集数据都存在固定的比例误差。
- 原因: VREF + 引脚电压不稳定或不准确。
- 解决方案: 确保 VREF + 有良好的去耦电容;如果对精度要求极高,可使用外部高精度基准电压源。
-
信号源阻抗过高:
- 症状: ADC 采样值不准确,特别是在快速采样时。
- 原因: ADC 输入引脚有寄生电容,高阻抗信号源无法在采样时间内为其充放电到正确电压。
- 解决方案: 尽量降低信号源的输出阻抗;在 ADC 输入引脚前使用缓冲放大器;适当增加采样时间。
6.2 软件配置常见错误
-
ADC 时钟配置错误:
- 症状: ADC 转换结果完全错误或没有结果。
- 原因: ADCCLK 频率超过 14MHz。
- 解决方案: 在
SystemClock_Config中正确设置 ADC 预分频器。
-
DMA 配置与缓冲区不匹配:
- 症状: 数据传输错误,程序崩溃。
- 原因: DMA 的数据宽度(Word/HalfWord/Byte)与缓冲区变量类型不匹配。
- 解决方案: 确保 CubeMX 中的
PeriphDataAlignment和MemDataAlignment与main.c中定义的数组类型一致。
-
忘记开启时钟:
- 症状: 外设无法工作。
- 原因: 在使用任何外设之前,必须先为其使能相应的时钟。
- 解决方案: 在
stm32f1xx_hal_msp.c中,HAL_XXX_MspInit函数会自动使能时钟。如果手动配置,需调用__HAL_RCC_XXX_CLK_ENABLE()宏。
-
数据对齐错误:
- 症状: 读取到的 ADC 值总是看起来 “偏大” 或 “偏小”,且不符合预期。
- 原因: ADC 数据对齐方式(左 / 右)与软件处理方式不一致。
- 解决方案: 在 CubeMX 中明确配置对齐方式,并在软件中正确处理。推荐使用右对齐。
6.3 电磁兼容性 (EMC) 设计
-
辐射发射 (Radiated Emission):
- 原因: 高速数字信号(如晶振、时钟)的边沿陡峭,会产生较强的电磁辐射。
- 对策:
- 晶振和其匹配电容尽量靠近 MCU。
- 对时钟线进行阻抗匹配和屏蔽。
- 使用多层 PCB,利用地平面作为屏蔽。
- 优化 PCB 布局,减少环路面积。
-
传导发射 (Conducted Emission):
- 原因: 噪声通过电源线传导出去。
- 对策:
- 在电源入口处使用 EMI 滤波器。
- 加强电源滤波和去耦设计。
-
静电放电 (ESD):
- 原因: 人体或设备上的静电可能通过接口或按键释放到 MCU,造成损坏。
- 对策:
- 在所有外部接口(如 UART、按键)上使用 ESD 保护器件(如 TVS 管)。
- 确保 PCB 有良好的接地。
6.4 电源完整性 (PI) 与信号完整性 (SI)
-
电源完整性 (Power Integrity):
- 目标: 确保在所有情况下,提供给芯片的电源电压都在其规格范围内,并且噪声足够小。
- 关键因素:
- 去耦电容: 选择合适的电容值和封装,放置在正确的位置。
- 电源平面: 使用足够宽的电源线或电源平面,降低阻抗。
- 压降: 考虑大电流下电源线上的压降。

9532

被折叠的 条评论
为什么被折叠?



