一、需求分析
二、硬件電路設計
本次案例需求與前面軟件模擬案例一致,這里不再贅述,不清楚可參見下面文章:軟件模擬I2C案例(寄存器實現)-CSDN博客
? ? ? ? 值得注意的是,前面是軟件模擬I2C,所以并沒有復用I2C模塊功能,只是使用了GPIO通用功能,而這里我們將直接硬件實現I2C,因此會使用到I2C復用功能,其中用到的也是前面案例所用引腳PB10和PB11,復用I2C2模塊功能。
三、軟件設計
? ? ? ?由于本次是使用寄存器方式實現硬件I2C,所以不免使用一些I2C外設相關的寄存器,對此的必要介紹可參見前面的文章硬件實現I2C常用寄存器簡單介紹-CSDN博客
? ? ? ?本次案例在軟件模擬I2C案例基礎上進行修改,因此這里不再贅述工程創建于配置過程,直接進入VSCode開始編寫代碼。
3.1 i2c.h
? ? ? ?通過硬件實現I2C,則不需要我們自己模擬時序,故基本不用自定義宏定義,而是我們配置好寄存器后,底層硬件自動執行協議相關功能。
? ? ? ?當然,前面介紹I2C相關寄存器時談到,一些信號在寄存器中做的只是一種設置,考慮到設置時可能相關主設備并沒有占用總線或者總線正被占用等情況,設置好的信號可能會相當于延時發出,因此這個過程我們需要進行循環等待判斷是否真的已經發出,同時為了避免一直等待,我們還可以設置一個超時時間timeout來避免一直等待。
因此,這里我們可以宏定義一個OK和FAIL表示信號成功發出的返回值0和未能成功發出(即超時)1。
// 宏定義
#define OK 0
#define FAIL 1
然后是一些必要的函數聲明:
主要根據I2C協議相關規范得出的相關函數聲明以及分析。
1、I2C的初始化 void I2C_Init(void)
? ? ? 這個函數主要用于GPIO的相關配置和I2C外設的基本配置。與前面軟件模擬的差別在于本次需要配置好I2C外設相關寄存器,選擇好合適的模式、時鐘頻率以及傳輸速率等,并開啟I2C使能。
2、設置起始信號 uint8_t I2C_Start(void)
? ? ? ?前面已經說到,I2C的寄存器只能設置START信號,并不會直接發出,因為需要考慮此時從機是否正在占用總線,如果總線不空閑則底層硬件無法讓主機發出起始信號。所以該函數主要用于設置起始信號并等待信號真正發出,同時可借助一個超時變量來限制等待一直進行而阻塞CPU,最后直接返回一個值表示信號發送的情況OK / fail。
3、設置停止信號 void I2C_Stop(void)
? ? ? ?設置停止信號以后主設備32就不用再管了,因為主設備在數據總線上負責一直傳輸數據就行了,當需要停止時把停止信號設置好就可以不用管了,該停止就直接停止然后釋放數據總線即可。
? ? ? ?后面需要注意的主要是進行數據傳遞時停止信號的設置時間,畢竟考慮到設置時可能正在傳輸數據或者發送地址,所以他并不是設置好就馬上產生,而是等傳遞完一個字節后才會被發出。這里只是提一下,在讀寫數據時會詳細介紹。
4、設置使能應答信號 void I2C_Ack(void)
? ? ? ?前面介紹寄存器也說過,響應的設置不代表立馬就會產生,而是當一個字節數據或者設備地址發送完成后才會被發出。由于應答咱主設備自己設置好了,該發的時候發出去就完事了,這只是數據傳遞過程中進行的事情,所以不用像起始信號那樣考慮那么多。
? ? ? 也就是說,應答的設置主要是在進行數據傳遞時需要注意什么時候設置好,關于這個在后面實現數據收發時詳細說。
5、設置使能非應答信號?void I2C_Nack(void)
與應答信號設置基本類似,這里不再贅述。
6、寫入一個設備地址 uint8_t?I2C_SendAddr(uint8_t addr)?
7、寫入一個字節數據 uint8_t?I2C_SendByte(uint8_t byte)
? ? ? ?根據前面介紹的I2C的控制寄存器描述,我們發現關于設備地址的寫入和數據的寫入完成被分別設置了標志位,也就是Addr和BTF標志位,分別用于標志設備地址寫入完成和字節寫入完成。而且寄存器對這倆標志位的描述是在這倆傳輸完成后收到應答時被置位,這就意味著我們可以在寫入操作函數中把設備地址和數據寫入成功后直接等待應答,然后收到應答的標志直接用Addr和BTF進行表示即可。那么既然要判斷這個應答的話,所以勢必也要有一個返回值來區別收到應答OK或者未收到應答FAIL。
所以我們編寫的函數將傳輸數據和設備地址分開實現最為合適。
8、讀取一個字節數據 uint8_t I2C_ReadByte(void)
? ? ? ?主機讀取一個字節數據主要就是將從機發出在總線上的數據獲取到然后返回就行了,所以該函數聲明沒有什么變化。
頭文件參考代碼如下
#ifndef __I2C_H
#define __I2C_H#include "stm32f10x.h"// 宏定義
#define OK 0
#define FAIL 1// 初始化
void I2C_Init(void);// 主設備設置起始信號
uint8_t I2C_Start(void);// 主設備設置停止信號
void I2C_Stop(void);// 主設備設置使能應答信號
void I2C_Ack(void);// 主設備設置使能非應答信號
void I2C_Nack(void);// 主機向從機寫入一個設備地址(發送),并等待應答
uint8_t I2C_SendAddr(uint8_t addr);// 主機向從機寫入一個字節的數據(發送),并等待應答
uint8_t I2C_SendByte(uint8_t byte);// 主機向從機讀取一個字節的數據(接收)
uint8_t I2C_ReadByte(void);#endif
3.2 i2c.c
寫好I2C頭文件后,接下來進入其源文件實現那些函數。
1、I2C_Init()函數
? ? ? ?介紹I2C初始化函數聲明時已經分析,這里主要配置GPIO以及I2C的基本配置。由于GPIO在本次案例中會復用I2C2模塊,所以相比前面還需要多開一個I2C2外設時鐘,我們知道I2C兩個外設均連在APB1低速外設總線上,所以配置時需要注意。
? ? ? ?然后I2C的配置,根據前面寄存器介紹,我們知道I2C外設兼容了兩種模式,可通過CR1中的SMBUS位進行置位選擇,顯然我們使用I2C模式,所以置0即可;選擇I2C模式后,要確定是標準模式還是快速模式,這影響了后面時鐘頻率和周期的計算。一般選擇標準模式,對應CR1中的FS位置0即可;緊接著就可以設置輸入的時鐘頻率了,我們直接給36MHz即可,對應CR2中的FREQ位,由于對應低位,所以直接給CR2寄存器36即可設置好FREQ了;然后要開始對輸入的時鐘頻率進行分頻操作使得數據傳輸速率匹配標準模式下的100kb/s,即配置CCR寄存器中的CCR,由于輸入時鐘頻率為36,然后標準模式下高電平時鐘周期時間大概占5us,所以CCR設置為5/(1/36) = 180即可,因為CCR位對應CCR寄存器低位,所以直接給寄存器180即可配好CCR了;然后還需要設置上升沿最大時間TRISE,根據前面寄存器介紹可知,設置36+1=37即可;最后再開啟I2C外設使能就配置好I2C2了。
參考代碼如下
// 初始化
void I2C_Init(void)
{// 1. 配置時鐘 GPIO與I2C2外設時鐘RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;// 2. 設置GPIO工作模式 復用開漏輸出 cnf-11 mode-11GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);// 3. 配置I2C// 3.1 選擇I2C標準模式 I2C2->CR1 &= ~I2C_CR1_SMBUS;I2C2->CCR &= ~I2C_CCR_FS;// 3.2 設置輸入的時鐘頻率I2C2->CR2 = 36;// 3.3 配置CCR,匹配傳輸速率 5/(1/36)I2C2->CCR = 180;// 3.4 設置上升沿最大時間 +1I2C2->TRISE = 37;// 3.5 開啟I2C模塊使能I2C2->CR1 |= I2C_CR1_PE;
}
2、uint8_t I2C_Start(void)函數
? ? ? ?設置起始信號,首先在寄存器CR1中配置START位產生一個起始信號,考慮到可能總線被其他設備占用沒有空閑,所以主機不會馬上發出起始信號,所以我們需要循環等待,什么時候結束等待呢?SR1寄存器中SB位描述起始信號發出后會被置1,未發出時置0,所以可以用SB位為0做循環條件,當置1時就結束循環表示起始信號被發出。為了避免CPU持續等待,我們引入一個超時時間16位值timeout限制循環作用時間,即當起始信號被發出或者超時的時候結束等待,然后返回信號發出結果(發出則返回OK的正常返回值0,未發出即超時則返回FAIL的1)。
參考代碼如下
// 主設備設置起始信號
uint8_t I2C_Start(void)
{// 1. 產生一個起始信號I2C2->CR1 |= I2C_CR1_START;// 引入一個超時時間uint16_t timeout = 0xffff;// 2. 等待起始信號發出while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout){timeout --;}return timeout ? OK : FAIL;
}
2、void?I2C_Stop(void)函數
? ? ? ? 該函數很簡單,直接產生一個停止信號就行即配置好CR1中的STOP位即可。由于主設備只負責向SDA線上傳輸接口,所以需要停止時,設置好停止信號即可,至于保證正確的時間被發出這件事,主設備并不用管,只需要傳輸數據時根據規定在合適的時間點設置即可。
參考代碼如下
// 主設備設置接收完成之后停止信號
void I2C_Stop(void)
{// 產生一個停止信號I2C2->CR1 |= I2C_CR1_STOP;
}
3、void I2C_Ack(void)函數
4、void I2C_Nack(void)函數
? ? ? ?應答與非應答信號的設置也很簡單,只需要設置應答與非應答即可,當一個字節數據傳輸完畢或者設備地址傳完后主機會自動發出應答或非應答。
參考代碼如下
// 主設備設置使能應答信號
void I2C_Ack(void)
{I2C2->CR1 |= I2C_CR1_ACK;
}// 主設備設置使能非應答信號
void I2C_Nack(void)
{I2C2->CR1 &= ~I2C_CR1_ACK;
}
5、uint8_t I2C_SendAddr(uint8_t addr)函數
? ? ? ?接下來就要開始進行傳輸了,首先剛發出起始信號時,還沒有任何數據寫入DR,所以這時候不需要判斷DR是否為空,顯然一定是空的。起始信號發出后我們首先會進行設備地址的寫入然后供從機設備去進行比較對應地址。所以該函數中首先就是將設備地址給到DR寄存器,然后等待設備地址寫入完成,收到應答后該操作才算結束,如何判斷設備地址寫入完成然后收到了應答呢?前面說過I2C相關的狀態寄存器SR1中有一位ADDR在設備地址寫入完成收到應答后就會被自動置位,然后同樣再結合超時時間控制等待時間有限,即可利用while循環來等待應答。最后返回一個值表示是否收到應答(0-收到應答,1-未收到應答即非應答響應)。
? ? ? ?需要注意的是,每次ADDR被置位后我們需要保證之后ADDR會自動清除,不然可能導致下一次設備地址的傳輸結果出現錯誤。比如主機讀取從機數據時會進行假寫真讀,該過程會進行兩次設備地址的寫入,如果第一次假寫時發送設備地址完成收到應答后ADDR置位了然后沒有清除,那么下一次傳輸的設備地址后,等待應答則會因為ADDR沒有清除而直接跳過等待認為收到應答,這樣的話就不能保證真的發送完成并收到反饋了。所以我們需要注意一下寄存器中對ADDR位的描述
? ? ? ?由上圖可知,ADDR不會自動清除,需要讀取SR1然后再對SR2進行讀操作,才能清除該位。由于等待過程會讀取SR1,所以我們最終只要在確實收到應答(timeout非0即沒有等待超時)的情況下再訪問一下SR2,即可清除ADDR。所以在返回是否收到應答的結果之前要加上ADDR清除操作。
參考代碼如下
// 主機向從機寫入一個設備地址(發送),并等待應答
uint8_t I2C_SendAddr(uint8_t addr)
{// 寫入設備地址I2C2->DR = addr;// 等待應答uint16_t timeout = 0xffff;while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout){timeout --;}// 應答發出后,訪問SR2,清除ADDRif (timeout > 0){I2C2->SR2;}return timeout ? OK : FAIL;
}
5、uint8_t I2C_SendByte(uint8_t byte)函數
? ? ? ?發了設備地址,后面就是發內部地址、數據那些了,當然我們將除了設備地址外的都看做數據的發送。那么既然前面進行了設備地址的發送,那么這里我們就要先等待數據寄存器為空唄,為保證與前面等待邏輯一致,也加個超時時間。然后數據寄存器為空了我們就可以往DR里面寫數據了。接著就是等待應答,同樣的等待,當然這時候等待的條件也是可讀取SR1中的一位BTF來作標志,意思是當字節數據發送完成后收到應答時被置位,恰好作為等待應答的循環條件。然后被置位后,同樣我們要想一下這一位怎么樣被清除,所以看看寄存器中對于該位的描述
? ? ? ? 由上圖可知,BTF位,在讀取SR1后,對DR進行讀寫操作或者傳輸中發送起始或者停止信號時可以被清除。首先,讀取SR1已經在等待過程的條件中進行,然后由于我們BTF是在傳輸數據過程中被置位的,而之后的傳輸無非兩種情況:一是繼續傳輸數據,則在下一次用到BTF之前就會進行數據的讀寫操作,此時BTF會被自動清除;另一種情況是結束主從通信,這時候主設備會在傳輸中發出停止信號,同樣BTF也會被自動清除。因此這里我們不需要手動加一道程序來清除BTF位。即等待應答之后直接返回一下應答的結果即可。
參考代碼如下
// 主機向從機寫入一個字節的數據(發送),并等待應答
uint8_t I2C_SendByte(uint8_t byte)
{uint16_t timeout = 0xffff;// 1. 等待數據為空 while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout){timeout --;}// 2. 寫入數據I2C2->DR = byte;// 3. 等待應答 timeout = 0xffff;while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout){timeout --;}return timeout ? OK : FAIL;
}
6、uint8_t I2C_ReadByte(void)函數
? ? ? ? 最后,就是讀取數據。首先,我們要等待數據寄存器變滿,同理加上超時時間,循環等待結束后,如果沒有超時則返回讀取到的數據寄存器中的數據,否則就返回FAIL就OK了。
參考代碼如下
// 主機向從機讀取一個字節的數據(接收)
uint8_t I2C_ReadByte(void)
{uint16_t timeout = 0xffff;// 1. 等待數據為滿while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout){timeout --;}return timeout ? I2C2->DR : FAIL;
}
這樣,關于I2C協議的部分就實現完畢了!
接下來,就是對M24C02的讀寫操作進行一個代碼編寫。
? ? ? 由于我們本次案例和前面軟件模擬I2C案例的區別主要在于I2C協議使用的不同,所以主要是I2C部分的函數變化較大。而在EEPROM部分,其使用的宏定義和函數不需要做出改變,只是其中的實現因為I2C部分的變化需要稍微修改。
3.3 m24c02.h
直接展示代碼如下
#ifndef __M24C02_H
#define __M24C02_H#include "i2c.h"// 宏定義
#define W_ADDR (0xA0)
#define R_ADDR (0xA1)// 初始化
void M24C02_Init(void);// 寫入一個字節的數據
void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte);// 讀取一個字節的數據
uint8_t M24C02_Readbyte(uint8_t innerAddr);// 連續寫入多個字節的數據(頁寫)
void M24C02_Writebytes(uint8_t innerAddr, uint8_t * bytes, uint8_t size);// 連續讀取多個字節的數據
void M24C02_Readbytes(uint8_t innerAddr, uint8_t * buffer, uint8_t size);#endif
3.4 m24c02.c
接下來,我們就來對頭文件中的函數進行一下實現。
1、void M24C02_Init(void)函數
? ? ? ? 要使用M24C02與32進行通信,主要是需要用I2C通訊,沒有其他硬件要求了,所以其初始化實際上就是初始化一下I2C部分。
參考代碼如下
// 初始化
void M24C02_Init(void)
{I2C_Init();
}
2、void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte)函數
? ? ? ?由于本次該函數的編寫只是I2C通訊由軟件模擬變成硬件模塊而產生了一些區別,但本質上還是調用I2C的函數接口,因此實際上我們可以借鑒之前案例中M24C02部分的函數邏輯,然后按照I2C部分的修改對實現邏輯進行修改即可。
? ? ? ? 我們思考,按照前面對I2C的函數的實現過程,我們可以發現最大的差別在于我們寫入數據被分成了發送設備地址和發送數據兩部分,同時相應收到應答的過程被合并在了寫入函數中。也就是說,向M24C02寫入單字節數據時,過程中的等待應答我們可以不用單獨調用了。
? ? ? ? 也就是說,對于寫入單字節數據的函數,一是不用單獨調用等待應答的函數,二是傳輸設備地址函數改成發送地址的函數即可。(如果大家看不明白,可去結合M24C02讀寫操作時序圖進行理解或者去看看前面軟件模擬I2C案例對M24C02部分代碼實現的介紹進行理解后再過來看)
參考代碼如下
// 主機寫入一個字節的數據
void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte)
{// 1. 主機設置起始信號I2C_Start();// 2. 主機傳輸設備地址,從機對應,并等待應答I2C_SendAddr(W_ADDR);// 3. 主機傳輸內部地址I2C_SendByte(innerAddr);// 4. 主機寫入具體數據I2C_SendByte(byte);// 5. 主機設置停止信號,結束寫入數據I2C_Stop();// 6. 延時等待字節寫入周期結束Delay_ms(5);
}
3、void M24C02_Writebytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)函數
? ? ? ?單字節的寫入做好了,那么連續寫入多個字節也是一樣的實現,簡單對應修改即可,這里不再贅述。
參考代碼如下
// 連續寫入多個字節的數據(頁寫)
void M24C02_Writebytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
{// 1. 主機發出起始信號I2C_Start();// 2. 主機傳輸設備地址,從機對應I2C_SendAddr(W_ADDR);// 3. 主機傳輸內部地址I2C_SendByte(innerAddr);for (uint8_t i = 0; i < size; i++){// 4. 主機寫入具體數據I2C_SendByte(bytes[i]);}// 5. 主機發出停止信號,結束寫入數據I2C_Stop();// 6. 延時等待字節寫入周期結束Delay_ms(5);
}
4、uint8_t M24C02_Readbyte(uint8_t innerAddr)函數
? ? ? ?那么同理,傳輸設備地址時專門使用發送地址的函數、不再單獨調用等待應答的函數。同時還需要注意的是,前面咱說過通過寄存器設置停止信號和應答信號后,不是馬上就會發出這倆信號,其寄存器對應的描述是
? ? ? ? 如上圖,描述的是應答將在收到一個字節之后反饋應答、當前字節傳輸后或者當前起始條件發出后才發出停止信號。這就意味著,從機發送數據到總線上后發出的應答以及之后主機發出的停止信號應該在當前發送數據前設置好才對,換句話說,如果從機發送數據了之后才設置ACK以及STOP的話,會導致這倆信號將在這之后傳輸的字節接收后才產生,這樣的話就對應的就是下一次傳輸的字節數據的應答與停止了。因此對于向M24C02讀取單字節數據過程來說,我們應該在接收讀取的數據之前先設置好應答與停止信號才更加合理。
? ? ? ?而且在參考手冊中對主機接收的過程實際也有相應的描述如下圖(具體可自行查看STM32參考手冊)
參考代碼如下
uint8_t M24C02_Readbyte(uint8_t innerAddr)
{// 1. 主機發出起始信號 I2C_Start();// 2. 主機傳輸設備地址(假寫),從機對應I2C_SendAddr(W_ADDR);// 3. 主機傳輸內部地址I2C_SendByte(innerAddr);// 4. 主機再次發出起始信號 I2C_Start();// 5. 主機傳輸設備地址(真讀),m24c02對應I2C_SendAddr(R_ADDR);// 6. 主機設置非應答I2C_Nack();// 7. 主機設置停止信號I2C_Stop();// 8. 獲取m24c02讀取的數據uint8_t data = I2C_ReadByte();return data;
}
5、void M24C02_Readbytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)函數
? ? ? ? 那么同理對應讀取單字節數據函數的實現方式,基于前面案例的實現,修改三個地方即可:一是傳輸設備地址調用的函數改成專門的傳輸地址函數、二是不用再單獨調用等待應答或者非應答的函數,其已經包含在寫入操作的函數中、三是注意主機接受數據時設置ACK與STOP的時機。這里就不再贅述。
所以參考代碼如下
// 連續讀取多個字節的數據
void M24C02_Readbytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)
{// 1. 主機發出起始信號I2C_Start();// 2. 主機傳輸設備地址(假寫),從機對應I2C_SendAddr(W_ADDR);// 3. 主機傳輸內部地址I2C_SendByte(innerAddr);// 4. 主機再次發出起始信號I2C_Start();// 5. 主機傳輸設備地址(真讀),m24c02對應I2C_SendAddr(R_ADDR);for (uint8_t i = 0; i < size; i++){// 6. 主機發出響應信號if (i < size - 1){I2C_Ack();}else{// 7. 主機發出非應答,m24c02釋放數據總線I2C_Nack();// 8. 主機發出停止信號,結束數據讀取I2C_Stop();}// 9. 獲取m24c02讀取的數據buffer[i] = I2C_ReadByte();}
}
? ? ? ?OK,到這里,關于M24C02部分的代碼實現也就寫完了,當然,這里M24C02的函數邏輯由于與前面案例是一模一樣的,只是因為I2C部分的變化導致其函數實現有一點點的區別,因此本次案例對M24C02部分的函數實現是基于前面案例進行的,也就是默認大家已經熟悉M24C02讀寫操作過程了。不過,如果大家發現難以理解,可以先去把前面軟件模擬I2C案例中關于M24C02的讀寫操作部分再多看看、理解一下,然后回頭來理解可能就會更加明白了。
3.5 main.c測試程序
和前面的案例測試程序一模一樣。直接貼上:
#include "usart.h"
#include "m24c02.h"
#include <string.h>int main(void)
{// 1. 初始化USART_Init();M24C02_Init();printf("hardware I2C will start...\n");// 2. 向m24c02中寫入單字符M24C02_Writebyte(0x00, 'a');M24C02_Writebyte(0x01, 'b');M24C02_Writebyte(0x02, 'c');// 3. 向m24c02讀取數據uint8_t byte1 = M24C02_Readbyte(0x00);uint8_t byte2 = M24C02_Readbyte(0x01);uint8_t byte3 = M24C02_Readbyte(0x02);// 4. 串口輸出打印printf("byte1 = %c\t byte2 = %c\t byte3 = %c\n", byte1, byte2, byte3);// 5. 向m24c02寫入字符串M24C02_Writebytes(0x00, "123456", 6);// 6. 向m24c02讀取數據uint8_t buffer[100] = {0};M24C02_Readbytes(0x00, buffer, 6);// 7. 串口輸出打印printf("buffer = %s\n", buffer);// 8. 測試頁寫超過數據范圍// 緩沖區清零memset(buffer, 0, sizeof(buffer));M24C02_Writebytes(0x00, "1234567890abcdefghijk", 21);M24C02_Readbytes(0x00, buffer, 21);printf("test -> buffer = %s\n", buffer);// 死循環保持狀態while(1){ }
}
然后我們開始測試,編譯然后燒錄,去串口助手看看現象是否與前面測試成功的現象一樣。
顯然是一樣的,說明本次案例成功實現完畢!
以上便是本次文章的所有內容,歡迎各位朋友在評論區討論,本人也是一名初學小白,愿大家共同努力,一起進步吧!
鑒于筆者能力有限,難免出現一些紕漏和不足,望大家在評論區批評指正,謝謝!