1. 4kB EERAM上的CSEc密鑰和用戶數據
使用S32K148,將啟用CSE模塊進行安全引導。根據應用說明(“使用S32K148 FlexNVM存儲器”,AN12003,版本0,2017年7月),在第3.3節中,64 kB的FlexNVM用作閃存(EEPROM備份和密鑰存儲在一起),CSEc使用最多512字節的密鑰存儲,剩下的3.5 kB EERAM用于EEPROM和啟用CSEc。在這種用法中,用戶數據是否以某種方式通過覆蓋或刪除注冊密鑰而保留。簡而言之,記錄密鑰的512字節是隨機記錄在EERAM中,還是記錄在特定位置,如4KB FlexRAM的開始/結束位置?
-> 如果分配的CSEc空間為512B,則FlexRAM(EEPROM)中將無法訪問空間0x14000E00-0x14000FFF。對該區域的任何訪問都會生成總線故障異常。
2. 關于CSEc安全引導和順序引導模式的問題。
測量從復位引腳釋放到高電平的安全啟動時間,不同boot_SIZE和Clock時鐘對于安全啟動時間有所不同。當安全啟動開始啟動時(即CSEc正在計算CMAC),MCU內核保持在復位狀態,復位引腳也保持在低電平狀態,直到CMAC計算完成。
3. 實現 SHA256
在S32K1上面的CSEc 模塊支持 AES128 引擎,但不支持 SHA256。但是用戶可以參考如下代碼實現它。
https://github.com/ARMmbed/mbedtls/blob/v2.16.8/library/sha256.c
4. 當在 S32K146中使用 CSEc模塊的時候,程序訪問 CSE_PRAM 空間,在調試模式下它將進入異常中斷。
#define REG_WRITE32(address, value) ((*(volatile uint32*)(address))= (value))
#define CRYPTO_PRAM_HDR_ADDR32 (0x14001000u)
REG_WRITE32(CRYPTO_PRAM_HDR_ADDR32, u32CommandHeader);
上述代碼高亮部分表示寫入 PRAM命令,會導致程序進入異常中斷。
->通過檢查SIM_SDID[FEATURES]寄存器,判斷S32K芯片是否支持 CSEc。為了使能 CSEc 模塊,內存需要分區用于EEPROM,FTFC寄存器需要編程。
5.CSEC模塊PRAM格式
在應用筆記 AN5401中,有的是通過 PRAM 頁1讀取數據,而有的是通過 PRAM頁2輸出數據,這兩種方法有什么不同的嗎?
-> 參考手冊里面的 CSEc命令
https://www.nxp.com/docs/en/reference-manual/S32K-RM.pdf
這是CMD_GET_ID命令的描述,可以在其中看到使用了哪些頁及其目的是什么:
如下是 AN5401文檔中的參考代碼部分。
6. CSEc不能恢復到工廠模式
通過 CSEC_DRV_DbgChal恢復到工廠模式失敗,返回"STATUS_SEC_KEY_WRITE_PROTECTED"。這里Keys不能被配置為寫保護,否則將不能被擦除。
https://www.nxp.com/docs/en/application-note/AN12130.pdf
7.S32K144閃存分區問題
使用S32K144EVB-Q100和SDK3.0.0以及S32DS進行ARM調試。當使用OpenSDA工具調試閃存時,可以擦除閃存并完成分區閃存,但當使用J-Link調試閃存時無法再次擦除閃存和分區閃存,必須先使用類似J-Flash的工具擦除閃存。如何使用J-Link擦除閃存和分區閃存。是否有調試配置的任何設置?
->在使用SDK API對FlexNVM進行分區時,由于啟用了CSEc。
如果啟用CSEc,則無法通過命令“全部擦除”或其他FTFC命令擦除FlexNVM。如果要擦除FlexNVM,唯一的方法是使用CSEc命令將FlexNVM復位為出廠狀態。有關更多信息,請參閱AN5401。
https://www.nxp.com/webapp/Download?colCode=AN5401
https://www.nxp.com/webapp/Download?colCode=AN5401SW&docLang=en
#include "CSEc_functions.h"
#include "CSEc_macros.h"
#include "CSEc_keys.h"
uint32_t dbg_challenge_out[4] = {0,0,0,0};
int main(void)
{
uint16_t __attribute__((unused)) csec_error = 0; //1 means No Error
csec_error = INIT_RNG(); /* Initialize the Random Number Generator before generating challenge */
//To Erase all keys and reset the part to the factory state
csec_error = DBG_CHAL(dbg_challenge_out); /* Generate the Challenge */
csec_error = DBG_AUTH(dbg_challenge_out); /* Issue the Authorization */
while(1);
/* to avoid the warning message for GHS and IAR: statement is unreachable*/
#if defined (__ghs__)
#pragma ghs nowarning 111
#endif
#if defined (__ICCARM__)
#pragma diag_suppress=Pe111
#endif
return 0;
}
由于 CSEc 模塊所有密鑰都只能 CSEc 模塊管理,這樣才能確保密鑰安全的安全性。雖然密鑰實際存儲區域是在模擬 EEPROM 中,但由于這段區域是由 CSEc 管理并且對用戶是不可見的,所以也無法通過直接對 DFlash 編程的方式寫入密鑰。在量產時,可以使用一份專用于寫入密鑰的程序,通過先刷寫這份程序,等寫入密鑰后,再刷寫正式的應用程序即可。需要注意的時,兩份程序需要有一致的 D-Flash 分區策略。當然也可以通過診斷服務的方式加載密鑰。先脫機計算出密鑰的 M1~M5 值,然后通過診斷服務(如 2Eh服務)將 M1~M3 加載至 CSEc 模塊中,并通過 M4 和 M5 驗證是否加載成功。由于不能通過 M1~M3 值逆推算出密鑰值,所以這種方式也是安全的。由于使能 CSEc 模塊后,只能通過 CSEc 模塊的恢復命令(使用調試器也不行)才能擦除 D-Flash 數據并恢復 D-Flash 至出廠狀態,所以如果在量產后需要再次擦除 D-Flash,或者重新對 D-Flash 進行分區等,可以在用戶代碼中加入相關程序,通過觸發這段程序來使用 CSEc 模塊的命令擦除 D-Flash。CSEc 模塊對 P-Flash的重新編程沒有影響。
7. 為CSEc操作分配密鑰大小后,需要按照AN5401 4.5中列出的步驟將閃存重置為出廠狀態。此外,還提供了一些連接/擦除/編程到S32K器件的提示。
(https://www.nxp.com/docs/en/application-note/AN12130.pdf )
如果沒有按照正確的步驟在分配密鑰后擦除Flash,器件可能會被鎖定,并且無法解鎖。
如參考手冊所述,分區操作在整個產品周期中最好只執行一次。
可以通過DBG_CHAL 和 DBG_AUTH 銷毀分區,不用擦除Flash。
當未分配CSEc時,沒有辦法在不擦除所有鍵的情況下刪除分區?因為只需要擦除數據Flash 以及Flash IFR。但是,一旦數據Flash 已經為EEPROM分區,FTFC擦除Flash 塊命令就不能擦除數據Flash。
運行DBG_CHAL和DBG_ AUTH命令會擦除所有密鑰,包括BOOT_MAC和BOOT_ MAC_KEY。在沒有這兩個參數的情況下,不會執行安全引導過程,因此應用程序可以自由執行,并且MCU 不會處于復位狀態。
DBG_CHAL/DBG_ AUTH即使在嚴格啟動模式處于活動狀態時也可以工作,假設所有密鑰都沒有寫保護。這不會鎖定 MCU。
成功執行DBG_CHAL/DBG_ AUTH后,所有密鑰將被擦除,安全引導處于非活動狀態??梢允褂萌魏畏绞剑ù?、并行、嚴格順序)再次激活安全引導。
注意:對于嚴格順序模式,自動MAC計算是不可能的。在激活嚴格順序引導之前,必須計算并存儲BOOT_MAC。否則將鎖定器件。MAC可通過CSEc本身或PC離線計算。有關更多詳細信息和代碼示例,請參閱 AN5401。
8. 當執行FLASH_DRV_DEFlashPartition 函數的時候, FTFx_FSTAT 是FTFx_FSTAT_ACCERR_MASK。
While((FTFC->FSTAT&FTFC_FSTAT_CCIF_MASK)==0U)
FTFC_FSTAT – 異常時,Flash狀態寄存器是 160, Flash Access Error
Flag 標志是 '1',但是正常狀態是 128。Flash已經分區,有如下原因導致 ACCERR位被置位。
已通過分區命令啟用CSEc,則此時擦除所有塊命令將無效(它也應返回ACCERR)。是否加載了密鑰并不重要。一旦Flash 分區,并且密鑰大小不是 0,則需要使用CMD_DBG_CHAL和CMD_ DBG_ AUTH命令來銷毀分區。為此,需要知道MASTER_ECU_KEY。如果尚未加載MASTER_ECU_KEY,則需要加載,然后可以使用CMD_DBG_CHAL和CMD_ DBG_ AUTH命令,別無選擇。可以檢查SIM模塊中的閃存配置寄存器1(FCFG1),查看器件是否已分區。
如果Flash已經分區、CSEc已經使能,FLASH_DRV_EraseAllBlock不能工作。唯一的方案是在了解MASTER_ECU_KEY的情況下使用DBG_CHAL 和 DBG_AUTH 命令恢復。
可以參考如下例子中的 eraseKeys()函數。S32DS.3.4S32DSsoftwareS32SDK_S32K1XX_RTM_4.0.2examplesS32K144driver_examplessystemcsec_keyconfig
在HSRUN模式(112MHz下),CSEc (安全) 或者 EEPROM 擦寫將觸發錯誤標志。因為在這種情況下不允許同時使用。器件需要切回到 RUN模式(80 Mhz) 來執行 CSEc(安全) 或者 EEPROM 寫/擦除。
Flash 內存安全在 CSEc 和non-CSEc 器件上都支持,SIM_SDID[7]表示 CSEc 是否在器件上支持。CSEc 和non CSEc 用戶需要運行 PGMPART 命令來配置 KeySize。針對 non CSEc 用戶,Key Size 必須配置為 0。
整個 EEERAM的空間是減小,為了存儲用戶 Keys的需要。用戶密鑰空間實際上成為EEERAM中的不可尋址空間。針對具有 CSEc或者沒有 CSEc的器件,如果 Key size為0,則CSEc_PRAM訪問是不允許的。Keysize為零時,是普通EEPROM分區么,直接mass erase就恢復了。通過操作 FLASH_DRV_EraseAllBlockUnsecure函數恢復,或者硬件將Reset引腳短接,然后通過JLINK等外部工具擦除MCU內部的 Flash。
如果S32K146板默認存在分區,通過判斷(SIM->FCFG1[FEATURE])確定分區,如果要使用新的分區操作,需要擦除舊的分區,如果不擦,在調試CSEc init_rng或者erase all key的時候會進入defaultISR中斷,可以通過上述mass erase方式恢復(keysize為零的情況)。然后重新分區使能 CSEc模塊,操作初始化隨機數以及加解密,預置秘鑰操作。
程序分區命令準備FlexNVM塊用作數據閃存、模擬EEPROM備份或兩者的組合,并初始化FlexRAM。程序分區命令不能從Flash 啟動,因為在程序分區命令執行期間無法訪問Flash 資源。與程序分區命令的執行相關的更改在下次復位后生效。在啟動其他FTFC和CSEc命令之前,預計將為新器件運行程序分區命令。
注意:雖然FlexNVM可以用不同分區,但其目的是在給定應用程序的整個生命周期中使用一次分區選擇。FlexNVM分區代碼選擇影響器件的耐久性和數據保留特性。中斷程序分區操作(由于斷電、復位、電源超出指定的操作范圍或任何其他原因)會使分區代碼處于不確定狀態。用戶必須采取適當的應對措施,以防止程序分區操作中斷時數據丟失。
對于未啟用CSEc的部件,Flash KeySize的數量必須配置為2'b00。對于啟用CSEc的器件,Flash密鑰的數量是用戶可配置的,但該空間將假設存在MASTER_ECU_KEY、BOOT_MAC_KEY和BOOT_ MAC(如果啟用了任何密鑰),因此將占用可用20個密鑰槽空間中的3個密鑰槽。這導致在1到17個用戶keys的范圍內留下鍵槽。對于未啟用CSEc的部件,密鑰分配必須設置為零密鑰(2'b00),否則該命令將返回錯誤。
注:對于具有CSEc的器件,一旦分配了Flash密鑰(無論是否初始化),SHE規范中不帶認證將不能擦除Flash 密鑰的要求將適用。這意味著在擦除數據Flash(所有Flash 密鑰都備份在數據Flash中的模擬EEPROM中)之前,必須運行并通過身份驗證(DBG_CHAL和DBG_ AUTH)命令(刪除所有 Flash密鑰)。因此,擦除所有塊和擦除所有塊解鎖將不起作用,如果所選塊/扇區包括存儲密鑰,則擦除Flash 塊或扇區也不起作用。此外,如果任何 Flash密鑰受寫保護,則無法擦除/刪除它們,因此無法擦除數據 Flash,身份驗證過程也不會通過。
9. 在S32DS環境下,使用如下 RTM3.0.0的example flash_partitioning_s32k144的例子,在debug RAM下運行就可以擦除以前老的 EEPROM分區。(如果keysize為零,沒有使用 CSEc的情況下)
/* Including needed modules to compile this module/procedure */
#include "Cpu.h"
#include "clockMan1.h"
#include "Flash.h"
volatile int exit_code = 0;
/* User includes (#include below this line is not maintained by Processor Expert) */
#include
#include
/* Declare a FLASH config struct which initialized by FlashInit, and will be used by all flash operations */
flash_ssd_config_t flashSSDConfig;
/* Data source for program operation */
#define BUFFER_SIZE 0x100u /* Size of data source */
#define FLASH_TARGET1
uint8_t sourceBuffer[BUFFER_SIZE];
/* Function declarations */
void CCIF_Handler(void);
/* If target is flash, insert this macro to locate callback function into RAM */
void CCIF_Callback(void)
int main(void)
{
/* Write your local variable definition here */
status_t ret; /* Store the driver APIs return code */
uint32_t address;
uint32_t size;
uint32_t failAddr;
uint32_t i;
flash_callback_t pCallBack;
#if (FEATURE_FLS_HAS_PROGRAM_PHRASE_CMD == 1u)
#ifndef FLASH_TARGET
uint8_t unsecure_key[FTFx_PHRASE_SIZE] = {0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFEu, 0xFFu, 0xFFu, 0xFFu};
#endif
#else /* FEATURE_FLASH_HAS_PROGRAM_LONGWORD_CMD */
uint8_t unsecure_key[FTFx_LONGWORD_SIZE] = {0xFEu, 0xFFu, 0xFFu, 0xFFu};
#endif /* FEATURE_FLS_HAS_PROGRAM_PHRASE_CMD */
/*** Processor Expert internal initialization. DON'T REMOVE THIS CODE!!! ***/
#ifdef PEX_RTOS_INIT
PEX_RTOS_INIT(); /* Initialization of the selected RTOS. Macro is defined by the RTOS component. */
#endif
/*** End of Processor Expert internal initialization. ***/
CLOCK_SYS_Init(g_clockManConfigsArr, CLOCK_MANAGER_CONFIG_CNT,
g_clockManCallbacksArr, CLOCK_MANAGER_CALLBACK_CNT);
CLOCK_SYS_UpdateConfiguration(0U, CLOCK_MANAGER_POLICY_AGREEMENT);
/* Init source data */
for (i = 0u; i < BUFFER_SIZE; i++)
{
sourceBuffer[i] = i;
}
/* Disable cache to ensure that all flash operations will take effect instantly,
* this is device dependent */
#ifndef FLASH_TARGET
#ifdef S32K144_SERIES
MSCM->OCMDR[0u] |= MSCM_OCMDR_OCM1(0x3u);
MSCM->OCMDR[1u] |= MSCM_OCMDR_OCM1(0x3u);
#endif /* S32K144_SERIES */
#endif /* FLASH_TARGET */
/* Install interrupt for Flash Command Complete event */
INT_SYS_InstallHandler(FTFC_IRQn, CCIF_Handler, (isr_t*) 0);
INT_SYS_EnableIRQ(FTFC_IRQn);
/* Enable global interrupt */
INT_SYS_EnableIRQGlobal();
/* Always initialize the driver before calling other functions */
ret = FLASH_DRV_Init(&Flash_InitConfig0, &flashSSDConfig);
DEV_ASSERT(STATUS_SUCCESS == ret);
#if ((FEATURE_FLS_HAS_FLEX_NVM == 1u) & (FEATURE_FLS_HAS_FLEX_RAM == 1u))
/* Config FlexRAM as EEPROM if it is currently used as traditional RAM */
if (flashSSDConfig.EEESize == 0u)
{
#ifndef FLASH_TARGET
/* First, erase all Flash blocks if code is placed in RAM to ensure
* the IFR region is blank before partitioning FLexNVM and FlexRAM */
ret = FLASH_DRV_EraseAllBlock(&flashSSDConfig);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Verify the erase operation at margin level value of 1 */
ret = FLASH_DRV_VerifyAllBlock(&flashSSDConfig, 1u);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Reprogram secure byte in Flash configuration field */
#if (FEATURE_FLS_HAS_PROGRAM_PHRASE_CMD == 1u)
address = 0x408u;
size = FTFx_PHRASE_SIZE;
#else /* FEATURE_FLASH_HAS_PROGRAM_LONGWORD_CMD == 1u */
address = 0x40Cu;
size = FTFx_LONGWORD_SIZE;
#endif /* FEATURE_FLS_HAS_PROGRAM_PHRASE_CMD */
ret = FLASH_DRV_Program(&flashSSDConfig, address, size, unsecure_key);
DEV_ASSERT(STATUS_SUCCESS == ret);
#endif /* FLASH_TARGET */
/* Configure FlexRAM as EEPROM and FlexNVM as EEPROM backup region,
* DEFlashPartition will be failed if the IFR region isn't blank.
* Refer to the device document for valid EEPROM Data Size Code
* and FlexNVM Partition Code. For example on S32K144:
* - EEEDataSizeCode = 0x02u: EEPROM size = 4 Kbytes
* - DEPartitionCode = 0x08u: EEPROM backup size = 64 Kbytes */
ret = FLASH_DRV_DEFlashPartition(&flashSSDConfig, 0x02u, 0x08u, 0x0u, false, true);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Re-initialize the driver to update the new EEPROM configuration */
ret = FLASH_DRV_Init(&Flash_InitConfig0, &flashSSDConfig);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Make FlexRAM available for EEPROM */
ret = FLASH_DRV_SetFlexRamFunction(&flashSSDConfig, EEE_ENABLE, 0x00u, NULL);
DEV_ASSERT(STATUS_SUCCESS == ret);
}
else /* FLexRAM is already configured as EEPROM */
{
ret = FLASH_DRV_EraseAllBlockUnsecure(&flashSSDConfig);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Re-initialize the driver to update the new EEPROM configuration */
ret = FLASH_DRV_Init(&Flash_InitConfig0, &flashSSDConfig);
DEV_ASSERT(STATUS_SUCCESS == ret);
}
#endif /* (FEATURE_FLS_HAS_FLEX_NVM == 1u) & (FEATURE_FLS_HAS_FLEX_RAM == 1u) */
/* Set callback function before a long time consuming flash operation
* (ex: erasing) to let the application code do other tasks while flash
* in operation. In this case we use it to enable interrupt for
* Flash Command Complete event */
pCallBack = (flash_callback_t)CCIF_Callback;
flashSSDConfig.CallBack = pCallBack;
/* Erase the last PFlash sector */
address = FEATURE_FLS_PF_BLOCK_SIZE - FEATURE_FLS_PF_BLOCK_SECTOR_SIZE;
size = FEATURE_FLS_PF_BLOCK_SECTOR_SIZE;
ret = FLASH_DRV_EraseSector(&flashSSDConfig, address, size);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Disable Callback */
flashSSDConfig.CallBack = NULL_CALLBACK;
/* Verify the erase operation at margin level value of 1, user read */
ret = FLASH_DRV_VerifySection(&flashSSDConfig, address, size / FTFx_DPHRASE_SIZE, 1u);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Write some data to the erased PFlash sector */
size = BUFFER_SIZE;
ret = FLASH_DRV_Program(&flashSSDConfig, address, size, sourceBuffer);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Verify the program operation at margin level value of 1, user margin */
ret = FLASH_DRV_ProgramCheck(&flashSSDConfig, address, size, sourceBuffer, &failAddr, 1u);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Try to write data to EEPROM if FlexRAM is configured as EEPROM */
if (flashSSDConfig.EEESize != 0u)
{
address = flashSSDConfig.EERAMBase;
size = sizeof(uint32_t);
ret = FLASH_DRV_EEEWrite(&flashSSDConfig, address, size, sourceBuffer);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Verify the written data */
if (*((uint32_t *)sourceBuffer) != *((uint32_t *)address))
{
/* Failed to write data to EEPROM */
exit_code = 1u;
return exit_code;
}
/* Try to update one byte in an EEPROM address which isn't aligned */
address = flashSSDConfig.EERAMBase + 1u;
size = sizeof(uint8_t);
sourceBuffer[0u] = 0xFFu;
ret = FLASH_DRV_EEEWrite(&flashSSDConfig, address, size, sourceBuffer);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Then verify */
if (sourceBuffer[0u] != *((uint8_t *)address))
{
/* Failed to update data to EEPROM */
exit_code = 1u;
return exit_code;
}
}
else
{
#if (FEATURE_FLS_HAS_FLEX_NVM == 1u)
/* Erase a sector in DFlash */
address = flashSSDConfig.DFlashBase;
size = FEATURE_FLS_DF_BLOCK_SECTOR_SIZE;
ret = FLASH_DRV_EraseSector(&flashSSDConfig, address, size);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Verify the erase operation at margin level value of 1, user read */
ret = FLASH_DRV_VerifySection(&flashSSDConfig, address, size / FTFx_PHRASE_SIZE, 1u);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Write some data to the erased DFlash sector */
address = flashSSDConfig.DFlashBase;
size = BUFFER_SIZE;
ret = FLASH_DRV_Program(&flashSSDConfig, address, size, sourceBuffer);
DEV_ASSERT(STATUS_SUCCESS == ret);
/* Verify the program operation at margin level value of 1, user margin */
ret = FLASH_DRV_ProgramCheck(&flashSSDConfig, address, size, sourceBuffer, &failAddr, 1u);
DEV_ASSERT(STATUS_SUCCESS == ret);
#endif /* FEATURE_FLS_HAS_FLEX_NVM */
}
#ifdef PEX_RTOS_START
PEX_RTOS_START(); /* Startup of the selected RTOS. Macro is defined by the RTOS component. */
#endif
for(;;) {
if(exit_code != 0) {
break;
}
}
return exit_code;
/*** Processor Expert end of main routine. DON'T WRITE CODE BELOW!!! ***/
} /*** End of main routine. DO NOT MODIFY THIS TEXT!!! ***/
void CCIF_Handler(void)
{
/* Disable Flash Command Complete interrupt */
FTFx_FCNFG &= (~FTFx_FCNFG_CCIE_MASK);
return;
}
void CCIF_Callback(void)
{
/* Enable interrupt for Flash Command Complete */
if ((FTFx_FCNFG & FTFx_FCNFG_CCIE_MASK) == 0u)
{
FTFx_FCNFG |= FTFx_FCNFG_CCIE_MASK;
}
}
運行調試模式,停止,然后再次運行會進入FLASH_DRV_EraseAllBlockUnsecure,關于 Flash組件配置如下。
-
mcu
+關注
關注
146文章
16991瀏覽量
350308 -
寄存器
+關注
關注
31文章
5317瀏覽量
120006 -
SHA256
+關注
關注
0文章
5瀏覽量
10022 -
EERAM
+關注
關注
0文章
9瀏覽量
4552
原文標題:S32K CSEc
文章出處:【微信號:嵌入式 MCU,微信公眾號:嵌入式 MCU】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論