前言
好久沒更新博客了,這篇文章寫寫停停,用了近一周的時間,終於寫完了。本篇文章介紹,串口協議數據幀格式、串行通信的工作方式、電平標准、編碼方式及Verilog實現串口發送一個字節數據和接收一個字節數據。
對於MCU串口的發送接收,可能就是1行代碼就能實現串口的發送和接收:
STM32的串口接收和發送
//STM32發送1個字節
USART_SendData(USART1, 'A');
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
//STM32接收1個字節:
uint8_t Res;
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
Res = USART_ReceiveData(USART1);
51單片機的發送和接收
//51單片機發送1個字節
SBUF = 'A;
while(!TI);
TI=0;
//51單片機接收1個字節:
char Res;
if(RI)
{
Res = SBUF;
RI = 0;
}
更方便一點的,通過重寫C庫fput函數和fgetc函數,還可以實現printf直接重定向到串口,用來輸出一些調試信息再方便不過了。
STM32實現輸入輸出重定向到串口發送接收
//可重定向printf函數
int fputc(int ch, FILE *f)
{
USART_SendData(DEBUG_USARTx, (uint8_t) ch);
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
return (ch);
}
//可重定向scanf函數
int fgetc(FILE *f)
{
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USARTx);
}
而MCU上的串口是半導體廠商預先設計好的,幾乎是MCU的標配,高度集成,使用起來十分方便,但是串口的引腳基本上是固定的,不可以更改。對於硬件橡皮泥——FPGA來說,需要使用HDL從底層串口數據幀來實現,可以直接在任意一個引腳實現串口功能。為了用Verilog HDL實現標准的串口通訊協議,我們有必要先來詳細了解一下串口通訊協議。
串口數據幀格式
波特率
波特率,即比特率(Baud rate),即通信雙方“溝通的語言”,通信雙方要設置為一樣的波特率才可以正常通信。表示每秒發送的二進制位數,即傳輸1位的時間是:1/波特率 秒,如,波特率9600bps,即每秒傳輸9600bit,那么每一位的時間為:1/9600 s = 104.1666us,常用的波特率有:4800/9600/115200/12800等等,也可以根據需要自定義波特率大小,如1M或者3M,但是有的PC或者USB-TTL模塊不支持太高速度的波特率,常用的USB-TTL芯片有:CH340,CP2102,PL2103,FT232等,其中FT232HL芯片最大支持12M的波特率,當然價格也比其他芯片高一些。
起始位和停止位
數據幀從起始位開始,到停止位結束。起始信號用邏輯0表示,而停止位是用邏輯1表示,一般有0.5位、1位、1.5位或2位停止位,常用的一般是1位停止位,只要通信雙方約定一致即可。
數據位
起始位之后,緊跟着的是數據位,低位(LSB)在前,高位(MSB)在后,一般有5位、6位、7位和8位數據位,常用的是8位數據位,因為一個字節正好是8位。
校驗位
校驗位一般用來判斷接收的數據位有無錯誤,校驗方法有:奇校驗(odd)、偶校驗(even)、0校驗(space)、1校驗(mark)及無校驗(noparity)。奇校驗要求有效數據和校驗位中“1”的個數為奇數,比如一個8位長的有效數據為:01101001,此時共有4個“1”,為達到奇數個"1"的效果,校驗位為“1”,讓“1”的個數變成5個(奇數)。偶校驗剛好相反,要求有效數據和校驗位的“1”數量為偶數,則此時為達到偶校驗效果,校驗位為“0”。而0校驗,即校驗位總是為“0”,1校驗校驗位總是為“1”。奇偶校驗邏輯相反,01校驗邏輯相反。一般是奇偶校驗或者是無校驗位。
奇偶校驗的Verilog實現
在Verilog中奇偶校驗的計算非常簡單,根據奇偶校驗的原理,偶校驗為數據位各位異或,奇校驗是偶校驗取反,通過使用單目運算符的縮減功能,可以非常簡單的計算奇偶校驗位:
input [7:0] data_in, //需要發送的8位數據
wire even_bit; //偶校驗位 = 各位異或
wire odd_bit; //奇校驗位 = ~偶校驗位
assign even_bit = ^data_in; //一元約簡運算符,等效於data_in[0] ^ data_in[1] ^ .....
assign odd_bit = ~even_bit;
wire POLARITY_BIT = even_bit; //偶校驗
關於波特率允許的誤差
經過我的實際測試,波特率是有一定的容錯范圍的,例如,STM32配置成115200波特率,每10ms發送一個30字節的字符串,串口芯片用的CH340,上位機波特率設置成113000-121000也可以接收,無亂碼,差不多正負2000的波特率,這容錯范圍也太大了,當然如果發送頻率太快,數據量太大,誤碼率肯定會大大增加,所以還是建議通信雙方使用同樣的波特率以減少誤差。
串口數據的實際波形
使用串口上位機連接USB-TTL模塊,發送一個字節數據:1位停止位+8位數據位+1位奇校驗位+1位停止位,使用示波器的單次觸發功能,可以在USB-TTL模塊的TX引腳測得串口協議數據的實際波形,你知道這發送的是什么字符嗎?
一個字符的實際波形
兩個字符的實際波形
單工、半雙工、全雙工、異步和同步的區別
在介紹串口的電平標准之前,先來了解一下串行通信的工作方式,即單工、半雙工、全雙工,異步和同步的區別。
單工
單工,即數據傳輸只在一個方向上傳輸,只能你給我發送或者我給你發送,方向是固定的,不能實現雙向通信,如:室外天線電視、調頻廣播等。
半雙工
半雙工比單工先進一點,傳輸方向可以切換,允許數據在兩個方向上傳輸,但是某個時刻,只允許數據在一個方向上傳輸,可以基本雙向通信,如:對講機,IIC通信。
全雙工
比半雙工更先進的是全雙工,允許數據同時在兩個方向傳輸。發送和接收完全獨立,在發送的同時可以接收信號,或者在接收的同時可以發送。它要求發送和接收設備都要有獨立的發送和接收能力,如:電話通信,SPI通信,串口通信。
同步和異步的區別
串行通信可以分為兩種類型,一種叫同步通信,另一種叫異步通信。
簡單的說,就是同步通信需要時鍾信號,而異步通信不需要時鍾信號。
- 同步:發送方發出數據后,等接收方發回響應以后才發下一個數據包的通訊方式。
- 異步:發送方發出數據后,不等接收方發回響應,接着發送下個數據包的通訊方式。
SPI和IIC為同步通信,UART為異步通信,而USART為同步&異步通信。
- USART:通用同步和異步收發器
- UART:通用異步收發器
即USART支持同步和異步收發,而UART只支持異步收發。
如STM32的串口工作在同步模式時,即智能卡模式時,就需要連接同步時鍾引腳。
常用的串行通信協議/電平標准
TTL電平
即普通MCU芯片輸出的串口電平,如各MCU輸出的串口信號就是TTL電平。低電平為0-GND,高電平為1-VCC,標准的數字電路邏輯。特點是速度快,延遲低,但是功耗大。基本上用於板內兩個芯片之間短距離通信。
RS232
RS232是工業上常用的串口標准,無論是PLC的232接口,還是工控機上的串口,輸出的串口電平都是232電平標准,232標准采用負邏輯電平,即-15-3v為邏輯1,+3+15為邏輯0,這里的電平是指RX和TX相對於GND的電壓,可見無論在電壓范圍還是電壓極性上都和TTL不同,顯然這兩種電平不能直接連接,需要使用MAX232類似的電平轉換芯片,對兩種電平進行互相轉換,全雙工,傳輸距離一般控制在20m以內,原因是RS-232屬單端信號傳送,存在共地噪聲和不能抑制共模干擾等問題。
RS485
在要求通信距離為幾十米到上千米時,廣泛采用RS-485 串行總線標准。RS-485采用平衡發送和差分接收,因此具有抑制共模干擾的能力。加上總線收發器具有高靈敏度,能檢測低至200mV的電壓,故傳輸信號能在千米以外得到恢復。 RS-485采用半雙工工作方式,任何時候只能有一點處於發送狀態,因此,發送電路須由使能信號加以控制。RS-485用於多點互連時非常方便,可以省掉許多信號線。應用RS-485 可以聯網構成分布式系統,其允許最多並聯32台驅動器和32台接收器。
RS422
RS-422和RS-485電路原理基本相同,都是以差分方式發送和接受,不需要數字地線。RS-422通過兩對雙絞線可以全雙工工作收發互不影響,而RS485只能半雙工工作,發收不能同時進行,但它只需要一對雙絞線。RS422和RS485在19kpbs下能傳輸1200米。RS-422的電氣性能與RS-485完全一樣。主要的區別在於:RS-422有4根信號線:兩根發送(Y、Z)、兩根接收(A、B)。由於RS-422的收與發是分開的所以可以同時收和發(全雙工)。
串行通信的編碼方式
RZ編碼
RZ編碼也成為歸零碼,歸零碼的特性就是在一個周期內,用二進制傳輸數據位,在數據位脈沖結束后,需要維持一段時間的低電平。如圖:
上圖表示的是單極性歸零碼,即低電平表示0,正電平表示1。對於雙極性歸零碼來說,則是高電平表示1,負電平表示0。如下圖所示:
NRZ編碼
NRZ編碼也成為不歸零編碼,也是我們最常見的一種編碼,即正電平表示1,低電平表示0。它與RZ碼的區別就是它不用歸零,也就是說,一個周期可以全部用來傳輸數據,這樣傳輸的帶寬就可以完全利用。
NRZI編碼
NRZI編碼的全稱為反向不歸零編碼,這種編碼方式集成了前兩種編碼的優點,即既能傳輸時鍾信號,又能盡量不損失系統帶寬。對於USB2.0通信的編碼方式就是NRZI編碼。其實NRZI編碼方式非常的簡單,即信號電平翻轉表示0,信號電平不變表示1。例如想要表示00100010(B),則信號波形如下圖所示:
例如有一段數據為:1111 1111 (B)要發送,則整個傳輸線上的電平狀態是這樣的:
Manchester編碼
曼徹斯特編碼,又稱數字雙向碼、分相碼或相位編碼(PE),是一種常用的的二元碼線路編碼方式。常用在以太網通信,列車總線控制,工業總線等領域。在曼徹斯特編碼中,每一位的中間有一跳變,位中間的跳變既作時鍾信號,又作數據信號;從高到低跳變表示“0”,從低到高跳變表示“1”。其中非常值得注意的是,在每一位的"中間"必有一跳變,根據此規則,可以得出曼徹斯特編碼波形圖的畫法。例如:傳輸二進制信息0,若將0看作一位,我們以0為中心,在兩邊用虛線界定這一位的范圍,然后在這一位的中間畫出一個電平由高到低的跳變。后面的每一位以此類推即可畫出整個波形圖。舉個圖例吧,若要表示數據1001 1010(B),則信號波形圖如下圖所示:
曼徹斯特編碼方式也如前面所說,雖然傳輸了時鍾信號,但也損失了一部分的帶寬,主要表現在相鄰相同數據上。但對於高速數據來說,這種編碼方式無疑是這幾種編碼方式中最優的,相比NRZI編碼,曼徹斯特編碼不存在長時間信號狀態不變導致的時鍾信號丟失的情況,所以在這種編碼方式在以太網通信中是十分常用的。
串行和並行哪個速度快?
串口,即串行通信接口,與之對應的是並行接口。在實際時鍾頻率比較低的情況下,並口因為可以同時傳輸若干比特,速率確實比串口快。但是,隨着技術的發展,時鍾頻率越來越高,當時鍾頻率提高到一定的程度時,並行接口因為有多條並行且緊密的導線,導線之間的相互干擾越來越嚴重。而串口因為導線少,線間干擾容易控制,況且加上差分信號的加持,抗干擾性能大大提升,因此可以通過不斷提高時鍾頻率來提高傳輸速率,這就是為什么現在高速傳輸都采用串行方式的原因。例如常見的USB、SATA、PCIe、以太網等。
如果有人問關於串行傳輸與並行傳輸誰更好的問題,你也許會脫口而出:串行通信好!但是,串行傳輸之所以走紅,是由於將單端信號傳輸轉變為差分信號傳輸,並提升了控制器工作頻率的原因,而“在相同頻率下並行通信速度更高”這個基本道理是永遠不會錯的,通過增加位寬來提高數據傳輸率的並行策略仍將發揮重要作用。當然,前提是有更好的措施來解決並行傳輸過程中的種種問題。
標准串口協議的Verilog實現
基於Verilog實現標准串口協議發送8位數據:起始位 + 8位數據位 + 校驗位 + 停止位 = 11位,每1位的時間是16個時鍾周期,所以輸入時鍾應該為:波特率*16,帶Busy忙信號輸出。實現方法比較簡單,數據幀的拼接、計數器計時鍾周期,每16個時鍾周期輸出一位數據即可。
串口發送1個字節實現
/*
串口協議發送:起始位 + 8位數據位 + 校驗位 + 停止位 = 11位 * 16 = 176個時鍾周期
clk頻率 = 波特率 * 16
*/
module uart_tx_8bit(
//input
input clk, //UART時鍾=16*波特率
input rst_n,
input [7:0] data_in, //需要發送的數據
input trig, //上升沿發送數據
//output
output busy, //高電平忙:數據正在發送中
output reg tx //發送數據信號
);
reg[7:0] cnt; //計數器
reg trig_buf;
reg trig_posedge_flag;
// reg trig_negedge_flag;
reg send;
reg [10:0] data_in_buf; //trig上升沿讀取輸入的字節,拼接數據幀
wire odd_bit; //奇校驗位 = ~偶校驗位
wire even_bit; //偶校驗位 = 各位異或
wire POLARITY_BIT = even_bit; //偶校驗
// wire POLARITY_BIT = odd_bit; //奇校驗
assign even_bit = ^data_in; //一元約簡,= data_in[0] ^ data_in[1] ^ .....
assign odd_bit = ~even_bit;
assign busy = send; //輸出的忙信號
//起始位+8位數據位+校驗位+停止位 = 11位 * 16 = 176個時鍾周期
parameter CNT_MAX = 176;
always @(posedge clk)
begin
if(!rst_n)
begin
trig_buf <= 0;
trig_posedge_flag <= 0;
// trig_negedge_flag <= 0;
end
else
begin
trig_buf <= trig;
trig_posedge_flag <= (~trig_buf) & trig; //在trig信號上升沿時產生1個時鍾周期的高電平
// trig_negedge_flag <= trig_buf & (~trig); //在trig信號下降沿時產生1個時鍾周期的高電平
end
end
always @(posedge clk)
begin
if(!rst_n)
send <= 0;
else if (trig_posedge_flag & (~busy)) //當發送命令有效且線路為空閑時,啟動新的數據發送進程
send <= 1;
else if(cnt == CNT_MAX) //一幀資料發送結束
send <= 0;
end
always @ (posedge clk)
begin
if(!rst_n)
data_in_buf <= 11'b0;
else if(trig_posedge_flag & (~busy)) //只讀取一次數據,一幀數據發送過程中,改變輸入無效
data_in_buf <= {1'b1, POLARITY_BIT, data_in[7:0], 1'b0}; //數據幀拼接
end
always @ (posedge clk)
begin
if(!rst_n)
cnt <= 0;
else if(!send || cnt >= CNT_MAX)
cnt <= 0;
else if(send)
cnt <= cnt + 1;
end
always @(posedge clk)
begin
if(!rst_n)
tx <= 1;
else if(send)
begin
case(cnt) //1位占用16個時鍾周期
0: tx <= data_in_buf[0]; //低位在前,高位在后
16: tx <= data_in_buf[1]; //bit0,占用第16~31個時鍾
32: tx <= data_in_buf[2]; //bit1,占用第47~32個時鍾
48: tx <= data_in_buf[3]; //bit2,占用第63~48個時鍾
64: tx <= data_in_buf[4]; //bit3,占用第79~64個時鍾
80: tx <= data_in_buf[5]; //bit4,占用第95~80個時鍾
96: tx <= data_in_buf[6]; //bit5,占用第111~96個時鍾
112: tx <= data_in_buf[7]; //bit6,占用第127~112個時鍾
128: tx <= data_in_buf[8]; //bit7,占用第143~128個時鍾
144: tx <= data_in_buf[9]; //發送奇偶校驗位,占用第159~144個時鍾
160: tx <= data_in_buf[10]; //發送停止位,占用第160~167個時鍾
CNT_MAX: tx <= 1; //無空閑位
default:;
endcase
end
else if(!send)
tx <= 1;
end
endmodule
仿真波形
串口接收1個字節實現
串口接收部分的實現,涉及到串口數據的采樣,對於MCU來說,不同單片機集成外設的處理方式有所不同,具體采樣原理可以參考內核的Reference Manual。以傳統51內核為例,按照所設置的波特率,每個位時間被分為16個時間片。UART接收器會在第7、8、9三個時間片進行采樣,按照三取二的邏輯獲得該位時間內的采樣結果。其它一些類型的單片機則可能會更加嚴苛,例如有些工業單片機會五取三甚至七取五(設置成抗干擾模式時)。
本程序中采用的中間值采樣,即取16個時鍾周期中的中間位作為當前的采樣值。
//Verilog實現串口協議接收,帶錯誤指示,校驗錯誤和停止位錯誤
/*
16個時鍾周期接收1位,中間采樣
*/
module my_uart_rx(
input clk, //采樣時鍾
input rst_n,
input rx, //UART數據輸入
output reg [7:0] dataout, //接收數據輸出
output reg rx_ok, //接收數據有效,高說明接收到一個字節
output reg err_check, //數據出錯指示
output reg err_frame //幀出錯指示
);
reg [7:0] cnt;
reg [10:0] dataout_buf;
reg rx_buf;
reg rx_negedge_flag;
reg receive;
wire busy;
wire odd_bit; //奇校驗位 = ~偶校驗位
wire even_bit; //偶校驗位 = 各位異或
wire POLARITY_BIT; //本地計算的奇偶校驗
// wire polarity_ok;
// assign polarity_ok = (POLARITY_BIT == dataout_buf[9]) ? 1 : 0; //校驗正確=1,否則=0
assign busy = rx_ok;
assign even_bit = ^dataout; //一元約簡,= data_in[0] ^ data_in[1] ^ .....
assign odd_bit = ~even_bit;
assign POLARITY_BIT = even_bit; //偶校驗
// assign POLARITY_BIT = odd_bit; //奇校驗
parameter CNT_MAX = 176;
//rx信號下降沿標志位
always @(posedge clk)
begin
if(!rst_n)
begin
rx_buf <= 0;
rx_negedge_flag <= 0;
end
else
begin
rx_buf <= rx;
rx_negedge_flag <= rx_buf & (~rx);
end
end
//在接收期間,保持高電平
always @(posedge clk)
begin
if(!rst_n)
receive <= 0;
else if (rx_negedge_flag && (~busy)) //檢測到線路的下降沿並且原先線路為空閑,啟動接收數據進程
receive <= 1; //開始接收數據
else if(cnt == CNT_MAX) //接收數據完成
receive <= 0;
end
//起始位+8位數據位+校驗位+停止位 = 11位 * 16 = 176個時鍾周期
always @ (posedge clk)
begin
if(!rst_n)
cnt <= 0;
else if(!receive || cnt >= CNT_MAX)
cnt <= 0;
else if(receive)
cnt <= cnt + 1;
end
//校驗錯誤:奇偶校驗不一致
always @ (posedge clk)
begin
if(!rst_n)
err_check <= 0;
else if(cnt == 152)
begin
// if(POLARITY_BIT == rx)
if(POLARITY_BIT != dataout_buf[9]) //奇偶校驗正確
err_check <= 1; //鎖存
// else
// err_check <= 1;
end
end
//幀錯誤:停止位不為1
always @ (posedge clk)
begin
if(!rst_n)
err_frame <= 0;
else if(cnt == CNT_MAX)
begin
if(dataout_buf[10] != 1) //停止位
err_frame <= 1;
// else
// err_frame <= 1; //如果沒有接收到停止位,表示幀出錯
end
end
always @ (posedge clk)
begin
if(!rst_n)
dataout <= 11'h00;
else if(receive)
begin
// if(rx_ok)
if(cnt >= 137)
dataout <= dataout_buf[8:1]; //數據位:8-1位
// else if(!rx_ok)
// dataout <= 0;
end
end
always @ (posedge clk)
begin
if(!rst_n)
rx_ok <= 0;
else if(receive)
begin
if(cnt >= 137) //137-169
rx_ok <= 1;
else
rx_ok <= 0;
end
else
rx_ok <= 0;
end
//起始位+8位數據+奇偶校驗位+停止位 = 11 * 16 = 176位
always @(posedge clk)
begin
if(!rst_n)
dataout_buf <= 8'h00;
else if(receive)
begin
case (cnt) //中間采樣
8'd8: dataout_buf[0] <= rx; //起始位=0
8'd24: dataout_buf[1] <= rx; //LSB低位在前
8'd40: dataout_buf[2] <= rx;
8'd56: dataout_buf[3] <= rx;
8'd72: dataout_buf[4] <= rx;
8'd88: dataout_buf[5] <= rx;
8'd104: dataout_buf[6] <= rx;
8'd120: dataout_buf[7] <= rx;
8'd136: dataout_buf[8] <= rx; //MSB高位在后
8'd152: dataout_buf[9] <= rx; //奇偶校驗位
8'd168: dataout_buf[10] <= rx; //停止位=1
default:;
endcase
end
end
endmodule
代碼工程下載
- Github工程地址:https://github.com/whik/UART_Demo_Verilog
- Gitee工程地址:https://gitee.com/whik/UART_Demo_Verilog
工程包含:
- my_uart_rx:串口接收1個字節示例程序
- uart_tx_8bit:串口發送1個字節示例程序
- uart_tx_demo:串口每隔500ms循環發送0-9字符
參考資料:
推薦閱讀:
- 玄鐵910是個啥?是芯片嗎?
- Qt平台下使用QJson解析和構建JSON字符串
- 國產處理器的逆襲機會——RISC-V
- 真正的RISC-V開發板——VEGA織女星開發板開箱評測
- 【2019北京國際消費電子博覽會】參觀總結
- Qt實現軟件自動更新的一種簡單方法
我的博客:www.wangchaochao.top
或微信掃碼關注我的公眾號:mcu149