欢迎光临
我们一直在努力

RSPLT是什么品牌STM32单片机RS232串口通讯从入门到精通

       RS ==Recommend Standard ==推荐标准;

        232==标识号,第232号;

        时间:1962年

        地点:美国

        人物:美国电子工业协会 == Electronic Industries Association ==(美国)电子工业协会

        事件:发布了一个串行通信的物理接口结合逻辑电平的规范文件,就是这个232号文件。

        

        现在关于串口通讯的叫法太多,什么RS232通讯、串口通讯、DB9通讯、UART通讯等等,其实这些称呼都跟这个额232号文件有些关系,随便怎么叫吧,理解就行。

        串行通讯,从字面意思理解就行,就是把数据串成一串发出去,比如一个字节8位,高位先发出去,那么发送顺序就是bit7–bit6–bit5–bit4–bit3–bit2–bit1–bit0。

        就某种单片机来说,比如STM32F103,它有好几个UART口,俗称串口,但是它的引脚高电平是3.3V,低电平是0V,正好满足TTL的电平标准,2.4V–5V表示逻辑1,0V–0.4V表示逻辑0。

        但是电压低了传输距离就比较短,为了传输远点,就把TTL电平转换成RS232电平,通过某种电平转换芯片,比如MAX232。RS232电平逻辑是-3V到-15V表示逻辑1,3V到15V表示逻辑0。这相当于把电平扩大,但是也只能传输十几米。

        人们为了传输更远,就用上了差分传输,比如RS422和RS485,用两根线的电压差来表示逻辑1和逻辑0,两根线的压差为2V至6V表示逻辑1,两线的压差为-2V至-6V表示逻辑0,这样就能传输1000米以上。

数据在一根线上传输,那什么时候是开始,什么时候是结束,每位数据的宽度是多少、数据有没有传输错误。那就需要约定一下,不是谁都像孙悟空一样有觉悟,头上敲三下就是让他凌晨3点过来,还是要讲清楚点好。

线顶一个空闲的状态,就是没传输数据的时候,传输线的电平逻辑是1,用单片机TTL的电平标准就是3.3V,高电平;

开始:发出1位逻辑0电平

结束:发出1位逻辑1电平

数据宽度:要定义每一位的宽度是多少,不然你发两位1我却认为是1位1,怎么办,发送和接收的双发要统一度量衡,才不会有误解。这个数据宽度就是用波特率的约定。

数据有没有传错:那就把收到的数据大家数一数,算一算,我给你发100块钱,我还告诉你是100张一块的,那你收到之后,要数一数,是不是100张,是不是100块,都对了,那就表示是我给你的。

传输一个字节数据的示意图

STM32F103单片机USART内部结构图

STM32单片机的RS232串口通讯可以通过轮询、中断和DMA三种方式实现,以下是每种方式的工作流程详解:

发送流程

        1、初始化

                配置USART时钟、TX和RX的GPIO引脚、波特率、数据位、停止位等,现在可以使用STM32CubeMX配置后直接生成配置代码。初始化代码如下所示:

                使能USART模块;

// 在main()中调用初始化函数
MX_USART1_UART_Init(); // 由CubeMX生成

        2、发送数据

                检查状态寄存器USART_SR中的TXE(发送缓冲器空)标志位;

                若TXE=1,向USART_DR寄存器写入数据;

                重复上述步骤直到所有数据发送完成;

                使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Transmit()

uint8_t tx_data[] = "Hello World!";
HAL_UART_Transmit(&huart1, tx_data, sizeof(tx_data), HAL_MAX_DELAY); // 阻塞发送

参数说明:

 &huart1:UART句柄(如huart1,huart2等);

tx_data:发送缓冲区指针;

sizeof_(tx_data):数据长度;

HAL_MAX_DELAY:无限阻塞等待发送完成,当然这个阻塞时间也可以自行设置一个短一点的时间。

接收流程

        1、初始化:同发送

        2、接收数据

                检查状态寄存器USART_SR中的RXNE(接收缓冲区非空)标志位;

                若RXNE=1,从USART_DR寄存器读取数据;

        使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Receive()

uint8_t rx_data[10];
HAL_UART_Receive(&huart1, rx_data, 10, HAL_MAX_DELAY); // 阻塞接收10字节

轮询方式的特点

轮询方式需要CPU主动检查状态标志位,效率低。

优点:实现简单

缺点:CPU 长时间阻塞,无法处理其他任务;

轮询方式缺点太明显,工程上要谨慎使用,最好别用!

