)
嵌入式驱动开发通信物联网【免费下载链接】tinyusbAn open source cross-platform USB stack for embedded system项目地址https://gitcode.com/gh_mirrors/ti/tinyusb点击查看免费下载TinyUSB 是一套面向嵌入式系统的跨平台 USB 协议栈它把 USB 协议的高层状态机全部内建底层只依赖单片机的 USB 外设完成各端点的数据收发。本文以 docs/porting.rst 为骨架完整讲解如何为一块全新的单片机移植 TinyUSB从放置寄存器定义、搭建板级支持BSP、配置 OS 抽象层OSAL到实现设备控制器驱动DCD的每一组 API并最终以cdc_msc设备例程为起点完成枚举验证。读完本文你将掌握移植 TinyUSB 的完整方法能独立把一个新 MCU 跑起来并用 Linuxusbtest和 USB 抓包工具定位问题。移植的本质协议栈与硬件外设的分工TinyUSB 面向“通用 USB 协议栈”设计它接管绝大部分高层 USB 协议枚举、控制传输、各类 class 的处理而把数据收发交给 MCU 的 USB 外设。移植Porting就是为这个通用栈补上“底层支持”这一块。底层一旦实现后续把 USB 能力接入其他项目就非常简单——尤其是那些已经在用 TinyUSB 的项目例如 CircuitPython。官方推荐的移植路线非常务实先把cdc_msc设备例程跑起来。这个目标本身涵盖了大部分通用代码同时把额外代码量压到最小。文档中凡是出现在里的占位符如vendor、chip_family、board在移植时都应替换为实际名称。第一步寄存器定义与启动代码TinyUSB 的实现是直接面向寄存器结构体编写的而不是封装成高层函数。这样做有两个目的一是保持代码体积小巧二是在链接大型工程时避免函数名冲突。对 ARM 单片机而言这套结构体就是 CMSIS 定义应放置在hw/mcu/vendor/chip_family放好寄存器定义后为你要用来验证代码的具体开发板在hw/bsp/下新建目录hw/bsp/board name官方建议直接复制一个现有开发板的目录作为起点例如hw/bsp/stm32f4/boards/下各板卡目录这是最快的上手方式。同时要注意选择的板子应当是市面上容易买到的开发板这样社区里的其他人也能跟着测试。第二步让构建系统跑通目录就位后即可开始“迭代式”构建。在 TinyUSB 仓库根目录执行make -C examples/device/cdc_msc BOARDboard不出意外的话第一次会“惨烈失败”文档原话。接下来要做的不是一次性修好而是让它“少失败一点”板级目录中的代码负责配置 MCU 的时钟和引脚让 USB 外设工作起来TinyUSB 本身只操作 USB 外设。此外板级目录还要告诉构建系统需要哪些源文件。逐步推进时有几件必须做的事修改board.mk中的-DCFG_TUSB_MCUC 编译选项它告诉 TinyUSB 当前构建的是哪个平台。先在 src/tusb_option.h 中为你的 MCU 添加一个OPT_MCU_XXX枚举值该文件第 22 行起以厂商分组维护了 LPC、NRF、SAM、STM32、TI、Espressif 等各家 MCU 的编号再把CFLAGS中的定义同步更新。同步更新board.mk中的VENDOR与CHIP_FAMILY值与寄存器结构体的存放目录保持一致。从 src/portable 下复制一份现有厂商/芯片系列的驱动源码到src/portable/vendor/chip_family把实现内部全部删掉只留骨架。每个函数具体干什么后面再填先让它编译过。此外board.mk还需要按需补充特定CFLAGS、链接脚本、链接选项、源文件列表和头文件搜索路径。所有这些路径均相对于 TinyUSB 仓库根目录。可以参考现有板卡例如 hw/bsp/stm32f4/boards/stm32f407disco/board.mk 中SRC_S .../startup_stm32f407xx.s与LD_FILE $(BOARD_PATH)/STM32F407VGTx_FLASH.ld的写法。第三步实现各层编译通过后会来到“实现问题”此时构建配置已基本就绪可以逐层补齐功能。板级支持 BSPBSP 代码只服务于仓库内自带的示例和测试当 TinyUSB 作为大型项目的一部分时并不会被使用。它的职责是把 MCU 启动起来、给 USB 外设提供时钟可选地提供 LED 定义用于闪烁以指示代码运行。位置在hw/bsp/board name/board_board name.c接口声明见 hw/bsp/board_api.h。board_init()负责启动 MCU、配置 USB 时钟与 USB 引脚同时初始化 LED 引脚hw/bsp/board_api.h。调试时钟有个很实用的技巧用 USB 时钟输出一个已知频率如 500Hz的 PWM 信号用逻辑分析仪或示波器验证时钟是否准确。另外只要硬件可用尽量让 USB 工作在crystal-less无晶振模式这样代码在不同板卡间迁移更容易。board_led_write()这个函数可以先跳过等想验证 demo 是否在跑时再实现。实现要点把 LED 对应引脚配置为输出当state为true时输出点亮 LED 的电平即可接口见 hw/bsp/board_api.h。OS 抽象层 OSALOSAL 为 TinyUSB 提供基础数据结构在搭配 RTOS 时支持并发在没有 RTOS 时它只需要处理主循环代码与中断之间的并发问题。OSAL 几乎与 MCU 无关代码全部位于 src/osal其接口与职责清单互斥锁、信号量、队列、自旋锁等定义在 src/osal/osal.h。在 RTOS 配置下tud_task()/tuh_task()会在事件队列为空时阻塞在同步结构上从而让调度器把 CPU 让给其他任务。要利用这个“无 USB 事件时让出 CPU”的能力必须正确定义CFG_TUSB_OS符号OPT_OS_NONE值 1无 RTOS默认配置OPT_OS_FREERTOS值 2启用 FreeRTOS 调度器让调用tud_task()/tuh_task()之外的其他线程有机会运行。完整的 OS 枚举定义见 src/tusb_option.hosal.h会按CFG_TUSB_OS的值选择对应的头文件src/osal/osal.h。注意当CFG_TUSB_OS OPT_OS_NONE且 MCU 非多核时OSAL_MUTEX_REQUIRED为 0即不生成互斥锁变量一旦启用 RTOS 或 MCU 为多核互斥锁即变为必需src/osal/osal.h。设备底层 APIDCD设备枚举之后USB 设备代码在主线程里通过调用tud_task()处理事件而这些事件由 USB 中断处理函数排队产生。因此设备底层 API 由三部分组成设备初始化、端点管理、中断处理。全部代码位于src/portable/vendor/chip family/dcd_chip family.c设备初始化函数职责与要点dcd_init()初始化 USB 外设到设备模式并使能必须使能内部的 D/D- 上拉电阻以完成枚举声明见 src/device/dcd.hdcd_int_enable()/dcd_int_disable()使能/禁止 USB 设备中断。当主线程与中断处理函数共享数据结构时可用于防止并发问题dcd_int_handler()处理所有硬件产生的事件例如总线复位、主机发来的新数据包等。由应用在 MCU 的 USB 中断服务函数中调用dcd_set_address()设备被分配新总线地址时被调用。如果外设会在枚举时自动改地址例如 nrf52可留空实现也无需为对应 SETUP 包排队事件dcd_remote_wakeup()挂起时远程唤醒主机例如 HID 键盘场景dcd_connect()/dcd_disconnect()连接/断开数据线上拉电阻。仅当 MCU 有内部上拉时才需要定义无内部上拉的 MCU 可由 BSP 代为实现以上函数原型均可在 src/device/dcd.h 中找到与文档一一对应。特殊事件上报TinyUSB 必须知道某些总线事件的发生才能继续工作DCD 侧通过下列方法把事件入队dcd_event_bus_signal()用于上报总线状态类事件其中加粗的事件是 TinyUSB 正常工作的必需项DCD_EVENT_BUS_RESET等价于DCD_EVENT_BUS_RESET_END即总线复位结束携带着协商后的速度主机复位总线导致外设复位时触发应在中断处理函数里做任何需要的内部复位例如复位控制端点。当前栈还支持可选的DCD_EVENT_BUS_RESET_START用于在复位信号被检测到的第一时间就让协议栈停止使用端点见 src/device/dcd.h 的事件枚举与注释。DCD_EVENT_SOF标识新 USB 帧的开始。调用示例注意实际事件名为DCD_EVENT_BUS_RESETdcd_event_bus_signal(0, DCD_EVENT_BUS_RESET, true);第一个参数0是 USB 外设编号单 USB 设备 MCU 上固定写0很常见第三个参数true表示从中断处理函数中调用按这种移植方式写代码时它始终为true。dcd_event_setup_received()用于上报 SETUP 包。SETUP 是控制端点编号0上随时可能发生的特殊事务绝大多数外设对它都有专门处理其数据长度恒为 8 字节dcd_event_setup_received(0, setup, true);中间参数是存放 SETUP 包内容的 8 字节数组可以放在栈上因为函数内部会拷贝进事件队列。从 src/device/dcd.h 的实现可以看到它先把数据拷入dcd_event_t再把wValue、wIndex、wLength从 USB 线缆小端格式转换为主机字节序保证协议栈在任何 CPU 字节序下看到的都是正确值。端点管理端点是 USB 数据传输的核心常见类型有控制control、等时isochronous、批量bulk与中断interrupt。通用流程是为某个端点地址配置一段指定长度的缓冲区发起传输然后等待中断通知传输完成。USB 端点地址同时编码了端点的编号与方向。TinyUSB 提供了两个内联辅助函数来拆解地址实现位于 src/common/tusb_types.huint8_t epnum tu_edpt_number(ep_addr); // 取端点编号addr TUSB_EPNUM_MASK uint8_t dir tu_edpt_dir(ep_addr); // 取方向按 TUSB_DIR_IN_MASK 判定 IN/OUTdcd_edpt_open()主机选定配置后为所有非控制端点调用。此时应在外设中使能该端点并按端点描述符配置。特别注意用上面两个辅助函数取得的方向——方向不同要设置的寄存器很可能不同。同时务必使能端点专属中断。dcd_edpt_close()⚠️已废弃等时传输应改用dcd_edpt_iso_alloc()与dcd_edpt_iso_activate()。该函数用于实现 alternate setting 切换调用后设备不应再回应发往该端点的任何包返回前必须中止该端点所有进行中的传输。实现可选且必须在 USB 任务中被调用调用期间中断可能开也可能关。当前代码中是否编译该 API 由宏TUP_DCD_EDPT_CLOSE_API控制src/device/dcd.h。dcd_edpt_iso_alloc()/dcd_edpt_iso_activate()前者在设备枚举时为等时端点分配覆盖所有 alternate interface 的最大缓冲区部分 MCU 需要手动分配包缓冲区分配最大值可避免碎片化后者在 alternate setting 被设置、携带实际最大包长时激活或去激活等时端点。dcd_edpt_xfer()这是 TinyUSB 工作必须实现的核心方法之一另一个是中断处理函数。它负责配置外设去发送或接收主机数据“xfer”即 “transfer” 的缩写。从主机收数据是 OUT 方向向主机发数据是 IN 方向。它适用于包括控制端点 0 在内的所有端点务必正确处理端点 0 上的零长度包ZLPSTATUS 事务——对外设而言它可能是特殊事务。以实际实现为例src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c 中的dcd_edpt_xfer()先用tu_edpt_number()/tu_edpt_dir()解出端点编号与方向再写入xfer_ctl_t的控制块buffer、total_len、queued_len最终交给edpt_xfer()触发硬件传输——这正是“端点地址决定缓冲区信息写到哪里”的标准写法。其他要点发送缓冲区的对齐要求由CFG_TUSB_MEM_ALIGN决定默认对齐到 4 字节见 src/tusb_option.h一个常见的坑缓冲区可能比单个 USB 包的最大长度还长。有的外设可以为一个缓冲区连续发多个 USB 包如 SAMD21有的如 nRF52则需要每个 USB 包单独排队这时你需要在中断处理函数里自行维护状态、逐个排队中间包传输进行中IN 数据缓冲区保证在dcd_xfer_complete()被调用前内存内容不被改动dcd_edpt_xfer()绝不能自行附加 ZLP。若需要 ZLP必须由协议栈显式以len0第二次调用dcd_edpt_xfer()来发送。控制传输的 ZLP 由usbd_control.c自动处理同一时刻只允许一个缓冲区在传没有双缓冲机制。除非发生 USB 复位否则同一个端点地址在驱动调用dcd_xfer_complete()之前不会被再次调用dcd_edpt_xfer()。dcd_xfer_complete()即dcd_event_xfer_complete()传输完成时必须从 USB 中断处理函数调用它通知 TinyUSB。示例dcd_event_xfer_complete(0, ep_addr, xfer-actual_len, XFER_RESULT_SUCCESS, true);参数含义USB 外设编号端点地址实际传输长度OUT 传输可能小于dcd_edpt_xfer()给出的缓冲区长度传输结果失败场景尚未处理true表示从中断处理函数调用。dcd_edpt_stall()/dcd_edpt_clear_stall()stall 是端点表达失败的一种方式例如收到了不支持的命令。这一对函数用来管理所有端点的 stall 状态clear_stall还会把数据切换位复位为 DATA0且控制端点不经过它——收到 SETUP 包时控制端点的 stall 会被自动清除见 src/device/dcd.h。第四步用 usbtest 验证cdc_msc能成功枚举后构建examples/device/usbtest在 Linux 主机上对它跑内核自带的usbtest测试组。usbtest 对设备控制器驱动的压榨力度远超普通 class 示例它按不同 tier 依次覆盖厂商控制传输、中断传输和等时传输取决于你的移植支持到哪一层。各 tier 的用例与运行方法参见 examples/device/usbtest 的 README 与src源码。第五步Woohoo调试锦囊走到这一步理论上一切都该工作了但代码未必一次写对。调试利器是WireShark 或 Beagle 系列 USB 分析仪去嗅探 USB 流量。USB 出问题往往发生在枚举的极早期定位到具体阶段能快速缩小范围主机发出 SETUP 包但没有被 ACK → 大概率是USB 外设没有正确启动外设启动正确但依然失败 → 检查USB 时钟是否正确之前让你按 USB 时钟输出 PWM 就是为此准备的 SETUP 包被 ACK 了却没有回应数据 →中断处理函数没有正确排队 setup 包如果你用的是自己的代码而不是示例还要确认tud_task()是否被周期调用如果这些都正常那问题可能出在dcd_xfer_complete()没有正确为下一次事务做好准备。移植清单速览阶段交付物参考位置寄存器定义CMSIS 头文件与启动代码hw/mcu/vendor/chip_family板卡目录复制的示例板目录含board.mkhw/bsp/board name/平台标识OPT_MCU_XXX枚举 CFG_TUSB_MCUsrc/tusb_option.h驱动骨架空实现 DCD 源文件src/portable/vendor/chip_family/dcd_*.cBSPboard_init()/board_led_write()hw/bsp/board/board_board.cOSAL选定CFG_TUSB_OSsrc/osal、src/tusb_option.hDCD初始化、事件上报、端点 APIsrc/device/dcd.h、src/portable/vendor/...验证cdc_msc枚举 usbtestexamples/device/cdc_msc、examples/device/usbtest按以上步骤走完你的新 MCU 就正式成为 TinyUSB 支持列表的一员后续接入其他项目只需复用这份底层实现即可。赞分享嵌入式驱动开发通信物联网【免费下载链接】tinyusbAn open source cross-platform USB stack for embedded system项目地址https://gitcode.com/gh_mirrors/ti/tinyusb点击查看免费下载相关推荐【免费下载】 TinyUSB项目移植指南从零开始为微控制器添加USB支持TinyUSB项目移植指南从零开始为微控制器添加USB支持 概述 TinyUSB作为一个轻量级的USB协议栈为嵌入式系统提供了强大的USB功能支持。本文将详嵌入式驱动开发通信物联网新手必看aws-lambda-go-api-proxy项目结构与核心组件解析新手必看aws lambda go api proxy项目结构与核心组件解析 aws lambda go api proxy是一个强大的工具库它能帮助开发者Arduino ESP32 核移植指南为 arduino-esp32 添加全新 SoC 支持的完整流程Arduino ESP32 核移植指南为 arduino esp32 添加全新 SoC 支持的完整流程 本指南以 arduino esp32 官方文档 add嵌入式物联网驱动开发上一篇hellocharts-android最佳实践总结从新手到专家的完整成长路径下一篇ControlNet FP16量化部署指南实现50%内存优化的企业级解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考