USART即为通用同步异步收发器,用于串行通信,例如其可以用于打印程序输出信息,以便于调试程序。
这里简单介绍下USART框图。
TX为发送数据的输出引脚,RX为接收数据的输入引脚,SCLK为发送器时钟输出引脚(同步模式下会用到)。其中SCLK来源于APB1总线时钟(36MHz)和APB2总线时钟(72MHz)。
这里涉及到USART数据寄存器(USART_DR)。如图10-2。
从图10-2的寄存器描述我们知道,USART_DR实际上包含了一个发送用的TDR寄存器,一个接收用的RDR寄存器。发送时,把TDR内容转移到发送移位寄存器,由发送移位寄存器一位一位发出;接收时,把收到的每一位保存到接收移位寄存器然后再转移到RDR。
USART有专门的发送器和接收器,在使用USART前需要先使能USART,将USART_CR1寄存器的UE位置1即可。而发送或接收的数据字长可选8位或9位,由USART_CR1的M位控制。
要启动数据发送,需要先使能USART_CR1的TE位,则发送移位寄存器的数据会在TX引脚输出,从低位开始发送,如果是同步模式,则SCLK也会输出时钟信号。在异步模式中,一个字符帧包含三部分:起始位+数据帧+停止位。中间部分的数据帧则是我们要发送的8位或9位数据。当使能TE位后,发送器开始会先发送一个空闲帧,然后往USART_DR写入要发送的数据。发送完成后,等待状态寄存器(USART_SR)的TC位置1后,则代表数据传输完成,同时如果USART_CR1的TCIE位置1,将产生中断。
同理,在接收时,需要置位USART_CR1的RE位,使能接收。接收完成后,会把USART_SR的RXNE位置1,同时如果USART_CR1的RXNEIE位置1,可以产生中断。
USART_DR、USART_SR和USART_CR1~3需要结合使用,相关寄存器描述可自行查阅参考手册。
USART中,波特率和比特率的值相等,所以一般不区分这两个概念。波特率越大,传输速率越快。USART的发送器和接收器使用相同的波特率,公式如下:
boud =
其中boud为波特率的值,f为USART时钟频率,USARTDIV是USART分频器除法因子,如图10-3的寄存器描述。
由描述可知,DIV_Mantissa为USARTDIV的整数部分,DIV_Fraction为USARTDIV的小数部分。那么,
USARTDIV = DIV_Mantissa + DIV_Fraction / 16
波特率的常用值有2400、9600、19200、115200。
例如,挂载在APB2总线的USART1,其有72MHz的时钟频率,即f=72MHz,假设我们需要115200的波特率,则由上面的公式可得:
115200 = 72000000 / (16*USARTDIV)
我们能得到USARTDIV=39.0625,那么
DIV_Mantissa=39=0x17,
DIV_Fraction=0.625*16=1=0x01
这时我们应该设置USART_BRR的值为0x171。
USART还支持奇偶校验。当使用校验位时,数据帧长度为8位数据帧加上1位校验位,共9位,此时USART_CR1的M位需要置1。将USART_CR1的PCE位置1可以使能校验控制。奇偶校验由硬件自动完成,在发送数据时会自动添加校验位,接收数据时会自动验证校验位。接收数据验证校验位时如果校验失败,USART_SR的PE位将会置1,同时如果USART_CR1的PEIE位置1,便能产生奇偶校验中断。
使能校验控制后,每个字符帧组成将变为:起始位+数据帧+校验位+停止位。
我们在写单片机程序的时候,在Debug时,往往要用到串口输出信息,这是会使用printf打印出我们想要的信息来,但是printf有一个弊端,就是输出打印时间较长。这样在一些对时间精度要求非常高的场合,使用printf将会带来一系列问题,这时,如果使用单片机的USART自定义一个协议,直接发送数据到上位机,将会得到我们想要的效果。下面对怎样使用USART发送数据做一个整理。
void USART1_PutChar(USART_TypeDef * USARTx,u8 ch){
USART_SendData8(USARTx,(u8)ch);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE)==RESET);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}
void USART1_PutStrLen(USART_TypeDef * USARTx,u8 *buf,u16 len){
for(;len > 0 ; len--) {
USART_SendData8(USARTx,*buf++);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE) == RESET);
}
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}
void USART1_PutStr(USART_TypeDef * USARTx,u8 *buf){
while(*buf){
USART_SendData8(USARTx,*buf++);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE) == RESET);
// 这句话有必要加,他是用于检查串口是否发送完成的标志,如果不加这句话会发生数据丢失的情况。
}
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
// 这句话有必要加,他是用于检查串口是否发送完成的标志,如果不加这句话会发生数据丢失的情况。
}
该函数就可以像printf使用可变参数。通过观察函数但这个函数只支持了%d,%s的参数,想要支持更多,可以仿照printf的函数写法加以补充。
void USART_printf ( USART_TypeDef * USARTx, char * Data, ... ){
const char *s;
int d;
char buf[16];
va_list ap;
va_start(ap, Data);
while ( * Data != 0 ){ // 判断是否到达字符串结束符
if ( * Data == 0x5c ){ //'\'
switch ( *++Data ){
case 'r': //回车符
USART_SendData(USARTx, 0x0d);
Data ++;
break;
case 'n': //换行符
USART_SendData(USARTx, 0x0a);
Data ++;
break;
default:
Data ++;
break;
}
}
else if ( * Data == '%'){
switch ( *++Data )
{
case 's'://字符串
s = va_arg(ap, const char *);
for ( ; *s; s++) {
USART_SendData(USARTx,*s);
while( USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET );
}
Data++;
break;
case 'd'://十进制
d = va_arg(ap, int);
itoa(d, buf, 10);
for (s = buf; *s; s++){
USART_SendData(USARTx,*s);
while( USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET );
}
Data++;
break;
default:
Data++;
break;
}
}
else
USART_SendData(USARTx, *Data++);
while ( USART_GetFlagStatus ( USARTx, USART_FLAG_TXE ) == RESET );
}
}
数据的头标识为“\n”既换行符,尾标识为“+”。该函数将串口接收的数据存放在USART_Buffer数组中,然后先判断当前字符是不是尾标识,如果是说明接收完毕,然后再来判断头标识是不是“+”号,如果还是那么就是我们想要的数据,接下来就可以进行相应数据的处理了。但如果不是那么就让Usart2_Rx=0重新接收数据。这样做的有以下好处:
这里我的把接收正确数据直接打印出来,也可以通过设置标识位,然后在主函数里面轮询再操作。
void USART2_IRQHandler(){
if(USART_GetITStatus(USART2,USART_IT_RXNE) != RESET){ //中断产生
USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除中断标志
Uart2_Buffer[Uart2_Rx] = USART_ReceiveData(USART2); //接收串口1数据到buff缓冲区
Uart2_Rx++;
if(Uart2_Buffer[Uart2_Rx-1] == 0x0a || Uart2_Rx == Max_BUFF_Len){ //如果接收到尾标识是换行符(或者等于最大接受数就清空重新接收)
if(Uart2_Buffer[0] == '+'){ //检测到头标识是我们需要的
printf("%s\r\n",Uart2_Buffer); //这里我做打印数据处理
Uart2_Rx=0;
}
else{
Uart2_Rx=0;//不是我们需要的数据或者达到最大接收数则开始重新接收
}
}
}
}
串口空闲中断,一帧数据过来中断进入一次且接收的数据时候是DMA来搬运到指定缓冲区(程序中是USART1_RECEIVE_DMABuffer数组),不占用CPU时间。
#define DMA_USART1_RECEIVE_LEN 18 //根据需要设置
void USART1_IRQHandler(void){
u32 temp = 0;
uint16_t i = 0;
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET){
USART1->SR;
USART1->DR; //这里我们通过先读SR(状态寄存器)和DR(数据寄存器)来清USART_IT_IDLE标志
DMA_Cmd(DMA1_Channel5,DISABLE);
temp = DMA_USART1_RECEIVE_LEN - DMA_GetCurrDataCounter(DMA1_Channel5); //接收的字符串长度=设置的接收长度-剩余DMA缓存大小
for (i = 0;i < temp;i++){
Uart2_Buffer[i] = USART1_RECEIVE_DMABuffer[i];
}
//设置传输数据长度
DMA_SetCurrDataCounter(DMA1_Channel5,DMA_USART1_RECEIVE_LEN);
//打开DMA
DMA_Cmd(DMA1_Channel5,ENABLE);
}
}