简介:本资源是一套基于STM32CubeMX与HAL库开发的STM32F103C8T6单片机ADC采样完整工程例程,面向嵌入式初学者及硬件开发者,解决模拟信号采集与数字转换这一核心外设应用难题。工程覆盖ADC初始化配置、多通道采样、12位分辨率设置、连续/单次转换模式、中断与DMA数据传输等关键实践环节,适配典型传感器信号采集、电压监测等实际场景。压缩包含608个文件(17.92MB),主体为340个C源文件与109个头文件(实现HAL驱动与主逻辑),辅以43个汇编启动文件、28个IAR链接脚本(.icf)及Keil工程文件(.uvprojx/.ioc),并集成CMSIS-DSP数学库(如arm_rfft_init_f32.c等),支持后续扩展滤波与信号处理。已有5104人学习下载,提供开箱即用的CubeMX生成代码、清晰的目录结构划分及可直接烧录调试的编译产物(axf/hex),显著降低ADC入门门槛与排错成本。
1. 项目概述与核心价值
最近在调试一个基于STM32F103C8T6的环境传感器项目,核心需求是要采集多路模拟信号,比如温湿度传感器输出的电压、光照强度等等。这类需求在嵌入式开发里太常见了,而ADC(模数转换器)就是实现这个功能的“桥梁”。我选择了STM32CubeMX这个官方图形化配置工具来搭建工程骨架,因为它能极大简化外设初始化的繁琐过程,特别是对于ADC这种配置项比较多的模块。很多新手,甚至一些有经验的开发者,在面对STM32的ADC时,常常会卡在通道配置、采样时间计算或者DMA传输这些环节上。这个例程的目的,就是通过一个完整的、可复现的ADC单通道采样实例,把从CubeMX配置到代码编写的全链路讲清楚,让你不仅能“配出来”,更能理解每一步背后的原理,以后遇到多通道、DMA、定时器触发等更复杂的场景也能举一反三。
2. 硬件平台与开发环境搭建
2.1 STM32F103C8T6核心板选型与引脚规划
STM32F103C8T6,也就是我们常说的“蓝莓板”或最小系统板的核心芯片,是一款性价比极高的Cortex-M3内核MCU。它内置了3个12位的ADC,其中ADC1和ADC2有16个外部通道,ADC3少一些。对于我们的例程,我们使用ADC1。这里有个关键点:不是所有GPIO口都支持ADC功能。以最常用的64引脚封装为例,ADC1的通道0到通道15对应着特定的GPIO口,比如通道0是PA0,通道1是PA1,以此类推。在画原理图或者连接传感器时,必须把模拟信号线接到这些指定的ADC引脚上,否则无法采样。
我手头用的是一块常见的STM32F103C8T6最小系统板,上面已经引出了所有的GPIO。为了演示,我选择将ADC1的通道1(PA1)作为我们的模拟信号输入引脚。你可以用一个电位器的中间抽头接到PA1,两端分别接3.3V和GND,这样通过旋转电位器就能产生一个0-3.3V可调的电压,方便我们测试。
2.2 STM32CubeMX与IDE安装要点
STM32CubeMX是ST官方推出的免费配置工具,它通过图形化界面生成初始化代码,支持多种IDE。我的开发环境是:
- STM32CubeMX:直接从ST官网下载安装。安装时注意,它会提示安装对应的HAL库和芯片包,务必确保安装了
STM32CubeF1的固件包(即F1系列的HAL库),版本选择较新的即可,比如V1.8.x。 - IDE:我选用的是Keil MDK(ARMCC编译器),因为它在国内应用广泛,调试方便。你也可以选择STM32CubeIDE(ST自家出品,基于Eclipse)或者IAR。CubeMX可以生成针对这些IDE的工程文件。
安装后第一次打开CubeMX,它会自动在线更新数据库和芯片包,可能需要一点时间。这里有个小技巧:如果网络环境不好导致包管理失败,可以手动从官网下载.pack文件(针对Keil)或.zip固件包,然后通过CubeMX的“Help -> Manage embedded software packages”进行本地安装。
3. 使用STM32CubeMX配置ADC
3.1 工程创建与时钟树配置
打开CubeMX,点击“New Project”,在芯片选择器里输入“STM32F103C8T6”并选中。在图形化引脚界面,你会看到芯片的引脚图。
- 系统核心配置:首先在“Pinout & Configuration”标签页的“System Core”里,配置
RCC(复位和时钟控制)。将High Speed Clock (HSE)设置为“Crystal/Ceramic Resonator”,因为我们最小系统板外部通常有一个8MHz的晶振。 - 时钟树配置:这是关键一步,ADC的时钟来源于APB2总线。点击“Clock Configuration”标签页,CubeMX会给出一个默认的时钟树。我们的目标是让系统主频跑到72MHz,这是F103系列的常见高速配置。通常路径是:HSE(8MHz) -> PLL倍频9倍 -> 72MHz。确保APB2的预分频器是1,即APB2时钟也是72MHz。ADC的时钟最大不能超过14MHz,因此我们需要对APB2时钟进行分频。在时钟树图上,找到ADC时钟源,将其设置为“PCLK2 divided by 6”,这样ADC时钟就是72MHz / 6 = 12MHz,符合要求。清晰的时钟树是稳定工作的基础。
3.2 ADC1单通道参数化配置
在“Analog”分类下,找到ADC1并点击。
- 模式配置:在“Configuration”标签页的“Parameter Settings”里:
Scan Conversion Mode: 设置为“Disabled”。因为我们目前是单通道采样,不需要扫描多个通道。Continuous Conversion Mode: 设置为“Enabled”。这样ADC就会在启动后自动连续进行转换,而不需要每次手动触发。Discontinuous Conversion Mode: “Disabled”。DMA Continuous Requests: “Disabled”。本例暂不使用DMA。End Of Conversion Selection: 选择“EOC flag at the end of single conversion”,即每次转换完成产生标志位。
- 通道配置:在左侧“Analog”的
ADC1下,展开“IN1”对应的引脚PA1。将IN1的“Mode”设置为“Single-ended”(单端输入)。然后回到右侧“Parameter Settings”下方的“Rank”表格。点击“Add”按钮,在“Channel”列选择“Channel 1”(对应IN1)。这里需要重点配置Sampling Time(采样时间)。采样时间决定了ADC内部采样电容对输入电压充电的时间。时间太短,采样不准确;时间太长,转换速率慢。它的计算公式是:采样周期数 * (1 / ADC时钟频率)。ADC时钟我们之前配成了12MHz。对于像电位器这样输出阻抗较低的信号源,采样时间可以短一些。F103的ADC采样周期数可配置为1.5到239.5个周期。我通常选择“55.5 Cycles”,这样采样时间就是55.5 * (1/12MHz) ≈ 4.625微秒,对于大多数中等阻抗(几十千欧以内)的信号源都足够了。如果你外接的传感器输出阻抗很高,就需要增加这个周期数。
3.3 生成工程代码
配置好ADC后,我们还需要配置一个串口,用于把采样到的数值打印出来查看,这是调试的“眼睛”。
- 配置USART:在“Connectivity”下选择
USART1。模式选择“Asynchronous”(异步通信)。在引脚图上,USART1的TX(PA9)和RX(PA10)会自动配置。在参数设置里,配置波特率为115200,数据位8,停止位1,无校验。 - 工程生成:点击“Project Manager”标签页。
- 给工程起个名字,比如
ADC_SingleChannel。 - 选择工程路径(路径不要有中文和空格)。
- “Toolchain / IDE”选择你使用的IDE,我选“MDK-ARM V5”。
- 在“Code Generator”部分,我强烈建议勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”。这样每个外设(如ADC、USART)的初始化代码会单独成对的文件,代码结构非常清晰,后续修改和维护方便得多。
- 给工程起个名字,比如
- 最后点击右上角的“GENERATE CODE”,CubeMX就会生成完整的工程文件。如果提示安装固件包,确认安装即可。
4. ADC采样核心代码实现与解析
4.1 HAL库ADC驱动函数剖析
打开生成的Keil工程,在Core/Src目录下的main.c中,我们可以看到MX_ADC1_Init()函数,这就是CubeMX根据我们配置生成的ADC初始化代码。我们不需要修改它,但要理解关键部分:
hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发启动 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐 hadc1.Init.NbrOfConversion = 1; // 转换通道数为1 if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); }初始化完成后,ADC就准备好了,但还没有开始转换。HAL库提供了简洁的启动和读取函数。
4.2 主循环中的采样与数据处理逻辑
我们在main.c文件的while (1)主循环里添加采样代码。思路是:启动转换 -> 等待转换完成 -> 读取值 -> 转换为电压 -> 通过串口打印。
// 在main.c的/* USER CODE BEGIN PV */区定义变量 uint32_t adc_value = 0; float voltage = 0.0f; // 在while(1)循环的/* USER CODE BEGIN 3 */里添加 if (HAL_ADC_Start(&hadc1) == HAL_OK) { // 等待转换完成,超时时间设为10ms if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { // 读取转换结果 adc_value = HAL_ADC_GetValue(&hadc1); // 计算实际电压值: ADC值 * 参考电压 / 分辨率 // STM32F103的ADC是12位,分辨率是2^12 = 4096。 // 参考电压Vref+通常接VDDA(模拟电源,一般与VDD同为3.3V) voltage = (float)adc_value * 3.3f / 4096.0f; // 通过串口打印结果 printf("ADC Value: %lu, Voltage: %.3f V\r\n", adc_value, voltage); } // 停止ADC转换(对于连续模式,这里停止后下次循环会重新启动) HAL_ADC_Stop(&hadc1); } // 添加一个延时,避免串口打印太快 HAL_Delay(500);这里有几个关键点:
HAL_ADC_Start: 启动ADC转换。对于连续转换模式,调用一次后ADC会一直转换,但我们这里采用“启动-读取-停止”的单次模式来演示,更直观。HAL_ADC_PollForConversion: 轮询等待转换完成。第二个参数是超时时间(毫秒)。在简单的单任务系统中可以这样用,但在实时性要求高的系统里,更推荐使用中断或DMA。- 电压计算:公式
电压 = ADC值 * Vref / 4096是核心。Vref是你的模拟参考电压。在大多数最小系统板上,Vref+直接接到了3.3V电源上。如果你的项目对精度要求极高,需要考虑使用独立、稳定的基准电压源。 - 串口打印:需要实现
printf函数重定向到串口。在main.c的/* USER CODE BEGIN 0 */区域添加以下代码:
#include <stdio.h> #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }并在工程选项的“Target”标签页下,勾选“Use MicroLIB”(针对Keil),这是一个针对嵌入式系统优化的精简标准库,支持printf。
4.3 编译、下载与实测验证
代码编写完成后,点击Keil的编译按钮。确保没有错误后,连接好ST-Link或J-Link等调试器到板子的SWD接口(通常对应SWCLK和SWDIO引脚)。在Keil中设置好调试器型号,点击下载按钮将程序烧录到芯片中。
打开串口助手(如XCOM、Putty等),选择正确的串口号,波特率设置为115200。给板子上电,你应该能看到串口助手每隔500毫秒打印出一行数据:
ADC Value: 2048, Voltage: 1.650 V此时,旋转你接在PA1上的电位器,ADC值和电压值应该会随之线性变化。当电位器调到一端(接地)时,电压应接近0V,ADC值接近0;调到另一端(接3.3V)时,电压应接近3.3V,ADC值接近4095。这就证明我们的ADC单通道采样成功了。
5. 关键问题排查与精度优化技巧
5.1 常见故障现象与解决方法
在实际操作中,你可能会遇到以下问题:
- 串口无输出:
- 检查接线:确认USB转串口工具的TX接板子的PA10(RX),RX接PA9(TX),且共地。
- 检查串口助手配置:波特率是否为115200,数据位、停止位、校验位是否与代码配置一致。
- 检查代码:确认
printf重定向代码已添加,且工程中勾选了“Use MicroLIB”。 - 检查芯片启动模式:BOOT0引脚需接地(从主Flash启动),这是最常见的问题之一。
- ADC值固定不变或跳变剧烈:
- 检查硬件连接:确认模拟输入信号线确实连接到了正确的ADC引脚(如PA1),并且接触良好。用万用表测量该引脚对地电压,看是否随电位器变化。
- 检查电源质量:ADC的参考电压(VDDA)必须干净、稳定。确保板子的3.3V电源纹波小。如果使用开发板的USB供电,可以尝试外接一个更稳定的电源。
- 检查采样时间:如果信号源阻抗较高(如某些传感器输出),默认的采样时间可能不足,导致采样电容充电不完全。在CubeMX中增加
Sampling Time,比如从55.5周期提高到239.5周期再测试。 - 检查接地:模拟地(AGND)和数字地(DGND)在单板上通常是连在一起的,但要确保接地回路良好。在布线时,模拟部分应尽量远离数字噪声源(如晶振、开关电源电路)。
- 程序下载失败:
- 检查调试器连接是否可靠。
- 检查Keil中调试器型号和接口(SWD)设置是否正确。
- 尝试先按住板子复位键,点击下载,再松开复位键(强制进入下载模式)。
5.2 提升ADC采样精度的实战经验
对于要求稍高的应用,直接读取的ADC值可能会有几个LSB的波动。以下是一些提升精度的实用方法:
- 软件滤波:最简单的就是多次采样取平均。在主循环中连续采样N次(比如16次或32次),求和后取平均值。这能有效抑制随机噪声。
#define SAMPLE_TIMES 32 uint32_t sum = 0; for(int i=0; i<SAMPLE_TIMES; i++) { HAL_ADC_Start(&hadc1); if(HAL_ADC_PollForConversion(&hadc1, 1) == HAL_OK) { sum += HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); // 可以加一个极短的延时,避免连续采样间隔过短 HAL_Delay(1); } adc_value = sum / SAMPLE_TIMES; - 硬件去耦:在ADC输入引脚与地之间,并联一个0.1uF的陶瓷电容,位置尽量靠近芯片引脚。这个电容可以滤除高频噪声,为采样保持电路提供一个低阻抗的源。
- 使用独立的基准电压源:如果板载的3.3V线性稳压器精度和温漂不够好,可以考虑使用一颗专用的基准电压芯片(如REF3033,输出3.3V)给VDDA和Vref+供电,这能显著提升绝对精度。
- 校准ADC:STM32的ADC模块出厂时带有校准数据。HAL库提供了
HAL_ADCEx_Calibration_Start()函数。在ADC初始化完成之后、启动之前调用一次校准函数,可以修正内部电容带来的误差。对于F1系列,通常只需要对单端输入进行校准。// 在MX_ADC1_Init()函数调用之后,主循环之前调用 if (HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) { Error_Handler(); } - 注意PCB布局:对于你自己的PCB设计,模拟信号走线要尽量短,远离高速数字线(如时钟线、数据总线)。如果空间允许,可以用地线包围模拟走线进行屏蔽。
6. 从单通道到多通道与DMA进阶
掌握了单通道采样,很多应用场景需要采集多路信号。最直接的方法是像单通道一样,在CubeMX的ADC配置里,在“Rank”表格中添加多个通道(如Channel 1, Channel 2),并设置好各自的采样时间和顺序。然后将Scan Conversion Mode设置为“Enabled”。在代码中,你需要将hadc1.Init.NbrOfConversion改为通道数量。读取时,HAL库提供了HAL_ADC_GetValue,但在扫描模式下,它返回的是最后一个转换完成的通道数据。要获取所有通道数据,必须使用DMA(直接存储器访问)或中断。
使用DMA进行多通道自动传输是更高效、更常用的方法。DMA可以在ADC转换完成后,自动将数据搬运到你指定的内存数组中,完全不需要CPU干预。配置步骤如下:
- CubeMX配置:在ADC1的配置中,开启
Scan Conversion Mode和Continuous Conversion Mode。在DMA Settings标签页点击“Add”,选择ADC1,模式选择“Circular”(循环模式,这样DMA会持续不断地搬运数据)。在参数设置中,将Data Width设置为“Word”(因为ADC数据寄存器是32位的,但实际数据是右对齐的16位有效数据)。 - 代码修改:定义一个数组来存储数据,例如
uint16_t adc_buffer[2];用于存储两个通道的数据。在main函数初始化部分,启动DMA传输:HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 2);。这个函数会启动ADC并关联DMA,ADC会按照Rank顺序连续转换,每转换完一个通道,DMA就自动把数据搬到adc_buffer里。之后,你只需要直接访问adc_buffer[0]和adc_buffer[1],就能得到两个通道最新的ADC值,主循环完全被解放出来做其他事情。
从单通道轮询到多通道DMA,是STM32 ADC应用的一个关键进阶。它能大幅提高系统效率,也是实现高速、实时数据采集的基础。理解了这个流程,你再去看定时器触发ADC、ADC注入通道等高级功能,思路就会清晰很多。
本文还有配套的精品资源,点击获取