飞书文档https://x509p6c8to.feishu.cn/wiki/W7ZGwKJCeiGjqmkvTpJcjT2HnNf
串口说明
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
TTL电平:+3.3V或+5V表现1,0V表现0
RS232电平:-3~ -15V表现1,+3~ +15V表现0
RS485电平:两线压差+2~+6V表现1,-2 ~-6V表现0(差分信号)
STM32F103RC系列芯片中,有五个串口
3个USART,2个UART
USART:通用同步和异步收发器
UART:通用异步收发器
当举行异步通讯时,这两者是没有区别的。区别在于USART比UART多了同步通讯功能。
|
IO口说明:
点击图片可查察完备电子表格
TX:发送数据输出引脚。
RX:吸收数据输入引脚。
如果发送设备发送太快,吸收设备来不及处置处罚,可以通过流控来控制传输的速度。
SCLK:发送器时钟输出引脚。这个引脚仅实用于同步模式,用于时钟同步
nRTS是请求发送,是输出脚,就是告诉别人,我当前能不能吸收,用于硬件流控
nCTS是扫除发送,是输入脚,用于吸收别人nRTS的信号,用于硬件流控
|
硬件流控说明,例如:
吸收端可以吸收数据时,会设置nRTS输出低电平,此时发送端读取到低电平,开始发送数据。
吸收端处置处罚不过来时,设置nRTS为高电平,此时发送端读取到高电平,停止发送数据。
|
创建工程,设置SWD,设置时钟。
配置USART1为异步通讯方式,不必要硬件流控制。
Asynchronous(异步通讯)紧张使用
Synchronous(同步通讯,同步通讯相比于异步通讯多了个时钟CLK输出)
Single Wire (Half-Duplex)(单线(半双工)通讯)
Multiprocessor Communication(多处置处罚器通讯)
SmartCard、IrDA、LIN 智能卡、IrDA、LIN,这些是其他的一些协议,这些协议与串口非常相似,所以STM32对USART加了些改动,可兼容这些协议。IrDA用于红外通讯的,一边红外发光管,另一边红外吸收管,靠闪耀红外光通讯,与遥控器的红外差别。LIN是局域网的通讯协议,详细可以查察芯片手册。
关于硬件流控制,比如A设备有个TX向B设备RX发送数据,A设备发的太快导致B处置处罚不过来,如果没有硬件流控制,B就只能扬弃新数据大概覆盖原数据了,如果有硬件流控制,在硬件电路上会多出一根线,如果B没预备好吸收就置于高电平,预备好了就置低电平,A只会在B预备好的时候发送数据。
硬件流控制必要多使用两个IO,所以大部门环境都倒霉用,直接用软件做数据处置处罚。
| 
然后设置波特率为115200bps 数据长度8bit 没有校验位 1位停止位。
串口中,每个字节都装载在一个数据帧(10或11位)里,每个数据帧都由起始位、数据位和停止位,数据位有8个代表一个字节的8位。参数如下:
波特率:串口通讯的速率。波特率本来的意思是每秒传输码元的个数,单元是码元/s,大概直接叫波特(Baud),还有个速率叫比特率,每秒传输的比特数,单元是bit/s,大概是bps。在二进制调制下,一个码元就是一个bit,此时波特率等于比特率,单片机的串口通讯根本都是二进制调制(高电平表现1,低电平表现0,一位就是1bit),所以串口的波特率经常会和比特率混用。
起始位:标记一个数据帧的开始,固定为低电平。空闲状态为高电平,起始位产生下降沿,来告诉设备要开始发送数据了
数据位:数据帧的有用载荷,1为高电平,0为低电平,低位先行。
校验位:用于数据验证,根据数据位盘算得来。这里串口用的是奇偶校验的数据验证方法,可以判断数据传输是否出错,如果出错可选择丢弃大概重传。可选择三种方式,无校验、奇校验、偶校验。奇校验,包罗校验位在内的9个数据位会出现奇数个1,根据8位数据环境奇校验位补0或1,包管1的个数位奇数,吸收方吸收数据时,会验证数据位和校验位,检出率不高比如有两位同时出错,只校验奇偶特性是检验不出的。偶校验同理,只能包管一定检出率,所以一样平常不必要校验位,如果要更高检出率可以在软件层使用CRC校验。
停止位:用于数据帧隔断,固定为高电平。也是为下一个起始位做预备(切换到高电平空闲状态)
|
波特率9600代表1s发送9600个bit,也就是1个bit发送必要100us左右
这时,软件会自动选择PA9与PA10做为串口的发送与吸收引脚。
这时,我们可以生成工程
- main.c
- MX_USART1_UART_Init();
- usart.c
- void MX_USART1_UART_Init(void)
- {
- huart1.Instance = USART1;
- huart1.Init.BaudRate = 115200;
- huart1.Init.WordLength = UART_WORDLENGTH_8B;
- huart1.Init.StopBits = UART_STOPBITS_1;
- huart1.Init.Parity = UART_PARITY_NONE;
- huart1.Init.Mode = UART_MODE_TX_RX;
- huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
- huart1.Init.OverSampling = UART_OVERSAMPLING_16;
- if (HAL_UART_Init(&huart1) != HAL_OK)
- {
- Error_Handler();
- }
- }
复制代码 那如何实现串口发送或吸收数据呢?
stm32f1xx_hal_uart.h
HAL_UART_Transmit(); 串口轮询模式发送,使用超时管理机制。
HAL_UART_Receive(); 串口轮询模式吸收,使用超时管理机制。
HAL_UART_Transmit_IT(); 串口中断模式发送,
HAL_UART_Receive_IT(); 串口中断模式吸收
HAL_UART_Transmit_DMA(); 串口DMA模式发送
HAL_UART_Receive_DMA(); 串口DMA模式吸收
HAL_UART_TxHalfCpltCallback(); 发送过半,通过中断处置处罚函数调用。
HAL_UART_TxCpltCallback(); 发送完成后,通过中断处置处罚函数调用。
HAL_UART_RxHalfCpltCallback(); 吸收过半,通过中断处置处罚函数调用。
HAL_UART_RxCpltCallback(); 吸收完成后,通过中断处置处罚函数调用。
HAL_UART_ErrorCallback(); 传输过程中出现错误时,通过中断处置处罚函数调用。
第一种是上面用到的轮询的模式。
CPU会不断查询串口是否传输完成,如传输超过则返回超时错误。轮询方式会占用CPU处置处罚时间,服从较低,在实时性要求较高的产品中不宜使用。
第二种就是中断控制方式。
当I/O操作完成时,输入输出设备控制器通过中断请求线向处置处罚器发出中断信号,处置处罚器收到中断信号之后,转到中断处置处罚步伐,对数据传送工作举行相应的处置处罚。
第三种就是直接内存存取技能(DMA)方式。
所谓直接传送,先发送到DMA,即在内存与IO设备间传送一个数据块的过程中,不必要CPU的任何中心干涉,只必要CPU在过程开始时向设备发出“传送块数据”的命令,然后通过中断来得知过程是否结束和下次操作是否预备停当。
整个过程只产生两次中断,第一次是进入DMAx_Streamy_IRQHandler;第二次进入USARTx_IRQHandler。
| 有多种方式,我们先来了解第一种,阻塞轮询模式
HAL_UART_Transmit (UART_HandleTypeDef *huart, const uint8 t *pData, uint16 t Size, uint32 t Timeout)
这个函数是一个阻塞函数,即当调用此函数时,步伐会一直等待数据发送完成或超时后才会继承实行后面的代码。
第一个参数是UART句柄,比如要使用USART1,参数就设置为USART1的句柄地址&huart1
第二个参数是必要发送的数据。
第三个参数是必要发送数据的巨细。
第四个参数是发送超时时间,单元是毫秒,如果超过设置的时间,则函数返回HAL_TIMEOUT,如果设置为HAL_MAX_DELAY,处置处罚器就会一直比及数据发送完成再实行下一条语句。
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
第一个参数是要使用的串口句柄地址,比如要使用USART1,参数就设置为USART1的句柄地址&huart1
第二个参数是接受数据的缓冲区首地址
第三个参数是接受的数据长度,这里可以直接用sizeof()函数获取接受缓冲区的长度
第四个参数是超时时间,单元是ms,如果超过设置的时间,则函数返回HAL_TIMEOUT,如果设置为HAL_MAX_DELAY,处置处罚器就会一直比及吸收到设置好的数据数目再实行下一条语句。
| 现在,我们先实现发送功能,在main.c中添加发送代码
- while (1)
- {
- /* USER CODE END WHILE */
- /* USER CODE BEGIN 3 */
- uint8_t txbuf[]="Hello,world!";
- HAL_UART_Transmit(&huart1,txbuf,sizeof(txbuf),1000);
- HAL_Delay(500);
- }
- /* USER CODE END 3 */
复制代码
- 编译烧录至板卡,然后接好串口线连接到电脑。
- 打开串口调试助手,选择COM口,例如下方是COM5,根据自己电脑设备管理器的COM选择,插拔USB线,会表现新COM,如果提示COM口有叹号,则必要自行搜索CH340驱动安装。
串口调试助手软件:自行安装即可:参考飞书文档
- 然后设置波特率115200 8 N 1,即可看到隔断500ms打印信息。
参考工程:
如果烧录完没打印,可以重启或复位下
- /* USER CODE BEGIN 2 */
- uint8_t rxbuf[12];
- /* USER CODE END 2 */
- while (1)
- {
- /* USER CODE END WHILE */
- /* USER CODE BEGIN 3 */
- if(HAL_UART_Receive(&huart1,rxbuf,sizeof(rxbuf),1000) == HAL_OK){
- HAL_UART_Transmit(&huart1,rxbuf,sizeof(rxbuf),1000);
- }
- }
- /* USER CODE END 3 */
复制代码 编译烧录至板卡,然后接好串口线连接到电脑,打开串口调试助手,设置波特率115200 8 N 1,发送ASCII码“Hello world”,
为什么“Hello world”是11个字符,我们必要吸收rxbuf[12]是12个字节呢?
因为串口助手工具,会自动加上换行符,点击右侧的发送后,我们可以看到TX是12个字节。
串口中断方式
我们可以看到,上方的方式都是阻塞式发送,轮询吸收的,简朴的产品如许设计没有题目,但是做一些复杂的,对实时性有要求的产品时,就满足不了了,所以我们可以用到串口中断的功能,在CUBEMX中使能中断。
阻塞方式就好比你要拿快递,就一遍遍都前台询问快递到没到,在这期间你不能干别的,
中断方式是你告诉前台快递到了给你打电话,在这期间你是可以腾身世子来干别的事情。
|
生成工程后,可以在stm32f1xx_it.c中看到生成了中断相关函数
- /**
- * @brief This function handles USART1 global interrupt.
- */
- void USART1_IRQHandler(void)
- {
- HAL_UART_IRQHandler(&huart1);
- }
- void HAL_UART_IRQHandler(UART_HandleTypeDef *huart){
- xxxx
- UART_Receive_IT(huart);
- xxxx
- }
- static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart){
- xxxx
- HAL_UART_RxCpltCallback(huart);
- xxxx
- }
- 最终找到需要重写的虚函数
- /**
- * @brief Rx Transfer completed callbacks.
- * @param huart Pointer to a UART_HandleTypeDef structure that contains
- * the configuration information for the specified UART module.
- * @retval None
- */
- __weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
- {
- /* Prevent unused argument(s) compilation warning */
- UNUSED(huart);
- /* NOTE: This function should not be modified, when the callback is needed,
- the HAL_UART_RxCpltCallback could be implemented in the user file
- */
- }
复制代码 然后在
- main.c
- /* USER CODE BEGIN 0 */
- uint8_t rxbuf[10];
- uint8_t ackbuf[] = "ack pack";
- /* USER CODE END 0 */
-
- /* USER CODE BEGIN 2 */
- HAL_UART_Receive_IT(&huart1,rxbuf,sizeof(rxbuf));
- /* USER CODE END 2 */
-
- /* USER CODE BEGIN 4 */
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
- if(huart == &huart1) //判断中断是否来自于串口1
- {
- HAL_UART_Transmit_IT(&huart1,ackbuf,sizeof(ackbuf)); //通过中断的方式发送应答数据出去
- HAL_UART_Receive_IT(&huart1,rxbuf,sizeof(rxbuf)); //开始接收下一轮数据
- }
- }
复制代码 下载完成,点击复位。打开串口助手,连接到相应的端口,设置波特率为115200,从串口助手向单片机发送10个字节的数据,单片机将会把发过去的数据在返回给串口助手。必须发够10个字节以上的数据,才气够触发中断。
参考工程:
如果烧录完没打印,可以重启或复位下
串口中断+DMA方式
这时候,如果我们在开发产品过程中,必要频繁收发数据,且通讯波特率较高时,如果接纳中断方式,每收发一个字节的数据,CPU都会被打断,造成CPU无法处置处罚其他事件。 因此在批量数据传输,通讯波特率较高时,建议接纳DMA方式。
串口中断每收发一个字节数据,CPU都会被打断
|
CPU只必要设置开始传输和处置处罚传输结束的中断
| DMA,全称Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间大概存储器和存储器之间的高速数据传输。
我们知道CPU无时不刻的在处置处罚着大量的事件,但有些事情却没有那么紧张,比方说数据的复制和存储数据,如果我们把这部门的CPU资源拿出来,让CPU去处置处罚其他的复杂盘算事件,是不是能够更好的使用CPU的资源呢?
所以串口收发数据量大时可借助DMA,减轻CPU负担。即在内存与IO设备间传送一个数据块的过程中,不必要CPU的任何中心干涉,只必要CPU在过程开始时向设备发出“传送块数据”的命令,然后通过中断来得知过程是否结束和下次操作是否预备停当。
整个过程只产生两次中断,第一次是进入DMAx_Streamy_IRQHandler;第二次进入USARTx_IRQHandler。
前文说过中断方式就好比你告诉前台,等快递到了给你打电话,让你亲身来取,假设你正在做着一些紧张的事情,正好来了电话让你取快递,如许一来就会延长事。
这怎么办呢?
雇个保姆不就好了吗,DMA就好比这个保姆,你告诉她在哪里取快递,她就会等快递到了之后自己帮你把快递拿回家。
|
STM32F103RC有12个独立的可配置的通道(请求):DMA1有7个通道,DMA2有5个通道
每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过 软件来配置。
这里,我们切换CUBEMX的USART1中,设置DMA,点击Add,把USART1_TX USART1_RX都添加进来。
注意,RX和TX下方的DMA Request Settings都必要设置为一样。
Channel:通道
例如USART1_RX会对应着上面表格的DMA1的通道5
Direction:方向
因为RX是吸收接口,吸收来自外设的数据,所以方向是Peripheral To Memory(外设到内存),TX相反
Priority: 优先级,当存在多个DMA传输时才必要根据详细业务设置,默认选择Low即可
最高优先级 Very Hight
高优先级 Hight
中等优先级 Medium
低优先级;Low
Mode:模式
Normal:正常模式
当一次DMA数据传输完后,停止DMA传送 ,下次传输则必要重新开启DMA传输
Circular: 循环模式
传输完成后又重新开始继承传输,不断循环永不停止
根据差别场景选择灵活差别模式:
例如:
正常模式:一次性传输固定长度的数据,例如发送一次性消息、吸收固定长度的数据包等。
循环模式:必要一连不断地发送或吸收数据的场景,如一连的传感器数据收罗等。
通讯类场景,一样平常TX使用正常模式,RX使用正常/循环模式,如果TX使用循环模式,必要注意数据同步题目,处置处罚欠好会导致新旧数据一起发送,而收罗数据,使用循环模式就可以大大降低CPU的压力。
Increment Address:地址递增器
左侧Peripheral表现外设地址寄存器
功能:设置传输数据的时候外设地址是稳定还是递增。如果设置为递增,那么下一次传输的时候地址加Data Width个字节
右侧Memory表现内存地址寄存器
功能:设置传输数据时候内存地址是否递增。如果设置为递增,那么下一次传输的时候地址加Data Width个字节,
例如:
串口发送数据是将数据不断存进固定外设地址的发送数据寄存器,所以外设的地址是不递增。
而内存储器存储的是要发送或吸收数据,地址指针要递增,包管数据依次被发出或不被覆盖保存。
Data Width 数据宽度
一样平常的串口都是8位,因此使用默认的DMA配置即可,也就是指针自增为Byte
| 这里有个必要注意的地方,就是函数调用顺序
MX_DMA_Init()函数必要在其他初始化前调用,特别是在这个串口初始化前,否则会发送使用DMA发送会发送失败,在如下图位置配置调用顺序,必须先配置时钟再配置外设,MX_DMA_Init()内里有DMA时钟初始化
设置完成上面步骤,生成工程后,我们会发现DMA初始化在USART1之前,如果不举行这步设置,可能会出现发送失败的环境哦。
- /* Initialize all configured peripherals */
- MX_GPIO_Init();
- MX_DMA_Init();
- MX_USART1_UART_Init();
复制代码 然后,我们可以使用DMA方式实现串口发送
- main.c
- while (1)
- {
- /* USER CODE END WHILE */
- /* USER CODE BEGIN 3 */
- uint8_t txbuf[]="Hello,world!";
- HAL_UART_Transmit_DMA(&huart1,txbuf,sizeof(txbuf));
- HAL_Delay(500);
- }
- /* USER CODE END 3 */
复制代码 也可以使用DMA方式实现串口收发
如果必要实时处置处罚串口的数据,则必要打开串口全局中断。
UART一旦开启DMA之后,DMA通道全局中断都是强制开启的,DMA传输完备数据后,会触发HAL_UART_RxCpltCallback或HAL_UART_TxCpltCallback中断产生。
- main.c
- /* USER CODE BEGIN 0 */
- uint8_t rxbuf[10];
- uint8_t ackbuf[] = "ack pack";
-
- /* USER CODE BEGIN 2 */
- //初始化DMA串口接收需要在串口初始化前?
- HAL_UART_Receive_DMA(&huart1,rxbuf,sizeof(rxbuf));
- /* USER CODE END 2 */
- /* USER CODE BEGIN 4 */
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
- if(huart == &huart1) //判断中断是否来自于串口1
- {
- HAL_UART_Transmit_DMA(&huart1,ackbuf,sizeof(ackbuf)); //通过中断的方式发送应答数据出去
- //如果接收使用循环模式,则不用重新开启
- HAL_UART_Receive_DMA(&huart1,rxbuf,sizeof(rxbuf)); //开始接收下一轮数据
- }
- }
复制代码 参考工程:参考飞书文档
使用USART+DMA吸收中断不定长数据
可以使用STM32 IDLE空闲中断实现,IDLE的中断产生条件:在串口无数据吸收的环境下,不会产生,当扫除IDLE标记位后,必须有吸收到第一个数据后,才开始触发,一但吸收的数据断流,没有吸收到数据,即产生IDLE中断
- main.c
- /* USER CODE BEGIN 0 */
- extern DMA_HandleTypeDef hdma_usart1_rx;
- #define BUFFER_SIZE 100
- uint8_t rxbuf[BUFFER_SIZE];
- /* USER CODE BEGIN 2 */
- __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
- HAL_UART_Receive_DMA(&huart1,rxbuf,sizeof(rxbuf));
- /* USER CODE BEGIN 4 */
- void UART_IDLEHandler(){
- if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) == SET) //如果串口处于空闲状态
- {
- __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_IDLE);//清空空闲状态标志
- HAL_UART_DMAStop(&huart1); //关闭DMA传输
- //计算接收到的数据长度 ,已接收长度=需要接收总长度-剩余待接收长度
- uint8_t rlen = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
- //发送数据到上位机,当然,这里可以把数据复制到其它位置进行处理
- HAL_UART_Transmit_DMA(&huart1,rxbuf,rlen);
- //重新打开DMA接收
- HAL_UART_Receive_DMA(&huart1,rxbuf,sizeof(rxbuf));
- }
- }
- main.h
- /* USER CODE BEGIN EFP */
- void UART_IDLEHandler(void);
- /* USER CODE END EFP */
- stm32f1xx_it.c
- /**
- * @brief This function handles USART1 global interrupt.
- */
- 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 */
- UART_IDLEHandler();
- /* USER CODE END USART1_IRQn 1 */
- }
复制代码 参考工程:参考飞书文档
端口复用
当然,USART1是支持复用功能的,可以重映像到别的IO上,如果我们在举行硬件设计时,发现PA9、PA10走线欠好走,大概必要作为别的用途,我们可以把USART1映射到PB6 PB7,如何知道是否支持重映像,可以查察手册8.3章节。
可以在右侧的芯片图中找到PB6,设置为USART1_TX,PB7,设置为USART1_RX
串口重定向
在单片机中使用printf打印
使用HAL_UART_Transmit发送字符串很不方便,可以重定向printf()函数使printf通过串口打印字符串
使用串口重定向,必须勾选MicroLIB
MicroLib 是一个高度优化的库,实用于用 C 编写的基于 ARM 的嵌入式应用步伐。
与 ARM 编译器工具链中包含的标准 C 库相比,MicroLib 提供了许多嵌入式系统所需的显着代码巨细上风。
|
- main.c
- /* USER CODE BEGIN Includes */
- #include <stdio.h>
- /* USER CODE END Includes */
- /* USER CODE BEGIN 4 */
- #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;
- }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |