在前三篇文章中由简到烦的介绍了模拟串口的设计规则,但是在前三篇文章中所实现的方法并不能满足我们在实际工程中的使用。在这篇文章中,我将详细的描述模拟串口的实现,并提供相关代码来供大家参考。

1 原理

为了书写的方便,我将使用我的模版文件,关于模版文件的详细介绍请参考这篇文章。同样的,我们认为一个字节是10个位【起始位(1bit)+数据位(8bit)+停止位(1bit)】。

同样的为了方便,我们先实现发送功能再实现接收功能。为了扩大本文章所述内容的适用性,我将使用MAX3485芯片,关于这款芯片的使用方法我将进行详细介绍,同时也请参见《》。

1.1 设计逻辑

发送数据:串口调试助手1发送数据——调用模拟串口发送——模拟串口发送数据——串口调试助手2接收数据


接收数据:串口调试助手2发送数据——模拟串口接收(中断接收)——模拟串口接收数据——调用自带串口——自带串口发送数据——串口调试助手1接收数据

1.2 IO口模拟串口发送原理

空闲状态为高电平,开始位为0,然后发送8个数据位,然后是奇偶校验位,停止位为高电平。数字电路中只有0、1两种状态,这是我们用IO口可以实现的,我们认为高电平是1,低电平是0。也就是说我们用只到了IO口的输出功能(对于实现TX功能的IO口而言),那么到底发多长时间的高电平呢?这是由TIM2决定的,TIM2通过计数器实现。这个时间取决于什么呢?取决于波特率。也就是说,只要我们初始化设置好了TIM2我们就不需要考虑时间问题了。为方便我们现发送低位,然后逐渐右移来发送高位。如何判断发送完一个字节呢?我们认为10位为一个字节。如何判断发送玩所有的字节呢?我们使用一个缓存区(也就是数组)缓存区的字节发送完了我们就认为发送完了。

1.3 IO口模拟串口接收原理

发送我们可以自己来控制(也就是通过函数开相关中断),但是接收怎么处理呢?我们认为不发送的状态就是接收状态,也就是说,只要不是发送,我们接收数据的中断就是使能的。如何判断数据来了呢?我们知道起始位是高电平,因此我们只用到了IO口的输入功能(对于实现RX功能的IO口而言),我们在初始化状态下设置此IO口为低电平,只要IO口接收到了高电平我们就会知道。我们是怎么知道的呢?因为这时会产生一个IO外部中断,所以我们就可以进入中断函数来操作了。进入IO外部中断函数之后我们就开启TIM2的比较捕获中断来判断接下来的8个位是什么电平。开启比较捕获中断后我们就进入的比较捕获中断函数,在这个函数中我们判断IO口的位来给我们的接收字节变量赋值。当接收了10位以后(起始位的时候就开始计数了)我们就认为接收了1个字节。当接收了8个字节的时候我们就可以放入我们的缓存区(数组),接收字节容量的大小根据缓存区的大小来决定。我们接收制定的大小以后,我们就可以在更新中断函数里面写相关的操作代码了,我们这里写的是调用自带串口发送数据。


2 实现过程

同样的,在实现过程中,我们在工程文件夹SimUART中共分了4个文件夹
System:存放系统文件;
Project:存放项目文件;
User:存放main.c和UserApp.c;
My_Lib:存放其它常用的文件。

根据我们将用到的单片机的资源,我们在My_Lib中分了5个文件夹,分别是:
Time:与定时器和中断相关函数的文件。
SimUART:与模拟串口相关函数的文件。
USART:与串口相关函数的文件(因为我们需要串口进行测试)。
EXTI:与中断相关函数的文件,我们用到中断初始化。
MAX3485:我们用到MAX3485芯片。

下面我贴出相关函数的.c文件,而.h文件省略不写,有需要的同学可以点击下载使用。我的编程环境是IAR,需要自己建立IAR工程。下面详细介绍(Project和System省略不写,其中System只用了stm8s.h)。

