android-USB-STM32

绿色再生·STM32+安卓4G设计的智能远程操作巡视机器人小车

https://mp.weixin.qq.com/s/Q1wSua7adrXZghVAF2Hwlg

【1】项目功能介绍

https://mp.weixin.qq.com/s/Q1wSua7adrXZghVAF2Hwlg

本项目设计并实现一款基于STM32微控制器的远程遥控安卓小车系统。该系统充分利用了淘汰下来的安卓旧手机作为车载信息处理单元,不仅实现了资源的有效再利用,还结合4G网络技术以及先进的流媒体服务和物联网技术,搭建起一套集远程操控、实时视频音频传输于一体的高效解决方案。

车载的旧安卓手机通过USB线连接到STM32主控板上,接收并执行来自远端手机APP的指令。这款由Qt开发的Android APP能够利用4G网络实现实时在线,并通过摄像头采集音视频数据,通过RTMP协议将这些数据推送到华为云ECS服务器上的NGINX流媒体服务器,从而实现高清流畅的远程视频监控。

为了实现双向交互和低延迟控制,整个系统还借助MQTT协议连接至华为云IOT服务器。另一台安装了同样由Qt开发的Android手机APP的终端设备,可以通过该APP拉取小车端的实时音视频流进行播放,并通过方向键菜单实现对小车的精准远程操控。这种设计不仅极大地拓展了传统遥控小车的功能性与实用性,还为其他类似应用场景提供了可借鉴的技术框架。

【2】设计实现的功能

(1)STM32主控板功能:

  • 控制4个电机:STM32通过L298N驱动芯片驱动4个电机,实现小车的前进、后退、左转、右转等动作。

  • 数据通信:STM32通过USB接口与安卓手机通信,接收手机APP发送的控制指令,并将小车的状态信息(如电量、速度、位置等)发送回手机。

  • 电源管理:管理2节18650锂电池的供电,确保电压稳定并监控电池电量。

(2)安卓手机APP功能

  • 控制指令下发:手机APP通过USB接口向STM32发送控制指令,控制小车的动作。

  • 视频和音频流获取:APP从手机摄像头和麦克风获取视频和音频流,并进行编码处理。

  • 流媒体推流:通过RTMP协议将编码后的视频和音频流推送到华为云ECS服务器+NGINX搭建的RTMP流媒体服务器。

  • MQTT连接:APP通过MQTT协议与华为云IOT服务器建立连接,实现双向通信。

(3)华为云服务器功能:

  • RTMP流媒体服务:接收并转发安卓手机APP推送的视频和音频流。

  • MQTT服务:作为MQTT消息代理,实现远程手机与STM32主控板之间的通信。

(4)远程Android手机APP功能:

  • 实时视频和音频播放:从华为云ECS服务器拉取视频和音频流,并实时显示在APP界面上。

  • MQTT连接:与华为云IOT服务器建立连接,接收STM32主控板发送的小车状态信息。

  • 远程控制:提供方向键控制菜单,允许用户远程控制小车前进、后退、转弯等动作。

【3】项目硬件模块组成

(1)电源模块:

  • 电池组:采用两节18650锂电池作为供电源,它们具有高能量密度、体积小的特点,能够为整个系统提供稳定的直流电能。

(2)主控模块:

  • STM32微控制器:这是整个小车的核心控制单元,负责处理所有的逻辑运算和数据通信任务。通过编程实现对电机驱动、USB通信、网络连接等功能的控制。

(3)电机驱动模块:

  • L298N驱动芯片:用于驱动底座上的四个电机,L298N是一个高性能的H桥电机驱动器,可以接收来自STM32的信号,转换为足够驱动电机工作的电流和电压,并且支持电机正反转及速度调节

(4)移动平台模块:

  • 四个直流电机:直接安装在小车底座上,通过L298N驱动进行精确的速度和方向控制,以实现小车前进、后退、左右转弯等运动功能。

(5)通信模块:

  • USB接口:STM32主控板通过USB线与安卓手机物理连接,实现数据传输,接收来自手机APP的控制指令。

  • 4G模组:集成在安卓手机内部,插入SIM卡后可实现高速无线网络连接,使小车能够在远程环境下通过互联网与其他设备通信。

(6)多媒体采集模块:

  • 安卓手机摄像头:用于捕捉实时视频和音频信息,是小车端环境感知的关键组件。

