STM32實現HID和u盤複合裝置
阿新 • • 發佈:2018-12-17
USB裝置可以定義一個複合裝置,複合裝置分兩種,一種是一個裝置多個配置,還有一種是一個配置多個介面,在本例中採用一個配置多個介面的方式
首先修改裝置描述符,標準裝置描述符和報告描述符都不需要修改,只需要修改配置描述符即可
//usb配置描述符 const u8 DinkUsbConfigDescriptor[DINK_USB_SIZ_CONFIG_DESC] = { /***************配置描述符***********************/ USB_CONFIGUARTION_DESC_SIZE, //bLength欄位。配置描述符的長度為9位元組。 USB_CONFIGURATION_DESCRIPTOR_TYPE, //bDescriptorType欄位。配置描述符編號為0x02。 //wTotalLength欄位。配置描述符集合的總長度, //包括配置描述符本身、介面描述符、類描述符、端點描述符等。 WBVAL( USB_CONFIGUARTION_DESC_SIZE + //配置描述符 USB_INTERFACE_DESC_SIZE + //介面1描述符 + //hid描述符 USB_ENDPOINT_DESC_SIZE + //端點描述符 USB_ENDPOINT_DESC_SIZE + //端點描述符 USB_INTERFACE_DESC_SIZE + //介面描述符2 USB_ENDPOINT_DESC_SIZE + //端點描述符1 USB_ENDPOINT_DESC_SIZE //端點描述符2 ), 0x02, //bNumInterfaces欄位。該配置包含的介面數,複合裝置,兩個介面。 0x01, //bConfiguration欄位。該配置的值為1。 0x00, //iConfigurationz欄位,該配置的字串索引。這裡沒有,為0。 USB_CONFIG_BUS_POWERED , //bmAttributes欄位,該裝置的屬性 USB_CONFIG_POWER_MA(500), //bMaxPower欄位,該裝置需要的最大電流量 /*********************第一個介面描述符,hid裝置**********************/ USB_INTERFACE_DESC_SIZE, //bLength欄位。介面描述符的長度為9位元組。 USB_INTERFACE_DESCRIPTOR_TYPE, //bDescriptorType欄位。介面描述符的編號為0x04。 0x00, //bInterfaceNumber欄位。該介面的編號,第一個介面,編號為0。 0x00, //bAlternateSetting欄位。該介面的備用編號,為0。 0x02, //bNumEndpoints欄位。非0端點的數目。該介面有2個批量端點 USB_DEVICE_CLASS_HUMAN_INTERFACE, //bInterfaceClass欄位。該介面所使用的類。大容量儲存裝置介面類的程式碼為0x08。, 0x00, //bInterfaceSubClass欄位。該介面所使用的子類。在HID1.1協議中, //只規定了一種子類:支援BIOS引導啟動的子類。 //USB鍵盤、滑鼠屬於該子類,子類程式碼為0x01。 //但這裡我們是自定義的HID裝置,所以不使用子類。 0x00, //bInterfaceProtocol欄位。如果子類為支援引導啟動的子類, //則協議可選擇滑鼠和鍵盤。鍵盤程式碼為0x01,滑鼠程式碼為0x02。 //自定義的HID裝置,也不使用協議。 0x00, //iConfiguration欄位。該介面的字串索引值。這裡沒有,為0。 /*********************HID報告描述符*************************/ //bLength欄位。本HID描述符下只有一個下級描述符。所以長度為9位元組。 0x09, //bDescriptorType欄位。HID描述符的編號為0x21。 0x21, //bcdHID欄位。本協議使用的HID1.1協議。注意低位元組在先。 0x10, 0x01, //bCountyCode欄位。裝置適用的國家程式碼,這裡選擇為美國,程式碼0x21。 0x21, //bNumDescriptors欄位。下級描述符的數目。我們只有一個報告描述符。 0x01, //bDescriptorType欄位。下級描述符的型別,為報告描述符,編號為0x22。 0x22, //bDescriptorLength欄位。下級描述符的長度。下級描述符為報告描述符。 sizeof(HID_ReportDescriptor)&0xFF, (sizeof(HID_ReportDescriptor)>>8)&0xFF, /*********************端點描述符**********************************/ /* 端點描述符 */ USB_ENDPOINT_DESC_SIZE, //bLength欄位。端點描述符長度為7位元組。 USB_ENDPOINT_DESCRIPTOR_TYPE, //bDescriptorType欄位。端點描述符編號為0x05。 USB_ENDPOINT_IN(1), //bEndpointAddress欄位。端點的地址。我們使用D12的輸入端點1。 USB_ENDPOINT_TYPE_INTERRUPT, //bmAttributes欄位。D1~D0為端點傳輸型別選擇。 WBVAL(0x0040), //wMaxPacketSize欄位。該端點的最大包長。最大包長為64位元組。 0x01, //bInterval欄位。端點查詢的時間,端點查詢的時間,此處無意義。 /***********************端點描述符*******************************************/ USB_ENDPOINT_DESC_SIZE, //bLength欄位。端點描述符長度為7位元組。 USB_ENDPOINT_DESCRIPTOR_TYPE, //bDescriptorType欄位。端點描述符編號為0x05。 USB_ENDPOINT_OUT(1), //bEndpointAddress欄位。端點的地址。我們使用D12的輸入端點1。 USB_ENDPOINT_TYPE_INTERRUPT, //bmAttributes欄位。D1~D0為端點傳輸型別選擇。 WBVAL(0x0040), //wMaxPacketSize欄位。該端點的最大包長。最大包長為64位元組。 0x01, //bInterval欄位。端點查詢的時間,端點查詢的時間,此處無意義。 /*******************第二個介面描述符 儲存裝置*********************/ USB_INTERFACE_DESC_SIZE, //bLength欄位。介面描述符的長度為9位元組。 USB_INTERFACE_DESCRIPTOR_TYPE, //bDescriptorType欄位。介面描述符的編號為0x04。 0x01, //bInterfaceNumber欄位。該介面的編號,第二個介面,編號為1。 0x00, //bAlternateSetting欄位。該介面的備用編號,為0。 0x02, //bNumEndpoints欄位。非0端點的數目。該介面有2個批量端點 USB_DEVICE_CLASS_STORAGE, //bInterfaceClass欄位。該介面所使用的類。大容量儲存裝置介面類的程式碼為0x08。, MSC_SUBCLASS_SCSI, //bInterfaceSubClass欄位。SCSI透明命令集的子類程式碼為0x06。 MSC_PROTOCOL_BULK_ONLY, //bInterfaceProtocol欄位。協議為僅批量傳輸,程式碼為0x50。 0x04, //iConfiguration欄位。該介面的字串索引值 /************************************* 端點描述符 *********************************************/ USB_ENDPOINT_DESC_SIZE, //bLength欄位。端點描述符長度為7位元組。 USB_ENDPOINT_DESCRIPTOR_TYPE, //bDescriptorType欄位。端點描述符編號為0x05。 USB_ENDPOINT_IN(2), //bEndpointAddress欄位。端點的地址。我們使用D12的輸入端點1。 USB_ENDPOINT_TYPE_BULK, //bmAttributes欄位。D1~D0為端點傳輸型別選擇。 WBVAL(0x0040), //wMaxPacketSize欄位。該端點的最大包長。最大包長為64位元組。 0x00, //bInterval欄位。端點查詢的時間,端點查詢的時間,此處無意義。 /************************************端點描述符********************************************************/ USB_ENDPOINT_DESC_SIZE, //bLength欄位。端點描述符長度為7位元組。 USB_ENDPOINT_DESCRIPTOR_TYPE, //bDescriptorType欄位。端點描述符編號為0x05。 USB_ENDPOINT_OUT(2), //bEndpointAddress欄位。端點的地址。我們使用D12的輸入端點1。 USB_ENDPOINT_TYPE_BULK, //bmAttributes欄位。D1~D0為端點傳輸型別選擇。 WBVAL(0x0040), //wMaxPacketSize欄位。該端點的最大包長。最大包長為64位元組。 0x00, //bInterval欄位。端點查詢的時間,端點查詢的時間,此處無意義。 };
修改描述符之後要同時記得修改描述符的長度,然後修改usb_prop檔案,主要是兩個多出來的命令GET_MAX_LEN用來獲取當前儲存裝置的個數,還有一個用來複位當前儲存裝置,如下
RESULT DinkUsbData_Setup(u8 RequestNo) { u8 *(*CopyRoutine)(u16); CopyRoutine = NULL; if ((RequestNo == GET_DESCRIPTOR) && (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT)) && (pInformation->USBwIndex0 == 0)) { //獲取報告描述符 if (pInformation->USBwValue1 == REPORT_DESCRIPTOR) { CopyRoutine = DinkUsbGetReportDescriptor; } //獲取HID描述符 else if (pInformation->USBwValue1 == HID_DESCRIPTOR_TYPE) { CopyRoutine = DinkUsbGetHIDDescriptor; } } /*** GET_PROTOCOL ***/ else if ((Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT)) && RequestNo == GET_PROTOCOL) { CopyRoutine = DinkUsbGetProtocolValue;//獲取協議值 } else if ((Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT)) && (RequestNo == GET_MAX_LUN) && (pInformation->USBwValue == 0) && (pInformation->USBwIndex == 0) && (pInformation->USBwLength == 0x01)) { CopyRoutine = Get_Max_Lun; } if (CopyRoutine == NULL) { return USB_UNSUPPORT; } pInformation->Ctrl_Info.CopyData = CopyRoutine; pInformation->Ctrl_Info.Usb_wOffset = 0; (*CopyRoutine)(0); return USB_SUCCESS; }
GET_MAX_LEN的函式體為
u8 *Get_Max_Lun(u16 Length)
{
if (Length == 0)
{
pInformation->Ctrl_Info.Usb_wLength = LUN_DATA_LENGTH;
return 0;
}
else
{
return((u8*)(&Max_Lun));
}
}
對了,因為這一次使用了端點2作為儲存裝置使用的端點,所以要在初始化的時候順便也多初始化兩個端點
//裝置復位 void DinkUsbReset(void) { Device_Info.Current_Configuration = 0; //選擇當前配置為0 pInformation->Current_Feature = DinkUsbConfigDescriptor[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); SetEPType(ENDP1, EP_INTERRUPT);//初始化端點1為中斷傳輸模式,用來報告一些狀態 SetEPTxAddr(ENDP1, ENDP1_TXADDR);//設定端點地址 SetEPRxAddr(ENDP1, ENDP1_RXADDR);//設定端點地址 SetEPRxStatus(ENDP1, EP_RX_VALID);//使能接收 SetEPTxStatus(ENDP1, EP_TX_NAK); //不使能傳送 SetEPRxCount(ENDP1, 64);//設定接收最大長度 Clear_Status_Out(ENDP1); SetEPType(ENDP2, EP_BULK);//初始化端點1為中斷傳輸模式,用來報告一些狀態 SetEPTxAddr(ENDP2, ENDP2_TXADDR);//設定端點地址 SetEPRxAddr(ENDP2, ENDP2_RXADDR);//設定端點地址 SetEPRxStatus(ENDP2, EP_RX_VALID);//使能接收 SetEPTxStatus(ENDP2, EP_TX_NAK); //不使能傳送 SetEPRxCount(ENDP2, 64);//設定接收最大長度 Clear_Status_Out(ENDP2); bDeviceState = ATTACHED;//裝置插入 SetDeviceAddress(0);//設定當前地址為0 usb_debug_printf("USB Reset\r\n"); }
然後就是端點響應了,端點2的響應檔案如下
void EP2_IN_Callback(void)
{
Mass_Storage_In();
}
//USB匯流排傳送過來資料
void EP2_OUT_Callback(void)
{
Mass_Storage_Out();
}
對應具體的程式碼就是這樣
/*******************************************************************************
* Function Name : Mass_Storage_In
* Description : Mass Storage IN transfer.
* Input : None.
* Output : None.
* Return : None.
//裝置->USB
*******************************************************************************/
void Mass_Storage_In (void)
{
USB_STATUS_REG|=0X10;//標記輪詢
switch (Bot_State)
{
case BOT_CSW_Send:
case BOT_ERROR:
Bot_State = BOT_IDLE;
SetEPRxStatus(ENDP2, EP_RX_VALID);/* enable the Endpoint to recive the next cmd*/
break;
case BOT_DATA_IN: //USB從裝置讀資料
switch (CBW.CB[0])
{
case SCSI_READ10:
USB_STATUS_REG|=0X02;//標記正在讀資料
SCSI_Read10_Cmd(CBW.bLUN , SCSI_LBA , SCSI_BlkLen);
break;
}
break;
case BOT_DATA_IN_LAST:
Set_CSW (CSW_CMD_PASSED, SEND_CSW_ENABLE);
SetEPRxStatus(ENDP2, EP_RX_VALID);
break;
default:
break;
}
}
/*******************************************************************************
* Function Name : Mass_Storage_Out
* Description : Mass Storage OUT transfer.
* Input : None.
* Output : None.
* Return : None.
//USB->裝置
*******************************************************************************/
void Mass_Storage_Out (void)
{
u8 CMD;
USB_STATUS_REG|=0X10;//標記輪詢
CMD = CBW.CB[0];
Data_Len = GetEPRxCount(ENDP2);
PMAToUserBufferCopy(Bulk_Data_Buff, ENDP2_RXADDR, Data_Len);//讀取端點快取
switch (Bot_State)//根據狀態進行處理
{
case BOT_IDLE://最開始的命令階段
CBW_Decode();
break;
case BOT_DATA_OUT://USB傳送資料到裝置
if (CMD == SCSI_WRITE10)
{
USB_STATUS_REG|=0X01;//標記正在寫資料
SCSI_Write10_Cmd(CBW.bLUN , SCSI_LBA , SCSI_BlkLen);
break;
}
Bot_Abort(DIR_OUT);
Set_Scsi_Sense_Data(CBW.bLUN, ILLEGAL_REQUEST, INVALID_FIELED_IN_COMMAND);
Set_CSW (CSW_PHASE_ERROR, SEND_CSW_DISABLE);
break;
default:
Bot_Abort(BOTH_DIR);
Set_Scsi_Sense_Data(CBW.bLUN, ILLEGAL_REQUEST, INVALID_FIELED_IN_COMMAND);
Set_CSW (CSW_PHASE_ERROR, SEND_CSW_DISABLE);
break;
}
}
/*******************************************************************************
* Function Name : CBW_Decode
* Description : Decode the received CBW and call the related SCSI command
* routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void CBW_Decode(void)
{
u32 Counter;
for (Counter = 0; Counter < Data_Len; Counter++)
{
*((u8 *)&CBW + Counter) = Bulk_Data_Buff[Counter];
}//將buf資料拷貝入cbw結構體,便於下一次處理
CSW.dTag = CBW.dTag;
CSW.dDataResidue = CBW.dDataLength;
if (Data_Len != BOT_CBW_PACKET_LENGTH)
{
Bot_Abort(BOTH_DIR);
/* reset the CBW.dSignature to desible the clear feature until receiving a Mass storage reset*/
CBW.dSignature = 0;
Set_Scsi_Sense_Data(CBW.bLUN, ILLEGAL_REQUEST, PARAMETER_LIST_LENGTH_ERROR);
Set_CSW (CSW_CMD_FAILED, SEND_CSW_DISABLE);
return;
}
if ((CBW.CB[0] == SCSI_READ10 ) || (CBW.CB[0] == SCSI_WRITE10 ))
{
/* Calculate Logical Block Address */
SCSI_LBA = (CBW.CB[2] << 24) | (CBW.CB[3] << 16) | (CBW.CB[4] << 8) | CBW.CB[5];
/* Calculate the Number of Blocks to transfer */
SCSI_BlkLen = (CBW.CB[7] << 8) | CBW.CB[8];
}
if (CBW.dSignature == BOT_CBW_SIGNATURE)
{
/* Valid CBW */
if ((CBW.bLUN > Max_Lun) || (CBW.bCBLength < 1) || (CBW.bCBLength > 16))
{
Bot_Abort(BOTH_DIR);
Set_Scsi_Sense_Data(CBW.bLUN, ILLEGAL_REQUEST, INVALID_FIELED_IN_COMMAND);
Set_CSW (CSW_CMD_FAILED, SEND_CSW_DISABLE);
}
else
{
switch (CBW.CB[0])
{
case SCSI_REQUEST_SENSE:
SCSI_RequestSense_Cmd (CBW.bLUN);
msc_debug_printf("SCSI_REQUEST_SENSE\r\n");
break;
case SCSI_INQUIRY:
SCSI_Inquiry_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_INQUIRY\r\n");
break;
case SCSI_START_STOP_UNIT:
SCSI_Start_Stop_Unit_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_START_STOP_UNIT\r\n");
break;
case SCSI_ALLOW_MEDIUM_REMOVAL:
SCSI_Start_Stop_Unit_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_MEDIA_REMOVAL\r\n");
break;
case SCSI_MODE_SENSE6:
SCSI_ModeSense6_Cmd (CBW.bLUN);
msc_debug_printf("SCSI_MODE_SENSE6\r\n");
break;
case SCSI_MODE_SENSE10:
SCSI_ModeSense10_Cmd (CBW.bLUN);
msc_debug_printf("SCSI_MODE_SENSE10\r\n");
break;
case SCSI_READ_FORMAT_CAPACITIES:
SCSI_ReadFormatCapacity_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_READ_FORMAT_CAPACITIES\r\n");
break;
case SCSI_READ_CAPACITY10:
SCSI_ReadCapacity10_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_READ_CAPACITY10\r\n");
break;
case SCSI_TEST_UNIT_READY:
SCSI_TestUnitReady_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_TEST_UNIT_READY\r\n");
break;
case SCSI_READ10:
SCSI_Read10_Cmd(CBW.bLUN, SCSI_LBA , SCSI_BlkLen);
msc_debug_printf("SCSI_READ10\r\n");
break;
case SCSI_WRITE10:
SCSI_Write10_Cmd(CBW.bLUN, SCSI_LBA , SCSI_BlkLen);
msc_debug_printf("SCSI_WRITE10\r\n");
break;
case SCSI_VERIFY10:
SCSI_Verify10_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_VERIFY10\r\n");
break;
case SCSI_FORMAT_UNIT:
SCSI_Format_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_FORMAT_UNIT\r\n");
break;
/*Unsupported command*/
case SCSI_MODE_SELECT10:
SCSI_Mode_Select10_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_MODE_SELECT10\r\n");
break;
case SCSI_MODE_SELECT6:
SCSI_Mode_Select6_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_MODE_SELECT6\r\n");
break;
case SCSI_SEND_DIAGNOSTIC:
SCSI_Send_Diagnostic_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_SEND_DIAGNOSTIC\r\n");
break;
case SCSI_READ6:
SCSI_Read6_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_READ6\r\n");
break;
case SCSI_READ12:
SCSI_Read12_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_READ12\r\n");
break;
case SCSI_READ16:
SCSI_Read16_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_READ16\r\n");
break;
case SCSI_READ_CAPACITY16:
SCSI_READ_CAPACITY16_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_READ_CAPACITY16\r\n");
break;
case SCSI_WRITE6:
SCSI_Write6_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_WRITE6\r\n");
break;
case SCSI_WRITE12:
SCSI_Write12_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_WRITE12\r\n");
break;
case SCSI_WRITE16:
SCSI_Write16_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_WRITE16\r\n");
break;
case SCSI_VERIFY12:
SCSI_Verify12_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_VERIFY12\r\n");
break;
case SCSI_VERIFY16:
SCSI_Verify16_Cmd(CBW.bLUN);
msc_debug_printf("SCSI_VERIFY16\r\n");
break;
default:
{
Bot_Abort(BOTH_DIR);
Set_Scsi_Sense_Data(CBW.bLUN, ILLEGAL_REQUEST, INVALID_COMMAND);
Set_CSW (CSW_CMD_FAILED, SEND_CSW_DISABLE);
}
}
}
}
else
{
/* Invalid CBW */
Bot_Abort(BOTH_DIR);
Set_Scsi_Sense_Data(CBW.bLUN, ILLEGAL_REQUEST, INVALID_COMMAND);
Set_CSW (CSW_CMD_FAILED, SEND_CSW_DISABLE);
}
}
/*******************************************************************************
* Function Name : Transfer_Data_Request
* Description : Send the request response to the PC HOST.
* Input : u8* Data_Address : point to the data to transfer.
* u16 Data_Length : the nember of Bytes to transfer.
* Output : None.
* Return : None.
*******************************************************************************/
void Transfer_Data_Request(u8* Data_Pointer, u16 Data_Len)
{
UserToPMABufferCopy(Data_Pointer, ENDP2_TXADDR, Data_Len);
SetEPTxCount(ENDP2, Data_Len);
SetEPTxStatus(ENDP2, EP_TX_VALID);
Bot_State = BOT_DATA_IN_LAST;
CSW.dDataResidue -= Data_Len;
CSW.bStatus = CSW_CMD_PASSED;
}
/*******************************************************************************
* Function Name : Set_CSW
* Description : Set the SCW with the needed fields.
* Input : u8 CSW_Status this filed can be CSW_CMD_PASSED,CSW_CMD_FAILED,
* or CSW_PHASE_ERROR.
* Output : None.
* Return : None.
*******************************************************************************/
void Set_CSW (u8 CSW_Status, u8 Send_Permission)
{
CSW.dSignature = BOT_CSW_SIGNATURE;
CSW.bStatus = CSW_Status;
UserToPMABufferCopy(((u8 *)& CSW), ENDP2_TXADDR, CSW_DATA_LENGTH);
SetEPTxCount(ENDP2, CSW_DATA_LENGTH);
Bot_State = BOT_ERROR;
if (Send_Permission)
{
Bot_State = BOT_CSW_Send;
SetEPTxStatus(ENDP2, EP_TX_VALID);
}
}
/*******************************************************************************
* Function Name : Bot_Abort
* Description : Stall the needed Endpoint according to the selected direction.
* Input : Endpoint direction IN, OUT or both directions
* Output : None.
* Return : None.
*******************************************************************************/
void Bot_Abort(u8 Direction)
{
switch (Direction)
{
case DIR_IN :
SetEPTxStatus(ENDP2, EP_TX_STALL);
break;
case DIR_OUT :
SetEPRxStatus(ENDP2, EP_RX_STALL);
break;
case BOTH_DIR :
SetEPTxStatus(ENDP2, EP_TX_STALL);
SetEPRxStatus(ENDP2, EP_RX_STALL);
break;
default:
break;
}
}
實質上就是實現usb的scsi儲存介面,具體請看工程程式碼,另外需要注意,因為USB讀取SD卡是在中斷中,所以我們實際上操作物理介質的時候需要將讀寫函式做成可重入的,否則會為儲存裝置帶來災難的,也就是每次讀取之前加一個標誌位,不讓其他資源來讀寫,類似於互斥訊號量吧
工程程式碼地址
1 |
|