串口在產品應用中很常見,但是單片機的默認帶的串口往往比較少,有時候就會出現串口不夠用,所以就想着能不能用普通IO口模擬串口來實現串口的功能。
要模擬串口首先要清楚串口數據傳輸過程中的原理。
常用的串口格式爲 1位起始位,8位數據位,無校驗位,1位結束位。起始位爲低電平,結束位爲高電平。數據0爲低電平,數據1爲高電平。
所以最簡單的串口傳輸一個字節總共有10個電平變化,每個電平的寬度由波特率決定的。
具體的串口數據分析,可以參考這篇文章:STM8學習筆記---通過示波器分析串口數據。
下面看一個通過波特率如何計算每個位的電平寬度。
發送一個字節,以stm8中9600bit/s的波特率計算的過程爲例(1秒鐘傳輸9600位)。
可以計算出傳輸1位所需要的時間 T1 = 1/9600 約爲104us。
通過計算可以看出來,如果波特率爲9600時,一個位的電平寬度要爲104us。
上圖中就是一個字節的完整波形,起始位爲低電平,結束位爲高電平,中間8位爲數據位,無校驗位。數據位是低位在前,高位在後。也就是和起始位挨着的是最低位,和結束位挨着的是最高位。通過這個波形就可以分析出,發送的數據是0x00。
下面在看一看0X01的波形。
起始位爲低電平,結束位爲高電平,中間數據位有一個高電平,其餘都是低電平,按照低位在前,高位在後規律,數據位就是0000 0001 剛好是16進制的0x01。
知道了串口數據的格式,下面就開始寫代碼,先寫串口發送代碼。
//輸出1
void Send_1( void )
{
SIM_TXD = 1;
}
//輸出0
void Send_0( void )
{
SIM_TXD = 0;
}
//發送一個字節
void WriteByte( unsigned char sdata ) //波特率9600
{
//如果數據誤碼率比較高,可以修改delay_us延時時間
unsigned char i;
unsigned char value = 0;
//發送起始位
Send_0();
delay_us( 100 );
//發送數據位
for( i = 0; i < 8; i++ )
{
value = ( sdata & 0x01 ); //先傳低位
if( value )
{
Send_1();
}
else
{
Send_0();
}
delay_us( 100 ); //經測試延時100us 數據沒有誤差 示波器上觀察波形時間爲104us左右
sdata = sdata >> 1;
}
//停止位
Send_1();
delay_us( 100 );
}
首先發送起始位,將IO口電平拉低,延時104us,下來發送8位數據位,低位在前,高位在後,每發送一位就延時104us。最後發送結束位,將IO口電平置高,在延時104us。這樣一個字節就發送結束了。
由於延時程序使用軟件實現的,所以延時精度不是很高,延時參數設置爲100時,通過示波器看一位剛好是104us。由於不同的編譯器,不同的單片機,通過軟件計算的延時時間不同,所以這裏的延時時間最好通過示波器來測,延時時間的精確度決定了通信的誤碼率。延時時間精確通信誤碼率就低,延時誤差比較大,通信誤碼率就高。
下面再看接收程序
//接收一個字節
void ReadByte( void )
{
unsigned char i, value = 0;
if( !SIM_RXD ) //RXD_IN RXD等於0時開始接收
{
//等過起始位 起始位爲低電平
delay_us( 100 );
//接收8位數據位
for( i = 0; i < 8; i++ )
{
value >>= 1;
if( SIM_RXD ) //RXD_IN
{
value |= 0x80;
}
delay_us( 100 );
}
//等過結束位 結束位爲高電平
//delay_us(100); //一次性接收大量數據時,防止程序執行代碼時浪費時間,在結束符時可以不用等待
RecFlag = 1; //標記已經接收到了數據
RecBuf = value;
return;
}
}
接收數據就更好理解了,讀取IO口的電平,如果出現低電平就開始接收數據,然後讀取8個數據位的電平,在等待結束位結束。這樣一個字節的數據就接收完成了。
那麼如何判斷什麼時候去接收串口的數據呢?
有兩種方式去實現,一種是在死循環中用查詢方式去判斷,一直讀取IO的的電平,如果出現低電平就認爲串口有數據發送進來了。
實現代碼如下:
//查詢方式接收數據
void ReadString( void )
{
unsigned int cnt = 0, i = 0, j = 0;
unsigned char recstr[100] = {0};
_Bool send_F = 0;
while( 1 )
{
if( !RecFlag ) //未收到數據時掃描
{
ReadByte(); //掃描數據
cnt++; //統計掃描次數
}
else if( RecFlag ) //收到數據後讀取數據 如果接收到數據沒有讀取時 不會繼續接收數據
{
cnt = 0;
RecFlag = 0;
recstr[i++] = RecBuf; //存儲接收的數據
RecBuf = 0;
send_F = 1; //標記數據可以發送
}
if( ( cnt >= 100 ) && ( send_F == 1 ) ) //掃描次數超過100 並且可以發送數據
{
cnt = 0;
WriteString( recstr ); //發送接收到的數據
send_F = 0; //清除發送標誌
i = 0; //清除接收數據數組下標
for( j = 0; j < 100; j++ ) //清除接收數據緩衝區
{
recstr[j] = 0;
}
return; //發送完數據後返回
}
else if( cnt > 500 ) //沒有接收到數據 超時退出
{
cnt = 0;
return;
}
}
}
這種方式實現起來比較簡單,但是對於程序編寫比較麻煩,因爲要一直監視者IO口,所以程序幹其他事情時,很有可能錯過數據的接收。可以用第二種方式,IO口中斷來判斷什麼時候要開始接收數據,將IO口設置爲下降沿中斷,當有下降沿出現時,說明串口有數據進來了,然後再去讀取串口數據。沒有中斷髮生時,程序就可以幹其他事情了。
實現代碼如下:
//通道PC3口的下降沿中斷檢測數據
//PC3口中斷 RXD
#pragma vector = 7 // IAR中的中斷號,要在STVD中的中斷號上加2
__interrupt void RXDInterrupt( void )
{
PC_CR2 &= ~( 1 << 3 ); //禁止外部中斷
ReadByte();
if( recEnd == 0x01 )
{
if( RecBuf == 0x0a ) //收到結束符 0x0a 標記數據接收完畢
{
recEnd |= 0x02;
recCNT = 0;
}
}
if( recEnd != 0x03 )
{
if( RecBuf != 0x0d ) //結束符爲回車換行符 0x0d 0x0a
{
recBUFF[recCNT++] = RecBuf; //沒收到結束符存儲數據
RecBuf = 0;
}
else if( RecBuf == 0x0d ) //收到0x0d 標記結束符開始
{
recEnd |= 0x01;
}
}
PC_CR2 |= ( 1 << 3 ); //使能外部中斷
}
當出現下降沿之後進入中斷程序,這時候要關閉外部中斷,開始讀取IO口電平狀態。若不關閉中斷,在讀取IO電平的過程中中斷還會不停的進入,這樣就會影響讀取數據的準確性。所以進入中斷會首先要關閉中斷,接收完一個字節之後,在打開中斷,接收下一個字節。直到收到了回車換行符(也就是0x0D 0x0A),就認爲數據發送已經結束。就退出接收過程,然後主程序就可以去處理接收到的數據了。
這樣串口的發送和接收通過IO的電平模式就可以實現了。
看一下測試效果
部分參考代碼如下:
模擬發送和接收代碼:
#include "myuart.h"
unsigned char recBUFF[100] = {0}; //存儲接收到的數據
unsigned char recCNT = 0; //接收數據個數
unsigned char recEnd = 0; //數結束標誌
unsigned char RecBuf; //接收緩衝區
_Bool RecFlag = 0; //接收到數據標誌位
//模擬串口初始化 PC3 RXD PC4 TXD
void MyUart_Init ( void )
{
PC_DDR |= ( 1 << 4 ); //PC4 輸出 TXD
PC_CR1 |= ( 1 << 4 ); //PC4 推輓輸出
PC_CR2 |= ( 1 << 4 );
PC_DDR &= ~( 1 << 3 ); //PC3 輸入 RXD
PC_CR1 &= ~( 1 << 3 ); //PC3
PC_CR2 |= ( 1 << 3 ); //使能外部中斷
EXTI_CR1 |= ( 1 << 5 ); //PC口下降沿觸發
}
//輸出1
void Send_1( void )
{
SIM_TXD = 1;
}
//輸出0
void Send_0( void )
{
SIM_TXD = 0;
}
//發送一個字節
//以stm8中9600bit/s的波特率計算的過程爲例(1秒鐘傳輸9600位)。
//可以計算出傳輸1位所需要的時間 T1 = 1/9600 約爲104us。
void WriteByte( unsigned char sdata ) //波特率9600
{
//如果數據誤碼率比較高,可以修改delay_us延時時間
unsigned char i;
unsigned char value = 0;
//發送起始位
Send_0();
delay_us( 100 );
//發送數據位
for( i = 0; i < 8; i++ )
{
value = ( sdata & 0x01 ); //先傳低位
if( value )
{
Send_1();
}
else
{
Send_0();
}
delay_us( 100 ); //經測試延時100us 數據沒有誤差 示波器上觀察波形時間爲104us左右
sdata = sdata >> 1;
}
//停止位
Send_1();
delay_us( 100 );
}
//發送字符串
void WriteString( unsigned char *s )
{
while( *s != 0 )
{
WriteByte( *s );
s++;
}
}
//接收一個字節
void ReadByte( void )
{
unsigned char i, value = 0;
if( !SIM_RXD ) //RXD_IN RXD等於0時開始接收
{
//等過起始位 起始位爲低電平
delay_us( 100 );
//接收8位數據位
for( i = 0; i < 8; i++ )
{
value >>= 1;
if( SIM_RXD ) //RXD_IN
{
value |= 0x80;
}
delay_us( 100 );
}
//等過結束位 結束位爲高電平
//delay_us(100); //一次性接收大量數據時,防止程序執行代碼時浪費時間,在結束符時可以不用等待
RecFlag = 1; //標記已經接收到了數據
RecBuf = value;
return;
}
}
//通道PC3口的下降沿中斷檢測數據
//PC3口中斷 RXD
#pragma vector = 7 // IAR中的中斷號,要在STVD中的中斷號上加2
__interrupt void RXDInterrupt( void )
{
PC_CR2 &= ~( 1 << 3 ); //禁止外部中斷
ReadByte();
if( recEnd == 0x01 )
{
if( RecBuf == 0x0a ) //收到結束符 0x0a 標記數據接收完畢
{
recEnd |= 0x02;
recCNT = 0;
}
}
if( recEnd != 0x03 )
{
if( RecBuf != 0x0d ) //結束符爲回車換行符 0x0d 0x0a
{
recBUFF[recCNT++] = RecBuf; //沒收到結束符存儲數據
RecBuf = 0;
}
else if( RecBuf == 0x0d ) //收到0x0d 標記結束符開始
{
recEnd |= 0x01;
}
}
PC_CR2 |= ( 1 << 3 ); //使能外部中斷
}
延時代碼:
#include "delay.h"
volatile u8 fac_us = 0; //us延時倍乘數
//延時函數初始化
//爲確保準確度,請保證時鐘頻率最好爲4的倍數,最低8Mhz
//clk:時鐘頻率(24/16/12/8等)
void delay_init( u8 clk )
{
if( clk > 16 )
{
fac_us = ( 16 - 4 ) / 4; //24Mhz時,stm8大概19個週期爲1us
}
else if( clk > 4 )
{
fac_us = ( clk - 4 ) / 4;
}
else
{
fac_us = 1;
}
}
//延時nus
//延時時間=(fac_us*4+4)*nus*(T)
//其中,T爲CPU運行頻率(Mhz)的倒數,單位爲us.
//準確度:
//92% @24Mhz
//98% @16Mhz
//98% @12Mhz
//86% @8Mhz
void delay_us( u16 nus )
{
/*
// STVD 編譯環境下彙編代碼
#asm
PUSH A //1T,壓棧
DELAY_XUS:
LD A,_fac_us //1T,fac_us加載到累加器A
DELAY_US_1:
NOP //1T,nop延時
DEC A //1T,A--
JRNE DELAY_US_1 //不等於0,則跳轉(2T)到DELAY_US_1繼續執行,若等於0,則不跳轉(1T).
NOP //1T,nop延時
DECW X //1T,x--
JRNE DELAY_XUS //不等於0,則跳轉(2T)到DELAY_XUS繼續執行,若等於0,則不跳轉(1T).
POP A //1T,出棧
#endasm
*/
//Keil 開發環境下彙編代碼
__asm(
"PUSH A \n" //1T,壓棧
"DELAY_XUS: \n"
"LD A,fac_us \n" //1T,fac_us加載到累加器A
"DELAY_US_1: \n"
"NOP \n" //1T,nop延時
"DEC A \n" //1T,A--
"JRNE DELAY_US_1 \n" //不等於0,則跳轉(2T)到DELAY_US_1繼續執行,若等於0,則不跳轉(1T).
"NOP \n" //1T,nop延時
"DECW X \n" //1T,x--
"JRNE DELAY_XUS \n" //不等於0,則跳轉(2T)到DELAY_XUS繼續執行,若等於0,則不跳轉(1T).
"POP A \n" //1T,出棧
);
}
主程序
#include "iostm8s103F3.h"
#include "main.h"
#include "led.h"
#include "exti.h"
#include "delay.h"
#include "myuart.h"
extern unsigned char recEnd;
extern unsigned char recBUFF[100];
void SysClkInit( void )
{
CLK_SWR = 0xe1; //HSI爲主時鐘源 16MHz CPU時鐘頻率
CLK_CKDIVR = 0x00; //CPU時鐘0分頻,系統時鐘0分頻
}
void main( void )
{
__asm( "sim" ); //禁止中斷
SysClkInit();
delay_init( 16 );
LED_GPIO_Init();
MyUart_Init();
__asm( "rim" ); //開啓中斷
WriteString("Virtual serial port test!!!\r\n");
while( 1 )
{
//WriteString("0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ "); //send test
if(recEnd==0x03) //判斷數據是否接收完成
{
WriteString(recBUFF);
recEnd=0x00;
}
LED = !LED;
delay_ms(500);
}
}
完整工程代碼下載地址:stm8單片機模擬串口功能實現