STM32 零基础可移植教程 14:ADC 单通道采样,不接电位器也能读电压
前面几篇我们一直在处理"数字信号"。
比如:
bash
GPIO 输出:只有高电平和低电平
按键输入:读到 0 或 1
PWM 输出:快速切换高低电平
输入捕获:测方波边沿之间的时间
这一篇开始进入另一个很常见的方向:
bash
模拟量采集
原计划这一篇用电位器:
bash
电位器 OUT 接 ADC 输入
转动旋钮,看 ADC 数值变化
这个实验很经典,也很适合入门。
但如果你手上没有电位器模块,或者暂时不想外接模块,也可以先用开发板自己的资源做一个自测。
这一篇我们改成:
bash
TIM3_CH2 输出 PWM
ADC1_IN4 读取这个 PWM
通过多次采样平均,观察不同占空比对应的平均电压
也就是:
bash
PB5 / TIM3_CH2 输出 PWM
PC4 / ADC1_IN1 读取 PWM
PB5 用一根杜邦线接到 PC4
这一篇只做一个明确目标:
bash
用 ADC 读取开发板自己输出的 PWM,打印平均 raw 和平均电压
先不做多通道扫描,不做 DMA,不做复杂滤波,也不做传感器标定。
先把 ADC 单通道采样这条链路跑通。
先说结论:直接读 PWM,不等于读稳定电压
这个点很重要。
PWM 不是一个稳定的模拟电压。
它本质上还是数字波形:
bash
一会儿高电平
一会儿低电平
所以 ADC 如果刚好在高电平那一刻采样,可能读到接近:
bash
4095
如果刚好在低电平那一刻采样,可能读到接近:
bash
0
这就会出现一个现象:
bash
单次 ADC 读取 PWM,raw 可能跳来跳去
如果想把 PWM 真正变成比较平滑的模拟电压,常见做法是加 RC 低通滤波。
但你说不想外接设备,所以这一篇不加电阻电容。
我们用一个适合入门的办法:
bash
对 PWM 连续采样很多次
把这些 raw 求平均
用平均值近似 PWM 的平均电压
比如参考电压是 3.3V:
bash
25% 占空比 -> 平均电压大约 0.825V
50% 占空比 -> 平均电压大约 1.65V
75% 占空比 -> 平均电压大约 2.475V
这不是在说 PWM 引脚真的一直稳定在 1.65V。
而是说:
bash
它一半时间是高电平,一半时间是低电平,所以平均下来像 1.65V
这个区别一定要分清楚。

本篇目标
最终现象:
串口助手里能看到类似输出:
bash
adc pwm self test start
duty=25%, avg raw=1023, avg voltage=824 mV
duty=50%, avg raw=2047, avg voltage=1649 mV
duty=75%, avg raw=3071, avg voltage=2474 mV
实际数值不一定完全一样,但趋势应该是:
bash
占空比越大
平均 raw 越大
平均 voltage 越大
本篇用到的外设:
bash
TIM PWM Output
ADC Single Conversion
USART printf
GPIO Analog
本篇跑通标准:
-
Keil 编译通过;
-
程序能下载到开发板;
-
不需要电位器模块;
-
TIM3 能输出 PWM;
-
ADC 能读取 PWM 引脚;
-
改变 PWM 占空比后,ADC 平均值会跟着变化;
-
能说清楚"PWM 瞬时电平"和"PWM 平均电压"不是一回事;
-
能说清楚换 ADC 通道、换 PWM 引脚、换参考电压时要改哪里。
准备工作
你需要准备:
|
项目
|
说明
|
| --- | --- |
|
STM32 开发板
|
任意 STM32 开发板
|
|
下载器
|
ST-LINK/V2 或板载 ST-LINK
|
|
串口工具
|
用来查看 printf() 输出
|
|
杜邦线
|
把 PWM 输出脚接到 ADC 输入脚
|
|
原理图
|
确认 PWM 引脚和 ADC 引脚
|
|
CubeMX 工程
|
可以从第 07 篇串口工程继续改
|
这里同样说明一下:
bash
不需要电位器
不需要传感器模块
不需要信号发生器
但通常需要一根杜邦线:
bash
PB5 / TIM3_CH2 -> PC4 / ADC1_IN14
大多数 STM32 不会自动把 PB5 的 PWM 在芯片内部送到 PC4 的 ADC。
所以最通用、最可移植的方式还是:
bash
用一根线把 PWM 输出脚接到 ADC 输入脚
如果你连这根线也不想接,那还有一个替代方案:
bash
读内部 VREFINT 或内部温度传感器
这个确实可以做到完全不外接。
但它有两个缺点:
-
不同 STM32 系列内部通道配置差异更大;
-
数值变化不明显,不如 PWM 占空比变化直观。
所以作为零基础教程,我更建议这一篇先用:
bash
PWM 输出接回 ADC 输入
这条线跑通后,后面再专门写内部 VREFINT、温度传感器、电池电压分压,会更顺。

