【單片機筆記】基於STM32F103C8的 USB 外部flash虛擬U盤

學習stm32已經很長時間了,但是一直沒有過多的學習stm32的USB部分,因爲實際工作還是用的比較少。說起USB那就有的說了,因爲USB的功能很強大,這裏主要重點記錄一下STM32的USB部分,這個官方給的有專門USB庫,筆者目前使用的是Cotex-M3內核的STM32F103系列,實驗的芯片爲STM32F103C8,這個是目前市場上性價比非常高的芯片,也是用的非常多的芯片。

USB基礎知識

USB按接口類型分

控制器/主機(controller/host)

設備(peripheral)

OTG(on-the-go),通過id線確定作爲主機還是作爲設備

按照USB速度分

低速(low speed)

全速(full speed)

高速(high speed)

USB接口一般是4根線,VCC GND DM(D-) DP(D+)

低速設備:在DM線上接入上拉

全速設備:在DP線上接入上拉

高速設備:在DP線上接入上拉,在主機對設備進行復位後進一步的確認

 

關於描述符

設備描述符(device description)

配置描述符(config description)

接口描述符 (interface description)

端點描述符 (endpoint description)

設備的“身份”信息存儲在描述符中。每個USB設備中都有如下描述符。需要注意的是一個USB設備只能有一個設備描述符,一個設備描述符可以有多個配置描述符,一個配置描述符可以有多個接口描述符,一個接口描述符可以包含多個端點。

關於傳輸

  1. 在USB的通訊中,有傳輸(transfer),事務(Transaction),包(packet)三級。包是最基礎的傳輸單元,與TCP/IP協議中的MAC層協議作用相同。
  2. 在一次傳輸中,由多次事務組成,每次的事務又由多個包組成
  3. 與衆多協議相同,較高級別的協議的報文是基於/內嵌在低級協議的報文當中的,在USB中也不例外,例如,包中預留了DATA位,其目的就是填寫報文

STM32F103芯片自帶了有USB模塊,可以用來做從設備,不能用作主機HOST。這裏使用USB的目的是講USB作爲一個大容量,這個可以基於官方USB Mass_Storage例程來移植。這裏就不過多的介紹裏面複雜的通信原理了,要徹徹底底搞明白並靈活運用取來還是有很大難度的,因爲內容太多了。這裏僅僅介紹下移植過程和描述符相關的重點內容。

準備工作

第一步

確保自己的KEIL開發環境已經完善。

第二步

下載好了兩份ST官方的庫文件(我是基於標準庫開發的),一個是標準外設庫文件,一個是USB庫文件。如下圖:

 

沒有的可以自行下載

標準外設庫:https://www.stmcu.org.cn/document/detail/index/id-213160

USB外設庫:https://www.stmcu.org.cn/document/detail/index/id-200293

第三步

建立一個帶有存儲介質驅動的STM32基礎工程,存儲介質常見有SD卡、外部FLASH芯片、內部的FLASH空間。的我是基於一個外部flash的工程去實現的,芯片具體型號是W25Q64,64Mbit的空間,換成字節就是8MByte。驅動部分如下:

C文件

#include "fy_w25qxx.h" 

u16 W25QXX_TYPE=0;//W25Q64

//4Kbytes爲一個Sector,16個扇區爲1個Block,容量爲16M字節,共有128個Block,4096個Sector 
//初始化SPI FLASH的IO口
void W25QXX_Configuration(void)
{	
	SPI2_SetSpeed(SPI_BaudRatePrescaler_2);//設置爲18M時鐘,高速模式
	W25QXX_TYPE=W25QXX_ReadID();//讀取FLASH ID.  
}  	

