USART串口通信(stm32笔记)
目录
简介:
通信接口:
下图是常用的通信接口:

电平标准:

串口参数及时序:

波特率:一秒发送的bits数
校验位:这里是奇校验偶校验。
奇校验:保证数据位和校验位共有奇数个1。
偶校验:保证数据位和校验位共有偶数个1。
USART:
简介:
- USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器。其内部内置了CLK时钟。
- USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
- 自带波特率发生器,最高达4.5Mbits/s。相当于一个分频器,将总线给的时钟分频得到我们想要的波特率时钟。
- 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
- 可选校验位(无校验/奇校验/偶校验)
- 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN。同步模式,就是多以一个CLK的输出,以此来同步时钟。硬件流控制:为了防止数据阻塞,A向B发数据,A发的快,B接受的慢,就会导致数据阻塞。
- STM32F103C8T6 USART资源: USART1、 USART2、 USART3。
UASRT框图:

黑色框图中的是数据寄存器,我们发送接收的数据都存放在这里。发送数据寄存器(TDR)和接收数据寄存器(RDR)共用一个地址,在程序上是一个寄存器:发送数据就是发送数据寄存器(TDR),接收数据就是接收数据寄存器(RDR)。内部硬件也是两个寄存器
发送:总线给TDR中写入一个数据,随后他会检查当前发送移位寄存器是不是正在有数据正在移位。如果没有那么数据就会立刻从TDR移动到发送移位寄存器准备发送。(当数据从TDR移动到发送移位寄存器时,会置一个标志位TXE,我们检查这个标志位就会知道当前移位寄存器是否为空。TXE = 1 ,数据还没彻底发送完毕;TXE = 0,数据彻底发送完毕)。随后,发送移位寄存器会在在发送器控制的作用下一位一位的将数据发送给左边的Tx引脚。
简洁:数据从TDR移动到发送移位寄存器,TDR会置TXE = 1,可以写入下一个数据
接收:数据在接收器控制的驱动下一位一位的读取Rx引脚的电平状态。以低位先行的模式储存在接收移位寄存器中。接收完毕后,数据会从接收数据寄存器移动到RDR中,此时RDR会置一个标志位RXNE,当RXNE = 1时,此时总线就可以把数据读走了。当RXNE = 0时,此时接收数据寄存器还没彻底接收完数据,不能读走。
简洁:接收移位寄存器接收数据完成后,将数据移动到DRD,此时DRD会置RXNE = 1,总线可以读取数据了;
图中绿色框图内是硬件数据流控。他有两个引脚:nRTS和nCTS。前面的n代表低电平有效
nRTS是请求发送,是输出脚。人话:告诉别人我当前能不能接收。
nCTS是清除发送,是输入脚,接收别的外设的nRTS信号的。
两个设备A和B之间互相通信,A设备的nRTS接到B设备的nCTS,A设备的nCTS接到B设备的nRTS。比如说此时A发数据给B,那么B的nRTS可以发送一个低电平信号到A的nCTS来告诉A,我现在可以接收信号。于是A设备就开始给B设备发送数据。当A数据发送过快,B设备接收过慢的时候,B设备的nRTS就会向A设备的nCTS发送一个高电平的信号,告诉A设备,我现在接收不过来了。
蓝色框图中的电路是用来产生同步信号的,他是配合发送移位寄存器输出的。发送数据寄存器每移位一次,同步时钟电平就跳变一个周期。
人话:喂,我移动了一位数据,你看要不要我的时钟信号来指导你接收一下。
用途:1. 兼容别的协议;串口 + 时钟 ≈ SPI协议
2.自适应波特率;别的设备不知道波特率,可以时钟测量一下。这就需要另外的程序来执行了
黄色框图中的唤醒单元是实现串口挂载多设备。他会给串口分配一个地址,当我发送指定的地址时,此设备唤醒开始工作。发送别的设备地址时,别的设备就唤醒开始工作。设备收到地址就会保持沉默。
串口只支持点对点的通信,多设备是在一条总线上有多个设备进行数据传输。每个设备分配一个地址我,我想跟哪个设备进行数据传输我就先寻址,确定通信对象后再进行数据传输。
下面红圈圈出来的是中断申请位,就是状态寄存器(紧挨着的蓝色框)里的标志位。里面的TXE和RXNE标志位是判断发送状态和接收状态的必要寄存器。中断控制是是否通向NVIC进入中断;
最后是波特率发生器部分。其实就是分频器。时钟输入是fPCLKx(x = 1 或 2)
PCLK1 = 36M。PCLK2 = 72M。USART2连接到PCLK2总线。
这个时钟进行分频。除以一个USARTDIV的分频系数,再除以16,就得到了发送器时钟和接收器时钟,通向发送器控制和接收器控制。
USARTDIV是右边绿色框的部分,分为整数和小数部分。
TE = 1 发送器使能,发送器波特率有效。RE = 1 接收器使能,接收器波特率有效
USART基本结构:

数据帧:


起始位侦测:

