串口通信是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。在嵌入式开发中,异步串口通信(UART)作为最基础的通信方式,运用非常广泛。常用在板间通信,以及主板与模块、外设间的通信。
串口通信是以字符为单位进行传输的,字符之间没有固定的时间间隔要求,而每个字符中的各个位则以固定的时间传送。异步通信中,收发双方取得同步是通过在字符格式中设置起始位和停止位的方法来实现的。具体来说就是,在一个有效字符正式发送之前,发送器先发送一个起始位,然后发送有效字符位,在字符结束时再发送一个停止位,起始位至停止位构成一帧。停止位至下一个起始位之间是不定长的空闲位,并且规定起始位为低电平(逻辑值为0),停止位和空闲位都是高电平(逻辑值为1),这样就保证了起始位开始处一定会有一个下跳沿,由此就可以标志一个字符传输的起始。而根据起始位和停止位也就很容易的实现了字符的界定和同步。
发送数据的具体步骤如下:
接收数据的具体步骤如下:
假设串口通信的波特率为B(每秒传送数据的位数),则由T=1/B即可得出每发送或者接受一个位所需的时间。发送与接收的波特率必须相同,才能保证发送与接收的同步。
串口波特率就是每秒钟传输的数据位数,用于描述通信速率快慢的描述。常见波特率有:600bps,1200bps,2400bps,4800bps,9600bps,115200bps,19200bps,38400bps。波特率数值越大,传输速率越快。波特率并不是越大越好,还需要保证数据传输的稳定性。
起始位: 起始位为"0"即低电平
数据位: 起始位之后就是传送数据位。数据位一般为8位一个字节的数据(也有6位、7位的情况),标准的ASCII码是0~ 127(7位),扩展的ASCII码是0~255(8位),低位(LSB)在前,高位(MSB)在后;
奇偶检验位: 在标准ASCII码中,其最高位(D7)用作奇偶校验位。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位D7添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位D7添1。
停止位: 停止位是按长度来算的。串行异步通信从计时开始,以单位时间为间隔(一个单位时间就是波特率的倒数),依次接受所规定的数据位和奇偶校验位,并拼装成一个字符的并行字节;此后应接收到规定长度的停止位“1”。所以说,停止位都是“1”,1.5是它的长度,即停止位的高电平保持1.5个单位时间长度。一般来讲,停止位有1、1.5、2个单位时间三种长度。
串口、UART口、COM口、USB口是指的物理接口形式(硬件)。
串口: 串口是一个泛称,UART、TTL、RS232、RS485都遵循类似的通信时序协议,因此都被通称为串口。
COM口: 特指台式计算机或一些电子设备上的D-SUB外形(一种连接器结构,VGA接口的连接器也是D-SUB)的串行通信口,应用了串口通信时序和RS232的逻辑电平。目前主流的主板一般都只带1个COM口,甚至不带,慢慢会被USB取代。
UART口: UART是串口收发的逻辑电路,采用TTL电平。这部分可以独立成芯片,也可以作为模块嵌入到其他芯片里,单片机、PC里都会有UART模块。
USB口: 通用串行总线,和串口完全是两个概念。虽然也是串行方式通信,但由于USB的通信时序和信号电平都和串口完全不同,因此和串口没有任何关系。USB是高速的通信接口,用于PC连接各种外设,U盘、键鼠、移动硬盘。USB虽然和串口不同,但是可以通过外加芯片(UART模块)和安装驱动的方式来模拟串口的输出与输入。
TTL,RS232,RS485都是一种逻辑电平的表示方式,是串口的三种不同的电平标准。
TTL :全双工 (逻辑1: 2.4V–5V 逻辑0: 0V–0.5V)。 TTL指双极型三极管逻辑电路,市面上很多“USB转TTL”模块,实际上是“USB转TTL电平的串口”模块。这种信号0对应0V,1对应3.3V或者5V,与单片机、SOC的IO电平兼容。我们进行串口通信的时候从单片机直接出来的基本上都是TTL电平。
RS232:全双工(逻辑1:-15V–5V 逻辑0:+3V–+15V)。 是电子工业协会制定的异步传输标准接口,同时对应着电平标准和通信协议(时序),其电平标准:+3V~+15V对应“0”,-3V~-15V对应“1”。RS232 的逻辑电平和TTL 不一样但是协议是一样的。
RS485:半双工(逻辑1:+2V–+6V 逻辑0: -6V—2V)这里的电平指AB两线间的电压差。 RS485是一种串口接口标准,为了长距离传输采用差分方式传输,传输的是差分信号,即通过AB两根线的电压差作为电平信号。差分信号能有效地抵御外界因素的干扰,因为干扰对两根线影响是一样的,两根线的电压差不变,信号传递也就不会受干扰。与TTL、RS232只能一对一连接不同,RS-485在总线上是允许连接多达128个收发器。
常见的串口接口有D型九针插头与四针杜邦头两种
对于九针串口(图一)主要是用在RS232与RS485,而单片机的串口输出一般是TTL电平,所以用的是四针杜邦头连接的(图二、图三)。四针的杜邦头中的Vcc是为了给模块上电,如果是板间通信(双方都是上电状态),则Vcc线可以省去。对于九针串口中各针的作用可以参考下表:
开发环境:CubeMX Vesion 5.4.0
Keil Vesion 5.28
CubeMX中配置串口非常简单,以串口一为例:将模式(Mode)调为异步通信,参数栏中按照自己的需求来设置波特率、字符长度、奇偶校验以及停止位。需要注意的是这里的Word Length不是指数据位长度,而是指数据位+奇偶校验位的长度,如果你需要使用奇偶校验,那么这里需设置为9Bit才能够保证每次都发送与接收1个字节(8Bit)的数据,这里我没有使用奇偶校验,所以长度设为8。在设置好参数栏后,在NVIC设置中打开UART1的中断。
串口数据的发送
/* @brief 串口的发送函数
* @param huart 串口句柄
* @param pData 需要发送数组的指针(必须为uint8_t型数组,因为串口是单字节发送的)
* @param Size 数组的字节数
* @param Timeout 超时时间(在指定时间内未完成数据发送则返回HAL_TIMEOUT)
* @retval HAL_status 返回值 HAL_OK、HAL_ERROR、HAL_BUSY、HAL_TIMEOUT */
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
在需要的地方调用 HAL_UART_Transmit(········)便能完成串口数据的发送,例如:
uint8_t data_10[4]={11,22,33,44};//数组一(10进制数)
HAL_UART_Transmit(&huart1,data,8,0XFFFF);
uint8_t data_16[4]={0x11,0x22,0x33,0x44};//数组二(16进制数)
HAL_UART_Transmit(&huart1,data_16,4,0XFFFF);
uint8_t sentence[]="hello!you are welcome!";//数组三(字符串)
HAL_UART_Transmit(&huart1,sentence,sizeof(sentence),0XFFFF);
数据发送后,通过 上位机(XCOM)查看发送的数据:
发送函数放在了main中的while(1)循环中,所以看到的是密密麻麻的数字
1. 十进制数在串口发送前必须转化为相对应的二进制数,然后上位机接收发来的数据后,通过16进制来解析,便能得到十进制数相对应的16进制数。如图一:数组一中的11、22、33、44对应的16进制数分别是0B、16、21、2C。
2. 如果发送的是16进制数,在16进制显示下,数组里是什么数据,上位机显示的就是什么数据。
3. 如果发送的是字符串,如果依旧用16进制显示,那么上位机将输出字符串中每个字符的ASCLL编号的16进制数。如果采取非16进制显示,那么上位机会用ASCLL去解析收到的数据,即你发送什么字符串,上位机就输出什么字符串。像图三、图四分别是数组三16进制与非16进制显示,字符串"hello!you are welcome!"的16进制便是[68 65 6C 6F 21·····6D 65]。
串口接收可以分为 定长接收 与 不定长接收 。
定长接收 就是要接收的数据量大小是事先知道的,比如MCU与伺服电机串口通信,电机会间断地发送电机转速(2个字节)给MCU,这便是定长接收,MCU每次接收的数据量为两个字节。定长接收的实现非常简单。只需要处理好下面三个函数。
/* @brief 串口接收中断函数
* @param huart 串口句柄
* @param pData 用于接收数据的缓冲数组指针
* @param Size 缓冲数组字节数
* @retval HAL_Status 返回值 HAL_OK、HAL_ERROR、HAL_BUSY、HAL_TIMEOUT
* */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
/*Cube自动生成的串口一中断服务函数,在stm32f4xx_it.c中*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
//串口接收回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){;}
举个例子,需要接收一个2字节的数据:
uint8_t data_buffer[2];//接收缓冲数组,设为全局变量
/*在main中开启接收中断,在数据接收完成后会进入接收回调函数*/
HAL_UART_Receive_IT(&huart1, data_buffer, 2);
/*Cube自动生成的串口一中断服务函数中加入中断接收,方便下一次数据的接收*/
/*每一个字节的接收完成都会产生中断,进入到中断服务函数当中*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
extern uint8_t data[2];//全局变量声明
HAL_UART_Receive_IT(&huart1, data, 2);//开启中断接收
/* USER CODE END USART1_IRQn 1 */
}
/*接收回调函数,中断接收中指定的数据接收完成后会执行回调函数*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{printf("\r\n 收到数据 %s",data);}//打印出接收到的数据(printf重定向输出后面会讲到)
}
反之就是 不定长接收 ,即接收的数据量是不确定的,可能是1个字节,也有可能是100个字节。不定长接收稍微麻烦一点,下面给出的代码是模仿正点原子教程写的,即通过最后两个字节是否为“0x0A”“0x0D”来判断接收是否完成,实现的功能是将上位机发送来的消息接收后再发送给上位机,实现代码如下:
//设为全局变量
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_RxBuff[256]; //
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
uint8_t cAlmStr[] = "数据溢出(大于256)\r\n";
//在main中开启中断接收
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
//中断回调函数中判断是否接收完成
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
UNUSED(huart); /* To avoid gcc/g++ warnings */
if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;//Uart1_Rx_Cnt为接收缓冲计数值,当达到256时,将清零
memset(Uart1_RxBuff,0x00,sizeof(Uart1_RxBuff));//对数组Uart1_RxBuff[]进行清零
HAL_UART_Transmit(&huart1, (uint8_t *)&cAlmStr, sizeof(cAlmStr),0xFFFF);
}
else
{
Uart1_RxBuff[Uart1_Rx_Cnt++] = aRxBuffer;
if((Uart1_RxBuff[Uart1_Rx_Cnt-1] == 0x0A)&&(Uart1_RxBuff[Uart1_Rx_Cnt-2] == 0x0D)) //判断是否到0x0A、oxoD结尾
{
HAL_UART_Transmit(&huart1, (uint8_t *)&Uart1_RxBuff, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
Uart1_Rx_Cnt = 0;
memset(Uart1_RxBuff,0x00,sizeof(Uart1_RxBuff)); //清空数组
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1); //重新开启接收中断,方便下一次接收
}
串口只能够一个个字节的发送,那么如果遇到浮点数这类多字节数据传输怎么办呢?
答: 有两种解决办法,一种是可以把它转化为字符串发送过去(比如:123.45转化“123.45”),另一种则是把浮点数拆分成单个字节,再发送过去,接收方收到数据后再重新拼凑出浮点数。对于前者而言,必须要有能够将数值转化为字符串的算法,是不是非常麻烦呢?其实并不需要这么麻烦,只需要重定向printf函数就能完成这项操作,并不局限于浮点数,所有数值型数据以及字符型数据都能够使用printf函数实现串口发送。
重定向需要完成三步:
初始化串口
包含stdio.h头文件
在keil中勾选使用C库 (Use MicroLIB)
重写fputc函数(以串口1为例)
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1 , (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
测试一下:
printf("Hello, I am %s\r\n", "马云");//printf输出字符串
printf("Test int: i = %d\r\n", 100);//printf输出int型数据
printf("Test float: i = %f\r\n", 1.234);//printf输出float型数据
printf("Test hex: i = 0x%2x\r\n",100);//printf输出16进制数据
printf("Test all: i = %d i = %f i = 0x%2x",100,1.234,100);//同时输出所有数据类型
/* 注意:“/r/n”是换行操作 */
至于printf的使用和C语言并没有什么区别。printf函数中数据类型所对应的符号也加在上面这份测试图中了。
既然有了printf,那么为什么还要自定义打印函数呢?
答: 由于笔者比较蠢,只会同时重定向一个串口,当遇到多个串口都需要使用printf时就没有办法了,所以只能自己定义打印函数,让所有串口都能像使用printf函数一样做串口输出。
首先需要包含几个头文件:
#include
#include
#include
其次,定义打印函数(以串口一为例):
/**
* @brief 自定义UART1串口打印
* @param *fmt,... 要打印的数据内容,用法类似printf
* @retval void
*/
extern UART_HandleTypeDef huart1;
void My_printf(char* fmt,...)
{
static __align(8) uint8_t USART1_buffer[128];
va_list ap;
va_start(ap,fmt);
vsnprintf((char*)USART1_buffer,2000,fmt,ap);
va_end(ap);
HAL_UART_Transmit(&huart1 , USART1_buffer, strlen((const char*)USART1_buffer), 0xFFFF);
}
使用的方法与printf函数类似:
My_printf("自定义串口打印函数测试:\r\n");
My_printf("Hello, I am %s\r\n", "马云");
My_printf("Test int: i = %d\r\n", 100);
My_printf("Test float: i = %f\r\n", 1.234);
My_printf("Test hex: i = 0x%2x\r\n",100);
My_printf("Test all: i = %d i = %f i = 0x%2x ",100,1.234,100);
测试结果如下:
本文给出的代码都是实现过程中最核心的部分,部分细节操作没有展示。如果看完上述教程依旧没有办法实现,那么可以参考下面的示例代码。因为是临时写的,有些地方并不是很规范,但是都是仿真调试过得,亲测有效。
1. HAL库+CubeMX+Stm32F407 实现串口不定长接收
2. HAL库+CubeMX+Stm32F405 实现串口定长收发、printf函数使用、自定义打印函数