首先,我们来看看
usb
的工作过程。
当
usb
设备接入到主机时,主机开始枚举
usb
设备,并向
usb
设备发出指令要求获取
usb
设备的相关描述信息,其中包括设备描述(
device descriptor
)、配置描述(
configuration descriptor
)、接口描述(
interface descriptor
)、端点描述(
endpoint descriptor
)等。这些信息是通过端点
0
(
endpoint 0
)传送到主机的。获取各种描述信息后,操作系统会为其配置相应的资源。这样主机就可以与设备之间进行通信了。
usb
通讯有四种通讯方式控制(
control
)、中断(
interrupt
)、批量(
bulk
)和同步(
synchronous
)。
usb
通讯是通过管道(
pipe
)实现的。管道是一个抽象的概念,指的是主机与设备之间通讯的虚拟链路。比如说一个
usb
通讯主机
A
和设备
B
,其中有
bulk in
(批量输入)、
bulk out
(批量输出)、
control out
(控制输出)三种通讯方式,那么
A
与
B
之间的通讯管道就有三个。(这里明确一个概念,在
usb
通信中数据流向都是相对设备来说的,
in
表示设备向主机传送数据,
out
表示表示主机箱设备传输数据)。在设备一端,每个管道对应一个端点,端点配置相关的寄存器和缓冲区。在通讯之前需对端点进行相关设置。在通信中,只需向缓冲写或读数据,并置位相关比特位即可。
下面具体从
usb
的中断输入输出来讲述基于
keil C mdk
开发环境的
stm32
的
USB
接口单片机程序设计。值得一提的是,
st
或相关公司给我们提供许多封装函数和相关例子,我们可以根据其中的例子并进行修改即可实现我们自己需要的
usb
通讯程序。
1.usb
描述符配置
从上面的讲述可以看出,
usb
描述符是
usb
通讯的前提。主机必须先了解设备后才能与其进行通讯。在
st
提供的例子中,描述符都在
usb_des.c
文件进行定义,下面就其中的
Joystick
例子说明
usb
描述负的配置。
1.1
设备描述符
const u8 Joystick_DeviceDescriptor[JOYSTICK_SIZ_DEVICE_DESC] =
{
0x12,
USB_DEVICE_DESCRIPTOR_TYPE,
0x00,
0x02,
0x00,
0x00,
0x00,
0x40,
0x84,
0x19,
0x06,
0x04,
0x00,
0x02,
1,
2,
3,
0x01
}
设备描述符两个重要参数是生产商ID和产品ID,主机将根据以上两个ID为设备选择相应驱动程序。在我们的应用中,我们一般只需修改例子中的这儿两个参数即可完成设备描述符的设置。
1.2
配置描述符
const u8 Joystick_ConfigDescriptor[JOYSTICK_SIZ_CONFIG_DESC] =
{
0x09,
USB_CONFIGURATION_DESCRIPTOR_TYPE,
JOYSTICK_SIZ_CONFIG_DESC,
0x00,
0x01,
0x01,
0x00,
0xE0,
0x32,
0x09,
USB_INTERFACE_DESCRIPTOR_TYPE,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0,
0x07,
USB_ENDPOINT_DESCRIPTOR_TYPE,
0x81,
0x03,
0x08,
0x00,
0x20,
0x07,
USB_ENDPOINT_DESCRIPTOR_TYPE,
0x01,
0x03,
0x40,
0x00,
0x20,
}
配置描述符中包括了接口、端点的配置。如果设备为
HID
设备,在配置描述符中还应加入
HID
描述,具体描述可以参照
Joystick
例子的配置。
还有一些其他配置可以参可相关资料与例子加以理解。
2.USB
通讯的执行过程。
首先,当主机数据传送到
USB
设备,
USB
怎样接收命令和数据呢
?USB
首先会产生一个中断,这个中断在
stm32fxxx_it.c
文件的
USB_HP_CAN_TX_IRQHandler
和
USB_LP_CAN_RX0_IRQHandler
中定义,一般使用
USB_LP_CAN_RX0_IRQHandler
。在这个函数中继续调用
USB_Istr()
函数,这个函数是
usb
通讯的关键。它接收到主机命令,指派调度相应函数进行处理。对于这一点,详细过程我现在还不是很明白。如果以后搞懂了再补述。
当
USB
设备接入主机时,主机要枚举该
USB
设备,他将要求
USB
设备提供自身相关信息,这是通过
endpoint0
实现的。
endpoint0
是一个特殊的端点,每一个接口(
interface
)必须有
endpoint0
。一般情况下,我们需要使用多个端点(如前所述,配置描述符定义了端点的数目、类型、传输数据大小等)。
在使用端点前需对端点进行初始化。这个过程在usb_prop.c文件中的xxx_reset()函数定义。如我定义端点1的两种传输方式:
SetEPType(ENDP1, EP_INTERRUPT);
SetEPRxAddr(ENDP1, ENDP1_RXADDR);
SetEPRxCount(ENDP1, 8);
SetEPRxStatus(ENDP1, EP_RX_VALID);
SetEPType(ENDP1, EP_INTERRUPT);
SetEPTxAddr(ENDP1, ENDP1_TXADDR);
SetEPTxCount(ENDP1, 64);
SetEPTxStatus(ENDP1, EP_TX_NAK);
在定义完端点后,我们就可以使用端点进行数据传输了。
向主机输入数据(
in
):
IN
传输过程是
1.
向缓冲区填入数据;
2.
设定
USB
数据计数器:
3.
设置
USB
输出有效。
XXX_send()
{
UserToPMABufferCopy(sendBuffer, ENDP1_TXADDR, 2);
SetEPTxCount(ENDP1, 2);
SetEPTxValid(ENDP1);
}
注意一般情况下,端点的输入输出缓冲区地址没有定义,须在
usb_conf.h
中定义具体定义可以参考端点
0
的定义。
读从主机输出的数据(
out
):
out
传输过程是
1.
定义
out
回调函数;
2.
从缓冲区读出数据:
3.
设置
USB
输入有效。
void EP1_OUT_Callback(void)
{
u8 DataLen;
DataLen = GetEPRxCount(ENDP1);
PMAToUserBufferCopy(rcvData, ENDP1_RXADDR, DataLen);
SetEPRxValid(ENDP1);
}
注意在一般情况下
,EPX_OUT_Callback
()回调函数的申明为空执行函数。需将
usb_conf.h
中
#define EPX_IN_Callback NOP_Process
隐掉。再在合适的地方从新定义
void EP1_OUT_Callback(void)
(合适的位置是指定义之后运行不会出现
EP1_OUT_Callback
为申明的错误就行)。
总结,在此将stm32芯片的usb通讯进行了简单的阐述。本人水平有限,以上难免会有错误,希望大家积极留言,共同探讨,共同进步。这篇文章是断断续续写的,给大家带来不便,在此向大家道歉了。不管怎样希望这篇文章能够对那些还在对stm32usb编程初步摸索的朋友有一点帮助。
EZ-USB中PID为何需要DATA0和DATA1两个
因为USB构架对错误的校正是非常严谨的。就如前面所提的,ACK握手是给主机一个信号:外围器件正确的接收到了主机所发送的数据。但是握手数据包自身会不会在传输中被混淆呢?为了能够检测这个错误,主机和外围设备两边都各自维护一个与数据包传输相关的校验位,当数据到达目的地时,内部校验位就会与DATA0或者DATA1来进行比较。当主机或者外围设备发送数据时,它们交互的发送DATA0和DATA1。主机和外围设备就可以通过对数据PID与内部校验位状态的比较来确定错误的握手数据包。
综上得出一个结论:ACK信号只是在收到DATA0或DATA1数据包后的一个回应,如果DATA0或者DATA1无法到达目标自然目标就认为不存在这次数据传输,这样容易产生书籍。因此通过DATA0和DATA1的交替传输,如果检测到两个DATA0或者两个DATA1那么就表明数据包传输错误或者被遗漏。
根据我做firmware的经验,data0/data1除了区分相邻的2个包以外,没有用处。之所以要有data0和data1,就是为了防止把某个包认成另外一个,造成数据出错。
也可以进行容错处理,或保持主机和设备双方之间的数据的同步。比如,设备接收到一个错误的包,他不改变自己的pid,主机只有接收到确认的包才改变自己的pid.
DATA0 ,DATA1主要是用于数据的检错,它是数据报的前导字段,在数据传输过程中,依次是“DATA0,DATA1...”。
是USB的数据包类型中的PID名称 令牌:IN,OUT,SOF,SETUP 数据:DATA0,DATA1,DATA2,MDATA 握手:ACK,NAK,STALL,NYET 特殊类型:PRE,ERR,SPLIT,PIN