1.5V
2.GND
3. PA0
4.PD11
使用DWA之前先看STM32的参考手册-DMA映像147,防止冲突
你的现状:完全够用,互不干扰
STM32F103 系列有两个 DMA 控制器,资源其实是挺丰富的:
- DMA1:有 7 个通道。
- DMA2:有 5 个通道。
- 总计:你有 12 条“高速公路”可以用。
你的项目目前只用了 2 条:
- 光敏 (ADC3):占用了
DMA2的通道5。 - MQ2 (ADC1):将占用
DMA1的通道1。
结论:你手里还有 10 条空闲通道,哪怕你再加几个传感器(比如再加个 ADC2,或者加几个串口 USART),通道都是绰绰有余的。
什么时候会觉得“通道不够”?
虽然总共有 12 个通道,但在实际工程中,确实会出现“抢车道”的情况。主要有两个原因:
1. “硬性绑定”导致的局部拥堵
这是最常见的问题。STM32 的外设和 DMA 通道是固定死的,不能随便换。
- 例子:假设你想用 3 个 ADC 同时采集(ADC1, ADC2, ADC3)。
ADC1只能走DMA1 Channel 1。ADC2只能走DMA1 Channel 1(注意!它和 ADC1 抢同一个通道)。ADC3只能走DMA2 Channel 5。
- 冲突点:如果你同时开启 ADC1 和 ADC2 的 DMA,它们就会在
DMA1 Channel 1撞车。这时候你就得二选一,或者放弃 DMA 改用中断。
2. 高级外设占用了“黄金通道”
有些通道是“兵家必争之地”。
- SPI / I2S:如果你要用高速 SPI 传输屏幕数据,或者用 I2S 播放音乐,它们通常会占用 DMA1 或 DMA2 的特定通道。
- 定时器 (TIM):如果你要做复杂的电机控制(PWM 输入捕获),定时器也会占用 DMA 通道。
- 如果你的项目既要搞音频(I2S),又要搞多路 ADC,还要搞串口通信,那 12 个通道可能很快就分完了。
如果通道真的不够了怎么办?
假如你是一个超级复杂的项目,12 个通道全被占满了,还想加功能,有以下 3 种解决方案(按推荐程度排序):
方案 A:使用“轮询”或“中断”代替 DMA(最常用)
不是所有数据都需要 DMA 搬运。
- 低速数据:比如温度传感器(DS18B20)、按键扫描,速度很慢,直接用 CPU 读取(轮询)或者用中断读取即可,没必要浪费 DMA 通道。
- 中速数据:比如普通的串口打印调试信息,用
TXE(发送寄存器空)中断来发,虽然比 DMA 稍微占一点 CPU,但完全来得及。
方案 B:分时复用(Time Division Multiplexing)
如果两个设备不会同时工作,可以共用一个通道。
- 例子:你有一个 ADC 用来测电池电压(几分钟测一次),另一个 ADC 用来测电机电流(一直在测)。
- 做法:平时把 DMA 配置给电机电流用。当需要测电池时,暂停电机 DMA,把 DMA 重新配置给电池 ADC,测完再切回去。这需要写代码动态切换 DMA 的配置。
方案 C:软件模拟 DMA(下策)
如果硬件 DMA 真的没了,你可以写一个定时器中断(比如 1ms 进一次),在中断里手动把数据从外设寄存器读到内存数组里。这其实就是用 CPU 模拟了 DMA 的功能,虽然效率低一点,但能解决问题。
然后此处使用
ADC 的作用就是把连续的模拟电压,量化成一个离散的数字
配置ADC的流程
1. 开 ADC 时钟
2. 配置引脚为模拟输入
3. 设置 ADC 分频(复制一份主频,降速到合适的频率,再送给这个机器)
4. 设置采样时间(采集一次花的时间)
5. 设置转换通道和顺序(通道 = 选哪个引脚,顺序 = 先采谁后采谁)
6. 设置工作模式
7. 上电 ADC 并校准(每次上电后校准一次,测出 ADC 内部的固定零点偏差并补偿,之后就可以正常采样了。校准解决的是"天生不准",不是"随机抖动"。)
8. 启动转换并读取结果
第一步:光敏电阻是什么?
光敏电阻就是一个会随光照变化的电阻。
- 光照强 → 电阻变小
- 光照弱 → 电阻变大
它本身不会说话,不会告诉你"现在是多亮"。
第二步:怎么让单片机"看到"它?
光敏电阻单独接在单片机上,单片机是读不出来的。
所以我们要把它和一个固定电阻串在一起,接在 3.3V 和 GND 之间,中间那个点接到单片机的 PA5 引脚。
这样光照变化时,PA5 上的电压就会跟着变化。
第三步:单片机怎么读这个电压?
单片机有一个东西叫ADC,它的作用就是:把电压变成一个数字。
你的单片机 ADC 是 12 位的,它会把 0~3.3V 的电压分成 4096 个等级:
- 0V → 数字 0
- 3.3V → 数字 4095
- 中间电压 → 中间的数字
所以 ADC 读出来的就是一个0 到 4095 之间的整数。
第四步:这个数字代表什么?
这个数字代表的是PA5 引脚上的电压高低,不是光照强度本身。
但因为你用了光敏电阻,电压会随光照变化,所以这个数字间接反映了光照强弱。
你的电路接法是:光越亮,ADC 数字越小;光越暗,ADC 数字越大。
第五步:怎么变成百分比?
百分比的意思就是:当前亮度占最大亮度的多少。
但是问题来了——你不知道"最亮"是多少,也不知道"最暗"是多少。
所以你要先测:
- 用手完全遮住光敏电阻,看 ADC 读出来是多少,比如 3800。这就是"最暗"。
- 用手电筒照光敏电阻,看 ADC 读出来是多少,比如 200。这就是"最亮"。
有了这两个数,就可以算了:
- ADC = 200 → 100%(最亮)
- ADC = 3800 → 0%(最暗)
- ADC = 2000 → 中间某个百分比
公式就是:用"最暗的值减去当前值",再除以"最暗减最亮的范围",乘以 100,就得到百分比。
总结一句话
光敏电阻随光照变阻值 → 变成电压 → ADC 变成 0~4095 的数字 → 你先测出最亮和最暗时各是多少 → 用公式算出 0~100% → 串口打印出来。
只要你能测出遮光和光照下的两个 ADC 值,就能映射
不好意思,2026年11月4号的时候发现光敏的电压不够,现在是调整过的3.3v
通过网盘分享的文件:ADC
链接: https://pan.baidu.com/s/1TVpcMFcf8o6lLUOWRFFCAg?pwd=8888 提取码: 8888
--来自百度网盘超级会员v5的分享
其中发送数字转字符串的逻辑如下
/*
* ==================== 串口发送数字(先转换再发送,逻辑清晰版) ====================
*
* 转换原理:
* 1. 整数在内存里是二进制,没有"5、2、8"这样的字符,
* 必须用除法"算"出每一位。
*
* 2. 每次用 val % 10 取出最右边一位(个位),
* 再用 '0' + 余数 把数字 0~9 变成字符 '0'~'9'。
* 例如:数字 8 → '0' + 8 → 字符 '8'(ASCII 码 56)
*
* 3. 因为每次都取最右边的个位,所以先取到的是低位,
* 存进 buf 的顺序是反的。
* 例如 val=528:
* 第1轮:528 % 10 = 8 → buf[0]='8'
* 第2轮: 52 % 10 = 2 → buf[1]='2'
* 第3轮: 5 % 10 = 5 → buf[2]='5'
* 此时 buf = ['8']['2']['5'],是反的。
*
* 4. 翻转 buf:首尾交换,把反序变成正序。
* ['8']['2']['5'] → 交换 buf[0] 和 buf[2] → ['5']['2']['8']
*
* 5. 正序发送:从头到尾逐个发出去,串口收到的就是 "528"。
*
* 示例:val = 528
* 阶段1 转换后:buf = ['8']['2']['5'] i = 3
* 阶段2 翻转后:buf = ['5']['2']['8']
* 阶段3 发送: 依次发出 '5' '2' '8' → 串口显示 528
*/
然后是DMA的配置和讲解
开时钟 → 复位通道 → 配规则 → 使能DMA → 使能外设DMA请求 → 启动外设
开启 DMA 时钟
DMA 也是一个外设,使用前必须先开时钟。DMA1 属于 AHB 总线,所以调用RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE)使能时钟。不开时钟的话,后面所有 DMA 寄存器都写不进去。复位 DMA 通道
调用DMA_DeInit(DMAy_Channelx),把该通道的寄存器恢复到默认状态。这样即使之前有人用过这个通道,也不会留下错误配置。设置外设基地址
告诉 DMA:数据从哪里来,或者数据送到哪里去。
如果是从外设读取数据,就写外设的数据寄存器地址,例如(uint32_t)&USART1->DR或(uint32_t)&ADC1->DR。
如果是向串口发送数据,源地址是内存,外设地址仍然是USART1->DR,因为 DMA 每次都要把数据写进同一个寄存器。设置内存基地址
告诉 DMA:数据要搬到内存里的哪个数组。
例如你定义了uint8_t rx_buf[100];,就写(uint32_t)rx_buf。
DMA 每收到一个数据,就会依次放进rx_buf[0]、rx_buf[1]、rx_buf[2]……
所以内存基地址就是缓冲区的起始地址。设置传输方向
传输方向决定数据是从外设到内存,还是从内存到外设。DMA_DIR_PeripheralSRC:外设是数据源,表示从外设搬到内存,常用于接收。DMA_DIR_PeripheralDST:外设是目的地,表示从内存搬到外设,常用于发送。
设置缓冲区大小
告诉 DMA 一共要搬运多少个数据。例如数组有 100 个元素,就写100。单位由后面的数据宽度决定:如果数据宽度是字节,就是 100 个字节;如果是半字,就是 100 个半字。设置外设地址是否递增
这个要看外设地址是不是同一个寄存器。
像USART1->DR、ADC1->DR都是固定地址,每次都要访问同一个寄存器,所以选择DMA_PeripheralInc_Disable,表示外设地址不递增。
如果外设是一连串寄存器,才需要开启递增。大多数情况下,外设地址都不递增。设置内存地址是否递增
如果数据要依次放进数组的每一个元素,就选择DMA_MemoryInc_Enable。
例如接收数据时,第一个数据放buf[0],第二个放buf[1],第三个放buf[2],所以内存地址要递增。
如果始终只操作同一个内存地址,就选择DMA_MemoryInc_Disable。设置外设数据宽度
告诉 DMA 每次从外设搬运多少数据。Byte:每次搬 1 个字节,常用于 USART。HalfWord:每次搬 2 个字节,常用于 ADC。Word:每次搬 4 个字节,常用于 32 位数据。
外设数据宽度要和外设寄存器宽度匹配,否则数据会错位。
设置内存数据宽度
内存数据宽度通常和外设数据宽度保持一致。
如果外设每次给 1 个字节,内存也按 1 个字节存放;如果外设每次给 2 个字节,内存也按 2 个字节存放。两边宽度不一致时,数据排列会出错。设置工作模式
DMA_Mode_Normal:正常模式。搬运完指定数量的数据后,DMA 自动停止。DMA_Mode_Circular:循环模式。搬运完成后自动回到起始位置继续搬运,适合连续采集数据,例如 ADC 连续采样。
设置通道优先级
当多个 DMA 通道同时请求时,优先级高的先服务。可选 Low、Medium、High、VeryHigh。一般普通外设用 Medium 或 High 即可。设置内存到内存模式
如果不使用内存到内存拷贝,就选择DMA_M2M_Disable。
如果开启DMA_M2M_Enable,DMA 可以不依赖外设,直接把一块内存的数据拷贝到另一块内存,类似于memcpy,但由硬件完成。初始化 DMA 通道
调用DMA_Init(DMAy_Channelx, &DMA_InitStructure),把前面设置的所有参数写入 DMA 通道寄存器。使能 DMA 通道
调用DMA_Cmd(DMAy_Channelx, ENABLE),DMA 通道正式开始工作。只初始化不使能,DMA 不会搬运数据。使能外设的 DMA 请求
仅使能 DMA 通道还不够,还必须让外设主动发出 DMA 请求。
例如串口发送需要调用USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE),ADC 需要调用ADC_DMACmd(ADC1, ENABLE)。
如果不开启外设的 DMA 请求,DMA 通道虽然已经使能,但不会开始传输。
通过网盘分享的文件:ADC +DMA
链接: https://pan.baidu.com/s/1d0BlhWj-LRbAZQ6BX-FIcg?pwd=8888 提取码: 8888
--来自百度网盘超级会员v5的分享
嵌入式外设开发实战流程
粗看手册,先抓框架
- 看芯片参考手册里这个模块是干什么的。
- 重点看:时钟、通道/引脚、寄存器、工作模式、触发条件。
- 不要一开始死磕每个位,先知道“有哪些寄存器、哪些参数要配”。
用 AI 或例程梳理流程
- 让 AI 给出配置步骤,比如 DMA 的:
- 开时钟
- 复位通道
- 配源地址、目标地址、方向、长度、递增、模式
- 使能 DMA
- 使能外设 DMA 请求
- 启动外设
- 这一步不是照抄,而是先建立正确骨架。
- 让 AI 给出配置步骤,比如 DMA 的:
对照手册逐项核对
- 每个参数都要回到手册确认:
- 通道是否对应正确?
- 地址是不是外设数据寄存器?
- 方向是外设到内存,还是内存到外设?
- 数据宽度是否匹配?
- 是否需要循环模式?
- 是否需要中断?
- 这一步最容易发现错误,比如 DMA 方向配反、通道选错、没开外设 DMA 请求。
- 每个参数都要回到手册确认:
写最小可运行代码
- 先只实现核心功能,不要一次加太多东西。
- 比如 DMA + ADC 先只验证:
- 数组能被自动填充;
remain会变化;- 数据会循环刷新。
烧录验证,看现象
- 用串口、LED、示波器、调试器观察。
- 如果现象不对,不要乱改,先判断是哪一步没生效:
- 没开时钟 → 模块不工作;
- 通道没使能 → 不搬运;
- 外设没开 DMA 请求 → 不触发;
- 方向/地址错 → 数据不对。
根据手册微调
- 发现异常后,再回到手册查具体位域。
- 比如 DMA 模式、优先级、中断标志、传输完成判断等。
封装成稳定函数
- 验证通过后,把初始化、启动、状态查询封装成函数。
- 后面换外设时,只要改源地址、方向、通道即可。
套到 DMA 上就是
看手册 → 列步骤 → 写初始化 → 开时钟 → 配通道 → 使能 DMA → 使能外设 DMA 请求 → 启动外设 → 串口/调试验证 → 对照手册修正。
你总结的“看手册 → AI 给流程 → 按手册改”已经抓住了重点,真正拉开差距的地方就在第三步:每个配置项都要回到手册确认,而不是只凭感觉改