前言
本节将会学习到串口的基本概念、发送形式、如何使用串口发送各类型数据、重定义printf函数、如何使用中断函数,所使用的串口工具附在最后。
目录
1、串口概述
在STM32的参考手册中,串口被描述成通用同步异步收发器(USART),它提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。
USART利用分数波特率发生器提供宽范围的波特率选择。它支持同步单向通信和半双工单线通信,也支持LIN(局部互联网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。
它还允许多处理器通信。还可以使用DMA方式,实现高速数据通信。
USART通过3个引脚与其他设备连接在一起,任何USART双向通信至少需要2个引脚:接受数据输入(RX)和发送数据输出(TX)。
RX: 接受数据串行输入。通过过采样技术来区别数据和噪音,从而恢复数据。
TX: 发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时,TX引脚处处于高电平。在单线和智能卡模式里,此I/O口被同时用于数据的发送和接收。
2、串口的工作方式
一般只有 查询 和 中断 两种方式
- 查询:串口程序不断地刷新查询,查看当前有无数据需要传送。
- 中断:需要在程序中编写中断函数。若有中断信号,则进行数据的传输。
2、USART帧格式
以下为串口发送一个字节的格式,格式为串口协议所规定。串口中,每个字节都装在一个数据帧中。每个数据帧都由起始位、数据位和停止位组成,下图共10位。
也可以在数据位最后一位加一位奇偶校验位。
其中有效载荷为前八位,校验位在最后占一位。
3、串口参数
- 波特率:串口通信的速率。单位是码元/s,或者直接叫波特(Baud)。
- 比特率:也可以表示串口通信的速率。每秒传输的比特数,单位是bit/s,或者叫bps。
在二进制情况下1码元=1bit,此时波特率等于比特率。
- 起始位:标志一个数据帧的开始,固定为低电平。
- 数据位:数据真的有效载荷,1为高电平,0为低电平,低位先行。
- 校验位:用于数据验证,根据数据位计算得来。
- 停止位:用于数据帧间隔,固定为高电平。
4、USART简化结构图
4、串口通信的具体程序实现
4.1 将需要用的USART和GPIO的时钟打开
与之前一致,不再赘述,直接上结果
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
}
4.2 GPIO初始化,将TX配置成复用输出
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
与之前一致,不再赘述,只是需要注意将PA9配置为服用推挽输出,供USART1的TX使用。
4.3 配置USART,打包为结构体
USART_InitStruture.USART_BaudRate = 9600;
将波特率设置为9600,只要设置好就可以不用管了,会自动计算出所需要的各种参数。
USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
这条代码是选择硬件控制流,因为不需要所以直接选None。
USART_InitStruture.USART_Mode = USART_Mode_Tx;
这条是选择USART的模式,可以选择发送或者接收模式。
USART_InitStruture.USART_Parity = USART_Parity_No;
这条是选择有无校验位,因需要校验位所以选择无校验。
USART_InitStruture.USART_StopBits = USART_StopBits_1;
这条是选择停止位,可以选择0.5、1、1.5、2,这里选择1便可。
USART_InitStruture.USART_WordLength = USART_WordLength_8b;
这条是选择字长,可以选择8位或9位,因不需要校验选择8位即可。
最后再加一个 USART_Cmd(USART1,ENABLE);
初始化完毕
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitTypeDef USART_InitStruture;
USART_InitStruture.USART_BaudRate = 9600;
USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruture.USART_Mode = USART_Mode_Tx;
USART_InitStruture.USART_Parity = USART_Parity_No;
USART_InitStruture.USART_StopBits = USART_StopBits_1;
USART_InitStruture.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStruture);
USART_Cmd(USART1,ENABLE);
}
4.4 发送函数
4.4.1 发送一个字节数据函数
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
这条函数可以在USART.H中找到。
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
}
将两个函数声明一下后,准备编写主程序代码,代码流程如下:
- 初始化串口
- 发送数据
int main(void)
{
Serial_Init();
Serial_SendByte(0x22);
while (1)
{
}
}
电脑端的话需要用串口工具来观察发送的数据是否成功以及正确,还要注意端口号、波特率等参数一定要和单片机端一致。
测试结果如下
代码测试正确无异常。
注意: 如果 输出函数 放置到 主函数的循环函数 中便会一直发送数据。
4.4.2 发送数组函数
发送数组函数的本质也是发送一个字节数据,但是利用循环将数组中的数据一个一个发送出来,变成了发送数组函数。
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
创建一个数组然后进行测试
int main(void)
{
Serial_Init();
uint8_t a[8] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};
Serial_SendArray(a,8);
while (1)
{
}
}
测试结果如下:
代码测试正确无异常。
4.4.3 发送字符串函数
字符串函数可以用来输出一些日志信息。
int main(void)
{
Serial_Init();
Serial_SendString("HelloWorld");
while (1)
{
}
}
测试结果如下:
代码测试正确无异常。
4.4.4 发送printf函数
1.打开工程选项,将MicroLIB勾选
2.对printf重定向
#include <stdio.h>
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
3.主函数测试
int main(void)
{
Serial_Init();
printf("Num2=%d\r\n", 222); //\r\n换行
while (1)
{
}
}
测试结果如下:
4.5 接收数据
与发送数据一致,首先需要初始化引脚,代码与发送差不多,只需简单修改即可。
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitTypeDef USART_InitStruture;
USART_InitStruture.USART_BaudRate = 9600;
USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruture.USART_Mode = USART_Mode_Rx;
USART_InitStruture.USART_Parity = USART_Parity_No;
USART_InitStruture.USART_StopBits = USART_StopBits_1;
USART_InitStruture.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStruture);
USART_Cmd(USART1,ENABLE);
}
4.5.1开启中断
需要中断接收数据,所以程序中需要打开中断。这条代码可以在USART中找到。
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState)
第一个参数选择 USART1 ,因为PA9、PA10在技术手册中是属于USART1。
第二个参数选择 USART_IT_RXNE ,因为我们想达到的效果是收到数据后进行中断,也就意味着接收 数据寄存器 在收到数据后会置1,会达到中断的条件。
第三个参数的意思是开启。
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
4.5.2 配置NVIC
先分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
使用结构体变量
NVIC_InitTypeDef NVIC_InitStructure;
继续使用.引出所有结构体变量。
第一项根据中断通道选择,这里选择 USART1_IRQn 。
第二项开启中断。
第三项选择先占优先级。
第四项选择从优先级。
因为目前只有这一个中断所以都选1就可以。
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
最后跟上结构体地址,完整开启代码如下:
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
4.5.3 编写使用中断代码
首先要检测中断标志位是否置1
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
如果置1进入该程序,看是否读取DR,如果读取了会自动清除,没有读取需要手动清除。
USART_ClearITPendingBit(USART1, USART_IT_RXNE).
注意:不管是手动清除还是自动清除,一定要确保清除掉标志位!!!
接下来需要将收到的数据放入缓存(可以自己定义),并且设置 标志位 置1 (自己定义)。
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1); //存储缓存数据
Serial_RxFlag = 1; //自己设置的标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除中断标志位
}
}
至此 中断程序结束,以上程序可以自己单独写一个封装进行独立操作。
4.5.4 主函数使用
同样在主程序中需要保证与上面一致,进入标志位后记得进行清除。
最终主函数代码如下:
uint8_t RxData;
int main(void)
{
Serial_Init();
while (1)
{
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
}
}
}
测试结果如下:
总结
- TX定时输出引脚的高低电平
- RX定时读取引脚的高低电平
- 每个字节的数据加上起始位、停止位、可选的校验位,打包为数据帧,依次让TX引脚输出,另一侧RX依次接受。
- 发送数据的所有形式都是由最基础发送单个字符组成的。
- 如果 输出函数 放置到 主函数的循环函数 中便会一直发送数据。
- 中断代码过程:开启中断、使用中断。使用中断的过程中不管是手动清除还是自动清除,一定要确保清除掉标志位!!!
参考资料(以下排序不分先后)
【1】江科大 [9-1] USART串口协议_哔哩哔哩_bilibili
【2】stm32中常见的通信协议之USART(串口)_usart串口协议_嵌入式进阶之路的博客-CSDN博客
【3】 STM32------USART详解_休闲可乐的博客-CSDN博客
【4】stm32中断优先级NVIC_IRQChannelPreemptionPriority的理解及使用_静戴冠的博客-CSDN博客
所用工具
串口工具:
链接:https://pan.baidu.com/s/1Ir2DbBOkO7VyND7zkCdazw?pwd=vm32
提取码:vm32