PIC单片机下降沿中断脚与定时器扩展串口接收
这是使用PIC单片机的下降沿中断脚,结合定时器扩展串口接收。
一、串口发送的原理
1、假定串口波特率为9600位/秒,则传送一位值的时间为:
1000000(us)/9600=104.16us。
2、假定要发送的数据data,其位格式如下:
bit7,bit6,bit5,bit4,bit3,bit2,bit1,bit0
bit7是data的最高位,bit0是data的嘴低位,则:
data= bit7 * 2^7 + bit6 * 2^6 + bit5 * 2^5 + bit4 * 2^4
+ bit3 * 2^3 + bit2 * 2^2 + bit1 * 2^1 + bit0 * 2^0
3、串口在发送数据时,总是先发送起始位start位,start是个“低电平位值”,所以TXD脚由高到低发生跳变,其低电平的时间为104.16us。接着发送数据data的bit0,bit1,bit2,bit3,bit4,bit5,bit6,bit7,最后发送停止位stop位,stop是个高电平位值,其高电平的时间为104.16us。
4、对于8位数据通讯(N8),则有10位值要被发送出去,即发送顺序如下:
start + bit0 + bit1 + bit2 + bit3 + bit4 + bit5 + bit6 + bit7 + stop
我们扩展模拟接收串口,主要是讲这个通讯。
5、对于9位数据通讯(N8、1),则有11位值要被发送出去,即发送顺序如下:
start + bit0 + bit1 + bit2 + bit3 + bit4 + bit5 + bit6 + bit7 + TX8 + stop
上面的TX8有3种意思:
① TX8是从机地址/数据识别位,TX8=1,表示data的值为地址,TX8=0,表示data值为数据。
② TX8是奇偶校验位。
对于偶校验,校验位就定义为1。对于奇校验,校验位就定义为0。
若是奇校验,则发送数据的格式为:
start + bit0 + bit1 + bit2 + bit3 + bit4 + bit5 + bit6 + bit7 + 0 + stop
若是偶校验,则发送数据的格式为:
start + bit0 + bit1 + bit2 + bit3 + bit4 + bit5 + bit6 + bit7 + 1 + stop
奇偶校验能够检测出信息传输过程中的部分误码(1位误码能检出,2位及2位以上误码不能检出),同时,它不能纠错。但由于其实现简单,仍得到了广泛使用。
③ TX8是停止位,这样就有两个停止位(stop1,stop2)。也就是我们通常所说的8位数据通讯(N8)这样发送数据的格式为:
start + bit0 + bit1 + bit2 + bit3 + bit4 + bit5 + bit6 + bit7 + stop1 + stop2
注意:stop1=1,stop2=1。
使用两个停止位,硬件串口发送位数为11位,但硬件串口接收位数是10位的,也是可以收到正确的数据。
二、模拟串口接收的原理:
通过对串口发送的数据格式,分析可以知道,每一位值的发送时间都要占用104.16us。所以串口在接收时,总是先收到start位,接着是bit0、bit1、bit2 、bit3、bit4、bit5、bit6、bit7,最后是stop位。我们可以设想模拟硬件串口“位采集”情况,假如“位采集”发生在104.16us的中间位置,是不是模拟串口就可以接收数据了呢?显然是可行的。我的想法是这样的:
- 当start到来时,下降沿产生中断,进入“下降沿中断服务程序”,立即打开定时器,并设置定时器在第52us时发生中断,将bit_counter=1,再设置不使能下降沿中断,退出“下降沿中断服务程序”。
- 当定时器产生中断时,设置定时器为每104us时间产生一次中断。将收到的位值temp_bit右移入data中。
- bit_counter左移一位。
- 如果bit_counter=0,则将保存接收到数据data,关闭定时器,使能下降沿中断,本次接收完成。
注意:停止位不接收。若要接收TX8位,在内部指令周期比较高的情况下是可以的。
- 退出“定时器中断服务程序”。
注意:
硬件串口发送位数为11位/10位,模拟串口接收位数是9位,好处是串口接收中断服务程序的执行时间被加宽了。
三、流程图设计:
1、波特率为9600bps,下降沿中断服务程序流程图:
2、波特率为9600bps,不接收TX8的位值,定时器中断服务程序流程图:
本流程图在指令周期小于等于0.5us的情况下,实现是可以的。
3、波特率为9600bps,接收TX8的位值,定时器中断服务程序流程图:
本流程图在指令周期小于等于0.25us的情况下,实现是可以的。
4、在指令周期为0.5us的情况下,举例:
#define Simulate_RXD2_Pin PIN_A2 //定义模拟接收中断脚;
#define SSP2_pwr PIN_C2
#byte TRISA=getenv("sfr:TRISA") //定义数据方向寄存器TRISA的地址;
#bit TRISA2 = TRISA.2
#byte PORTA=getenv("sfr:PORTA") //定义数据方向寄存器TRISA的地址;
#bit RA2 = PORTA.2
#byte TMR1H=getenv("sfr:TMR1H") //定义Timer1高8位计数器TMR1H的地址;
#byte TMR1L=getenv("sfr:TMR1L") //定义Timer低8位计数器TMR1L的地址;
#byte T1CON=getenv("sfr:T1CON") //定义Timer1控制寄存器T1CON的地址;
#bit TMR1ON = T1CON.0
#byte PIR1=getenv("sfr:PIR1") //定义PIR1的地址;
#bit TMR1IF = PIR1.0
#byte INTCON=getenv("sfr:INTCON") //定义INTCON3的地址;
#bit INTIF = INTCON.1 //INT脚中断标志位
///CPU的晶振为8MHz///
#define FOSC2 8000000L //模拟串口执行要用43.5us~53.5us;
#define RXD2_Baud 9600L
#define Load_us52 256-(FOSC2>>3)/RXD2_Baud+80 //设置Timer1为52us中断一次;
#define Load_us104 256-(FOSC2>>2)/RXD2_Baud+6 //设置Timer1为104us中断一次;
#INT_EXT
void INT0_interrupt_program()
{ TMR1H=0xFF;
//TMR1L=Load_us52;
TMR1L=INT_value;
TMR1ON=1;
RXD2_bit_cnt=1; //移位计数器,RXD2_bit_cnt=0表示接收完成;
clear_interrupt(INT_EXT);
//enable_interrupts(INT_EXT); //使能外部中断0
disable_interrupts(INT_EXT); //不使能外部中断0
}
//在指令周期为0.5us时,模拟串口执行要用43.5us~53.5us,
//其他硬件中断的服务程序执行时间要小于50.5us;
#INT_TIMER1
void Timer1_Counter()
{ unsigned int8 temp;
RXD2_temp_bit_value=RA2; //记忆RA2的电平;
//RXD2_temp_bit_value=~RXD2_temp_bit_value;
TMR1H=0xFF;
TMR1_temp_value = TMR1L;
TMR1_temp_value=TMR1_temp_value+TMR1_value;
TMR1L = TMR1_temp_value;
shift_right( &RXD2_Receiver_temp_Data,1, RXD2_temp_bit_value );
/*
RXD2_Receiver_temp_Data=RXD2_Receiver_temp_Data>>1;
if(RXD2_temp_bit_value)
{ RXD2_Receiver_temp_Data=RXD2_Receiver_temp_Data|0x80;
}
else RXD2_Receiver_temp_Data=RXD2_Receiver_temp_Data&0x7F;
*/
///0.5us指令周期,模拟串口执行要用43.5us~53.5us,TX8位和停止位不能被接收///
if(RXD2_bit_cnt==0x00) //接收完成,停止位不接收,直接处理数据;
{ TMR1ON=0; //接收完成;
SSP2_RCV_buffer[SSP2_in]=RXD2_Receiver_temp_Data; //保存数据;
temp=SSP2_in; //将接收缓冲区的下标值暂存放在temp中;
SSP2_in++; //SSP2_RCV_buffer[]下标加1;
if(SSP2_in>=SSP2_RCV_buffer_Size-1) SSP2_in=0;
//若接收缓冲区满,则将SSP2_in设置为0;
if(SSP2_in==SSP2_out) SSP2_in=temp;
//若"输入下标值"同"输出下标值"相等,则将"输入下标值"改为原值;
delay_us(57); //跳过对TX8的检测;
clear_interrupt(INT_EXT); //清除下降沿标志位;
enable_interrupts(INT_EXT); //使能外部中断0
}
RXD2_bit_cnt=RXD2_bit_cnt<<1;
}
四、在没有串口调试的情况下,用示波器看串口波形,使用人工方式读取接收到的数据。
1、串口发送一个字节的波形图如下:
T的单位是us,可以看处,串口发送的数据是0x55。
2串口发送一个字节的波形图如下:
T的单位是us,可以看处,串口发送的数据是0xAA。
注意:串口在发送时,总是先发最低位,因此,根据波形图读数据,在有些公司面试时,可能遇到,因为他们想知道你对串口掌握的是不是很精通。其实,回答错了,没有关系,但是,你可能会失去这次机会。
3、看串口发送的波形,主要是为了测量波特率是不是正确。
在上图中,波特率是多少呢?
通过:T=1000000(us)/BPS;可以计算波特率的值。
4、没有示波器时,我们可以将数据发给PC机,通讯终端或是串口调试助手,看看我们发送的数据是不是正确,由此而来判断我们的波特率设置是不是正确。
只有先设置好波特率,才能进行正确的数据交换,否则,串口接收到的数据都是没有意义的。
五、编写串口通讯的经验交流:
1、只有先设置好波特率,才能进行正确的数据交换,否则,串口接收到的数据都是没有意义的。
2、确认能否进入串口接收中断服务程序。
我的做法是,当串口中断时,将接收到的数据保存接收缓冲区RCV_Buffer[]中。在主程序中查询RCV_Buffer[]中的数据是否有新数据被收到,若收到,通过串口发送脚发送出去,用示波器查看,或是发给PC机。
//全局变量定义
#define GSM_RCV_buffer_Size 600 //定义GSM接收缓冲区的长度;
unsigned int8 GSM_RCV_buffer[GSM_RCV_buffer_Size]; //用来存放硬件串口接收到的数据;
unsigned int16 GSM_in = 0; //GSM接收缓冲区的输入下标;
unsigned int16 GSM_out = 0; //GSM接收缓冲区的输出下标;
#define GSM_RCV_Flag (GSM_in!=GSM_out)
//GSM_RCV_Flag=1,表示GSM的接收缓冲区有新的数据输入;
//函数功能:GSM串口的接收中断服务函数;
#int_rda2 HIGH
void GSM_Serial_Interface_Interrupt_Program()
{ unsigned int8 temp;
restart_wdt();
temp=fgetc(GSM); //从GSM串口读取一个字节;
GSM_RCV_buffer[GSM_in]=temp;
temp=GSM_in; //将接收缓冲区的下标值暂存放在temp中;
GSM_in=GSM_in+1; //修改GSM_in的值,为下1次接收做准备;
if(GSM_in>=GSM_RCV_buffer_Size-1) GSM_in=0;
//若接收缓冲区满,则将GSM_in设置为0;
if(GSM_in==GSM_out) GSM_in=temp;
//若"输入下标值"同"输出下标值"相等,则将"输入下标值"改为原值;
//GSM_RCV_buffer[]满了;
}
3、中断服务程序写得短小的原因:
在做中断服务程序时,力求所有的中断服务程序越短小越好。当一个中断服务程序太长时,就会影响其它中断服务程序的执行质量。滥用中断,CPU运行时,会出现各种莫名其妙的错误。如果想用好所有的中断服务程序,就将中断服务程序写得越短小越好,分配好中断服务程序的执行时间大小,就可以了。
例如:
① 如果一个程序,有串口中断,还有其他中断,并且串口中断服务程序执行时间为1000(ms)/波特率,则其它中断的服务程序的总执行时间就要小于1000(ms)*8/波特率。这就要求在分配中断服务程序的执行时间时,就要我们注意了。
② 如果一个程序,只有一个串口接收中断服务程序程序,没有其他中断了,则串口中断服务程序执行时间要小于1000(ms)*9/波特率。
也就是是说在下一个数据到来之前,必须读一次串口,才不会导致串口接收错误。
通过上面的举例说明,我们知道为什么要将中断服务写得短小的原因了吧。
不要过分追求完美,因为在我们的生活中,很多东西都不是完美的。希望大家一起进步。.
作者:LaoZhangGong123