STM32 HAL库实战:轻松实现串口通信驱动蓝牙模块与ESP8266开发

STM32 HAL库实战:轻松实现串口通信驱动蓝牙模块与ESP8266开发


引言

STM32F103C8T6作为一款性能强劲的32位微控制器,广泛应用于各类嵌入式系统。本文将详细介绍如何使用STM32F103C8T6的HAL库进行串口通信,并展示如何通过串口驱动蓝牙模块(如HC-05)和WiFi模块(如ESP8266),实现无线通信功能。


一、串口通信基础

1.1 串口通信原理

串口通信是一种广泛使用的通信方式,通过一根发送线(TX)和一根接收线(RX)进行数据传输。STM32F103C8T6提供了多个USART(通用同步异步收发器)接口,支持全双工通信。

1.2 HAL库配置串口

串口初始化设置

在HAL库中,串口通信的配置主要包括波特率设置、数据位、停止位和校验位等。以下是一个基本的串口初始化代码示例:

c 复制代码
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引脚(配置为复用功能)。
  • 配置中断(串口中断和总线空闲中断)。
c 复制代码
/**
 * @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函数进行该写。

c 复制代码
#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函数进行串口打印。

c 复制代码
/**
 * @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蓝牙模块。例如,设置蓝牙模块的名称和配对密码,

我们使用串口二与之交叉相连。

初始化函数
c 复制代码
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 */
}
中断处理函数
c 复制代码
/**
 * @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总线空闲中断 */
    }
}
重写蓝牙发送函数
c 复制代码
#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 数据收发

配置完成后,即可通过串口与蓝牙模块进行数据收发。例如:

c 复制代码
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连接示例:

串口初始化
c 复制代码
#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
c 复制代码
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);
}
中断函数
c 复制代码
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);
    }
}
判断函数
c 复制代码
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;
}
清除函数
c 复制代码
void esp8266_rx_clear(void)
{
    memset(esp8266_rx_buf, 0, sizeof(esp8266_rx_buf));
    esp8266_cnt = 0;
}
接收函数
c 复制代码
void esp8266_receive_data(void)
{
    if(esp8266_wait_receive() == ESP8266_EOK)
    {
        printf("esp8266 recv: %s\r\n", esp8266_rx_buf);
        esp8266_rx_clear();
    }
}
发送函数
c 复制代码
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;
}
测试函数
c 复制代码
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数据收发。例如,发送数据到服务器:

c 复制代码
#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)。这些技术为实现无线通信功能提供了坚实的基础,适用于各种嵌入式系统应用。

相关推荐
智商偏低5 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen6 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森8 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白8 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D9 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术12 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt12 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘12 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang12 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n15 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件