PIC单片机软件异步串行口实现技巧单片机软件异步串行口实现技巧单片机软件异步串行口实现技巧单片机软件异步串行口实现技巧
关键词:PIC单片机,异步串行通讯,软件 UART
在用单片机开发各种嵌入式应用系统时,异步串行通讯是经常要用到的一种通讯模式,很多
应用中还
要求
对教师党员的评价套管和固井爆破片与爆破装置仓库管理基本要求三甲医院都需要复审吗
实现多路异步串行通讯。大家平时熟悉的各种厂家的单片机,绝大部分片上只
提供一个硬件 UART模块,利用它可以方便实现一路串行通讯。PIC系列单片机也不例外,
在其丰富的产品家族成员中,除高端系列(PIC17/18)一些型号片上带有两路硬件 UART
模块外,其他大部分型号片上只有一路 UART,一些低端廉价的 PIC单片机甚至还不带硬件
UART。为了提高系统的性能价格比,就要求设计工程师用软件增加实现一路或多路异步串
行通讯。很多工程师对用软件实现的 UART 在可靠性和效率方面持怀疑态度,其实关键问
题是看软件采用何种方式来实现可靠的 UART功能。
在讨论具体实现方式前我们先来简单回顾一下异步串行通讯的格式定义。发送一个完整的字
节信息,必须有“起始位”、“若干数据位”、“奇偶校验位”和“停止位”;必须定义每位信
息的时间宽度――每秒发送的信息位个数即为“波特率”,单片机系统中常用的波特率从
300~19200BPS,当波特率为 1200BPS 时,每个信息位的时间宽度为 1/1200≈833us;无
数据通讯时,数据线空闲状态应该是高电平;“起始位”为低电平;数据位低位先发,后跟
奇偶校验位(若有);“停止位”为高电平。如图(1)所示。
按上图最基本的异步串行通讯时序,软件实现 UART 在不同架构的单片机上有多种方法。
其中数据接收是关键,因异步通讯没有可参照的时钟信号,发送方随时都可能发送数据,任
何时刻串行数据到来时,系统都应该及时准确地接收;比较而言,本机发送串行数据相对容
易,只要对发送出去的电平做持续时间的定时即可。按不同的接收技巧并针对 PIC 单片机
的特点,这里介绍两种常用且十分可靠的方法。
一、三倍速采样法
三倍速采样法顾名思义就是以三倍于波特率的频率对接收引脚Rx进行采样,保证检测到“起
始位”,又可以调整采样的时间间隔,将有效数据位的采样点控制在码元的中间 1/3处,最
大限度地减少误码,提高接收的准确性。我们把图(1)的起始位和部分数据位放大如图(2),
把每个信息位分成三等份,每等份的时间宽度设为 Ts,以方便分析。
起
始
位
空
闲
状
态
数
据
位0
数
据
位1
数
据
位2
数
据
位3
数
据
位4
数
据
位5
数
据
位6
数
据
位7
校
验
位
停
止
位
空
闲
状
态
图(1)
起始位 空闲状态 数据位 0 数据位 1 数据位 2
图(2)
S0 S1 S2 Dx Ds Dx Dx Ds Dx Dx Ds Dx
空闲
状态
数据
采样
停止位
确认
起始位
确认 Rx=1
Rx=0
4Ts
时间间隔
3Ts
时间间隔
数据位
结束
Rx=1
帧
错误
Rx=0
图(3)
以三倍频对信息位进行采样时,每个信息位都将可能被采样到三次。当处于空闲状态并检测
起始位时,最早检测到起始位低电平的时刻必将落在 S0阴影区,虽然每次具体的采样点会
在此 S0阴影区随机变化;检测到起始位低电平后,间隔 4*Ts时间,正好是第一位数据位
的中间 1/3处(图 2中 Ds阴影区);此后的数据位、校验位和停止位的采样间隔都是 3*
Ts,所有采样点均落在码元的中间 1/3处,采样数据最可靠。
PIC单片机采用此法实现软件 UART时,硬件上只要任意定义两个 IO引脚,分别初始化成
输入(串行数据接收)和输出(串行数据发送)即可;软件上只要实现定时采样,定时时间
间隔在中档以上有中断机制的单片机上可以用不同的定时器(TMR0、TMR1、TMR2 等)
通过定时中断实现,在低挡无中断的 PIC单片机上可以控制每次主循环所耗的时间来实现。
对于 1200BPS波特率,码元宽度为 833us,采样时间间隔即为 278us。整个串行接收或发送
是一个过程控制问题,用状态机方式实现最为高效简易。图(3)给出了串行接收的参考状
态机转移过程。
下面是简单的 C语言参考源程序。此段程序实现 1200BPS全双工串行通讯,1位起始位,8
位数据位,无校验位,1位停止位,没有帧错误等判别。编译环境为 HITECH-PICC编译器
V8.00PL4或更高版。
#include
//PIC单片机通用头文件,实际型号为 16F84
__CONFIG(XT | PROTECT | PWRTEN | WDTEN);//程序中设定配置信息
//===========================
//定义软件 UART发送/接收引脚
//===========================
#define RX_PIN RB0 //串行接收脚
#define TX_PIN RB1 //串行发送脚
//===========================
//定义软件 UART状态机控制字
//===========================
#define RS_IDLE 0 //空闲
#define RS_DATA_BIT 1 //数据位
#define RS_STOP_BIT 2 //停止位
#define RS_STOP_END 3 //停止位结束
//===========================
//定义软件 UART采样频率
//===========================
#define OSC_FREQ 4000 //单片机工作频率(单位:KHz)
#define BAUDRATE 1200 //通讯波特率
#define TMR0PRE 2 //TMR0预分频比 1:2
#define TMR0CONST 117 //256 - OSC_FREQ*1000/TMR0PRE/4/(BAUDRATE*3)
//===================================================================
//定义函数类型
void UART_Out(void);
void UART_In(void);
//===================================================================
//定义位变量
bit rsTxBusy; //串行发送忙标志
//定义串行发送的数据结构
struct {
unsigned char state; //发送状态机控制单元
unsigned char sliceCount; //波特率控制
unsigned char shiftBuff; //字节数据发送移位寄存器
unsigned char shiftCount; //字节数据发送移位计数器
} rsTx;
//定义串行接收的数据结构
struct {
unsigned char state; //接收状态机控制单元
unsigned char sliceCount; //波特率(采样点)控制
unsigned char shiftBuff; //字节数据接收移位寄存器
unsigned char shiftCount; //字节数据接收移位计数器
unsigned char dataBuff[8]; //接收数据 FIFO缓冲队列
unsigned char putPtr, getPtr;//FIFO队列存放/读取指针
} rsRx;
//用于串行发送的变量定义
unsigned char outBuff[10]; //发送队列
unsigned char outPtr, //发送队列指针
outTotal, //发送的字节总数
chkSum; //发送的校验码
//=====================================================================
//主程序
//=====================================================================
void main(void)
{
PORTA = 0;
PORTB = 0;
TRISB = 0b01; //输入输出定义
OPTION = 0b10000000; //TMR0选择内部指令周期计数
//TMR0预分频 1:2
rsRx.state = RS_IDLE; //初始化接收状态
rsTxBusy = 0; //发送空闲
INTCON = 0b00100000; //T0IE使能
GIE = 1; //打开中断
while(1) { //程序主循环
asm("clrwdt"); //清看门狗
UART_In(); //接收串行数据
UART_Out(); //发送串行数据
}
}
//=====================================================================
//查询在接收 FIFO队列中是否有新数据到
//然后解读数据
//=====================================================================
void UART_In(void)
{
unsigned char data1;
if (rsRx.putPtr==rsRx.getPtr)
return; //如果读取和存放的指针相同,则队列为空
data1 = rsRx.dataBuff[rsRx.getPtr]; //读取 1个数据字节
rsRx.getPtr++; //调整读取指针到下一位置
rsRx.getPtr &= 0x07; //考虑环形队列回绕
//此处为数据解读分析,略
}
//=====================================================================
//软件 UART发送数据
//数据在 outBuff中,outTotal为总字节数
//=====================================================================
void UART_Out(void)
{
if (rsTxBusy==1)
return; //正处于移位发送忙
//可以发送新数据
if (outTotal) { //如果有字节要发送
rsTx.shiftBuff = outBuff[outPtr++]; //取字节到发送移位寄存器
rsTxBusy = 1; //置发送忙标志,启动发送
outTotal--; //字节计数器减 1
}
}
//===================================================================
//中断服务程序
//===================================================================
void interrupt isr(void)
{
//利用 TMR0 定时中断实现全双工软件 UART
if (T0IE && T0IF) {
T0IF = 0; //清 TMR0中断标志
//实现串行接收 RX 状态机控制
switch (rsRx.state) { //判当前接收状态
case RS_IDLE:
//当前状态为"空闲", 唯一要做的就是判"起始位"出现
if (RX_PIN==0) { //如果接收到低电平
rsRx.sliceCount = 4; //准备 4*Ts时间间隔
rsRx.shiftCount = 8; //总共接收 8位数据位
//改变此数值可以实现任意位数的数据接收
rsRx.state = RS_DATA_BIT; //切换到数据位接收状态
}
break;
case RS_DATA_BIT:
//当前状态为"数据接收"
if (--rsRx.sliceCount==0) { //等采样时间到
rsRx.shiftBuff >>= 1; //接收移位寄存器右移 1位
if (RX_PIN) rsRx.shiftBuff|=0x80; //保存最新收到的数据位
rsRx.sliceCount = 3; //下次采样间隔为 3*Ts
if (--rsRx.shiftCount==0) { //已经收到 8位数据位?
//保存数据字节到 FIFO缓冲队列
rsRx.dataBuff[rsRx.putPtr] = rsRx.shiftBuff;
//队列存放指针调整,最多 8个字节缓冲
rsRx.putPtr = (rsRx.putPtr+1) & 0x07;
//转去下个状态,判停止位
rsRx.state = RS_STOP_BIT;
}
}
break;
case RS_STOP_BIT:
//当前状态为停止位判别(此程序没有判别)
if (--rsRx.sliceCount==0) { //等采样时间到
//此处可以判 RX_PIN是否为 1
rsRx.state = RS_IDLE; //复位接收过程
}
break;
default:
//异常处理
rsRx.state = RS_IDLE; //复位接收过程
}
//实现串行发送 TX 状态机控制
switch (rsTx.state) { //判当前发送状态
case RS_IDLE: //发送起始位
if (rsTxBusy) { //如果发送启动
TX_PIN = 0; //发出起始位低电平
rsTx.sliceCount = 3; //持续时间 3*Ts
rsTx.shiftCount = 8; //数据位数为 8位
rsTx.state = RS_DATA_BIT; //转去下一状态
} else TX_PIN = 1; //如果没有数据发送则保证数据线为空闲
break;
case RS_DATA_BIT: //发送 8位数据位
if (--rsTx.sliceCount==0) { //码元宽度定时到
if (rsTx.shiftBuff & 0x01)//看数据位是 0还是 1
TX_PIN = 1; //发送 1
else
TX_PIN = 0; //发送 0
rsTx.shiftBuff >>= 1; //准备下次数据位发送
rsTx.sliceCount = 3; //数据位宽度为 3*Ts
if (--rsTx.shiftCount==0) {
//8位数据位发送结束,转去发送停止位
rsTx.state = RS_STOP_BIT;
}
}
break;
case RS_STOP_BIT: //发送 1位停止位
if (--rsTx.sliceCount==0) { //等数据位发送结束
TX_PIN = 1; //发送停止位高电平
rsTx.sliceCount = 9; //持续宽度 9*Ts
//额外考虑字节连续发送的时间间隔
rsTx.state = RS_STOP_END; //转停止位宽度延时
}
break;
case RS_STOP_END: //等待停止位时间宽度结束
if (--rsTx.sliceCount==0) { //如果停止位结束时间到
rsTxBusy = 0; //一个字节发送过程结束,清发送忙标志
rsTx.state = RS_IDLE; //复位发送过程
}
break;
default:
//异常处理
rsTx.state = RS_IDLE; //复位发送过程
}
TMR0 += TMR0CONST; //重载 TMR0,实现下次定时中断
}
}
在上面这段程序中,关键部分是 TMR0的中断服务,TMR0每隔 278us左右中断一次,TMR0
的中断响应即为软件 UART接收和发送全双工通讯过程的实现。通过 Hitech-PICC高效的代
码编译后,约有 150条单字指令代码,整个中断服务平均用约 35个左右指令周期,即实现
一路软件 UART在 4MHz工作频率下占用MCU约 12%的运行带宽。理论上,只要保证MCU
留有足够的运行带宽给其它任务,在此中断服务程序内把接收和发送的代码再复制一份或多
份(数据结构独立),即可实现多路软件 UART。当然,如果每路的波特率不同,采样频率
必须是最高波特率的三倍,不同波特率的采样点间隔独立调整。
此法最大的好处是软硬件配置极其灵活:接收发送的管脚可以任意定义;采样定时可以用不
同的定时器实现;利用同一个定时采样可以方便地实现多路软件 UART 等。缺点是不管有
无数据通讯,始终占用MCU运行带宽;串行通讯的波特率不能太高,4MHz工作的 PIC单
片机一般能实现 2400BPS 的全双工通讯。当然可以通过提高 MCU 的振荡频率来实现高波
特率通讯,当 PIC单片机工作在 20MHz,实现 9600BPS绰绰有余。
二、起始位中断捕捉、定时采样法
实现此法的硬件条件是 PIC 单片机有外部脉冲下降沿中断触发功能,在中档以上 PIC 单片
机中有 RB0/INT外部中断脚;CCP1/CCP2脉冲沿捕捉脚,PORTB的第 4/5/6/7电平变
化中断脚等都可以满足。另外需配备一个定时器,以定时中断方式对接收码元正确采样,或
发送串行数据流。其关键的异步接收工作原理简介如下:
设串行数据位宽度为 Td。起始位到来时刻(图 3 A点)的下降沿触发一个中断并立即响应
该中断;在此中断服务中立即关闭本中断使能位(后续的数据流变化无需触发中断),开启
定时器使其在 1.5Td后产生定时中断用于采样第一个数据位(确保 S0采样点落在数据位的
中心位置处),在处理下降沿中断服务的最后再检测接收端是否还是 0电平以区分窄脉冲干
扰;在 S0点采样到第一个数据位后的所有采样间隔都是 1Td,直到收到停止位后,关闭定
时器中断,重新开放下降沿捕捉中断,准备接收下一个字节。
异步数据接收和发送的状态机控制流程除了起始位判断和定时时间
参数
转速和进给参数表a氧化沟运行参数高温蒸汽处理医疗废物pid参数自整定算法口腔医院集中消毒供应
设置和前述方式不
同外,其它几乎一样,此处不再重复。
此法的好处是可以实现较高的通讯波特率,对于通讯不是很频繁的系统,此软件 UART 几
乎不耗MCU运行带宽,9600BPS接收或发送在 4MHz运行的 PIC单片机上即可轻松实现;
另外由于下降沿中断可以唤醒处于睡眠的单片机,故极易实现通讯唤醒的功能。缺点是不能
全双工通讯(除非另外单独用一个定时器实现发送定时),异步接收的管脚必须有下降沿触
发中断的能力。
上面介绍的两种方法在实际产品设计中都得到了很好的验证,最典型的是红外线自动抄
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
系
统。该系统要求收发均为 38KHz红外调制,串行数据 1200BPS半双工通讯。用软件实现此
UART,并充分利用 PIC单片机 CCP模块的脉宽调制 PWM输出 38KHZ载波时,在单片机
外除了一个一体化红外接收头和一个红外发射二极管,无需其它任何外围器件,即可完成所
有设计要求,最大程度地减化了硬件设计,降低了成本,提高了系统的可靠性和性能价格比。
起始位 空闲状态 数据位 0 数据位 1 数据位 2
图(3)
A S0 S3 S2 S1
1.5 Td 1 Td 1 Td 1 Td