STM32-HAL库实战轻松实现串口通信驱动蓝牙模块与ESP8266开发
STM32 HAL库实战:轻松实现串口通信驱动蓝牙模块与ESP8266开发
STM32 HAL库实战:轻松实现串口通信驱动蓝牙模块与ESP8266开发
引言
STM32F103C8T6作为一款性能强劲的32位微控制器,广泛应用于各类嵌入式系统。本文将详细介绍如何使用STM32F103C8T6的HAL库进行串口通信,并展示如何通过串口驱动蓝牙模块(如HC-05)和WiFi模块(如ESP8266),实现无线通信功能。
一、串口通信基础
1.1 串口通信原理
串口通信是一种广泛使用的通信方式,通过一根发送线(TX)和一根接收线(RX)进行数据传输。STM32F103C8T6提供了多个USART(通用同步异步收发器)接口,支持全双工通信。
1.2 HAL库配置串口
串口初始化设置 :
在HAL库中,串口通信的配置主要包括波特率设置、数据位、停止位和校验位等。以下是一个基本的串口初始化代码示例: UART_HandleTypeDef uart1_handle; /* UART1句柄 / /*
- @brief 串口1初始化函数
- @param baudrate: 波特率, 根据自己需要设置波特率值
- @retval 无 / void uart1_init(uint32_t baudrate) { /UART1 初始化设置/ uart1_handle.Instance = USART1; / USART1 / uart1_handle.Init.BaudRate = baudrate; / 波特率 / uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; / 字长为8位数据格式 / uart1_handle.Init.StopBits = UART_STOPBITS_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); / HAL_UART_Init()会使能UART1 */ }
串口底层初始化:
在以下代码中,当函数调用HAL_UART_Init()函数时,就会自动调用 HAL_UART_MspInit(),以完成底层硬件相关的配置。 在下方代码中,我们配置与硬件相关的部分,例如:
- 使能UART和GPIO的时钟。
- 初始化TX/RX引脚(配置为复用功能)。
- 配置中断(串口中断和总线空闲中断)。 /**
- @brief UART底层初始化函数
- @param huart: UART句柄类型指针
- @note 此函数会被HAL_UART_Init()调用
- 完成时钟使能,引脚配置,中断配置
- @retval 无 */ void HAL_UART_MspInit(UART_HandleTypeDef huart) { GPIO_InitTypeDef gpio_init_struct; if (huart->Instance == USART1) / 如果是串口1,进行串口1 MSP初始化 / { __HAL_RCC_GPIOA_CLK_ENABLE(); / 使能串口TX脚时钟 / __HAL_RCC_USART1_CLK_ENABLE(); / 使能串口时钟 / gpio_init_struct.Pin = GPIO_PIN_9; / 串口发送引脚号 / gpio_init_struct.Mode = GPIO_MODE_AF_PP; / 复用推挽输出 / gpio_init_struct.Pull = GPIO_PULLUP; / 上拉 / gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; / IO速度设置为高速 / HAL_GPIO_Init(GPIOA, &gpio_init_struct); gpio_init_struct.Pin = GPIO_PIN_10; / 串口RX脚 模式设置 / gpio_init_struct.Mode = GPIO_MODE_AF_INPUT; HAL_GPIO_Init(GPIOA, &gpio_init_struct); / 串口RX脚 必须设置成输入模式 / HAL_NVIC_EnableIRQ(USART1_IRQn); / 使能USART1中断通道 / HAL_NVIC_SetPriority(USART1_IRQn, 3, 3); / 组2,最低优先级:抢占优先级3,子优先级3 / __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); / 使能UART1接收中断 / __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); / 使能UART1总线空闲中断 */ } }
串口中断处理函数:
这边暂时体供串口1的中断处理函数。由于我们串口1开启了两个中断:
- 接收寄存器非空触发中断
- 串口空闲线路中断 我们在以下中断处理函数中,处理这两个中断。接收非空中断触发时,将接收到的数据进行缓存到数组中,当触发空闲中断说明数据接收完毕,则将数据打印出来。这边使用printf函数,需要堆fputc函数进行该写。 #define UART1_RX_BUF_SIZE 128 // #define UART1_TX_BUF_SIZE 64 uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE]; /* UART1接收缓冲区 / /*
- @brief UART1接收缓冲区清除
- @param 无
- @retval 无 / void uart1_rx_clear(void) { memset(uart1_rx_buf, 0, sizeof(uart1_rx_buf)); / 清空接收缓冲区 / uart1_rx_len = 0; / 接收计数器清零 / } /*
- @brief 串口1中断服务函数
- @note 在此使用接收中断及空闲中断,实现不定长数据收发
- @param 无
- @retval 无 */ void HAL_UART_MspInit(UART_HandleTypeDef huart) { GPIO_InitTypeDef gpio_init_struct; if (huart->Instance == USART1) / 如果是串口1,进行串口1 MSP初始化 / { __HAL_RCC_GPIOA_CLK_ENABLE(); / 使能串口TX脚时钟 / __HAL_RCC_USART1_CLK_ENABLE(); / 使能串口时钟 / gpio_init_struct.Pin = GPIO_PIN_9; / 串口发送引脚号 / gpio_init_struct.Mode = GPIO_MODE_AF_PP; / 复用推挽输出 / gpio_init_struct.Pull = GPIO_PULLUP; / 上拉 / gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; / IO速度设置为高速 / HAL_GPIO_Init(GPIOA, &gpio_init_struct); gpio_init_struct.Pin = GPIO_PIN_10; / 串口RX脚 模式设置 / gpio_init_struct.Mode = GPIO_MODE_AF_INPUT; HAL_GPIO_Init(GPIOA, &gpio_init_struct); / 串口RX脚 必须设置成输入模式 / HAL_NVIC_EnableIRQ(USART1_IRQn); / 使能USART1中断通道 / HAL_NVIC_SetPriority(USART1_IRQn, 3, 3); / 组2,最低优先级:抢占优先级3,子优先级3 / __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); / 使能UART1接收中断 / __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); / 使能UART1总线空闲中断 / } else if (huart->Instance == USART2) / 如果是串口2,进行串口2 MSP初始化 / { __HAL_RCC_GPIOA_CLK_ENABLE(); / 使能串口TX脚时钟 / __HAL_RCC_USART2_CLK_ENABLE(); / 使能串口时钟 / gpio_init_struct.Pin = GPIO_PIN_2; / 串口发送引脚号 / gpio_init_struct.Mode = GPIO_MODE_AF_PP; / 复用推挽输出 / gpio_init_struct.Pull = GPIO_PULLUP; / 上拉 / gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; / IO速度设置为高速 / HAL_GPIO_Init(GPIOA, &gpio_init_struct); gpio_init_struct.Pin = GPIO_PIN_3; / 串口RX脚 模式设置 / gpio_init_struct.Mode = GPIO_MODE_AF_INPUT; HAL_GPIO_Init(GPIOA, &gpio_init_struct); / 串口RX脚 必须设置成输入模式 / HAL_NVIC_EnableIRQ(USART2_IRQn); / 使能USART2中断通道 / HAL_NVIC_SetPriority(USART2_IRQn, 3, 3); / 组2,最低优先级:抢占优先级3,子优先级3 / __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); / 使能UART2接收中断 / __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); / 使能UART2总线空闲中断 */ } }
改写fputc函数
我们改写fputc函数,这样就可以直接使用printf函数进行串口打印。 /**
- @brief 重定义fputc函数
- @note printf函数最终会通过调用fputc输出字符串到串口
*/
int fputc(int ch, FILE f)
{
while ((USART1->SR & 0X40) == 0); / 等待上一个字符发送完成 /
USART1->DR = (uint8_t)ch; / 将要发送的字符 ch 写入到DR寄存器 */
return ch;
}
做完这些操作,编译时也记得打开使用MicroLIB。
至此,我们可以使用printf函数直接利用CH340单片机给电脑发送串口消息。
二、驱动蓝牙模块(HC-08)
2.1 硬件连接
将HC-08蓝牙模块的TX引脚连接到STM32的RX引脚,将HC-08的RX引脚连接到STM32的TX引脚。同时,确保蓝牙模块和STM32共地。
2.2 软件配置
在串口初始化完成后,可以通过串口发送AT指令来配置HC-08蓝牙模块。例如,设置蓝牙模块的名称和配对密码, 我们使用串口二与之交叉相连。
初始化函数
UART_HandleTypeDef uart2_handle= {0}; void bt_init(uint32_t baudrate) { /uart2 初始化设置/ uart2_handle.Instance = USART2; /* USART1 / uart2_handle.Init.BaudRate = baudrate; / 波特率 / uart2_handle.Init.WordLength = UART_WORDLENGTH_8B; / 字长为8位数据格式 / uart2_handle.Init.StopBits = UART_STOPBITS_1; / 一个停止位 / uart2_handle.Init.Parity = UART_PARITY_NONE; / 无奇偶校验位 / uart2_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; / 无硬件流控 / uart2_handle.Init.Mode = UART_MODE_TX_RX; / 收发模式 / HAL_UART_Init(&uart2_handle); / HAL_UART_Init()会使能uart2 */ }
中断处理函数
/**
- @brief uart2接收缓冲区清除
- @param 无
- @retval 无 / void uart2_rx_clear(void) { memset(uart2_rx_buf, 0, sizeof(uart2_rx_buf)); / 清空接收缓冲区 / uart2_rx_len = 0; / 接收计数器清零 / } /*
- @brief 串口2中断服务函数
- @note 在此使用接收中断及空闲中断,实现不定长数据收发
- @param 无
- @retval 无 / void USART2_IRQHandler(void) { uint8_t receive_data = 0; if(__HAL_UART_GET_FLAG(&uart2_handle, UART_FLAG_RXNE) != RESET){ / 获取接收RXNE标志位是否被置位 / if(uart2_rx_len >= sizeof(uart2_rx_buf)) / 如果接收的字符数大于接收缓冲区大小, / uart2_rx_len = 0; / 则将接收计数器清零 / HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000); / 接收一个字符 / uart2_rx_buf[uart2_rx_len++] = receive_data; / 将接收到的字符保存在接收缓冲区 / } if (__HAL_UART_GET_FLAG(&uart2_handle, UART_FLAG_IDLE) != RESET) / 获取接收空闲中断标志位是否被置位 / { printf(“bt recv: %s\r\n”, uart2_rx_buf); / 将接收到的数据打印出来 / uart2_rx_clear(); __HAL_UART_CLEAR_IDLEFLAG(&uart2_handle); / 清除UART总线空闲中断 */ } }
重写蓝牙发送函数
#include “stdarg.h” void bt_send(char * format, …) { uint8_t send_buf[128] ={0}; va_list arg; va_start(arg, format); vsprintf((char *)send_buf, format, arg); va_end(arg); HAL_UART_Transmit(&uart2_handle, send_buf, sizeof(send_buf), 100); }
2.3 数据收发
配置完成后,即可通过串口与蓝牙模块进行数据收发。例如: int main(void) { HAL_Init(); /* 初始化HAL库 / stm32_clock_init(RCC_PLL_MUL9); / 设置时钟, 72Mhz */ uart1_init(115200); bt_init(115200); printf(“hello world!\r\n”); uint8_t i = 0; while(1) { bt_send(“hello, bt%d\r\n”, i++); delay_ms(500); } }
三、驱动WiFi模块(ESP8266)
为避免文章过长,ESP8266的驱动,本文暂时只讲最简单的连接测试,他的几种连接方式放在明天的文章中,进行详细介绍。
3.1 硬件连接
将ESP8266模块的TX引脚连接到STM32的RX引脚,将ESP8266的RX引脚连接到STM32的TX引脚。同时,确保ESP8266的VCC、GND正确连接。
3.2 软件配置
ESP8266的配置相对复杂,需要通过串口发送AT指令进行WiFi连接和TCP/UDP通信设置。
在这个案例中,我们接收连续字符串不采用空闲中断,所以在HAL_UART_MspInit
函数中,我们将串口2的IDLE中断注释。
以下是一个简单的WiFi连接示例:
串口初始化
#define ESP8266_RX_BUF_SIZE 128 #define ESP8266_TX_BUF_SIZE 64 #define ESP8266_EOK 0 #define ESP8266_ERROR 1 #define ESP8266_ETIMEOUT 2 #define ESP8266_EINVAL 3 UART_HandleTypeDef esp8266_handle = {0}; void esp8266_uart_init(uint32_t baudrate) { esp8266_handle.Instance = USART2; esp8266_handle.Init.BaudRate = baudrate; esp8266_handle.Init.WordLength = UART_WORDLENGTH_8B; esp8266_handle.Init.StopBits = UART_STOPBITS_1; esp8266_handle.Init.Parity = UART_PARITY_NONE; esp8266_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; esp8266_handle.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&esp8266_handle); }
中断函数
uint8_t esp8266_rx_buf[ESP8266_RX_BUF_SIZE]; uint16_t esp8266_cnt = 0, esp8266_cntPre = 0; void USART2_IRQHandler(void) { uint8_t receive_data = 0; if(__HAL_UART_GET_FLAG(&esp8266_handle, UART_FLAG_RXNE) != RESET) { if(esp8266_cnt >= sizeof(esp8266_rx_buf)) esp8266_cnt = 0; HAL_UART_Receive(&esp8266_handle, &receive_data, 1, 1000); esp8266_rx_buf[esp8266_cnt++] = receive_data; //uart1_cnt++; //HAL_UART_Transmit(&uart1_handle, &receive_data, 1, 1000); } }
判断函数
uint8_t esp8266_wait_receive(void) { if(esp8266_cnt == 0) return ESP8266_ERROR; if(esp8266_cnt == esp8266_cntPre) { esp8266_cnt = 0; return ESP8266_EOK; } esp8266_cntPre = esp8266_cnt; return ESP8266_ERROR; }
清除函数
void esp8266_rx_clear(void) { memset(esp8266_rx_buf, 0, sizeof(esp8266_rx_buf)); esp8266_cnt = 0; }
接收函数
void esp8266_receive_data(void) { if(esp8266_wait_receive() == ESP8266_EOK) { printf(“esp8266 recv: %s\r\n”, esp8266_rx_buf); esp8266_rx_clear(); } }
发送函数
uint8_t esp8266_send_command(char *cmd, char *res) { uint8_t time_out = 250; esp8266_rx_clear(); HAL_UART_Transmit(&esp8266_handle, (uint8_t )cmd, strlen(cmd), 100); while(time_out–) { if(esp8266_wait_receive() == ESP8266_EOK) { if(strstr((const char)esp8266_rx_buf, res) != NULL) return ESP8266_EOK; } delay_ms(10); } return ESP8266_ERROR; }
测试函数
void esp8266_test(void) { if(esp8266_send_command(“AT”, “OK”) == ESP8266_EOK) printf(“esp8266 test: %s\r\n”, esp8266_rx_buf); }
3.3 数据收发
WiFi连接成功后,即可通过串口与ESP8266进行TCP/UDP数据收发。例如,发送数据到服务器: #include “sys.h” #include “delay.h” #include “uart1.h” #include “esp8266.h” int main(void) { HAL_Init(); /* 初始化HAL库 / stm32_clock_init(RCC_PLL_MUL9); / 设置时钟, 72Mhz */ uart1_init(115200); esp8266_init(115200); printf(“hello world!\r\n”); while(1) { esp8266_test(); delay_ms(10); } }
四、总结
本文通过详细的步骤介绍了如何在STM32F103C8T6上使用HAL库进行串口通信,并展示了如何通过串口驱动蓝牙模块(HC-08)和WiFi模块(ESP8266)。这些技术为实现无线通信功能提供了坚实的基础,适用于各种嵌入式系统应用。