不占CPU的GPIO高速采集:K60上PIT触发eDMA完整指南
2026/9/16 9:29:46 网站建设 项目流程

简介:针对NXP Kinetis K60微控制器的GPIO与DMA驱动开发,这份压缩包给出了基于IAR环境的完整参考工程,面向嵌入式底层开发者与备赛学生,用于解决外设驱动配置繁琐、数据搬运占用CPU等问题。工程内附C源码与头文件,可学习引脚方向、上下拉、中断及PORT/PIN/PCR寄存器操作,也能通过DMA通道配置实现内存与外设间的直接传输,理解源/目标地址、传输长度、触发源选择,以及单次、连续等传输模式,适合与LPLD等低功耗负载控制场景结合。包内共6个文件,以IAR工程配置(eww、ewp、ewd、dep)为主,配合.c/.h源码,结构精简、便于直接打开编译调试,整个压缩包仅12KB。已有172人学习浏览,足以说明其对K60入门者有参考价值;对希望深挖K60底层寄存器操作、提升系统实时性与降低功耗的开发者来说,是一份轻量而实用的模板。

1. GPIO 到底能不能走 DMA:Kinetis K60 上的 LPLD_GPIO_DMA 是什么

结论放在最前面:K60 的 GPIO 模块本身没有 DMA 请求线,但通过 PIT 定时器做触发源,eDMA 可以直接读写 GPIO 的 PDIR/PDOR 寄存器,实现不占 CPU 的数字量高速采集或并行输出。网上流传的 LPLD_GPIO_DMA.rar,本质是拉普兰德(LPLD)库围绕这条链路整理的参考工程,价值集中在 DMAMUX 触发映射、minor/major loop 参数和缓冲切换几处。它适合波形录制、逻辑分析、并行总线模拟和脉冲序列输出,和 STM32 上用定时器触发 ADC 多通道 DMA 采集是同一类思路。下文按"触发链路 → 最小工程 → 双缓冲 → 验证"推进,顺手把 BCR 计数、DSR 轮询这几个 K60 特有概念说透。

2. K60 eDMA 与 DMAMUX:GPIO DMA 的触发链路和参数映射

2.1 eDMA 不是普通"搬运工":minor loop 与 major loop 先分清

K60 的 DMA 内核是飞思卡尔/恩智浦的 eDMA,和 STM32 的 DMA 在概念模型上差异明显。STM32 习惯用"通道 + 外设请求 + 传输数量"描述一次事务,eDMA 则把一次任务拆成两层:minor loop 是单次 DMA 请求触发后执行的搬运单位,major loop 是由多个 minor loop 组成的完整任务。这里的表达习惯直接对应 LPLD 结构体里的 minorLoopCount 和 majorLoopCount 两个字段,填反是 GPIO DMA 最常见的初学错误——填反后的典型现场是 DMA 只搬了一个字就停住,或者中断反复触发但缓冲区内容不变化。

对应到运行图景来说:PIT 每来一个脉冲,eDMA 执行一个 minor loop;minor loop 执行到设定次数后,major loop 才算完成。major loop 完成时刻是唯一安全的"切缓冲"时机,也是 DMA 回调函数真正应该触发的时刻。理解这一点,后面双缓冲里的重配置逻辑就顺理成章了。

2.1.1 BCR 寄存器是调试第一入口

eDMA 每个通道的 TCD(传输描述符)结构体里有一个 16 位 BCR(字节计数寄存器),每个 minor loop 结束后按搬移字节数递减。调试时直接读 BCR,能一眼区分"触发没触发"和"搬错字节数"两类问题。下面是最小的读取写法:

// 读取 DMA 通道 0 的 TCD 中的 BCR 字段,剩余字节数为 16 位 uint16_t remaining = DMA0->TCD[0].BCR; printf("remaining: %u bytes\r\n", remaining);