(7)云服务交互模块:

  • 华为云ECS服务器+NGINX RTMP流媒体服务器:小车端将采集到的音视频流推送到华为云服务器上,通过RTMP协议实现实时音视频的低延迟传输和分发。

  • 华为云IOT服务器:小车和远端控制手机均通过MQTT协议与之建立连接,实现远程数据交换和控制命令的下发。

【4】功能总结

(1)电机驱动与控制:通过STM32微控制器和L298N驱动芯片,实现对小车上四个电机的精确控制,包括前进、后退、左转、右转等动作,从而控制小车的移动方向和速度。

(2)无线通信与数据传输:STM32与安卓手机之间通过USB接口建立通信,实现控制指令的下发和小车状态信息的上传。同时,安卓手机通过4G网络连接到华为云服务器,实现了远程控制命令的远程传输和视频音频流的推送。

(3)流媒体推流与播放:安卓手机APP能够捕获手机摄像头和麦克风的视频和音频流,通过RTMP协议推送到华为云服务器。另一台安卓手机APP则从服务器拉取这些流,实现实时播放,从而允许用户远程观看小车的实时画面和音频。

(4)华为云服务器支持:华为云ECS服务器和NGINX搭建的RTMP流媒体服务器负责接收、转发视频和音频流,确保流媒体的稳定性和实时性。同时,华为云IOT服务器通过MQTT协议提供消息代理服务,实现远程手机与STM32之间的双向通信。

(5)用户界面与交互设计:安卓手机APP提供了直观的用户界面,包括控制按钮、状态显示、视频播放器等,使用户能够方便地对小车进行控制、观看视频、监听音频,以及监控小车的状态信息。

(6)远程控制:通过结合STM32的电机控制、华为云服务器的数据处理和传输,以及安卓手机的用户界面和交互设计,实现了从远程手机到小车的远程控制功能。用户可以在远离小车的地点,通过手机APP发出控制指令,实时观察小车的动作和周围环境。

在启动MainActivty 的Manifest声明USB授权 和在 Service初始化时 代码调用requestPermission 声明USB授权有什么区别

Manifest方式:适合需要系统级集成的普通应用 如果你不授权 你连APP都无法启动

在启动MainActivty 的Manifest声明USB授权,启动程序会 弹出系统 让用户手动授权 否则无法进入 启动MainActivty 无法启动App

Kotlin 复制代码
<activity
  android:name=".MainActivity"
 android:exported="true">
 <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
       <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
     </intent-filter>
            
     <meta-data
       android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
        android:resource="@xml/device_filter" />  
				
        </activity>
Kotlin 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 指定要过滤的USB设备 -->
    <usb-device
        vendor-id="1155"   <!-- 0x0483,STMicroelectronics的VID -->
        product-id="22336" <!-- 0x5740,STM32 CDC设备的PID -->
        class="2"          <!-- 通信设备类 -->
        subclass="2" />    <!-- ACM子类(抽象控制模型) -->
</resources>

哈哈
**requestPermission()代码方式:**适合需要精确控制请求时机的场景,可以让你先启动app 进入主界面,但是代码方式

usbManager.requestPermission()也是会弹框的

这个我们是可以先启动APP 进入主Activity, 只是用户操作到需要使用USB设备时,触发启动了 Service初始化时 使用代码动态请求 代码调用requestPermission 声明USB授权 才会弹出USB授权系统弹窗。

Kotlin 复制代码
class UsbCommunicationService : Service() {
    fun requestUsbPermissionWhenNeeded() {
        val device = findUsbDevice()
        if (device != null && !usbManager.hasPermission(device)) {
            // 在合适的时机请求权限
            val permissionIntent = PendingIntent.getBroadcast(
                this, 0, Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE
            )
            usbManager.requestPermission(device, permissionIntent)
        }
    }
}

目前采用requestPermission()代码方式, Manifest方式的声明测试掉 , 代码执行第一次弹出USB授权弹框 点击确认授权后 后面多次执行 不会再次弹框了 系统已经记录你的授权操作了

针对以上方式无论是 **Manifest方式还是requestPermission()代码方式 都会弹出系统USB授权弹框,都需要用户点击确认授权才可以正常使用USB功能。**所以最好的方案是 如果你的android开发板已经Root了 通过Root方案可以彻底解决避免USB授权弹框的 问题