发送流程

        1、初始化

                配置USART(与轮询方式差不多);

                使能发送中断(USART_CR1中的TXEIE或者TCIE);

                在NVIC中配置USART中断优先级;

        2、发送数据

                写入第一个数据到USART_DR;

                后续数据在中断服务程序(ISR)中处理:

                        TXE中断:自动触发当发送缓冲区空,在ISR中写入下一个数据;

                        TC中断:发送完成时触发,用于关闭中断或通知主程序;

         使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Transmit_IT()

uint8_t tx_data[] = "Interrupt Mode";
HAL_UART_Transmit_IT(&huart1, tx_data, sizeof(tx_data)); // 非阻塞发送

函数触发后,HAL库自动管理中断,数据逐个发送

        3、中断回调处理:数据发送完之后触发HAL_UART_TxCpltCallback回调函数:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) 
}

接收流程

        1、初始化

               配置USART(与轮询方式差不多);

                使能接收中断(USART_CR1中的RXNEIE)

        2、接收数据

                当数据到达是触发RXNE中断,在ISR中读取USART_DR

使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Receive_IT()

uint8_t rx_buffer[100];
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 启动中断接收

        3、中断回调处理:每接收一个字节触发HAL_UART_RxCpltCallback(需要再回调函数中重新开机接收中断,否则是接受多个字节之后才触发回调函数)

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
}

 

HAL库中断接收的底层机制

1、中断服务函数 USARTx_IRQHandler

        HAL库通过USARTx_IRQHandler处理中断;

        每次中断会检查RXNE标志,读取数据并存入缓冲区,同时更新接收状态;

2、接收计数器管理

        当调用HAL_UART_Receive_IT时,记录目标缓冲区地址和剩余接收长度;

        每次中断将剩余长度减1,直到为0时触发回调;

重难点理解

中断接收函数默认是可以接受多个字节的,字节个数是Size个,函数原型如下

/**
  * @brief  Receives an amount of data in non blocking mode.
  * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
  *         the received data is handled as a set of u16. In this case, Size must indicate the number
  *         of u16 available through pData.
  * @param  huart Pointer to a UART_HandleTypeDef structure that contains
  *               the configuration information for the specified UART module.
  * @param  pData Pointer to data buffer (u8 or u16 data elements).
  * @param  Size  Amount of data elements (u8 or u16) to be received.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

    /* Process Locked */
    __HAL_LOCK(huart);

    /* Set Reception type to Standard reception */
    huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

    return(UART_Start_Receive_IT(huart, pData, Size));
  }
  else
  {
    return HAL_BUSY;
  }
}

我们以接收10个字节为例

1、初始化代码,以中断的方式开始接收10个字节的数据

uint8_t rx_buffer[10];
HAL_UART_Receive_IT(&huart1, rx_buffer, 10); // 启动接收10字节

2、中断触发逻辑

当USART接收到1个字节数据时,硬件触发接收缓冲区非空RXNE中断;

进入HAL库的USART中断服务函数USARTx_IRQHandler;

HAL库自动读取USART->DR寄存器,将数据存入rx_buffer,并减少剩余接收计数器;

每接收1个字节触发一次中断,HAL库的默认行为是在所有字节接收完成之后触发1次完成回调。

所以上面的代码来接收数据,硬件中断10次,但是回调函数只执行一次;

3、回调函数的执行

当10个字节全部接收完成之后,HAL库才调用HAL_UART_RxCpltCallback回调函数;

用户可以在回调函数中处理完成的数据包;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
}

一次性接收多个字节,等数据全部接收之后才执行一次回调函数,虽然用起来很方便但是会产生一个风险,比如由于某些原因,10个数据只接受了9个,那就会一直等第十个10数据的出现,如果不使用超时管理,软件系统也会出错一直死等。

在需要实时处理或者协议解析的时候,需要每次中断都进行一次回调函数,判断命令协议,此时需要使用单字节接收,如下所示:

1、初始代码

uint8_t rx_byte;
HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 启动单字节接收

2、回调函数中处理数据并重启接收

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
}

对比多字节数据接收和单字节数据接收

 

 

发送流程

        1、初始化

                驱启动USART模块和GPIO;

                启动DMA通道,配置方向;

                        发送方向:Memory → Peripheral(USART->DR)

                        接收方向:Peripheral → Memory(USART->DR)

        2、启动发送

uint8_t tx_data[] = "DMA Mode";
HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data)); // 启动DMA发送

        3、回调处理

                 发送完成时触发HAL_UART_TxCpltCallback

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) 
}

接收流程

        1、初始化

        2、启动接收,核心函数 HAL_UART_Receive_DMA

