PUYA ADC — 单通道转换(内部温度传感器)(LL 库)

PUYA PY32F0xx_Firmware V1.5.0 ADC 单通道转换(ADC_TempSensor_Init)示例教学(LL 库版本)

ADC — 单通道转换(内部温度传感器)(LL 库)

HAL vs LL:PUYA 固件库提供 HAL 和 LL 两套 API。

  • HAL 库:封装度高,可移植性强,适合快速开发
  • LL 库:接近寄存器操作,代码精简效率高,适合性能敏感场景 本文使用 LL 库版本。

固件库: PY32F0xx_Firmware V1.5.0
芯片: PY32F030(PY32F002A / F003 同库可参考)
源文件: PUYA/PY32F0xx_Firmware_V1.5.0/Projects/PY32F030-STK/Example_LL/ADC/ADC_TempSensor_Init/Src/main.c


功能简介

本示例演示如何用 ADC 读取芯片内部温度传感器的电压,并换算成摄氏度,通过串口打印出来。它是 LL 库里“单通道单次转换”最典型的代表——流程是:校准 ADC → 配置通道与触发源 → 软件触发 → 轮询结束标志 → 读结果 → 换算。

说明:PUYA 的 LL 示例库中没有 ADC_SingleConversion_TriggerSW_Polling 这个同名示例,但 ADC_TempSensor_Init 本质上就是“软件触发 + 单次转换 + 轮询读取”的范式,完全覆盖了你要学的 HAL ADC_SingleConversion_TriggerSW_Polling 的核心套路。把通道从“内部温度传感器”换成某个 GPIO 引脚(如 LL_ADC_CHANNEL_0),就是通用单端采样。

与 HAL 版(HAL_ADC_Start / HAL_ADC_PollForConversion / HAL_ADC_GetValue)相比,LL 把每一步都拆成独立函数,且校准过程需要你手动执行(HAL 在 HAL_ADC_Init 里已悄悄帮你校准过)。

硬件准备

  • PY32F030 开发板
  • 串口线(用于打印温度,接 USART,由 BSP_USART_Config() 初始化)
  • 无需外部传感器——温度传感器在芯片内部
  • 注意:温度传感器的采样值受芯片自身发热影响,读数仅供参考

完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include "main.h"
#include "py32f030xx_ll_Start_Kit.h"

#define HighTemp_85        /* 按数据手册选择 85℃ 或 105℃ 标定 */
/* #define HighTemp_105 */

#define ADC_CALIBRATION_TIMEOUT_MS       ((uint32_t) 1)
#define VAR_CONVERTED_DATA_INIT_VALUE    (__LL_ADC_DIGITAL_SCALE(LL_ADC_RESOLUTION_12B) + 1)

__IO uint16_t uhADCxConvertedData = VAR_CONVERTED_DATA_INIT_VALUE;
__IO uint16_t uhADCxConvertedData_Temp = 0;

static void APP_SystemClockConfig(void);
static void APP_AdcConfig(void);
static void APP_AdcEnable(void);
static void APP_AdcCalibrate(void);
static void APP_QuickSort(int32_t arr[], int32_t length);

int main(void)
{
  APP_SystemClockConfig();

  BSP_USART_Config();              /* 初始化调试串口,用于 printf */

  LL_ADC_Reset(ADC1);             /* 复位 ADC */
  LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_ADC1);  /* 开 ADC1 时钟 */

  APP_AdcCalibrate();             /* 校准(LL 必须手动做) */
  APP_AdcConfig();                /* 配置通道、触发源等 */
  APP_AdcEnable();                /* 使能 ADC */

  LL_ADC_REG_StartConversion(ADC1);   /* 软件触发一次转换 */
  while (1)
  {
    if (LL_ADC_IsActiveFlag_EOS(ADC1))    /* 等待“序列转换结束” */
    {
      LL_ADC_ClearFlag_EOS(ADC1);
      uhADCxConvertedData = LL_ADC_REG_ReadConversionData12(ADC1);  /* 读 12 位结果 */

      /* 按所选标定曲线换算温度 */
#if defined(HighTemp_85)
      uhADCxConvertedData_Temp = __LL_ADC_CALC_TEMPERATURE_85(3300, uhADCxConvertedData, LL_ADC_RESOLUTION_12B);
#else
      uhADCxConvertedData_Temp = __LL_ADC_CALC_TEMPERATURE_105(3300, uhADCxConvertedData, LL_ADC_RESOLUTION_12B);
#endif
      printf("Temperature: %d\r\n", uhADCxConvertedData_Temp);
      LL_mDelay(200);
      LL_ADC_REG_StartConversion(ADC1);  /* 再次软件触发 */
    }
  }
}

