學習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設備只能有一個設備描述符,一個設備描述符可以有多個配置描述符,一個配置描述符可以有多個接口描述符,一個接口描述符可以包含多個端點。
關於傳輸
- 在USB的通訊中,有傳輸(transfer),事務(Transaction),包(packet)三級。包是最基礎的傳輸單元,與TCP/IP協議中的MAC層協議作用相同。
- 在一次傳輸中,由多次事務組成,每次的事務又由多個包組成
- 與衆多協議相同,較高級別的協議的報文是基於/內嵌在低級協議的報文當中的,在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