新闻详情

新闻详情

首页 / 资讯中心 / 详情

MicroPython下DMA链式触发与Scatter-Gather采集实战

发布时间:2026/9/8 7:08:13来源:尧图网络
MicroPython下DMA链式触发与Scatter-Gather采集实战
Micropython跑着DMA链式触发和Scatter-Gather这话放到三年前我肯定觉得是怪谈。MicroPython的优势是业务逻辑写得快底层数据搬运本来就不是它的菜但前段时间一个多通道ADC采样项目把我给逼到了墙角四路传感器每路要2kHz连续采样数据量不算离谱可要是用纯Python在中断里搬数据CPU占用直接暴涨到没法看。后来把DMA拎出来做Scatter-Gather数据聚合才把这个死局真正解开。这篇文章就聊聊我在MicroPython环境里实现DMA链式触发的完整过程从原理、控制块结构、C扩展实现到踩坑记录一次讲清楚。项目本身不复杂但把“Python字节码 底层DMA”这两个世界的东西缝在一起过程很能代表嵌入式场景里做高性能采集的通用思路。如果你正准备给MicroPython设备做多通道数据采集、串口不定长接收或者纯粹想搞明白Scatter-Gather具体是怎么一回事这篇文章应该能帮你少走不少弯路。1. 为什么在MicroPython里折腾DMA链式触发1.1 MicroPython的性能边界在哪先说一个扎心的现实MicroPython的本质是运行在MCU上的Python解释器一边解析字节码一边执行。业务逻辑、状态机、协议解析这些用它写确实舒服但一旦涉及“高频、重复、大批量”的数据搬运解释执行的开销就变得非常刺眼。举个例子。一块168MHz的STM32F4芯片用Python写个for循环把一个8000字节的bytearray逐个元素拷贝到另一个缓冲区我实测要花好几毫秒。放在数据采集场景里几毫秒意味着至少几百个采样点被CPU占用期间漏掉了。有人会说那我在C中断里直接搬不就行了可以但中断里做大量拷贝很容易把系统实时性拖垮而且多个外设同时需要搬运数据时中断嵌套能把人折磨疯。DMA的价值就在这里。它就是一个不需要CPU参与的内存搬运工外设触发一次它就按照配置好的源地址、目标地址和长度自己搬。CPU只负责在搬运完成后读取结果。MicroPython里虽然没法直接操作寄存器但这不代表没法用DMA——通过自定义C扩展把底层的DMA能力封装成Python可以调用的接口既能享受MicroPython的开发效率又能让关键路径跑在硬件上。1.2 Scatter-Gather到底解决什么问题刚接触DMA的人通常用的都是普通模式一个固定源地址、一个固定目标地址、一个固定长度触发一次搬一段。这在单通道ADC采样、单串口收发这些场景里没问题但一旦遇到“数据来自多个源”或者“数据要放到多个不连续缓冲区”普通DMA就有点力不从心了。Scatter-Gather直译过来就是“分散-聚集”。要做的事情很直观把一份连续的数据分散(Scatter)到多个不同的内存缓冲区或者反过来把多个不连续缓冲区里的数据聚集(Gather)到一起去发送。我习惯把它理解成快递中转站的传送带。普通DMA是一条固定路线的传送带每次都把包裹从同一个仓库搬到同一个分拣口。Scatter-Gather变成了一张分拣清单传送带每走完一段就自动查阅清单上下一段应该把货送到哪个分拣口。传送带本身不用停整个分拣过程由硬件自动完成。对应到实际采集场景最典型的就是多通道ADC。四路传感器轮流采样硬件会把四路数据按时间顺序串成一条数据流。传统做法是一个大缓冲区把四路数据全收下之后再由软件按通道索引手动拆开Scatter-Gather则是让DMA直接根据描述符链表把第一路的数据写到buf0第二路的写到buf1整个过程结束之后四个通道的数据已经各自躺在独立缓冲区里了。1.3 先想清楚这套方案适合哪些芯片和固件直接改MicroPython内核源码去集成DMA驱动维护成本和门槛都高得离谱。更合理的是走官方提供的USER_C_MODULE机制把底层功能通过C扩展模块挂到MicroPython运行时里。不过Scatter-Gather虽然听着高大上不是所有芯片的DMA都原生支持。我在这块的经验是这么分类的明确支持链式描述符的STM32H7系列尤其是MDMANXP的eDMATI的uDMAESP32的I2S/RMT自带的链表描述符结构。只支持固定双缓冲、需要软件参与切换的STM32F4/F1的普通DMA部分ESP32的GPIO矩阵外设。完全没有复杂DMA能力的一些8位MCU、极简内核芯片。这里得说清楚MicroPython官方固件并不保证底层DMA外设可以被用户层直接访问。想要做Scatter-Gather多半得自己编译固件并加入C扩展。文章中后续案例以STM32H743为例这块芯片的MDMA支持真正的链式描述符MicroPython也有对应的移植分支做实验非常顺。其他芯片思路完全一样只要硬件有Link/Next指针机制代码稍作调整就能迁移。2. 链式触发与Scatter-Gather的核心原理2.1 一切围绕DMA控制块链表上的节点要理解Scatter-Gather首先得把它最关键的数据结构搞清楚DMA控制块。不同厂商叫法不一样MDMA里叫CTLControl BlockeDMA里叫TCDTransfer Control Descriptor本质上都是同一个东西描述一次DMA传输所需全部信息的数据结构。一个典型的DMA控制块长这样typedef struct { uint32_t saddr; // 源地址可以是外设数据寄存器地址或内存地址 uint32_t daddr; // 目标地址通常是内存缓冲区地址 uint32_t len; // 本次传输的数据长度 uint32_t ctrl; // 控制字段传输宽度、地址增减模式、是否使能中断 sg_desc_t *link; // 下一个控制块的地址NULL表示链表结束 } sg_desc_t;源地址和目标地址不必多说稍微解释一下控制字段。它决定了DMA搬运数据的方式例如传输宽度是每次搬1字节、2字节还是4字节。如果ADC采样值是16位宽度设成半字效率最高。地址增减模式源地址固定、目标地址递增这是最常用的组合比如从ADC数据寄存器固定位置采样但结果要连续写进缓冲区所以源地址固定、目标地址递增。中断使能当前描述符完成后是否触发DMA中断。通常只在链表最后一个节点使能中断避免每个节点都中断一次CPU被打爆。link指针是Scatter-Gather的灵魂。它指向下一个要执行的控制块DMA硬件会在完成当前控制块之后自动把link字段里的地址加载为当前控制块然后接着继续传输。2.2 链式触发一个节点干完活下一个节点怎么接上所谓“触发”在DMA这个概念里有两个层面。第一层是外设触发条件ADC转换完成触发一次DMA传输串口收到一字节触发一次。第二层是控制块与描述符之间的“启动”逻辑也就是链路怎么从一个节点跳到下一个节点。常规想法是DMA顺着链表一路执行到底中途不需要任何干预。这种“连续”模式确实存在适用于已知固定长度、希望一口气搬完的场景。但Scatter-Gather还有一个更高级的玩法下一个控制块可以设置成不立即执行而是等待一个外部硬件事件。这就是链式触发里“触发”二字的精髓。比如说在ADC采集场景里我希望第一个控制块负责搬运一轮四通道的采样结果搬完之后第二个控制块不马上执行而是等下一个ADC转换完成事件再开始搬下一轮。于是DMA可以在ADC外设和内存之间形成一种“自动同步 自动分拣”的流水线CPU从头到尾不参与数据搬运只负责最后去取已经分拣好的数据。这种机制拿来处理串口不定长接收特别实用。把接收缓冲区拆成若干个固定大小的控制块比如每个控制块指向一个256字节的缓冲区。数据包来的时候DMA自动按256字节为一段从左到右填满一段就通过链式触发切到下一段。等串口空闲中断一来CPU从多个控制块里按序拼出完整报文完全不清理定长接收的痛点。普通DMA只能固定在一个缓冲区里连续写数据超长就会溢出Scatter-Gather让“接收一个可变长度的帧”变得非常自然。2.3 缓冲区布局、对齐与缓存一致性控制块和缓冲区怎么布局直接决定代码能不能稳定运行。这里有几个硬性要求是这次项目里真正踩过坑的地方。对齐要求。DMA控制块的地址必须按芯片要求对齐有的芯片要求32位对齐有的要求64位甚至128位对齐。STM32H7的MDMA控制块要求相对严格对齐不对轻则DMA行为异常重则直接HardFault。给控制块数组加__attribute__((aligned(32)))是基本操作。缓存一致性问题也不能忽略。Cortex-M7这类带D-Cache的内核上如果DMA先把数据写进了内存CPU从缓存里读到的可能还是旧数据因为缓存比内存“新”。反过来如果CPU刚往内存写了配置数据DMA直接去搬可能搬到的也是缓存里还没来得及回写的脏数据。H7上建议在DMA传输之前对源缓冲区做Clean操作传输完成后对目标缓冲区做Invalidate操作确保两侧拿到一致的数据。缓冲区本身最好在C扩展层用static数组或预先分配的大块内存管理而不是让MicroPython的GC堆来管。GC可能会移动对象、回收内存DMA硬件可不懂GC它拿着一个地址猛写等到对象被回收内存被复用数据就废了。我在后面“实操记录”里会专门讲怎么处理Python对象和DMA内存之间的关系。3. 实操记录从C扩展到MicroPython调用3.1 构建支持USER_C_MODULE的固件直奔主题第一步是让MicroPython固件认识我们写的C扩展模块。MicroPython源码从1.20版本开始对USER_C_MODULES支持做得比较完善整体流程也不复杂。工程结构大概这样micropython/ ├── ports/ │ └── stm32/ │ ├── main.c │ └── ... └── usermods/ └── sg_dma/ ├── sg_dma.c ├── sg_dma.h └── micropython.mkmicropython.mk里需要写清楚要编译哪些源文件以及需要include的头文件路径标准写法是SRC_USERMOD sg_dma.c CFLAGS_USERMOD -Isg_dma编译STM32固件时用环境变量指定模块路径cd ports/stm32 make USER_C_MODULES../../usermods BOARDSTM32H743第一次编译确实耗时后续增量编译就快了。耐心烧录完这版固件在MicroPython REPL里执行import sg_dma如果能正常导入说明底层的C扩展已经挂上了。3.2 编写链式DMA驱动关键代码拆解接下来是C扩展的核心。我这里直接用一组简化但结构完整的代码来说明思路实际驱动代码可以在这个基础上扩展。首先是描述符结构和控制块数组#include py/obj.h #include py/runtime.h #include py/binary.h #define SG_MAX_DESC 8 typedef struct { uint32_t saddr; uint32_t daddr; uint32_t len; uint32_t ctrl; struct sg_desc *link; } sg_desc_t; // 描述符数组要求32字节对齐避免DMA对齐问题 static sg_desc_t sg_desc[SG_MAX_DESC] __attribute__((aligned(32)));初始化描述符链表的函数参数包括源地址、一组目标缓冲区地址和长度static void sg_link_init(uint32_t src, uint32_t *dsts, uint32_t *lens, uint32_t cnt) { if (cnt SG_MAX_DESC) { cnt SG_MAX_DESC; } for (uint32_t i 0; i cnt; i) { sg_desc[i].saddr src; sg_desc[i].daddr dsts[i]; sg_desc[i].len lens[i]; sg_desc[i].ctrl SG_CTRL_DST_INCR | SG_CTRL_SRC_FIXED; sg_desc[i].link (i cnt - 1) ? NULL : sg_desc[i 1]; } }启动DMA硬件的函数以MDMA为例最关键的是把第一个描述符地址写到控制块基地址寄存器里然后置使能位static void sg_start(void) { // 把链表首地址写入MDMA控制块基地址寄存器 MDMA-CBAR (uint32_t)sg_desc[0]; // 使能MDMA通道启动链式传输 MDMA-CCR MDMA_CCR_EN; }有人问为什么不把整个DMA中断处理逻辑也放进C模块我的做法是中断服务函数里只做“当前控制块序号递增”和“清中断标志”这两个动作剩下的事交给Python层去轮询。原因很简单MicroPython中断回调机制有上下文限制在中断里调用Python对象容易出问题不如把中断做得极简或者干脆用查询方式完成同步。等待完成的函数也可以做成MicroPython接口加个超时保护避免死等。static int sg_wait_done(uint32_t timeout_ms) { volatile uint32_t tick 0; while (1) { if (MDMA-CISR MDMA_CISR_TCIF) { MDMA-CIFCR MDMA_CISR_TCIF; return 0; } if (tick timeout_ms * 1000) { return -1; } } }注意这里的寄存器名和操作方式以STM32H7 MDMA为例具体位定义务必以对应芯片参考手册为准。不是说网上代码不能抄而是DMA外设细节差异太大直接照搬容易翻车。3.3 封装MicroPython接口并完成调用把上面的C函数暴露给MicroPython标准做法是定义函数对象和模块表STATIC mp_obj_t sg_link_init(mp_obj_t src_obj, mp_obj_t dsts_obj, mp_obj_t lens_obj) { uint32_t src mp_obj_get_int(src_obj); mp_obj_t *dsts_items; mp_obj_get_array(dsts_obj, NULL, dsts_items); // 同样解析lens数组 // 调用底层的sg_link_init() return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_3(sg_link_init_obj, sg_link_init); STATIC MP_DEFINE_CONST_FUN_OBJ_0(sg_start_obj, sg_start_func); STATIC MP_DEFINE_CONST_FUN_OBJ_1(sg_wait_obj, sg_wait_func); STATIC const mp_rom_map_elem_t sg_module_globals_table[] { { MP_ROM_QSTR(MP_QSTR_link_init), MP_ROM_PTR(sg_link_init_obj) }, { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(sg_start_obj) }, { MP_ROM_QSTR(MP_QSTR_wait), MP_ROM_PTR(sg_wait_obj) }, }; STATIC MP_DEFINE_CONST_DICT(sg_module_globals, sg_module_globals_table); const mp_obj_module_t sg_user_cmodule { .base { mp_type_module }, .globals (mp_obj_dict_t *)sg_module_globals, }; MP_REGISTER_MODULE(MP_QSTR_sg, sg_user_cmodule);Python侧调用就非常简洁了import sg src 0x40000000 # 外设地址不同芯片不同外设不一样 buf0 bytearray(256) buf1 bytearray(256) buf2 bytearray(256) dsts [buf0, buf1, buf2] sg.link_init(src, dsts, [len(buf0), len(buf1), len(buf2)]) sg.start() sg.wait(1000) print(buf0[:10])注意一点dsts数组里的bytearray在传给C扩展后C代码拿到的其实是指向对象内部数据缓冲区的手指针。MicroPython的bytearray底层内存本身是连续且固定的只要Python侧还持有这些对象引用GC不会把它们回收掉所以可以安全地让DMA去写。但前提是在DMA传输期间不能让这些对象被重新赋值或者删除否则引用计数减到零缓冲区就可能被释放。稳妥做法是在C扩展里给这些对象加引用计数让模块自身持有它们。我在代码里加了一步把dsts里的对象指针保存到一个全局数组里模块被释放时才解除引用。这样即使Python临时删了变量对象也不会被GC回收。static mp_obj_t sg_hold_objs[SG_MAX_DESC]; // 在link_init里保存引用 sg_hold_objs[i] dsts_items[i]; mp_obj_t_keep_ref(sg_hold_objs[i]);3.4 两个典型场景的完整验证串口不定长接收与ADC四通道采集理论说再多最终还得靠场景验证。这次项目里我完整跑通了两类典型应用。第一类是串口DMA接收不定长数据。假设一帧数据长度在64到1024字节之间波动我配置了4个控制块每个控制块指向一个256字节的缓冲区控制块0目标buf0长256字节。控制块1目标buf1长256字节。控制块2目标buf2长256字节。控制块3目标buf3长256字节。配置成链式模式后串口每收到一个字节DMA就自动搬运一次。前256字节会填到buf0第257字节开始自动跑到buf1依此类推。硬件层面我同时启动了串口的空闲中断数据流结束后线路空闲CPU在空闲中断回调里根据当前控制块序号和长度计算出总共收到了多少字节。Python侧组装数据帧时按顺序把buf0到bufN拼接起来就行。这个方案最让我满意的地方是无论数据帧多长搬运和分块都是硬件自动完成的CPU只在整帧接收完成后做一次拼接几乎不占任何实时资源。第二类是ADC四通道DMA采集。把ADC配置成扫描模式四路通道循环转换。每个通道转换完成后ADC硬件会产生DMA请求。我配置了4个控制块目标地址分别指向4个独立缓冲区控制字段里设置目标地址递增、源地址固定链表的最后一个控制块再通过链式触发重新指向第一个控制块也就是环形链表。每轮四通道采样结束DMA自动进入下一轮四路数据从头到尾分开存放。采集结束后Python侧代码不需要再做通道拆分ch0 buf0[0:1000] ch1 buf1[0:1000] ch2 buf2[0:1000] ch3 buf3[0:1000]这份清爽是纯Python手动拆分数据流的方案永远给不了的。4. 常见问题与排查技巧实录4.1 传输完成中断不触发或程序卡死这个问题我在调试时遇到过不止一次。刚开始配置好的DMA怎么都不产生中断程序一直卡在wait循环里。排查思路按优先级这么来先看描述符地址是否对齐。DMA硬件要求控制块地址对齐尤其是H7系列不对齐可能直接导致DMA控制器异常中断根本不会来。检查方法是打印sg_desc数组地址确认它落在对齐边界上。我用的__attribute__((aligned(32)))基本能保证这一点。再看中断使能和NVIC配置。C模块里只配置了DMA控制块的描述符中断位还不够外层还得把对应DMA通道的NVIC中断使能打开。如果你和我一样用轮询方式等待完成这一步可以省略但如果是中断回调方案NVIC配置漏了就会“静默失败”。最后看DMA搬运的源地址到底有没有数据。串口场景里如果UART没有正确使能DMA请求DMA请求信号永远不会来控制块再漂亮也没用。这个排查点最简单但也最容易被忽略。4.2 MicroPython对象与DMA内存的纠缠MicroPython的GC是这次项目里最大的隐性雷区。DMA拿着CPU给的内存地址埋头写数据GC根本不知道这个地址正在被硬件使用。Python侧如果对这个bytearray做了垃圾回收或者变量被重新赋值底层内存可能被释放或重新分配DMA还在往旧地址写轻则数据被覆盖重则直接内存访问违例。我的经验就一条DMA要访问的缓冲区绝对不能只存在于Python层变量里。C扩展模块要在link_init阶段保存这些对象的引用让对象在DMA生命周期内始终处于被引用状态。等DMA传输彻底结束后模块再释放引用。实现上可以是简单的mp_obj_t数组也可以更稳妥地用MicroPython的mp_obj_t_keep_ref机制。另外描述符链表本身也不要在MicroPython堆里动态分配。用C层的static数组是最稳妥的宁可多占一点RAM也不能让硬件依赖的内存被GC控制。4.3 缓存一致性问题数据读出来全是旧的带D-Cache的Cortex-M7内核上DMA和目标缓冲区之间还隔着一层缓存。DMA把数据写进RAMCPU从L1缓存读如果没有做缓存失效Invalidate读到的可能是缓存里那份之前的内容也就是“数据读出来全是旧的”。解决办法是在DMA传输完成、CPU读取之前对目标缓冲区执行SCB_InvalidateDCache_by_Addr操作。反过来如果是CPU先写好数据再由DMA搬运则要先做SCB_CleanDCache_by_Addr把CPU缓存里的新数据强制回写到RAMDMA才能搬到最新值。SCB_InvalidateDCache_by_Addr((uint32_t *)buf0, len);这个细节在F1/F4这类没有D-Cache的内核上并不需要但H7上不做必然翻车。我的调试建议是先用一个简单的单通道DMA测试缓存是否一致确认没问题再上多通道Scatter-Gather排查起来会快很多。4.4 性能实测与调优建议这条要有数据说话。同样跑四通道ADC、每通道2kHz采样率、单次采样2字节的场景我对比过两种方案。纯Python方式是从DMA中断里逐字节读取数据再拼接CPU长期高占用大循环里还要频繁处理字节数组实测CPU占用在60%以上。切到Scatter-Gather以后CPU只负责配置DMA、等待完成、读取结果四路数据已经按通道分好CPU占用降到了10%以下。更直观的时间数据是这样纯Python中断搬运8000字节大概需要2到5毫秒不同固件优化程度有差异DMA搬运同样数据量只需要几十微秒而且完全不占用CPU周期。这一块性能差距主要就是“CPU一条条搬”和“硬件流水线批量搬”的差距。调优建议方面控制块里的len尽量设置成DMA单次传输宽度的整数倍避免短尾传输影响效率。链表长度不是越长越好控制块本身也占用RAM。在数据帧长度稳定的场景里控制块数量够用就行。中断只开在最后一个控制块不要每个节点都开中断否则中断频率依然会拖慢整体实时性。如果DMA支持FIFO模式适当增加FIFO阈值可以大幅降低外设与内存之间传输的次数提升总线利用率。5. 写在最后几个值得记住的取舍说句实在话只在MicroPython上层打转的时候你很难意识到DMA链式触发的威力。一旦把视野放到真实硬件上Scatter-Gather这项技术在数据采集、物联网网关、信号处理这些场景里几乎是绕不开的底层能力。这次做完之后我自己的体会是想在MicroPython里搞底层优化最大的门槛从来不是寄存器配置而是把Python对象生命周期和硬件内存模型对齐。描述符内存用C层static数组传输缓冲区用C扩展持有引用Python层只做配置和结果读取整体架构就非常干净了。最后分享一个从这次项目里攒下的小技巧调试DMA链表时不要一上来就上多通道、多控制块先把单个控制块的普通搬运跑通确认基本时序和内存地址没问题再逐步增加链表节点。这样即使出了问题也能够快速定位是硬件配置错了、地址没对齐还是GC回收的锅。多做几轮这种“简报”式验证比一次性写一个大功能然后反复调试要高效得多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Earcut三角剖分库的工程实践:原理、应用场景与踩坑全解析 2026/9/8 7:50:20

Earcut三角剖分库的工程实践:原理、应用场景与踩坑全解析

简介:基于耳切法(Ear Clipping)的多边形三角化 C 实现,核心源自 mapbox 的 earcut 库,并通过 z 阶曲线散列优化顶点访问顺序,能够处理无序顶点并输出三角形顶点索引。算法在经典耳切法基础上吸收了 FIST&am…

阅读更多 →
误删数据库不慌:SQL Server事务日志与ApexSQL Log恢复实践 2026/9/8 7:50:20

误删数据库不慌:SQL Server事务日志与ApexSQL Log恢复实践

简介:面对误删数据库的紧急场景,这份ApexSQL Log 误删数据库还原破解版工具包能帮助DBA、运维与开发人员从事务日志层面快速定位并恢复数据,支持多种数据库版本,实测在SQL Server 2008下运行稳定,适合需要处理误删、日…

阅读更多 →
微信小程序图书管理系统开发实战:架构设计到上线避坑指南 2026/9/8 7:50:20

微信小程序图书管理系统开发实战:架构设计到上线避坑指南

简介:这是一份面向微信小程序开发学习者与前端初学者的图书管理系统项目文件包,完整覆盖用户注册登录、图书分类搜索、借阅归还、预约续借、订单支付、个人中心、评论评分及管理员后台等核心业务模块,可直接在微信开发者工具中导入运行与二次…

阅读更多 →
办公设备管理系统OAMS:从状态机设计到二维码盘点的全流程实践 2026/9/8 7:50:20

办公设备管理系统OAMS:从状态机设计到二维码盘点的全流程实践

简介:办公设备管理系统OAMS是一套面向企事业单位的Java Web项目,覆盖设备采购、入库、领用、维修、报废等全生命周期管理,并支持库存与供应商管理,能有效提升办公设备使用效率。资源共451个文件,以JSP页面、Java业务类…

阅读更多 →
Focas V4.0在线考试系统实战:从部署到高并发调优全解析 2026/9/8 7:50:20

Focas V4.0在线考试系统实战:从部署到高并发调优全解析

简介:面向FANUC数控系统二次开发工程师的FOCAS V4.0接口资料包,定位为数控机床数据采集与远程监控的基础开发套件,可应用于生产数据实时读取、设备状态上报、故障诊断与远程维护等场景。压缩包共6813个文件、26.16MB,文件构成涵盖…

阅读更多 →
国产MCU替换STM32的5个隐藏坑,你踩过几个? 2026/9/8 7:47:19

国产MCU替换STM32的5个隐藏坑,你踩过几个?

从PCB上一个引脚都不改,到程序烧进去能跑,再到跑一跑就出事——国产MCU替换STM32这条路,我陪客户走了不少遍,也替自己板子踩过不少坑。原理图上PIN对PIN,内核都叫Cortex-M3/M4,不少人潜意识里觉得"兼容…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