//讀取W25QXX的狀態寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//SPR:默認0,狀態寄存器保護位,配合WP使用
//TB,BP2,BP1,BP0:FLASH區域寫保護設置
//WEL:寫使能鎖定
//BUSY:忙標記位(1,忙;0,空閒)
//默認:0x00
u8 W25QXX_ReadSR(void)   
{  
	u8 byte=0;   
	W25QXX_CS_L();                            //使能器件   
	SPI2_ReadWriteByte(W25X_ReadStatusReg); //發送讀取狀態寄存器命令    
	byte=SPI2_ReadWriteByte(0Xff);          //讀取一個字節  
	W25QXX_CS_H();                            //取消片選     
	return byte;   
} 
//寫W25QXX狀態寄存器
//只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以寫!!!
void W25QXX_Write_SR(u8 sr)   
{   
	W25QXX_CS_L();                            //使能器件   
	SPI2_ReadWriteByte(W25X_WriteStatusReg);//發送寫取狀態寄存器命令    
	SPI2_ReadWriteByte(sr);               	//寫入一個字節  
	W25QXX_CS_H();                            //取消片選     	      
}   
//W25QXX寫使能	
//將WEL置位   
void W25QXX_Write_Enable(void)   
{
	W25QXX_CS_L();                          	//使能器件   
    SPI2_ReadWriteByte(W25X_WriteEnable); 	//發送寫使能  
	W25QXX_CS_H();                           	//取消片選     	      
} 
//W25QXX寫禁止	
//將WEL清零  
void W25QXX_Write_Disable(void)   
{  
	W25QXX_CS_L();                            //使能器件   
    SPI2_ReadWriteByte(W25X_WriteDisable);  //發送寫禁止指令    
	W25QXX_CS_H();                            //取消片選     	      
} 		

//讀取芯片ID 
u16 W25QXX_ReadID(void)
{
	u16 Temp = 0;	  
	W25QXX_CS_L();				    
	SPI2_ReadWriteByte(0x90);//發送讀取ID命令	    
	SPI2_ReadWriteByte(0x00); 	    
	SPI2_ReadWriteByte(0x00); 	    
	SPI2_ReadWriteByte(0x00); 	 			   
	Temp|=SPI2_ReadWriteByte(0xFF)<<8;  
	Temp|=SPI2_ReadWriteByte(0xFF);	 
	W25QXX_CS_H();				    
	return Temp;
}   		    

//在指定地址開始讀取指定長度的數據
//pBuffer:數據存儲區,ReadAddr:開始讀取的地址(24bit),NumByteToRead:要讀取的字節數(最大65535)
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)   
{ 
 	u16 i;   										    
	W25QXX_CS_L();                            	//使能器件   
	SPI2_ReadWriteByte(W25X_ReadData);         	//發送讀取命令   
	SPI2_ReadWriteByte((u8)((ReadAddr)>>16));  	//發送24bit地址    
	SPI2_ReadWriteByte((u8)((ReadAddr)>>8));   
	SPI2_ReadWriteByte((u8)ReadAddr);   
	for(i=0;i<NumByteToRead;i++)
	{ 
        pBuffer[i]=SPI2_ReadWriteByte(0XFF);   	//循環讀數  
  }
	W25QXX_CS_H();  				    	      
}

