串口基础知识
串口介绍
串口是指外设和处理器之间通过数据信号线、地线和控制线等,按位进行传输数据的一种通讯方式。尽管传输速度比并行传输低。但串口可以在使用一根线发送数据的同时用另一根线接收数据。这种通信方式使用的数据线少,在远距离通信中可以节约通信成本。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验位,这些参数在两个通信端口之间必须一致。
串口通信参数介绍
- 波特率: 衡量通信速度的参数,它表示每秒钟传送的 bit 的个数。
- 数据位: 衡量通信中实际数据位的参数,表示一个信息包里包含的数据位的个数。
- 停止位: 用于表示单个信息包的最后位,典型值为 1、1.5 和 2 位。由于数据是在传输线上传输的,每个设备都有自己的时钟,很有可能在通信过程中出现不同步,停止位不仅仅表示传输的结束,还能提供校正时钟同步的机会。停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率也越慢。
- 奇偶检验位: 表示一种简单的检查错误的方式。
串口工作模式
串口可以工作在单工、半双工和全双工模式下。
- 单工:在通信的任意时刻,信息只能由 A 传到 B。
- 半双工:在通信的任意时刻,信息即可由 A 传到 B,又能由 B 传到 A,但同时只能有一个方向上的传输存在。
- 全双工:在通信的任意时刻,通信线路上存在 A 到 B 和 B 到 A 的双向信号传输。
串口通信协议
串口在进行通信的时候会按照数据包的形式进行发送,帧格式如图所示。
串口通信是一位一位地传输,每传输一个字节总是以起始位开始,以停止位结束,字符之间没有固定的时间间隔要求。每一个字符的前面都有一位起始位(低电平),后面由 8 位数据位组成,如果开启了校验位,则最后一位数据位是校验位,最后是停止位。停止位后面是不定长的空闲位,停止位和空闲位都规定为高电平。
配置流程
一般我们使用串口,都需要有以下几个步骤。
- 开启时钟(包括串口时钟和 GPIO 时钟)
- 配置 GPIO 复用模式
- 配置 GPIO 的模式
- 配置 GPIO 的输出
- 配置串口(配置一些参数)
- 使能串口(串口使能和发送使能)
开启时钟
使用串口 1 的话就是 PA9 和 PA10 引脚。那第一步就是先开启端口 A 的时钟,在前面的学习给大家介绍了使能时钟的函数 RCC_AHB1PeriphClockCmd,只需要传入对应的参数即可。使能端口 A 的时钟就把 GPIOA 的时钟当做参数传入。第二步就是开启串口的时钟,把对应的串口 1 的时钟 RCC_APB2Periph_USART1 传入即可。为了方便后续修改,把端口 A 的时钟和串口 1 的时钟用宏定义定义,如下:
#define BSP_USART_RCC RCC_APB2Periph_USART1
#define BSP_USART_TX_RCC RCC_AHB1Periph_GPIOA
#define BSP_USART_RX_RCC RCC_AHB1Periph_GPIOA
然后对应的使能时钟的代码就是
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启串口1的时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); // 开启端口时钟
配置 GPIO 复用模式
STM32 的引脚是可以有复用功能的,就是说单个引脚可有很多个功能,默认的功能一般都是作为 GPIO 使用。在 STM32f4xx_gpio.h 中可以查找到设置复用的函数
void GPIO_PinAFConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_PinSource, uint8_t GPIO_AF);
这个函数有三个参数,第一个参数就是要配置的引脚端口,第二个参数就是要复用的功能,第三个参数就是要配置的引脚。关于最后一个 AF 参数可以到数据手册查找,如图所示。
从图可以看到 PA9 对应的 USART1_TX 的功能复用为 AF7,PA10 对应的 USART1_RX 的功能复用为 AF7,在库函数中官方已经封装了 AF 所以我们使用相应的参数:GPIO_AF_USART1 即可,代码编写如下。首先定义一下端口和复用功能的宏定义。
#define BSP_USART USART1
#define BSP_USART_TX_PORT GPIOA
#define BSP_USART_TX_PIN GPIO_Pin_9
#define BSP_USART_RX_PORT GPIOA
#define BSP_USART_RX_PIN GPIO_Pin_10
#define BSP_USART_AF GPIO_AF_USART1
#define BSP_USART_TX_AF_PIN GPIO_PinSource9
#define BSP_USART_RX_AF_PIN GPIO_PinSource10
然后调用复用函数使能复用功能。
//IO口用作串口引脚要配置复用模式
GPIO_PinAFConfig(BSP_USART_TX_PORT, BSP_USART_TX_AF_PIN, BSP_USART_AF);
GPIO_PinAFConfig(BSP_USART_RX_PORT, BSP_USART_RX_AF_PIN, BSP_USART_AF);
通过上面两句就可以把 PA9,PA10 设置为串口功能了。
配置 GPIO
配置 GPIO 的模式还是使用 GPIO_Init 这个函数,不同的是第二个参数要配置为复用功能而不是输出功能,第三个参数要配置为上拉,转化为代码如下。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = BSP_USART_TX_PIN; //TX引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //IO口用作串口引脚要配置复用模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = BSP_USART_RX_PIN; //RX引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //IO口用作串口引脚要配置复用模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA,&GPIO_InitStructure);
为什么要调用GPIO_StructInit(&GPIO_InitStructure);是因为要将第一次的成员变成默认值,在进行第二次配置。
配置串口
我们依然使用结构体的方式进行配置串口数据,里面有些参数是必须要配置的:
- 波特率:我们使用形参__Baud 可以自由调整波特率
- 字节长度:8 位
- 停止位:1 位停止位
- 校验位:不需要校验位
- 收发模式:收发
- 流控选择:不流控
相关的代码:
USART_StructInit(&USART_InitStructure);
USART_InitStructure.USART_BaudRate = __Baud;//设置波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字节长度为8bit
USART_InitStructure.USART_StopBits = USART_StopBits_1;//1个停止位
USART_InitStructure.USART_Parity = USART_Parity_No ;//没有校验位
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//将串口配置为收发模式
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不提供流控
USART_Init(BSP_USART,&USART_InitStructure);//将相关参数初始化给串口1
USART_ClearFlag(BSP_USART,USART_FLAG_RXNE);//初始配置时清除接受置位
使能串口
串口配置好之后并不能开始工作,还需要去使能,就是相当于有一个开关可以打开关闭。要使用串口,首先要打开总开关,
USART_Cmd(BSP_USART,ENABLE); //开启串口1
这样就能正常使用串口了,那么串口又有那些函数呢?我们可以打开usart.h文件中查看
可以看到有相当多的函数,感兴趣的朋友们可以去里面仔细看看,下面我就介绍一下我们今天要用的函数。
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);发送一个字节的函数,第一个参数是串口的端口号,第二个参数是要发送的数据。
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);获取标志位的函数,状态位选项为图所示。
/**
* @brief Checks whether the specified USART flag is set or not.
* @param USARTx: where x can be 1, 2, 3, 4, 5, 6, 7 or 8 to select the USART or
* UART peripheral.
* @param USART_FLAG: specifies the flag to check.
* This parameter can be one of the following values:
* @arg USART_FLAG_CTS: CTS Change flag (not available for UART4 and UART5)
* @arg USART_FLAG_LBD: LIN Break detection flag
* @arg USART_FLAG_TXE: Transmit data register empty flag
* @arg USART_FLAG_TC: Transmission Complete flag
* @arg USART_FLAG_RXNE: Receive data register not empty flag
* @arg USART_FLAG_IDLE: Idle Line detection flag
* @arg USART_FLAG_ORE: OverRun Error flag
* @arg USART_FLAG_NE: Noise Error flag
* @arg USART_FLAG_FE: Framing Error flag
* @arg USART_FLAG_PE: Parity Error flag
* @retval The new state of USART_FLAG (SET or RESET).
*/
从图可以了解到当将要发送的数据写入 USART_DATA 时,此位被清 0,当数据发送完成之后,此位置 1。所以当检测到此位为 1 时就表明当前数据缓冲区为空,可以继续发送数据。
关于串口发送数据可以封装为一个函数如下。
void usart_send_data(uint8_t ucch)
{
USART_SendData(BSP_USART, (uint8_t)ucch);
// 等待发送数据缓冲区标志置位
while( RESET == USART_GetFlagStatus(BSP_USART, USART_FLAG_TXE) ){}
}
当我们调用这个函数时
usart_send_data('h');
usart_send_data('e');
usart_send_data('l');
usart_send_data('l');
usart_send_data('o');
使用串口助手可以看到发送过来的数据
同样的我们也可以封装一个字符串函数
void usart_send_String(uint8_t *ucstr)
{
while(ucstr && *ucstr) // 地址为空或者值为空跳出
{
usart_send_data(*ucstr++);
}
}
usart_send_String("hello\r\n");
这样也能收到数据。
串口重定向
上面封装的发送字符串的方式打印信息看似方便,但如果我们想要打印数字,小数,该怎么打印呢?大家是否习惯使用了 printf 这个函数,可以通过 %d,%f 打印整形和小数,这一小节就教大家怎么把串口重定向到 printf 函数。
串口重定向介绍
C 语言中的 printf 函数默认输出设备是显示器,如果要在串口显示,必须重新定义标准库函数里调用的与输出设备相关的函数。需要注意的是,在 keil 中使用 printf 一定要勾选“微库”选项。
printf 重定向
首先 c 语言的 printf 函数中不断循环调用 fputc 函数,所以需要重写 fputc 函数,这个函数的功能就是打印输出一个字符,这不正和我们编写的 usart_send_data 函数功能一样。fputc 函数可写为
#if !defined(__MICROLIB)
//不使用微库的话就需要添加下面的函数
#if (__ARMCLIB_VERSION <= 6000000)
//如果编译器是AC5 就定义下面这个结构体
struct __FILE
{
int handle;
};
#endif
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
#endif
/* retarget the C library printf function to the USART */
int fputc(int ch, FILE *f)
{
USART_SendData(BSP_USART, (uint8_t)ch);
while( RESET == USART_GetFlagStatus(BSP_USART, USART_FLAG_TXE) ){}
return ch;
}
编写好 fputc 函数之后就可以使用 printf 函数输出信息了。