文章目录
一、数据通信的基本概念
1.1.串行 / 并行通信
按数据通信方式分类:串行通信、并行通信
下图是串行通信:
下图是并行通信:
特点 | 传输速率 | 抗干扰能力 | 通信距离 | IO资源占用 | 成本 |
---|---|---|---|---|---|
串行通信 | 较低 | 较强 | 较长 | 较少 | 较低 |
并行通信 | 较强 | 较弱 | 较短 | 较多 | 较高 |
1.2.单工 / 半双工 / 全双工通信
按数据传输方向分类:单工通信、半双工通信、全双工通信
下图是单工通信,数据只能沿着一个方向传输。
下图是半双工通信,数据可以沿着两个方向传输,但需要分开时间进行,当a正在发送数据给b的时候,b不可以发送数据给a。
下图是全双工通信,数据可以同时进行双向传输。
1.3.同步 / 异步通信
按数据同步方式分类:同步通信、异步通信。区别该通信方式是否同步或异步,通过该通信有没有时钟线即可。
下图是同步通信,共用同一时钟信号。
下图是异步通信,没有时钟信号,通过在数据信号中加入起始位和停止位等一些同步信号。
1.4.波特率
- 比特率:每秒传送的比特数,单位bit/s
- 波特率:每秒钟传送的码元数,单位Baud
- 二进制系统中,波特率数值上等于比特率,意义不一样
- 码元数:通信中波特率与采样率的关系(如 USART 中 1 位数据对应 16 个采样时钟),但需结合具体外设(如 SPI、CAN 等)确定其在数据传输里的具体含义。
1.5.常见的串行通信接口
通信接口 | 接口引脚 | 数据同步方式 | 数据传输方向 |
---|---|---|---|
UART(通用异步收发器) | TXD(发送端)、RXD(接收端)、GND(公共地) | 异步通信 | 全双工 |
1-wire | DQ(发送 / 接收端) | 异步通信 | 半双工 |
IIC | SCL(时钟线)、SDA(数据线) | 同步通信 | 半双工 |
SPI | SCL(时钟线)、MISO(主机输入、从机输出)、MOSI(主机输出、从机输入) | 同步通信 | 全双工 |
二、串口(RS-232接口)
2.1.RS-232接口的引脚
串口是指串行通信接口,按位发送和接收的接口。下面以RS-232接口为例:
以下由9个引脚,一般只是用RXD、TXD和GND。
- DCD(PIN1):数据载波检测
- RXD(PIN2):串口数据输入
- TXD(PIN3):串口数据输出
- DTR(PIN4):数据终端就绪
- GND(PIN5):信号地
- DSR(PIN6):数据发送就绪
- RTS(PIN7):请求发送
- CTS(PIN8):清除发送
- RI(PIN9):振铃指示
2.2.RS-232电平与COMS / TTL电平对比
电平 | 逻辑1 | 逻辑0 |
---|---|---|
RS-232电平 | -15V ~ -3V | +3V ~ +15V |
COMS电平 | 3.3V | 0V |
TTL电平 | 5V | 0V |
一般51单片机使用TTL电平,STM32使用COMS电平,如果直接使用RS-232电平,会烧毁机器。COMS / TTL电平不能与RS-232电平直接交换信息。
2.3.设备间的RS-232通信示意图
使用RS-232电平,需要在硬件部分接上一个电平转换芯片。
两设备之间的TXD和RXD必须交叉连接才能正常通信。
2.4.RS-232异步通信协议
- 启动位:必须占1个位长,保持逻辑0电平
- 有效数据位:可选5、6、7、8、9个位长,LSB在前,MSB在后,也就是低位在前
- 校验位:可选占1个位长,也可以没有该位
- 停止位:必须有,可选0.5、1、1.5、2个位长,保持逻辑1电平
三、USART(通用同步异步收发器)
3.1.简介
USART的全称是Universal synchronous asynchronous receiver transmitter,通用同步异步收发器;UART是裁剪了同步功能,所以叫通用异步收发器,它们都可以与外部设备进行全双工通信。
3.2.主要特征
- 全双工异步通信
- 单线半双工通信
- 单独的发送器和接收器使能位
- 可配置使用DMA的多缓冲器通信
- 多个带标志的中断源
3.3.工作原理框图
CPU发送一个数据,CPU对数据寄存器(DR)进行写操作,不能直接对发送数据寄存器(TDR)进行操作,在数据寄存器(DR)写好了数据之后,自动传入发送数据寄存器,等该寄存器存到满数据之后,并发现发送移位寄存器为空才把数据传入,最后由编解码模块发送出去;CPU接收一个数据,RX引脚从外部接收到数据,先是传入接收移位寄存器,再传入接收数据寄存器(RDR),CPU通过读取数据寄存器(DR)读到该数据,CPU不能对接收数据寄存器进行直接操作。图中最下面有一个传统的波特率发生器,通过USART_BRR寄存器的低16位,来控制发送器和接收器波特率的控制。
3.4.设置波特率(F1)
f(PCK1)是APB1总线,最高时钟频率36M;f(PCK2)是APB2总线,最高时钟频率72M。如要设置USART1的波特率为9600,可知道以下参数,f(CK)=72M,通过数据手册可知USART1是挂在APB2总线上,USARTDIV也可以算出来,其中DIV_Mantissa是整数部分,由[15:4]位控制,(DIV_Fraction/16)是小数部分,由[3:0]位来控制。
3.4.1.下图是波特率寄存器(USART_BRR):
3.4.2.使用寄存器设置波特率
将USART1设置波特率为115200,USART1挂载在APB2总线上,最大时钟频率为72M,并根据上述公式可知,115200 = 72000000 / 16*USARTDIV,求出USARTDIV = 39.0625。下面代码是将39.0625写入USART_BRR寄存器里:
uint16_t mantissa;
uint16_t fraction;
mantissa = 39;
fracion = 0.625 * 16 + 0.5 = 0x01; //加上0.5是四舍五入,减少误差
USART->BRR = (mantissa<<4) + fracion; //小数部分占用[3:0]这四位,因此整数部分要要左移四位
四、USART相关寄存器
4.1.控制寄存器1(USART_CR1)
该寄存器需要完成的配置:
- 位13:使能USART
- 位12:配置8个数据位
- 位10:禁止检验控制
- 位5:使能接收缓冲区非空中断
- 位3:使能发送
- 位2:使能接收
4.2.控制寄存器2(USART_CR2)
该寄存器需要完成的配置:
位13和位12,设置暂停位的位数。
4.3.控制寄存器3(USART_CR3)
该寄存器需要配置:
位3,不选择半双工模式。
4.4.状态寄存器(USART_SR)
该寄存器一般用到这三个位:
- 位7:当发送数据寄存器空了,但是发送移位寄存器非空的情况置1
- 位6:发送数据寄存器和发送移位寄存器都空的情况置1,
- 位5:读数据寄存器(DR)非空,意味着CPU可以再数据寄存器里读取数据
根据位6可以知道能否发数据,根据位5知道是否收到数据。
4.5.数据寄存器(USART_DR)
该寄存器只有[8:0]位有效,其他都保留了。
五、回调机制
5.1.HAL库外设初始化MSP回调机制
HAL_PPP_Init()
,用户调用该类函数初始化PPP外设的工作参数,它的里面又调用MSP回调函数:HAL_PPP_MspInit()
。MSP回调函数的作用是配置PPP外设用到的硬件,如:GPIO、NVIC、CLOCK等,用户可选择是否使用并重新定义该函数。下面是使用MSP回调函数的注意事项:
- 当多个PPP外设同时使用MSP函数,则可以通过判断外设寄存器基地址区分是哪一个外设,再配置PPP外设用到的硬件
- 当多个PPP外设共用该函数,文件不好管理,而且放到一起看着比较乱,这时候就不建议使用该函数
下面以UART为例:
HAL_UART_Init();
,里面调用MSP回调函数HAL_UART_MspInit();
void HAL_UART_MspInit(UART_HandleTypeDef *huart) //参数里面称为句柄
{
GPIO_InitTypeDef gpio_init_struct;
if(huart->Instance == USART1) //如果是USART1的基地址,就执行if语句里面的操作
{
//使能USART1和对应IO时钟;初始化IO;使能USART1中断,设置优先级
}
}
5.2.HAL库中断回调机制
在.s文件里寻找相应的中断服务函数HAL_PPP_IRQHandler();
,该函数里面也调用一系列中断回调函数HAL_PPP_xxxCallback();
,该回调函数的作用是根据回调函数类型,编写对应的中断处理程序,用户可选择是否使用并重新定义该函数。下面是使用中断回调函数的注意事项:
- 当多个PPP外设同时使用MSP函数,则可以通过判断外设寄存器基地址区分是哪一个外设,再配置PPP外设用到的硬件
- 当多个PPP外设共用该函数,文件不好管理,而且放到一起看着比较乱,这时候就不建议使用该函数
下面也是以UART为例:
用户调用HAL库中断共用处理函数HAL_UARTx_IRQHandler();
,HAL库再自己调用中断回调函数。
下图是UART中断回调函数,用户需要用到哪一个函数只需重新定义即可。
六、配置步骤
6.1.UART异步通信配置步骤
- 配置串口工作参数:
HAL_UART_Init()
- 串口底层初始化:
HAL_UART_MspInit()
,配置GPIO、NVIC、CLOCK等 - 开启串口异步接收中断:
HAL_UART_Receive_IT()
- 设置优先级,使能中断:
HAL_NVIC_SetPriority()
、HAL_NVIC_EnableIRQ()
- 编写中断服务函数:
UARTx_IRQHandler()
- 串口数据发送:
USART_DR()
、HAL_UART_Transmit()
6.2.配置串口工作参数
该函数完整写法是:HAL_StatusTypeDef HAL_UART_Init(UART_HandlerTypeDef *huart)
,HAL_StatusTypeDef
是一个枚举类型:只有返回0x00才标志着OK;返回0x01是初始化错误;返回0x02是串口处于忙状态;返回0x03是初始化超时。
形参1是串口的句柄,结构体类型是 UART_HandleTypeDef
,下图是该句柄的成员:
初始化操作只用到前面两个成员USART_TypeDef
(配置相关的寄存器)、UART_InitTypeDef
(配置对应的功能):
6.3.开启串口异步接收中断
该函数的完整写法是:HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
,作用是以中断方式接收指定字节数据,它的函数类型也是枚举类型,与上面小节的函数类型一样,形参1是UART_HandleTypeDef
结构体类型指针变量;形参2是指向接收数据缓冲区;形参3是要接收的数据大小,以字节位单位。
6.4.串口数据发送
该函数完整写法是:HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
,作用是以阻塞的方式发送指定字节数据,阻塞的方式就是必须把设定的全部字节发送出去才走出该函数,形参1与上小节的形参1一样;形参2是指向要发送的数据地址;形参3要发送的数据大小并以字节位单位;形参4是设置超时时间,以ms单位。
七、IO引脚复用
7.1.复用
复用是建立在通用上面的,通用:IO端口的输入或输出是由GPIO外设控制(由GPIO的寄存器进行控制,如:ODR,BSRR);复用:IO端口的输入或输出是由其他非GPIO外设控制(由非GPIO寄存器控制,如:USART的DR,TIM的某个通道等)。
7.2.F1系列的IO引脚复用
- 可查询数据手册引脚定义了解引脚的复用情况
- 同一时间IO只能用作一种复用功能,只能由用户尽量去避免,否则会造成冲突,两个功能都失效
- 如果遇到复用冲突情况,考虑重映射功能,重映射到另一个IO口
八、通过串口接受或发送一个字符
8.1.初始化usart
uint16_t g_usart_rx_sta = 0; //接收标志,初始化为0,接收到置1
uint8_t g_rx_buffer[RXBUFFERSIZE]; //HAL库使用的串口接收缓存区
UART_HandleTypeDef g_uart1_handle; //UART句柄
void usart_init(uint32_t baudrate)
{
//UART_InitTypeDef里的成员:
g_uart1_handle.Instance = USART_UX; //USART1
g_uart1_handle.Init.BaudRate = baudrate; //波特率
g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; //字长为8
g_uart1_handle.Init.StopBits = UART_STOPBITS_1; //一个停止位
g_uart1_handle.Init.Parity = UART_PARITY_NONE; //无奇偶校验位
g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //无硬件流控
g_uart1_handle.Init.Mode = UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&g_uart1_handle); //使能USART1
HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE);
//上面函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
}
8.2.串口底层初始化;设置优先级,使能中断
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if (huart->Instance == USART_UX) //如果是串口1,进行串口1 MSP初始化
{
USART_TX_GPIO_CLK_ENABLE(); //使能串口TX引脚时钟
USART_RX_GPIO_CLK_ENABLE(); //使能串口RX引脚时钟
USART_UX_CLK_ENABLE(); //使能串口时钟
//配置PA9为发送引脚,复用推挽输出
gpio_init_struct.Pin = USART_TX_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
//配置PA10为接收引脚,复用输入模式
gpio_init_struct.Pin = USART_RX_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;
HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct);
#if USART_EN_RX
HAL_NVIC_EnableIRQ(USART_UX_IRQn); //使能USART1中断通道
HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);
#endif
}
}
8.3.编写中断服务函数
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&g_uart1_handle); //调用HAL库中断处理公共函数,该函数会使能接收中断,具体情况如下图,因此要再次开启中断
HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE);
}
8.4.定义Callback函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
g_usart_rx_sta = 1; //标志为1,收到信息
}
8.5.主函数
int main(void)
{
HAL_Init();
sys_stm32_clock_init(RCC_PLL_MUL9);
delay_init(72);
usart_init(115200);
led_init();
while(1)
{
if(g_usart_rx_sta == 1)
{
HAL_UART_Transmit(&g_uart1_handle, (uint8_t*)g_rx_buffer, 1, 1000); //发送一个字符,1s为超时时间
while(__HAL_UART_GET_FLAG(&g_uart1_handle,UART_FLAG_TC) != SET); //如果没发送完成就一直在该循环里
printf("\r\n"); //回车换行
g_usart_rx_sta = 0; //发送完成就把标志位置0
}
else //如果没收到,就执行下面程序
{
times++;
if (times % 5000 == 0)
{
printf("ALIENTEK\r\n\r\n\r\n");
}
if (times % 200 == 0) printf("输入数据,以回车结束\r\n");
delay_ms(10);
}
}
}