RT1060 ADC demo 寄存器解析_Part2 中断

RT1060 ADC demo 寄存器解析_Part2 中断

i.MX RT1060 下的 ADC 开发实战:从 HAL 库到寄存器级驱动

在嵌入式开发中,官方提供的 HAL (Hardware Abstraction Layer) 库虽然使用方便,但有时为了追求极致的性能、更小的代码体积,或者仅仅是为了“知其所以然”,我们需要深入底层,直接操作寄存器。

本文将以 NXP i.MX RT1060 的 ADC 中断例程为例,深入剖析 ADC 外设的工作原理、寄存器配置细节,并演示如何将官方 SDK 的 HAL 库代码重构为直接操作寄存器的高效代码。

1. 工程概述与逻辑架构

1.1 功能描述

RT1060 adc_interrupt Demo 演示了一个 12-bit SAR ADC 在软件触发模式下的基础功能:

  • 交互方式:通过串口终端 (Debug Console) 交互。
  • 工作流:*用户按下任意键 -> 触发一次 ADC 转换 -> ADC 转换完成触发中断 -> 在 ISR 中读取数据 -> 串口打印结果*。
  • 核心价值:展示了从触发到中断读取的完整闭环。

1.2 逻辑时序图 (UML)

为了理清代码并未直接展现的硬件交互流程,我们绘制了如下的时序图:

sequenceDiagram
    participant User as 用户 (Terminal)
    participant Main as 主程序 (Main Loop)
    participant Register as 寄存器接口
    participant ADC_HW as ADC 硬件核心
    participant ISR as 中断服务程序

    Note over Main, ADC_HW: === 初始化阶段 ===
    Main->>Register: 配置 CFG (时钟/模式) & GC (功能)
    Main->>Register: 写入 GC[CAL] 启动自校准
    ADC_HW-->>Register: 校准完成自动清零 CAL
    Main->>Register: 检查 GS[CALF] 确认成功

    Note over User, ISR: === 运行时循环 ===
    loop Every Conversion
        Main->>User: "Press any key..."
        User->>Main: 按键输入
        Main->>Main: 清除软件标志位 Flag = false
        
        Main->>Register: 写入 HC0[ADCH] (通道号)
        Note right of Main: 关键:写入 HC0 立即启动软件触发转换
        
        ADC_HW->>ADC_HW: 采样 & 转换 (Sampling)
        ADC_HW-->>ISR: 完成中断 (COCO Interrupt)
        
        activate ISR
        ISR->>Register: 读取 R0 寄存器
        Note right of ISR: 关键:读取 R0 自动清除 COCO 标志
        ISR->>Main: 置位 Flag = true
        deactivate ISR
        
        Main->>Main: 退出等待循环
        Main->>User: 打印采样值
    end

2. 核心寄存器深度解析

要脱离 HAL 库,必须读懂 Datasheet。RT1060 的 ADC 主要由以下几个关键寄存器控制:

2.1 ADCx_CFG (Configuration Register) - 全局大脑

这是配置 ADC 基础属性的地方。

  • ADICLK (Input Clock): 设置为 11b (Asynchronous Clock)。选用异步时钟让 ADC 可以在 CPU 总线时钟停止时继续运行(低功耗场景)。
  • MODE (Resolution): 设置为 10b (12-bit)。输出范围 0-4095。
  • ADTRG (Trigger): 设置为 0 (Software Trigger)。这是本例程的核心,如果不配置为软件触发,写入命令寄存器将无效。
  • ADIV (Divider): 时钟分频,通常设为 0 (不分频) 以获得最快采样速度。

2.2 ADCx_GC (General Control) - 功能开关

  • CAL (Calibration): 写 1 启动硬件自校准。
  • ADACKEN (Asynchronous Clock Output): 必须置 1。因为我们在 CFG 中选择了异步时钟作为源,所以这里必须打开它。

2.3 ADCx_HCn (Heap Command/Control) - 发令枪

RT1060 有多个 HC 寄存器 (HC0-HC7),但在软件触发模式下,通常只用 HC0

  • 写入动作这是最关键的一点。在软件触发模式下,向 HC0 写入通道号的操作,等同于按下了“启动按钮”。
  • ADCH (Channel): 写入目标通道号。
  • AIEN (Interrupt Enable): 置 1 则转换完成后触发中断。

2.4 ADCx_R0 (Data Result) - 终点

  • 读取该寄存器获得 12-bit 结果。
  • 隐含操作:读取 R0 会自动清除状态寄存器 (HS) 中的“转换完成标志” (COCO)。

3. 从 HAL 到寄存器的代码重构实战

