USB设备
SpinalHDL库中存在一个USB设备控制器。
用几个要点总结支持的功能:
实现了允许CPU配置和管理端点
存储端点状态和事务描述符的内部RAM
多达6个端点(几乎没有额外开销)
支持全速USB主机(12Mbps)
在Linux上使用自己的驱动程序进行测试(https://github.com/SpinalHDL/linux/blob/dev/drivers/usb/gadget/udc/spinal_udc.c)
用于配置的Bmb内存接口
内部物理层需要一个时钟,该时钟需为12 Mhz的倍数,至少48 Mhz
控制器频率不受限制
无需外部物理层
Linux小工具经过测试且功能正常:
串行连接
以太网连接
大容量存储(ArtyA7 Linux上约为8 Mbps)
部署:
https://github.com/SpinalHDL/SaxonSoc/tree/dev-0.3/bsp/digilent/ArtyA7SmpLinux
https://github.com/SpinalHDL/SaxonSoc/tree/dev-0.3/bsp/radiona/ulx3s/smp
架构
控制器由以下部分组成:
一小部分控制寄存器
一个用于存储端点状态、传输描述符和端点0配置数据的内部RAM。
每个端点的描述符链表时用于处理USB出入(IN/OUT)事务和数据。
端点0也像所有其他端点一样管理出入USB的传输事务,但也会有一些额外的硬件来管理设置(SETUP)事务:
它的链表在每次设置事务时都会被清除
来自设置(SETUP)事务的数据存储在固定位置(SETUP_DATA)
它有一个用于设置(SETUP)事务的特定中断标志
寄存器
请注意,控制器的所有寄存器和存储器只能以32位字的访问方式进行访问,不支持字节访问。
帧FRAME (0xFF00)
名称 |
类型 |
位 |
描述 |
---|---|---|---|
usbFrameId |
RO |
31-0 |
当前USB帧的ID |
地址ADDRESS (0xFF04)
名称 |
类型 |
位 |
描述 |
---|---|---|---|
address |
WO |
6-0 |
设备将仅侦听具有指定地址的令牌,该字段在USB复位事件发生时自动清除 |
enable |
WO |
8 |
如果置1,启用USB地址过滤 |
trigger |
WO |
9 |
在下一个EP0 IN令牌完成时置位enable(见上文),在任何EP0完成后由硬件清零 |
这里的想法是在EP0上收到USB SET_ADDRESS的设置(setup)数据包前,保持整个寄存器清零。此时,您可以设置地址和触发字段,然后向EP0提供IN零长度描述符以完成SET_ADDRESS序列。控制器将在该描述符完成时自动打开地址过滤。
中断INTERRUPT (0xFF08)
该寄存器的每个位都可以通过写入 ‘1’ 来清除。读取该寄存器将返回当前中断状态。
名称 |
类型 |
位 |
描述 |
---|---|---|---|
endpoints |
W1C |
15-0 |
当端点产生中断时拉高 |
reset |
W1C |
16 |
当USB复位发生时拉高 |
ep0Setup |
W1C |
17 |
当端点0收到设置请求时拉高 |
suspend |
W1C |
18 |
当发生USB挂起时拉高 |
resume |
W1C |
19 |
当USB发生恢复时拉高 |
disconnect |
W1C |
20 |
当USB断开连接时拉高 |
暂停HALT (0xFF0C)
该寄存器允许将单个端点置于休眠状态,以确保CPU操作的原子性,从而允许在端点寄存器和描述符上执行读/修改/写操作。如果USB主机在暂停启用且端点启用的情况下寻址给定端点,那么外设将返回NAK。
名称 |
类型 |
位 |
描述 |
---|---|---|---|
endpointId |
WO |
3-0 |
您想要进入休眠状态的目标端点 |
enable |
WO |
4 |
当设置暂停时为活动状态,当清除时端点解除暂停。 |
effective enable |
RO |
5 |
设置使能后,需要等待硬件本身设置该位,以保证原子性 |
配置CONFIG (0xFF10)
名称 |
类型 |
位 |
描述 |
---|---|---|---|
pullupSet |
SO |
0 |
写入 ‘1’ 以使能dp引脚上的USB设备上拉 |
pullupClear |
SO |
1 |
|
interruptEnableSet |
SO |
2 |
写入 ‘1’ 让当前和未来的中断发生 |
interruptEnableClear |
SO |
3 |
信息INFO (0xFF20)
名称 |
类型 |
位 |
描述 |
---|---|---|---|
ramSize |
RO |
3-0 |
内部RAM将有 (1 << this) 字节 |
端点ENDPOINTS (0x0000 - 0x003F)
端点状态存储在内部RAM的开头,每个端点状态有一个32位字。
名称 |
类型 |
位 |
描述 |
---|---|---|---|
enable |
RW |
0 |
如果不设置,端点将忽略所有流量 |
stall |
RW |
1 |
如果设置,端点将始终返回STALL状态 |
nack |
RW |
2 |
如果设置,端点将始终返回NACK状态 |
dataPhase |
RW |
3 |
指定使用的出入数据PID。 ‘0’ => DATA0。该字段也由控制器更新。 |
head |
RW |
15-4 |
指定当前描述符头(链表)。0 => 空列表,字节地址 = this << 4 |
isochronous |
RW |
16 |
|
maxPacketSize |
RW |
31-22 |
要获得端点响应,您需要:
将其使能标志设置为1
那么有几种情况: -要么设置了stall或nack标志,所以控制器将始终响应相应的响应 -要么,对于EP0设置请求,控制器不会使用描述符,而是会将数据写入SETUP_DATA寄存器和ACK -要么你有一个空链表 (head==0),在这种情况下它将响应NACK -要么你至少有一个由head指向的描述符,在这种情况下,它将执行该描述符,并在一切顺利时进行ACK
设置数据SETUP_DATA (0x0040 - 0x0047)
当端点0接收到SETUP事务时,该事务的数据将存储在该位置。
描述符
描述符允许指定一个端点需要如何处理出入(IN/OUT)事务的数据阶段。它们存储在内部 RAM 中,可以通过链表链接在一起,并且需要在16字节边界上对齐
名称 |
字 |
位 |
描述 |
---|---|---|---|
offset |
0 |
15-0 |
指定当前传输进度(以字节为单位) |
code |
0 |
19-16 |
0xF => 进行中,0x0 => 成功 |
next |
1 |
15-4 |
指向下一个描述符 0 => 无,字节地址 = this << 4 |
length |
1 |
31-16 |
分配给数据字段的字节数 |
方向 |
2 |
16 |
‘0’ => 输出,’1’ => 输入 |
interrupt |
2 |
17 |
如果置位,描述符完成时将产生中断。 |
completionOnFull |
2 |
18 |
通常,描述符补全只会在USB传输小于maxPacketSize时发生。但如果置位了该字段,那么当描述符被填满时也被视为事件已完成。(offset == length) |
data1OnCompletion |
2 |
19 |
描述符完成时强制端点dataPhase为DATA1 |
data |
… |
… |
请注意,如果控制器接收到IN/OUT与描述符IN/OUT不匹配的帧,那么该帧将被忽略。
此外,要初始化描述符,CPU应将代码字段设置为0xF
用法
import spinal.core._
import spinal.core.sim._
import spinal.lib.bus.bmb.BmbParameter
import spinal.lib.com.usb.phy.UsbDevicePhyNative
import spinal.lib.com.usb.sim.UsbLsFsPhyAbstractIoAgent
import spinal.lib.com.usb.udc.{UsbDeviceCtrl, UsbDeviceCtrlParameter}
case class UsbDeviceTop() extends Component {
val ctrlCd = ClockDomain.external("ctrlCd", frequency = FixedFrequency(100 MHz))
val phyCd = ClockDomain.external("phyCd", frequency = FixedFrequency(48 MHz))
val ctrl = ctrlCd on new UsbDeviceCtrl(
p = UsbDeviceCtrlParameter(
addressWidth = 14
),
bmbParameter = BmbParameter(
addressWidth = UsbDeviceCtrl.ctrlAddressWidth,
dataWidth = 32,
sourceWidth = 0,
contextWidth = 0,
lengthWidth = 2
)
)
val phy = phyCd on new UsbDevicePhyNative(sim = true)
ctrl.io.phy.cc(ctrlCd, phyCd) <> phy.io.ctrl
val bmb = ctrl.io.ctrl.toIo()
val usb = phy.io.usb.toIo()
val power = phy.io.power.toIo()
val pullup = phy.io.pullup.toIo()
val interrupts = ctrl.io.interrupt.toIo()
}
object UsbDeviceGen extends App{
SpinalVerilog(new UsbDeviceTop())
}