~~NOCACHE~~ ====== 一个基于固件的软USB设备 ====== * 这个项目已经上传到[[https://github.com/GenieKits/Yet-Another-Firmware-Based-SOFT-USB-Device|Github]],并且 * 你也可以从本站的[[:zh:downloads|下载页]]上取得源码包和参考资料。 ---- 我们能在一个很简单的没有任何USB接口的MCU上实现一个USB设备吗?在这个项目中我会给你一个肯定的答案。事实上多年以前就有一个名为[[https://www.obdev.at/products/vusb/powerswitch.html|PowerSwitch]]的项目发布在互联网上。其作者用了一个[[https://www.microchip.com/design-centers/8-bit/avr-mcus/device-selection|AVR]]芯片制作了一个USB设备。这几天我创建了一个类似的项目,在另一种MCU上实现了另一种USB设备。我选择的MCU是[[https://www.microchip.com|Microchip公司]]设计制造的[[https://www.microchip.com/design-centers/16-bit|PIC24/dsPIC33]]芯片,并在其上实现了一个自定义的[[wp>Human_interface_device|HID]]设备。项目包括了电路原理图和完整源代码。 ---- ===== 背景 ===== 大约在2006年,我在互联网上发现了一个神奇的项目:[[https://www.obdev.at/products/vusb/powerswitch.html|PowerSwitch]]。这是一个通过USB接口控制的八通道电子开关,但它的主控MCU([[wp>Atmel|ATMEL]] [[http://ww1.microchip.com/downloads/en/DeviceDoc/DOC0839.PDF|90S2313]])本身并没有USB硬件接口,设计者使用两个GPIO口线连接USB HOST的D+/D-线,通过固件捕获D+/D-上的信号并进行解码,最终实现了一个USB低速设备。 AT90S2313芯片的高速度是这个项目成功的关键,使用12MHz的石英晶体驱动时它具有12MIPS的高性能,每8个指令周期刚好对应了USB低速通信比特流(1.5Mbps)中的一个比特。所以固件可以准确地采样每一个比特,并且在采样间隔期间对比特流进行处理。由于12MIPS仍然不够快,所以固件只做了NRZI解码和BIT UNSTUFF,没有进行CRC校验。在发送数据时,固件会事先计算出CRC16校验码并附加在数据之后,且在数据和CRC码准备好之前一直向主机返回NAK。 非常精巧的设计,ATMEL公司有一篇应用笔记(AVR309)记录了这一技术的要点。不过这个项目的核心代码使用了AVR汇编语言,而我当时并不熟悉AT90系列芯片,所以我没有对此项目进行深入研究。到目前为止PowerSwitch演变为一个独立项目[[https://www.obdev.at/products/vusb/index.html|V-USB]],移植到ATMEGA系列芯片上并派生出一些实用产品,比如[[https://www.fischl.de/usbasp/|USBAsp]],但从未在非ATMEL的芯片上实现过。((最近在Github上发现一个基于[[https://www.st.com/zh/microcontrollers-microprocessors/stm32f0x0-value-line.html|STM32F030]]的[[https://github.com/ads830e/stm32f030-vusb|同类项目]])) 几年前我曾经设计过一些基于USB接口的产品,由于我使用的芯片都有USB硬件接口,所以我对USB的底层协议不是特别关注。这一次我准备仿照“V-USB”重新实现一个基于固件的软USB设备,目的是更深入地学习USB底层协议。我选择了Microchip公司的PIC24F/dsPIC33系列芯片,使用汇编语言和C语言完成编码,如果你不是Microchip系列MCU产品的专家,这个项目可能会带给你相当大的困难。又或者说,带给你一个深入研究Microchip系列产品的绝好途径。 ---- ===== 电路原理图 ===== {{:ya-vusb:ya-vusb-sch.png}} 我选择了[[https://www.microchip.com/wwwproducts/en/dsPIC33FJ12MC201|dsPIC33FJ12MC201]]这个芯片,该芯片具有20引脚的SSOP小封装,提供最大40MIPS的高性能。我使用8MHz石英晶体X1并通过PLL为CPU核提供30MHz(FPLLOUT=30MHz)主时钟,这样CPU具有15MIPS的运行速度,每10个机器周期对应了1.5Mbps比特流中的1个比特。我使用了GPIO A中的RA0/RA1口线连接USB D+/D-,由于RA0/RA1可直接触发“Change Notification”中断,因此不必额外再使用INT0口线。注意USB D+/D-必须连接到同一GPIO的两根口线上,将D+连接到RAx上而将D-连接到RBx上是不允许的。USB D-上的1.5K上拉电阻R7并未直连到3.3V,而是由GPIO B中的RB4控制。RB15口线上连接了一个LED,主机端的测试程序将通过USB口发送一些数据控制这个LED的状态。MCLR/PGC1/PGD1用做[[wp>In-system_programming|ICSP]]接口J2,可以使用诸如[[https://www.microchip.com/Developmenttools/ProductDetails/PG164140|PICKit3]]之类的编程器烧写固件代码。RB7口线连接到ICSP接口J2的第6脚上,用于在必要时通过UART发送一些调试信息。注意ICSP接口J2的第6脚应该是对应PICKit3的LVP信号,我没用过PICKit3,不知道将这个引脚连接到RB7上是否会影响PICKit3。我用了一个NPN晶体管T1产生3.3V电源,在我的实验板上则是一颗[[http://www.advanced-monolithic.com/products/voltreg.html|AMS1117-33]],你也可以尝试在VBUS之后串接一个红色LED代替晶体管T1,将+5V电压降至3.2V。芯片第6脚到GND之间可以直接焊一个锡桥代替R4,不必真正焊一个0欧电阻。我增加R4目的是允许把dsPIC33FJ12MC201更换成[[https://www.microchip.com/wwwproducts/en/PIC24F16KA101|PIC24F16KA101]],这只需要断开第6脚到GND的连接并去掉第14脚上的10μ电容C3,同时把石英晶体换成30MHz即可。 源代码中没有直接定义熔丝位,所以在烧写代码时需手工设置“FCKSM1/POSCMD1/FWDTEN/JTAGEN”为“编程”状态,也就是0,其它位都设为“未编程”状态即可。若使用PIC24F16KA101芯片外接30MHz晶体,则需设置“FNOSC2/FNOSC0/POSCMD0/FWDTEN”。 {{:ya-vusb:fuses.png|left:dsPIC33 right:PIC24F}} ---- ===== 软件编程环境 ===== 这个项目在WINDOWS平台上开发并测试。我使用了Microchip的XC16编译套件,版本号1.25。除此外我没有使用集成开发环境(MPLAB IDE)和调试器(如[[https://www.microchip.com/DevelopmentTools/ProductDetails/DV164045|ICD 4]]等),也没有购买任何商用许可证。没有商用许可证意味着其GCC编译器仅能支持-O1级别的优化,这基本够用了。你可以点击下面的图标下载XC16编译器,更高版本的应该也能正常使用。 [[https://www.microchip.com/development-tools/pic-and-dspic-downloads-archive|{{ :ya-vusb:mplab-xc-compiler.png }}]] 我直接使用一个批处理文件(usb.bat)对源码进行编译: xc16-gcc -mcpu=33FJ12MC201 -O1 main.c hid.c usb.c sie.s dbg.s -o main.elf -T p33FJ12MC201.gld -Wl,--defsym,__has_user_init=1,-Map=main.map xc16-bin2hex main.elf xc16-objdump -D main.elf >main.txt pause 我给连接器传了一个参数“-Wl,--defsym,__has_user_init=1”,这样在源码文件中sie.s的“__user_init”函数就会被C启动代码调用,在XC16编译器安装目录中找一下名为“libpic30.zip”的库源码包,其中的“crt0_standard.s”文件引用了“__has_user_init”这个符号。 运行于主机端的测试例程是基于[[wp>Microsoft_Visual_Studio|Visual Studio]] 2008开发的。我有一个自制的固件代码烧写器代替PICKit3,有关这个烧写器的详情我会另外撰文。 ---- ===== 源代码的结构 ===== {{:ya-vusb:file-structure.png}} 这个项目包括5个源码文件,其中两个是汇编语言写的(sie.s/dbg.s)。dbg.s是用于调试的,通过UART输出一些信息以及通过LED显示一些状态,这个源码完全可以省略掉。其余三个是C语言源码,usb.c用于处理部分USB标准请求,如“取描述符”等。hid.c用于处理HID协议的“Set/Get Report”请求。main.c则包括程序的初始化代码和主任务循环。需要注意的是C语言的入口点“main函数”定义在sie.s之中,main.c中只有setup函数和loop函数,这很象[[wp>Arduino|arduino]]的源码。 sie.s是这个项目的核心代码,它包括了“Change Notification”中断服务程序,USB底层的所有处理都在这个中断服务程序中进行。它还包括了存储通信数据的缓冲区和几个用于记录状态的全局变量,以及一组供usb.c调用的API函数,这个API层可以避免usb.c直接访问sie.s定义的变量。 ---- ===== 源代码详解 ===== 我不打算把源代码的完整说明都放上来,因为文字太长了。你可以从本站的[[:zh:downloads|下载页]]上取得源码包,包中有PDF文件记录了本项目源码的所有关键信息。