拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ESP32-P4 USB MSC实战:基于TinyUSB将SD卡模拟为U盘

1. 从一块开发板到随身读卡器这个实验到底在做什么手里攥着一块 ESP32-P4 开发板板子上有个 USB 口旁边插着一张 TF 卡你有没有想过——能不能让电脑把这整块板子当成一个普通的 U 盘来用插上数据线电脑资源管理器里直接蹦出一个可移动磁盘往里拖文件、删文件、格式化跟用普通读卡器一模一样。这就是 USB 读卡器Slave实验要干的事。说白了这个实验的核心目标只有一个让 ESP32-P4 通过 USB 接口模拟成一个 USB 大容量存储设备MSCMass Storage Class把板子上的 SD 卡或者 SPI Flash 暴露给主机PC读写。主机那边完全不需要装任何驱动Windows、macOS、Linux 都能直接识别因为 MSC 是 USB 协议里最基础、最通用的设备类之一操作系统原生就支持。这个实验适合谁我觉得有三类人值得花时间动手做一遍。第一类是刚接触 ESP32-P4 和 ESP-IDF 的嵌入式新手想找一个“看得见摸得着”的入门项目比点灯有意思多了做完能直接看到电脑上多出一个盘符成就感拉满。第二类是做产品原型的工程师很多设备需要“配置导出”或者“日志抓取”功能与其写上位机软件不如直接让设备模拟成 U 盘用户插上就能拷文件省事。第三类是想深入理解 USB 协议栈的开发者TinyUSB 这套协议栈在 ESP-IDF 里封装得比较好通过这个实验可以顺带把 USB Device 的枚举流程、描述符配置、端点通信这些概念过一遍。涉及到的关键技术点其实就三个ESP32-P4 的 USB OTG 外设、ESP-IDF 里的 TinyUSB 协议栈、SD 卡/FATFS 文件系统。这三个东西串起来才能实现“电脑读写 SD 卡”这个完整链路。下面我会从整体设计思路开始拆然后一步步把代码和配置讲清楚最后把我踩过的坑和排查经验都倒出来。2. 整体方案设计与技术选型拆解2.1 为什么选 USB MSC 而不是其他方案让设备跟电脑传文件方案其实不少。可以走串口传但速度慢还得装驱动或者写上位机可以走网络但得配 IP、搞协议复杂度高可以走 MTP媒体传输协议但 MTP 在嵌入式端实现起来比 MSC 麻烦得多而且不同系统兼容性参差不齐。MSC 的优势在于“零驱动、零配置、零上位机”。主机操作系统把它当成一个标准 U 盘文件系统的解析工作全部由主机完成设备端只需要老老实实响应读写扇区的请求就行。设备端甚至不需要理解 FAT32 文件系统的结构只要能把扇区数据搬进搬出就完事了。这个“甩锅”设计大大降低了嵌入式端的开发难度。当然MSC 也有它的局限。它是块设备级别的访问主机拿到的是原始扇区如果设备端同时也想访问同一个存储介质就会出现“两边同时写”的冲突。这个问题后面会专门讲怎么处理。2.2 ESP32-P4 的 USB 外设能力ESP32-P4 这颗芯片在 USB 方面给得比较足它有一个 USB OTG 2.0 High-Speed 控制器支持 Device 和 Host 两种模式理论速率能到 480 Mbps。做 Slave 实验用的是 Device 模式也就是板子作为“从设备”被电脑识别。这里有个容易搞混的点ESP32-P4 的 USB 控制器和 USB Serial/JTAG 是两套独立的东西。USB Serial/JTAG 那个口是用来烧录和看日志的而 OTG 口才是我们做读卡器实验要用的。有些开发板把这两个功能做到了同一个 Type-C 口上通过芯片内部切换有些板子则是分开的两个口。动手之前一定要看清楚原理图确认你插的是哪个口。2.3 TinyUSB 协议栈的引入逻辑ESP-IDF 从 v5.x 开始USB Device 协议栈主推 TinyUSB。TinyUSB 是一个开源的小型 USB 协议栈代码量不大可裁剪性强支持 MSC、HID、CDC、MIDI 等多种设备类。ESP-IDF 把它移植过来封装成了esp_tinyusb组件通过 menuconfig 就能启用。为什么不用 ESP-IDF 早期那套esp_modem或者自己撸 USB 描述符因为 TinyUSB 已经把枚举流程、标准请求处理、端点管理这些脏活累活都干完了我们只需要提供一个“读写扇区”的回调函数剩下的它全包。这就好比你要开个店TinyUSB 已经把店面、水电、收银系统都给你装好了你只需要负责进货出货。2.4 存储介质的选择SD 卡还是 SPI Flash实验里存储介质可以选 SD 卡SDMMC 接口或者 SPI Flash比如板载的 16MB Flash 划一个分区出来。两者各有优劣对比项SD 卡方案SPI Flash 方案容量大可换卡受限于 Flash 大小速度较快SDMMC 4-bit 模式较慢SPI 接口灵活性高用户可自行换卡固定不可更换适用场景数据采集、文件导出固件配置、小文件存储驱动复杂度需要 SDMMC 驱动 FATFS需要 SPI Flash 驱动 FATFS 或 LittleFS大多数教程和官方例程默认用 SD 卡因为更直观插上卡就能用。我下面也以 SD 卡方案为主线来讲SPI Flash 方案会在关键差异点上做说明。3. 核心细节解析与实操要点3.1 USB 描述符配置让电脑认识你USB 设备插到电脑上第一件事是“枚举”。主机问“你是什么设备”设备得回答一串描述符包括设备描述符、配置描述符、接口描述符、端点描述符。MSC 设备的描述符里最关键的是接口类代码要设成0x08Mass Storage Class子类设成0x06SCSI Transparent Command Set协议设成0x50Bulk-Only Transport。在 TinyUSB 里这些描述符大部分已经由 MSC 类驱动自动生成好了我们只需要在配置结构体里填几个参数static const tusb_desc_device_t msc_device_desc { .bLength sizeof(tusb_desc_device_t), .bDescriptorType TUSB_DESC_DEVICE, .bcdUSB 0x0200, .bDeviceClass TUSB_CLASS_MISC, .bDeviceSubClass MISC_SUBCLASS_COMMON, .bDeviceProtocol MISC_PROTOCOL_IAD, .bMaxPacketSize0 CFG_TUD_ENDPOINT0_SIZE, .idVendor 0x303A, // Espressif 的 VID .idProduct 0x4002, // 自定义 PID .bcdDevice 0x0100, .iManufacturer 0x01, .iProduct 0x02, .iSerialNumber 0x03, .bNumConfigurations 0x01, };idVendor用 Espressif 的0x303A是合规的做法idProduct可以自己定只要不跟别的产品冲突就行。字符串描述符里可以写厂商名、产品名、序列号这些会显示在电脑的设备管理器里方便识别。注意bMaxPacketSize0对于 High-Speed 设备必须是 64Full-Speed 设备可以是 8、16、32 或 64。ESP32-P4 的 OTG 支持 High-Speed所以填 64。3.2 MSC 回调函数读写扇区的核心逻辑TinyUSB 的 MSC 类驱动会调用我们注册的回调函数来读写扇区。核心就是两个函数read_cb和write_cb。主机发来“读第 N 个扇区”的请求我们就从 SD 卡里把对应数据读出来放到缓冲区主机发来“写第 N 个扇区”的请求我们就把缓冲区数据写到 SD 卡对应位置。static int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) { esp_err_t ret sdmmc_read_sectors(card, buffer, lba, bufsize / 512); if (ret ! ESP_OK) { return -1; } return bufsize; } static int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t bufsize) { esp_err_t ret sdmmc_write_sectors(card, buffer, lba, bufsize / 512); if (ret ! ESP_OK) { return -1; } return bufsize; }这里的lba是 Logical Block Address也就是逻辑块地址单位是扇区通常 512 字节。bufsize是本次要传输的字节数一般是 512 的整数倍。返回值告诉 TinyUSB 实际传输了多少字节返回 -1 表示失败。还有一个msc_capacity_cb用来告诉主机这个“U 盘”有多大static void msc_capacity_cb(uint32_t *block_count, uint16_t *block_size) { *block_count card-csd.capacity; *block_size 512; }block_count是总扇区数block_size是每个扇区的字节数。主机拿到这两个值就能算出总容量显示在资源管理器里。3.3 SD 卡初始化与 FATFS 挂载在 USB 读卡器工作之前SD 卡本身得先初始化好。ESP-IDF 提供了sdmmc驱动初始化流程大致是配置 SDMMC 主机、配置 SD 卡槽、初始化卡、获取卡信息。sdmmc_host_t host SDMMC_HOST_DEFAULT(); host.max_freq_khz SDMMC_FREQ_HIGHSPEED; sdmmc_slot_config_t slot_config SDMMC_SLOT_CONFIG_DEFAULT(); slot_config.width 4; // 4-bit 模式 slot_config.clk GPIO_NUM_43; slot_config.cmd GPIO_NUM_44; slot_config.d0 GPIO_NUM_39; slot_config.d1 GPIO_NUM_40; slot_config.d2 GPIO_NUM_41; slot_config.d3 GPIO_NUM_42; esp_vfs_fat_sdmmc_mount_config_t mount_config { .format_if_mount_failed false, .max_files 5, .allocation_unit_size 16 * 1024, }; sdmmc_card_t *card; esp_err_t ret esp_vfs_fat_sdmmc_mount(/sdcard, host, slot_config, mount_config, card);这里有个关键点USB 读卡器实验其实不一定需要挂载 FATFS。因为 MSC 是块设备级别的访问主机自己会解析文件系统。设备端只需要能读写原始扇区就行不需要挂载文件系统。但如果你想让设备端自己也能访问 SD 卡里的文件比如记录日志那就需要挂载 FATFS。两者可以共存但要注意互斥问题后面会讲。3.4 引脚配置与硬件连接ESP32-P4 的 SDMMC 接口引脚是可以灵活映射的但有些引脚有默认功能配置时要注意避开。以官方开发板为例SDMMC 的典型引脚分配是信号GPIO说明CLKGPIO43时钟线CMDGPIO44命令线D0GPIO39数据线 0D1GPIO40数据线 1D2GPIO41数据线 2D3GPIO42数据线 3如果你的板子引脚不一样改slot_config里的对应字段就行。但要注意有些 GPIO 在上电时有特殊状态可能会影响 SD 卡初始化选引脚之前最好查一下芯片的数据手册。实操心得SD 卡的 4-bit 模式比 1-bit 模式快不少但走线要求也高。如果 PCB 走线比较长或者干扰大4-bit 模式可能会不稳定这时候可以降到 1-bit 模式试试。另外SD 卡座子的供电也要注意有些卡座没有内置稳压需要外部提供 3.3V。4. 完整实操流程与关键环节实现4.1 工程创建与 menuconfig 配置第一步用 ESP-IDF 的命令行工具创建一个新工程。如果你用的是 VSCode 的 ESP-IDF 插件可以直接在命令面板里选“New Project”然后选sample_project模板。如果习惯命令行就idf.py create-project usb_msc_demo cd usb_msc_demo然后打开menuconfig找到Component config→TinyUSB Stack把Enable TinyUSB Stack打开。接着在TinyUSB Stack→Communication Device Class (CDC)里确认 MSC 是启用的。有些版本的 ESP-IDF 里 MSC 是默认关的要手动勾上。还要配置 USB 的速率模式。在Component config→USB OTG里把USB OTG Mode设成Device速率设成High Speed。如果你不确定板子支持哪种速率可以先设Full Speed跑通了再改。4.2 代码骨架搭建整个工程的代码结构大概是这样的#include esp_tinyusb.h #include sdmmc_cmd.h #include driver/sdmmc_host.h static sdmmc_card_t *card; // MSC 回调函数 static int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) { ... } static int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t bufsize) { ... } static void msc_capacity_cb(uint32_t *block_count, uint16_t *block_size) { ... } // SD 卡初始化 static esp_err_t init_sd_card(void) { ... } // USB MSC 初始化 static esp_err_t init_usb_msc(void) { tinyusb_config_t tusb_cfg { .device_descriptor NULL, // 用默认描述符 .string_descriptor NULL, .external_phy false, .configuration_descriptor NULL, }; ESP_ERROR_CHECK(tinyusb_driver_install(tusb_cfg)); tinyusb_config_msc_t msc_cfg { .callback_read msc_read_cb, .callback_write msc_write_cb, .callback_capacity msc_capacity_cb, }; ESP_ERROR_CHECK(tinyusb_msc_init(msc_cfg)); return ESP_OK; } void app_main(void) { ESP_ERROR_CHECK(init_sd_card()); ESP_ERROR_CHECK(init_usb_msc()); // 主循环可以空转或者做其他事情 }这个骨架里tinyusb_driver_install负责初始化 USB 硬件和协议栈tinyusb_msc_init负责注册 MSC 类驱动和回调。两个都调完USB 读卡器就开始工作了。4.3 扇区读写回调的详细实现回调函数的实现看起来简单但有几个细节要注意。首先是扇区对齐问题SD 卡的读写操作要求缓冲区地址和大小都按扇区大小512 字节对齐。TinyUSB 传过来的缓冲区通常是对齐的但为了保险可以在回调里加个检查。static int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) { if (bufsize % 512 ! 0) { ESP_LOGE(TAG, bufsize not aligned: %lu, bufsize); return -1; } esp_err_t ret sdmmc_read_sectors(card, buffer, lba, bufsize / 512); if (ret ! ESP_OK) { ESP_LOGE(TAG, read failed: %s, esp_err_to_name(ret)); return -1; } return bufsize; }写回调类似但多了一个考虑写入缓存。SD 卡内部有缓存sdmmc_write_sectors返回成功不代表数据已经真正写到卡里了。如果主机突然拔线数据可能丢失。对于读卡器场景这个问题一般可以接受但如果你的应用对数据完整性要求高可以在写回调里定期调用sdmmc_write_sectors之后跟一个 flush 操作。注意sdmmc_read_sectors和sdmmc_write_sectors是阻塞式的在 USB 中断上下文里调用可能会引起问题。TinyUSB 的回调是在 USB 任务里执行的不是中断上下文所以可以安全调用。但如果你在回调里做了太耗时的操作会影响 USB 响应速度主机可能会超时。所以回调里尽量只做扇区读写别加打印日志之类的慢操作。4.4 容量回调与主机识别容量回调告诉主机这个“U 盘”有多大。card-csd.capacity是 SD 卡的总扇区数乘以 512 就是总字节数。主机拿到这个值后会尝试读取第一个扇区MBR 或引导扇区来解析分区表和文件系统。如果 SD 卡里已经有一个格式化成 FAT32 的分区电脑上就能直接看到盘符和里面的文件。如果 SD 卡是空的或者格式不对电脑可能会提示“需要格式化”。这时候你可以让电脑格式化它格式化完成后就能正常读写了。static void msc_capacity_cb(uint32_t *block_count, uint16_t *block_size) { *block_count card-csd.capacity; *block_size 512; ESP_LOGI(TAG, capacity: %lu sectors, %d bytes/sector, *block_count, *block_size); }4.5 编译、烧录与验证代码写完后编译烧录idf.py build idf.py -p /dev/ttyUSB0 flash monitor烧录完成后用一根 USB 数据线把开发板的 OTG 口连到电脑。注意有些开发板的 OTG 口和调试口是分开的别插错了。插上后电脑应该会“叮咚”一声然后资源管理器里多出一个可移动磁盘。如果没反应先看串口日志里有没有 USB 枚举相关的打印。TinyUSB 在枚举成功后会打印设备描述符信息。如果日志里什么都没有可能是 USB 口没插对或者 menuconfig 里 USB 模式没设成 Device。验证读写是否正常可以往盘里拷一个文件然后拔下来插到别的电脑上看看文件在不在。或者用dd命令在 Linux 下直接读写扇区做压力测试。5. 常见问题与排查技巧实录5.1 电脑识别不到设备这是最常见的问题原因可能有好几种。先按这个顺序排查现象可能原因排查方法电脑完全没反应USB 口插错确认插的是 OTG 口不是调试口设备管理器里有未知设备描述符配置错误检查 VID/PID 和接口类代码提示“设备描述符请求失败”USB 时钟或 PHY 配置问题检查 menuconfig 里的 USB 速率设置识别到了但提示格式化SD 卡文件系统损坏让电脑格式化或换一张卡我遇到过一种情况板子上的 USB 口是 Type-C但用的数据线是“只充电不传数据”的那种。这种线插上后设备能供电但 USB 数据线是断的电脑当然识别不到。换一根能传数据的线就好了。这个坑很隐蔽因为设备明明上电了日志也在跑就是电脑没反应。5.2 读写速度慢MSC 的读写速度受几个因素影响USB 速率模式、SD 卡速率模式、扇区大小、回调效率。如果发现速度只有几百 KB/s先检查是不是跑在 Full Speed 模式下了。Full Speed 理论最大 12 Mbps实际也就 1 MB/s 左右。改成 High Speed 后能到几十 MB/s。SD 卡这边确认host.max_freq_khz设的是SDMMC_FREQ_HIGHSPEED40 MHz而不是默认的 20 MHz。另外4-bit 模式比 1-bit 模式快将近 4 倍能用 4-bit 就用 4-bit。还有一个容易被忽略的点TinyUSB 的 MSC 缓冲区大小。在tusb_config.h里有个CFG_TUD_MSC_BUFSIZE宏默认可能是 512 或者 1024。把它改大一点比如 4096 或 8192能减少 USB 传输次数提升吞吐量。5.3 设备端和主机端同时访问冲突如果你在设备端也挂载了 FATFS同时电脑也在读写同一个 SD 卡就会出现数据不一致甚至文件系统损坏。因为两边各自有缓存互相不知道对方改了啥。解决办法有两个一是设备端不挂载 FATFS只做原始扇区读写文件系统的事全交给主机二是加一个互斥锁设备端访问文件系统之前先检查 USB 是否连接如果 USB 连着就暂停设备端访问。实际项目中我倾向于第一种方案简单可靠。实操心得如果确实需要设备端和主机端都能访问可以在 USB 连接时把设备端的 FATFS 卸载断开后再重新挂载。这样虽然麻烦但能保证数据一致性。5.4 拔插后无法重新识别有些情况下拔掉 USB 再插上电脑识别不到了。这可能是 TinyUSB 的状态机没有正确复位。可以在 USB 断开事件里做一些清理工作比如重新初始化 MSC 回调。ESP-IDF 的 TinyUSB 封装里提供了事件回调可以注册tinyusb_event_cb来处理TINYUSB_EVENT_USB_MOUNT和TINYUSB_EVENT_USB_UNMOUNT事件。static void tinyusb_event_cb(tinyusb_event_t event, void *event_data) { switch (event) { case TINYUSB_EVENT_USB_MOUNT: ESP_LOGI(TAG, USB mounted); break; case TINYUSB_EVENT_USB_UNMOUNT: ESP_LOGI(TAG, USB unmounted); break; default: break; } }在tinyusb_driver_install之前把这个回调注册进去就能在拔插时收到通知方便做状态管理。5.5 SD 卡兼容性问题不是所有 SD 卡都能在 ESP32-P4 上稳定工作。我实测下来一些老旧的 Class 4 卡或者杂牌卡在 4-bit 高速模式下容易初始化失败或者读写超时。换成 Class 10 以上的品牌卡问题基本就没了。如果遇到 SD 卡初始化失败可以先降到 1-bit 模式试试或者把频率降到 20 MHz。如果还是不行换张卡。这个排查过程比较费时间但没办法SD 卡的兼容性就是这么玄学。6. 进阶玩法与性能优化建议6.1 多分区支持默认情况下MSC 把整张 SD 卡作为一个块设备暴露给主机。如果 SD 卡里有多个分区主机也能识别因为分区表在扇区 0 里主机自己会解析。但如果你想在设备端控制哪些分区可见就需要在msc_capacity_cb和读写回调里做地址映射把主机的 LBA 映射到 SD 卡的实际 LBA。这个玩法比较高级适合需要隐藏某些分区的场景。6.2 读写缓存优化前面提到过增大CFG_TUD_MSC_BUFSIZE能提升吞吐量。但也不是越大越好因为缓冲区占的是内部 RAMESP32-P4 的 RAM 虽然不少但也要省着用。我一般设成 4096 或 8192再大收益就不明显了。另外SD 卡的读写本身也有缓存机制。sdmmc驱动支持 DMA确保 DMA 是启用的能减少 CPU 占用。在sdmmc_host_t里flags字段可以设置SDMMC_HOST_FLAG_DMA。6.3 低功耗考虑如果做的是电池供电的设备USB 读卡器的功耗也要考虑。USB 连接时设备不能进入深度睡眠否则 USB 会断开。可以在 USB 断开后让设备进入 light sleep有 USB 插入事件时再唤醒。ESP32-P4 的 USB 外设支持唤醒功能具体配置可以参考 ESP-IDF 的电源管理文档。6.4 与 UI 结合的思路最近看到不少人在搜“esp32-p4 ui 源码”其实 USB 读卡器可以和 UI 结合起来做。比如板子上跑一个 LVGL 界面显示当前 USB 连接状态、SD 卡容量、读写速度等信息。用户插上 USB 后屏幕上显示“已连接到电脑”拔掉后显示“已断开”。这样产品体验会好很多也更有科技感。实现思路是在 USB 事件回调里更新 UI 状态LVGL 任务定期刷新显示。注意 UI 任务和 USB 任务之间的数据同步用队列或者事件组来传递状态变化比较稳妥。7. 关于工具链和开发环境的一些经验搜“esp-idf 安装进度一直卡在 0%”的人不少我刚开始用的时候也遇到过。这个问题通常是网络原因导致的安装脚本要从 GitHub 拉取工具链网络不通就会卡住。解决办法是配置镜像源或者手动下载工具链压缩包放到指定目录。具体操作是设置IDF_TOOLS_PATH环境变量然后把下载好的工具解压到对应位置。“vscode 下使用终端编译 esp-idf”也是个高频问题。VSCode 的 ESP-IDF 插件会自动配置环境变量但有时候终端里idf.py命令找不到。这时候可以在 VSCode 的设置里搜idf.espIdfPath确认路径配置正确。或者直接在 ESP-IDF 的终端里操作那个终端是插件专门配置过的环境变量肯定没问题。至于“clion2023 工具里的 marketplace 里为什么找不到 esp-idf 插件”这是因为 CLion 的插件市场里确实没有官方的 ESP-IDF 插件。CLion 主要通过 CMake 来支持 ESP-IDF 项目需要手动配置 CMakeLists.txt 和工具链文件。如果你习惯用 CLion可以搜一下“CLion ESP-IDF CMake 配置”的教程配置好之后也能正常编译烧录只是没有 VSCode 插件那么一键化。“eim esp-idf”这个关键词我猜是在问 ESP-IDF 的安装管理器ESP-IDF Installation Manager。这是 Espressif 新推出的一个工具用来简化 ESP-IDF 的安装和版本管理。如果你经常需要在多个 ESP-IDF 版本之间切换用这个工具会比手动安装方便很多。8. 我踩过的几个坑和最后的建议第一个坑是 USB 描述符里的字符串索引。我一开始把iManufacturer、iProduct、iSerialNumber都设成了 0结果电脑上显示的设备名是空的。后来查了 TinyUSB 的文档才知道字符串索引要从 1 开始0 表示没有字符串。改成 1、2、3 之后就正常了。第二个坑是 SD 卡的热插拔。实验里 SD 卡是在启动时初始化的如果运行过程中把卡拔了再插上设备端不会自动重新初始化。主机那边会报读写错误。要支持热插拔得加一个卡检测引脚的中断处理检测到卡插入后重新初始化 SDMMC。这个功能在实际产品里挺重要的但教程里一般不讲。第三个坑是 USB 和 SDMMC 的引脚冲突。ESP32-P4 的某些 GPIO 是复用的如果 USB PHY 和 SDMMC 用了同一个引脚就会冲突。配置引脚之前一定要查数据手册的“Pin Functions”表格确认没有重叠。最后给个建议这个实验虽然叫“USB 读卡器”但它的核心价值在于让你理解 USB Device 协议栈的工作方式。把 MSC 跑通之后可以试试改成 HID 设备模拟键盘鼠标或者 CDC 设备模拟串口代码结构大同小异只是回调函数和描述符不同。把这几种设备类都玩一遍对 USB 协议的理解会上一个大台阶。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门