Kotlin 复制代码
/**
 * Root方案:直接修改权限文件(需要Root权限)
 */
private fun grantUsbPermissionWithRoot(device: UsbDevice) {
    try {
        // 构建权限文件路径
        val deviceName = device.deviceName.substringAfterLast("/")
        val permissionFile = File("/data/system/users/0/usb_permissions.xml")
        
        // 使用su命令修改权限文件
        val process = Runtime.getRuntime().exec("su")
        val outputStream = process.outputStream
        val command = """
            echo '  <permission deviceId="${device.deviceId}" 
                        serialNumber="${device.serialNumber}" 
                        granted="true" />' >> $permissionFile
        """.trimIndent()
        
        outputStream.write(command.toByteArray())
        outputStream.flush()
        outputStream.close()
        process.waitFor()
        
    } catch (e: Exception) {
        Log.e(tag, "Root授权失败: ${e.message}")
    }
}

流程:找设备 → 找数据接口 → 找端点 → 用端点通信

如何找到我需要的usb设备STM32

usbManager.deviceList 会返回所有连接到Android设备的USB设备列表,包括鼠标、键盘、U盘、你的STM32等等。如何从中精准地找到你的STM32设备,

方法一:通过 Vendor ID 和 Product ID (最可靠、最专业)

这是首选方法,也是最准确的方式。每个USB设备都有两个唯一的标识符:

  • Vendor ID (VID):由USB-IF协会分配给设备厂商的唯一编号。

  • Product ID (PID):由厂商给自己产品分配的唯一编号。

STM32的CDC设备通常使用STMicroelectronics的默认VID和PID ,这是最准确、最可靠的方法。在代码中硬编码你的STM32设备的VID和PID。

  • VID (Vendor ID) : 0x0483 (十进制 1155)

  • PID (Product ID) : 0x5740 (十进制 22336)

Kotlin 复制代码
// 获取所有连接的USB设备
val deviceList = usbManager.deviceList

// 遍历设备列表,根据VID和PID进行过滤
usbDevice = deviceList.values.find { device ->
    device.vendorId == 1155 && device.productId == 22336 // 使用十进制
    // 或者 device.vendorId == 0x0483 && device.productId == 0x5740 (使用十六进制)
}

if (usbDevice != null) {
    binding.textViewStatus.text = "找到STM32设备: ${usbDevice!!.deviceName}"
} else {
    binding.textViewStatus.text = "未找到STM32设备"
}

如果你有多个相同的STM32开发板会怎样?

如果你有多个相同型号烧录了相同固件 的STM32开发板,它们插入电脑后,VID和PID会完全一样

这时,usbManager.deviceList 中会出现多个具有相同VID/PID的设备。deviceList.values.find {...} 只会返回第一个匹配的设备,这显然不是你想要的。

最佳实践建议

  1. 对于个人项目/单一设备:直接使用VID/PID过滤第一个设备,简单有效。

  2. 对于可能连接多个相同设备的情况

    • 实现让用户选择的逻辑(方法一),这是最健壮的方式。

    • 如果可能,修改STM32固件 ,为每个设备烧写一个唯一的PID或序列号,这是最专业的解决方案。

usbDevice.getInterfaceCount()

usbDevice.getInterfaceCount() 这个方法返回该USB设备提供的接口(Interface)的数量。

一个USB设备就像一个公司,而接口(Interface) 就像是这个公司里的不同部门。每个部门有自己特定的功能和工作方式。

USB设备有几个接口?

一个USB设备至少有一个接口。

复杂的USB设备(如多功能打印机、集线器)可能有多个接口。

这完全取决于设备的类型和功能。对于你的STM32 CDC设备,答案很典型:通常是 2 个接口。

|---------------------|----------------------------------|---------------------------|-----------------------------------|--------------------------------------------------------------|
| 接口索引 | 接口类型 | 类代码 | 作用 | 比喻 |
| 接口 0 (管理接口) | 通信接口 (Control Interface) | USB_CLASS_COMM (2) | 管理连接。 用于设置波特率、数据位、流控等通信参数。 | 公司的行政部门。不处理具体业务,只负责制定规则和管理(如:我们公司今天9点上班,用中文沟通)。 |
| 接口 1 (数据接口) | 数据接口 (Data Interface) | USB_CLASS_CDC_DATA (10) | 传输数据。 所有实际要发送和接收的字节数据都通过这个接口。 | 公司的业务部门。真正干活的部门,按照行政部门定好的规则进行实际工作(如:销售部根据"用中文沟通"的规则去谈客户) |

