串口在产品应用中很常见,但是单片机的默认带的串口往往比较少,有时候就会出现串口不够用,所以就想着能不能用普通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单片机模拟串口功能实现