一、基本原理
UART的全称是Universal Asynchronous Receiver and Transmitter,即通用异步发送和接收器。串口在嵌入式中用途非常的广泛,主要的用途有:1、打印调试信息;2、外接各种模块;
通过3条线就可以实现数据的接收和发送。
TXD:数据发送端;RXD:数据接收端;GND:地线;TXD和RXD要彼此交叉连接。
若是芯片与PC机相连,通常PC机使用的都是RS232接口,因此不能直接交叉连接,需要经过电平转换。
具体细节在单片机学习笔记9–串口通信中有详细记载,这里不做介绍了。
1.1 内部结构
对于ARM芯片来说,不同芯片的串口内部结构都是类似的。
在uart内部有两个FIFO模式的缓冲区,用来存放发送或接收的数据;两个移位器作用是把数据变成一位一位的,并行数据变为串行数据,实现数据一个bit一个bit的传输。同时可以产生中断,数据发送完毕或接收完毕的时候,会产生中断。
1.2 数据传输协议
之前说过,一个串口的数据帧由起始位、数据位、校验位、停止位组成。
那么接收端是什么时候读取数据呢?串口数据的接收是通过时间来决定的。
上图中,是按照数据位是8位,有校验位画的时序图。
1、传输线默认电平是高电平,当电平拉低了,就认为是起始信号产生了,系统就会在t1时刻开始计时。
2、经过Tclk时间后,认为到了数据位,然后在1/2个Tclk时间后,也就是t3时刻,采集电平信息;
3、在后面的每个Tclk的中间时刻,采集数据,直到8个数据位全部采集完毕,经过8个Tclk后,认为下一个Tclk时间是校验位;
4、校验位之后的一个Tclk认为是停止位,电平拉高,到此一帧数据就发送接收完毕了;
从上面可以看出,对于每个位的判断,系统是通过时间来判断的。按照计时时间的不同,分析当前的数据是什么类型的,是第几个bit的数据位还是校验位还是停止位。
这也是为什么在串口通信之前要统一双方的波特率----要确定Tclk,确定数据采集的时间标准。所以串口传输数据最重要的参数就是波特率,只有收发双方处在同一波特率的情况下,数据才能正常收发。
波特率就是数据在1S内传输的bit的个数。以115200为例,就是1S内传输115200个bit,那么传输一个bit的时间就是1/115200,(假设没有校验位,数据位是8个bit),那么一个数据帧总共10个bit,传输一个数据帧的时间就是10/115200,1S中可以传输11520个数据帧也就是115200个byte。
二、串口编程
2.1 STM32F103的串口框架
STM32F103的串口框架和上面讲的差不多,不同的是STM32F103没有FIFO缓冲区,数据保存在寄存器中,这样寄存器一次只能写入一个字节的数据,只有当上一次的数据发送完毕,才能写入下一次的数据,若上一次数据没有发送完成就有新的数据被写入,那么上一次的数据就会被覆盖,通信就会发生错误。同理,接收数据寄存器中,必须要及时把数据读出来,否则数据会被下一次的数据覆盖掉。
2.2 相关寄存器
下面介绍一些串口常用的几个寄存器。
2.2.1 时钟使能寄存器
USART1是挂载到APB2总线上的,bit14就是USART1的时钟使能位。
2.2.2 状态寄存器
这个寄存器保存的是串口当前的某些状态的。比如:
TXE:发送数据寄存器空,0表示数据被转移到移位寄存器中,1表示数据没有完全被转移到移位寄存器中。
TC:发送完成标志,1–发送数据完成,此时移位寄存器里面的数据也发送完毕,一帧数据彻底发送完。0–发送未完成。
RXNE:读数据寄存器非空,0:读数据寄存器中没有数据; 1:收到数据,可以读出。
一般常用到的就是以上三个标志位,他们都可以在某些条件下产生中断。
2.2.3 数据寄存器
数据寄存器就是保存串口发送和接收的数据的寄存器。包含了发送或接收的数据。
它是由两个寄存器组成的,一个给发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR寄存器提供了输入移位寄存器和内部总线之间的并行接口。
2.2.4 波特率寄存器
该寄存器用来设置串口的波特率,其中4-15位存放USARTDIV的整数部分,0-3位存放USARTDIV小数部分。
以STM32F103工作在最大工作频率,串口波特率是115200来计算,USART1挂载到APH2总线上,时钟频率fCK=72MHz,USARTDIV=72000000/(16*115200)=39.0625。
这样USART_CRR的bit4-bit15=39=0x27,bit0-bit3=0.625,总共四个bit表示0-1。所以0.625用二进制表示就是0.625/1*16=10,用二进制表示就是0b1010,所以bit3-bit0就是1010。当波特率设置为115200时,USART_BRR=0x27<<4+0x05=0x27A。
2.2.5 控制寄存器
STM32F103的串口一共有三个控制寄存器CR1、CR2、CR3,最常用的是CR1,这里只介绍CR1,其余两个不做介绍。
USART_CR1寄存器主要是控制串口的使能、数据位的个数(8个或者9个)、校验使能、校验方式还有一些中断的使能。下面是一些常用的bit位说明。
bit位 | 名字 | 功能 |
---|---|---|
bit13 | UE:USART使能 | 0:USART分频器和输出被禁止 1:USART模块使能 |
bit12 | M:字长 | 0:一个起始位,8个数据位,n个停止位; 1:一个起始位,9个数据位,n个停止位 |
bit10 | PCE:检验控制使能 | 0:禁止校验控制; 1:使能校验控制。 |
bit9 | PS:校验选择 | 0:偶校验; 1:奇校验。 |
bit7 | TXEIE:发送缓冲区空中断使能 | 0:禁止产生中断; 1:当USART_SR中的TXE为’1’时,产生USART中断。 |
bit6 | TCIE:发送完成中断使能 | 0:禁止产生中断; 1:当USART_SR中的TC为’1’时,产生USART中断。 |
bit5 | RXNEIE:接收缓冲区非空中断使能 | 0:禁止产生中断; 1:当USART_SR中的ORE或者RXNE为’1’时,产生USART中断。 |
bit3 | TE:发送使能 | 0:禁止发送; 1:使能发送。 |
bit2 | R E:接收使能 | 0:禁止接收; 1:使能接收,并开始搜寻RX引脚上的起始位 |
2.2.6 用结构体表示寄存器
在之前的程序中,都是定义指针然后让指针等于要操作的寄存器,对于寄存器少的外设来说可以这样操作,但是一旦外设的寄存器多了,这样处理就不方便,所以可以定义一个结构体,把所有寄存器保存在结构体中,然后利用结构体指针来操作外设的各种寄存器。下面以串口为例:
typedef unsigned int uint32_t;
typedef struct
{
volatile uint32_t SR; /*状态寄存器,偏移地址:0x00 */
volatile uint32_t DR; /*!<数据寄存器,偏移地址: 0x04 */
volatile uint32_t BRR; /*!<波特率寄存器,偏移地址: 0x08 */
volatile uint32_t CR1; /*!<控制寄存器1,偏移地址: 0x0C */
volatile uint32_t CR2; /*!<控制寄存器2,偏移地址: 0x10 */
volatile uint32_t CR3; /*!<控制寄存器3,偏移地址: 0x14 */
volatile uint32_t GTPR; /*!<保护时间和预分频寄存器,偏移地址: 0x18 */
} USART_TypeDef;
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //让结构体指针等于串口的基地址
2.3 程序代码
开发板串口引脚对应关系:PA9–TX,PA10–RX。
2.3.1 串口初始化
1、使能GPIOA和USART1
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //让结构体指针等于串口的基地址
unsigned int * pAPB2En = ( unsigned int * )(0x40021000 + 0x18);
unsigned int * pGPIOACrl = ( unsigned int * )(0x40010800 + 0x04);
//使能GPIOA和USART1
* pAPB2En |= (1<<2) | (1<<14);
2、设置引脚工作模式
//配置PA9--TXD
* pGPIOACrl &= ~(0x0f<<4); //清空bit4-bit7
* pGPIOACrl |= (1<<4) | (2<<6); //设置bit7和bit4为1,PA9--复用推挽输出10MHz
//配置PA10--RXD
* pGPIOACrl &= ~(0x0f<<8); //清空bit8-bit11
* pGPIOACrl |= (1<<10); //设置bit10=1,PA10--浮空输入模式
3、设置波特率
//设置波特率USARTDIV=8000000/16/115200 = 4.34,整数部分=4=0x04;小数部分=0.34*16 = 5=0x05
usart1->BRR = 4<<4 | 5; //注意小数部分无法整除所以是近似值,实际波特率与理论值有误差
4、设置串口参数并使能相关功能
//设置串口数据格式
usart1->CR1 |= (1<<13) | (1<<3) | (1<<2);/*使能串口/发送使能/接收使能,因为CR1/CR2的默认值是0,
所以需要置0的位就没有设置,比如/禁止校验/8个数据位/1个停止位*/
经过以上四步串口就初始化完成了,接下来只需对串口的数据寄存器进行读写就可以实现数据的接收和发送了。
2.3.2 串口发送程序
void putchar_one(char c)//一次发送一个数据
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //让结构体指针等于串口的基地址
while(((usart1->SR) & 0x80) == 0);//判断上一次数据是否已经发送到移位寄存器了
usart1->DR = c; //向发送数据寄存器写入数据
}
void putchar_more(unsigned char * str,unsigned short int len)//一次发送多个数据
{
unsigned short int i;
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //让结构体指针等于串口的基地址
if(len == 0)
{
return;
}
for(i = 0;i < len;i++)
{
usart1->SR &= ~(1<<6);
while(((usart1->SR) & 0x80) == 0);//上一次数据已经发送到移位寄存器了
usart1->DR = str[i];
while(((usart1->SR) & 0x40) == 0);//移位寄存器中数据发送完毕,此时数据才真正的发送过去了
}
}
2.3.3 串口接收程序
int getchar_one(void)//一次接收一个数据
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //让结构体指针等于串口的基地址
while(((usart1->SR) & 0x20) == 0);
return usart1->DR; //返回接收数据寄存器中的值
}
void getchar_more(unsigned char * str,unsigned short int len)//一次接收多个数据
{
unsigned short int i;
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //让结构体指针等于串口的基地址
if(len == 0)
{
return;
}
for(i = 0;i < len;i++)
{
while(((usart1->SR) & 0x20) == 0);//数据接收寄存器没有数据,读该寄存器清零
str[i] = usart1->DR;
}
}