ADC 到底在做什么
ADC 的全称是:
bash
Analog to Digital Converter
也就是:
bash
模拟量转数字量
假设 STM32 的 ADC 是 12 位分辨率。
12 位能表示多少个数字?
bash
2^12 = 4096
所以 ADC 转换结果范围是:
bash
0 ~ 4095
这里要先记住一句话:
bash
ADC 读出来的不是电压,而是一个数字
这个数字和输入电压之间有对应关系。
如果 ADC 参考电压是 3.3V,大概可以这样理解:
bash
输入 0V -> ADC 接近 0
输入 1.65V -> ADC 接近 2048
输入 3.3V -> ADC 接近 4095
所以我们可以用公式换算:
bash
voltage_mv = raw * vref_mv / 4095
如果:
bash
raw = 2048
vref_mv = 3300
那么:
bash
voltage_mv = 2048 * 3300 / 4095 ≈ 1650 mV

为什么 PWM 平均值和占空比有关
假设 PWM 高电平是 3.3V,低电平是 0V。
如果占空比是 25%:
bash
25% 时间是 3.3V
75% 时间是 0V
平均值大约是 0.825V
如果占空比是 50%:
bash
50% 时间是 3.3V
50% 时间是 0V
平均值大约是 1.65V
如果占空比是 75%:
bash
75% 时间是 3.3V
25% 时间是 0V
平均值大约是 2.475V
所以这篇的实验现象应该是:
bash
duty=25% -> avg voltage 接近 825 mV
duty=50% -> avg voltage 接近 1650 mV
duty=75% -> avg voltage 接近 2475 mV
注意,我们没有加 RC 滤波,所以 ADC 每次单独采样仍然可能读到 0 或 4095。
稳定的是:
bash
多次采样后的平均值

为什么不是除以 4096
12 位 ADC 一共有 4096 个码值:
bash
0, 1, 2, ... 4095
如果我们在入门教程里想让:
bash
raw = 4095
对应显示:
bash
3300 mV
那就用:
bash
raw * 3300 / 4095
这样更符合新手直觉:
bash
0 -> 0 mV
4095 -> 3300 mV
严格从量化区间讲,有些资料会用 4096 做分母。
这在理论分析里也有它的道理。
但本系列是入门工程手册,这篇先采用更直观的:
bash
APP_ADC_MAX_RAW = 4095
后面如果做高精度测量、校准、误差分析,我们再单独展开。
硬件连接
本篇示例使用:
bash
PB5 -> TIM3_CH2
PC4 -> ADC1_IN14
连接方式:
|
PWM 输出
|
ADC 输入
|
| --- | --- |
|
PB5 / TIM3_CH2
|
PC4 / ADC1_IN14
|
也就是:
bash
PB5 用杜邦线接到 PC4
注意三件事:
-
PB5 输出的是 0~3.3V PWM,不能接超过 ADC 允许范围的电压;
-
PC4 要在 CubeMX 里配置成
ADC1_IN14; -
不要把 PB5 接到 5V,也不要把 ADC 输入接到超过 VDDA 的电压。
CubeMX 配置步骤
1. 先保证 USART printf 能用
这一篇需要打印 ADC 结果,所以建议先从第 07 篇串口打印工程继续。
你至少要先确认:
bash
printf("hello\r\n");
串口助手能收到。
如果串口还没跑通,先回到第 07 篇。
ADC 采样本身不依赖串口,但没有串口你很难观察结果。

