嵌入式硬件通信接口-UART

UART协议简介

UART(Universal Asynchronous Receiver/Transmitter),通用异步收发器。UART在一开始发明出来的时候用作电脑硬件的一部分,主要作用是将数据通过串行通信和并行通信间作传输转换,后来UART作为微处理器的外设应用非常广泛,同时在UART上追加同步的时钟信号,称为USART(Universal Synchronous Asynchronous Receiver Transmitter)。UART它包括RS232、RS449、RS422和RS485等接口标准规范和总线规范,是异步串行通信口的总称。可以理解为UART通信协议是软件上的通信网络中的数据链路层,表示数据传输协议。而RS232、RS485是通信接口的电器特性和连接特性、机械特性等硬件上的范畴,属于物理层

UART协议特点

UART通信可以是半双工、全双工,在空闲状态(没有数据传输期间)保持高电平,表示传输线路正常。每一个字符为一帧数据,以逻辑低电平为开始比特,然后是数据比特,可选的奇偶校验比特,最后是一个或多个停止比特(逻辑高电平)。大部分的应用都是先传输低位数据比特

UART协议应用

UART应用是非常广泛的,在早期的电脑中都会带一个RS232的接口,后来变为USB口。目前市场上绝大部分的微处理器(MCU)都带有UART硬件,可用于通信或应用程序下载。需要注意的是MCU上的UART是TTL电平

UART协议时序

UART时序包含:起始位、数据位、奇偶校验位(可选)、停止位。在无数据传输的期间(空闲状态下),数据线保持高电平;通信过程中,先发送1bit起始位(低电平),接着发送数据位(数据位多少由用户配置决定,通常为8bit,也就是1Byte),发送完数据位后接着发送1bit奇偶校验位1bit(根据通信决定可选择不发送,一般都配置为无奇偶校验),最后发送停止位(通常位1bit)。数据位选择、奇偶校验位选择和停止位选择如下表

起始位 数据位 奇偶校验位 停止位
1(bit) 5、6、7、8(bit) 奇校验或偶校验(1bit)、无 1、1.5、2(bit)

注:奇偶校验位-当配置为奇校验时,数据位中数据‘1’的个数+奇偶校验位 = 奇数;偶校验同理

数据帧结构

UART通信配置:起始位1bit+数据位8bit+无奇偶校验+停止位1bit。通信数据帧如下图:

通信速率

UART的通信速率是以波特率位单位衡量的,UART一般通信速率使用9600、115200。需要特别说明的是:UART通信中,比如波特率是9600bps,但是不意味着是9600bit/s,因为使用的波特率单位bps与平常的比特率单位bit/s是不一样的,波特率指的是每秒钟传送的码元符号的个数,是衡量数据传送速率的指标,它用单位时间内载波调制状态改变的次数来表示,比特率指的是每秒传送的比特(bit)数

UART通信中,1个bit表现为高电平或低电平,在这种情况下是9600bps = 9600bit的,一帧数据 = 起始位1bit+数据位8bit+无奇偶校验+停止位1bit = 10bit,9600bps的实际有效数据就是9600/10 = 960Byte

注:当一个一个码元符号表示两个bit时,比特率 = 波特率 x 2

UART协议在MCU中的应用

目前市场上大部分的MCU都带有UART外设,以STM32F103RCT6型号为例,该型号MCU带有数个USART(Universal Synchronous Asynchronous Receiver Transmitter),比UART多了一个时钟同步信号线(该功能比较少用),还有有CTS与RTS信号引脚,可用于流控[^有软件和硬件两种]。UART大量应用在数据收发的通信场合,因此一个好的算法处理收发过程的数据是很必要的,下面介绍一个常用的队列结构在UART收发上的应用

下面例子使用的环境如下:

  • Windows10
  • ARM KEIL MDK V5.25
  • Thermal Printer MainBoard V1.0(STM32F103RC开发板也可以直接使用)
  • DAPLink(或其他支持SWD下载方式的下载器,如ST-LINK)
  • 友善串口调试助手(支持串口调试的终端都可以)

