串口在我印象中是從來不會丟包的,那是因爲以前都是用的廠家提供的SDK,現在用MCU裸板開發,自己做驅動,如果驅動沒做好,就會丟包。
今天來總結兩個串口驅動層的丟包問題,一個是發數據丟包,即實際發出的數據比預期發的少;一個是收數據丟包,即實際收到的數據比對端發出的數據少。
1、發數據丟包
調試過程發現,當應用層連續兩次調用驅動層的串口發數據接口去發數據時,對端wifi板收到的數據會比發出的少了1byte!這麼詭異的問題,是不是wifi板驅動層有問題?因爲畢竟自己寫的代碼總覺得是那麼完美哈哈哈,然而wifi板是廠家提供的SDK,經過市場驗證很久了,大概率不會有bug。
先來了解一下MCU這邊串口發數據的過程,用的是51內核的某款芯片。
用戶要用串口發數據時,把一個發送數據寄存器置1(即設置TI =1),然後底層會自動產生髮數據的中斷,
在發數據中斷服務函數中,用戶要做的是:
1、 把TI 清零,即TI =0;
2、 把要發的數據裝載到數據寄存器(SBUF),每次只能裝載1個byte。
串口的發送器就開始發送數據,當1byte數據發送完成時,硬件會自動把TI置1,再次產生寫數據中斷,在寫數據中斷裏用戶繼續重複剛剛的操作。
那麼數據發送是怎麼結束的?當某次進入發數據中斷,用戶把TI清零,沒有再往數據寄存器裝載數據時,就不會再產生髮數據中斷,發送過程就結束了。
原來的代碼大致如下,
//驅動層用於發數據的先進先出的環形fifo,大小256
char *send_buf_fifo[256];
//fifo寫指針,指示fifo數據裝到什麼地方了,如果裝到末尾了,又會返回頭部從頭部裝
int tx_index;
//fifo讀指針,指示當前數據發送發到哪了
int rx_index;
//驅動層發送數據接口實現
int uart_send(char *buf, int len)
{
//buf 是要發的數據緩存指針,len是要發的長度
把buf中數據拷貝到驅動層發數據的 fifo中,代碼略
tx_index += len; // Fifo 寫指針 往後移len;
tx_index = tx_index%256;
TI =1;//啓動發送
}
//中斷服務函數
void uart_isr()
{
if(TI )
{
TI = 0;
//fifo中數據還沒有發完
if(tx_index != rx_index)
{
//把fifo中沒發的數據中的第1個byte裝載到串口數據寄存器;
SBUF= send_buf_fifo[rx_index];
rx_index++;
rx_index = rx_index %256; //Fifo讀指針往後移;
}
}
調試發現,當應用層連續兩次調用uart_send接口時,會發生丟包現象,wifi板收到的數據少了1 byte,如果應用層調用uart_send接口是每調用一次隔開一段時間再就不會出現丟包現象。
那麼連續調用時,丟包是怎麼發生的?連續兩次調用uart_send函數,當第二次調用時,第一次調用uart_send函數發的數據驅動層應該還沒有發完,應該還在一次次產生中斷髮數據。
此時應用層又調用uart_send函數,把數據裝載到fifo中,這沒問題,問題在於應用層把 TI = 1了。如果此時串口的硬件發送器正在發數據,此時應用層把TI置爲1了,就會自動馬上進入串口發數據中斷中,剛剛正在發的那1byte數據還沒有成功發出去,此時也就丟掉了。
正確的做法應該怎麼樣?uart_send函數中把 TI置爲1之前先判斷一下,此時驅動層是不是正在發數據過程中,即fifo中有原來的數據還沒有發完,如果是,那麼不用再次把TI置爲1;
修改如下:
char sending_flag = 0; //發送標誌位,指示當前是否正在發送數據過程中
//驅動層發送數據接口實現
int uart_send(char *buf, int len)
{
//buf 是要發的數據緩存指針,len是要發的長度
把buf中數據拷貝到驅動層發數據的 fifo中,代碼略
tx_index += len; // Fifo 寫指針 往後移len;
tx_index = tx_index%256;
//若沒有在發送數據去啓動發送,若正在發送中不用再次啓動
if (sending_flag == 0)
{
TI =1;//啓動發送
sending_flag = 1;
}
}
//中斷服務函數
void uart_isr()
{
if(TI )
{
TI = 0;
//fifo中數據還沒有發完
if(tx_index != rx_index)
{
//把fifo中沒發的數據中的第1個byte裝載到串口數據寄存器;
SBUF= send_buf_fifo[rx_index];
rx_index++;
rx_index = rx_index %256; //Fifo讀指針往後移;
}
else //若fifo中數據已經發送完,清發送指示標誌
{
sending_flag = 0;
}
}
這樣修改之後連續兩次調用發送接口,就不再有丟包的問題出現了。
2、收數據丟包
把各個模塊代碼全部跑起來調試發現,用電腦模擬wifi板給MCU發送指令時,大概率出現了丟包現象,MCU收到的數據包不完整,比實際電腦發出的少,偶爾才能收到一包完整的。
電腦串口驅動層總不會有問題吧?哈哈哈,這時候不懷疑自己程序有問題都不行了。
驅動層打印log發現,驅動層收到的數據確實少了,收數據是在串口接收中斷中收的,在中斷服務函數中用戶把數據寄存器的數據拷到自己的緩存中去,初步懷疑是串口接收中斷丟了。
之前有一個版本是好的,對比那個版本代碼發現,有問題的版本LED驅動用的是可調光模式,即可以調亮度的模式,MCU包括一個LED驅動,有8個COM口16個SEG口,最多可以接8*16=128個LED燈。可調光模式與不可調光模式的區別就是,在LED中斷服務服務函數裏,不可調光模式是不做任何事直接返回,可調光模式要去執行一段代碼,去給當前COM週期下的燈進行亮度賦值。
那是不是串口收數據中斷被led中斷耽誤了?
查芯片手冊瞭解到,
1、這款芯片中斷是有優先級的而且可以嵌套,低優先級中斷可以被高優先級中斷打斷,
2、如果各個中斷優先級相同,當同時發生中斷時,就看輪詢優先級,即按一定順序輪詢,先查到誰就去執行誰的中斷服務函數。
3、如果不去設置優先級,那各個中斷優先級是默認都是最低優先級。
那麼當串口有數據要收時中斷多久發生一次?數據是1byte 1 byte收的,也就是1byte發生一次中斷,波特率115200,也就是115200 bit 每秒,那麼發送1 byte 用的時間是 1/115200 *9 = 78.12us(這裏乘以9是因爲串口設置的是8個數據位1個停止位,也就是發送1byte數據實際發送有8位數據+1位停止位);
系統時鐘用的是24M,一條空指令nop時間是 1/24M =0.042us,平均一條指令算5個nop,0.042 *5= 0.21us;
那78.12us之內大概可以執行多少條指令?78.12/0.21 = 372;
也就是說如果在LED中斷服務函數裏的指令超過了大概372條彙編指令,而且LED中斷剛發生馬上又發生了串口中斷,此時在LED中斷服務函數裏面呆的時間太長了,等從LED中斷出來時,串口又再次產生了新的中斷,把老的中斷覆蓋了,也就是說此時串口SBUF寄存器裏裝的就是新的數據了,把老的數據覆蓋了,剛剛收到的老數據漏掉了,沒有傳給用戶的緩存。
如何解決?
設置串口中斷優先級爲最高,如果在led中斷服務函數裏面時,串口接收中斷產生了,由於串口中斷優先級>led中斷優先級,那麼CPU會馬上從LED中斷出來去執行串口接收中斷服務函數,如果LED中斷和串口中斷同時產生,會先執行串口中斷函數;
這樣就保證了串口數據接收不會丟包。
LED中斷被影響關係不大,頂多某次閃燈或者亮燈不太對(目前沒發現不對),但是串口數據丟了就是個大問題,會出現控制設備不成功,影響用戶體驗。
把串口中斷設爲最高優先級之後丟包問題解決。
關於串口收數據丟包問題之前也出現過,把系統時鐘調低到6M就會出現115200波特率收數據丟包,調高爲12M、24M就好了,當系統時鐘調到6M時,意味着執行一條指令的時間變長了,應該也是串口中斷被自己的或者其他的中斷服務函數耽誤了。
算了下,時鐘6M時,一個nop是1/6M =0.167us,在串口的一箇中斷週期內,中斷服務函數如果執行大於93條彙編指令就可能耽誤串口中斷,此問題後續再驗證看一下。
免責聲明:本文系網絡轉載,版權歸原作者所有。如涉及作品版權問題,請與我們聯繫,我們將根據您提供的版權證明材料確認版權並支付稿酬或者刪除內容。