STM8學習筆記----普通IO口模擬串口功能

        串口在產品應用中很常見,但是單片機的默認帶的串口往往比較少,有時候就會出現串口不夠用,所以就想着能不能用普通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單片機模擬串口功能實現

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章