串口通信作为嵌入式系统中最基础也最常用的通信方式,掌握其原理和实现方法对于STM32开发者来说至关重要。这次我们将通过HAL库实现STM32与电脑之间的双向串口通信,并完成一个实用的LED控制功能。这个项目不仅适合初学者入门,对于有经验的开发者也能从中获得HAL库的最佳实践。
在实际工程中,串口常用于调试信息输出、设备参数配置、固件升级等场景。相比其他通信协议,串口的优势在于硬件简单、协议易懂、调试方便。通过本项目的学习,你将掌握从硬件连接到软件配置的完整流程,以及如何避免常见的通信问题。
串口通信的物理连接看似简单,但有几个关键点需要注意:
电平标准:STM32的USART接口使用TTL电平(0-3.3V),而PC的串口通常是RS232电平(±12V)。这就是为什么我们需要USB转TTL模块作为中介,它完成了电平转换和USB协议转换的双重功能。
交叉连接原则:发送端(Tx)必须连接接收端(Rx),这个规则看似简单但在实际调试中经常被忽略。我曾遇到过一个案例:工程师花费数小时排查通信问题,最后发现只是Tx-Rx连接反了。
共地要求:除了Tx和Rx,GND连接同样重要。没有共同的参考地,通信将极不稳定。在长距离通信时,可以考虑使用差分信号(如RS485)来提高抗干扰能力。
串口数据帧的每个部分都有其特定作用:
起始位:持续1个位时间的低电平,作用是同步时钟。在实际通信中,接收端会检测这个下降沿来启动采样时序。
数据位:通常选择8位,与计算机的字节单位一致。但在某些特殊场合(如Modbus协议)可能会使用9位模式,第9位作为地址/数据标识位。
校验位:虽然很多应用场景下不使用校验,但在工业环境中强烈建议启用。奇偶校验虽然简单,但能捕捉到单比特错误。更可靠的方案是使用CRC校验,不过这需要在软件层面实现。
停止位:不仅标志帧结束,还给了接收方处理时间。在高速通信或长距离传输时,适当增加停止位宽度可以提高可靠性。
波特率的选择需要考虑多方面因素:
常用值:9600、19200、38400、115200等是常见选择。115200bps意味着每秒传输11520字节(考虑起始位和停止位),对于大多数调试用途已经足够。
误差容忍:USART模块对波特率误差有一定容忍度,通常要求误差小于3%。计算实际波特率的公式为:
code复制实际波特率 = fCK / (USARTDIV × 8 × (2 - OVER8))
其中USARTDIV是一个16位无符号定点数(整数部分+小数部分)。
自动波特率检测:某些STM32系列支持自动波特率检测功能,这在对接不同设备时非常有用。可以通过配置USART_CR2寄存器的ABREN位来启用。
开发板选择:任何带有USART接口的STM32开发板都可以,F1系列(如STM32F103C8T6)是性价比很高的选择。
USB转TTL模块:CH340和CP2102是目前最稳定的两种方案。避免使用劣质PL2303模块,它们经常会出现驱动兼容性问题。
连接线材:建议使用带屏蔽的杜邦线,特别是在有强电磁干扰的环境中。线长最好不要超过50cm,否则需要考虑增加终端电阻。
时钟树配置:
USART参数设置:
NVIC设置:
DMA配置(可选):
关键提示:生成代码前务必检查Pinout视图,确认USART引脚没有被其他功能占用。我曾遇到SPI和USART1的引脚冲突导致通信失败的情况。
HAL库提供了多种发送方式,各有适用场景:
c复制// 阻塞式发送(最简单但效率低)
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello", 5, 100);
// 中断发送(适合中等数据量)
HAL_UART_Transmit_IT(&huart1, buffer, length);
// DMA发送(大数据量最佳选择)
HAL_UART_Transmit_DMA(&huart1, buffer, length);
实际项目中,建议封装自己的发送函数,增加以下功能:
接收处理更加复杂,需要考虑数据完整性:
c复制// 阻塞式接收(不推荐在主循环中使用)
uint8_t data;
if(HAL_UART_Receive(&huart1, &data, 1, 100) == HAL_OK) {
// 处理数据
}
// 中断接收(推荐方式)
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
// 在回调函数中处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart == &huart1) {
// 处理rx_buffer中的数据
// 重新启动接收
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
}
}
对于不定长数据,可以考虑以下方案:
原始代码中的LED控制可以进一步优化:
c复制// 定义命令协议
typedef enum {
CMD_LED_ON = 0x01,
CMD_LED_OFF = 0x00,
CMD_LED_TOGGLE = 0x02
} LedCommand;
// 改进后的处理函数
void process_uart_command(uint8_t cmd) {
static uint8_t led_state = 0;
char response[32];
switch(cmd) {
case CMD_LED_ON:
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
snprintf(response, sizeof(response), "LED ON (V%.1f)\r\n", get_voltage());
break;
case CMD_LED_OFF:
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
snprintf(response, sizeof(response), "LED OFF (T=%.1fC)\r\n", get_temperature());
break;
case CMD_LED_TOGGLE:
led_state = !led_state;
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, led_state);
snprintf(response, sizeof(response), "LED TOGGLED (%d)\r\n", led_state);
break;
default:
snprintf(response, sizeof(response), "ERR:0x%02X\r\n", cmd);
}
HAL_UART_Transmit(&huart1, (uint8_t*)response, strlen(response), 100);
}
电平检查:
环回测试:
示波器观测:
数据丢失:
乱码问题:
只能收不能发:
这个基础项目可以扩展出许多实用功能:
一个实用的建议是为项目添加版本信息查询功能:
c复制void send_version_info(void) {
const char *info =
"\r\n"
"STM32 UART Demo v1.0\r\n"
"Board: %s\r\n"
"CPU: %s @ %luMHz\r\n"
"Build: %s %s\r\n";
char buffer[256];
snprintf(buffer, sizeof(buffer), info,
BOARD_NAME,
MCU_NAME,
SystemCoreClock/1000000,
__DATE__, __TIME__);
HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), 100);
}
在实际项目中,我发现很多工程师忽略了串口通信的健壮性设计。一个经验法则是:永远假设传输过程会出现错误。因此,重要的数据应该包含校验机制,关键指令需要确认回复,长时间通信应该有心跳机制。