2.1 一切从main()函数开始

同样的,我们建立完工程后需要从main()函数开始,我们使用的是模版工程(大家可以点击查看设计思路),所以我们main.c不需要做任何改动,我们从初始化开始。

2.2 初始化

初始化相关代码如下:

/************************************************* 
*Function:		All_Config
*Calls:			void
*Called By: 	<span style="white-space:pre">	</span>main.c	
*Input:			void
*OUTPUT:		void
*Return:		void
*DESCRIPTION:	<span style="white-space:pre">	</span>1.初始化函数
                <span style="white-space:pre">	</span>2.选择内部16M晶振
*Others:		nothing
*************************************************/
void All_Config()
{
    //必备初始化
    Clock_Config();     		//时钟初始化 
    PilotLED_Init();                <span style="white-space:pre">	</span>//初始化指示灯 
    TIM4_Init();        		//定时器4初始化
    
    
    //常规初始化
    UART_Init(SYS_CLOCK, 9600);	<span style="white-space:pre">	</span>//串口初始化
    MAX3485_Init();                 <span style="white-space:pre">	</span>//485通讯
    
    //功能初始化
    /*****************************************
    1.模拟串口
    *****************************************/
    SimUART_IO_Init();        <span style="white-space:pre">		</span>//模拟串口IO口初始化
    TIM2_Init();                    <span style="white-space:pre">	</span>//定时器2初始化
    EXTI_Init();                    <span style="white-space:pre">	</span>//中断初始化
}

我们的指示灯使用的是TIM4来控制,所以需要初始化TIM4。如果不需要485通讯可以把这里的485初始化去掉。其他的不说,我下面只介绍模拟串口的初始化功能,其他请参见代码。根据原理我们知道,我们需要用到TXD管脚的输出功能和RXD管脚的输入功能,具体代码如下:

/********************************************************* 
*Function:	IO_Init
*Calls:		void
*Called By: 	All_Config	
*Input:		void
*OUTPUT:	void
*Return:	void
*DESCRIPTION:	1.模拟串口IO口初始化(RX:PC3  TX:PC4)
                2.用到的模拟串口IO口的初始化PC4:发送 PC3:接收
                3.使用不同的IO口作为模拟串口的时候,我们在使
                  用中断(这里使用的TIM2)需要选用不同的中断向量。
*Others:	nothing
*********************************************************/
void SimUART_IO_Init()
{
    /************************************
    1.模拟串口IO口初始化
    2.PC4:发送
    3.PC3:接收    
    ************************************/
    //TXD:TXD位推挽输出  PC4
    SimUART_PORT->ODR |=  SimUART_PIN_TX;
    SimUART_PORT->DDR |=  SimUART_PIN_TX; 
    SimUART_PORT->CR1 |=  SimUART_PIN_TX; 
    SimUART_PORT->CR2 &= ~SimUART_PIN_TX;  
    //RXD:悬浮输入 高电平 PC3
    SimUART_PORT->IDR |=  SimUART_PIN_RX; 
    SimUART_PORT->DDR &= ~SimUART_PIN_RX; 
    SimUART_PORT->CR1 &= ~SimUART_PIN_RX; 
    SimUART_PORT->CR2 &= ~SimUART_PIN_RX; 
}




2.3 模拟串口发送数据

完成时钟、定时器、中断的初始化以后我们就可以开始主体程序的设计了,

逻辑伪代码如下:


void SimUART_TxByte( u8 SendData )
{
    将发送数据放入缓存区;
    缓存区待发送计数++;
    缓存区放满了 就不放了;
	
    开启发送 相关的中断;
}

void SimUART_TxBytes(const u8* SendData , u8 len )
{
    //循环添加,直到添加完毕
    while( len )
    {
        将发送数据放入缓存区;
        需要添加的计数--;
        需要发送的数据的指针++;
        缓存区待发送计数++;
        缓存区放满了 就不放了;
    }
    
    开启发送 相关的中断;
}