注:如果选择非DAPLink下载器(DAPLink自带虚拟串口),则还需要一个USB转TTL来连接PC与开发板

UART基础配置

例程使用STM32F103RCT6中的USART1口,基本配置为(起始位时通讯中必须存在的):

  • 波特率115200
  • 8位数据位
  • 一位停止位
  • 无奇偶校验
  • 无流控
  • 全双工模式
  • 开启接收中断

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* @函数名 USART1_Init
* @功 能 初始化串口,波特率115200,8位数据为,1位停止位,无奇偶校验
* @参 数 无
* @返回值 无
*/
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义一个GPIO_InitTypeDef类型的变量
USART_InitTypeDef USART_InitStructure; // 定义一个USART_InitTypeDef类型的变量
NVIC_InitTypeDef NVIC_InitStructure; //定义中断向量结构体

/* 允许GPIOA和USART1的时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

/* 配置USART1 */
/* 配置PA9(TXD) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // 选择PIN9
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz速度
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化配置

/* 配置PA10(RXD) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // 选择PIN10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 选择浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化配置

/* 配置USART1的NVIC */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //嵌套向量中断控制器组配置为2
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //配置通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//抢占优先级:1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级:1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能通道
NVIC_Init(&NVIC_InitStructure); //初始化配置

/* 配置串口USART1的模式 */
USART_InitStructure.USART_BaudRate = 115200; // 波特率115200
USART_InitStructure.USART_WordLength = USART_WordLength_8b;// 8个数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1个停止位
USART_InitStructure.USART_Parity = USART_Parity_No ; // 无奇偶校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure); //把上面配置的参数带进函数里面初始化串口

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中断
USART_Cmd(USART1, ENABLE); //打开串口1
}

环形队列应用

USART配置完毕后,开始实现队列结构,队列结构由五部分组成:

1
2
3
4
5
6
7
8
typedef struct Circular_Queue_Str
{
uint8_t *BuffHead; //缓冲区首地址
uint16_t WritePtr; //写入指针(位置)
uint16_t ReadPtr; //读取指针(位置)
uint16_t Count; //已使用数据量计数器
uint16_t BuffSize; //缓冲区大小
}CirQueue_Str;

在实际使用队列前,需要初始化队列,收发数据过程也伴随着不断入队和出队过程,相关队列函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* @函数名 Circular_Queue_Init
* @功 能 初始化队列
* @参 数 queue:队列结构, bufHead:队列数组首地址, bufSize:队列大小
* @返回值 无
*/
void Circular_Queue_Init(CirQueue_Str *queue, uint8_t *bufHead, uint16_t bufSize)
{
queue->BuffHead = bufHead; //队列数组首地址
queue->BuffSize = bufSize; //队列数组大小
queue->ReadPtr = 0; //队列读指针指向首地址
queue->WritePtr = 0; //队列写指针指向首地址
queue->Count = 0; //队列计数器清零
}


/**
* @函数名 Circular_Queue_IsEmpty
* @功 能 判断队列是否为空
* @参 数 queue:队列结构
* @返回值 计数器是否等于0的结果,空 = 1,非空 = 0;
*/
uint8_t Circular_Queue_IsEmpty(CirQueue_Str *queue)
{
return (queue->Count == 0); //返回队列计数器是否等于0的结果
}


/**
* @函数名 Circular_Queue_IsFull
* @功 能 判断队列是否为满
* @参 数 queue:队列结构
* @返回值 计数器是否等于队列大小的结果,满 = 1,非满 = 0;
*/
uint8_t Circular_Queue_IsFull(CirQueue_Str *queue)
{
return (queue->Count == queue->BuffSize); //返回队列计数器是否等于队列大小的结果
}