//SPI在一頁(0~65535)內寫入少於256個字節的數據
//在指定地址開始寫入最大256字節的數據
//pBuffer:數據存儲區,WriteAddr:開始寫入的地址(24bit),NumByteToWrite:要寫入的字節數(最大256),該數不應該超過該頁的剩餘字節數!!!	 
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
 	u16 i;  
	W25QXX_Write_Enable();                  	//SET WEL 
	W25QXX_CS_L();                            	//使能器件   
	SPI2_ReadWriteByte(W25X_PageProgram);      	//發送寫頁命令   
	SPI2_ReadWriteByte((u8)((WriteAddr)>>16)); 	//發送24bit地址    
	SPI2_ReadWriteByte((u8)((WriteAddr)>>8));   
	SPI2_ReadWriteByte((u8)WriteAddr);   
	for(i=0;i<NumByteToWrite;i++)SPI2_ReadWriteByte(pBuffer[i]);//循環寫數  
	W25QXX_CS_H();                            	//取消片選 
	W25QXX_Wait_Busy();					   		//等待寫入結束
} 
//無檢驗寫SPI FLASH 
//必須確保所寫的地址範圍內的數據全部爲0XFF,否則在非0XFF處寫入的數據將失敗!
//具有自動換頁功能 
//在指定地址開始寫入指定長度的數據,但是要確保地址不越界!
//pBuffer:數據存儲區
//WriteAddr:開始寫入的地址(24bit)
//NumByteToWrite:要寫入的字節數(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{ 			 		 
	u16 pageremain;	   
	pageremain=256-WriteAddr%256; //單頁剩餘的字節數		 	    
	if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大於256個字節
	while(1)
	{	   
		W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
		if(NumByteToWrite==pageremain)break;//寫入結束了
	 	else //NumByteToWrite>pageremain
		{
			pBuffer+=pageremain;
			WriteAddr+=pageremain;	

			NumByteToWrite-=pageremain;			  //減去已經寫入了的字節數
			if(NumByteToWrite>256)pageremain=256; //一次可以寫入256個字節
			else pageremain=NumByteToWrite; 	  //不夠256個字節了
		}
	};	    
} 
//寫SPI FLASH  
//在指定地址開始寫入指定長度的數據
//該函數帶擦除操作!
//pBuffer:數據存儲區
//WriteAddr:開始寫入的地址(24bit)						
//NumByteToWrite:要寫入的字節數(最大65535)   
u8 W25QXX_BUFFER[4096];		 
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{ 
	u32 secpos;
	u16 secoff;
	u16 secremain;	   
 	u16 i;    
	u8 *W25QXX_BUF;	  
	
   	W25QXX_BUF=W25QXX_BUFFER;
//	
//    W25QXX_BUF = (u8 *)_mem.Alloc(4096);//申請一個扇區大小的內存
//    if(W25QXX_BUF==NULL)	return;
	
 	secpos=WriteAddr/4096;//扇區地址  
	secoff=WriteAddr%4096;//在扇區內的偏移
	secremain=4096-secoff;//扇區剩餘空間大小  

 	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//測試用
 	if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大於4096個字節
	while(1) 
	{	
		W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//讀出整個扇區的內容
		for(i=0;i<secremain;i++)//校驗數據
		{
			if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除  	  
		}
		if(i<secremain)//需要擦除
		{
			W25QXX_Erase_Sector(secpos);		//擦除這個扇區
			for(i=0;i<secremain;i++)	   		//複製
			{
				W25QXX_BUF[i+secoff]=pBuffer[i];	  
			}
			W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//寫入整個扇區  

		}else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//寫已經擦除了的,直接寫入扇區剩餘區間. 				   
		if(NumByteToWrite==secremain)break;//寫入結束了
		else//寫入未結束
		{
			secpos++;//扇區地址增1
			secoff=0;//偏移位置爲0 	 

		   	pBuffer+=secremain;  				//指針偏移
			WriteAddr+=secremain;				//寫地址偏移	   
		   	NumByteToWrite-=secremain;			//字節數遞減
			if(NumByteToWrite>4096)secremain=4096;//下一個扇區還是寫不完
			else secremain=NumByteToWrite;		//下一個扇區可以寫完了
		}	 
	}
//	_mem.Free(W25QXX_BUF);
}
//擦除整個芯片		  
//等待時間超長...
void W25QXX_Erase_Chip(void)   
{                                   
    W25QXX_Write_Enable();                 	 	//SET WEL 
    W25QXX_Wait_Busy();   
  	W25QXX_CS_L();                            	//使能器件   
    SPI2_ReadWriteByte(W25X_ChipErase);        	//發送片擦除命令  
	W25QXX_CS_H();                            	//取消片選     	      
	W25QXX_Wait_Busy();   				   		//等待芯片擦除結束
}   
//擦除一個扇區
//Dst_Addr:扇區地址 根據實際容量設置
//擦除一個山區的最少時間:150ms
void W25QXX_Erase_Sector(u32 Dst_Addr)   
{  
	//監視falsh擦除情況,測試用   
// 	printf("fe:%x\r\n",Dst_Addr);	  
 	Dst_Addr*=4096;
    W25QXX_Write_Enable();                  	//SET WEL 	 
    W25QXX_Wait_Busy();   
  	W25QXX_CS_L();                            	//使能器件   
    SPI2_ReadWriteByte(W25X_SectorErase);      	//發送扇區擦除指令 
    SPI2_ReadWriteByte((u8)((Dst_Addr)>>16));  	//發送24bit地址    
    SPI2_ReadWriteByte((u8)((Dst_Addr)>>8));   
    SPI2_ReadWriteByte((u8)Dst_Addr);  
	W25QXX_CS_H();                            	//取消片選     	      
    W25QXX_Wait_Busy();   				   		//等待擦除完成
}  
//等待空閒
void W25QXX_Wait_Busy(void)   
{   
	while((W25QXX_ReadSR()&0x01)==0x01);  		// 等待BUSY位清空
}  
//進入掉電模式
void W25QXX_PowerDown(void)   
{ 
  	W25QXX_CS_L();                           	 	//使能器件   
    SPI2_ReadWriteByte(W25X_PowerDown);        //發送掉電命令  
	W25QXX_CS_H();                            	//取消片選     	      
    Delay_us(3);                               //等待TPD  
}   
//喚醒
void W25QXX_WAKEUP(void)   
{  
  	W25QXX_CS_L();                            	//使能器件   
    SPI2_ReadWriteByte(W25X_ReleasePowerDown);	//  send W25X_PowerDown command 0xAB    
	W25QXX_CS_H();                            	//取消片選     	      
    Delay_us(3);                            	//等待TRES1
}   

