在“多通道 + DMA”模式下,单片机会像一台自动流水线:ADC 负责把电压“打包”成数字,DMA 负责把包“搬运”到内存。CPU 甚至可以去喝咖啡,只需要在想看数据的时候,去数组里看一眼就行了。
1. 硬件连接 (准备两只“眼睛”)
假设你准备了两个模拟传感器(比如:一个电位器、一个光敏电阻):
- 传感器 A 的输出 $\rightarrow$ 连接到 PA0 (ADC1IN0)
- 传感器 B 的输出 $\rightarrow$ 连接到 PA1 (ADC1IN1)
- 所有 GND $\rightarrow$ 必须全部连在一起(共地)
- 电源 $\rightarrow$ 传感器接板子上的 3.3V
2. STM32CubeIDE 配置 (.ioc) —— 这是实验的灵魂
这里的配置比单通道稍微复杂一点,请屏住呼吸:
A. ADC1 配置
- 勾选通道:勾选 IN0 和 IN1。
- 基本参数 (Parameter Settings):
- Scan Conversion Mode:Enabled(扫描模式,必须开,否则它只看一个引脚)。
- Continuous Conversion Mode:Enabled(连续转换,自动循环看)。
- DMA Continuous Requests:Enabled(极其重要! 只有开了这个,DMA 才会源源不断地搬运,否则搬一次就停了)。
- 规则组参数 (ADC_Regular_ConversionMode):
- Number of Conversion:改为 2。
- Rank 1:选择 Channel 0 (对应 PA0)。
- Rank 2:选择 Channel 1 (对应 PA1)。
- 建议将 Sampling Time 调大一点(比如 55.5 或 71.5 Cycles),这样数据更稳。
(注意上图中采样时间才 1.5 个周期,需要调大一点,太快采不到)
| 配置项 | 位置 | 设置值 | 作用 |
|---|---|---|---|
| DMA Mode | DMA Settings | Circular | 搬运指针自动折返。 |
| Continuous Conversion | ADC Parameter Settings | Enabled | ADC 拍完一张不停,接着拍。 |
| DMA Continuous Requests | ADC Parameter Settings | Enabled | 每次拍完都呼叫快递员搬运。 |
B. DMA 配置 (关键点)
- 点击下方的 DMA Settings 选项卡 -> 点击 Add。
- 选择 ADC1。
- DMA 模式设置:
- Mode:选择 Circular (循环模式)。(这样 DMA 搬完两个数后,会自动回到数组开头覆盖旧数据,形成闭环)。
- Memory Address Increment:勾选 Increment Address。(这样第一个数存数组 [0],第二个数存数组 [1])。
- Data Width:Peripheral 和 Memory 都选 Half Word (16 位)。(因为 ADC 结果是 12 位,8 位装不下,用 16 位刚好)。
3. 编写代码 (main.c)
你会惊讶地发现,有了 DMA 后,代码反而变得异常简洁。
第一步:准备缓冲区
在 main.c 的 USER CODE BEGIN PV 区域:
|
|
第二步:启动 ADC 搬运流水线
在 main 函数的 while(1) 之前,启动 DMA 传输:
|
|
第三步:读取并显示数据
此时,数据已经在后台自动刷写到 adc_buffer 数组里了。在 while(1) 中:
|
|
运行后为什么看不到数字:
因为在代码中使用 %f 格式化字符串,但编译器没有开启浮点数支持时,printf 就会直接忽略这部分内容,导致你看到的输出跳过了数字,直接显示了后面的 V。
解决方法 A:开启 IDE 设置(最简单)
- 在左侧 Project Explorer 中,右键点击你的项目名称,选择 Properties。
- 进入 C/C++ Build -> Settings。
- 在右边的选项卡中选择 Tool Settings。
- 找到 MCU Settings。
- 勾选底部的
Runtime Library: Use float with printf from newlib-nano (-u _printf_float)。- 点击 Apply and Close。
- 重新编译(小锤子)并下载。
4. 实验原理图解 (Mermaid)
graph TD
subgraph "硬件自动流转 (无需 CPU 参与)"
SensorA["传感器 A (PA0)"] -- 采样 --> CH0["ADC 通道 0"]
SensorB["传感器 B (PA1)"] -- 采样 --> CH1["ADC 通道 1"]
CH0 -- DMA 搬运 --> Buf0["adc_buffer[0]"]
CH1 -- DMA 搬运 --> Buf1["adc_buffer[1]"]
Buf1 -- 循环模式重置 --> Buf0
end
Buf0 -.-> Read["CPU 直接读取数组内容"]
Buf1 -.-> Read
style SensorA fill:#f9f,stroke:#333
style SensorB fill:#f9f,stroke:#333
style Read fill:#bbf,stroke:#333
1. 为什么这个结构很强大?
在传统的做法中,如果你想测两个通道,你需要:
- 切到通道 0 $\to$ 启动 $\to$ 等待 $\to$ 读数。
- 切到通道 1 $\to$ 启动 $\to$ 等待 $\to$ 读数。
而在你现在配置的 扫描模式 (Scan) + 循环模式 (Circular) + DMA 下:
- ADC 会自动在 PA0 和 PA1 之间来回切换采样。
- DMA 会自动把 PA0 的结果放进
adc_buffer[0],把 PA1 的结果放进adc_buffer[1]。 - 完全同步:这个过程是硬件级锁定的,你永远不用担心 PA1 的数据被误存到了
adc_buffer[0]里。
2. DMA 循环模式
结构图当中,循环模式重置是什么意思:
你可以把 adc_buffer[2] 想象成一个只有两个格子的环形跑道:
- 第 1 步:ADC 转换完第一个通道(PA0),DMA 把结果送进
Buf0(第一个格子)。 - 第 2 步:ADC 转换完第二个通道(PA1),DMA 把结果送进
Buf1(第二个格子)。
此时,DMA 的搬运任务“名义上”完成了(因为你告诉它只搬 2 个数)。
- 如果是普通模式 (Normal Mode):搬运工 DMA 就地解散,去睡觉了。如果你想再看数据,必须由 CPU 再次下令
HAL_ADC_Start_DMA。 - 如果是循环模式 (Circular Mode):DMA 发现自己到了终点,会瞬间自动回到起点,把指针重新指向
Buf0。
5. 这个实验的“专业感”在哪里?
- 零等待:CPU 不需要调用
PollForConversion去死等转换完成。无论何时 CPU 去读adc_buffer,拿到的都是最新的电压值。 - 严格同步:PA0 和 PA1 的数据几乎是同时采集并按顺序排列的,绝对不会出现通道对错位。
- 自由扩展:如果你有 10 个传感器,只需要在配置里把 Number 改成 10,代码里定义长度为 10 的数组即可,代码量完全不增加。