2. 配置 PWM 输出引脚
先配置开发板自己产生的测试 PWM。
本篇示例使用:
bash
PB5 -> TIM3_CH2
在 CubeMX Pinout 页面点击 PB5,选择:
bash
TIM3_CH2
然后进入:
bash
Timers -> TIM3
把 Channel1 设置为:
bash
PWM Generation CH1
TIM3 基础参数建议先这样:
|
配置项
|
推荐值
|
说明
|
| --- | --- | --- |
|
Prescaler
| 72 - 1 |
TIM3 计数频率 1 MHz,假设 TIM3 时钟 72 MHz
|
|
Counter Mode
|
Up
|
向上计数
|
|
Counter Period
| 1000 - 1 |
1000 个计数,对应 1 kHz
|
|
Clock Division
|
No Division
|
入门先不分频
|
PWM 通道参数:
|
配置项
|
推荐值
|
说明
|
| --- | --- | --- |
|
Mode
|
PWM mode 1
|
常用 PWM 模式
|
|
Pulse
|
500
|
初始 50% 占空比
|
|
CH Polarity
|
High
|
高电平有效
|
这样 PB5 会输出一个约 1000 Hz 的 PWM。
后面代码会把占空比改成 25%、50%、75%,观察 ADC 平均值变化。

3. 配置 ADC 输入引脚
本篇示例使用:
bash
PC4 -> ADC1_IN14
在 CubeMX Pinout 页面找到 PC4,点击后选择:
bash
ADC1_IN14
不同芯片、不同板子可用 ADC 通道不一样。
你要以 CubeMX 和原理图为准。

4. 配置 ADC1 参数
进入:
bash
Analog -> ADC1
先做单通道普通采样,建议这样配置:
|
配置项
|
推荐值
|
说明
|
| --- | --- | --- |
|
Scan Conversion Mode
|
Disabled
|
只采一个通道
|
|
Continuous Conversion Mode
|
Disabled
|
主循环需要时采一次
|
|
Discontinuous Conversion Mode
|
Disabled
|
入门先不用
|
|
External Trigger Conversion Source
|
Software Start
|
软件触发采样
|
|
Data Alignment
|
Right alignment
|
右对齐,常用
|
|
Number Of Conversion
|
1
|
只有一个通道
|
为什么先关闭 Continuous?
因为这篇用最直观的方式:
bash
启动一次 ADC -> 等转换完成 -> 读取结果 -> 停止 ADC
然后在应用层循环调用多次,做软件平均。
连续采样和 DMA 放到后面再讲。

5. 配置 ADC 通道 Rank
在 ADC1 的 Regular Conversion 里,添加一个通道:
bash
Rank 1: Channel 1
如果你用的是 ADC1_IN0,这里就是:
bash
Rank 1: Channel 0
采样时间 Sampling Time 建议先选稍微长一点,比如:
bash
55.5 Cycles
或者你的芯片界面里接近的中等/较长采样时间。
为什么不一上来选最短?
虽然 PWM 输出脚阻抗不高,但入门阶段先用稍长一点的采样时间,更容易得到稳定结果。

6. ADC 时钟先按 CubeMX 推荐
不同 STM32 系列 ADC 时钟配置位置不完全一样。
入门阶段先记住两点:
-
ADC 时钟不能超过芯片手册允许范围;
-
CubeMX 如果提示 ADC clock 太高,要按提示调整分频。
如果你看到 CubeMX 有红色或黄色警告,先不要硬生成。