H文件

#ifndef __FY_W25QXX_H
#define __FY_W25QXX_H			    

#include "fy_includes.h" 

//W25QXX對應唯一識別ID
#define W25Q80 	0XEF13
#define W25Q16 	0XEF14
#define W25Q32 	0XEF15
#define W25Q64 	0XEF16
#define W25Q128	0XEF17

//指令表
#define W25X_WriteEnable		0x06 
#define W25X_WriteDisable		0x04 
#define W25X_ReadStatusReg		0x05 
#define W25X_WriteStatusReg		0x01 
#define W25X_ReadData			0x03 
#define W25X_FastReadData		0x0B 
#define W25X_FastReadDual		0x3B 
#define W25X_PageProgram		0x02 
#define W25X_BlockErase			0xD8 
#define W25X_SectorErase		0x20 
#define W25X_ChipErase			0xC7 
#define W25X_PowerDown			0xB9 
#define W25X_ReleasePowerDown	0xAB 
#define W25X_DeviceID			0xAB 
#define W25X_ManufactDeviceID	0x90 
#define W25X_JedecDeviceID		0x9F 

void W25QXX_Configuration(void);
u16  W25QXX_ReadID(void);  	    		//讀取FLASH ID
u8	 W25QXX_ReadSR(void);        		//讀取狀態寄存器 
void W25QXX_Write_SR(u8 sr);  			//寫狀態寄存器
void W25QXX_Write_Enable(void);  		//寫使能 
void W25QXX_Write_Disable(void);		//寫保護
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead);   //讀取flash
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//寫入flash
void W25QXX_Erase_Chip(void);    	  	//整片擦除
void W25QXX_Erase_Sector(u32 Dst_Addr);	//扇區擦除
void W25QXX_Wait_Busy(void);           	//等待空閒
void W25QXX_PowerDown(void);        	//進入掉電模式
void W25QXX_WAKEUP(void);				//喚醒

extern u16 W25QXX_TYPE;					//定義W25QXX芯片型號		   

#endif

基礎工程如下:

第四步:USB代碼移植

拷貝USB底層庫到工程根目錄,新建一個目錄命名成USB,拷貝Mass_Storage例程下的src和inc目錄下的所以文件到USB。

拷貝過來是有37個文件,這個時候需要刪除一些不必要的文件,因爲官方的例程裏面有nand,這裏的話先刪除以下文件

4.打開工程把分組和文件及包含路徑添加進去,這裏就不細說了,結果如下

打開usb下面的main和it 把重要的代碼先拷貝出來

把main和stm32_it裏面的內容拷貝到自己的main裏面進行下封裝去掉不要的保留剩下的,然後就刪除it和main三個文件。

然後編譯下,這個時候會出現一堆報錯,需要慢慢一步步搞定,編譯前注意路徑包含,

定位到第一個錯誤進去

這個定義的是官方的開發板 我們直接修改成自己的公共文件就可以,並且註釋掉開發板定義

公共文件如下,並且添加usb部分的頭文件

繼續編譯,定位第一個錯誤,這部分主要修改hw_config文件

刪除Set_System函數

刪除Led_Config函數

 

註釋LED相關的內容

刪除USB_Disconnect_Config

接下來主要修改mass_mal部分,我這裏是只有一個flash,所以我修改成如下:

頭文件

 

下面修改memory文件,主要修改變量定義,C文件和H文件分別如下

接下來是usb_scsi部分的變量定義