/**
  * @brief  ADC 配置:通道 + 触发源(软件触发)
  */
static void APP_AdcConfig(void)
{
  __IO uint32_t wait_loop_index = 0;

  LL_ADC_InitTypeDef     ADC_Init            = {0};
  LL_ADC_REG_InitTypeDef LL_ADC_REG_InitType = {0};

  /* ADC 基础参数 */
  ADC_Init.Clock         = LL_ADC_CLOCK_SYNC_PCLK_DIV4;   /* ADC 时钟 = PCLK/4 */
  ADC_Init.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT;       /* 右对齐 */
  ADC_Init.LowPowerMode  = LL_ADC_LP_MODE_NONE;
  ADC_Init.Resolution    = LL_ADC_RESOLUTION_12B;         /* 12 位分辨率 */
  LL_ADC_Init(ADC1, &ADC_Init);

  /* 采样时间 */
  LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_239CYCLES_5);

  /* 规则转换(Regular)参数 */
  LL_ADC_REG_InitType.ContinuousMode    = LL_ADC_REG_CONV_CONTINUOUS;     /* 连续模式 */
  LL_ADC_REG_InitType.DMATransfer       = LL_ADC_REG_DMA_TRANSFER_NONE;
  LL_ADC_REG_InitType.Overrun           = LL_ADC_REG_OVR_DATA_OVERWRITTEN;
  LL_ADC_REG_InitType.SequencerDiscont  = LL_ADC_REG_SEQ_DISCONT_DISABLE;
  LL_ADC_REG_InitType.TriggerSource     = LL_ADC_REG_TRIG_SOFTWARE;       /* 软件触发 */
  LL_ADC_REG_Init(ADC1, &LL_ADC_REG_InitType);

  LL_ADC_REG_SetSequencerScanDirection(ADC1, LL_ADC_REG_SEQ_SCAN_DIR_BACKWARD);

  /* 打开内部温度传感器通道 */
  LL_ADC_SetCommonPathInternalCh(__LL_ADC_COMMON_INSTANCE(ADC1), LL_ADC_PATH_INTERNAL_TEMPSENSOR);
  /* 等待温度传感器稳定 */
  wait_loop_index = ((LL_ADC_DELAY_TEMPSENSOR_STAB_US * (SystemCoreClock / (100000 * 2))) / 10);
  while (wait_loop_index != 0) { wait_loop_index--; }

  LL_ADC_REG_SetSequencerChannels(ADC1, LL_ADC_CHANNEL_TEMPSENSOR);  /* 选通道 11=温度传感器 */
}

/**
  * @brief  ADC 校准(LL 需手动执行,HAL 在 Init 时已做)
  */
static void APP_AdcCalibrate(void)
{
  /* 多次采样取中位数写入校准寄存器(厂商提供的校准流程) */
  uint32_t ADC_CALRR1_Buff[5];
  int32_t  ADC_CALRR1_BuffInt[5];
  if (LL_ADC_IsEnabled(ADC1) == 0)
  {
    for (int i = 0; i < 5; i++)
    {
      LL_ADC_StartCalibration(ADC1);
      while (LL_ADC_IsCalibrationOnGoing(ADC1) != 0) {}
      ADC_CALRR1_Buff[i]     = *((__IO uint32_t *)(0x40012448));
      ADC_CALRR1_BuffInt[i]  = (int32_t)(ADC_CALRR1_Buff[i] << 9);
    }
    APP_QuickSort(ADC_CALRR1_BuffInt, 5);
    *((__IO uint32_t *)(0x40012450)) = (ADC_CALRR1_BuffInt[2] >> 9);
    *((__IO uint32_t *)(0x40012454)) = *((__IO uint32_t *)(0x4001244C));
    *((__IO uint32_t *)(0x40012444)) |= 0x8000;
    LL_mDelay(1);
  }
}

