什么是pstore
pstore最初是用于系統發生oops或panic時,自動保存內核log buffer中的日志。不過在當前內核版本中,其已經支持了更多的功能,如保存console日志、ftrace消息和用戶空間日志。同時,它還支持將這些消息保存在不同的存儲設備中,如內存、塊設備或mtd設備。 為了提高靈活性和可擴展性,pstore將以上功能分別抽象為前端和后端,其中像dmesg、console等為pstore提供數據的模塊稱為前端,而內存設備、塊設備等用于存儲數據的模塊稱為后端,pstore core則分別為它們提供相關的注冊接口。
通過模塊化的設計,實現了前端和后端的解耦,因此若某些模塊需要利用pstore保存信息,就可以方便地向pstore添加新的前端。而若需要將pstore數據保存到新的存儲設備上,也可以通過向其添加后端設備的方式完成。
除此之外,pstore還設計了一套pstore文件系統,用于查詢和操作上一次重啟時已經保存的pstore數據。當該文件系統被掛載時,保存在backend中的數據將被讀取到pstore fs中,并以文件的形式顯示。
pstore工作原理
pstore 源文件主要有以下幾個:fs/pstore/ram_core.c
fs/pstore/ ├──ftrace.c#ftrace前端的實現 ├──inode.c#pstore文件系統的注冊與操作 ├──internal.h ├──Kconfig ├──Makefile ├──platform.c#pstore前后端功能的核心 ├──pmsg.c#pmsg前端的實現 ├──ram.c#pstore/ram后端的實現,dram空間分配與管理 ├──ram_core.c#pstore/ram后端的實現,dram的讀寫操作
文件創建
pstore文件系統位置在:
#ls/sys/fs/pstore console-ramoops-0dmesg-ramoops-0
控制臺日志位于 pstore 目錄下的console-ramoops文件中,因為采用console機制,該文件中的日志信息也受printk level控制,并不一定是全的。
oops/panic日志位于 pstore 目錄下的dmesg-ramoops-x文件中,根據緩沖區大小可以有多個文件,x從0開始。函數調用序列日志位于 pstore 目錄下的ftrace-ramoops文件中。
相關代碼在inode.c pstore_mkfile里:
/* *Makearegularfileintherootdirectoryofourfilesystem. *Loaditupwith"size"bytesofdatafrom"buf". *Setthemtime&ctimetothedatethatthisrecordwasoriginallystored. */ intpstore_mkfile(enumpstore_type_idtype,char*psname,u64id,intcount, char*data,boolcompressed,size_tsize, structtimespectime,structpstore_info*psi) { ........................ rc=-ENOMEM; inode=pstore_get_inode(pstore_sb); .............................. switch(type){ casePSTORE_TYPE_DMESG: scnprintf(name,sizeof(name),"dmesg-%s-%lld%s", psname,id,compressed?".enc.z":""); break; casePSTORE_TYPE_CONSOLE: scnprintf(name,sizeof(name),"console-%s-%lld",psname,id); break; casePSTORE_TYPE_FTRACE: scnprintf(name,sizeof(name),"ftrace-%s-%lld",psname,id); break; casePSTORE_TYPE_MCE: scnprintf(name,sizeof(name),"mce-%s-%lld",psname,id); break; casePSTORE_TYPE_PPC_RTAS: scnprintf(name,sizeof(name),"rtas-%s-%lld",psname,id); break; casePSTORE_TYPE_PPC_OF: scnprintf(name,sizeof(name),"powerpc-ofw-%s-%lld", psname,id); break; casePSTORE_TYPE_PPC_COMMON: scnprintf(name,sizeof(name),"powerpc-common-%s-%lld", psname,id); break; casePSTORE_TYPE_PMSG: scnprintf(name,sizeof(name),"pmsg-%s-%lld",psname,id); break; casePSTORE_TYPE_PPC_OPAL: sprintf(name,"powerpc-opal-%s-%lld",psname,id); break; casePSTORE_TYPE_UNKNOWN: scnprintf(name,sizeof(name),"unknown-%s-%lld",psname,id); break; default: scnprintf(name,sizeof(name),"type%d-%s-%lld", type,psname,id); break; } .................... dentry=d_alloc_name(root,name); ....................... d_add(dentry,inode); ................ }
pstore_mkfile根據不同的type,使用snprintf函數生成文件名name。生成的文件名格式為
接著使用d_alloc_name函數為根目錄創建一個目錄項dentry,最后使用d_add函數將目錄項dentry與索引節點inode關聯起來,將其添加到文件系統中。
pstore_register
ramoops負責把message write到某個ram區域上,platform負責從ram讀取存到/sys/fs/pstore,ok,先來看機制代碼platform.c。
backend需要用pstore_register來注冊:
/* *platformspecificpersistentstoragedriverregisterswith *ushere.Ifpstoreisalreadymounted,calltheplatform *readfunctionrightawaytopopulatethefilesystem.Ifnot *thenthepstoremountcodewillcalluslatertofillout *thefilesystem. */ intpstore_register(structpstore_info*psi) { structmodule*owner=psi->owner; if(backend&&strcmp(backend,psi->name)) return-EPERM; spin_lock(&pstore_lock); if(psinfo){ spin_unlock(&pstore_lock); return-EBUSY; } if(!psi->write) psi->write=pstore_write_compat; if(!psi->write_buf_user) psi->write_buf_user=pstore_write_buf_user_compat; psinfo=psi; mutex_init(&psinfo->read_mutex); spin_unlock(&pstore_lock); ... /* *Updatethemoduleparameterbackend,soitisvisible *through/sys/module/pstore/parameters/backend */ backend=psi->name; module_put(owner);
backend判斷確保一次只能有一個并記錄了全局psinfo。
看下結構體pstore_info:
structpstore_info{ structmodule*owner; char*name; spinlock_tbuf_lock;/*serializeaccessto'buf'*/ char*buf; size_tbufsize; structmutexread_mutex;/*serializeopen/read/close*/ intflags; int(*open)(structpstore_info*psi); int(*close)(structpstore_info*psi); ssize_t(*read)(u64*id,enumpstore_type_id*type, int*count,structtimespec*time,char**buf, bool*compressed,ssize_t*ecc_notice_size, structpstore_info*psi); int(*write)(enumpstore_type_idtype, enumkmsg_dump_reasonreason,u64*id, unsignedintpart,intcount,boolcompressed, size_tsize,structpstore_info*psi); int(*write_buf)(enumpstore_type_idtype, enumkmsg_dump_reasonreason,u64*id, unsignedintpart,constchar*buf,boolcompressed, size_tsize,structpstore_info*psi); int(*write_buf_user)(enumpstore_type_idtype, enumkmsg_dump_reasonreason,u64*id, unsignedintpart,constchar__user*buf, boolcompressed,size_tsize,structpstore_info*psi); int(*erase)(enumpstore_type_idtype,u64id, intcount,structtimespectime, structpstore_info*psi); void*data; };
name就是backend的name了。
*write和*write_buf_user如果backend沒有給出會有個默認compat func,最終都走的*write_buf。
if(!psi->write) psi->write=pstore_write_compat; if(!psi->write_buf_user) psi->write_buf_user=pstore_write_buf_user_compat;
staticintpstore_write_compat(enumpstore_type_idtype, enumkmsg_dump_reasonreason, u64*id,unsignedintpart,intcount, boolcompressed,size_tsize, structpstore_info*psi) { returnpsi->write_buf(type,reason,id,part,psinfo->buf,compressed, size,psi); } staticintpstore_write_buf_user_compat(enumpstore_type_idtype, enumkmsg_dump_reasonreason, u64*id,unsignedintpart, constchar__user*buf, boolcompressed,size_tsize, structpstore_info*psi) { ... ret=psi->write_buf(type,reason,id,part,psinfo->buf, ... }
繼續pstore注冊:
if(pstore_is_mounted()) pstore_get_records(0);
如果pstore已經mounted,那就創建并填充文件by pstore_get_records:
/* *Readalltherecordsfromthepersistentstore.Create *filesinourfilesystem.Don'twarnabout-EEXISTerrors *whenwearere-scanningthebackingstorelookingtoaddnew *errorrecords. */ voidpstore_get_records(intquiet) { structpstore_info*psi=psinfo;//tj:globalpsinfo ... mutex_lock(&psi->read_mutex); if(psi->open&&psi->open(psi)) gotoout; while((size=psi->read(&id,&type,&count,&time,&buf,&compressed, &ecc_notice_size,psi))>0){ if(compressed&&(type==PSTORE_TYPE_DMESG)){ if(big_oops_buf) unzipped_len=pstore_decompress(buf, big_oops_buf,size, big_oops_buf_sz); if(unzipped_len>0){ if(ecc_notice_size) memcpy(big_oops_buf+unzipped_len, buf+size,ecc_notice_size); kfree(buf); buf=big_oops_buf; size=unzipped_len; compressed=false; }else{ pr_err("decompressionfailed;returned%d ", unzipped_len); compressed=true; } } rc=pstore_mkfile(type,psi->name,id,count,buf, compressed,size+ecc_notice_size, time,psi); if(unzipped_len0)?{ ????????????/*?Free?buffer?other?than?big?oops?*/ ????????????kfree(buf); ????????????buf?=?NULL; ????????}?else ????????????unzipped_len?=?-1; ????????if?(rc?&&?(rc?!=?-EEXIST?||?!quiet)) ????????????failed++; ????} ????if?(psi->close) psi->close(psi); out: mutex_unlock(&psi->read_mutex);
if needed,call pstore_decompress解壓然后創建pstore文件by vfs接口pstore_mkfile。
pstore注冊接下來是按類別分別注冊:
if(psi->flags&PSTORE_FLAGS_DMESG) pstore_register_kmsg(); if(psi->flags&PSTORE_FLAGS_CONSOLE) pstore_register_console(); if(psi->flags&PSTORE_FLAGS_FTRACE) pstore_register_ftrace(); if(psi->flags&PSTORE_FLAGS_PMSG) pstore_register_pmsg();
psi->flags仍是由backend決定,只看pstore_register_kmsg和pstore_register_console。
pstore panic log注冊
staticstructkmsg_dumperpstore_dumper={ .dump=pstore_dump, }; /* *Registerwithkmsg_dumptosavelastpartofconsolelogonpanic. */ staticvoidpstore_register_kmsg(void) { kmsg_dump_register(&pstore_dumper); }
pstore_dump最終會call backend的write,直接用全局psinfo。
/* *callbackfromkmsg_dump.(s2,l2)hasthemostrecently *writtenbytes,olderbytesarein(s1,l1).Saveasmuch *aswecanfromtheendofthebuffer. */ staticvoidpstore_dump(structkmsg_dumper*dumper, enumkmsg_dump_reasonreason) { ... ret=psinfo->write(PSTORE_TYPE_DMESG,reason,&id,part, oopscount,compressed,total_len,psinfo);
kmsg_dump_register是內核一種增加log dumper方法,called when kernel oopses or panic。
/** *kmsg_dump_register-registerakernellogdumper. *@dumper:pointertothekmsg_dumperstructure * *Addsakernellogdumpertothesystem.Thedumpcallbackinthe *structurewillbecalledwhenthekerneloopsesorpanicsandmustbe *set.Returnszeroonsuccessand%-EINVALor%-EBUSYotherwise. */ intkmsg_dump_register(structkmsg_dumper*dumper) { unsignedlongflags; interr=-EBUSY; /*Thedumpcallbackneedstobeset*/ if(!dumper->dump) return-EINVAL; spin_lock_irqsave(&dump_list_lock,flags); /*Don'tallowregisteringmultipletimes*/ if(!dumper->registered){ dumper->registered=1; list_add_tail_rcu(&dumper->list,&dump_list); err=0; } spin_unlock_irqrestore(&dump_list_lock,flags); returnerr; }
/** *kmsg_dump-dumpkernellogtokernelmessagedumpers. *@reason:thereason(oops,panicetc)fordumping * *Calleachoftheregistereddumper'sdump()callback,whichcan *retrievethekmsgrecordswithkmsg_dump_get_line()or *kmsg_dump_get_buffer(). */ voidkmsg_dump(enumkmsg_dump_reasonreason) { structkmsg_dumper*dumper; unsignedlongflags; if((reason>KMSG_DUMP_OOPS)&&!always_kmsg_dump) return; rcu_read_lock(); list_for_each_entry_rcu(dumper,&dump_list,list){ if(dumper->max_reason&&reason>dumper->max_reason) continue; /*initializeiteratorwithdataaboutthestoredrecords*/ dumper->active=true; raw_spin_lock_irqsave(&logbuf_lock,flags); dumper->cur_seq=clear_seq; dumper->cur_idx=clear_idx; dumper->next_seq=log_next_seq; dumper->next_idx=log_next_idx; raw_spin_unlock_irqrestore(&logbuf_lock,flags); /*invokedumperwhichwilliterateoverrecords*/ dumper->dump(dumper,reason); /*resetiterator*/ dumper->active=false; } rcu_read_unlock(); }
pstore console 注冊
staticstructconsolepstore_console={ .name="pstore", .write=pstore_console_write, .flags=CON_PRINTBUFFER|CON_ENABLED|CON_ANYTIME, .index=-1, }; staticvoidpstore_register_console(void) { register_console(&pstore_console); }
->write最終也會call backend write:
#ifdefCONFIG_PSTORE_CONSOLE staticvoidpstore_console_write(structconsole*con,constchar*s,unsignedc) { constchar*e=s+c; while(spsinfo->bufsize) c=psinfo->bufsize; if(oops_in_progress){ if(!spin_trylock_irqsave(&psinfo->buf_lock,flags)) break; }else{ spin_lock_irqsave(&psinfo->buf_lock,flags); } memcpy(psinfo->buf,s,c); psinfo->write(PSTORE_TYPE_CONSOLE,0,&id,0,0,0,c,psinfo);//tj:here spin_unlock_irqrestore(&psinfo->buf_lock,flags); s+=c; c=e-s; } }
ramoops
下面來看下RAM backend: ramoops,先看probe:
staticintramoops_probe(structplatform_device*pdev) { structdevice*dev=&pdev->dev; structramoops_platform_data*pdata=dev->platform_data; ... if(!pdata->mem_size||(!pdata->record_size&&!pdata->console_size&& !pdata->ftrace_size&&!pdata->pmsg_size)){ pr_err("Thememorysizeandtherecord/consolesizemustbe" "non-zero "); gotofail_out; } ... cxt->size=pdata->mem_size; cxt->phys_addr=pdata->mem_address; cxt->memtype=pdata->mem_type; cxt->record_size=pdata->record_size; cxt->console_size=pdata->console_size; cxt->ftrace_size=pdata->ftrace_size; cxt->pmsg_size=pdata->pmsg_size; cxt->dump_oops=pdata->dump_oops; cxt->ecc_info=pdata->ecc_info;
pdata應該來源ramoops_register_dummy:
staticvoidramoops_register_dummy(void) { ... pr_info("usingmoduleparameters "); dummy_data=kzalloc(sizeof(*dummy_data),GFP_KERNEL); if(!dummy_data){ pr_info("couldnotallocatepdata "); return; } dummy_data->mem_size=mem_size; dummy_data->mem_address=mem_address; dummy_data->mem_type=mem_type; dummy_data->record_size=record_size; dummy_data->console_size=ramoops_console_size; dummy_data->ftrace_size=ramoops_ftrace_size; dummy_data->pmsg_size=ramoops_pmsg_size; dummy_data->dump_oops=dump_oops; /* *Forbackwardscompatibilityramoops.ecc=1means16bytesECC *(using1byteforECCisn'tmuchofuseanyway). */ dummy_data->ecc_info.ecc_size=ramoops_ecc==1?16:ramoops_ecc; dummy=platform_device_register_data(NULL,"ramoops",-1, dummy_data,sizeof(structramoops_platform_data));
有幾個可配參數:
/* *Ramoopsplatformdata *@mem_sizememorysizeforramoops *@mem_addressphysicalmemoryaddresstocontainramoops */ structramoops_platform_data{ unsignedlongmem_size; phys_addr_tmem_address; unsignedintmem_type; unsignedlongrecord_size; unsignedlongconsole_size; unsignedlongftrace_size; unsignedlongpmsg_size; intdump_oops; structpersistent_ram_ecc_infoecc_info; };
mem_size:用于Ramoops的內存大小,表示分配給Ramoops的物理內存的大小。
mem_address:用于Ramoops的物理內存地址,指定用于存儲Ramoops的物理內存的起始地址。
mem_type:內存類型,用于進一步描述內存的屬性和特征。
record_size:每個記錄的大小
console_size:控制臺記錄的大小
ftrace_size:Ftrace記錄的大小
pmsg_size:pmsg消息記錄的大小
dump_oops:是否轉儲oops信息的標志,表示是否將oops信息轉儲到Ramoops中。
ecc_info:RAM的ECC(糾錯碼)信息,用于提供關于ECC配置和處理的詳細信息。
有個結構表示了ramoops的context:
structramoops_context{ structpersistent_ram_zone**przs; structpersistent_ram_zone*cprz; structpersistent_ram_zone*fprz; structpersistent_ram_zone*mprz; phys_addr_tphys_addr; unsignedlongsize; unsignedintmemtype; size_trecord_size; size_tconsole_size; size_tftrace_size; size_tpmsg_size; intdump_oops; structpersistent_ram_ecc_infoecc_info; unsignedintmax_dump_cnt; unsignedintdump_write_cnt; /*_read_cntneedclearonramoops_pstore_open*/ unsignedintdump_read_cnt; unsignedintconsole_read_cnt; unsignedintftrace_read_cnt; unsignedintpmsg_read_cnt; structpstore_infopstore; };
在ramoops_probe時也是把ramoops_platform_data的成員賦給了context對應的。要了解具體含義,繼續probe:
paddr=cxt->phys_addr; dump_mem_sz=cxt->size-cxt->console_size-cxt->ftrace_size -cxt->pmsg_size; err=ramoops_init_przs(dev,cxt,&paddr,dump_mem_sz); if(err) gotofail_out; err=ramoops_init_prz(dev,cxt,&cxt->cprz,&paddr, cxt->console_size,0); if(err) gotofail_init_cprz; err=ramoops_init_prz(dev,cxt,&cxt->fprz,&paddr,cxt->ftrace_size, LINUX_VERSION_CODE); if(err) gotofail_init_fprz; err=ramoops_init_prz(dev,cxt,&cxt->mprz,&paddr,cxt->pmsg_size,0); if(err) gotofail_init_mprz; cxt->pstore.data=cxt;
可見,是逐個init每個persistant ram zone,size一共有4段:
dump_mem_sz+cxt->console_size+cxt->ftrace_size+cxt->pmsg_size=cxt->size
mem_size就是總大小了,mem_address是ramoops的物理地址,record_size再看下oops/panic ram:
staticintramoops_init_przs(structdevice*dev,structramoops_context*cxt, phys_addr_t*paddr,size_tdump_mem_sz) { interr=-ENOMEM; inti; if(!cxt->record_size) return0; if(*paddr+dump_mem_sz-cxt->phys_addr>cxt->size){ dev_err(dev,"noroomfordumps "); return-ENOMEM; } cxt->max_dump_cnt=dump_mem_sz/cxt->record_size; if(!cxt->max_dump_cnt) return-ENOMEM;
ok dump_mem_size大小的區域分成max_dump_cnt個,每個記錄大小是record_size。
接著會call persistent_ram_new來分配內存給這個ram zone。
for(i=0;imax_dump_cnt;i++){ cxt->przs[i]=persistent_ram_new(*paddr,cxt->record_size,0, &cxt->ecc_info, cxt->memtype,0);
console/ftrace/pmsg ram zone同上分配。
最后處理flags并注冊pstore:
cxt->pstore.flags=PSTORE_FLAGS_DMESG;//tj:默認dumpoops/panic if(cxt->console_size) cxt->pstore.flags|=PSTORE_FLAGS_CONSOLE; if(cxt->ftrace_size) cxt->pstore.flags|=PSTORE_FLAGS_FTRACE; if(cxt->pmsg_size) cxt->pstore.flags|=PSTORE_FLAGS_PMSG; err=pstore_register(&cxt->pstore); if(err){ pr_err("registeringwithpstorefailed "); gotofail_buf; }
來看下ramoops pstore的定義的callback,他們通過全局psinfo而來:
staticstructramoops_contextoops_cxt={ .pstore={ .owner=THIS_MODULE, .name="ramoops", .open=ramoops_pstore_open, .read=ramoops_pstore_read,//psi->read .write_buf=ramoops_pstore_write_buf,//fornonpmsg .write_buf_user=ramoops_pstore_write_buf_user,//forpmsg .erase=ramoops_pstore_erase, }, };
pstore使用方法
ramoops
配置內核
CONFIG_PSTORE=y CONFIG_PSTORE_CONSOLE=y CONFIG_PSTORE_PMSG=y CONFIG_PSTORE_RAM=y CONFIG_PANIC_TIMEOUT=-1
由于log數據存放于DDR,不能掉電,只能依靠自動重啟機制來查看,故而要配置:CONFIG_PANIC_TIMEOUT,讓系統在 panic 后能自動重啟。
dts
ramoops_mem:ramoops_mem{ reg=<0x0?0x110000?0x0?0xf0000>; reg-names="ramoops_mem"; }; ramoops{ compatible="ramoops"; record-size=<0x0?0x20000>; console-size=<0x0?0x80000>; ftrace-size=<0x0?0x00000>; pmsg-size=<0x0?0x50000>; memory-region=<&ramoops_mem>; };
mtdoops
內核配置
CONFIG_PSTORE=y CONFIG_PSTORE_CONSOLE=y CONFIG_PSTORE_PMSG=y CONFIG_MTD_OOPS=y CONFIG_MAGIC_SYSRQ=y
分區配置
cmdline方式:
bootargs="console=ttyS1,115200loglevel=8rootwaitroot=/dev/mtdblock5rootfstype=squashfsmtdoops.mtddev=pstore"; blkparts="mtdparts=spi0.0:64k(spl)ro,256k(uboot)ro,64k(dtb)ro,128k(pstore),3m(kernel)ro,4m(rootfs)ro,-(data)";
part of方式:
bootargs="console=ttyS1,115200loglevel=8rootwaitroot=/dev/mtdblock5rootfstype=squashfsmtdoops.mtddev=pstore";
partition@60000{ label="pstore"; reg=<0x60000?0x20000>; };
blkoops
配置內核
CONFIG_PSTORE=y CONFIG_PSTORE_CONSOLE=y CONFIG_PSTORE_PMSG=y CONFIG_PSTORE_BLK=y CONFIG_MTD_PSTORE=y CONFIG_MAGIC_SYSRQ=y
配置分區
cmdline方式:
bootargs="console=ttyS1,115200loglevel=8rootwaitroot=/dev/mtdblock5rootfstype=squashfspstore_blk.blkdev=pstore"; blkparts="mtdparts=spi0.0:64k(spl)ro,256k(uboot)ro,64k(dtb)ro,128k(pstore),3m(kernel)ro,4m(rootfs)ro,-(data)";
part of方式:
bootargs="console=ttyS1,115200loglevel=8rootwaitroot=/dev/mtdblock5rootfstype=squashfspstore_blk.blkdev=pstore";
partition@60000{ label="pstore"; reg=<0x60000?0x20000>; };
pstore fs
掛載pstore文件系統
mount-tpstorepstore/sys/fs/pstore
掛載后,通過mount能看到類似這樣的信息:
#mount pstoreon/sys/fs/pstoretypepstore(rw,relatime)
如果需要驗證,可以這樣主動觸發內核崩潰:
#echoc>/proc/sysrq-trigger
不同配置方式日志名稱不同
ramoops
#mount-tpstorepstore/sys/fs/pstore/ #cd/sys/fs/pstore/ #ls console-ramoops-0dmesg-ramoops-0dmesg-ramoops-1
mtdoops
#cat/dev/mtd3>1.txt #cat1.txt
blkoops
cd/sys/fs/pstore/ ls dmesg-pstore_blk-0dmesg-pstore_blk-1
總結
pstore setup 流程:
ramoops_init ramoops_register_dummy ramoops_probe ramoops_register
查看 pstore 數據保存流程:
registerapstore_dumper //whenpanichappens,kmsg_dumpiscalled calldumper->dump pstore_dump
查看 pstore 數據讀取流程:
ramoops_probe persistent_ram_post_init pstore_register pstore_get_records ramoops_pstore_read pstore_decompress(onlyfordmesg) pstore_mkfile(savetofiles)
-
RAM
+關注
關注
8文章
1369瀏覽量
115029 -
DDR
+關注
關注
11文章
715瀏覽量
65536 -
ECC
+關注
關注
0文章
97瀏覽量
20644 -
vfs
+關注
關注
0文章
14瀏覽量
5283
原文標題:【調試】pstore原理和使用方法總結
文章出處:【微信號:嵌入式與Linux那些事,微信公眾號:嵌入式與Linux那些事】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
BTool、PacketSniffer、BLE_Device_Monitor、USBDongle使用方法總結
BTool、PacketSniffer、BLE_Device_Monitor、USBDongle使用方法總結
轉:Keil的使用方法 - 常用功能(一)
轉:Keil的使用方法 - 常用功能(二)
GPIO的常用庫函數使用方法總結
總結一下串口的幾種使用方法
介紹SPI的使用方法
示波器的使用方法(三):示波器的使用方法詳解
DWIN屏使用方法總結(上)
![DWIN屏<b class='flag-5'>使用方法</b><b class='flag-5'>總結</b>(上)](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
DWIN屏使用方法總結(下)
![DWIN屏<b class='flag-5'>使用方法</b><b class='flag-5'>總結</b>(下)](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
評論