恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Mongoose OS与Arduino兼容层实现ESP32 CAN总线通信
首页
资讯中心
/
基于Mongoose OS与Arduino兼容层实现ESP32 CAN总线通信
基于Mongoose OS与Arduino兼容层实现ESP32 CAN总线通信
发布时间:2026/9/2 5:47:29
简介本资源是一套面向嵌入式开发工程师与物联网系统学习者的ESP32 CAN总线通信实战项目聚焦于在Mongoose OS环境下绕过官方Arduino兼容层、直接集成ESP32 Arduino核心库实现高可靠性CAN通信适用于工业现场数据采集、车载诊断OBD原型开发及多节点CAN网络教学实验。压缩包共124个文件含67个头文件.h定义硬件抽象接口与CAN协议结构、24个C源文件.c如esp32-hal-uart.c、esp32-hal-spi.c等底层驱动实现、23个C文件.cpp封装文件系统写入与CAN消息处理逻辑以及静态库.a和构建配置文件.yml、.md整体体积2.19MB结构清晰、模块职责分明。已有73人下载学习读者可直接获取完整可编译工程、ESP32硬件抽象层封装代码、CAN数据落盘存储逻辑及跨平台Mongoose OS部署方案显著降低CAN通信功能在ESP32上的移植与调试门槛。1. 项目缘起为什么要在ESP32上折腾CAN总线最近在做一个车载数据采集的小项目核心需求是从汽车的OBD-II接口读取发动机的实时数据。OBD-II接口背后跑的就是CAN总线协议这几乎是现代汽车内部通信的“普通话”。手头正好有几块闲置的ESP32开发板性能强劲又带Wi-Fi和蓝牙想着如果能用ESP32直接读取CAN数据再通过无线网络上传到服务器岂不是省去了中间转换模块既简化了硬件结构又降低了成本这个想法听起来很美但实操起来第一个拦路虎就是开发环境的选择。ESP32的开发路子很多最主流的有两种一是乐鑫官方的ESP-IDF框架功能强大但学习曲线陡峭对只想快速实现功能的开发者来说有点“杀鸡用牛刀”二是我们熟悉的Arduino框架库生态丰富上手极快但对于CAN总线这种相对底层的通信其封装库的稳定性和灵活性有时会让人心里打鼓。就在我纠结是硬啃ESP-IDF还是将就用Arduino的CAN.h库时一个朋友提到了Mongoose OS。这是一个专门为物联网设备设计的开源操作系统它神奇地融合了两者的优点底层基于ESP-IDF保证了驱动和性能的可靠性而上层则提供了类似JavaScript或C的简易开发接口并且完美兼容Arduino的核心库。这意味着我可以用写Arduino sketch的熟悉感去调用ESP-IDF级别的稳定CAN驱动。这个“缝合怪”一样的特性瞬间吸引了我。于是一个基于“Arduino兼容层”的Mongoose OS项目专门用于实现ESP32的CAN总线通信就这么开始了。这个项目的目标很明确构建一个稳定、可配置的ESP32 CAN通信节点。它要能灵活地设置CAN总线速率如500kbps、250kbps可靠地收发标准帧和扩展帧并且将接收到的CAN数据包通过串口打印出来同时也可以通过简单的Web界面进行状态监控和配置。下面我就把从环境搭建、代码编写到调试踩坑的完整过程梳理出来。2. 开发环境搭建Mongoose OS与Arduino的“共生”模式很多人第一次接触Mongoose OS会觉得有点迷惑它既不是纯Arduino也不是纯ESP-IDF。理解它的“双模式”是顺利开始的关键。2.1 核心工具链安装Mongoose OS的核心是一个名为mos的命令行工具。它的安装非常简单在Linux/macOS上一条命令在Windows上则推荐使用其提供的独立工具包。安装后mos工具会帮你管理所有依赖包括交叉编译工具链、ESP-IDF的特定版本、以及各种库文件。你不需要像传统Arduino IDE那样手动去安装ESP32板卡支持包也不用担心ESP-IDF版本冲突的问题这一切都由mos统一管理这是它最大的优势之一。安装完mos后为ESP32创建一个新项目只需要执行mos create mongoose-os-esp32-can cd mongoose-os-esp32-can这条命令会生成一个标准项目骨架。但我们的目标是使用Arduino兼容库所以需要在项目配置文件mos.yml中显式地启用这个功能。2.2 关键配置启用Arduino兼容层项目根目录下的mos.yml文件是Mongoose OS项目的“大脑”。我们需要在其中添加两个关键配置libs: - origin: https://github.com/mongoose-os-libs/arduino-compat - origin: https://github.com/mongoose-os-libs/can manifest_version: 2017-09-29第一行arduino-compat就是灵魂所在。它不是一个模拟器而是一个精妙的适配层。这个库将Arduino的核心函数如digitalWrite,delay,Serial.print映射到Mongoose OS底层是ESP-IDF的相应实现上。这意味着你几乎可以无缝地使用绝大部分你熟悉的Arduino库。第二行can库则是Mongoose OS官方维护的CAN总线驱动库。它基于ESP-IDF的twai驱动ESP32的CAN控制器原名TWAI提供了更稳定和功能更丰富的API。我们将主要使用这个库而不是Arduino生态里常见的第三方CAN库。2.3 开发与编译流程配置好后你的开发流程通常是这样的在src目录下编写你的主文件例如main.c或main.cpp。使用mos build --platform esp32命令在本地或云端编译固件。mos会自动下载所有声明的库arduino-compat和can及其依赖。使用mos flash命令通过串口将固件烧录到ESP32开发板。使用mos console命令打开串口监视器查看日志输出。这个流程和PlatformIO有些类似但更加一体化尤其适合需要稳定底层驱动和云端编译协作的团队项目。注意arduino-compat库会占用一定的Flash和RAM空间。如果你的项目对资源极其敏感需要评估是否值得。但对于大多数应用其带来的开发效率提升是远超这点开销的。3. 硬件连接与CAN控制器初始化纸上谈兵结束接下来是实战。要让ESP32说“CAN话”首先得把硬件线路接对。3.1 ESP32的CAN引脚与硬件连接ESP32内部集成了CAN控制器但需要外接一个CAN收发器芯片如常见的SN65HVD230、TJA1050才能连接到物理CAN总线上。控制器负责协议处理收发器负责电平转换。ESP32的CAN控制器默认使用以下GPIORX (接收) GPIO 4TX (发送) GPIO 5这是ESP-IDFtwai驱动的默认引脚Mongoose OS的can库也沿用了这一设定。如果你的板子这些引脚被占用也可以在代码中重新映射但建议优先使用默认引脚以避免不必要的麻烦。连接示意图如下ESP32 DevKit (例如ESP32-WROOM-32) GPIO4 (CAN_RX) --- CAN收发器芯片的RXD引脚 GPIO5 (CAN_TX) --- CAN收发器芯片的TXD引脚 ESP32的GND --- 收发器芯片的GND ESP32的3.3V --- 收发器芯片的VCC (注意必须是3.3V兼容的收发器如SN65HVD230) CAN收发器芯片 CANH引脚 --- 连接至CAN总线的CAN_High线 CANL引脚 --- 连接至CAN总线的CAN_Low线一个至关重要的细节终端电阻。CAN总线两端必须各接一个120欧姆的终端电阻用以消除信号反射保证通信质量。如果你的ESP32节点位于总线末端那么在你的CAN收发器模块上或者直接在CANH和CANL之间跨接一个120Ω电阻是必须的。很多集成好的CAN收发器模块已经自带了一个120Ω电阻并通过一个跳线帽来选择是否启用使用时务必确认其状态。3.2 在代码中初始化和配置CAN硬件连接妥当后我们开始在main.c或main.cpp中编写软件部分。得益于arduino-compat我们可以混用Arduino风格和Mongoose OS原生风格的代码。首先包含必要的头文件#include mgos.h #include mgos_can.h #include Arduino.h // 允许使用Serial等Arduino对象接着在Mongoose OS的应用入口函数mgos_app_init()中进行CAN控制器的初始化。这里我强烈建议将配置参数提取为宏或变量方便修改// CAN总线配置 #define CAN_TX_PIN 5 // GPIO5 #define CAN_RX_PIN 4 // GPIO4 #define CAN_BAUDRATE 500000 // 500kbps 常见车载速率 bool mgos_app_init(void) { // 初始化Arduino兼容层主要是Serial Serial.begin(115200); struct mgos_can_config cfg { .bitrate CAN_BAUDRATE, .tx_pin CAN_TX_PIN, .rx_pin CAN_RX_PIN, .mode MGOS_CAN_MODE_NORMAL, // 正常模式 非只听模式 }; // 初始化CAN控制器 if (!mgos_can_configure(cfg)) { LOG(LL_ERROR, (CAN controller configuration failed!)); // 这里可以加入错误处理如闪烁LED报警 return false; } LOG(LL_INFO, (CAN controller initialized successfully at %d bps, CAN_BAUDRATE)); Serial.println(CAN System Ready.); // 启动一个定时器用于周期性发送测试帧或执行其他任务 mgos_set_timer(1000, MGOS_TIMER_REPEAT, can_periodic_task_cb, NULL); return true; }关键点解析mgos_can_configure这个函数是Mongoose OScan库的核心它封装了ESP-IDFtwai_driver_install和twai_start等复杂操作。其返回值直接告诉你初始化是否成功比直接操作ESP-IDF驱动更直观。模式选择MGOS_CAN_MODE_NORMAL是正常收发模式。还有一个有用的模式是MGOS_CAN_MODE_LISTEN_ONLY只听模式在这个模式下节点只接收总线数据而不发送任何报文包括ACK位非常适合用于总线监听、分析而不干扰原有通信。日志系统LOG(LL_INFO, (...))是Mongoose OS原生的日志宏输出格式更统一且可以通过mos工具进行远程日志查看。Serial.println是Arduino方式两者可以共存方便调试。4. CAN报文收发实战从发送测试帧到解析真实数据初始化成功只是万里长征第一步接下来是实现数据的收发。这是整个通信系统的核心。4.1 构造与发送CAN帧假设我们要周期性地向总线发送一个模拟的发动机转速RPM信号ID为0x123标准帧数据为2个字节。我们创建一个定时器回调函数static void can_periodic_task_cb(void *arg) { struct mgos_can_msg msg {0}; // 初始化消息结构体 // 填充CAN消息 msg.id 0x123; // CAN ID 11位标准帧 msg.flags 0; // 标准帧 数据帧 msg.len 2; // 数据长度2字节 msg.data[0] 0x12; // RPM高字节 (模拟值) msg.data[1] 0x34; // RPM低字节 (模拟值) // msg.data[2]... 未使用的数据字节默认为0 // 尝试发送消息 if (mgos_can_send(msg)) { // 发送成功可以记录或处理 Serial.printf(Sent: ID0x%03X, Data%02X %02X\n, msg.id, msg.data[0], msg.data[1]); } else { // 发送失败可能是总线关闭、队列满或仲裁失败 LOG(LL_WARN, (Failed to send CAN message 0x%03X, msg.id)); // 在实际应用中这里可能需要更复杂的错误恢复逻辑 } (void) arg; }发送函数的细节与坑mgos_can_send是非阻塞的。它把消息放入发送队列后立即返回成功。真正的发送由硬件在后台处理。这意味着发送函数的调用速度可以很快但你需要确保发送队列的深度足够在mgos_can_config中可配置tx_queue_len否则在总线负载高时可能丢帧。ID与帧类型msg.id直接赋值即可库函数会根据ID大小自动判断是11位标准帧还是29位扩展帧。如果你想显式指定可以通过msg.flags设置MGOS_CAN_FLAG_EXTD扩展帧或MGOS_CAN_FLAG_RTR远程帧。数据长度msg.len必须准确设置为1-8对应实际有效数据的字节数。即使你只用了data[0]如果len2总线也会发送两个字节第二个字节是data[1]的当前值可能是未初始化的垃圾数据这会导致通信错误。4.2 接收与解析CAN帧发送是主动的接收则是被动的。我们需要设置一个接收回调函数或者在一个循环里主动去读取接收队列。使用回调函数是更高效、更事件驱动的方式。首先在mgos_app_init中注册接收回调// ... CAN初始化之后 ... mgos_can_add_rx_handler(can_rx_callback, NULL); // 注册全局接收回调然后实现回调函数can_rx_callbackstatic void can_rx_callback(const struct mgos_can_msg *msg, void *user_data) { // 这个函数在中断上下文中被调用处理要快不能做耗时操作 char data_str[32] {0}; char *p data_str; // 将数据字节格式化为字符串方便打印 for (int i 0; i msg-len; i) { p sprintf(p, %02X , msg-data[i]); } // 使用Arduino Serial打印格式更自由 Serial.printf(RCV: ID0x%08X, Len%d, Data[%s]\n, msg-id, msg-len, data_str); // 可以根据ID进行具体的业务逻辑解析 switch (msg-id) { case 0x7E8: // 假设这是OBD标准响应ID parse_obd_response(msg); break; case 0x0CF00203: // 一个29位扩展帧ID的例子 parse_extended_frame(msg); break; default: // 处理其他ID或记录 break; } (void) user_data; }接收回调的注意事项执行上下文这个回调函数通常在CAN中断服务程序ISR中被调用。这意味着你不能在里面使用delay()、进行大量的动态内存分配如malloc、或调用可能阻塞的函数如某些网络操作。它的任务应该是快速拷贝或解析关键数据然后通过队列、标志位等方式通知主循环中的任务进行后续处理。这是嵌入式开发中常见的“中断快进快出”原则。数据有效性回调被触发时msg结构体中的数据已经是完整的、通过CRC校验的一帧。你无需再检查帧完整性。性能考量如果总线负载极高每秒有成千上万帧这个回调会被频繁触发。确保你的处理逻辑足够轻量否则可能丢失帧。Mongoose OS的can库内部有一个接收缓冲区但如果你的处理速度跟不上接收速度缓冲区还是会溢出。4.3 解析真实OBD-II数据示例从汽车CAN总线读取OBD-II数据通常需要你先发送一个请求帧例如询问发动机转速然后等待并解析响应帧。这是一个简单的请求-响应模型。// 发送一个请求发动机转速的标准OBD-II PID请求帧 void request_engine_rpm(void) { struct mgos_can_msg req_msg {0}; // 标准OBD请求帧ID 0x7DF (广播到所有ECU) 数据长度8 格式通常为 02 01 0C 00 00 00 00 00 // 02: 后续数据字节数 01: 服务模式当前数据 0C: PID发动机转速 req_msg.id 0x7DF; req_msg.len 8; req_msg.data[0] 0x02; req_msg.data[1] 0x01; req_msg.data[2] 0x0C; // PID 0x0C Engine RPM // data[3] to data[7] 通常为0x00 if (mgos_can_send(req_msg)) { LOG(LL_DEBUG, (Engine RPM request sent.)); } } // 在接收回调中解析响应 static void parse_obd_response(const struct mgos_can_msg *msg) { // OBD响应ID通常是请求ID 8 即 0x7E8 也可能来自特定ECU如0x7E9 if (msg-id 0x7E8 || msg-id 0x7E9) { // 检查PID是否匹配我们请求的 // 响应格式 04 41 0C 12 34 AA AA AA AA // 04: 后续字节数 41: 服务模式0x40响应 0C: PID 12 34: 数据 if (msg-len 4 msg-data[1] 0x41 msg-data[2] 0x0C) { // 计算发动机转速RPM (256 * A B) / 4 其中A和B是数据字节 uint16_t rpm (256 * msg-data[3] msg-data[4]) / 4; Serial.printf(Engine RPM: %u\n, rpm); // 这里可以将rpm值存入全局变量或通过Wi-Fi发送出去 } } }这个例子展示了从原始CAN报文到具体工程数值的完整转换链。理解你所在系统的CAN数据库DBC文件或协议文档是正确解析数据的前提。5. 错误处理与总线状态监控一个健壮的CAN系统不能只关心正常收发还必须能应对异常。ESP32的CAN控制器提供了丰富的错误状态信息。5.1 获取与解读错误计数器CAN协议有发送错误计数器TEC和接收错误计数器REC。它们的值直接反映了总线健康状况。我们可以定期查询这些计数器void monitor_can_bus_health(void) { struct mgos_can_status status; if (mgos_can_get_status(status)) { Serial.printf(CAN Status: ); Serial.printf(RxErr%lu, TxErr%lu, , status.rx_error_counter, status.tx_error_counter); Serial.printf(State%s\n, can_state_to_str(status.state)); } } const char* can_state_to_str(mgos_can_state_t state) { switch(state) { case MGOS_CAN_STATE_STOPPED: return STOPPED; case MGOS_CAN_STATE_RUNNING: return RUNNING; case MGOS_CAN_STATE_BUS_OFF: return BUS-OFF; // 严重错误 控制器已脱离总线 default: return UNKNOWN; } }错误计数器增长如果REC或TEC缓慢增长可能是偶发的总线干扰。如果TEC急剧增加可能是本节点发送有问题如终端电阻缺失、线路短路。BUS-OFF状态这是最严重的错误状态。当TEC超过255时控制器会进入“Bus-Off”状态自动与总线断开连接以阻止其持续发送错误帧干扰网络。控制器随后会尝试自动恢复根据协议在检测到128次11位连续隐性位后。你的代码需要监控这个状态并可能进行报警或复位操作。5.2 处理总线关闭Bus-Off与恢复在mgos_can_config中可以设置总线关闭后的恢复行为但更主动的做法是在应用层监控static void can_bus_off_recovery_task(void *arg) { struct mgos_can_status status; mgos_can_get_status(status); if (status.state MGOS_CAN_STATE_BUS_OFF) { LOG(LL_ERROR, (CAN Bus-Off detected! Attempting recovery...)); // 1. 首先停止控制器 mgos_can_stop(); // 2. 等待一段时间可选 mgos_msleep(100); // 3. 重新按照原配置初始化 struct mgos_can_config cfg {...}; // 复用之前的配置 if (mgos_can_configure(cfg)) { LOG(LL_INFO, (CAN controller recovered from Bus-Off.)); } else { LOG(LL_ERROR, (CAN recovery failed!)); } } // 每隔一段时间检查一次 mgos_set_timer(5000, MGOS_TIMER_REPEAT, can_bus_off_recovery_task, NULL); }在实际项目中我会将总线状态和错误计数器通过Wi-Fi上报到监控平台这样就能远程诊断是某个节点出了问题还是整个总线网络存在干扰。6. 系统集成与进阶功能一个孤立的CAN节点价值有限。将它与ESP32的其他能力结合才能发挥最大效用。6.1 通过Wi-Fi转发CAN数据这是本项目最典型的应用场景。我们可以使用Mongoose OS内置的、极其易用的网络库来创建一个HTTP API端点或者通过WebSocket实时推送CAN数据。#include mgos_net.h #include mgos_http_server.h // 定义一个全局缓冲区或队列来存储最新的CAN数据 struct can_data_point { uint32_t id; uint8_t len; uint8_t data[8]; uint64_t timestamp; } latest_can_msg; // 在接收回调中更新数据注意线程安全这里简化处理 static void can_rx_callback(const struct mgos_can_msg *msg, void *user_data) { latest_can_msg.id msg-id; latest_can_msg.len msg-len; memcpy(latest_can_msg.data, msg-data, msg-len); latest_can_msg.timestamp mgos_uptime_micros(); // ... 其他解析逻辑 ... } // 创建一个HTTP接口 /api/can/latest static void handle_can_latest(struct mg_connection *nc, void *ev_data, void *user_data) { struct http_message *hm (struct http_message *) ev_data; mgos_http_send_responsef(nc, 200, Content-Type: application/json\r\n, {\id\:%u,\data\:[%u,%u,%u,%u,%u,%u,%u,%u],\ts\:%llu}, latest_can_msg.id, latest_can_msg.data[0], latest_can_msg.data[1], latest_can_msg.data[2], latest_can_msg.data[3], latest_can_msg.data[4], latest_can_msg.data[5], latest_can_msg.data[6], latest_can_msg.data[7], latest_can_msg.timestamp); (void) hm; (void) user_data; } // 在mgos_app_init中注册HTTP端点 bool mgos_app_init(void) { // ... CAN初始化 ... mgos_register_http_endpoint(/api/can/latest, handle_can_latest, NULL); // ... 连接Wi-Fi ... return true; }这样同一网络内的电脑或手机就能通过访问http://esp32-ip/api/can/latest来获取最新的CAN报文了。更进一步可以集成MQTT客户端将数据发布到云端物联网平台如阿里云IoT、AWS IoT实现远程监控。6.2 基于Web的配置界面Mongoose OS原生支持设备配网Wi-Fi和RPC远程过程调用。我们可以轻松地暴露几个RPC方法用于动态修改CAN总线速率、开关接收过滤器等并为其创建一个简单的Web界面。首先在mos.yml中启用RPC和Dashboard库libs: - origin: https://github.com/mongoose-os-libs/rpc-service - origin: https://github.com/mongoose-os-libs/dashboard然后注册一个RPC方法CAN.SetBitratestatic void can_set_bitrate_handler(struct mg_rpc_request_info *ri, void *cb_arg, struct mg_rpc_frame_info *fi, struct mg_str args) { int new_bitrate; if (json_scanf(args.p, args.len, ri-args_fmt, new_bitrate) ! 1) { mg_rpc_send_errorf(ri, 400, Invalid arguments); return; } // 停止当前CAN mgos_can_stop(); // 使用新速率重新配置 struct mgos_can_config cfg {...}; cfg.bitrate new_bitrate; bool success mgos_can_configure(cfg); mg_rpc_send_responsef(ri, {%Q: %B}, success, success); (void) cb_arg; (void) fi; } // 在init中注册 mg_rpc_add_handler(mgos_rpc_get_global(), CAN.SetBitrate, , can_set_bitrate_handler, NULL);编译烧录后你可以通过mos工具调用这个RPCmos call CAN.SetBitrate {bitrate: 250000}。更棒的是启用Dashboard库后设备会自带一个Web界面通常位于http://esp32-ip你可以在那里看到所有已注册的RPC方法并直接调用无需自己写前端页面。6.3 功耗优化考量如果项目是电池供电功耗就至关重要。CAN控制器本身在活跃状态下功耗不低。Mongoose OS的can库支持进入“只听模式”MGOS_CAN_MODE_LISTEN_ONLY这会显著降低功耗因为你不再发送ACK位控制器部分电路可以休眠。在不需要主动发送的监控场景下这是很好的省电方式。此外你可以结合ESP32的深度睡眠功能。让ESP32周期性地唤醒例如通过定时器或外部中断启动CAN控制器接收一批数据并通过Wi-Fi发送然后再次进入深度睡眠。这需要仔细设计你的硬件确保CAN收发器也能被断电或进入低功耗模式和软件流程。7. 项目总结与避坑指南回顾整个项目使用Mongoose OS配合其Arduino兼容层来开发ESP32的CAN应用确实是一条“捷径”。它让你避开了ESP-IDF复杂的构建系统和驱动细节又能享受到其稳定性的红利同时保留了Arduino生态的便捷性。几个我踩过或见过的“坑”值得你特别注意电源与地线噪声这是导致CAN通信不稳定、错误帧频发的头号元凶。务必确保你的ESP32和CAN收发器使用干净、稳定的3.3V电源。如果从车载点烟器或开关电源取电强烈建议增加LC滤波或使用独立的LDO稳压模块。数字电路ESP32的快速开关噪声很容易通过电源串扰到模拟的CAN差分信号上。终端电阻必不可少我见过不止一个项目因为忘记接终端电阻而无法通信。用万用表测量总线末端的CANH和CANL之间的电阻应该在60欧姆左右两个120欧姆并联。如果不是检查你的终端电阻跳线。波特率匹配必须和总线上的其他节点使用完全相同的波特率。常见的车载CAN有500kbps高速CAN和125kbps/250kbps低速CAN。如果无法通信先用示波器或专业的CAN分析仪确认总线速率。Mongoose OS库的版本arduino-compat和can库都在不断更新。在mos.yml中指定一个稳定的版本号如- origin: https://github.com/mongoose-os-libs/can?version1.0比使用master分支更可靠可以避免因库更新带来的意外编译错误或行为变化。中断回调中的操作再次强调在can_rx_callback等中断上下文中不要调用任何可能阻塞或分配内存的函数。简单的赋值、拷贝、设置标志位是安全的。复杂的处理请交给由mgos_set_timer创建的低优先级任务。线缆与连接CAN总线推荐使用双绞线如CAT5网线中的一对并确保连接器牢固。接触不良会导致间歇性通信失败。这个项目源码包即标题中的zip文件里应该包含了完整的、可编译的mos.yml、main.c、以及可能用到的Web界面文件。拿到后你只需要安装好mos工具接好硬件一条mos build和mos flash命令就能让整个系统跑起来。希望这份详细的梳理能帮你绕过我走过的弯路更快地构建出稳定可靠的ESP32 CAN通信系统。本文还有配套的精品资源点击获取