具体实现代码如下:

/*************************************************
*Function:	SimUART_TxByte
*Input:		u8 SendData:需要发送的字节
*OUTPUT:	void
*DESCRIPTION:	发送一个字节
*************************************************/
void SimUART_TxByte( u8 SendData )
{
    TxDataBuf[SimUART_SendBuf_IN] = SendData;
    SimUART_SendBuf_IN++;
    
    //缓存区放满了 就不放了
    if( SimUART_SendBuf_IN >= sizeof(TxDataBuf) )
    {
        SimUART_SendBuf_IN = 0;
    }
    
    //开启发送 相关的中断
    TIM2->IER |= TIM2_IER_UIE;      //更新中断使能
}


/*************************************************
*Function:	SimUART_TxBytes
*Input:		const u8* SendData:需要发送的地址
                u8 len:需要发送的字节个数
*OUTPUT:	void
*DESCRIPTION:	发送SendData的前len个字节
*************************************************/
void SimUART_TxBytes(const u8* SendData , u8 len )
{
    while( len )
    {
        //数据逐个放入缓存区并更新相关计数
        TxDataBuf[SimUART_SendBuf_IN] = *SendData;
        len--;
        SendData++;
        SimUART_SendBuf_IN++;
        
        //缓存区放满了,就不放了
        if( SimUART_SendBuf_IN >= sizeof(TxDataBuf) )
        {
            SimUART_SendBuf_IN = 0;
        }
    }
    
    //开启发送 相关的中断
    TIM2->IER |= TIM2_IER_UIE;      //更新中断使能
}


开启更新中断以后我们就需要进入更新中断函数,

逻辑伪代码如下:

#pragma vector = TIM2_Updata_vector
__interrupt void SimUART_Update_IRQHandler()
{
    第一步,清中断标志位;
    第二步,发送一个位;
    
    //第三步,移位和计数
    移位到下一位;
    已经发送的位计数;          //发送一个位 计数
    
    
    //第四步,处理发送完一个字节情况
    if( SimUART_SendData_BitNum >= 10 ) //发送完一个字节  的处理
    {
        已经发送的字节计数;
        
        缓存区发送完成直接退出;
        缓存区没有发送完;
	缓存区发送完成关中断;
    }
}

关于这个中断向量我要提一句,初学者经常不知道这是什么,你会用就性。我们这里用到TIM2的更新中断,所以是TIM2_Updata_vector,至于这个数值代表的含义,我们无需知道。具体代码如下:

/*************************************************
*Function:	SimUART_Update_IRQHandler
*Calls:		void
*Called By: 	void
*Input:		void
*OUTPUT:	void
*Return:	void
*DESCRIPTION:	1.模拟串口  发送
                2.TIM2更新中断  发送数据
*Others:	nothing
*************************************************/
#pragma vector = TIM2_Updata_vector
__interrupt void SimUART_Update_IRQHandler()
{
    //第一步,清中断标志位
    TIM2->SR1 &= ~TIM2_SR1_UIF;
    
    
    //第二步,发送一个位
    /*************************************
    1.先发送 一个 位
    2.如果是高电平,发高电平
    3.如果是低电平,发低电平
    ***************************************/
    if( 0X0001 == ((SimUART_SendData) & 0X0001) )
    {
        SimUART_PORT->ODR |= SimUART_PIN_TX;
    }
    else
    { 
        SimUART_PORT->ODR &= ~SimUART_PIN_TX;
    }
    
    
    //第三步,移位和计数
    SimUART_SendData >>= 1;             //移至下一位
    SimUART_SendData_BitNum++;          //发送一个位 计数
    
    
    //第四步,处理发送完一个字节情况
    if( SimUART_SendData_BitNum >= 10 ) //发送完一个字节  的处理
    {
        SimUART_SendBuf_OUT++; //已经发送的位置 变化
        
        //缓存区发送完成(最多30个字节)
        if( SimUART_SendBuf_OUT >= sizeof(TxDataBuf) )
        {
            SimUART_SendBuf_OUT = 0;  //下一个需要发送的位置 变为 0
            if( SimUART_SendBuf_IN >= sizeof(TxDataBuf) )
            {
                SimUART_SendBuf_IN = 0;
            }
        }
        //缓存区没有发送完
        if(SimUART_SendBuf_IN != SimUART_SendBuf_OUT )
        {
            SimUART_SendData = TxDataBuf[ SimUART_SendBuf_OUT ];
            SimUART_SendData = ( ( SimUART_SendData << 1 )| (0xFE00) );
            SimUART_SendData_BitNum = 0;
        }
        else   //所有需要发送的数据发送完毕  关中断
        {
            /***********************************
            根据实际发现,下一次发送的时候 会跳过
            一个字节数据,所以这里需要往回走一个字
            节,保证不丢数据
            ************************************/
            SimUART_SendBuf_OUT--;          //回走一个字节
            TIM2-> IER &= ~TIM2_IER_UIE;    //禁止更新中断(无法发送数据了)
        }
    }
}