uint8_t rx_buffer[200];
HAL_UART_Receive_DMA(&huart1, rx_buffer, 200); // 启动DMA接收

        3、回调处理

                接收完成时触发HAL_UART_RxCpltCallback

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
}

 

1、硬件配置:需要正确配置GPIO、时钟、波特率、校验位等;

2、错误处理:检查USART_SR中的错误标志(如溢出错误ORE);

3、回调函数:中断和DMA方式都依赖回调函数

4、错误处理:通过HAL_UART_ErrorCallback处理通信错误

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) 
}

5、DMA优化:合理设置DMA缓冲区大小和中断优先级。

        可以使用双缓冲区HAL_UARTEx_ReceiveToIdle_DMA处理不定长数据;

        避免在DMA传输中修改缓冲数据。

处理不定长串口数据是实际项目中常见的需求,如Modbus、自定义协议等。

不定长数据接收的核心挑战

1、数据长度位置:无法预先设定;

2、数据完整性判断:需明确数据包结束标志(如空闲时间、特定字符、超时等);

3、高效性要求:避免CPU轮询,优先使用硬件特性,如IDLE中断、DMA;

常用的解决方案有:

原理 :USART在数据中线空闲时触发中断,配合DMA自动板运数据。

实现步骤

Step1:CubeMX配置

        1、启用USART和DMA,接收方向:Peripheral→Memory

        2、启用USART的空闲中断(IDLE Interrupt)

                在USARTx的NVIC设置中勾选USARTx global interrupt;

                代码中手动开启IDLE中断:

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 在初始化后添加

Step2:启动DMA接收

// 启动DMA循环接收(缓冲区需足够大)
uint8_t rx_buffer[256];
HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer));

Step3:处理空闲中断

// 在USART中断服务函数中检测IDLE标志
void USART1_IRQHandler(void) 
    HAL_UART_IRQHandler(&huart1); // 调用HAL库默认处理
}

原理:通过定时器检测两次接受数据的间隔时间,超过阈值则认为数据包结束。

实现步骤

Step1:启动中断接收

uint8_t rx_buffer[256];
uint16_t rx_index = 0;
HAL_UART_Receive_IT(&huart1, &rx_buffer[rx_index], 1); // 单字节接收

Step2:接收中断中重置定时器

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
        // 重置超时定时器(如TIM6)
        __HAL_TIM_SET_COUNTER(&htim6, 0);
        HAL_TIM_Base_Start_IT(&htim6);
        // 继续接收下一字节
        HAL_UART_Receive_IT(&huart1, &rx_buffer[rx_index], 1);
    }
}

Step3:定时器超时中断处理

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 
}

原理:每接收1个字节触发中断,手动管理缓冲区

实现步骤

Step1:启动单字节接收

uint8_t rx_byte;
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);

Step2:在回调中填充缓冲区

#define MAX_BUF_SIZE 256
uint8_t rx_buffer[MAX_BUF_SIZE];
uint16_t rx_index = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
        } else {
            rx_index = 0; // 缓冲区溢出处理
        }
        // 重启接收
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
    }
}

HAL库提供HAL_UARTEx_ReceiveToIdle_XXX系列函数,简化不定长接收:

使用 HAL_UARTEx_ReceiveToIdle_DMA

可以自动处理IDLE中断和DMA传输;

支持超时和IDLE双触发条件;

// 启动接收,直到IDLE或超时
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));

// 接收完成回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) 
}

前面的代码还存在问题与对应的解决方案

代码优化(IDLE + DMA双缓冲区)

// 定义双缓冲区
uint8_t rx_buf1[256], rx_buf2[256];
volatile bool buf1_ready = false, buf2_ready = false;
uint16_t data_length = 0;

// 启动首次接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf1, sizeof(rx_buf1));

// 接收事件回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)  else {
            data_length = Size;
            buf2_ready = true;
            // 切换回缓冲区1
            HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf1, sizeof(rx_buf1));
        }
    }
}

// 主循环中处理数据
while (1) 
    if (buf2_ready) {
        process_data(rx_buf2, data_length);
        buf2_ready = false;
    }
}

1、清除中断标志:在IDLE中断中必须调用__HAL_UART_CLEAR_IDLEFLAG,否则会持续触发;

2、DMA循环模式:若使用循环DMA,需确保缓冲区足够大以避免数据覆盖;

3、线程安全:在中断和主程序间传递数据时,需使用标志位或临界区保护;

4、协议解析:在process_data中需实现数据校验(如CRC)和协议解析。

赞(0)
未经允许不得转载:上海聚慕医疗器械有限公司 » RSPLT是什么品牌STM32单片机RS232串口通讯从入门到精通

登录

找回密码

注册