在物联网和嵌入式开发领域,CH340作为一款常用的USB转串口芯片,广泛应用于各类硬件设备与计算机的通信场景。而Unity作为跨平台游戏引擎,在工业仿真、虚拟现实等领域的应用日益广泛。这个项目的核心目标,是打通Unity与Android设备之间的CH340通信链路,实现Unity应用直接调用Android设备上的串口数据。
我去年为一个智能家居控制系统开发类似功能时,发现市面上缺乏完整的Unity与Android串口通信解决方案。传统方案往往需要开发者分别处理Android原生层和Unity层的通信逻辑,调试过程繁琐且容易出错。通过Unity与Android Studio的相互调用机制,我们可以将CH340的串口操作封装到Android原生代码中,再通过JNI接口暴露给Unity调用。
这个方案的核心在于建立三层通信架构:
mermaid复制graph TD
A[Unity C#脚本] -->|调用| B[Android Java接口]
B -->|JNI| C[CH340驱动]
C -->|USB| D[硬件设备]
重要提示:在实际开发中需要特别注意AndroidManifest.xml的USB权限声明,缺少声明会导致设备无法识别
首先在Android Studio项目中添加CH340驱动支持。最新版的驱动库可以从厂商官网获取,建议使用Gradle依赖方式引入:
gradle复制implementation 'com.github.mik3y:usb-serial-for-android:3.4.3'
创建核心串口服务类时需要处理几个关键点:
java复制public class CH340Service {
private static final String TAG = "CH340Service";
private UsbSerialPort serialPort;
// 初始化串口
public boolean initPort(UsbDevice device, int baudRate) {
UsbSerialDriver driver = UsbSerialProber.getDefaultProber().probeDevice(device);
if (driver == null) {
Log.e(TAG, "No compatible driver found");
return false;
}
serialPort = driver.getPorts().get(0);
try {
serialPort.open(connection);
serialPort.setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1,
UsbSerialPort.PARITY_NONE);
return true;
} catch (IOException e) {
Log.e(TAG, "Error setting up device: " + e.getMessage());
return false;
}
}
// 数据发送方法
public void sendData(byte[] data) {
try {
serialPort.write(data, 0);
} catch (IOException e) {
Log.e(TAG, "Error sending data: " + e.getMessage());
}
}
}
在Unity中需要通过AndroidJavaClass和AndroidJavaObject来调用Android原生代码。建议封装一个专门的串口管理类:
csharp复制public class CH340Manager : MonoBehaviour
{
private AndroidJavaObject unityActivity;
private AndroidJavaObject ch340Service;
void Start()
{
// 获取当前Activity实例
AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
// 初始化CH340服务
AndroidJavaClass serviceClass = new AndroidJavaClass("com.example.ch340.CH340Service");
ch340Service = serviceClass.CallStatic<AndroidJavaObject>("getInstance", unityActivity);
}
public void SendCommand(string command)
{
byte[] data = System.Text.Encoding.ASCII.GetBytes(command);
ch340Service.Call("sendData", data);
}
}
Android系统对USB设备访问有严格的权限控制,需要处理以下几种情况:
java复制public boolean hasPermission(UsbDevice device) {
return usbManager.hasPermission(device);
}
java复制private static final int USB_PERMISSION_REQUEST_CODE = 1;
public void requestPermission(UsbDevice device) {
PendingIntent permissionIntent = PendingIntent.getBroadcast(context,
USB_PERMISSION_REQUEST_CODE,
new Intent(ACTION_USB_PERMISSION),
PendingIntent.FLAG_IMMUTABLE);
usbManager.requestPermission(device, permissionIntent);
}
xml复制<receiver android:name=".UsbPermissionReceiver">
<intent-filter>
<action android:name="com.example.USB_PERMISSION" />
</intent-filter>
</receiver>
为了确保通信可靠性,建议设计简单的帧协议:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| SOF | 1 | 起始符0xAA |
| LEN | 1 | 数据长度 |
| CMD | 1 | 命令类型 |
| DATA | N | 有效载荷 |
| CRC | 1 | 校验和 |
Unity端的协议解析示例:
csharp复制private void ProcessReceivedData(byte[] rawData)
{
if(rawData[0] != 0xAA) return;
int length = rawData[1];
byte command = rawData[2];
byte[] payload = new byte[length];
Array.Copy(rawData, 3, payload, 0, length);
byte crc = CalculateCRC(rawData, 3 + length);
if(crc != rawData[3 + length]) return;
// 处理有效数据
switch(command)
{
case 0x01:
HandleSensorData(payload);
break;
case 0x02:
HandleDeviceStatus(payload);
break;
}
}
由于串口通信可能阻塞主线程,建议采用生产者-消费者模式:
java复制// Android端
private final LinkedBlockingQueue<byte[]> sendQueue = new LinkedBlockingQueue<>();
private void startSendThread() {
new Thread(() -> {
while (!Thread.interrupted()) {
try {
byte[] data = sendQueue.take();
serialPort.write(data, 0);
} catch (Exception e) {
Log.e(TAG, "Send thread error", e);
}
}
}).start();
}
// Unity端
private void Update()
{
while(receivedQueue.Count > 0)
{
byte[] data = receivedQueue.Dequeue();
ProcessReceivedData(data);
}
}
现象:Unity无法检测到连接的CH340设备
排查步骤:
xml复制<uses-feature android:name="android.hardware.usb.host" />
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
xml复制<resources>
<usb-device
vendor-id="0x1a86"
product-id="0x7523" />
</resources>
优化方案:
java复制serialPort.setParameters(baudRate, 8, STOPBITS_1, PARITY_NONE);
serialPort.setDTR(true);
serialPort.setRTS(true);
性能优化技巧:
java复制private static final int BUFFER_SIZE = 4096;
private CircularByteBuffer receiveBuffer = new CircularByteBuffer(BUFFER_SIZE);
private void startReadThread() {
new Thread(() -> {
byte[] buffer = new byte[1024];
while (!Thread.interrupted()) {
try {
int len = serialPort.read(buffer, 0);
if(len > 0) {
receiveBuffer.put(buffer, 0, len);
}
} catch (IOException e) {
Log.e(TAG, "Read error", e);
}
}
}).start();
}
通过设备ID区分多个CH340设备:
csharp复制public void ConnectToDevice(string deviceId)
{
ch340Service.Call("connectDevice", deviceId);
}
Android端实现:
java复制private HashMap<String, UsbSerialPort> deviceMap = new HashMap<>();
public void connectDevice(String deviceId) {
UsbSerialPort port = deviceMap.get(deviceId);
if(port != null && !port.isOpen()) {
port.open(usbConnection);
}
}
集成AES加密保障数据安全:
java复制public byte[] encryptData(byte[] raw) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameter);
return cipher.doFinal(raw);
}
Unity端解密:
csharp复制private byte[] DecryptData(byte[] encrypted)
{
using(Aes aes = Aes.Create())
{
aes.Key = encryptionKey;
aes.IV = initializationVector;
ICryptoTransform decryptor = aes.CreateDecryptor();
using(MemoryStream ms = new MemoryStream(encrypted))
using(CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
byte[] decrypted = new byte[encrypted.Length];
int bytesRead = cs.Read(decrypted, 0, decrypted.Length);
Array.Resize(ref decrypted, bytesRead);
return decrypted;
}
}
}
实现通信质量监控:
java复制public class TrafficStats {
private long totalBytesSent;
private long totalBytesReceived;
public void updateSent(int bytes) {
totalBytesSent += bytes;
}
public float getKbpsSent() {
return totalBytesSent / 1024f / getUptime();
}
}
Unity端显示:
csharp复制private void OnGUI()
{
float kbps = (float)androidService.Call<float>("getCurrentKbps");
GUI.Label(new Rect(10, 10, 200, 20), $"Speed: {kbps:F2} KB/s");
}
在实际项目中,我发现最影响稳定性的往往是USB线材质量和Android设备的供电能力。曾经遇到一个案例,使用廉价USB线导致数据传输错误率高达15%,更换优质线材后立即降至0.1%以下。建议开发者在测试阶段就使用最终部署环境的硬件配置。