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(摄氏度)。
- 用手摸芯片或吹热气,读数会明显变化(注意:受自热影响,绝对值可能偏高)。
常见问题
-
读数一直是 0 或满量程?
- 几乎都是漏了“开 ADC1 时钟”或“校准”。LL 不会自动开时钟,也requires 手动校准。
- 确认
LL_ADC_REG_TRIG_SOFTWARE 后确实调用了 LL_ADC_REG_StartConversion()。
-
温度跳变很大?
- 温度传感器本身噪声较大,可多次采样取平均;示例只打印单次值。
-
想采样外部引脚怎么改?
- 把
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 完全不介入连续采样。
- 把温度换算后用于控制风扇/报警,做最简单的温控演示。