這時候再次編譯就發現沒有錯誤了,但是這個地方USB中斷部分還需要配置下,官方用的宏定義方式去實現不同的代碼,這裏一開始就去掉了宏,所以最終修改如下:

/*******************************************************************************
* Function Name  : USB_Interrupts_Config
* Description    : Configures the USB interrupts
* Input          : None.
* Return         : None.
*******************************************************************************/
void USB_Interrupts_Config(void)
{
	
	NVIC_InitTypeDef NVIC_InitStructure;
	EXTI_InitTypeDef EXTI_InitStructure;

 
	/* Configure the EXTI line 18 connected internally to the USB IP */
	EXTI_ClearITPendingBit(EXTI_Line18);
											  //  開啓線18上的中斷
	EXTI_InitStructure.EXTI_Line = EXTI_Line18; // USB resume from suspend mode
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;	//line 18上事件上升降沿觸發
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStructure); 	 

	/* Enable the USB interrupt */
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;	//組2,優先級次之 
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	/* Enable the USB Wake-up interrupt */
	NVIC_InitStructure.NVIC_IRQChannel = USBWakeUp_IRQn;   //組2,優先級最高	
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_Init(&NVIC_InitStructure);  

 
}

 

 

 

再註釋掉USB_Cable_Config的內容(DP上有1.5K上拉電阻)並修改中斷函數

//USB喚醒中斷服務函數
void USBWakeUp_IRQHandler(void) 
{
	EXTI_ClearITPendingBit(EXTI_Line18);//清除USB喚醒中斷掛起位
} 

修改pwr的Suspend內容:

void Suspend(void)
{
	uint32_t i =0;
	uint16_t wCNTR;
	__IO uint32_t savePWR_CR=0;
	/* suspend preparation */
	/* ... */
	
	/*Store CNTR value */
	wCNTR = _GetCNTR();  

    /* This a sequence to apply a force RESET to handle a robustness case */
    
	/*Store endpoints registers status */
    for (i=0;i<8;i++) EP[i] = _GetENDPOINT(i);
	
	/* unmask RESET flag */
	wCNTR|=CNTR_RESETM;
	_SetCNTR(wCNTR);
	
	/*apply FRES */
	wCNTR|=CNTR_FRES;
	_SetCNTR(wCNTR);
	
	/*clear FRES*/
	wCNTR&=~CNTR_FRES;
	_SetCNTR(wCNTR);
	
	/*poll for RESET flag in ISTR*/
	while((_GetISTR()&ISTR_RESET) == 0);
	
	/* clear RESET flag in ISTR */
	_SetISTR((uint16_t)CLR_RESET);
	
	/*restore Enpoints*/
	for (i=0;i<8;i++)
	_SetENDPOINT(i, EP[i]);
	
	/* Now it is safe to enter macrocell in suspend mode */
	wCNTR |= CNTR_FSUSP;
	_SetCNTR(wCNTR);
	
	/* force low-power mode in the macrocell */
	wCNTR = _GetCNTR();
	wCNTR |= CNTR_LPMODE;
	_SetCNTR(wCNTR);
	
	Enter_LowPowerMode();
}

把USB初始化部分不要的註釋掉:

void USB_MSC_Configuration(void)
{
//  Set_System();
  Set_USBClock();
//  Led_Config();
  USB_Interrupts_Config();
  USB_Init();
//  while (bDeviceState != CONFIGURED);

//  USB_Configured_LED();

//  while (1)
//  {}
}

最後在USB初始化之前給U盤定義的數組賦值,不然就只有個盤符

    Mass_Memory_Size[0]=1024*1024*4;	//w25q64->8M 給4M做U盤
    Mass_Block_Size[0] =512;			//設置SPI FLASH的操作扇區大小爲512
    Mass_Block_Count[0]=Mass_Memory_Size[0]/Mass_Block_Size[0];

	USB_MSC_Configuration();

編譯下載後就可以正常運行了。

 

傳輸文件、新建文件夾都可以了,就是傳輸速度感人。

以上就是USB虛擬U盤的所有內容了,內容太多而且複雜,所以如果真的想親手試試,還是要點耐心的。雖然能學到的並不是很多,這裏主要可以幹茶mass_mal部分,可以學到不少東西,方便以後的熟練運用。我這裏也是爲以後更進一步的開發做鋪墊。

By Urien 2019年10月27日 03:19:23

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章