嵌入式调试必备:MODBUS RTU协议底层原理与实战解析
1. 为什么嵌入式工程师绕不开MODBUS这台老式收音机我做嵌入式调试这些年接触过的总线协议少说也有十几二十种I2C、SPI、CAN、EtherCAT、Profinet……但有一个协议它论速度不是最快的论功能不是最强的论优雅程度甚至排不上号却在工业现场活了几十年至今还在PLC、传感器、电表、阀门、变频器、触摸屏里到处出现——没错就是MODBUS。如果你刚入行可能觉得奇怪都什么年代了为什么还要学这么古老的东西原因其实很现实不是因为它技术上有多先进而是因为它足够便宜、足够简单、足够容易实现。一个MCU只要有一路UART外加一颗RS485收发芯片几十行代码就能实现一个支持多设备通信的从站。工程师想调试它一台电脑、一个USB转串口模块、一个串口调试助手就够用了——不需要示波器不需要逻辑分析仪不需要任何专用工具。这种零门槛恰恰是它在工业现场不可替代的原因。这篇文章是我嵌入式调试笔记系列的第七篇围绕MODBUS协议本身展开。我会把RTU模式下完整的报文结构、功能码含义、寄存器模型、CRC校验规则这些底层细节拆开讲清楚然后给你一套可以直接抄走的从机程序框架再演示怎么用串口调试助手手动组帧、模拟主站、抓包回放最后复盘我实际调试中踩过的几个坑。无论是写设备端固件、做上位机联调还是纯粹在产线上用串口助手查问题这篇笔记都适用。为了让你有直观概念先给一个最简单的调度场景现场有一台温控器从站站号1PLC主站要读它的当前温度值保持寄存器地址40001MODBUS RTU的请求帧就是这样的01 03 00 00 00 01 84 0A其中01是从站地址03是功能码读保持寄存器00 00是起始寄存器地址协议地址从0开始00 01是读取数量84 0A是CRC16校验。这一帧一共8个字节完成一次完整的读取。后面你会看到整篇笔记其实就是围绕这几段字段的拆解和展开。先说清楚三件事。第一MODBUS分RTU、ASCII和TCP三种模式嵌入式串口通信里RTU占绝对统治地位所以本文默认说MODBUS就是指MODBUS RTU涉及TCP的我会单独标注。第二MODBUS是主从式协议总线上同一时刻只能有一个主站发起请求从站不能主动说话——理解了这句话后面很多调试坑都能想明白。第三MODBUS的寄存器地址和数据区编号之间存在偏1关系这是初学者乃至部分老手都容易犯迷糊的地方我也会专门讲。2. MODBUS的底层骨架报文结构、寄存器模型与功能码2.1 RTU报文就是一问一答的对讲机MODBUS RTU的报文结构可以用一种很直白的方式理解主站和从站之间是一次对讲机式的通话。主站拿起对讲机喊一嗓子请求帧点名某个从站地址字段然后说清楚要干什么功能码附带干活的具体参数数据字段最后喊一句完毕校验字段。从站听到后如果发现是在叫自己就回一声响应帧告诉主站干得怎么样。标准RTU请求帧的格式如下字段长度说明地址1字节从站地址有效范围1~2470用于主站广播功能码1字节1~255具体含义见下表数据N字节寄存器地址、数量、数据值等取决于功能码CRC162字节低字节在前、高字节在后对地址、功能码、数据所有字节做校验这里有两个细节容易被轻视。第一从站地址0是广播地址主站发到地址0时所有从站都会收但从站不回复。第二CRC是低字节在前。很多人在PC端组帧时算出来的CRC是0x0A84但实际发到串口上必须先发84再发0A。你看到的那些抓包工具里显示为84 0A的顺序就是标准传输顺序。响应帧稍微有点变化。正常响应中数据字段包含实际数据如寄存器值异常响应中功能码的最高位置1数据字段放错误码。比如主站询问功能码03从站如果返回83 04就表示你让我读保持寄存器但我处理的时候就出错了错误码04表示非法数据地址。2.2 寄存器模型线圈、离散量、保持寄存器、输入寄存器MODBUS把从站设备里的数据划分成四个区这在协议里叫作数据模型。理解这个模型特别关键因为你写上位机或者调设备的时候地址对照错了功能码就全乱了。数据区区域编号位/字读写特性常见用途线圈Coil0区位可读可写继电器输出、启停控制离散输入Discrete Input1区位只读限位开关、按钮状态输入寄存器Input Register3区16位字只读模拟量输入、传感器采集值保持寄存器Holding Register4区16位字可读可写设定参数、运行累计值、可修改配置你注意编号功能区编号是0、1、3、4没有2区。原因是2区在历史上曾经被定义过一些用途但后来MODBUS规范里没有采用完整独立的对应关系实际产品中2区基本不露面。所以你在设备手册里看到3区、4区对应就是输入寄存器和保持寄存器。再强调一遍偏1问题。PLC的组态软件里通常把保持寄存器的地址写成40001这个叫PLC地址但实际在协议报文里寄存器地址字段是0000。也就是说协议地址 PLC地址 - 1。你要读40001报文的寄存器地址就得写0x0000。如果你直接拿着40001的十进制转十六进制0x9C41填进报文里从站返回错误码04非法数据地址几乎是必然的。这个坑我见过无数人踩包括当年的我自己。2.3 常用功能码不是每个码都要背但这8个得门儿清MODBUS官方定义的功能码有几十个但实际嵌入式设备上常见的就下面几张牌功能码名称对应数据区用途0x01读线圈0区读开关输出状态0x02读离散输入1区读开关输入状态0x03读保持寄存器4区读参数或运行数据0x04读输入寄存器3区读传感器采集值0x05写单个线圈0区控制单个继电器/开关0x06写单个保持寄存器4区修改单个参数0x0F写多个线圈0区批量控制多个开关0x10写多个保持寄存器4区批量修改参数读操作基本围绕0x03和0x04展开写操作最常用0x06和0x10。0x0F用得相对少但批量控制场景下会用报文格式是地址、功能码、起始地址、数量、字节数、N个字节的数据、CRC比如向站1写2个线圈地址0和1都为ON01 0F 00 00 00 02 01 03 11 0F最后一字节11 0F是CRC。你可以看出发送数据是03二进制0000 0011表示线圈0和线圈1都为1。单个线圈在MODBUS里的标准值是0xFF00表示ON、0x0000表示OFF但批量写线圈时用的是位映射每个开关占1位最后一字节剩余位补0。这种单点用值、批量用位的差异也是新手容易晕的地方。2.4 从站错误码的快速解读异常响应里从站会返回错误码通常意义如下错误码含义常见触发原因01非法功能码从站固件不支持该功能02非法数据地址寄存器地址超出范围或者起始地址数量越界03非法数据值写入值超出范围比如把一个4位数写进温度设定值04从站设备故障从站内部处理出错、硬件异常调试时如果收到83 02第一反应应该是我读的寄存器地址是不是越界了。收到90 01功能码0x10读保持寄存器不对0x10的错误响应是0x90就检查功能码本身它支不支持。这个映射关系记牢排查效率能提高一半。3. 从零手写一套MODBUS RTU从机状态机、缓存与CRC实现3.1 为什么从机程序适合用串口接收中断超时定时器设备端实现MODBUS RTU从机核心问题只有一个怎么判断一帧报文结束。MODBUS RTU没有帧头标志没有帧尾定界符协议规范里明确要求报文内字节间隔不能大于1.5个字符时间帧与帧之间的间隔必须大于3.5个字符时间。也就是说接收端要么靠超过1.5T35后认为一帧结束要么靠收到至少3.5T35的空闲认为前面的数据是完整一帧。在实际代码里常见的做法是串口接收到首个字节时启动一个定时器定时时间设为比如9600波特率下3.5个字符时间。每次收到新的字节都重置定时器一旦定时器超时就说明总线已经安静了足够长时间当前缓冲区的数据就是一帧完整报文。这个做法实现简单而且能天然处理主站连续发两帧的情况。我个人的经验是从设备端建议直接用串口接收中断 一个5ms或10ms的软件定时器而不是用空闲中断或DMA半满中断。虽然STM32的串口空闲中断IDLE处理看起来更高端但不同芯片的IDLE行为有差异调试时容易踩雷。用通用定时器逻辑直白移植到任何MCU平台上都能跑。9600波特率下一个字符大约1.04ms一个字节含起始位、8个数据位、1个停止位共10位所以一个字符时间约1.04ms3.5字符时间约3.65ms取整数5ms比较稳妥如果是115200波特率一个字符约0.087ms3.5字符约0.3ms软件定时器取1ms~2ms也够用。3.2 状态机框架接收、解析、应答三步走下面给一个完整可用的从机逻辑框架以STM32平台为例伪代码风格可平移到任何MCU。整个从机处理流程分成三步接收、解析、应答。接收在中断里做解析和应答在主循环或单独任务里做。接收部分// 串口接收中断 void USART_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t ch USART_ReceiveData(USART1); modbus_rx_buf[modbus_rx_len] ch; if (modbus_rx_len MODBUS_RX_BUF_SIZE) modbus_rx_len 0; // 防止溢出简单粗暴地丢掉重收 // 重置超时定时器 modbus_timer_count 0; } }定时器部分// 1ms或5ms的定时中断 void Tim_IRQHandler(void) { if (modbus_timer_count MODBUS_FRAME_GAP_MS) { modbus_frame_ready 1; // 一帧接收完成 modbus_timer_count 0; } else { modbus_timer_count; } }主循环解析与应答void modbus_poll(void) { if (modbus_frame_ready) { uint8_t addr modbus_rx_buf[0]; uint8_t func modbus_rx_buf[1]; // 1. 地址匹配只有收到自己的地址才回复 if (addr ! MODBUS_SLAVE_ADDR addr ! 0x00) { modbus_frame_ready 0; modbus_rx_len 0; return; } // 广播帧地址0执行命令但不回复 // 2. CRC校验注意先比较CRC还是先解析数据 // 建议先校验CRC不对直接丢弃不构造响应 uint16_t crc_calc modbus_crc16(modbus_rx_buf, modbus_rx_len - 2); uint16_t crc_recv (modbus_rx_buf[modbus_rx_len - 2]) | (modbus_rx_buf[modbus_rx_len - 1] 8); if (crc_calc ! crc_recv) { modbus_frame_ready 0; modbus_rx_len 0; return; } // 3. 按功能码分发 switch (func) { case 0x03: modbus_handle_read_holding(); break; case 0x04: modbus_handle_read_input(); break; case 0x06: modbus_handle_write_single(); break; case 0x10: modbus_handle_write_multiple(); break; default: // 功能码不支持回异常码01 modbus_send_exception(func | 0x80, 0x01); break; } modbus_frame_ready 0; modbus_rx_len 0; } }这段代码里我特意把CRC校验放在解析前面这是经验之谈。如果CRC错误你还去做地址和功能码解析可能在日志里输出一堆非法功能码之类的误导信息把问题引到错误方向。先校验CRC确保这帧数据是完整的、可信的再做业务处理整个系统调试起来会清爽很多。3.3 CRC16的实现与自测方法MODBUS RTU的CRC本质是对全部字节做循环冗余校验生成多项式是x^16 x^15 x^2 1即多项式值0xA001反位序后。可以用查表法加速也可逐位计算。我贴一个逐位实现的版本方便新手看懂原理uint16_t modbus_crc16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t bit 0; bit 8; bit) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }这个函数返回的CRC就是标准的MODBUS CRC16值。发送时先发低字节crc 0xFF再发高字节crc 8。自测方法很关键拿着前面那帧报文01 03 00 00 00 01手动算一遍CRC你应该得到0x0A84发送顺序是84 0A。用你自己的串口助手发这个帧给从站从站如果正常响应说明收发链路通了。3.4 寄存器映射表设计从机固件里寄存器地址到实际数据的映射一般用一张表完成。建议用一张静态结构体数组来维护typedef struct { uint16_t addr; // 协议寄存器地址 uint8_t access; // 读写权限REG_READ_ONLY / REG_READ_WRITE / REG_WRITE_ONLY uint16_t min_value; // 写入值下限 uint16_t max_value; // 写入值上限 uint16_t *data_ptr; // 指向实际存储变量 } reg_entry_t; static uint16_t g_temperature_value 0; static uint16_t g_setpoint_value 250; static const reg_entry_t g_reg_table[] { {0x0000, REG_READ_ONLY, 0, 0xFFFF, g_temperature_value}, {0x0001, REG_READ_WRITE, 0, 10000, g_setpoint_value}, // ... };收到0x03读保持寄存器请求时遍历表找地址收到0x06写请求时需要检查权限和写入值范围超限就回错误码03。这样做的好处有三个一是代码结构清晰加一个新寄存器就加一行表二是权限和范围校验统一不容易漏三是调试时打印整个表就能看到全部映射关系。4. 用串口调试助手模拟主站手把手组帧、抓包、回放4.1 为什么我先不用MODBUS调试软件网上有大量专门的MODBUS调试工具比如ModbusPoll、Modbus Slave、Docklight这些功能确实很强。但我还是强烈建议你先学会用串口调试助手手动组帧。原因很朴素调试工具帮你封装好了地址转换、CRC计算、报文解析你反而看不见底层发生了什么。等你在现场遇到一个不响应报文或者回复异常码的设备手边又没有专用工具时唯一能救你的就是裸的串口助手加你的脑子和笔。我用得最多的串口调试助手是SSCOM界面简洁、支持HEX发送和显示免费且不需要安装。其他像AccessPort、格西烽火、SerialPortPlot也行只要具备三个能力就行能按HEX字节发送、能按HEX字节显示接收、能手动输入任意字节序列。反正核心逻辑都一样。4.2 第一个压测案例读取温控器的当前温度假设现场有一台温控器站号01当前温度寄存器是40001协议地址0x0000支持功能码03。我们要做的请求就是文章开头那一帧01 03 00 00 00 01 84 0A在SSCOM里的操作步骤如下串口参数选择波特率9600、数据位8、停止位1、无校验、无流控。注意绝大多数默认MODBUS RTU配置是8N1少数设备出厂是8E1或8O1具体看设备手册。勾选HEX显示和HEX发送不然你输入的01会被当成ASCII字符0和1发送报文直接烂掉。发送区输入01 03 00 00 00 01 84 0A空格不影响HEX输入的解析。点击发送观察接收区。正常响应应该是01 03 02 0A 7B 8C 3F拆解一下01是站号03是功能码02是后面数据字节数0A 7B是温度值十六进制0x0A7B 26838C 3F是CRC。如果温度值带小数或补码就要根据设备手册再做换算比如乘以0.1得到268.3摄氏度。如果接收区回的是01 83 02 C0 F1那说明从站明确告诉你03功能码请求中的寄存器地址或数量越界。这时候你就该回头查自己的寄存器地址有没有算错——大概率是你把PLC地址40001直接当成协议地址用了。如果接收区完全没反应那就是另一个问题接下来讲排查链路。4.3 写单个寄存器怎么组帧写单个保持寄存器用功能码06帧格式是地址、功能码、寄存器地址、数据值、CRC。比如把站号01的设备地址0x0001的值写成1000x006401 06 00 01 00 64 19 CA正常响应会把请求帧原样返回这在调试时非常有价值如果响应帧和请求帧一模一样说明从站已经认定数据写成功了。如果从站返回01 86 03 02 0C则说明功能码05/06请求中数据值非法——比如你要写温度设定值但值超过了固件里定义的上限。4.4 连续读和批量写的报文格式连续读多个保持寄存器用功能码03寄存器数量字段写N。比如读地址0x0002开始的3个寄存器01 03 00 02 00 03 85 CA响应帧会一次性返回6个字节的数据值3个寄存器各2字节。批量写用功能码10比如往0x000A和0x000B两个寄存器分别写0x1111和0x222201 10 00 0A 00 02 04 11 11 22 22 B4 A6中间那个04是数据总字节数。注意计算CRC时要把整条数据都算进去包括数量、字节数这些中间字段很多人在这一步把CRC算错函会一直报CRC错误。4.5 用回环法验证PC端组帧工具如果你用ModbusPoll这类工具自动组帧怎么验证它没算错方法很简单拿一台你确定没问题的从站设备或者你刚写完的自制从机轮询读一个已知寄存器同时在PC端用串口镜像软件或者干脆把主站发的原始串口数据抓到示波器/逻辑分析仪上看实际发出的报文。我一般用逻辑分析仪挂在RS485收发芯片的RO引脚上抓PC发出的原始电平再对照工具填写的地址和数量几秒钟就能发现工具的地址偏移设置有没有跟你理解一致。特别是PC端工具里从0开始还是从1开始这种设置错了就全场翻车。5. 调试实际项目时最常见的一堆坑5.1 从站没有响应到底先查哪个环节排查从站没响应是MAX级别的高频场景。我总结了一条经验原则按物理层→配置层→协议层→业务层的顺序逐层排查别跳级别凭感觉猜。物理层用万用表量A/B线电压。因为RS485是差分信号静态空闲时A线比B线高200mV以上如果两个引脚电压接近甚至反向说明接反了。接反了报文照发但设备收不到。如果A/B间电压为0检查是否共地以及是否终端电阻缺失。很多短距离调试出问题的原因是总线上忘了加120Ω终端电阻导致信号反射数据传输时好时坏。配置层检查串口参数波特率、校验位、停止位。MODBUS RTU默认8N1但有些老设备用8E1两边对不上报文根本解不出来。其次是站号请求帧的地址和设备DIP拨码设置的地址不一致一切都白搭。协议层抓实际发送的HEX字节确认串口助手发出去的是不是你想发的那一帧。最常见的坑就是我前面说的没开HEX发送ASCII字符01变成字节0x30 0x31。另一个坑是CRC算错。业务层以上都正常用示波器观察从站UART的RX引脚有没有收到完整的波形并确认从站程序里中断是否被更高优先级的中断堵住了。尤其是带无线模块或者频繁Flash擦除的程序串口中断被长时间关闭报文来了没进中断从站自然没反应。5.2 CRC计算正确但CRC校验失败的诡异现象有一个现象我遇到很多次上位机工具抓包显示报文完整CRC看起来也对但设备端总是校验失败。最后定位出来的原因通常是字节间隙超时。主站在发送一帧报文时因为操作系统调度或USB转串口的驱动缓冲问题帧内相邻字节之间被插入了很长的延迟超过1.5字符时间从站按MODBUS规则认为这不是同一帧直接拆成了两帧或丢弃。报文抓出来每个字节都对但设备就是不处理。解决方法有几种一是从站接收超时时间适当放宽比如9600波特率下从3.5字符时间放宽到10ms能容忍USB转串口的偶发卡顿二是排查主站程序的发送方式避免在发送一帧中间做耗时操作三是如果主站在Windows上用了USB转串口可以考虑设置更小的延迟定时器参数或者使用实时性更强的操作系统。5.3 寄存器值高低字节顺序之争MODBUS规定寄存器是16位且一帧内高字节在前Big-endian大端序。比如0x0A7B发送时先发0x0A再发0x7B。但你写从机代码时如果直接把一个16位变量的两个字节塞进响应缓冲区就得注意平台的大小端。在STM32这类小端平台上uint16_t value 0x0A7B; resp[0] value 8; // 高字节0x0A resp[1] value 0xFF; // 低字节0x7B这句赋值顺序错了上位机读到的温度就会从2683变成31810这种莫名其妙的数字。另外PLC和触摸屏那边通常也有一个字节序设置AB/CD或CD/AB两边对不上也会出现同样的问题。遇到读数异常先花10秒钟检查字节序而不是怀疑打数逻辑。5.4 多机通信时一个从站掉线拖垮整条总线RS485是多点总线只要有一个节点的收发芯片串到A/B线上整个总线都瘫痪。我遇到过一种情况新接一个从站设备后原来的两个从站全部没响应。用示波器看A/B波形是零碎的毛刺完全不是干净方波。最后查出是新设备的地线没有和总线的地接在一起导致共模电压超范围收发芯片工作异常。多从站组网现场要记住三件事A/B线必须双绞屏蔽层单端接地或按设备规范处理每个从站的地线要和主站地线相连通信距离长时尤其重要只有末端两个节点加120Ω终端电阻。中间节点的终端电阻加不加总线上信号完整性的表现完全不同。5.5 写入大参数时不要用0x06而用0x10有些参数的数值超过65535吗不会。但一个功能命令要同时更新多个寄存器时比如校准参数有K值、B值、量程下限、量程上限4个寄存器如果你逐条用06写中间状态就是已写K未写B的不一致状态上位机或主站万一在写第二条前掉线设备参数就处于半更新状态。用0x10一次写完整组参数从站收到完整帧后一次性更新才是可靠的做法。5.6 从站收到广播帧地址0执行但不回复广播地址0的帧会发给所有从站且不允许从站回复。这个特性很有用——主站可以用广播发一个所有从站同步保存参数的命令。但如果多个从站需要反馈执行结果广播就不适用了。调试时注意不要用广播帧去测试单站响应它根本不会回也不要在广播帧后立刻发点对点请求给从站留一点执行时间。6. 进阶调试技巧与工具配合6.1 用示波器和逻辑分析仪看RS485时序当串口助手层面已经看不出问题时就该上示波器了。很多人觉得RS485看波形很麻烦其实没那么复杂。你只需要把示波器探头挂到A线、B线对地或者用差分探头看A-B电压触发条件设成下降沿或上升沿单次触发。抓到的波形应该能看到明显的显性/隐性电平交替。有个省事做法把示波器探头直接挂在RS485收发芯片的RO接收输出脚上这里已经是转换成TTL电平后的波形清晰更易读。数据波特率可以从波形宽度估算出来一个位的时间就是1/波特率。比如9600波特率的一个位大约104微秒带起始位的第一个下降沿就能看到稳定方波。判断总线是否正常收发这一招很直接能收到稳定的方波说明收发链路是通的剩下就查协议逻辑。6.2 用脚本工具做自动化和边界测试手动组帧适合初学和排查但要验证从站对边界条件的处理比如同时读全部寄存器会怎样、CRC错误会不会卡死手动组帧效率太低。我推荐用Python的pyserial库顺手写个小脚本自动循环发各种异常报文看从站是否还能正常响应。脚本逻辑很简单核心就几行import serial, time, struct ser serial.Serial(COM3, 9600, timeout0.5) def crc16_modbus(data): crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 1: crc (crc 1) ^ 0xA001 else: crc 1 return crc # 读站号1保持寄存器地址0取1个 req bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01]) crc crc16_modbus(req) req bytes([crc 0xFF, crc 8]) ser.write(req) resp ser.read(100) print(resp.hex())我当时用这个方法一次性发现了从站程序里一个隐藏的Bug当主站请求读取的寄存器数量超过固件定义长度时从站异常处理里数组越界导致设备直接死机。这个Bug在手动测试时很难触发但边界测试几万帧下去马上暴露。跑批量异常注入时建议头一次用500个无效请求加1个有效请求的组合观察从站是否还能正常响应有效请求——这考察的就是主站乱发东西时你的从站固件够不够稳。6.3 从站日志给设备留一个真实串口观察窗口很多嵌入式设备只有一个调试串口如果这个串口被MODBUS占用了现场调试就没法输出日志。我的做法是设备上保留一个独立的调试日志串口用另一个USB转TTL模块连接PC打印完整收发报文、解析状态和错误标志。打印格式一般是这样[T] 10.123 RX: 01 03 00 00 00 01 84 0A [T] 10.124 FRAME_OK addr01 func03 reg0000 cnt0001 crcOK [T] 10.130 TX: 01 03 02 0A 7B 8C 3F这样一个日志串口能看到的不只是最终结果还能看到从站内部每一步的判断结果。尤其在做主从联调时两边的日志一对照(是谁没回这个语义上的端到端问题)到底卡在哪个环节一目了然。6.4 把MODBUS集成进自己的协议分析框架最后说一个长远角度的事。如果你经常做嵌入式通信调试建议把这套组帧/CRC/解析的代码整理成一个独立模块做成一个平台无关的协议库。这样新项目要支持MODBUS时把库拿过来改几个回调函数就能用。我在实际项目中验证过一个struct加几个handler写得好的MODBUS协议栈可以做到数据收发完全解耦——底层UART、SPI、TCP都能用上层应用只需要维护寄存器表。这种抽象层次清晰的设计后期加新设备、新寄存器都会非常省心。再补充一句不同MCU厂家的外设库在串口接收中断的处理上略有差异但接收中断定时器超时状态机解析的组合在STM32、GD32、NXP、国产M0核、甚至老式C51上都跑得通。这套心智模型可以说是一通百通。

相关新闻

最新新闻

日新闻

周新闻

月新闻