我们将对比 NXP SDK 的 HAL 实现与我们重构后的寄存器实现。本例中,我们将完整的 HAL 调用替换为了直接的指针操作。

3.1 初始化配置 (Initialization)

HAL 方式:

1
2
3
4
5
adc_config_t adcConfigStruct;
ADC_GetDefaultConfig(&adcConfigStruct);
adcConfigStruct.clockSource = kADC_ClockSourceAD;
adcConfigStruct.resolution = kADC_Resolution12Bit;
ADC_Init(DEMO_ADC_BASE, &adcConfigStruct);

寄存器方式 (重构后): 我们直接操作 CFGGC 寄存器,代码更加直观且无函数调用开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint32_t tmp32;
/* 配置 CFG 寄存器 */
/* 保留 AVGS (硬件平均) 设置,清除 ADTRG (设为软件触发) */
tmp32 = DEMO_ADC_BASE->CFG & (ADC_CFG_AVGS_MASK | ADC_CFG_ADTRG_MASK);

/* 这里的宏定义直接对应寄存器位域 */
tmp32 |= ADC_CFG_REFSEL(kADC_ReferenceVoltageSourceAlt0) |
ADC_CFG_ADSTS(kADC_SamplePeriod2or12Clocks) |
ADC_CFG_ADICLK(kADC_ClockSourceAD) | // 异步时钟源
ADC_CFG_ADIV(kADC_ClockDriver1) |
ADC_CFG_MODE(kADC_Resolution12Bit); // 12位分辨率

DEMO_ADC_BASE->CFG = tmp32;

/* 配置 GC 寄存器 */
tmp32 = DEMO_ADC_BASE->GC & ~(ADC_GC_ADCO_MASK | ADC_GC_ADACKEN_MASK);
tmp32 |= ADC_GC_ADACKEN_MASK; // 使能异步时钟输出
DEMO_ADC_BASE->GC = tmp32;

/* 再次确认关闭硬件触发 */
DEMO_ADC_BASE->CFG &= ~ADC_CFG_ADTRG_MASK;

3.2 硬件自校准 (Auto-Calibration)

HAL 库的 ADC_DoAutoCalibration 内部逻辑较为复杂,隐藏了状态查询细节。重写后我们可以清晰看到校准的完整握手流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 1. 清除旧的校准失败标志 */
DEMO_ADC_BASE->GS = ADC_GS_CALF_MASK;

/* 2. 启动校准 (写入 GC 寄存器的 CAL 位) */
DEMO_ADC_BASE->GC |= ADC_GC_CAL_MASK;

/* 3. 阻塞等待硬件清除 CAL 位 (表示校准结束) */
while (0U != (DEMO_ADC_BASE->GC & ADC_GC_CAL_MASK))
{
/* 可选:检查中途是否失败 */
if (DEMO_ADC_BASE->GS & ADC_GS_CALF_MASK) break;
}

/* 4. 结果验证 */
if (DEMO_ADC_BASE->HS & (1UL << 0U)) {
/* 虚拟读取 R0 以清除 COCO 标志,复位状态机 */
(void)DEMO_ADC_BASE->R[0];
}

3.3 触发转换与中断读取

这是主循环中最频繁执行的代码段。

寄存器方式启动转换:

1
2
3
4
5
6
// 准备命令字:通道号 + 中断使能位
uint32_t tmpHC = ADC_HC_ADCH(DEMO_ADC_USER_CHANNEL);
tmpHC |= ADC_HC_AIEN_MASK;

// 写入 HC0,硬件立即启动转换
DEMO_ADC_BASE->HC[DEMO_ADC_CHANNEL_GROUP] = tmpHC;

中断服务程序 (ISR):

1
2
3
4
5
6
7
8
9
10
11
void EXAMPLE_ADC_IRQHandler(void)
{
g_AdcConversionDoneFlag = true;

// 直接读取数据寄存器 R[0]
// 这一步既获取了数据,又清除了硬件中断标志
g_AdcConversionValue = DEMO_ADC_BASE->R[DEMO_ADC_CHANNEL_GROUP];

g_AdcInterruptCounter++;
SDK_ISR_EXIT_BARRIER;
}

4. 总结

通过这次重构,我们不仅缩减了代码的 Flash 占用(移除了庞大的 fsl_adc.c 依赖),更重要的是彻底掌握了:

  1. 触发机制明确了“写 HC0”就是“软件触发”的本质。
  2. 标志位管理:理解了读取数据寄存器与清除状态标志的硬件联动关系。
  3. 时钟构建:理解了异步时钟源配置与 GC 控制寄存器的依赖关系。