7. 生成 Keil 工程
配置完成后点击:
bash
GENERATE CODE
打开 Keil 后先编译一次。

Keil 工程生成和编译
打开 Keil 后,先编译:
bash
Build / F7
确认输出里没有错误:
bash
0 Error(s)

如果这一步还没写自己的代码就报错,先检查 CubeMX 工程、芯片 Pack、工程路径。
完整代码
这一篇有两部分应用代码:
bash
Core/Inc/app_test_pwm.h
Core/Src/app_test_pwm.c
Core/Inc/app_adc.h
Core/Src/app_adc.c
app_test_pwm 负责输出测试 PWM。
app_adc 负责读取 ADC,并提供多次采样平均接口。
1. Core/Inc/app_test_pwm.h
bash
#ifndef APP_TEST_PWM_H
#define APP_TEST_PWM_H
#include "main.h"
#include <stdint.h>
void App_TestPWM_Init(void);
HAL_StatusTypeDef App_TestPWM_Start(void);
void App_TestPWM_Stop(void);
void App_TestPWM_SetFrequency(uint32_t frequency_hz);
void App_TestPWM_SetDutyPermille(uint16_t duty_permille);
#endif
2. Core/Src/app_test_pwm.c
bash
#include "app_test_pwm.h"
/*
* Default test PWM output is TIM3 Channel 1.
* If your board uses another PWM pin, change these macros.
*/
#ifndef APP_TEST_PWM_HANDLE
#define APP_TEST_PWM_HANDLE htim3
#endif
#ifndef APP_TEST_PWM_CHANNEL
#define APP_TEST_PWM_CHANNEL TIM_CHANNEL_1
#endif
/*
* Timer counter clock after prescaler.
* Example: TIM clock 72 MHz, Prescaler = 72 - 1, counter clock = 1 MHz.
*/
#ifndef APP_TEST_PWM_COUNTER_CLK_HZ
#define APP_TEST_PWM_COUNTER_CLK_HZ 1000000u
#endif
#ifndef APP_TEST_PWM_DEFAULT_FREQ_HZ
#define APP_TEST_PWM_DEFAULT_FREQ_HZ 1000u
#endif
#ifndef APP_TEST_PWM_DEFAULT_DUTY_PERMILLE
#define APP_TEST_PWM_DEFAULT_DUTY_PERMILLE 500u
#endif
#ifndef APP_TEST_PWM_MAX_ARR
#define APP_TEST_PWM_MAX_ARR 0xFFFFu
#endif
extern TIM_HandleTypeDef APP_TEST_PWM_HANDLE;
static uint32_t s_period_counts = 1000u;
static uint16_t s_duty_permille = APP_TEST_PWM_DEFAULT_DUTY_PERMILLE;
void App_TestPWM_Init(void)
{
App_TestPWM_SetFrequency(APP_TEST_PWM_DEFAULT_FREQ_HZ);
App_TestPWM_SetDutyPermille(APP_TEST_PWM_DEFAULT_DUTY_PERMILLE);
}
HAL_StatusTypeDef App_TestPWM_Start(void)
{
return HAL_TIM_PWM_Start(&APP_TEST_PWM_HANDLE, APP_TEST_PWM_CHANNEL);
}
void App_TestPWM_Stop(void)
{
HAL_TIM_PWM_Stop(&APP_TEST_PWM_HANDLE, APP_TEST_PWM_CHANNEL);
}
void App_TestPWM_SetFrequency(uint32_t frequency_hz)
{
uint32_t arr;
if (frequency_hz == 0u)
{
App_TestPWM_Stop();
return;
}
s_period_counts = APP_TEST_PWM_COUNTER_CLK_HZ / frequency_hz;
if (s_period_counts < 2u)
{
s_period_counts = 2u;
}
arr = s_period_counts - 1u;
if (arr > APP_TEST_PWM_MAX_ARR)
{
arr = APP_TEST_PWM_MAX_ARR;
s_period_counts = arr + 1u;
}
__HAL_TIM_SET_AUTORELOAD(&APP_TEST_PWM_HANDLE, arr);
App_TestPWM_SetDutyPermille(s_duty_permille);
__HAL_TIM_SET_COUNTER(&APP_TEST_PWM_HANDLE, 0u);
__HAL_TIM_GENERATE_EVENT(&APP_TEST_PWM_HANDLE, TIM_EVENTSOURCE_UPDATE);
}
void App_TestPWM_SetDutyPermille(uint16_t duty_permille)
{
uint32_t ccr;
if (duty_permille > 1000u)
{
duty_permille = 1000u;
}
s_duty_permille = duty_permille;
ccr = (s_period_counts * duty_permille) / 1000u;
__HAL_TIM_SET_COMPARE(&APP_TEST_PWM_HANDLE, APP_TEST_PWM_CHANNEL, ccr);
}
3. Core/Inc/app_adc.h
bash
#ifndef APP_ADC_H
#define APP_ADC_H
#include "main.h"
#include <stdint.h>
typedef struct
{
uint32_t raw;
uint32_t voltage_mv;
} App_ADC_Result;
void App_ADC_Init(void);
HAL_StatusTypeDef App_ADC_ReadRaw(uint32_t *raw);
HAL_StatusTypeDef App_ADC_ReadVoltageMv(uint32_t *voltage_mv);
HAL_StatusTypeDef App_ADC_Read(App_ADC_Result *result);
HAL_StatusTypeDef App_ADC_ReadAverageRaw(uint32_t *raw, uint16_t sample_count);
HAL_StatusTypeDef App_ADC_ReadAverage(App_ADC_Result *result, uint16_t sample_count);
#endif
4. Core/Src/app_adc.c
bash
#include "app_adc.h"
/*
* Default ADC is ADC1.
* If your project uses another ADC instance, change this macro.
*/
#ifndef APP_ADC_HANDLE
#define APP_ADC_HANDLE hadc1
#endif
/*
* Adjust this value to your board's real analog reference voltage.
* Many beginner boards use VDDA = 3.3 V.
*/
#ifndef APP_ADC_VREF_MV
#define APP_ADC_VREF_MV 3300u
#endif
/*
* 12-bit ADC range is 0..4095.
* If you use another resolution on other STM32 families, change this macro.
*/
#ifndef APP_ADC_MAX_RAW
#define APP_ADC_MAX_RAW 4095u
#endif
#ifndef APP_ADC_POLL_TIMEOUT_MS
#define APP_ADC_POLL_TIMEOUT_MS 10u
#endif
/*
* ADC calibration APIs differ slightly across STM32 families.
* Keep it disabled by default in this beginner portable example.
*/
#ifndef APP_ADC_ENABLE_CALIBRATION
#define APP_ADC_ENABLE_CALIBRATION 0u
#endif
extern ADC_HandleTypeDef APP_ADC_HANDLE;
void App_ADC_Init(void)
{
#if APP_ADC_ENABLE_CALIBRATION
(void)HAL_ADCEx_Calibration_Start(&APP_ADC_HANDLE);
#endif
}
HAL_StatusTypeDef App_ADC_ReadRaw(uint32_t *raw)
{
HAL_StatusTypeDef status;
if (raw == 0)
{
return HAL_ERROR;
}
status = HAL_ADC_Start(&APP_ADC_HANDLE);
if (status != HAL_OK)
{
return status;
}
status = HAL_ADC_PollForConversion(&APP_ADC_HANDLE, APP_ADC_POLL_TIMEOUT_MS);
if (status == HAL_OK)
{
*raw = HAL_ADC_GetValue(&APP_ADC_HANDLE);
}
(void)HAL_ADC_Stop(&APP_ADC_HANDLE);
return status;
}
HAL_StatusTypeDef App_ADC_ReadVoltageMv(uint32_t *voltage_mv)
{
uint32_t raw;
HAL_StatusTypeDef status;
if (voltage_mv == 0)
{
return HAL_ERROR;
}
status = App_ADC_ReadRaw(&raw);
if (status != HAL_OK)
{
return status;
}
*voltage_mv = (raw * APP_ADC_VREF_MV) / APP_ADC_MAX_RAW;
return HAL_OK;
}
HAL_StatusTypeDef App_ADC_Read(App_ADC_Result *result)
{
HAL_StatusTypeDef status;
if (result == 0)
{
return HAL_ERROR;
}
status = App_ADC_ReadRaw(&result->raw);
if (status != HAL_OK)
{
return status;
}
result->voltage_mv = (result->raw * APP_ADC_VREF_MV) / APP_ADC_MAX_RAW;
return HAL_OK;
}
HAL_StatusTypeDef App_ADC_ReadAverageRaw(uint32_t *raw, uint16_t sample_count)
{
uint64_t sum = 0u;
uint32_t one_raw;
uint16_t i;
HAL_StatusTypeDef status;
if ((raw == 0) || (sample_count == 0u))
{
return HAL_ERROR;
}
for (i = 0u; i < sample_count; i++)
{
status = App_ADC_ReadRaw(&one_raw);
if (status != HAL_OK)
{
return status;
}
sum += one_raw;
}
*raw = (uint32_t)(sum / sample_count);
return HAL_OK;
}
HAL_StatusTypeDef App_ADC_ReadAverage(App_ADC_Result *result, uint16_t sample_count)
{
HAL_StatusTypeDef status;
if (result == 0)
{
return HAL_ERROR;
}
status = App_ADC_ReadAverageRaw(&result->raw, sample_count);
if (status != HAL_OK)
{
return status;
}
result->voltage_mv = (result->raw * APP_ADC_VREF_MV) / APP_ADC_MAX_RAW;
return HAL_OK;
}
5. 把 .c 文件加入 Keil 工程
手动新建 .c 文件后,Keil 不一定会自动编译。
在 Keil 工程树里右键:
bash
Application/User/Core
选择:
bash
Add Existing Files to Group 'Application/User/Core'
添加:
bash
Core/Src/app_test_pwm.c
Core/Src/app_adc.c

