资讯动态

DMA详解:从工作原理到串口/ADC实战与常见坑

发布时间:2026/9/8 12:46:03 来源:尧图企业网站定制
1. 先搞懂 DMA 到底在解决什么问题1.1 没有 DMA 的时候数据搬运有多痛苦很多刚接触 DMA 的人觉得它就是一个“自动拷数据”的硬件模块配置好通道、地址、长度它就在后台吭哧吭哧帮你把数据搬完然后触发一个中断告诉你“完事了”。这个理解方向是对的但如果你只停留在“会用”的层面一旦遇到数据错位、传输卡死、和 CPU 抢总线这类问题你会完全没有排查思路。先看没有 DMA 的时候一次最简单的串口接收要经历什么CPU 不断查询串口状态寄存器发现接收数据寄存器非空就把它读出来放进内存数组。如果是低速串口、少量数据这样做没问题。可是当数据变成连续高频的中断接收、ADC 多通道高速采样、或者需要搬运大块数据到外设存储时CPU 几乎整段整段地被绑死在“读一下寄存器写一下内存”这种低效操作上。更难受的是高频中断还会打断正在执行的关键任务导致其他逻辑出现时序抖动。DMA 解决的就是这件事它作为一个独立的总线主设备可以直接在内存和外设之间搬运数据搬运过程中不需要 CPU 一条指令一条指令地干预。CPU 只需要在开始的时候告诉 DMA“源地址在哪、目标地址在哪、要搬多少、每次搬多宽”然后就可以去干别的了。数据搬完DMA 再来报告结果。这就是 DMADirect Memory Access直接存储器访问最基本的存在意义。1.2 DMA 的工作流程一次传输到底发生了什么要真正“读懂 DMA”我建议你把一次完整传输拆成六个阶段来看而不是只记那几个配置函数。第一阶段是初始化配置你要设定数据从哪里来、到哪里去、搬多少、按什么模式搬。第二阶段是使能启动CPU 把 DMA 通道的使能位置一DMA 进入等待状态。第三阶段是请求响应外设或者软件发出传输请求比如串口接收寄存器出现新数据、ADC 转换完成、定时器触发脉冲来了。第四阶段是总线仲裁和搬运执行。DMA 拿到请求后会作为总线主设备向总线仲裁器申请总线使用权。如果此时 CPU 也在访问总线双方需要进行仲裁这涉及优先级配置。拿到总线后DMA 把数据从源地址读出、写入目标地址同时传输计数器自动减一。第五阶段是重复判断计数器不为零就继续等待下一次请求为零则说明整块数据传输完成。第六阶段是完成报告DMA 置起传输完成标志如果使能了中断就会触发中断回调给 CPU。这个流程里最容易忽略的其实是第三阶段和第四阶段。我见过不少人在配置 DMA 后直接往发送缓冲区写数据结果数据乱飞原因就是 DMA 还没被外设请求触发缓冲区就被改写了。还有一类问题是 DMA 和 CPU 同时访问同一个存储区域导致数据撕裂这在双核芯片、或者带缓存一致性要求的高性能 SoC 上尤其明显。所以你在学习一个芯片的 DMA 时一定要先看总线连接框图搞清楚 DMA 控制器挂在哪个总线上它访问外设寄存器和内存走的是一条什么路径。1.3 传输方向、搬运宽度和一次搬多少三个关键参数DMA 的三大核心参数我习惯用“搬家”来类比。传输方向就是“从哪个房间搬到哪个房间”常见的有外设到内存、内存到外设、内存到内存三种。搬运宽度就是“每次搬多大的箱子”常见配置是字节8 位、半字16 位、字32 位。一次搬多少就是“总共要搬多少件行李”对应 DMA 的传输计数器。这三个参数必须和外设的数据寄存器宽度严格匹配否则会出现数据错位。最典型的例子是 ADC 用 DMA 搬运采样结果如果 ADC 数据寄存器是 16 位你的 DMA 搬运宽度也必须是半字如果用字节宽度搬 16 位数据每一次搬运都会把高字节和低字节拆开得到的结果完全没法看。反过来SPI 的 FIFO 是 8 位还是 16 位也会直接影响 DMA 配置。很多“为什么我 DMA 采出来的数据全乱”的问题根源就在这里。2. 传输模式四种类型怎么选2.1 单次传输模式最简单也最好理解单次传输模式是 DMA 最基础的工作方式每收到一次外设请求就搬运一个传输单元一个字节、半字或字搬运完成后计数器减一减到零就停止并产生完成事件。这种模式特别适合并发量小、要求简单的外设比如低速串口按帧接收固定字节数。它的优点是好调试、逻辑清晰出了问题时只要看计数器值就能定位。缺点是每搬一个单元都需要一次外设请求如果外设请求频率不高整体传输效率会偏低。另外在单次模式下如果外设请求来得比 DMA 搬运速度快可能出现请求丢失或数据覆盖这个在高速外设场景下要特别注意。2.2 块传输模式适合需要一次性搬运大块数据的场景块传输模式下DMA 在一段连续的传输过程中会一次性搬运多个传输单元。比如一个块的大小是 16 个字节那么 DMA 每触发一次就连搬 16 个字节如果一共有 4 个块则总共搬运 64 个字节。块传输的好处是减少了请求交互次数提高了数据吞吐量。适合 SD 卡读写、Flash 读写、USB 控制传输这类一次需要移动大量数据的场景。但它的代价是连续搬运期间会持续占用总线如果此时 CPU 有实时性要求很高的任务在跑可能会出现延迟。所以块大小不是越大越好要根据系统实时性要求做取舍这也是我在实际项目中反复验证过的一个点。2.3 突发传输模式高带宽外设的必经之路突发传输模式可以看作块传输的强化版DMA 会连续占用总线完成一个固定长度比如 4、8、16 个传输单元的搬运序列中间不被其他请求打断。这个模式对 UFS、SD、USB、以太网这类需要高带宽持续读写数据的设备特别关键因为它们的数据吞吐量动不动就是几百 MB/s 甚至几 GB/s。我最初接触“DMA 突发传输”这个概念是在做 UFS 相关调试的时候。UFS 协议本身对数据搬运的吞吐量和连续性要求很高如果 DMA 不能按突发方式连续读取/写入内存上的数据缓冲区整个链路的带宽就会断崖式下降。后来在做 MCU 上的 SPI Flash 加速读写时也遇到了类似情况用单次模式读快闪速度上不去改成 16 字节突发后读写速率明显提升。但要注意突发传输对总线带宽占用极大使能之前务必评估系统中其它外设的时序要求否则很容易导致低优先级中断响应被拖垮。2.4 循环模式与 DMA continuous requests 的坑循环模式是嵌入式开发中用得最多、也最容易出问题的模式。它的特点是 DMA 搬完一块数据后地址和计数器自动回卷到初始值继续新一轮传输不需要 CPU 重新配置非常适合串口不定长接收、ADC 连续采样、麦克风数据采集等场景。和循环模式经常一起出现的一个术语是“continuous requests”有些芯片叫“连续请求模式”有些地方叫“往返模式”。它的核心含义是当 DMA 计数器归零后是停止并等待下一次触发还是自动重新加载配置并继续请求传输。如果是后者DMA 就会一直占用总线资源进行连续搬运。这个特性在某些场景下很方便但也有个大坑如果你开启了 continuous requests又没有在硬件上限制外设请求的节奏DMA 可能会持续霸占总线或者持续搬运无效数据导致 CPU 明显卡顿、其它中断被饿死。我调试过一个案子就是误开了连续请求结果系统整体响应变慢最后用示波器量到 DMA 连续访问总线的电平波形才定位到问题。3. 串口 DMA 接收不定长数据的经典方案3.1 为什么必须靠 DMA 加空闲中断来收不定长数据串口接收定长数据最简单的做法就是 DMA 固定搬运 N 个字节搬运完成产生中断。但实际调试最多的场景是“不知道一帧数据有多长”比如 Modbus RTU、私有协议、GPS 上报数据。这种情况下DMA 不知道什么时候是结尾如果一直配置搬运固定长度要么数据还没发完就开始误处理要么一帧结束后没办法在正确的时机做解析。业界经典的解法是“DMA 循环接收 串口空闲中断IDLE”。串口空闲中断表示“总线上超过一个字节时间没有新数据”也就是目前这一帧数据大概率已经收完了。此时 CPU 读取 DMA 当前剩余的计数器值用缓冲区总长度减去剩余计数就能算出本次究竟收到了多少个字节然后按这个长度去解析数据。3.2 空闲中断加 DMA 的完整流程整个流程分这么几步。第一步配置串口接收 DMA 为循环模式缓冲区是一个环形数组DMA 写指针自动循环写。第二步开启串口空闲中断同时保证 DMA 通道的接收事件不会产生干扰中断。第三步每当总线上一个字节后的空闲时间到来CPU 进入空闲中断回调在中断里读取 DMA 当前计数值。第四步最关键计算有效数据长度。举例来说DMA 缓冲区总长度是 256 字节当前 DMA 剩余计数是 180接收数据长度就是 256 - 180 76 字节。但这 76 字节并不一定是从缓冲区开头开始的因为 DMA 的写指针在循环移动可能是从某个中间位置连续写到缓冲区末尾再绕回到开头也可能整个 76 字节都连续存在某个区间内。如果你不处理换行回绕解析时就会读到跨越缓冲区边界的数据导致帧头帧尾错乱。正确做法是分两段拷贝或者直接用环形队列的方式记录读索引和写索引解析时按环形队列逻辑处理。第五步是协议解析。解析完成后缓冲区可以不清空交给下一轮 DMA 继续循环写入或者在某些场景下需要主动重设 DMA 计数器让下一帧从固定位置重新开始。3.3 一个能直接抄的 py32f003 串口 DMA 接收参考例程py32f003 是最近项目里用过的一颗小封装 MCU它的 DMA 资源和主流 STM32 类似。网上不少人问“py32f003 使用串口 DMA 方式接收通讯数据使用接收空闲中断判断接收结束给下参考例程”这里结合我用过的方式给一个可直接参考的流程和伪代码重点是架构思路具体寄存器映射各芯片略有差异。初始化部分核心是三步使能 DMA 时钟和串口时钟、配置 ADC 时钟如果你也用 ADC、配置 DMA 通道。串口接收 DMA 配置为循环模式数据宽度为字节源地址是串口数据寄存器地址目标地址是接收缓冲区数组地址传输长度为缓冲区大小。然后开启串口的空闲中断和 DMA 接收请求映射。#define RX_BUF_SIZE 256 uint8_t uart_rx_buf[RX_BUF_SIZE]; void uart_dma_init(void) { // 使能 DMA 时钟 RCC-AHBENR | RCC_AHBENR_DMA1EN; // 配置 DMA 通道循环模式、外设到内存、字节宽度 DMA1_Channel-CPAR (uint32_t)USART1-RDR; // 源地址串口数据寄存器 DMA1_Channel-CMAR (uint32_t)uart_rx_buf; // 目标地址接收缓冲区 DMA1_Channel-CNDTR RX_BUF_SIZE; // 传输长度缓冲区大小 DMA1_Channel-CCR DMA_CCR_CIRC | DMA_CCR_MINC | DMA_CCR_PINC | DMA_CCR_PSIZE_8BIT | DMA_CCR_MSIZE_8BIT | DMA_CCR_DIR_P2M | DMA_CCR_EN; } void USART1_IRQHandler(void) { if (USART1-ISR USART_ISR_IDLE) { USART1-ICR | USART_ISR_IDLE; // 清除空闲标志 volatile uint32_t remain DMA1_Channel-CNDTR; // 读取剩余计数 uint16_t rx_len RX_BUF_SIZE - remain; // 计算有效长度 // 注意此处应拷贝数据并做帧解析建议将拷贝和解析放到主循环或任务中 protocol_parse_ring_buffer(uart_rx_buf, rx_len, remain); } }这段代码有几个细节必须强调。第一读取 CNDTR 必须在空闲中断里尽早完成因为下一个字节如果来得太快DMA 可能已经更新了计数导致长度计算错误。第二清除空闲标志的时机很讲究一定要在读完计数器后再清某些芯片清除标志的时序如果不对会误触发下一次空闲中断。第三实际解析不要直接在主中断里做长耗时操作而是把已经收到的数据记录下来等出中断后再处理。还有一点经验很多人在初始化 DMA 循环接收后会忘记在 DMA 中断里做任何处理结果缓冲区被覆盖了都没发现。串口 DMA 循环模式本身不产生完成中断除非你配置了半传输中断所以“我明明开启了 DMA为什么永远不知道数据来了”这类问题的答案往往是你需要靠串口空闲中断来通知数据到达而不是依赖 DMA 完成中断。4. ADC 加 DMA多通道采样的正确姿势4.1 单通道多次采样和四通道循环采样ADC 用 DMA 的场景大致分两类。第一类是单通道多次采样为了平均值滤波或者提升信噪比连续采样几百次甚至上千次由 DMA 把采样结果依次存入数组采完后统一处理。第二类是多通道循环采样比如四通道电压采集、电位器加传感器组合采集ADC 按照通道顺序依次转换DMA 把转换结果依次放进对应的数组位置。我最初做 ADC 多通道 DMA 采样时犯过一个非常低级的错误只配置了 ADC 的扫描模式却没有设置 DMA 循环模式。结果就是采样一轮后 DMA 停止后面通道的数据永远不更新。正确的做法是ADC 开启扫描模式和连续转换模式DMA 开启循环模式这样 ADC 才会一直循环扫描四个通道DMA 才会一直把结果搬进数组。这个组合是 ADC 加 DMA 场景的核心前提缺一个都不行。4.2 HAL 库配置 ADC DMA 的要点与对齐问题STM32 HAL 库让 ADC DMA 配置简化了不少但自动化也掩盖了很多细节。以单通道多次采样为例你调用HAL_ADC_Start_DMA(hadc, (uint32_t*)adc_buf, sample_count)之前必须确认以下几个参数ADC 数据对齐方式、DMA 数据宽度、缓冲区元素的类型。ADC 数据寄存器通常是 16 位所以 DMA 搬运宽度必须选半字缓冲区元素也要用uint16_t。如果你用uint32_t数组并让 DMA 宽度和 ADC 对齐有些平台会出现数据堆叠错误因为每次写入 32 位宽度会把两次转换结果拼在一起。另一个容易踩的坑是多通道模式下 DMA 缓冲区大小必须覆盖所有通道比如四通道缓冲区至少要有四个元素否则 DMA 传输长度小于 ADC 通道数时后面的通道结果根本写不进去。四通道的场景我举个例子配置好 ADC 的四个通道顺序比如 CH0、CH1、CH2、CH3设置扫描模式设置 DMA 为循环模式、半字宽度、传输长度为 4。每次一轮扫描完成后DMA 计数器减到 0 再自动回卷四个通道的结果就会循环写入数组的 0 到 3 号位置。如果使用中断可以开启 DMA 半传输中断或传输完成中断在回调里读取数组并处理。4.3 DMA 传输完成中断和采样频率计算很多做信号采集的工程师关心得最多的是“我到底能以多高的频率连续采样”。这个涉及到 ADC 采样时钟、转换周期和 DMA 搬运能力之间的配合。单个通道的单次转换时间约等于采样周期加上固定转换周期比如采样周期 1.5 个 ADC 时钟转换周期 12.5 个 ADC 时钟总的就是 14 个 ADC 时钟。如果 ADC 时钟 14 MHz单次转换大约 1 微秒理论上采样率可以到 1 MHz。这时候 DMA 必须能在 1 微秒内完成一次数据搬运。DMA 搬运一个半字数据通常只需要几个 AHB 时钟周期所以大多数场景下 DMA 不会成为瓶颈。但如果 DMA 总线被其它高优先级外设长期占用或者你在中断回调里做了太多耗时操作DMA 的持续读取就会受到影响采集结果会出现周期性的数据缺席。实测中我建议你做一个简单的带宽测试将 DMA 缓冲区可以大一些开启传输完成中断在主循环里给一个 GPIO 翻转用示波器量 GPIO 翻转频率就能估算实际的采样中断频率。如果发现实际频率远低于理论值优先排查中断处理和 DMA 总线占用问题而不是怀疑 ADC 本身。5. DMA 发送需不需要等待上一轮发完5.1 不同芯片、不同外设的实现差异这是热词里反复出现的问题“DMA 串口发送需要等待上一轮数据发送完吗”。直接回答是需要而且必须在写新数据前确认上一次发送已经结束否则会出现数据覆盖、发送截断、甚至总线冲突。但不同芯片、不同库判断方式完全不一样所以网上的答案才会千奇百怪。早期 STM32 标准库时代很多人会在启动一次 DMA 发送前检查某个传输完成标志比如DMA_GetFlagStatus(DMA1_FLAG_TC5)。HAL 库里则习惯通过HAL_UART_TxCpltCallback回调维护一个“发送完毕”标志。更简单粗暴的做法是直接在发送函数里加个超时等待等到 DMA 对应的传输完成中断标志置位才返回。这种做法的缺点是会阻塞主流程但优点是逻辑简单可靠。在串口 DMA 发送场景里还有一个细节发送完成后串口移位寄存器里可能还有最后一个字节没送出去。DMA 把数据全部搬到串口数据寄存器只代表“发送 FIFO 已满”不代表物理线路上的最后一个位已经发送完。如果此时立刻切换 RS485 方向引脚比如将 DE 置低可能会把最后一个字节的半截截掉接收端收到一个错误的帧。这个问题在 Modbus、私有 RS485 协议里非常常见后面我还会专门提。5.2 怎么判断 DMA 发送真正结束我在实际项目中总结了一套判断发送真正结束的方案分三个层面。第一层是 DMA 层的传输结束对应 DMA 通道的传输完成标志说明 DMA 已经把所有数据从内存搬到了外设的数据寄存器。第二层是外设层的发送结束通过读取串口状态寄存器里的发送完成标志如 USART_ISR_TC来判断确认最后一个字节已经从移位寄存器发送完毕线路已经空闲。第三层是应用层的业务确认如果你在做 Modbus/RS485 这类需要控制收发方向切换的通信应该在第二层结束后再延迟几十微秒或者一个字节时间再切换方向引脚。void uart_dma_send_blocking(uint8_t *buf, uint16_t len) { // 1. 等待上一次 DMA 发送完成 while (dma_tx_busy) { /* 等待超时处理 */ } // 2. 启动本次 DMA 发送 dma_tx_busy 1; HAL_UART_Transmit_DMA(huart, buf, len); // 3. 等 DMA 搬运完成 while (dma_tx_busy) { /* 等待超时处理 */ } // 4. 等待串口移位寄存器发送完最后一个字节 while (!__HAL_UART_GET_FLAG(huart, UART_FLAG_TC)) { /* 等待超时处理 */ } // 5. 此时才允许切 RS485 方向或修改 buf 内容 }这套流程看起来麻烦但实际非常稳。我测试过很多次如果不做第 4 步RS485 方向切换早了最后一个字节丢失的几率在低速波特率下几乎接近百分之百。波特率越高这种问题越隐蔽因为时间窗口很小不容易被主观感知但接收端的数据校验会频繁失败。5.3 和 FreeModbus 这类协议栈配合时的注意点FreeModbus 是嵌入式里非常常用的开源 Modbus 协议栈很多人把它和串口 DMA 配合使用时会踩坑。核心原因是 FreeModbus 的事件循环默认是“查询式”的它通过串口中断或轮询来接收数据如果用 DMA 接管了接收路径协议栈本身感知不到数据到达必须额外做一层桥接。我建议的方式是DMA 接收完成后把解析得到的一帧完整数据拷贝到一个协议栈缓冲区然后调用eMBPoll()或者对应的接收事件处理函数。同时要注意Modbus RTU 的帧间隔要求是 3.5 个字符时间如果你的 DMA 空闲中断响应不够及时或者中断延迟过大有可能影响帧定界判断。DMA 发送时也要注意我上面提到的方向切换时序问题否则总线上的设备经常收不到响应帧排查起来会非常痛苦。6. 常见问题与排查技巧实录6.1 bat32mcu DMA 通道的详解以及容易踩的坑近期不少人在问 bat32mcu 的 DMA因为它也是国产 MCU 里出货量比较大的系列。bat32mcu 的 DMA 通道数量和外设映射关系和 STM32 有相似但不完全相同的地方。最大的坑是外设和 DMA 通道的映射表不是所有外设都能任意接到所有 DMA 通道必须查数据手册里的请求映射表否则即使 DMA 配置正确外设也不会有任何请求到来。另一个常见问题是 bat32mcu 的 DMA 在某些低功耗模式下会被禁用唤醒后 DMA 配置丢失需要重新初始化。如果你在做低功耗产品一定要在休眠唤醒流程里重新调用 DMA 初始化函数否则下次外设触发 DMA 时会静默失效表现就是“为什么唤醒后串口收不了数据”。这种问题没有报错、没有中断排查难度很高。我当时的做法是把 DMA 初始化函数提取出来在每次唤醒后的统一初始化路径中调用经过这一改动问题彻底消失。6.2 DMA 测速软件到底在测什么“DMA 测速”这个需求通常出现在需要评估 DMA 在某一平台上的实际带宽、确认 DMA 是否成为性能瓶颈的时候。PC 和服务器平台上有一些专门的 DMA 测试工具会通过驱动接口发起 DMA 请求然后测量数据搬运速率。嵌入式平台则比较简单粗暴一般是在内存里准备两个大缓冲区配置 DMA 进行内存到内存的大块搬运同时用定时器计时最终算出有效带宽。测试结果要有参考价值必须注意两点。第一内存到内存的 DMA 搬运速度和应用场景里的外设搬运速度两者往往差很多因为外设请求频率、总线仲裁都会影响实际速率。第二测试缓冲区必须做地址对齐和缓存一致性处理否则在有缓存但缺少一致性维护机制的平台测出来的数据会有很大偏差。我见过有人用 DMA 搬运 64 KB 数据结果是源目标都在 cache 里真正落到内存的数据只有一小部分测出的带宽虚高误导了后续架构设计。6.3 UFS DMA 和分布式 DMA 是怎么回事UFS DMA 这个关键词看起来和 MCU 上的 DMA 是两回事本质上却殊途同归。UFS 控制器内部有一套 DMA 引擎负责把存储介质里的数据搬到系统内存或者把内存数据写入存储介质。UFS 的 DMA 一般基于描述符机制主机驱动程序准备好一组 DMA 描述符每个描述符包含地址、长度、标志位UFS 控制器按顺序解析并执行描述符完成数据搬运后更新完成状态。这种机制的吞吐量非常高适合大容量存储场景。分布式 DMA 则是另一个层次的概念常见于多核处理器或带 IOMMU/SMMU 的 SoC 中。它的核心思路是让不同的总线主设备各自拥有独立的 DMA 地址空间由系统级 IOMMU 把设备地址映射到物理内存。这样每个设备可以在自己的虚拟地址空间里布置 DMA 缓冲区避免互相干扰。如果你在嵌入式 Linux 驱动里看到dma_alloc_coherent、dma_map_single这些接口背后基本就是分布式/IOMMU DMA 体系在支撑。6.4 疑难杂症排查清单最后把我在实际调试中攒下的排查经验整理成一份清单遇到 DMA 相关问题可以直接对照排查。症状可能原因排查方向数据全乱或错位DMA 搬运宽度与外设寄存器宽度不匹配检查 PSIZE/MSIZE确认字节/半字/字对齐数据少一段或多一段传输计数器配置错误或者缓冲区长度与协议帧长不一致核对 CNDTR/NDTR 和实际需求长度发送数据被截断未等待串口 TC 标志就切换方向或释放缓冲区加“等待发送完成”逻辑完成中断不触发使能位没打开或 NVIC 优先级配置错误检查 DMA 中断使能、NVIC、回调注册接收一段时间后停止循环模式未开启或者计数回卷逻辑错误确认 DMA 是否配置为循环/连续模式CPU 明显卡顿continuous requests 持续占用总线限制 DMA 请求节奏或关闭连续请求低功耗唤醒后失效DMA 配置在睡眠期间丢失唤醒后重新初始化 DMA数据被 cache 不一致影响有缓存但 DMA 缓冲未做一致性处理使用一致性 API 或执行 cache clean/invalidate总线冲突导致数据撕裂DMA 与 CPU 同时访问同一内存区域锁机制、内存隔离或协议层面加校验排查 DMA 问题我还有一个笨但好用的方法先用逻辑分析仪或者示波器抓取外设端的请求信号/数据引脚波形确认外设确实在正常工作然后再把问题的范围缩小到 DMA 环节。很多时候“DMA 不工作”的真相是外设根本没有产生请求这跟 DMA 本身的配置毫无关系。最后再分享一个小技巧。任何 DMA 调试我都建议在缓冲区里填一个固定的初始化值比如 0xA5。这样数据搬完以后打开内存窗口看哪些地方变了你就能一目了然地确认搬运轨迹。这个办法帮我定位过至少三四个看似诡异的问题每次都能从数据填充模式里看出端倪比反复看寄存器快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价