#申请原创# @21小跑堂
沁恒 CH32V103 在引脚和功能上几乎和 STM32F103 系列通用,在保持通用的情况下,还创新的增加了 USB Host 功能
曾经用 STM32F103 做过仪表,需要把仪表数据导出到U盘,但 STM32F103 不支持 Host,需要外接沁恒的 CH375、CH376 进行读写U盘
现在使用单片 CH32V103 就能替代完成这些功能,省钱了
最近使用沁恒 CH32V103 做了个 MP3,支持 SD 卡歌曲播放,既然有 USB Host,那也能支持U盘歌曲播放
Duang,开整(U盘用的少,就用读卡器+SD卡替代了)
沁恒官方也提供了一份适用于 CH32V103 的U盘读写库 libRV3UFI.a,提供了常用的文件读写操作
extern UINT8 CHRV3GetVer( void ); /* 获取当前子程序库的版本号 */
extern void CHRV3DirtyBuffer( void ); /* 清除磁盘缓冲区 */
extern UINT8 CHRV3BulkOnlyCmd( PUINT8 DataBuf ); /* 执行基于BulkOnly协议的命令 */
extern UINT8 CHRV3DiskReady( void ); /* 查询磁盘是否准备好 */
extern UINT8 CHRV3AnalyzeError( UINT8 iMode ); /* USB操作失败分析CHRV3IntStatus返回错误状态 */
extern UINT8 CHRV3FileOpen( void ); /* 打开文件或者枚举文件 */
extern UINT8 CHRV3FileClose( void ); /* 关闭当前文件 */
extern UINT8 CHRV3FileErase( void ); /* 删除文件并关闭 */
extern UINT8 CHRV3FileCreate( void ); /* 新建文件并打开,如果文件已经存在则先删除后再新建 */
extern UINT8 CHRV3FileAlloc( void ); /* 根据文件长度调整为文件分配的磁盘空间 */
extern UINT8 CHRV3FileModify( void ); /* 查询或者修改当前文件的信息 */
extern UINT8 CHRV3FileQuery( void ); /* 查询当前文件的信息 */
extern UINT8 CHRV3FileLocate( void ); /* 移动当前文件指针 */
extern UINT8 CHRV3FileRead( void ); /* 从当前文件读取数据到指定缓冲区 */
extern UINT8 CHRV3FileWrite( void ); /* 向当前文件写入指定缓冲区的数据 */
extern UINT8 CHRV3ByteLocate( void ); /* 以字节为单位移动当前文件指针 */
extern UINT8 CHRV3ByteRead( void ); /* 以字节为单位从当前位置读取数据块 */
extern UINT8 CHRV3ByteWrite( void ); /* 以字节为单位向当前位置写入数据块 */
extern UINT8 CHRV3DiskQuery( void ); /* 查询磁盘信息 */
extern void CHRV3SaveVariable( void ); /* 备份/保存/恢复子程序库的变量,用于子程序库在多个芯片或者U盘之间进行切换 */
这函数看着眼熟,看起来就像是 CH375 上面的那套
直接使用,就能遍历、读写文件了
官方例程也提供了创建长文件名的示例,但是没找到读取长文件名的示例,肯定是偷懒了
咋办,想想办法呗
USB协议相对复杂,可以参考相关文档,这里就不展开了
简单来说,U盘插入后,引起USB总线电平变化触发中断,根据D+D-的电平信号,USB主机就知道设备的速度类型,低速或者全速
接着USB主机复位USB总线(ResetRootHubPort),然后读取设备描述符(CtrlGetDeviceDescr),获取以下设备信息
typedef struct __PACKED _USB_DEVICE_DESCR {
UINT8 bLength;
UINT8 bDescriptorType;
UINT16 bcdUSB;
UINT8 bDeviceClass;
UINT8 bDeviceSubClass;
UINT8 bDeviceProtocol;
UINT8 bMaxPacketSize0;
UINT16 idVendor;
UINT16 idProduct;
UINT16 bcdDevice;
UINT8 iManufacturer;
UINT8 iProduct;
UINT8 iSerialNumber;
UINT8 bNumConfigurations;
} USB_DEV_DESCR;
至此主机就知道设备的 PID、VID、使用的USB协议版本、端点情况等
接着主机发出设置地址指令(CtrlSetUsbAddress),重设USB设备的地址(这之前USB设备都是以默认的0地址与主机通信)
接下去都以新地址与USB设备通信,首先读取配置描述符(CtrlGetConfigDescr),获取以下配置信息
typedef struct __PACKED _USB_CONFIG_DESCR {
UINT8 bLength;
UINT8 bDescriptorType;
UINT16 wTotalLength;
UINT8 bNumInterfaces;
UINT8 bConfigurationValue;
UINT8 iConfiguration;
UINT8 bmAttributes;
UINT8 MaxPower;
} USB_CFG_DESCR;
接口描述符和端点描述符也可以在这个过程返回给主机
typedef struct __PACKED _USB_INTERF_DESCR {
UINT8 bLength;
UINT8 bDescriptorType;
UINT8 bInterfaceNumber;
UINT8 bAlternateSetting;
UINT8 bNumEndpoints;
UINT8 bInterfaceClass;
UINT8 bInterfaceSubClass;
UINT8 bInterfaceProtocol;
UINT8 iInterface;
} USB_ITF_DESCR;
typedef struct __PACKED _USB_ENDPOINT_DESCR {
UINT8 bLength;
UINT8 bDescriptorType;
UINT8 bEndpointAddress;
UINT8 bmAttributes;
UINT16 wMaxPacketSize;
UINT8 bInterval;
} USB_ENDP_DESCR;
至此主机已经获取到足够的信息,可以根据 bInterfaceClass 判断设备是 鼠标、键盘、U盘等类型
主机设置配置,如果是U盘,可以使用 SCSI 指令集或者 UFI 指令集中的一部分指令操作U盘
当然,这些指令还需要进行 CBW 命令块封包,通过批量传输 BulkOnly 协议发送
U盘数据读写也是通过 CBW 命令设置读取的,能读写U盘后,剩下的文件系统就由 FATFS 接管了
修改 FATFS 里 diskio.c 文件里的 disk_initialize 函数,增加U盘初始化函数
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber (0..) */
)
{
u8 res=0;
switch(pdrv)
{
case SD_CARD:
res = SD_Initialize();
if(res) // STM32 SPI的bug,在sd卡操作失败的时候如果不执行下面的语句,可能导致SPI读写异常
SD_SPI_ReadWriteByte(0xff); // 错误,提供额外的8个时钟
SD_Card_SPI_SetSpeed(0);
break;
case EX_FLASH:
break;
case USB_DISK:
res = CHRV3DiskReady();
break;
default:
res = 1;
}
if(res) return STA_NOINIT;
else return 0; //初始化成功
}
修改读写扇区函数
// 读扇区
// pdrv:磁盘编号0~9,*buff:数据接收缓冲首地址,sector:扇区地址,count:需要读取的扇区数
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address (LBA) */
UINT count /* Number of sectors to read (1..128) */
)
{
u8 res=0;
if (!count) return RES_PARERR; // count不能等于0,否则返回参数错误
switch(pdrv)
{
case SD_CARD:
res = SD_ReadDisk(buff, sector, count);
if(res) // STM32 SPI的bug,在sd卡操作失败的时候如果不执行下面的语句,可能导致SPI读写异常
SD_SPI_ReadWriteByte(0xff); // 提供额外的8个时钟
break;
case EX_FLASH:
break;
case USB_DISK:
CHRV3vLbaCurrent = sector;
res = CHRV3ReadSector(count, buff);
break;
default:
res=1;
}
if(res==0x00) return RES_OK;
else return RES_ERROR;
}
// 写扇区
// pdrv:磁盘编号0~9,*buff:发送数据首地址,sector:扇区地址,count:需要写入的扇区数
#if _USE_WRITE
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber (0..) */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address (LBA) */
UINT count /* Number of sectors to write (1..128) */
)
{
u8 res=0;
if (!count) return RES_PARERR;
switch(pdrv)
{
case SD_CARD:
res = SD_WriteDisk((u8*)buff,sector,count);
break;
case EX_FLASH:
res=0;
break;
case USB_DISK:
CHRV3vLbaCurrent = sector;
res = CHRV3WriteSector(count, (u8*)buff);
break;
default:
res=1;
}
if(res == 0x00) return RES_OK;
else return RES_ERROR;
}
#define SD_CARD 0 // SD Card
#define EX_FLASH 1 // Flash Chip
#define USB_DISK 2 // USB Disk
就可以了,记得把 ffconf.h 里的 #define _VOLUMES 3,改掉,支持几个盘,就改成几个
好了,再总结一下:
先初始化USB Host
pHOST_RX_RAM_Addr = RxBuffer;
pHOST_TX_RAM_Addr = TxBuffer;
USB_Host_Config(ENABLE);
CHRV3LibInit();
SetUsbSpeed(1); // 全速
然后循环检测U盘是否插入
if(R8_USB_INT_FG & RB_UIF_DETECT) // RB_UIF_DETECT:USB主机模式下USB设备连接或断开事件中断标志位
{
R8_USB_INT_FG = RB_UIF_DETECT ; // 检测到USB设备连接或断开触发
ret = AnalyzeRootHub(); // 分析跟集线器状态
if(ret == ERR_USB_CONNECT)
{
FoundNewDev = 1;
printf("Insert Device\r\n");
}
else if(ret == ERR_USB_DISCON)
{
printf( "Remove Device\r\n" );
}
}
检测到插入后,初始化设备,即获取描述符,设置地址等
FoundNewDev = 0;
ret = InitRootDevice(Com_Buffer);
if (ret == ERR_SUCCESS)
{
CHRV3DiskStatus = DISK_USB_ADDR;
printf( "Device Enum Succeed\r\n" );
}
else
{
printf("Device Enum Failed\r\n");
}
然后就可以挂载U盘,进行读写了
res_sd = f_mount(&usbfs, u"2:", 1);
if(res_sd == FR_OK)
{
printf("Mount OK\r\n");
ListIndex = 0;
ListCount = File_Scan(u"2:", "MP3", ListIndex, fname);
printf("%d\r\n", ListCount);
}
else if(res_sd == FR_NO_FILESYSTEM)
{
printf("No File System\r\n");
}
else
{
printf("Mount Fail: %d\r\n", res_sd);
}
USB全速设备速度不会超过12Mbps,即每秒1.5M Byte
|