STM32 零基础可移植教程 14:ADC 单通道采样,不接电位器也能读电压

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 或内部温度传感器

这个确实可以做到完全不外接。

但它有两个缺点:

  1. 不同 STM32 系列内部通道配置差异更大;

  2. 数值变化不明显,不如 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

注意三件事:

  1. PB5 输出的是 0~3.3V PWM,不能接超过 ADC 允许范围的电压;

  2. PC4 要在 CubeMX 里配置成 ADC1_IN14

  3. 不要把 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 时钟配置位置不完全一样。

入门阶段先记住两点:

  1. ADC 时钟不能超过芯片手册允许范围;

  2. 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) |

换板子的推荐顺序:

  1. 找一个能输出 PWM 的引脚,比如 TIMx_CHy

  2. 找一个能作为 ADC 输入的引脚,比如 ADCx_INy

  3. 确认这两个引脚都能在开发板上接线;

  4. CubeMX 配置 PWM 输出;

  5. CubeMX 配置 ADC 单通道;

  6. 用杜邦线把 PWM 输出脚接到 ADC 输入脚;

  7. 修改 APP_TEST_PWM_*APP_ADC_* 宏;

  8. 串口打印验证 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.capp_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. 编译报 htim3hadc1 未定义

说明你的工程里没有生成对应句柄。

可能原因:

  • 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_StartApp_ADC_ReadAverage

通常是 .c 文件没加入 Keil 工程。

解决方法:

  1. 右键 Application/User/Core

  2. 选择 Add Existing Files to Group

  3. 添加 Core/Src/app_test_pwm.cCore/Src/app_adc.c

  4. 重新编译。

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 输入端两组资源。

相关推荐
记帖3 小时前
STM32C542开发(1)----点亮LED
嵌入式硬件·stm32cubemx·stm32cubeide·stm32cubemx2·stm32c542cct6
m0_377108143 小时前
stm32平衡车mpu6050
stm32·单片机·嵌入式硬件
资深流水灯工程师4 小时前
STM32 SAI 通讯原理与 TDM 应用
stm32·单片机·嵌入式硬件
Deitymoon4 小时前
FreeRTOS——任务信息查询API
stm32·单片机·嵌入式硬件
踏着七彩祥云的小丑5 小时前
嵌入式测试学习第 24 天:串口通信详细流程、收发数据原理
单片机·嵌入式硬件
周周记笔记5 小时前
【元器件专题】MOS管内部结构
嵌入式硬件
周周记笔记5 小时前
【元器件专题】MOS管的设计应用
单片机·嵌入式硬件
一路往蓝-Anbo5 小时前
第九章:OTA 与 Flash 驱动 —— 如何用TDD验证固件升级逻辑的鲁棒性
stm32·单片机·嵌入式硬件·软件工程·tdd·ota·嵌入式测试驱动开发
zlinear数据采集卡6 小时前
电源纹波无处遁形!工业采集卡电源去耦与滤波电路深度解析
c语言·嵌入式硬件·fpga开发·自动化·硬件架构