ECU模拟器
问题
我想为我的汽车制作数字仪表/指示器,而无需坐在车里操作。
规划
我大致知道该怎么做:让我的车载诊断(OBD)设备与Arduino通信,Arduino像汽车的ECU(电子控制单元)一样发送信息。然后,OBD设备再将这些信息转发给连接到它的任何设备。
值得一提的是,我使用的OBD设备是基于ELM327平台的。我用的是Vgate iCar Pro,但所有廉价的OBD设备实际上都大同小异。它们只是外壳不同,或者连接手机/电脑的方式不同,例如WiFi、蓝牙低功耗(BLE)或物理连接。iCar Pro使用蓝牙低功耗(BLE)连接手机或其他支持BLE的设备,所以我选择了它。
这篇博文,我将重点介绍虚线矩形框内的区域,即Arduino接收和响应来自OBD设备的请求的部分。
如何?
有了大致的想法后,我接下来需要弄清楚如何接收和回复来自 OBD 设备的消息,那么汽车通常是如何做到这一点的呢?
大多数汽车都使用一种叫做控制器局域网 (CAN) 的技术。CAN 允许许多传感器(或其他任何设备)通过公共总线(CAN 总线)进行通信,而无需像网状网络那样互连,这有助于减少汽车中的线路数量。带有(某种程度上)标准化 ID 的 CAN 数据帧通过 CAN 总线发送。在这个项目中,我只需要 RPM 参数 ID,但还有很多其他参数。我找到了一个很方便的工具,它显示了大多数常见的 CAN ID 以及数据帧示例(https://www.csselectronics.com/pages/OBD-pid-table-on-board-diagnostics-j1979)。
为了让我的 Arduino 和 OBD 设备通信,我需要使用 CAN 总线将它们连接起来并发送 CAN 数据帧。接下来,我需要弄清楚如何实现 CAN 总线。那么,我有哪些选择呢?
由于这只是个周末项目,我希望它既经济实惠又能是个学习机会,所以我查找了支持CAN协议的微控制器以及实现该功能所需的电子元件。但当时疫情后订单积压严重,这些微控制器都缺货。所以我选择了最快捷但也最贵的方案,订购了一个MCP2515模块。
这些模块包含了通过 CAN 总线发送消息所需的一切。我只需要将它们连接到 Arduino 和 OBD 设备即可。
因此,加上 MCP2515 模块后,上面的图表现在看起来像下面这样,其中 MCP2515 处理 CAN,而 Arduino 决定如何处理消息。
组装
有了计划之后,接下来我需要弄清楚如何将 OBD 设备、MCP2515 和 Arduino 连接起来。由于 MCP2515 是一个相当常见的模块,所以有很多关于如何将其连接到 Arduino 的文档。
此外,我还连接了一个电位器来改变转速值。虽然可以通过代码随机化返回的值,但这并非必需,但有总比没有好。我还加装了一个 OBD 母头接口,方便连接 OBD 设备。如果没有这个接口,每次将 OBD 设备从车上移到模拟器上时,都必须手动将 MCP2515 连接到 OBD 设备的引脚上。
接线时需要注意以下几点:
VIN(输入电压)必须为 12V,因为你的 OBD 设备很可能需要 12V 电压,就像它连接到汽车上一样。所以,如果你使用的设备没有将 VIN 上的电压降压到 5V 或 3.3V 来驱动你的微控制器,那么你的微控制器很可能会烧毁。
对于这种设置,您需要将 MCP2515 上的 J1 接头引脚短接,这会给 CAN 总线增加 120 欧姆的电阻。如果您将 MCP2515 直接连接到汽车上,则无需这样做。
我还设计了一个可3D打印的盒子,用来放置ECU模拟器和电位器,并带有一个OBD接口。您可以在这里找到它:https://www.thingiverse.com/thing: 5532047
代码
组装完成后,我们就可以开始编写代码了。对于像这样的小型硬件相关项目,我喜欢使用 Arduino 框架和 PlatformIO。
我需要的代码必须满足以下要求:
-
利用电位器
-
使用 MCP2515 模块
-
处理传入的 RPM 消息
-
构建 RPM CAN 消息
-
发送 RPM 消息响应
我会一步一步地讲解,最后给出最终结果(如果你只想看代码,可以直接跳到最后)。
利用Arduino 框架读取电位器的电阻值非常简单。我们只需要使用analogRead相应的函数以及连接电位器信号线的头文件名称即可。例如,A0将电位器的值赋给一个变量的代码大致如下int potent_rpm = analogRead(A0);:
使用 MCP2515 模块,我不会为这个项目重新开发,而且有很多库可以处理 CAN 总线相关的功能。我使用的是https://github.com/autowp/arduino-mcp2515,因为它有文档并且支持发送和接收 CAN 消息。
处理传入的转速信息。当ECU模拟器收到来自OBD设备的传入信息时,它只需要关注CAN参数ID。因此,我们可以使用switch语句,并根据参数ID来决定发送什么信息。
例如
unsigned char p_id = read_can_msg.data[2];
switch (p_id)
{
case 0:
...
break;
case 12:
...
break;
default:
break;
}
现在我们知道了接收到的消息,就可以构建 RPM CAN 消息并发送响应了。借助我们引入的库,我们可以构建一条消息发送给 OBD 设备。同样,使用 CSS Electronics 的工具,您可以找到要发送的消息示例。switch 语句大致如下所示:
unsigned char p_id = read_can_msg.data[2];
switch (p_id)
{
case 0:
write_can_msg.can_id = 2024;
write_can_msg.can_dlc = 8;
write_can_msg.data[0] = 6;
write_can_msg.data[1] = 65;
write_can_msg.data[2] = 0;
write_can_msg.data[3] = 255;
write_can_msg.data[4] = 255;
write_can_msg.data[5] = 255;
write_can_msg.data[6] = 255;
write_can_msg.data[7] = 170;
mcp2515.sendMessage(&write_can_msg);
break;
case 12:
write_can_msg.can_id = 2024;
write_can_msg.can_dlc = 8;
write_can_msg.data[0] = 4;
write_can_msg.data[1] = 65;
write_can_msg.data[2] = 12;
write_can_msg.data[3] = potent_rpm;
write_can_msg.data[4] = 0;
write_can_msg.data[5] = 170;
write_can_msg.data[6] = 170;
write_can_msg.data[7] = 170;
mcp2515.sendMessage(&write_can_msg);
break;
default:
break;
}
您可能已经注意到案例 0,我添加它是因为我发现我用来调试 ECU 模拟器的 OBD 移动应用程序通常会首先请求该信息,以查看 ECU 上所有支持的 CAN 参数 ID。如果应用程序没有收到对该消息的响应,它们通常会超时。
如果你只想盗用ECU模拟器,最终的版本看起来是这样的:
#include <SPI.h>
#include <mcp2515.h>
struct can_frame read_can_msg;
struct can_frame write_can_msg;
MCP2515 mcp2515(10);
void setup() {
Serial.begin(9600);
mcp2515.reset();
mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ);
mcp2515.setNormalMode();
}
void loop() {
if (mcp2515.readMessage(&read_can_msg) == MCP2515::ERROR_OK) {
int potent_rpm = analogRead(A0) / 10;
unsigned char p_id = read_can_msg.data[2];
switch (p_id)
{
case 0:
write_can_msg.can_id = 2024;
write_can_msg.can_dlc = 8;
write_can_msg.data[0] = 6;
write_can_msg.data[1] = 65;
write_can_msg.data[2] = 0;
write_can_msg.data[3] = 255;
write_can_msg.data[4] = 255;
write_can_msg.data[5] = 255;
write_can_msg.data[6] = 255;
write_can_msg.data[7] = 170;
mcp2515.sendMessage(&write_can_msg);
break;
case 12:
write_can_msg.can_id = 2024;
write_can_msg.can_dlc = 8;
write_can_msg.data[0] = 4;
write_can_msg.data[1] = 65;
write_can_msg.data[2] = 12;
write_can_msg.data[3] = potent_rpm;
write_can_msg.data[4] = 0;
write_can_msg.data[5] = 170;
write_can_msg.data[6] = 170;
write_can_msg.data[7] = 170;
mcp2515.sendMessage(&write_can_msg);
break;
default:
break;
}
}
}
如果您遇到问题,以下几点或许会有帮助:
由于我使用的是 Arduino Uno 克隆板,波特率可能会有所不同Serial.begin(9600);。
int potent_rpm = analogRead(A0) / 10;因为我用的电位器阻值比较高,所以我把电位器的值除以十。
该线路的mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ);时钟频率应与你MCP2515模块上安装的时钟频率相匹配。我的模块是8MHz,但我见过其他模块是16MHz的。
最终结果
所有部件都连接好,代码也上传到 Arduino 后,最终产品完全符合我的预期。我成功设置了一个换挡指示灯,这在我的车上是很难实现的。以前每次想看看指示灯是否亮着,都得把油门踩到底。
我计划读取歧管压力并计算增压值(单位为 PSI),因为不知何故,我的车虽然出厂时就配备了涡轮增压器,但却没有显示涡轮增压器是否处于工作状态。
这跟dev.to上的大多数话题略有不同,但我在寻找相关帮助时,更希望得到一些指导,而不是规格表和那些看起来乱七八糟的时序图。
文章来源:https://dev.to/devjameshay/basic-ecu-simulator-using-an-arduino-1064