main.c 调用方式
1. Includes 区域添加头文件
找到:
bash
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
改成:
bash
/* USER CODE BEGIN Includes */
#include "app_test_pwm.h"
#include "app_adc.h"
#include <stdio.h>
/* USER CODE END Includes */
如果你的 printf() 重定向需要包含 app_uart.h,也按第 07 篇写法加进去。
2. 初始化区域启动 PWM 和 ADC
确保这些初始化已经执行:
bash
MX_USART1_UART_Init();
MX_TIM3_Init();
MX_ADC1_Init();
然后在 USER CODE BEGIN 2 里添加:
bash
/* USER CODE BEGIN 2 */
App_TestPWM_Init();
App_TestPWM_Start();
App_ADC_Init();
printf("adc pwm self test start\r\n");
/* USER CODE END 2 */
建议先启动 PWM,再开始 ADC 读取。
3. while 循环里改变占空比并读取平均值
在 USER CODE BEGIN 3 区域添加:
bash
/* USER CODE BEGIN 3 */
App_ADC_Result adc_result;
App_TestPWM_SetDutyPermille(250);
HAL_Delay(20);
if (App_ADC_ReadAverage(&adc_result, 256) == HAL_OK)
{
printf("duty=25%%, avg raw=%lu, avg voltage=%lu mV\r\n",
adc_result.raw,
adc_result.voltage_mv);
}
App_TestPWM_SetDutyPermille(500);
HAL_Delay(20);
if (App_ADC_ReadAverage(&adc_result, 256) == HAL_OK)
{
printf("duty=50%%, avg raw=%lu, avg voltage=%lu mV\r\n",
adc_result.raw,
adc_result.voltage_mv);
}
App_TestPWM_SetDutyPermille(750);
HAL_Delay(20);
if (App_ADC_ReadAverage(&adc_result, 256) == HAL_OK)
{
printf("duty=75%%, avg raw=%lu, avg voltage=%lu mV\r\n",
adc_result.raw,
adc_result.voltage_mv);
}
printf("\r\n");
HAL_Delay(1000);
/* USER CODE END 3 */
这里的 256 表示连续采样 256 次再求平均。
如果你改成单次读取,就可能看到 raw 在 0 和 4095 附近跳动。
这正好说明:
bash
PWM 不是稳定模拟电压
编译、下载和验证
代码加完后,先编译:
bash
Build / F7
没有错误后下载:
bash
Download
接好这根线:
bash
PA6 -> PA1
打开串口助手,正常会看到类似:
bash
adc pwm self test start
duty=25%, avg raw=1023, avg voltage=824 mV
duty=50%, avg raw=2047, avg voltage=1649 mV
duty=75%, avg raw=3071, avg voltage=2474 mV
实际数值可能会有一些偏差。
只要趋势满足:
bash
25% < 50% < 75%
就说明 ADC 单通道采样链路已经跑通。