DMA0->TCD[0]对应 DMA 模块第 0 个通道的传输描述符,要查通道 1 就换成DMA0->TCD[1]。在主循环里轮询这个值,它会按采样节奏单调递减;如果一直不动,先查 PIT 有没有跑起来;如果递减速度不对,再查传输宽度和 minorLoopCount。BCR 是 16 位,意味着一个 major loop 最多搬 65535 字节,32 位采样时相当于 16383 个点,这是后面设计缓冲深度时绕不过去的硬上限。

2.2 GPIO 没有 DMA 请求线,触发源从哪来

K60 的 DMA 请求先到 DMAMUX(DMA 多路复用器),由它决定哪个外设请求映射到哪条 DMA 通道,映射关系写在 DMAMUX_CHCFG 寄存器的 SOURCE 字段。问题在于:K60 的请求源列表里有 UART、SPI、I2C、ADC、PIT、TPM、FTM,唯独没有 GPIO——GPIO 端口寄存器不能像串口一样发出"收到新数据"这类握手信号。

常见做法是引入 PIT 定时器当触发源。PIT 通道在计数归零时产生一个周期脉冲,该脉冲进入 DMAMUX 后触发指定 DMA 通道执行一次 minor loop,DMA 随即把 GPIO 数据寄存器里的值搬到内存。采样率完全由 PIT 周期决定,CPU 只需要在 major loop 完成后处理一整块数据。注意一个关键性质:触发源是 PIT 脉冲而不是引脚电平变化,所以采样是固定节拍"拍照",不是事件驱动。如果既要高频采样又要精确记录电平跳变时刻,就得靠 FTM 或 PORT 中断打时间戳,DMA 只负责批量搬运。

提示:PIT 触发脉冲是边沿性质的。DMAMUX 只在脉冲到达的当前时钟周期内产生一次 DMA 请求,所以 PIT 周期必须大于 DMA 完成一个 minor loop 所需的全部总线周期,否则会丢失触发。实践中 PIT 频率预留 20% 余量最稳妥。

2.3 LPLD 库的三模块协作:GPIO、DMA、PIT 各管一段

LPLD(拉普兰德)是面向 Kinetis 系列的外设驱动库,GPIO DMA 场景下它把三个模块拼在一起:

模块职责LPLD 头文件
GPIO配置引脚方向、复用和上下拉,读写 PDIR/PDORLPLD_GPIO.h
DMA配置 eDMA 通道、地址模式、传输宽度和触发源LPLD_DMA.h
PIT产生周期触发脉冲,决定采样率LPLD_PIT.h

三个模块的初始化顺序建议按 GPIO → PIT → DMA 排列。原因是 LPLD_DMA_Init 在内部会把触发源写进 DMAMUX,如果 PIT 还没起来,部分库实现会校验触发源通道状态,导致初始化行为异常;即便不报错,后续也容易出现"复位后前几次触发丢失"的偶发现象。把初始化顺序固定下来,能少踩一个很隐蔽的坑。

3. 用 LPLD_GPIO_DMA 在 K60 上实现 GPIO 高速采样:最小可运行代码

3.1 引脚初始化:先决定采样对象是什么

假设要采集 PTC 端口的低 4 位(PTC0~PTC3)数字量,先用 LPLD_GPIO_Init 把它们配置为输入:

LPLD_GPIO_InitTypeDef gpio_init; gpio_init.GPIOx = PTC; gpio_init.Pin = PIN0 | PIN1 | PIN2 | PIN3; gpio_init.PinControl = GPIO_Input; LPLD_GPIO_Init(gpio_init);

PinControl = GPIO_Input会把对应引脚的方向寄存器 PDDR 位置 0。如果信号源是集电极开路输出,需要利用引脚内部上拉,去操作 PORTx 的 PCR 寄存器里的 PE 位,或者使用库版本提供的 Pull 字段。引脚悬空时必须处理好上下拉,否则 DMA 采回来的是一堆不确定值,排查时很容易误判成 DMA 配置问题,白绕一大圈。

3.2 PIT 触发源的频率计算

