一.串口
什么是串口?
UART : Universal Asynchronous Receiver/Transmitter
通用异步收发器
USART:Universal Synchronous/Asynchronous Receiver/Transmitter
通用同步/异步收发器
那么什么是同步?什么是异步?
通信双方有没有共同的时钟线来作为同步时钟使用
如果有那么就是同步通信 如果没有那么就是异步的通信
时钟信号可以作为通信时的同步信号,比如:
1.在时钟线下降沿的时候 改变信号 -->发送方发送数据
2.在时钟线上升沿的时候 保存信号不变 -->接收方接收数据
同步:发送方发送数据之后 等待接收方发回响应之后 才发下一个数据包
异步:发送方发送数据之后 不等待接收方发回响应 接着发下一个数据包
串口是单片机中最为常见 也是最简单的串行数据传输协议。
串口只需要两根数据线 就可以实现全双工通信
两根数据线分别为:
Tx : 发送数据端 用于向对方发送数据
Rx : 接收数据段 用于接收对方发送过来的数据
比如:两个设置之间要通信的话 接线如下
设备A 设备B
Tx ------------->Rx
Rx <-------------Tx
VCC ------------ VCC
GND ------------ GND
全双工通信: 两个设备的接收端和发送端是互相独立的 互不干扰的
两个设备可以同时收/发
还有其他的通信方式 : 单工(设备只能发或者只能收 只有一根数据线)
半双工(设备不能同时收/发)
串行: 数据的传输只有一根“电线” 每次就只能传送1bit数据
当多个数据发送的时候 也只能1bit 1bit地发送
A -------- B
比如: 我要发送0x5c
0101 0000
就是把这一个字节地每一个bit 一个bit接着1个bit地发送出去
与之对应地就是并行,简单来说就是两个设备之间有多根线 可以同时发送
但是有问题就是:比如我要发个bit1 过去 此时我把对应地线拉高 ,拉高多久就可以了呢?
还有你这个电线肯定是接入完整地电路了 就算我不发数据 电线上也有一个电平状态
那么我怎么知道此时电平状态代表数据呢?
那么我们就需要一个协议了,协议就是双方约定好地传输方式 --》串口协议
二.串口协议
作用: 规定了串口发送和接收数据地方式 必须以帧(frame)为单位
1帧(frame) = 1 start bit(起始位) + 5~9bit的数据位 + 0/1校验位 + (0.5,1,1.5,2)stop bit(停止位)
起始位: 一个周期的低电平信号
所以串口的数据发送线Tx在空闲(idle)的时候应该要永远保持高电平。
5~9bit数据位: 通信的正文 具体是发送多少bit 由双方协商
并且是先发送最低位(LSB)最后发送最高位(MSB)
1个校验位: 表示是否需要检验
0bit空间 没有校验位
1bit空间 有校验
奇校验: 要保证前面的数据位加上校验位的1的个数是奇数个
偶校验: 要保证前面的数据位加上校验位的1的个数是偶数个
比如:
9bit 发送0x5c (0101 1100)
奇校验: 0 0 1 1 1 0 1 0 x=1
偶校验: 0 0 1 1 1 0 1 0 x=0
0.5-2个停止位: 持续的高电平
具体持续多久高电平 由双方约定
0.5个周期 1个周期 1.5个周期 2个周期...
但是由于UART异步串口 没有时钟进行同步 因此光靠帧格式传输数据还是不能准确收发
因此我们有另外一个东西来保证传输准确
Baudrate:波特率 : UART的传输速率 决定1Frame传输周期
常见的波特率有 9600bps (bits Per second)
波特率的设置收发双方必须一样
UART协议: 帧格式 + 波特率
三. 物理层标准
串口有不同的分类:
TTL level UART: TTL电平串口
TX 数据发送端口
RX 数据接收端口
VCC 电源端口(+)
GND 接地端口(-)
还有常见的分类比如:RS-232 RS-422 RS-485
不同的电器标准的串口 引脚的个数也不一样 但是数据线RX/TX是一定存在的
不同电器标准的串口的区别如下:
TTL Uart RS-232 RS-422 RS-485
高电平 3.3V/5V -3v~-15v +2v +1.5v
低电平 0V +3v~+15v -2v -1.5v
信号 单端信号 单端信号 差分信号 差分信号
传输长度 <2m <15m <1200m <1200m
四.STM32F4xx串口控制器
参考《stm32f4xx参考手册》678页 UART框图
从图中来看呢 串口不仅有TX RX两根线 还有两根用于硬件流控的线
这两根线存在的作用是: 有时候发送方通过TX往外发送数据的时候
如果对方还没有准备好接收 发送的数据肯定就被丢弃了
所以在硬件上加了两根硬件流控的信号
RTS:(Require To Send 发送请求) 为输出信号 用于指示本设备准备好可以接收数据
低电平有效 低电平说明被设备可以接收数据
CTS:(Clear To Send 发送允许) 为输出信号 用于判断是否可以向对方发生发送数据
低电平有效 低电平说明本设备可以向对方发送数据
接线方式如下:
A B
RTS ------> CTS
CTS <------ RTS
当然不一样要用RTS/CTS
在图的下方有一个比特率发生器(USART_BRR) 主要是用来控制数据的收发速率的
另外在途中有有两种寄存器:
CR(Control Register):控制寄存器 用来控制串口的一些行为
SR(Status Register):状态寄存器 用来指示串口控制器的一些状态 711页左右
串口收发数据的流程:
a.从读(接收数据)的角度来说
外部设备的TX ---> 我的RX --> 接收移位寄存器 --> 接收数据寄存器(RDR) --> CPU读取
b.从发(发送数据)的角度来说
CPU写数据 -->发送数据寄存器(TDR) -->发送移位寄存器 --> 我的TX --> 对方的RX
这个数据收发的过程 会产生很多标志!!!
我们重点讲解几个标志:
RXNE: Rx Data Register Not Empty 接收数据寄存器非空标志
如果这个标志位被置为1 就表示接收数据寄存器(RDR)中有数据了
此时CPU应该要尽快地去读取RDR以获得数据
TXE : Tx Data Register Empty 发送数据寄存器为空标志
如果这个标志被置为1 表示发送寄存器为空!!!表示可以写数据了。
请问 你觉得如果这个标志位被置为1 能够表示数据发送出去了吗?
不行! 因为 数据有可能还在发送移位寄存器中!
TC : Transmit Complete 发送完成标志
如果这个标志被置为1 表示数据发送完成!
数据发送完成的意思是 发送数据寄存器中没有数据 发送移位寄存器中也没有数据了 数据已经都通过TX引脚发送到对面了
注意:在发送数据之前 必须要确保TDR寄存器为空(TXE被设置) 否则上一个发送的数据就有可能还没有发送完成 那么就会被覆盖
接收数据呢 需要等RXNE标志被设置 才能去接收 。
因此一般使用串口中断来完成数据接收。
另外还有一些在CR寄存器中的中断使能位
RXNEIE:串口接收数据寄存器不为空中断使能
如果该标志位被置为1 那么当串口接收数据寄存器不为空的时候
就会产生一个串口中断
TXEIE:发送数据寄存器为空中断使能
如果该标志位被置为1 那么当串口发送数据寄存器不为空的时候
就会产生一个串口中断
TCIE :发送完成中断使能
如果该标志位被置为1 那么当串口数据都发送完成后
就会产生一个串口中断
注意: 一个串口控制器 只对应一个中断处理函数 但是一个串口他的多种情况都可以引起中断
所以在对应的中断处理函数中 需要对不同的情况做出不同的处理
五.STM32F4xx代码
串口配置步骤:
串口的TX和RX引脚是GPIO的功能复用而来的
开发板的三个串口 都可以由跳线帽选择不同的用途
USART1:
1-3 和 2-4(跳线帽全接左边)作为调试/烧写串口(通过CH340转USB后可以与PC相连)
3-5 和 4-6(跳线帽全接右边)表示作为P4的外接串口使用
USART2 和USART3 类似
如果想把他们用作外接串口的话 就把对应的两个跳线帽接到右边
所以在使用串口的时候 先根据用途 对跳线帽进行短接
然后再通过程序写代码
//USART1_TX PA9
//USART1_RX PA10
0.串口对应的GPIO引脚的配置
a.使能GPIO分组时钟
b.初始化GPIO
GPIO_Init() -->AF模式
c.配置GPIO具体复用成哪个功能
GPIO_PinAFConfig
1.使能USART的外设时钟
RCC_xxxPeriphClockCmd()
xxx就表示你这个外设接在哪根总线上 不同的串口可能位于不同的总线上面 APB1 or APB2
2.初始化USART
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct)
参数列表:
USARTx: 具体你要初始化的串口编号
USART1
...
USART_InitStruct:指向你配置串口信息的结构体
typedef struct
{
uint32_t USART_BaudRate; //指定串口通信的波特率 是一个整数 通信双方必须要保持一致
//常见的有9600 或 115200 bps
uint16_t USART_WordLength; //指定数据帧数据位的长度 也即是传输字长
//USART_WordLength_8b 无校验8bit数据位 《---
//USART_WordLength_9b 有校验9bit数据位
uint16_t USART_StopBits; //指定停止位长度
//USART_StopBits_0_5
//USART_StopBits_1 1个周期的停止位
//USART_StopBits_1_5
//USART_StopBits_2
uint16_t USART_Parity; //指定校验方式
//USART_Parity_Odd 奇校验
//USART_Parity_Even 偶校验
//USART_Parity_No 不要校验 <---
uint16_t USART_Mode;//指定串口的收发模式
//USART_Mode_Rx 接收模式 只接受数据
//USART_Mode_Tx 发送模式 只发送数据
// USART_Mode_Rx | USART_Mode_Tx 收发模式(全双工)
uint16_t USART_HardwareFlowControl;//指定硬件流控
//USART_HardwareFlowControl_CTS
//USART_HardwareFlowControl_RTS
//USART_HardwareFlowControl_RTS_CTS
//USART_HardwareFlowControl_None 不需要硬件流控 <---
} USART_InitTypeDef;
3)中断配置
1. 中断源的控制
产生串口的中断的事件或标志有很多 比如:
TXE --> 串口中断
RXNE --> 串口中断
...
TC -->串口中断
这些事件需要“中断控制位使能”才能产生中断
怎么去配置呢?
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState)
参数列表:
USARTx: 你要配置哪个串口产生中断
USART1
USART2
...
USART_IT:你配置串口的哪种具体事件产生中断
USART_IT_TXE : 表示配置 发送数据寄存器为空的时候 就可以产生一个中断
USART_IT_RXNE :表示配置 当接收数据寄存器不为空的时候 就可以产生一个中断
USART_IT_TC :表示配置 当发送完成的时候 就可以产生一个中断
...
NewState: 使能或者禁止中断
ENABLE
DISABLE
2. NVIC的控制
串口中断使能(需要配置NVIC)
线:USART1_IRQn
...
4)使能串口
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState)
参数列表:
USARTx: 你要具体操作的串口
USART1
USART2
...
NewState: 使能/禁止
ENABLE
DISABLE
5)串口数据的收发
一般来说 在串口通信中 接收部分几乎都是由中断来完成的
那么中断处理函数名字如下:
void USART1_IRQHandle(void)
{
//有多个事件(RXNE TXE TC...)可以引起中断
//所以 你在串口中断处理函数中 一般要判断到底是什么事件引起了这次串口中断 再进行对应的处理
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET ) //RXNE事件产生了串口中断
{
//去读取串口收到的数据
...
//清除中断标志 USART_ClearITPendingBit()
}
else if(xxx)
{
...
}
...
}
获取串口中断标志:
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG)
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT)
参数列表:
USARTx: 你要查看哪个串口的中断标志 指定具体的串口
USART_IT or USART_FLAG : 你要查看串口的具体哪个中断标志 指定具体的中断标志
返回值:
SET : 1 表示查询的对应的事件产生了
RESET:0 表示查询的对应的事件没有产生
清除串口中断标志:
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
串口接收数据: 从指定的串口接收一个字节数据
uint16_t USART_ReceiveData(USART_TypeDef* USARTx)
参数列表:
USARTx : 你要从哪个串口接收数据
返回值:
会把接收到的数据返回给你
返回从串口接收到的数据
串口发送数据:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
参数列表:
USARTx: 你要通过哪个串口发送数据
Data: 你要发送的数据 1个字节
注意:在发送数据之前必须要确保TDR为空 如果没有为空的话 不能发送下一个字节
接收是被动的 我们并不知道什么时候有数据过来需要接收 所以在中断服务函数中接收
而发送是主动的 由我们自己决定什么时候发送数据出去 所以不用中断实现发送
而自定义串口发送函数
/*
USART_SendDatas:通过指定的串口把sendbuf里面的数据 发送n个字节出去
参数列表:
@USARTx :指定串口编号
@sendbuf :指向你要发送的字符串
@n : 你要发送的字节个数
*/
void USART_SendDatas(USART_TypeDef* USARTx , uint8_t * sendbuf , uint8_t n)
{
int i ;
for(i=0;i<n;i++)
{
//当TDR不为空的时候 我就等待 当TDR为空的时候 我就发送一个字节
while(USART_GetFlagStatus(USARTx , USART_FLAG_TXE) == RESET) ;
USART_SendData(USARTx,sendbuf[i]);
}
}
配置串口1(USRAT1跳线帽全接左边) 实现与PC机(串口助手XCOM)双向通信
在STM32中可以将printf重定向到串口:
一般把串口1作为调试串口。
步骤:
1.初始化USART1
2.在调用的文件处添加stdio.h 同时keil需要勾选<options>--><target>--><USE MicroLIB>
3.printf实际上是调用fputc这个函数来实现输出的
所以我们需要重新改写fputc这个函数如下
int fputc(int c, FILE * stream)
{
USART_SendData(USART1,c & 0xff);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET );
return 0;
}
之后就可以使用printf函数 并且可以在串口1作为调试串口中看到输出结果
见工程文件usart.c
#include "usart.h"
/*
串口1的初始化函数
@baudrate:串口传输的波特率
*/
void Usart1_Init(int baudrate)
{
//*
1.串口GPIO配置
*/
//使能GPIO分组时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
//≥ı ºªØGPIO
GPIO_InitTypeDef p;
p.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
p.GPIO_OType = GPIO_OType_PP;
p.GPIO_Mode = GPIO_Mode_AF;
p.GPIO_PuPd = GPIO_PuPd_NOPULL;
p.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&p);
//配置GPIO复用功能
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
/*
2.USART配置
*/
//使能USART分组时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//初始化配置USART
USART_InitTypeDef u;
u.USART_BaudRate = baudrate;
u.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
u.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
u.USART_Parity = USART_Parity_No;
u.USART_StopBits = USART_StopBits_1;
u.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&u);
/*
3.中断配置
*/
//中断控制位使能
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
//配置NVIC
NVIC_InitTypeDef n;
n.NVIC_IRQChannel = USART1_IRQn;
n.NVIC_IRQChannelCmd = ENABLE;
n.NVIC_IRQChannelPreemptionPriority = 2;
n.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&n);
/*
4.开启串口
*/
USART_Cmd(USART1,ENABLE);
}
/*
通过USARTx指定的串口将SendBuf中n个字节的数据发送出去
*/
void USART_SendDatas(USART_TypeDef* USARTx,uint8_t* SendBuf,uint8_t n)
{
int i;
for(i = 0;i < n;i++)
{
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE) == RESET)
;
USART_SendData(USARTx,SendBuf[i]);
}
}
/*
串口1的中断服务函数
*/
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)//RXNE事件产生
{
//去读取数据
unsigned char buf = USART_ReceiveData(USART1);
if(buf == '0')
{
Led_Ctrl_Lib(LED1,LED_OFF);
}
else if(buf == '1')
{
Led_Ctrl_Lib(LED1,LED_ON);
}
else
{
//...
}
//清除中断标志
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
int fputc(int c,FILE *stream)
{
USART_SendData(USART1,c & 0xFF);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
return 0;
}