如何用代码找到正确的接口?

在你的Android代码中,你需要遍历所有接口,找到那个真正用于数据传输的数据接口

Kotlin 复制代码
// 遍历设备的所有接口
for (i in 0 until usbDevice.interfaceCount) {
    val usbIf = usbDevice.getInterface(i)

    // 打印接口信息用于调试(非常重要!)
    Log.d("USB", "接口索引: $i")
    Log.d("USB", "接口类: ${usbIf.interfaceClass}")
    Log.d("USB", "接口子类: ${usbIf.interfaceSubclass}")
    Log.d("USB", "接口协议: ${usbIf.interfaceProtocol}")

    // 寻找CDC数据接口:类代码为 10 (0x0A)
    if (usbIf.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA) { // USB_CLASS_CDC_DATA = 10
        usbInterface = usbIf // 这就是我们要找的"业务部门"!
        Log.d("USB", "找到数据接口!")
        break
    }
}

找到了接口,然后呢?端点是做什么的?

找到了正确的接口(部门)后,还需要找到这个部门里的具体工作人员 ,这就是端点(Endpoint)

  • 每个接口可以有多个端点

  • 端点是数据通信的最终目的地

  • 端点有方向

  1. UsbConstants.USB_DIR_OUT (0): 输出,主机→设备 (Android→STM32)
  2. UsbConstants.USB_DIR_IN (128): 输入,设备→主机 (STM32→Android)
  • 端点有类型
  1. 批量传输(Bulk Transfer) : 用于大量、可靠的数据传输(文件、串行数据)。你的项目就用这种
  2. 中断传输(Interrupt Transfer): 用于频繁、小量、保证延迟的数据(鼠标、键盘)。
  3. 等时传输(Isochronous Transfer): 用于实时数据流(音频、视频),可能丢失数据但保证速度。

在你的STM32 CDC设备的数据接口中,通常有2个端点:

  1. 一个Bulk Out端点(用于Android发送数据给STM32)

  2. 一个Bulk In端点(用于STM32发送数据给Android)

查找端点的代码:

Kotlin 复制代码
usbInterface?.let { interface ->
    // 遍历该接口的所有端点
    for (i in 0 until interface.endpointCount) {
        val ep = interface.getEndpoint(i)
        when (ep.direction) {
            UsbConstants.USB_DIR_OUT -> {
                // 找到输出端点,用于发送数据给STM32
                endpointOut = ep
                Log.d("USB", "找到Bulk Out端点: 地址=${ep.address}")
            }
            UsbConstants.USB_DIR_IN -> {
                // 找到输入端点,用于接收来自STM32的数据
                endpointIn = ep
                Log.d("USB", "找到Bulk In端点: 地址=${ep.address}")
            }
        }
    }
}
相关推荐
richxu202510011 小时前
嵌入式学习之路->stm32篇->(14)通用定时器(上)
stm32·单片机·嵌入式硬件·学习
Deitymoon3 小时前
STM32——外部中断按键控制led
stm32·单片机·嵌入式硬件
czwxkn3 小时前
7STM32(stdl)flash内部闪存
stm32·单片机·嵌入式硬件
咕噜咕噜啦啦3 小时前
STlink下载程序
stm32·单片机
Deitymoon5 小时前
STM32——串口中断接收
stm32·单片机·嵌入式硬件
Deitymoon7 小时前
STM32——串口通信发送数据
stm32·单片机·嵌入式硬件
czwxkn8 小时前
8STM32(stdl)低功耗模式
stm32·单片机·嵌入式硬件
czwxkn8 小时前
9STM32(stdl)看门狗
stm32·单片机·嵌入式硬件
LCG元9 小时前
STM32实战:基于STM32F103的SPI通信驱动W25Qxx Flash存储
stm32·单片机·嵌入式硬件
iCxhust10 小时前
led_pattern = (led_pattern << 1) | (led_pattern >> 7)执行顺序
stm32·单片机·嵌入式硬件·51单片机·微机原理