这样我们就实现了模拟串口的发送。我们在此需要先用串口调试助手检查一下,如果可以,我们再进行下面的工作。在我提供的代码中,如果大家不想使用MAX3485,就可以


在根据MAX3485.c中的使用方法反向操作即可。测试方法见参考代码。



2.4 模拟串口接收数据

逻辑伪代码如下:

#pragma vector = EXTI2_PC_vector
__interrupt void SimUART_IO_IRQHandler(void)
{
    第一步,关闭IO中断;
    
    第二步,设置比较/捕获寄存器1的值;
    
     //第三步,准备工作
    变量清零;
    清中断标志位; 

    //第四步开启中断
    使能 捕获/比较中断1;  
}

#pragma vector = TIM2_Capture_vector
__interrupt void SimUART_Capture_IRQHandler(void)
{
    第一步,清中断标志位(防止始终进入中断);   
    接收到的位计数; 
    第二步,读IO输入 ;
	
    //第三步,判断是否接收了10个位             
    if( ( 10 <= RxDataNum ) )                 //如果是则
    {
       接收到的位计数清零;
        
        关 比较中断 ; 
        清中断标志位;
        开 IO中断 ;//现在又可以接收数据了
       
        //处理有效数据
        if(  (RxDataValue & 0x0402) == 0x0400 )
        {
        }             
    }
}

具体代码如下(标准串口是先发低位再发高位,因此接收也是先接收低位再发送高位);