移植到其他板子的修改点
这篇有两组移植点。
一组是 PWM 输出端。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
PWM 输出引脚
|
不同板子可用 PWM 引脚不同
|
CubeMX Pinout
|
|
PWM TIM 实例
|
TIM3/TIM4/TIM1 不同
| APP_TEST_PWM_HANDLE |
|
PWM 通道
|
CH1/CH2/CH3/CH4 不同
| APP_TEST_PWM_CHANNEL |
|
PWM 计数频率
|
用来生成目标频率
| APP_TEST_PWM_COUNTER_CLK_HZ |
|
PWM 默认频率
|
本篇默认 1 kHz
| APP_TEST_PWM_DEFAULT_FREQ_HZ |
另一组是 ADC 输入端。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
ADC 输入引脚
|
不同板子模拟输入接到不同引脚
|
CubeMX Pinout
|
|
ADC 通道
|
PA0/PA1/PB0 等对应 IN0/IN1/IN8 不同
|
CubeMX ADC Rank
|
|
ADC 实例
|
ADC1/ADC2/ADC3 可能不同
| APP_ADC_HANDLE |
|
参考电压
|
VDDA 不一定就是 3300 mV
| APP_ADC_VREF_MV |
|
ADC 分辨率
|
12 位、10 位、8 位最大值不同
| APP_ADC_MAX_RAW |
|
采样次数
|
PWM 直读需要平均更稳定
| App_ADC_ReadAverage(..., 256) |
换板子的推荐顺序:
-
找一个能输出 PWM 的引脚,比如
TIMx_CHy; -
找一个能作为 ADC 输入的引脚,比如
ADCx_INy; -
确认这两个引脚都能在开发板上接线;
-
CubeMX 配置 PWM 输出;
-
CubeMX 配置 ADC 单通道;
-
用杜邦线把 PWM 输出脚接到 ADC 输入脚;
-
修改
APP_TEST_PWM_*和APP_ADC_*宏; -
串口打印验证 25%、50%、75% 的平均值趋势。