/**
* @函数名 Circular_Queue_Clear
* @功 能 清空队列
* @参 数 queue:队列结构
* @返回值 无
*/
void Circular_Queue_Clear(CirQueue_Str *queue)
{
queue->ReadPtr = 0; //队列读指针指向首地址
queue->WritePtr = 0; //队列写指针指向首地址
queue->Count = 0; //队列计数器清零
}


/**
* @函数名 Circular_Queue_Enter
* @功 能 入队
* @参 数 queue:队列结构,data:数据
* @返回值 无
*/
void Circular_Queue_Enter(CirQueue_Str *queue, uint8_t data)
{
if(Circular_Queue_IsFull(queue)) //判断队列是否满
{
printf("the queue is full \n");
return;
}

*(queue->BuffHead + queue->WritePtr) = data;//数据入队
__disable_irq(); //禁能所有中断
queue->Count++; //队列计数器加一
queue->WritePtr = (queue->WritePtr + 1) % queue->BuffSize; //写指针加一,循环队列的操作方法
__enable_irq(); //解禁中断
}


/**
* @函数名 Circular_Queue_Exit
* @功 能 出队
* @参 数 queue:队列结构
* @返回值 temp:队列数据或出错信息
*/
uint16_t Circular_Queue_Exit(CirQueue_Str *queue)
{
uint8_t temp;

if(Circular_Queue_IsEmpty(queue))
{
printf("the queue is empty \n");
return 0xffff;
}

temp = *(queue->BuffHead + queue->ReadPtr); //数据出队
__disable_irq(); //禁能所有中断
queue->Count--; //队列计数器减一
queue->ReadPtr = (queue->ReadPtr + 1) % queue->BuffSize; //读指针加一,循环队列的操作方法
__enable_irq(); //解禁中断
return temp; //返回数据
}

相关队列函数实现后,开始初始化队列。初始化队列需要由一个数据缓冲区,以及缓冲区大小和一个队列结构体三个参数,可以定义在USART配置的文件中:

1
2
3
#define USART_BUFF_SIZE		256
uint8_t USART_Buff[USART_BUFF_SIZE]; //定义缓冲区数组
CirQueue_Str my_queue; //定义队列

然后对USART和QUEUE进行初始化:

1
2
3
4
5
6
7
8
9
10
11
/**
* @函数名 USART1_Config
* @功 能 串口配置
* @参 数 无
* @返回值 无
*/
void USART1_Config(void)
{
USART1_Init(); //初始化串口
Circular_Queue_Init(&my_queue, USART_Buff, USART_BUFF_SIZE); //初始化队列
}

当接收到数据时,会引发USART的接收中断,在中断中把数据入队:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @函数名 USART1_IRQHandler
* @功 能 串口1中断函数
* @参 数 无
* @返回值 无
*/
void USART1_IRQHandler(void)
{
//串口空闲中断
if(USART_GetFlagStatus(USART1, USART_IT_RXNE) != RESET)
{
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除中断标志
Circular_Queue_Enter(&my_queue, USART_ReceiveData(USART1)); //入队
}
}

最后在主函数中判断队列是否存在数据,存在数据则发送出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @函数名 main
* @功 能 主函数入口
* @参 数 无
* @返回值 无
*/
int main(void)
{
uint8_t temp; //定义变量
USART1_Config(); //串口初始化

while(1)
{
if(!Circular_Queue_IsEmpty(&my_queue)) //判断队列书否存在数据
{
while(my_queue.Count != 0) //队列存在Count个数据
{
temp = Circular_Queue_Exit(&my_queue); //把数据出队(1Byte)
USART_SendData(USART1, (uint8_t) temp); //发送数据
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); //等待发送完成
}
}
}
}

效果如下所示:

本文标题:嵌入式硬件通信接口-UART

文章作者:LGG001

发布时间:2019年03月19日 - 23:03

最后更新:2019年05月08日 - 11:05

原始链接:http://yoursite.com/2019/03/19/嵌入式硬件通信接口-UART/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------
Thank You For Your Approval !