以总线时钟 50MHz 为例,要得到 10kHz 采样率,PIT 周期计数值是 50,000,000 / 10,000 = 5000。LPLD_PIT_Init 的第二个参数就是这个计数值,单位是总线时钟周期:

// PIT_CH0,每计数 5000 个总线时钟周期产生一个脉冲,即 10kHz LPLD_PIT_Init(PIT_CH0, 5000, 0); // 如果库版本初始化后未自动使能,补一次显式使能 LPLD_PIT_Enable(PIT_CH0);

第三个参数 0 表示重复模式。如果填成 1(单次模式),PIT 只输出一个脉冲,DMA 搬完一轮后永远不再启动,现象是缓冲区里只有首个数据有效,其余全是初始值。这个字段被填反的频率比想象中高,因为很多其他库的定时器初始化第三个参数是"是否立即启动",语义完全相反,移植代码时容易照搬错。

3.3 DMA 通道配置与 LPLD_DMA_Init 参数逐项说明

核心配置是把 DMA 源指向 PTC 的 PDIR 寄存器,目的指向 RAM 缓冲区:

#define SAMPLE_NUM 128 uint32_t sample_buf[SAMPLE_NUM]; void GpioDma_Init(void) { DMA_ConfigType dma_cfg; dma_cfg.ch = DMA_CH0; // 源:PTC 端口输入数据寄存器,地址固定 dma_cfg.srcAddr = (uint32_t)&PTC->PDIR; dma_cfg.srcAddrMode = DMA_ADDR_FIXED; dma_cfg.srcTransferSize = DMA_WIDTH_32BIT; // 目的:RAM 缓冲区,地址递增 dma_cfg.dstAddr = (uint32_t)sample_buf; dma_cfg.dstAddrMode = DMA_ADDR_INC; dma_cfg.dstTransferSize = DMA_WIDTH_32BIT; // 每触发一次搬 1 个 32 位字,搬满 SAMPLE_NUM 次完成一个大循环 dma_cfg.minorLoopCount = 1; dma_cfg.majorLoopCount = SAMPLE_NUM; // 触发源指向 PIT_CH0 的脉冲 dma_cfg.triggerSource = DMA_REQ_PIT_CH0; LPLD_DMA_Init(&dma_cfg, GpioDma_Callback); }

各字段的取值理由和常见错误整理如下:

字段本场景取值常见错误与后果
srcAddrModeDMA_ADDR_FIXED写成 INC,源地址乱跳,采到 PDIR 附近的随机寄存器
dstAddrModeDMA_ADDR_INC写成 FIXED,缓冲区始终只写首地址
srcTransferSizeDMA_WIDTH_32BIT用 8 位宽度读 PDIR,高 24 位丢失
minorLoopCount1填 2 以上,一次触发连搬多字,采样率失真
triggerSourceDMA_REQ_PIT_CH0填成 PIT_CH1 而 PIT1 未初始化,永远不触发

DMA_REQ_PIT_CH0是 DMAMUX SOURCE 字段的枚举名,具体写法以所用 LPLD_DMA.h 版本为准,常见变体还有 DMAMUX_PIT_CH0,含义相同。源地址坚持用 32 位宽度:PDIR 是 32 位寄存器,即使只关心低 4 位,也建议整个寄存器搬回内存再做掩码,因为 8 位或 16 位访问在总线层会被拆成多次操作,对采样时序没有任何好处,只会多一个出错点。

3.4 回调函数里处理"搬完一轮"

major loop 完成后 eDMA 置位完成标志并进入中断,LPLD_DMA_Init 注册的回调被调用。回调属于中断上下文,只做标志位更新:

volatile uint8_t dma_done = 0; void GpioDma_Callback(void) { dma_done = 1; // 只置标志,不做耗时处理 } int main(void) { // 按 GPIO -> PIT -> DMA 的顺序完成初始化 GpioDma_Init(); for (;;) { if (dma_done) { dma_done = 0; ProcessSamples(sample_buf, SAMPLE_NUM); // 重新启动 DMA,开始下一轮 major loop LPLD_DMA_Enable(DMA_CH0); } } }

关键在最后一行:LPLD_DMA_Enable 重新激活通道。eDMA 的通道在 major loop 结束后处于暂停状态,PIT 脉冲照常发出但 DMA 不响应,必须显式重新使能。如果忘记这一步,现象非常迷惑——程序跑起来第一次数据完整,之后缓冲区永远不再更新。

4. 连续采集与双缓冲:DMA continuous requests 场景下的 K60 工程实践

4.1 单缓冲的停顿问题:处理数据时采样点全丢

第 3 章的工程是"采 128 点 → 停下 → 处理 → 再采",处理期间 PIT 继续发脉冲,DMA 通道却被停用,这段时间的采样点全部丢失。对一次性抓波形够用,对长时间连续采集不行。理论上把 ProcessSamples 的执行时间压缩进两个 major loop 的间隔里可以做到无缝,但处理时间一旦抖动,丢采样点的问题立刻暴露。

解决思路是双缓冲,也叫 ping-pong 缓冲:两块缓冲区轮流接数据,DMA 写 A 时 CPU 处理 B,major loop 完成后交换。K60 的 eDMA 不要求"一通道一固定目的地址",只要在 major loop 完成时刻切换目的地址即可。LPLD 没有把 TCD 重载暴露成专门 API,常见做法是在回调里重新调用 LPLD_DMA_Init 并修改 dstAddr:

#define BUF_LEN 256 uint32_t buf_a[BUF_LEN]; uint32_t buf_b[BUF_LEN]; uint32_t *volatile dma_cur = buf_a; uint32_t *volatile data_ready = buf_b; void GpioDma_Callback(void) { // 交换读写角色:当前 DMA 正在写的那块,变成就绪块 data_ready = dma_cur; dma_cur = (dma_cur == buf_a) ? buf_b : buf_a; // 重建配置,仅目的地址变化 DMA_ConfigType cfg = DefaultDmaCfg; cfg.dstAddr = (uint32_t)dma_cur; LPLD_DMA_Init(&cfg, GpioDma_Callback); }

DefaultDmaCfg是提前保存的一份完整配置副本,回调里只改 dstAddr 一个字段。这种重配方式本身很快,只花几十个总线周期,对毫秒级采样毫无压力;但如果 PIT 频率超过 1MHz,重配开销开始明显,就该考虑 scatter/gather 方式——eDMA 的 DLAST_SGA 字段可以在 major loop 结束时由硬件自行加载下一份 TCD,CPU 完全不参与。LPLD 没有封装这一层,需要自己维护 TCD 数组,这是连续采集做到极限吞吐时的进阶路线。

4.1.1 重配置 DMA 会重置通道状态,只能在完成点做

重新调用 LPLD_DMA_Init 会重置整个通道的 TCD、BCR 和中断标志。这件事必须发生在 major loop 完成之后、新一轮数据搬运开始之前。回调函数天然满足这个时机,因为回调就是从完成中断进来的。如果把重配置移到主循环轮询里做,主循环执行时机不确定,可能撞上"上一轮还没搬完、新一轮已经开始"的竞态。这个竞态的典型现象是缓冲区尾部偶尔出现几个旧值,且出现频率不稳定——和 ADC 多通道 DMA 数据紊乱的经典案例表现相似,排查方向都在"底层驱动里有没有在下一次请求前碰过通道使能位"。

4.2 用 DMA 中断切缓冲,还是主循环轮询 DSR

切缓冲的动作可以放回调里,也可以放主循环里读 DSR 寄存器。后者不依赖中断:

// DSR 的 DONE 位(bit30)置位表示有通道完成 major loop if (DMA0->DSR & DMA_DSR_DONE_MASK) { // 清除 DONE 位:向 CDNE 寄存器写入完成通道号 DMA0->CDNE = 0; SwapBuffers(); }

两种方式的选择依据是 CPU 占用率和响应时延。DMA 中断方式时延最小,框架里就是 LPLD_DMA_Init 传入的回调,入口出口有固定开销,回调里不能做重活;轮询方式代码直观、天然嵌入主循环,几百 kHz 以下的采样率完全够用,代价是处理时延取决于主循环周期。工程上如果主循环里已有若干耗时任务,优先用中断方式;如果主循环极简、只有状态机跳转,轮询更省事。

4.3 采样率、缓冲区深度与 BCR 的计算对照

连续采集的参数设计通常从"一次 major loop 持续多久"倒推。32 位宽度、minorLoopCount=1 时:

PIT 触发频率缓冲区长度(字)一轮持续时间数据量
1 kHz128128 ms512 B
10 kHz25625.6 ms1 KB
100 kHz102410.24 ms4 KB
1 MHz40964.096 ms16 KB

数据量 = 缓冲区长度 × 4,这个值受两个上限约束:一是 BCR 的 16 位宽度,单轮最多 65535 字节;二是 K60 的 SRAM 分布在多个物理块,缓冲区不能跨越不连续的块地址,否则 DMA 地址递增会跳变,采样数据出现重复或乱序。检查链接脚本里缓冲区落在哪一块 SRAM,必要时用链接器指令强制放到连续区域。

5. 验证 GPIO DMA 采集结果的三个技巧,和一个经常被忽略的坑

5.1 用固定电平先验证地址与字节序

先用跳线把 PTC0~PTC3 接到确定电平(全部接 GND 或全部接 VDD),跑一轮 DMA,检查 sample_buf 的值。PDIR 是 32 位寄存器,PTC0 对应 bit0,PTC3 对应 bit3。读出值与预期不符时,先检查引脚编号是否写反,再检查 DMA 源地址是不是&PTC->PDIR。这一步能把"引脚配置错误"和"DMA 配置错误"两个大类的故障分离,比直接上波形快得多。

5.2 用 PWM 方波验证采样时间戳的均匀性

把任意 PWM 信号接到被测引脚,DMA 采集 4096 个点,主循环遍历 sample_buf 统计相邻跳变之间的采样点间隔。间隔最小值除以采样周期就是该配置下能可靠分辨的最短脉冲宽度;间隔方差过大说明触发抖动明显,优先检查 DMAMUX 里有没有其他通道抢占请求,并把不用的 DMA 通道全部 Disable 后再测。反过来做 PWM DMA 输出时,把波形表从内存搬到 PDOR,同一套 BCR 观测法也能验证输出波形节拍是否均匀。

5.3 用 BCR 剩余值判断触发是否丢失

在处理回调或主循环里顺手打印 BCR:

uint16_t remain = DMA0->TCD[0].BCR; printf("BCR remaining: %u\r\n", remain);

如果一轮处理开始时 remain 不为 0,说明上一轮 major loop 没跑满,中间丢过触发。原因通常是 PIT 频率设得高于实际处理能力,或者重配置 DMA 的时机晚了。此时先降 PIT 频率验证,再检查双缓冲切换逻辑,而不是盲目加长缓冲区。

5.4 一个经常被忽略的坑:DMAMUX 的 ENBL 位

第一轮正常、后续偶发丢数的场景,排查到最后往往落在 DMAMUX_CHCFG 的 ENBL 位。eDMA 在 major loop 结束时把 CSR 的 DONE 位置 1,若 ENBL 位被错误配置为"每次请求后自动撤销使能",通道会在第一轮完成后被 DMAMUX 解除使能,后续 PIT 脉冲全部落空。LPLD_DMA_Init 默认把 ENBL 位写 1,正常使用不会碰到;一旦为了性能绕过库直接改写 TCD 做 scatter/gather,就必须在每次 TCD 更新时同步把 ENBL 位置回 1。手动配置时核对DMAMUX->CHCFG[ch]的最高位,这是 GPIO DMA 从"能跑"到"长时间稳定跑"的分水岭。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询