常见问题排查
1. 串口没有输出
先别急着看 ADC。
先确认第 07 篇串口打印是否正常:
bash
printf("hello\r\n");
如果收不到,优先检查:
-
USART 是否初始化;
-
TX/RX/GND 是否接对;
-
串口助手波特率是否一致;
-
printf()是否已经重定向; -
Keil 是否勾选了 MicroLIB。
2. raw 一直是 0
重点检查:
-
PA6 是否真的接到了 PA1;
-
TIM3 PWM 是否已经启动;
-
PA1 是否配置成
ADC1_IN1; -
ADC Rank 是否选了 Channel 1;
-
APP_ADC_HANDLE是否和实际 ADC 一致; -
app_test_pwm.c和app_adc.c是否都加入 Keil。
如果 PA6 没接到 PA1,ADC 输入可能就是悬空或被拉低。
3. raw 一直接近 4095
常见原因:
-
PA1 被接到了 3.3V;
-
ADC 输入悬空后受干扰偏高;
-
ADC 通道选错;
-
PA6 PWM 一直高电平,比如 CCR 配置异常;
-
GND 或开发板供电异常。
可以先把 PA1 短接到 GND 测试。
如果短接 GND 后 raw 还能接近 4095,优先查 ADC 通道配置。
4. 单次 raw 在 0 和 4095 之间跳
这是正常现象。
因为你读的是 PWM。
PWM 本来就是:
bash
一会儿高
一会儿低
如果想看到更稳定的结果,就使用:
bash
App_ADC_ReadAverage(&adc_result, 256)
如果还觉得跳,可以把 256 改成:
bash
512 或 1024
但采样次数越多,读取一次结果花的时间也越长。
5. 25%、50%、75% 数值没有明显变化
重点检查:
-
App_TestPWM_SetDutyPermille()是否被调用; -
APP_TEST_PWM_HANDLE是否和 CubeMX 的 PWM TIM 一致; -
APP_TEST_PWM_CHANNEL是否和 PWM 通道一致; -
PA6 是否接到 PA1;
-
ADC 通道是否选对;
-
是否读取了平均值,而不是只读单次值。
6. 电压换算不准
先确认你写的参考电压是否真实。
代码默认:
bash
#define APP_ADC_VREF_MV 3300u
但你的板子 VDDA 可能不是刚好 3.300V。
如果你用万用表量到 VDDA 是 3.26V,就可以改成:
bash
#define APP_ADC_VREF_MV 3260u
另外,本篇没有 RC 滤波,软件平均只是帮助理解平均电压,不适合拿来做高精度模拟输出测量。
7. 编译报 htim3 或 hadc1 未定义
说明你的工程里没有生成对应句柄。
可能原因:
-
CubeMX 没启用 TIM3 或 ADC1;
-
你用的是其他 TIM 或 ADC;
-
应用代码里的宏没改。
如果 PWM 用 TIM2,就改:
bash
#define APP_TEST_PWM_HANDLE htim2
如果 ADC 用 ADC2,就改:
bash
#define APP_ADC_HANDLE hadc2
8. 编译报 undefined symbol App_TestPWM_Start 或 App_ADC_ReadAverage
通常是 .c 文件没加入 Keil 工程。
解决方法:
-
右键
Application/User/Core; -
选择
Add Existing Files to Group; -
添加
Core/Src/app_test_pwm.c和Core/Src/app_adc.c; -
重新编译。
9. 能不能完全不接线
可以
完全不接线的 ADC 实验可以读:
bash
内部参考电压 VREFINT
内部温度传感器
它的好处是不用任何外部连接。
但它的问题是:
-
不同 STM32 系列内部通道配置差异比较大;
-
数值不会像 PWM 占空比那样明显变化;
-
对新手来说,不如"输出多少、读到多少"直观。
所以本篇先用一根线做自测。
后面可以单独补一篇:
bash
STM32 ADC 内部通道:不用接线读取 VREFINT 和芯片温度
本篇小结
这一篇我们完成了 ADC 单通道采样的第一个自测实验:
bash
PWM 输出接回 ADC 输入,通过多次采样平均观察平均电压
你现在至少应该知道:
-
ADC 是把模拟电压转换成数字量;
-
12 位 ADC 原始值范围通常是 0~4095;
-
ADC 读出来的 raw 不是电压,需要换算;
-
入门换算可以用
voltage_mv = raw * vref_mv / 4095; -
PWM 不是稳定模拟电压,单次 ADC 读取可能是 0 或 4095;
-
多次采样平均可以近似观察 PWM 平均电压;
-
占空比越大,PWM 平均电压越高;
-
CubeMX 里 ADC 引脚要选
ADCx_INy; -
单通道入门可以用软件触发、右对齐、单次转换;
-
HAL_ADC_Start()启动采样; -
HAL_ADC_PollForConversion()等待转换完成; -
HAL_ADC_GetValue()读取结果; -
换板子时重点检查 PWM 输出端和 ADC 输入端两组资源。