有些产品需要用到多个UART串口,而单片机UART数量不足,这时需要用IO口模拟UART通讯。
本文采用IO口+定时器+外部中断的方法,模拟UART时序,实现串口接收和发送的功能,程序已经在量产产品上应用。
实现原理:
1、发送部分,通过定时器控制波特率,改变IO口电平来发送数据,比较简单。
2、接收部分,通过外部中断,识别起始位,定时器控制波特率,使IO口恰好在电平信号的中间位置读取数据,减少其他中断的影响,大大降低误码率。
注意事项:
1、目前本程序只适用于接收数据定长的情况,不定长数据不适用。
2、在串口发送和接收过程中,尽量减少中断,频繁的中断打断程序运行,会产生误码。
3、每个数据包帧头是0xAA,帧尾0x0D,校验值是除了帧尾所有数据的累加和,帧头和帧尾可以根据用户自己的需求修改。
全部代码如下:
//UART波特率是9600,1 bit≈104us,
//1个起始位,1个停止位,8个数据位
//每个数据包帧头是0xAA,帧尾0x0D,校验值是除了帧尾所有数据的累加和。
//通讯方式采用一问一答形式,每500ms发送一次数据并接收应答数据,
//接收数据总共22byte,22*(1+1+8)*104us=22.88ms,即接收每帧数据的时间长度.
#define d_head 0xAA
#define d_tail 0x0D
volatile bit RXFLAG = 0;//外部中断标志位,在下降沿中断中置1;
u8 Rx_Tim=0; //接收计时器,每次接收到数据时重置倒计时。倒计时为0则认为上一帧结束,
//开始接收下一帧数据,复位接收计数器。
#define Set_Clear_RxTim() Rx_Tim=50 //50ms倒计时,我的产品接收数据时间长度22.88ms
//理论上23ms就可以了,不过要考虑时序存在误差。
void Uart_IO_GPIO_Init(void)//模拟串口的IO口初始化
{
PORTA|= 0B00000001; //Tx输出1
TRISA &= 0B11111110; //PA0-Output_Tx PA1-Input_Rx
WPUA |= 0B00000010; //PA1上拉电阻
}
void WaitTF0(void)//延时等待控制UART时序
{
while(T2UIE);//定时器开始计时后,在定时器中断中清零T2UIE
T2UIE=1;
}
void WByte(u8 input)//发送1 Byte
{
u8 i=8;
TXIO=0; //Tx置0,发送起始位
T2UIF = 1; //清零定时器2中断标志位
T2CEN =0; //失能定时器2
TIM2CNTRH=0;//清零定时器2计数值高位
TIM2CNTRL=0;//清零定时器2计数值低位
T2CEN=1; //启动定时器2
T2UIE =1; //使能定时器2
WaitTF0(); //等待定时器中断,等待时间取决于定时器2的装载值。这里的定时时间是104us
while(i--)
{
if(input&0x01) //逐个bit发送,先传低位
TXIO=1;
else
TXIO = 0;
WaitTF0(); //等待延时104us
input=input>>1;
}
TXIO=1; //发送结束位
WaitTF0();
T2UIE=0; //发送1 byte完成了
}
u8 RByte()//接收1 Byte
{
u8 Output=0;
u8 i=8;
T2UIF = 1; //清零定时器2中断标志位
T2CEN=0; //失能定时器2
TIM2CNTRH=0; //清零定时器2计数值高位
TIM2CNTRL=0; //清零定时器2计数值低位
TIM2ARRH =0x09; //设定时器2装载值
TIM2ARRL =0x00; //起始位1.5bit(156us)后再读取电平,确保在第一bit中间位置读取数据
T2CEN=1; //启动定时器2
T2UIE =1; //使能定时器2
WaitTF0(); //等待104us+52us=156us,刚好在第一个bit的中间位置读取数据
T2CEN=0; //失能定时器2
TIM2ARRH =0x06; //重新装载104us,确保在后续的bit中间位置读取数据
TIM2ARRL =0x80;
T2CEN=1; //使能定时器2
while(i--)
{
Output =Output>>1;
if(RXIO)
{
Output |=0x80; //先接收收低位
}
WaitTF0(); //位间延时104us
}
T2UIE=0; //停止Timer0
return Output; //返回接收到的byte
}
u8 Rx_buff[Rx_size]={0};
volatile bit B_Rx_decode;
void UART_IO_RxProcess(vu8 *buff,vu8 size)//接收size长度的数据放在buff中
{
u8 rdata;
static u8 cnt,checksum;
if(RXFLAG) //RXFLAG在下降沿外部中断函数中置1
{
rdata=RByte(); //有下降沿中断说明有起始位,开始接收数据
if(!Rx_Tim)//倒计时结束,说明超过50ms没有接收到数据,清零接收计数值,开始接收下一帧.
{
cnt=0;
checksum=0;
}
Set_Clear_RxTim();//接收到数据,复位倒计时
buff[cnt]=rdata;
if(cnt<(Rx_size-2))
checksum+=buff[cnt];//计算数据校验和,与接收到的校验和对比。
if(cnt==(Rx_size-1))//计数值与数据长度相等,说明接收完一帧数据。
{
if((buff[0]==d_head)&&(buff[Rx_size-1]==d_tail)&&(checksum==buff[Rx_size-2]))
B_Rx_decode=1;//帧头,帧尾,校验和,全部正确,接收数据有效。
}
cnt++;
EPIE0 = 0B00000010;//接收完一个数据,重新使能外部下降沿中断,准备接收下一个数据
RXFLAG = 0; //标志位清零
}
}
u8 Tx_buff[Tx_size]={0};
void UART_IO_TxProcess(vu8 *buff,vu8 size)//发送数据
{
u8 i;
for(i=0;i<size;i++)
{
WByte(buff[i]);
}
}
定时器2中断函数,外部中断函数和定时器4中断函数
void TIMER2_INPT(void)//定时器2中断函数
{
if(T2UIE && T2UIF)
{
T2UIF = 1;//写1清零标志位
T2UIE=0; //清零T2UIE,与WaitTF0()配合,等待延时。
}
}
void IO_INPT(void)//外部中断函数 下降沿
{
if(EPIF0 & 0x02)
{
EPIF0 |= 0x02;//写1清零标志位
NOP();NOP();NOP();NOP();//进入中断后延时等待,滤除干扰
NOP();NOP();NOP();NOP();
if(RXIO == 0)//等待一段时间后,管脚仍然低电平,则认为有下降沿产生
{
EPIE0 = 0B00000000; //禁止PA1下降沿中断
RXFLAG = 1;
}
}
}
void TIMER4_INPT(void) 定时器4中断,1ms中断一次
{
static u16 TIM_500ms;
if(T4UIE && T4UIF) //中断标志
{
T4UIF = 1; //写1清零标志位
TIM_500ms++;
if(Rx_Tim)//接收倒计时。
Rx_Tim--;
if(TIM_500ms>499)
{
TIM_500ms=0;
B_uart_500ms=1;//500ms发送一次询问数据,并接收应答数据
}
}
}
main.c
void main (void)
{
Timer2_Init();
Timer4_Init();
Uart_IO_GPIO_Init();
IO_INT_INITIAL();
while(1)
{
if(B_uart_500ms)//每500ms发送一次询问数据,并等待接收
{
B_uart_500ms=0;
UART_IO_TxProcess(Txbuff,Txsize);
}
UART_IO_RxProcess(Rxbuff,Rxsize);//必须在while(1)里面一直运行,等待接收
if(B_Rx_decode)//接收到的数据正确
{
B_Rx_decode=0;
//对数据进行处理
}
}
}