UART的发送与接收(RS232)
- 单片机通过串口发送数据时,需要选择停止位、校验位和数据位。停止位有三种选择:1、1.5、2;校验位也有三种选择:奇校验、偶校验和无校验。数据位有四种选择:8bit,7bit,6bit,5bit。一般没有特殊要求,数据位选择8bit,刚好一字节,方便数据处理。这三个信息一般配置为:1个停止位,无校验位和8位数据。传输一帧数据为9个bit。
- PIC24HJ128GP506A Uart的相关寄存器:a.Uartx模式寄存器、b.Uartx状态和控制寄存器、c、Uartx发送寄存器、d、Uartx接收寄存器(只读)、Uartx波特率发送寄存器。波特率设置跟PIC24HJ所用的时钟有关,有具体的波特率计算公式,下面列出。
- 先说明PIC24HJ的波特率如何计算:BRGH是Uartx模式寄存器中的一位,BRGH = 0表示使用低速波特率;BRGH = 1表示使用高速波特率。高速和低速波特率计算方法稍有区别。a.BRGH = 0:
主要根据公式1反推出公式2,假设我们要设置波特率为9600,就可以反推出UxBRG的值,UxBRG是波特率发生器寄存器,公式2计算值就是写入该寄存器。b.BRGH = 1: 可以看到区别就在于16变成了4,自然波特率就比刚才的大了。将反推出来的值赋给UxBRG寄存器即可,设置好相应的波特率。 设置UxMODE寄存器,这个寄存器一般设置停止位,数据位和校验位,自动波特率的开启和选择波特率的模式(高速或低速),该寄存器控制着UART的开启。寄存器的每一位请自行查看PIC24的数据手册;接下来就是设置UxSTA寄存器,该寄存器设置发送和接收时产生中断的方式,一般选择发送或接收一个char产生中断。然后该寄存器控制着发送功能的开启。这里有必要说明:必选先开启UART功能,然后再开启TX发送功能,在程序里面的顺序很重要,否则无法使用UART的发送功能。。UART数据接收与发送寄存器很简单,一个用来读数据,另一个用来写数据。下面给出UART的配置代码,并使用XCOM上位机通信。
#pragma config BWRP = WRPROTECT_OFF
#pragma config BSS = NO_FLASH
#pragma config RBS = NO_RAM
#pragma config SWRP = WRPROTECT_OFF
#pragma config SSS = NO_FLASH
#pragma config RSS = NO_RAM
#pragma config GWRP = OFF
#pragma config GSS = OFF
#pragma config FNOSC = PRIPLL
#pragma config IESO = ON
#pragma config POSCMD = HS
#pragma config OSCIOFNC = OFF
#pragma config FCKSM = CSECMD
#pragma config WDTPOST = PS32768
#pragma config WDTPRE = PR128
#pragma config WINDIS = ON
#pragma config FWDTEN = OFF
#pragma config FPWRT = PWR128
#pragma config ICS = PGD1
#pragma config JTAGEN = ON
#include <xc.h>
#define FCY 40000000
#define BAUDRATE 9600
#define BRGVAL ((FCY/BAUDRATE)/16)-1
unsigned int i;
char RecvData;
void uart_init(void)
{
PLLFBD = 18;
CLKDIVbits.PLLPOST = 0;
CLKDIVbits.PLLPRE = 0;
OSCTUN = 0;
RCONbits.SWDTEN = 0;
while(OSCCONbits.LOCK != 1);
U1MODEbits.STSEL = 0;
U1MODEbits.PDSEL = 0;
U1MODEbits.ABAUD = 0;
U1MODEbits.BRGH = 0;
U1BRG = BRGVAL;
U1STAbits.UTXISEL0 = 0;
U1STAbits.URXISEL = 0;
IEC0bits.U1TXIE = 1;
IEC0bits.U1RXIE = 1;
IFS0bits.U1TXIF = 0;
IPC2bits.U1RXIP = 0x02;
IPC3bits.U1TXIP = 0x01;
U1MODEbits.UARTEN = 1;
U1STAbits.UTXEN = 1;
for(i = 0; i < 4160; i++){
Nop();
}
U1TXREG = 'a';
}
void led_init(void)
{
TRISDbits.TRISD10 = 0;
}
int main(void)
{
uart_init();
led_init();
while(1){
if(U1STAbits.URXDA == 1){
RecvData = U1RXREG;
U1TXREG = RecvData;
}
if(RecvData == 'o'){
LATDbits.LATD10 = 0;
}else if(RecvData == 'c'){
LATDbits.LATD10 = 1;
}
}
return 0;
}
void __attribute__((__interrupt__, __no_auto_psv__)) _U1TXInterrupt(void)
{
IFS0bits.U1TXIF = 0;
}
void __attribute__((__interrupt__, __no_auto_psv__)) _U1RXInterrupt(void)
{
IFS0bits.U1RXIF = 0;
}
代码中宏定义 #define FCY 40000000以为指令执行速度,和上面给出公式中的Fcy是一个概念,表示当前UART使用的时钟源是40MHz,这是我设置外部时钟源得到的时钟频率,有关时钟频率的设置在我上一篇时钟设置里面有提到。在这里提醒一下自己:写完代码的时候,发现上位机下发数据,单片机接收数据变量的值,并不与上位机的值一样,找了一天代码的错误,代码怎么看都没错,最后发现硬件连接上接了一个RS485转RS232的器件,它就是罪魁祸首,有时候检查错误,错误不一定在软件上,每个地方都应该排查,不能忽视。这个程序是基于硬件RS232实现的全双工串口通信。接下来在此基础上介绍RS485半双工的串口通信。
UART实现RS485通信
- 其实了解RS232和RS485通信的区别后,RS485很容易实现。它是半双工通信,意思就是说:在同一时段内,只能有一个发送方和接收方,不能同时发送和接收。RS485区分逻辑1和逻辑是计算两根信号的电压差值来判决逻辑1和逻辑0的。术语叫做差分信号,就是说RS485传输的是差分信号。使用TC485H芯片实现485通信。该芯片有一个控制传输方向的引脚D/R,这样就可以使用PIC24HJ一个普通I/O口来控制数据传输方向了。
- 这里需要注意:单片机发送数据到上位机,这时候D/R引脚默认为这个传输方向,而上位机不能下发数据到单片机,所以想要把数据传输控制权交给上位机,就需要想办法把TC485H的D/R引脚电平进行跳变。否则RS485通信就成了单工通信了。
- 这里给出一个粗略的解决办法:开启一个定时器,让单片机发送完数据,到时间后,跳板TC485H D/R引脚电平,将发送权交给上位机。上位机可以发送特定的数据再将发送权交给单片机。这样就实现了RS485半双工通讯。下面给出参考代码:
#pragma config BWRP = WRPROTECT_OFF
#pragma config BSS = NO_FLASH
#pragma config RBS = NO_RAM
#pragma config SWRP = WRPROTECT_OFF
#pragma config SSS = NO_FLASH
#pragma config RSS = NO_RAM
#pragma config GWRP = OFF
#pragma config GSS = OFF
#pragma config FNOSC = PRIPLL
#pragma config IESO = ON
#pragma config POSCMD = HS
#pragma config OSCIOFNC = OFF
#pragma config FCKSM = CSDCMD
#pragma config WDTPOST = PS32768
#pragma config WDTPRE = PR128
#pragma config WINDIS = OFF
#pragma config FWDTEN = ON
#pragma config FPWRT = PWR128
#pragma config ICS = PGD1
#pragma config JTAGEN = OFF
#include <xc.h>
#define FCY 40000000
#define BAUDRATE 9600
#define BRGVALUE ((FCY/BAUDRATE)/16)-1
void init_clk(void)
{
PLLFBD = 18;
CLKDIVbits.PLLPOST = 0;
CLKDIVbits.PLLPRE = 0;
OSCTUN = 0;
RCONbits.SWDTEN = 0;
while(OSCCONbits.LOCK != 1);
}
void timer1_init(void)
{
T1CONbits.TON = 0;
T1CONbits.TCS = 0;
T1CONbits.TGATE = 0;
T1CONbits.TCKPS = 0;
TMR1 = 0x00;
PR1 = 39999;
IPC0bits.T1IP = 0x01;
IFS0bits.T1IF = 0;
}
void relay_init(void)
{
TRISDbits.TRISD11 = 0;
}
void init_uart_rs485(void)
{
uint16_t i;
TRISFbits.TRISF6 = 0;
LATFbits.LATF6 = 0;
U1MODEbits.STSEL = 0;
U1MODEbits.PDSEL = 0;
U1MODEbits.ABAUD = 0;
U1MODEbits.BRGH = 0;
U1BRG = BRGVALUE;
U1STAbits.URXISEL = 0;
U1STAbits.UTXISEL0 = 0;
IPC3bits.U1TXIP = 0x02;
IPC2bits.U1RXIP = 0x03;
IFS0bits.U1TXIF = 0;
IFS0bits.U1RXIF = 0;
IEC0bits.U1TXIE = 1;
IEC0bits.U1RXIE = 1;
U1MODEbits.UARTEN = 1;
U1STAbits.UTXEN = 1;
for(i = 0; i < 4160; i++){
Nop();
}
}
int main(void)
{
char Recv;
init_clk();
init_uart_rs485();
timer1_init();
relay_init();
while(1){
if(U1STAbits.URXDA == 1){
Recv = U1RXREG;
}
if(Recv == 'o'){
LATDbits.LATD11 = 1;
}
if(Recv == 't'){
LATFbits.LATF6 = 1;
T1CONbits.TON = 1;
IEC0bits.T1IE = 1;
}
if(Recv == 'c'){
LATDbits.LATD11 = 0;
}
}
return 0;
}
void __attribute__((__interrupt__, __no_auto_psv__)) _U1TXInterrupt(void)
{
IFS0bits.U1TXIF = 0;
}
void __attribute__((__interrupt__, __no_auto_psv__)) _U1RXInterrupt(void)
{
IFS0bits.U1RXIF = 0;
}
void __attribute__((__interrupt__, __no_auto_psv__)) _T1Interrupt(void)
{
static uint16_t cnt = 0;
cnt++;
if(cnt == 500)
U1TXREG = 'a';
if(cnt == 1000){
LATFbits.LATF6 = 0;
T1CONbits.TON = 0;
cnt = 0;
}
IFS0bits.T1IF = 0;
}
在定时器1中断函数中一定要注意把计数器cnt的值在恰当位置清零,刚开始我没有清零,上位机下发’t’给单片机后,第一次单片机很快给上位机回复了字符a,进行第二次这样的操作,单片机回复字符a需要很长时间,大概一分钟左右,造成这个结果的原因就是cnt没有清零,cnt的数据类型是无符号整型,它的最大值为216-1,**单片机是一毫秒进一次定时器中断,**也就是说,在单片机第一次发送完字符a后,cnt的值一直朝着最大值自增,这里计算一下从零加到最大值需要多少时间,自增一次的时间是1ms,216 - 1是65535ms,折合65.5s,一分钟多一点,这还是自增到最大值,还需要加上cnt溢出后,从零开始计数到1000的时间,即1s,所以说第二次发送字符a的时间间隔为65.5s+1s=66.5秒,时间还是有点稍长。这里再次提醒自己:使用计数器一定要清零。
|