它是将起始帧这一位分成了16份。检测到下降沿开始,他就开始将起始位分为16份。如果后面的都是0,说明没有噪声,就是起始位。但是实际上会有一些噪声。所以他会在3 5 7 再采一次样,8 9 10次再采一次样。这两次采样都要求每三位里面至少有两个0。
如果全是0,那就是起始位。
如果两个0,一个1,也是起始位,但是状态寄存器里面会置一个标志位NE,噪声标志位。就是告诉你,数据有噪声,你悠着点用。
如果之后一个0,两个1。或者全是1,那就说明前面那个下降沿是由噪声引起的,不是起始位下降沿。再从此时的位置向后检测,重新捕获下降沿。
如果3 5 7位通过了检测(里面至少有两个0),那么接收状态就由空闲变为接收起始位。刚好8 9 10位是数据的中央,刚好可以接收到准确的高低电平。(避免刚好检测到上升沿或下降沿)
代码:
步骤:
- 打开RCC时钟,把需要用到的USART和GPIO时钟打开
- GPIO初始化,把Tx设置为复用输出,Rx设置为输入
- 配置USART,结构体
- 配置ITconfig和NVIC的功能
- 开启USART
Serial.h模块:
#ifndef __SERIAL__H #define __SERIAL__H #include <stdio.h> void Serial_Init(void); void Serial_SendByte(uint8_t Byte); void Serial_SendArray(uint8_t *Array, uint16_t Length); void Serial_SendString(char *String); void Serial_SendNumber(uint32_t Number, uint8_t Length); void Serial_printf(char *format, ...); #endif
Serial.c模块:
#include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h> /* printf()函数默认输出到屏幕,所以需要重映射到串口 */ /* * * 1.打开RCC时钟,把需要用到的USART和GPIO时钟打开 * 2.GPIO初始化,把Tx设置为复用输出,Rx设置为输入 * 3.配置USART,结构体 * 4.配置ITconfig和NVIC的功能 * 5.开启USART * */ void Serial_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); GPIO_InitTypeDef GPIOA_InitStructure; GPIOA_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIOA_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIOA_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIOA_InitStructure); USART_InitTypeDef USART1_InitStructure; USART1_InitStructure.USART_BaudRate = 9600; //配置波特率的数值,他会自己算USARTDIV的值 USART1_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控设置 USART1_InitStructure.USART_Mode = USART_Mode_Tx; //选择发送还是接收模式 USART1_InitStructure.USART_Parity = USART_Parity_No; //校验 USART1_InitStructure.USART_StopBits = USART_StopBits_1; //停止位 0.5 1 1.5 2 USART1_InitStructure.USART_WordLength = USART_WordLength_8b; //选择数据长度 USART_Init(USART1, &USART1_InitStructure); USART_Cmd(USART1, ENABLE); } //发送一个字节 void Serial_SendByte(uint8_t Byte) { USART_SendData(USART1, Byte); //等待标志位,数据从TDR移动到发送数据寄存器后会将TEX置1 while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } //发送一个数组 void Serial_SendArray(uint8_t *Array, uint16_t Length) { uint16_t i = 0; for(i = 0; i < Length; i ++) { Serial_SendByte(Array[i]); } } //发送一个字符串 void Serial_SendString(char *String) { uint8_t i = 0; for(i = 0; String[i] != '\0'; i ++) { Serial_SendByte(String[i]); } } uint32_t Serial_Pow(uint32_t x, uint32_t y) { uint32_t result = 1; while(y --) { result *= x; } return result; } //发送一个十进制数 void Serial_SendNumber(uint32_t Number, uint8_t Length) { uint32_t i = 0; for(i = 0; i < Length; i ++) { Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); } } /* * 第一种 * fputc()函数是printf()函数的底层 * printf()函数在使用的时候是不断去调用fputc()这个函数的 * 这种方法是将printf重定向到串口1,串口2再用的话就用不了了 */ //重写fputc()函数 int fputc(int ch, FILE *f) { //重定向到串口 Serial_SendByte(ch); return ch; } /* * 第三种方法 * 将第二种方法封装起来 * 先添加头文件 #include <stdarg.h> * 因为sprintf()参数的第二个位置会变化 * 所以需要可变参数列表 */ void Serial_printf(char *format, .../*可变参数列表*/) //参数用来接收格式化参数 { char string[100]; va_list arg; //定义一个参数列表变量 va_start(arg, format); //从format里面接收参数列表放在arg里面 vsprintf(string, format, arg); va_end(arg); //释放参数表 Serial_SendString(string); }
main.c模块:
#include "stm32f10x.h" // Device header #include "Delay.h" #include "Oled.h" #include "Serial.h" int main(void) { OLED_Init(); Serial_Init(); // Serial_SendByte(0x41); // uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45}; // Serial_SendArray(MyArray, 4); // Serial_SendString("Hello World!\r\n"); //Serial_SendNumber(12345, 5); // printf("Num = %d\r\n",789); /* 如果多个串口都想用 * 第二种方法 * 先定义一个字符串 * 通过sprintf将内容打印到字符串中 * 再通过发送字符串的函数来实现 */ // char string[100]; // sprintf(string, "Num = %d\r\n", 789); // Serial_SendString(string); // Serial_printf("你好,世界"); while(1) { } }
更多推荐

所有评论(0)