/**
  * @brief  ADC 使能并等待稳定
  */
static void APP_AdcEnable(void)
{
  LL_ADC_Enable(ADC1);
  LL_mDelay(1);
}

/* APP_QuickSort / APP_SystemClockConfig / APP_ErrorHandler 略(与前述示例同构) */

代码分段讲解

1. 校准:LL 与 HAL 的关键差异

HAL 里你几乎感觉不到“校准”的存在——HAL_ADC_Init() 内部已经帮你调好。LL 里校准是显式的:先 LL_ADC_StartCalibration(),等 LL_ADC_IsCalibrationOnGoing() 清零,再把结果写回校准寄存器。示例里采样 5 次取中位数,是对出厂标定的微调。忽略这步,读数会明显不准。

2. 触发源:软件触发

LL_ADC_REG_InitType.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE 表示“想转换时,由软件写寄存器启动”,对应你点名要学的 ADC_SingleConversion_TriggerSW_*。运行时调用 LL_ADC_REG_StartConversion(ADC1) 即启动一次(连续模式下会持续转换)。

3. 等待结束 + 读结果

  • HAL:HAL_ADC_PollForConversion() 封装了等待。
  • LL:直接 LL_ADC_IsActiveFlag_EOS(ADC1) 轮询“序列结束”标志,再 LL_ADC_REG_ReadConversionData12() 读 12 位值。名字虽长,但语义一目了然。

4. 温度换算

__LL_ADC_CALC_TEMPERATURE_85() 是 LL 提供的宏,按 Vref(3300mV)、原始值、分辨率换算成摄氏温度。把通道换成外部 GPIO 引脚时,你只需用 LL_ADC_CHANNEL_x 替换 LL_ADC_CHANNEL_TEMPSENSOR,并去掉“内部通道”那几行。

实验现象

  • 串口每约 200ms 打印一行 Temperature: xx(摄氏度)。
  • 用手摸芯片或吹热气,读数会明显变化(注意:受自热影响,绝对值可能偏高)。

常见问题

  1. 读数一直是 0 或满量程?

    • 几乎都是漏了“开 ADC1 时钟”或“校准”。LL 不会自动开时钟,也requires 手动校准。
    • 确认 LL_ADC_REG_TRIG_SOFTWARE 后确实调用了 LL_ADC_REG_StartConversion()
  2. 温度跳变很大?

    • 温度传感器本身噪声较大,可多次采样取平均;示例只打印单次值。
  3. 想采样外部引脚怎么改?

    • LL_ADC_CHANNEL_TEMPSENSOR 换成 LL_ADC_CHANNEL_0~LL_ADC_CHANNEL_x,并删掉 LL_ADC_SetCommonPathInternalCh(...) 与稳定等待那几行。

小结

  • LL 的 ADC 使用范式:开时钟 → 校准 → 配置(通道/触发/分辨率) → 使能 → 软件触发 → 轮询 EOS → 读值
  • 与 HAL 最大的不同是“校准”和“启动转换”都暴露给你自己控制,没有任何隐藏动作。
  • 单通道软件触发轮询,是 LL ADC 最基础也最常用的模板。

拓展应用

  • 改成多通道扫描(LL_ADC_REG_SetSequencerChannels 设多个通道),轮流采样电压/温度。
  • 触发源换成定时器(LL_ADC_REG_TRIG_EXT_TIMx_TRGO),实现固定采样率。
  • 配合 DMA(ADC_MultiChannelSingleConversion_TriggerSW_DMA)让 CPU 完全不介入连续采样。
  • 把温度换算后用于控制风扇/报警,做最简单的温控演示。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计