/******************************************************
*Function:	UART_RX_IRQHandler
*Calls:		void
*Called By: 	void
*Input:		void
*OUTPUT:	void
*Return:	void
*DESCRIPTION:	1.模拟串口  接收
                2.判断一个数据的开始  IO外部中断
*Others:		
1.注意事项
  使用不同的IO口我们需要使用不同的中断向量,这点非常重要
******************************************************/
#pragma vector = EXTI2_PC_vector
__interrupt void SimUART_IO_IRQHandler(void)
{
    //第一步,关闭IO中断
    SimUART_PORT->CR2 &= ~SimUART_PIN_RX;
    
    
    /****************************************************
    1.这句话不是很明白
    2.位3——>禁止TIN2_CCR1寄存器的预装载功能,可随时写入
      TIM2_CCR1寄存器,并且新写入的数值立即起作用 
    3.位1:0 ——>00:CC1通道被配置为输出
    *****************************************************/
    TIM2-> CCMR1 &= 0x00;       //设置比较中断
    
    /*****************************************************
    1.设置比较/捕获寄存器1的值
    2.因用不到 比较/捕获寄存器1 的高位,故下高位代码可有可无
    3.下面这几行代码也不是很明白
    ******************************************************/
    //TIM2-> CCR1H = 0;
    TIM2->CCR1L = TIM2->CNTRL + ( ARRValue_9600/2 );
    if( TIM2->CCR1L >= ARRValue_9600 )
    {
        TIM2->CCR1L -= ARRValue_9600;
    }
    
    RxDataValue = 0x0000;           //变量清零
    //TIM2->CCER1 |= 0x00;          
    TIM2->SR1 &= ~TIM2_SR1_CC1IF;   //清中断标志位    
    TIM2->IER |=  TIM2_IER_CC1IE;   //使能 捕获/比较中断1  
}
/*************************************************
*Function:	SimUART_Capture_IRQHandler
*Calls:		void
*Called By: 	void
*Input:		void
*OUTPUT:	void
*Return:	void
*DESCRIPTION:	1.模拟串口 接收
                2.接收数据  比较中断
*Others:	nothing
*************************************************/
#pragma vector = TIM2_Capture_vector
__interrupt void SimUART_Capture_IRQHandler(void)
{
    //第一步,清中断标志位(防止始终进入中断)
    TIM2->SR1 &= ~TIM2_SR1_CC1IF;   //清中断标志位    
    RxDataNum++;                    //接收到的位计数
 
    
    //第二步,读IO输入 
    if( SimUART_PIN_RX == (SimUART_PORT->IDR & SimUART_PIN_RX ) )//高电平置1
    {
        RxDataValue |= ( 0x01 << (RxDataNum) );//该位 置1       
    }    
 
    
    //第三步,判断是否接收了10个位             
    if( ( 10 <= RxDataNum ) )                 //如果是则
    {
        RxDataNum = 0;
        
        TIM2->IER &=  ~TIM2_IER_CC1IE;    //1、关 比较中断    
        //STM8S没有外中断标志位,STM8L有标志位,因此暂时不需要清中断标志位
        SimUART_PORT->CR2 |= SimUART_PIN_RX;        //2、开 IO中断 
        
        
        //处理有效数据
        if(  (RxDataValue & 0x0402) == 0x0400 )
        {
            RxDataValue_Temp = ( RxDataValue >> 2 );     
            RxDataBuf[ RxData_ValidNum ] = RxDataValue_Temp;
            RxData_ValidNum ++;
            
            //=========================用户添加==============================
            
            if(  RxData_ValidNum <= 2 )          //数据头
            {
                if( (RxData_ValidNum == 2) )
                {
                    /********************************
                    测试数据为如下:
                    16 05 02 01 01 01 01 21
                    *********************************/
                    if( (RxDataBuf[0] != 0X16) || (RxDataBuf[1] != 0X05) )
                    {
                        RxDataBuf[0] = RxDataBuf[1];
                        RxData_ValidNum = 1;
                    }
                }
                return;
            }
            
            //获得了一个数据包
            if( 7 == RxData_ValidNum )
            { 
                //获得校验位
                u8 i = 0;
                u8 Check = 0 ;
                for( i = 0; i < 6; i++ )
                {
                    Check += RxDataBuf[i];
                }
                //Check = 0xFF - Check + 1;
                
                //检查校验位并做相关处理
                if( Check == RxDataBuf[6] )         //检查校验位
                {
                    UART_SendChar(0x96);
                    UART_TxBytes(TestBuf, 10);
                    
                    RxData_ValidNum = 0;            //长度清零
                }
            }
        }             
    }
}

需要注意的是,SimUART_Capture_IRQHandler的中断向量是随着IO口变化的,如果我们选用IO口的PD管脚,那么我们的中断向量为:

#pragma vector = EXTI3_PD_vector



3 结束语

至此,我们的模拟串口功能已经写完了,这些代码可以直接使用在实际的项目中。相关代码可以移步下面的地址下载使用,欢迎大家和我一起学习和交流。

源代码下载地址:点击下载。

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
版本:V1.0
时间:2015.11.06 
作者:Alan
说明:完成文章。
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<