这种寄存器级别的掌控力,在进行高性能电机控制、电源管理等对 ADC 采样时序要求极高的应用中,是必不可少的核心能力。

附录

本工程针对 i.MX RT1060 ADC (12-bit SAR) 外设。以下是核心用到的寄存器及其位域分析。

1. ADCx_CFG (Configuration Register)

用于设置 ADC 的工作模式、时钟源和采样时间。

Bit(s) Field Name Description in Project Value Analysis
1-0 ADICLK Input Clock Select 11b (kADC_ClockSourceAD) - 选择异步时钟 (Asynchronous Clock)。ADACK,该时钟是ADC模块中的时钟源生成的,所以当单片机处于停止模式时该时钟仍然在运行。使用该时钟在停止模式下ADC可以进行转换。
3-2 MODE Conversion Mode Selection 10b (kADC_Resolution12Bit) - 12位分辨率。结果范围 0-4095。
4 ADLSMP Long Sample Time Configuration 0 (Short Sample) - 使用短采样时间。
6-5 ADIV Clock Divide Select 00b (Divide by 1) - 输入时钟不分频。
7 ADLPC Low-Power Configuration 0 - 普通功耗模式。
12-11 REFSEL Voltage Reference Selection 0- 只有一个选项(kADC_ReferenceVoltageSourceAlt0
13 ADTRG Conversion Trigger Select 0 - 软件触发 (Software Trigger)。这是本工程的关键,转换由写入 HC 寄存器启动。
16 OVWREN Data Overwrite Enable 0 - 禁止覆写。如果旧数据未读,新数据不会覆盖它(这是通常的安全做法,虽然在中断模式下通常能及时读取)。

2. ADCx_GC (General Control Register)

用于控制校准、DMA请求和异步时钟输出。

Bit(s) Field Name Description in Project Value Analysis
0 ADACKEN Asynchronous Clock Output Enable 1 - 使能。因为 CFG[ADICLK] 选择了 ADACK,所以必须开启此位以产生时钟。
1 DMAEN DMA Enable 0 - 关闭 DMA。本工程使用中断 (Interrupt) 方式搬运数据,而非 DMA。
6 ADCO Continuous Conversion Enable 0 - 单次转换 (One-shot)。每次触发只进行一次转换。
7 CAL Calibration Func 1 (Momentary) - 写入 1 启动自校准流程。校准结束后硬件自动清零。

3. ADCx_GS (General Status Register)

用于指示 ADC 模块的全局状态(非特定通道)。

Bit(s) Field Name Description in Project Value Analysis
1 CALF Calibration Failed Flag 用于 ADC_DoAutoCalibration 函数中。如果校准失败,此位会被置 1。程序需检查此位确保校准成功。
2 ADACT Conversion Active 指示转换是否正在进行中。

4. ADCx_HCn (Control register for hardware triggers)

重要:在 RT1050 芯片中,有 HC0HC7对于软件触发,只能使用 HC0写入此寄存器是启动软件转换的触发器

Bit(s) Field Name Description in Project Value Analysis
4-0 ADCH Input Channel Select 设置为 DEMO_ADC_USER_CHANNEL。选择物理模拟通道引脚。
7 AIEN Interrupt Enable 1 - 开启中断。当转换完成 (COCO) 时,产生中断请求。

5. ADCx_Rn (Data Result Register)

保存转换结果。在此工程中主要关注 R0(对应 HC0)。

Bit(s) Field Name Description in Project Value Analysis
11-0 D Data (Result) 12位转换结果数据。

注意:读取 Rn 寄存器也是清除该通道 HS (Status) 寄存器中 COCO (Conversion Complete) 标志位的标准方法。


6. ADCx_HS (Status Register)

通道状态寄存器。

Bit(s) Field Name Description
0 COCO0 Conversion Complete Flag 0。当 R0 数据准备好时置 1。读取 R0 后自动清零。

附录2

完整的修改后的寄存器代码

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
127
128
129
130
int main(void)
{
// adc_config_t adcConfigStruct;
// adc_channel_config_t adcChannelConfigStruct;

BOARD_InitHardware();
EnableIRQ(DEMO_ADC_IRQn);

PRINTF("\r\nADC interrupt Example (Register Level).\r\n");

/*
* Register Level Initialization
* Replaces: ADC_GetDefaultConfig & ADC_Init
*/

/*
* CRITICAL FIX: Enable the ADC Clock!
* In the original HAL ADC_Init(), CLOCK_EnableClock() is called.
* Accessing ADC registers without the clock enabled results in hard faults or ignored writes (reads return 0).
*/
CLOCK_EnableClock(kCLOCK_Adc1);

uint32_t tmp32;

/* ADCx_CFG Configuration */
/* Keep existing AVGS and ADTRG bits (though we clear ADTRG later) */
tmp32 = DEMO_ADC_BASE->CFG & (ADC_CFG_AVGS_MASK | ADC_CFG_ADTRG_MASK);

/* Config:
* - Reference: Alt0 (kADC_ReferenceVoltageSourceAlt0)
* - Sample Period: 2/12 Clocks (kADC_SamplePeriod2or12Clocks)
* - Clock Source: Asynchronous (kADC_ClockSourceAD)
* - Clock Driver (Divider): 1 (kADC_ClockDriver1)
* - Resolution: 12 Bit (kADC_Resolution12Bit)
*/
tmp32 |= ADC_CFG_REFSEL(kADC_ReferenceVoltageSourceAlt0) |
ADC_CFG_ADSTS(kADC_SamplePeriod2or12Clocks) |
ADC_CFG_ADICLK(kADC_ClockSourceAD) |
ADC_CFG_ADIV(kADC_ClockDriver1) |
ADC_CFG_MODE(kADC_Resolution12Bit);

/* Other bools from default config are false (LowPower, LongSample, HighSpeed, OverWrite), so we don't set their masks. */
DEMO_ADC_BASE->CFG = tmp32;

/* ADCx_GC Configuration */
tmp32 = DEMO_ADC_BASE->GC & ~(ADC_GC_ADCO_MASK | ADC_GC_ADACKEN_MASK);
/* Enable Asynchronous Clock Output (needed for kADC_ClockSourceAD) */
tmp32 |= ADC_GC_ADACKEN_MASK;
/* Disable Continuous Conversion (ADCO not set) */
DEMO_ADC_BASE->GC = tmp32;

/* Disable Hardware Trigger (Software Trigger Mode) */
// Replaces: ADC_EnableHardwareTrigger(DEMO_ADC_BASE, false);
DEMO_ADC_BASE->CFG &= ~ADC_CFG_ADTRG_MASK;


/* Do auto hardware calibration. */
/* Register Level Implementation of ADC_DoAutoCalibration */
bool calibrationSuccess = true;

/* 1. Clear CALF (Calibration Failed Flag) */
DEMO_ADC_BASE->GS = ADC_GS_CALF_MASK; //特殊的W1C,写1清零

/* 2. Launch Calibration */
DEMO_ADC_BASE->GC |= ADC_GC_CAL_MASK;

/* 3. Wait for CAL bit to clear */
while (0U != (DEMO_ADC_BASE->GC & ADC_GC_CAL_MASK))
{
/* Check if CALF happened during wait */
if (DEMO_ADC_BASE->GS & ADC_GS_CALF_MASK)
{
calibrationSuccess = false;
break;
}
}

/* 4. Final Status Check */
/* Check COCO0 (Conversion Complete) in HS register - Calibration result is stored in R0 */
if (0U == (DEMO_ADC_BASE->HS & (1UL << 0U))) // ADC_GetChannelStatusFlags(base, 0)
{
calibrationSuccess = false;
}
if (DEMO_ADC_BASE->GS & ADC_GS_CALF_MASK)
{
calibrationSuccess = false;
}

/* Clear conversion done flag by reading the result */
(void)DEMO_ADC_BASE->R[0];

if (calibrationSuccess)
{
PRINTF("ADC_DoAutoCalibration() Done.\r\n");
}
else
{
PRINTF("ADC_DoAutoCalibration() Failed.\r\n");
}

/* Configure the user channel and interrupt. */
// adcChannelConfigStruct.channelNumber = DEMO_ADC_USER_CHANNEL;
// adcChannelConfigStruct.enableInterruptOnConversionCompleted = true;

g_AdcInterruptCounter = 0U;

PRINTF("ADC Full Range: %d\r\n", g_Adc_12bitFullRange);
while (1)
{
PRINTF("Press any key to get user channel's ADC value.\r\n");
GETCHAR();
g_AdcConversionDoneFlag = false;

/*
Register Level: Start Conversion
Replaces: ADC_SetChannelConfig(DEMO_ADC_BASE, DEMO_ADC_CHANNEL_GROUP, &adcChannelConfigStruct);
*/
uint32_t tmpHC = ADC_HC_ADCH(DEMO_ADC_USER_CHANNEL);
tmpHC |= ADC_HC_AIEN_MASK; // Enable Interrupt

/* Writing to HC register triggers the software conversion */
DEMO_ADC_BASE->HC[DEMO_ADC_CHANNEL_GROUP] = tmpHC;

while (g_AdcConversionDoneFlag == false)
{
}
PRINTF("ADC Value: %d\r\n", g_AdcConversionValue);
PRINTF("ADC Interrupt Counter: %d\r\n", g_AdcInterruptCounter);
}
}

ADC1_IRQHandler

1
2
3
4
5
6
7
8
9
10
11
void EXAMPLE_ADC_IRQHandler(void)
{
g_AdcConversionDoneFlag = true;
/* Read conversion result to clear the conversion completed flag. */
// g_AdcConversionValue = ADC_GetChannelConversionValue(DEMO_ADC_BASE, DEMO_ADC_CHANNEL_GROUP);
/* Register Level: Read R register directly */
g_AdcConversionValue = DEMO_ADC_BASE->R[DEMO_ADC_CHANNEL_GROUP];

g_AdcInterruptCounter++;
SDK_ISR_EXIT_BARRIER;
}

附录3 Polling(轮询) vs Interrupt(中断)版本概要

两者的核心寄存器配置(CFG、GC、时钟/校准)一致,主要差异集中在 转换完成后的处理方式初始化流程中的中断配置


详细差异对比表

特性 Polling(轮询)版本 Interrupt(中断)版本 adc_interrupt.c
1. 中断控制器使能 main 开头调用 EnableIRQ(DEMO_ADC_IRQn);,使能 NVIC 层级的中断
2. HCn 寄存器配置(触发转换) tmp32 = ADC_HC_ADCH(...)设置 AIEN);DEMO_ADC_BASE->HC[...] = tmp32; tmpHC = ADC_HC_ADCH(...) 后, **置位 AIEN**;
uint32_t tmpHC = ADC_HC_ADCH(DEMO_ADC_USER_CHANNEL);
`tmpHC
3. 等待转换完成 主动忙等:死循环查询 HS/COCO 状态位:while (0U == ((...->HS >> ...) & 0x1U)) { ; } 被动等待软件标志位:while (g_AdcConversionDoneFlag == false) { }(主循环不直接查寄存器)
4. 结果读取 在 while 循环结束后直接读取:result = DEMO_ADC_BASE->R[...] 在 ISR(EXAMPLE_ADC_IRQHandler(...))里读取 R[...] 并更新全局变量
5. 状态清除(COCO) 读取 R[...] 时硬件自动清除(在主程序中发生) 读取 R[...] 时硬件自动清除(在ISR中发生)

代码逻辑差异深度分析

1) 触发转换的区别(HC 寄存器)

Polling 版本:仅写入 ADCH 通道号。这会启动转换,但不会请求中断。

1
2
3
tmp32 = ADC_HC_ADCH(adcChannelConfigStruct.channelNumber);
// 注意:这里没有 |= ADC_HC_AIEN_MASK
DEMO_ADC_BASE->HC[DEMO_ADC_CHANNEL_GROUP] = tmp32;

Interrupt 版本:写入 ADCH 通道号 + AIEN (Interrupt Enable) 位。

1
2
3
uint32_t tmpHC = ADC_HC_ADCH(DEMO_ADC_USER_CHANNEL);
tmpHC |= ADC_HC_AIEN_MASK; // <--- 关键区别
DEMO_ADC_BASE->HC[DEMO_ADC_CHANNEL_GROUP] = tmpHC;

2) 等待机制的区别(Busy-Wait vs. Interrupt-Wait)

Polling:这种方式占用 CPU。CPU 在 while 循环中不断去访问外设总线读取 HS (Status) 寄存器,直到 COCO (Conversion Complete) 标志变 1。

1
while (0U == ((DEMO_ADC_BASE->HS >> DEMO_ADC_CHANNEL_GROUP) & 0x1U)) { ; }

Interrupt:这种方式释放 CPU(虽然本例中 CPU 也在死循环等待 flag,但在实际 RTOS 或复杂系统中,CPU 可以去干别的事或者休眠)。当硬件转换完成,硬件信号直接触发 NVIC 跳转到 ISR。

1
while (g_AdcConversionDoneFlag == false) { } // 等待 ISR 修改此变量

3) 读取数据的时机与位置

Polling: 就在 main 函数里,确认 HS 标志变位后立即读取。 Interrupt: 在 EXAMPLE_ADC_IRQHandler 函数里读取。


总结

polling 代码是同步阻塞的寄存器写法,而 adc_interrupt.c 是异步事件驱动的寄存器写法。两者底层的 CFG (时钟/模式) 和 GC (校准) 配置是完全通用的。


作者

Gavin

发布于

2025-12-15

更新于

2025-12-15

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×