通信的基础
串行通信与并行通信
全双工、半双工及单工通信
单工通信:数据只能沿一个方向传输
半双工通信:数据可以沿两个方向传输,但需要分时进行
全双工通信:数据可以同时进行双向传输
同步通信与异步通信
- 同步通信
同步通信:发送和接收双方按照预定的时钟节拍进行数据的发送和接收,双方的操作严格同步。
- 异步通信
异步通信:双方不需要严格的时钟同步,每个数据块之间通过特定的起始位和停止位进行分隔,接收方可以独立地识别每个数据块。
通信速率
通信速率是指在通信系统中单位时间内传输的信息量,是评估通信系统性能的重要指标之一。
- 比特率(Bit rate)
定义:比特率是指在通信线路(或系统)中单位时间(每秒)内传输的信息量,即每秒能传输的二进制位数。它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为比特/秒(bit/s或 bps)。
含义:比特率越高,表示单位时间内传送的数据量越大,信息传输的速率越快。它经常被用作连接速度、传输速度、信息传输速率和数字带宽容量的同义词。
- 波特率(Baud rate)
定义:在电子通信领域,波特率表示每秒传送的码元的个数,即单位时间内载波调制状态变化的次数。它用单位时间内载波调制状态改变次数来表示,其单位为波特(Baud)。
含义:波特率描述的是单位时间内调制信号的能力,它决定了在给定时间内可以通过通信通道发送多少个离散的信号单元(码元)。在数字通信中,码元是表示数字信息的最小单位。
-
tips
比特率 = 波特率 * log2 M ,M表示每个码元承载的信息量
二进制系统中,波特率数值上等于比特率
串口通信简介
串口,也称为串行接口或串行通信接口(通常指COM接口),是一种采用串行通信方式的扩展接口。它实现了数据一位一位地顺序传送,具有通信线路简单、成本低但传送速度慢的特点。只要一对传输线,串口就可以实现双向通信(全双工通信)。
-
串口通信的接口类型:(不同的电平标准)
TTL |
逻辑
1
:
5V
,逻辑
0
:
0V
|
CMOS |
逻辑
1
:供电电压的最大值,逻辑
0
:
0V
|
RS-232 |
逻辑
1
:
-3V
~
-15V
,逻辑
0
:
+3
~
+15V
|
RS-408 |
采用
差分信号
,逻辑
1
:两线间的电压差为
+(0.2~6)V
,逻辑
0
:两线间的电压差为
-(0.2~6)V
|
- 常用的是TTL电平标准:
-
同步串口通信的框图 --- 同步串口不常用
起始位(Start Bit)
| 起始位为低电平时,告诉接收方数据传输即将开始,准备接收。在通信开始时,发送端首先会发送一个起始位,它是一个逻辑0(低电平)的信号,用于同步发送和接收设备之间的时钟。接收端在检测到起始位后,会开始准备接收后续的数据位。 |
有效数据位(Data Bits)
| 数据位是由一系列二进制值组成,用于传输或接收实际的数据。数据位的数量决定了可以传输的不同二进制值的数量,常见的有5位、6位、7位、8位,LSB在前,MSB在后。数据位紧随起始位之后,包含了要传输的实际信息。 举例:0x50对应的二进制 0000,0101 在数据帧中低位先行(LSB),如下: |
校验位(Parity Bit)
|
校验位用于验证数据的完整性,以确保传输过程中没有出现错误。常见的校验位选项有
None(无校验位)、Odd(奇校验位)和Even(偶校验位)
。在发送数据时,校验位会根据
数据位中1的个数
进行计算,并加入到数据中一起传输。接收端则会根据校验位的值进行校验,以判断数据是否存在错误。
举例:如果数据帧中1的个数是3,校验位设置的是Even(偶校验)则校验位会置1,将1的个数变成偶数。
|
停止位(Stop Bit)
| 停止位是一个逻辑高电平(1),用于指示数据传输的结束。当停止位出现时,接收端知道数据传输已经完成,并且可以开始处理接收到的数据。停止位位于数据位和校验位之后,它的作用是确保接收端有足够的时间来识别数据帧的结束,并为下一个数据帧的到来做好准备。 |
STM32的USART简介
Universal synchronous asynchronous receiver transmitter,通用同步异步收发器
Universal asynchronous receiver transmitter,通用异步收发器(常用)
特点:
-
全双工通信:USART支持全双工通信,即数据可以在两个方向上同时传输(A→B且B→A)。这使得USART能够满足许多需要双向通信的应用场景。
-
同步与异步传输:尽管USART的“S”代表同步,但在实际应用中,USART更常用于异步通信。然而,它也支持同步通信模式,只是这种模式通常用于兼容其他协议或特殊模式,并且两个USART设备不能通过同步模式进行直接通信。
-
波特率发生器:USART自带波特率发生器,最高可达4.5Mbits/s,可以根据需要配置不同的波特率。
-
4. 硬件流控制:USART支持硬件流控制,通过特定的信号线(如RTS/CTS)实现数据的可靠传输。当接收端没有准备好接收数据时,可以通过RTS信号通知发送端暂停发送;当接收端准备好接收数据时,再通过CTS信号通知发送端恢复发送。
USART框图
- 状态寄存器
TE和RE对应的是波特率的TE和RE:发送使能和接收使能
TXE:(发送1个字符时看这个)发送数据寄存器为空时,置1;TC(发送一堆字符时看这个):表示发送数据寄存器和发送移位寄存器发送完1 。
TCIE:置1产生一个中断,;RXNE:接收数据寄存器中存在数据时(非空),置1;RXNEIE:接收数据产生一个中断,用于接收一个不定长的数据。
IDLE:空闲,表示接收寄存器和接收移位寄存器处于空闲的状态。IDLE IE:空闲时,产生一个中断。用于:检测不定长数据接收完成。
串口寄存器
- 状态寄存器(USART_SR)
- 数据寄存器(USART_DR)
- 波特比率寄存器(USART_BRR)
- 控制寄存器 1(USART_CR1)
- 控制寄存器 2(USART_CR2)
- 控制寄存器 3(USART_CR3)
串口常用的库函数
- HAL_UART_Init(UART_HandleTypeDef *huart)
- UART_InitTypeDef Init;
小实验1:通过串口发送/接受一个字符
实验目的
使用串口发送/接收一个字符。
硬件清单
开发板、ST-Link、USB转TTL
硬件接线
文件代码
- usart1.c文件代码
#include "uart1.h"
UART_HandleTypeDef uart1_handle = {0};
void uart1_init(uint32_t boundrate){
uart1_handle.Instance = USART1;
uart1_handle.Init.BaudRate = boundrate; //波特率
uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; //数据位长度:8位
uart1_handle.Init.StopBits = UART_STOPBITS_1; //停止位:1位
uart1_handle.Init.Parity = UART_PARITY_NONE ; //校验位:无校验
uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //流控
uart1_handle.Init.Mode = UART_MODE_TX_RX; //单工或双工模式:双工模式
HAL_UART_Init(&uart1_handle);
}
//Msp初始化MCU相关外设---公用函数:要进行判断这个函数是否被别的函数占用
void HAL_UART_MspInit(UART_HandleTypeDef *huart){
if(huart->Instance == USART1){
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_initstruct;
//配置GPIOA GPIO_PIN_9 - TX
gpio_initstruct.Pin = GPIO_PIN_9;
gpio_initstruct.Mode = GPIO_MODE_AF_PP; //复用推挽输出。参考手册GPIO外设配置
gpio_initstruct.Pull = GPIO_NOPULL;
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_initstruct);
//配置GPIOA GPIO_PIN_10 - RX
gpio_initstruct.Pin = GPIO_PIN_10;
gpio_initstruct.Mode = GPIO_MODE_AF_INPUT;
gpio_initstruct.Pull = GPIO_PULLUP; //浮空输入或上拉输入。参考手册GPIO外设配置
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_initstruct);
//开始NVIC中断
HAL_NVIC_SetPriority(USART1_IRQn,2,2); //配置中断线,抢占优先级和响应优先级
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能中断
//使能获取数据的中断
__HAL_UART_ENABLE_IT(&uart1_handle,UART_IT_RXNE);
}
}
//中断服务函数
void USART1_IRQHandler(void){
//没有接收数据的中断回调函数
uint8_t recive_data = 0;
if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_RXNE) != RESET) //中断服务函数是一个公共的函数,用之前要进行判断是否时接收数据位非空引起的。
{
HAL_UART_Receive(&uart1_handle,&recive_data,1,1000); //接收的数据存放在哪里,接收数据的长度,超时(ms):数据在那边阻塞,
HAL_UART_Transmit(&uart1_handle,&recive_data,1,1000); //发送函数,接收到数据立即发送。
}
}
- usart.h文件代码
#ifndef __USART_H__
#define __USART_H__
#include "stm32f1xx.h"
#include "sys.h"
void uart1_init(uint32_t boundrate);
#endif
- main.c文件代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
while(1)
{
led1_on();
led2_off();
delay_ms(500);
led1_off();
led2_on();
delay_ms(500);
}
}
实验现象
小实验2:串口接收不定长数据(接收中断)
实验目的
利用串口接收不定长数据,采用接受中断+超时判断的方法
接收中断+超时判断
串口接收到一个数据时,就会触发接收中断。但如何判断数据已经发送完了呢?
通常来讲,两帧数据之间,会有个时间间隔。因此,我们可以使用一个计时器,如果在一个固定的时间点里没接收到新的字符,则认为一帧数据接收完成了。
文件代码
代码流程
- uart1.c文件代码
#include "uart1.h"
#include "stdio.h"
#include "string.h"
uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE];
uint16_t uart1_cnt = 0;
uint16_t uart1_cntold = 0;
uint16_t uart1_rx_len = 0;
UART_HandleTypeDef uart1_handle = {0};
void uart1_init(uint32_t boundrate){
uart1_handle.Instance = USART1;
uart1_handle.Init.BaudRate = boundrate; //波特率
uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; //数据位长度:8位
uart1_handle.Init.StopBits = UART_STOPBITS_1; //停止位:1位
uart1_handle.Init.Parity = UART_PARITY_NONE ; //校验位:无校验
uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //流控
uart1_handle.Init.Mode = UART_MODE_TX_RX; //单工或双工模式:双工模式
HAL_UART_Init(&uart1_handle);
}
//Msp初始化MCU相关外设---公用函数:要进行判断这个函数是否被别的函数占用
void HAL_UART_MspInit(UART_HandleTypeDef *huart){
if(huart->Instance == USART1){
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_initstruct;
//配置GPIOA GPIO_PIN_9 - TX
gpio_initstruct.Pin = GPIO_PIN_9;
gpio_initstruct.Mode = GPIO_MODE_AF_PP; //复用推挽输出。参考手册GPIO外设配置
gpio_initstruct.Pull = GPIO_NOPULL;
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_initstruct);
//配置GPIOA GPIO_PIN_10 - RX
gpio_initstruct.Pin = GPIO_PIN_10;
gpio_initstruct.Mode = GPIO_MODE_AF_INPUT;
gpio_initstruct.Pull = GPIO_PULLUP; //浮空输入或上拉输入。参考手册GPIO外设配置
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_initstruct);
//开始NVIC中断
HAL_NVIC_SetPriority(USART1_IRQn,2,2); //配置中断线,抢占优先级和响应优先级
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能中断线
//使能获取数据的中断
__HAL_UART_ENABLE_IT(&uart1_handle,UART_IT_RXNE);
}
}
//中断服务函数
void USART1_IRQHandler(void){
//没有接收数据的中断回调函数
uint8_t recive_data = 0;
if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_RXNE) != RESET) //中断服务函数是一个公共的函数,用之前要进行判断是否时接收数据位非空引起的。
{
if(uart1_cnt >= sizeof(uart1_rx_buf))
uart1_cnt = 0; //避免数据缓冲区被刷爆
HAL_UART_Receive(&uart1_handle,&recive_data,1,1000);
uart1_rx_buf[uart1_cnt] = recive_data;
uart1_cnt ++;
// HAL_UART_Transmit(&uart1_handle,&recive_data,1,1000);
}
}
//由于printf函数会调用fputc,进行重写
int fputc(int ch ,FILE *f){
//重定向到串口1中
while((USART1->SR & 0x40) == 0);
USART1->DR = (uint8_t)ch;
return ch;
}
//判断是否数据传输完成
uint8_t uart1_wait_receive(void){
if(uart1_cnt == 0)
return UART_ERROR;
if(uart1_cnt == uart1_cntold){
uart1_cnt = 0;
return UART_EOK;
}
uart1_cntold = uart1_cnt;
return UART_ERROR;
}
//接收寄存器的值清空
void uart1_rx_clear(void){
memset(uart1_rx_buf,0,sizeof(uart1_rx_buf)); //清除数组中的内容
uart1_rx_len = 0;
}
//在主函数中调用
void uart1_receive_test(void){
if(uart1_wait_receive() == UART_EOK){
//表示数据接收完成,请求发送数据:这里有两种方法,1.利用发出数据的函数,2.重写printf函数,进行打印。
printf("rec: %s \r\n",uart1_rx_buf);
uart1_rx_clear();
}
}
- uart1.h文件代码
#ifndef __USART_H__
#define __USART_H__
#include "stm32f1xx.h"
#include "sys.h"
#define UART1_RX_BUF_SIZE 128
#define UART1_TX_BUF_SIZE 64
#define UART_EOK 0
#define UART_ERROR 1
#define UART_ETIMEOUT 2
#define UART_EINVAL 3
void uart1_init(uint32_t boundrate);
void uart1_receive_test(void);
#endif
- main.c文件代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
while(1)
{
uart1_receive_test();
delay_ms(10);
}
}
实验现象
遇到的问题和注意事项
- 关于重写printf函数
- memset函数
- 公共中断服务函数中的条件判断
- 计数器的状态。
小实验3:串口接受不定长数据(空闲中断)
空闲中断
串口在空闲时,也就是说串口在一段时间里没有接收到新数据,则会触发空闲中断。细心的同学应该发现了,空闲中断实际上跟上面的超时判断是一样样的,只不过空闲中断是硬件自带,但超时判断需要我们自己实现。
所以,一旦接收到空闲中断,可以认为接收到一帧完整的数据。
但是,空闲中断并不是所有的 MCU 都具备,一般高端一点的 MCU 才有,低端一些的 MCU 并没有空闲中断。
实验目的
当串口接收完数据后,会处于空闲的状态,产生一个空闲中断,
代码配置步骤
文件代码
- uart1.c文件代码
#include "uart1.h"
#include "stdio.h"
#include "string.h"
uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE];
uint16_t uart1_cnt = 0;
uint16_t uart1_cntold = 0;
uint16_t uart1_rx_len = 0;
UART_HandleTypeDef uart1_handle = {0};
void uart1_init(uint32_t boundrate){
uart1_handle.Instance = USART1;
uart1_handle.Init.BaudRate = boundrate; //波特率
uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; //数据位长度:8位
uart1_handle.Init.StopBits = UART_STOPBITS_1; //停止位:1位
uart1_handle.Init.Parity = UART_PARITY_NONE ; //校验位:无校验
uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //流控
uart1_handle.Init.Mode = UART_MODE_TX_RX; //单工或双工模式:双工模式
HAL_UART_Init(&uart1_handle);
}
//Msp初始化MCU相关外设---公用函数:要进行判断这个函数是否被别的函数占用
void HAL_UART_MspInit(UART_HandleTypeDef *huart){
if(huart->Instance == USART1){
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_initstruct;
//配置GPIOA GPIO_PIN_9 - TX
gpio_initstruct.Pin = GPIO_PIN_9;
gpio_initstruct.Mode = GPIO_MODE_AF_PP; //复用推挽输出。参考手册GPIO外设配置
gpio_initstruct.Pull = GPIO_NOPULL;
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_initstruct);
//配置GPIOA GPIO_PIN_10 - RX
gpio_initstruct.Pin = GPIO_PIN_10;
gpio_initstruct.Mode = GPIO_MODE_AF_INPUT;
gpio_initstruct.Pull = GPIO_PULLUP; //浮空输入或上拉输入。参考手册GPIO外设配置
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_initstruct);
//开始NVIC中断
HAL_NVIC_SetPriority(USART1_IRQn,2,2); //配置中断线,抢占优先级和响应优先级
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能中断线
//使能获取数据的中断
__HAL_UART_ENABLE_IT(&uart1_handle,UART_IT_RXNE);
//打开空闲中断
__HAL_UART_ENABLE_IT(&uart1_handle,UART_IT_IDLE);
}
}
//接收寄存器的值清空
void uart1_rx_clear(void){
memset(uart1_rx_buf,0,sizeof(uart1_rx_buf)); //清除数组中的内容
uart1_cnt = 0;
}
//中断服务函数
void USART1_IRQHandler(void){
//没有接收数据的中断回调函数
uint8_t recive_data = 0;
if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_RXNE) != RESET) //中断服务函数是一个公共的函数,用之前要进行判断是否时接收数据位非空引起的。
{
if(uart1_cnt >= sizeof(uart1_rx_buf))
uart1_cnt = 0; //避免数据缓冲区被刷爆
HAL_UART_Receive(&uart1_handle,&recive_data,1,1000);
uart1_rx_buf[uart1_cnt] = recive_data;
uart1_cnt ++;
// HAL_UART_Transmit(&uart1_handle,&recive_data,1,1000);
}
//表示数据接受完成,处于空闲状态
if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_IDLE) != RESET){
printf("receive:%s \n",uart1_rx_buf);
uart1_rx_clear(); //清除数组中元素
//手动清楚标志位
__HAL_UART_CLEAR_IDLEFLAG(&uart1_handle);
}
}
//由于printf函数会调用fputc,进行重写
int fputc(int ch ,FILE *f){
//重定向到串口1中
while((USART1->SR & 0x40) == 0);
USART1->DR = (uint8_t)ch;
return ch;
}
- usart1.h文件代码
#ifndef __USART_H__
#define __USART_H__
#include "stm32f1xx.h"
#include "sys.h"
#define UART1_RX_BUF_SIZE 128
#define UART1_TX_BUF_SIZE 64
#define UART_EOK 0
#define UART_ERROR 1
#define UART_ETIMEOUT 2
#define UART_EINVAL 3
void uart1_init(uint32_t boundrate);
#endif
- main.c文件代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
while(1)
{
}
}
实验现象
问题和注意事项
这个文件代码实际上是两个中断函数的结合:接受中断函数+空闲中断函数
手动清除IDEL中断标志位
重写printf函数