本帖最后由 lilijin1995 于 2022-12-9 16:39 编辑
背景:
今天要水的帖子是关于使用CH32V203替代旧STM32F103实现USB霍尔摇杆的项目。关于调试经验啥的,主要分享CH32V203的USB使用。这个项目最开始是使用迈来芯的方案,但是价格30RMB/PCS,也是芯片涨价时候涨上去的,后来替换了同厂不同方案,这个方案芯片对磁场灵敏度很高,高得离谱,迫不得已更换了磁铁,后来总算满足了,2021年时候发现了国产替代方案,矽睿科技的QMC磁力计方案,本来想推给老板更换的,然而老板觉得不成熟,时至今日,老板实在用不起国外的方案了,机会来了,我对WCH的CH32V RISC-V系列MCU比较熟悉,今天我们就开始国产化替代过程吧!
关于硬件
由于需要兼容STM32的USB,所以这里使用了PA11(DM)和PA12(DP),对应CH32V203的USB1,我看USB Device 的实现跟STM32 的HAL库居然有点一致了,之前一致是在中断回调函数中枚举的。其实我更喜欢第二种方式,可能效率会更高。不过USB2好像还是原来的方式。
软件设计
我是在懒得新建工程,移植USB Library了,干脆直接在官方工程上改,我用的是EXAM\USB\USBD\CompositeKM,USBD目录的工程用的USB1口对应PA11(DM)和PA12(DP),大家不要搞错了,我就搞错过,还寄了芯片给WCH的FAE调试,后来发现我自己用错IO了。
1.时钟初始化配置,
初始化需要配置RCC_USBCLKConfig,以及RCC_APB1PeriphClockCmd,而在例程中是使用Set_USBConfig调用实现的,如下代码:
void Set_USBConfig( )
{
RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_Div3);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);
}
大家都知道,USB 设备一般都是使用48MHz的时钟,而例程的PLLCLK是144Mhz,所以这里是3分频,另外需要使能一下APB1的外设时钟。
2.USB端点配置、描述符配置标准请求的实现等。
端点地址、大小,描述符,各类请求的实现是通过以下函数实现:void USB_Init(void)
{
pInformation = &Device_Info;
pInformation->ControlState = 2;
pProperty = &Device_Property;
pUser_Standard_Requests = &User_Standard_Requests;
pProperty->Init();
}
如果你定位到pInformation,pProperty,以及pUser_Standard_Requests的定义,可以发现,
pInformation的定义其实是DEVICE_INFO这样子的,这其实是一些USB请求事务信息。
typedef struct _DEVICE_INFO
{
uint8_t USBbmRequestType; /* bmRequestType */
uint8_t USBbRequest; /* bRequest */
uint16_t_uint8_t USBwValues; /* wValue */
uint16_t_uint8_t USBwIndexs; /* wIndex */
uint16_t_uint8_t USBwLengths; /* wLength */
uint8_t ControlState; /* of type CONTROL_STATE */
uint8_t Current_Feature;
uint8_t Current_Configuration; /* Selected configuration */
uint8_t Current_Interface; /* Selected interface of current configuration */
uint8_t Current_AlternateSetting;/* Selected Alternate Setting of current
interface*/
ENDPOINT_INFO Ctrl_Info;
}DEVICE_INFO;
而pProperty的定义更复杂以下,DEVICE_PROP定义如下,如果说DEVICE_INFO是数据结构的话,我觉得DEVICE_PROP更像是实现,因为里面都是一些函数指针,和一些必要的指针和变量,囊括了USB 设备的枚举过程:端点的配置,各种请求类,相关描述符的获取。
typedef struct _DEVICE_PROP
{
void (*Init)(void); /* Initialize the device */
void (*Reset)(void); /* Reset routine of this device */
void (*Process_Status_IN)(void);
void (*Process_Status_OUT)(void);
RESULT (*Class_Data_Setup)(uint8_t RequestNo);
RESULT (*Class_NoData_Setup)(uint8_t RequestNo);
RESULT (*Class_Get_Interface_Setting)(uint8_t Interface, uint8_t AlternateSetting);
uint8_t* (*GetDeviceDescriptor)(uint16_t Length);
uint8_t* (*GetConfigDescriptor)(uint16_t Length);
uint8_t* (*GetStringDescriptor)(uint16_t Length);
void* RxEP_buffer;
uint8_t MaxPacketSize;
}DEVICE_PROP;
在这里我们先来改描述符的获取吧, uint8_t* (*GetDeviceDescriptor)(uint16_t Length);
uint8_t* (*GetConfigDescriptor)(uint16_t Length);
uint8_t* (*GetStringDescriptor)(uint16_t Length);
这三个指针函数是获取设备描述符,配置描述符集合,字符串描述符。
设备描述符:以下是我雷蛇鼠标的:
配置描述符集合包含了配置描述符,接口描述符,HID描述符(如果是HID),端点描述符:
上图只是接口0的,共有三个接口,其实都是同理可得。
HID每个接口几乎都有自己的报表描述符。
在代码中如何实现的呢?
Device_Property中的USBD_Data_Setup里面有个USBD_GetReportDescriptor请求,这是获取报表描述符的。
uint8_t *USBD_GetReportDescriptor(uint16_t Length)
{
uint8_t wIndex0 = pInformation->USBwIndexs.bw.bb0;
if (wIndex0 > 2)
{
return NULL;
}
else
{
return Standard_GetDescriptorData(Length, &Report_Descriptor[wIndex0]);
}
}
ONE_DESCRIPTOR Report_Descriptor[3] =
{
{(uint8_t*)USBD_JoystickRepDesc, USBD_SIZE_REPORT_DESC_JS},
{(uint8_t*)USBD_MouseRepDesc, USBD_SIZE_REPORT_DESC_MS},
{(uint8_t*)USBD_VendorRepDesc, USBD_SIZE_REPORT_DESC_VD},
};
如果设备描述符、配置描述符、接口描述符、HID描述符、端点描述符、报表描述符没有问题的话,是不是就一定可以枚举成功了呢,是不是就可以高枕无忧了呢,然而事实并非如此。还需要注意一下端点地址和大小的配置。
端点地址和缓存大小其实也是在Device_Property中的USBD_Reset复位中的,
void USBD_Reset(void)
{
pInformation->Current_Configuration = 0;
pInformation->Current_Feature = USBD_ConfigDescriptor[7];
pInformation->Current_Interface = 0;
SetBTABLE(BTABLE_ADDRESS);
SetEPType(ENDP0, EP_CONTROL);
SetEPTxStatus(ENDP0, EP_TX_STALL);
SetEPRxAddr(ENDP0, ENDP0_RXADDR);
SetEPTxAddr(ENDP0, ENDP0_TXADDR);
Clear_Status_Out(ENDP0);
SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
SetEPRxValid(ENDP0);
_ClearDTOG_RX(ENDP0);
_ClearDTOG_TX(ENDP0);
SetEPType(ENDP1, EP_INTERRUPT);
SetEPRxAddr(ENDP1, ENDP1_RXADDR);
SetEPTxAddr(ENDP1, ENDP1_TXADDR);
SetEPTxStatus(ENDP1, EP_TX_NAK);
Clear_Status_Out(ENDP1);
SetEPRxCount(ENDP1, Device_Property.MaxPacketSize);
SetEPRxValid(ENDP1);
_ClearDTOG_TX(ENDP1);
_ClearDTOG_RX(ENDP1);
SetEPType(ENDP2, EP_INTERRUPT);
SetEPTxAddr(ENDP2, ENDP2_TXADDR);
SetEPTxStatus(ENDP2, EP_TX_NAK);
_ClearDTOG_TX(ENDP2);
_ClearDTOG_RX(ENDP2);
SetEPType(ENDP3, EP_INTERRUPT);
SetEPTxAddr(ENDP3, ENDP3_TXADDR);
SetEPTxStatus(ENDP3, EP_TX_NAK);
_ClearDTOG_TX(ENDP3);
_ClearDTOG_RX(ENDP3);
SetDeviceAddress(0);
bDeviceState = ATTACHED;
}
地址是偏移地址,需要根据端点描述符中的最大Size来配置并偏移。
如果你的设备成功枚举,这时候HID还不能动的,需要实现端点数据的上报,如下:uint8_t USBD_ENDPx_DataUp( uint8_t endp, uint8_t *pbuf, uint16_t len )
{
if( endp == ENDP1 )
{
if (USBD_Endp1_Busy)
{
return USB_ERROR;
}
USB_SIL_Write( EP1_IN, pbuf, len );
USBD_Endp1_Busy = 1;
SetEPTxStatus( ENDP1, EP_TX_VALID );
}
else if( endp == ENDP2 )
{
if (USBD_Endp2_Busy)
{
return USB_ERROR;
}
USB_SIL_Write( EP2_IN, pbuf, len );
USBD_Endp2_Busy = 1;
SetEPTxStatus( ENDP2, EP_TX_VALID );
}
else if( endp == ENDP3 )
{
if (USBD_Endp3_Busy)
{
return USB_ERROR;
}
USB_SIL_Write( EP3_IN, pbuf, len );
USBD_Endp3_Busy = 1;
SetEPTxStatus( ENDP3, EP_TX_VALID );
}
else
{
return USB_ERROR;
}
return USB_SUCCESS;
}
依葫芦画瓢,根据端点不同改一下即可实现不同的端点数据上报。
最后,希望大家样机调试成功!!
|