stm32基础学习——串口(USART)的基本使用

stm32基础学习------串口的基本使用

所有可用资源可在我的gitee仓库查找,点击即可前往

一、前言

本篇文章继续介绍stm32的一些基本使用,文章主要是介绍串口的基本使用,包括使用DMA和不使用DMA的方式发送数据和接收数据,关于串口的使用在代码量上较之前来说确实多了不少,说实话让我从头到尾写过去我也做不到,大家开始只需要了解基本的使用流程,用得多了有经验后自然能熟练使用。

三、准备工作

硬件

  • stm32F103C8T6开发板
  • ST-LINK V2下载器一个
  • USB转TTL模块一个
  • 杜邦线若干+面包板

软件

其他(需要自行下载,点击前往,下载template文件即可

  • stm32工程模板(由于新建模板比较麻烦,初学还是找个工程模板吧,方便点)

电路图: 这里就不贴图了,因为比较简单,我用的板子上的PA9(TX)和PA10(RX)这两个引脚,只需要把usb转ttl模块的TX和RX接口与是stm32的交替连接,也就是TTL的TX接PA10,RX接PA9,最后共地即可。

三、编写代码

1. 用DMA方式

基本使用的整体流程主要如下:时钟配置(USART及GPIO)、引脚配置、USART配置、中断配置、DMA配置(发送和接收)、启动USART,还有配置对应USART的中断函数。

cpp 复制代码
#include "stm32f10x.h"
#include "string.h"

uint8_t Serial_RxBuffer[100];
uint8_t Serial_TxBuffer[100];
char Serial_RxPacket[100];  //缓冲区
uint8_t Serial_RxFlag = 0;
uint16_t Serial_RxLength = 0;

// 声明发送函数
void uart_send_dma(uint8_t *data, uint16_t length);
void uart_send_string(char *str);

void uart_init(void)
{
    // 1. 开启时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); // 开启USART1时钟(APB2总线)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);   // 开启GPIOA时钟(PA9、PA10引脚)

    // 2. 初始化GPIO引脚:
    GPIO_InitTypeDef GPIO_Initstructure;
    // TX引脚(PA9)- 复用推挽输出模式,用于串口发送数据
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP;    // 复用推挽输出
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_9;          // PA9(USART1_TX)
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz; 
    GPIO_Init(GPIOA,&GPIO_Initstructure);
    // RX引脚(PA10)- 上拉输入模式,用于串口接收数据
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_IPU;        // 上拉输入
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_10;           // PA10(USART1_RX)
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_Initstructure);

    // 3. 初始化USART串口参数:
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 115200;    // 波特率:115200 bits/s
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 无硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;                  // 使能发送和接收模式
    USART_InitStructure.USART_Parity = USART_Parity_No;         // 无校验位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;      // 1个停止位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;  // 8位数据位
    USART_Init(USART1, &USART_InitStructure);

    // 4. 初始化中断:
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);  // 使能IDLE(空闲)中断,用于检测一帧数据接收完成
    
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  // 设置中断优先级分组为组2(2位抢占优先级,2位响应优先级)
    
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;  // 中断通道:USART1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;       // 使能中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级为1(0-3,数值越小优先级越高)
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;    // 子优先级为1(0-3,数值越小优先级越高)
    NVIC_Init(&NVIC_InitStructure);  // 应用NVIC配置

    // 5. 配置DMA(直接存储器访问):
    // 开启DMA时钟(DMA挂载在AHB总线上)
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // 接收DMA配置(USART1_RX使用DMA1通道5)
    DMA_InitTypeDef DMA_InitStructure;
    DMA_DeInit(DMA1_Channel5);  // 复位DMA1通道5到默认状态
    
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;  // 外设地址:USART1数据寄存器
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Serial_RxBuffer;   // 内存地址:接收缓冲区
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  // 传输方向:外设→内存(Peripheral Source)
    DMA_InitStructure.DMA_BufferSize = 100;             // 缓冲区大小:100字节
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  // 外设地址不递增(始终读取USART->DR)
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;           // 内存地址递增(顺序存储到缓冲区)
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  // 外设数据宽度:字节
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;          // 内存数据宽度:字节
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;     // 循环模式:缓冲区满后自动从头开始覆盖
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  // 优先级:中等
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;        // 非内存到内存模式(外设到内存)
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);  // 应用配置到DMA1通道5
    
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);  // 使能USART1的DMA接收请求
    DMA_Cmd(DMA1_Channel5, ENABLE);  // 使能DMA1通道5
    DMA_ClearFlag(DMA1_FLAG_TC5 | DMA1_FLAG_HT5 | DMA1_FLAG_TE5);   // 清除DMA通道5的标志位(传输完成、半传输、传输错误)
    
    // 发送DMA配置(USART1_TX使用DMA1通道4)
    DMA_DeInit(DMA1_Channel4);  // 复位DMA1通道4到默认状态
    
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;  // 外设地址:USART1数据寄存器
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Serial_TxBuffer;   // 内存地址:发送缓冲区
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;  // 传输方向:内存→外设(Peripheral Destination)
    DMA_InitStructure.DMA_BufferSize = 1;               // 缓冲区大小:设为1(避免0导致的立即完成)
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  // 外设地址不递增(始终写入USART->DR)
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;           // 内存地址递增(顺序读取缓冲区数据)
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  // 外设数据宽度:字节
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;          // 内存数据宽度:字节
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;       // 普通模式:传输指定次数后停止
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  // 优先级:中等
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;        // 非内存到内存模式
    DMA_Init(DMA1_Channel4, &DMA_InitStructure);  // 应用配置到DMA1通道4
    
    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);  // 使能USART1的DMA发送请求
    DMA_Cmd(DMA1_Channel4, DISABLE);  // 初始禁用!等待发送时再启用

    // 6. 最后使能USART1串口
    USART_Cmd(USART1, ENABLE);  // 使能USART1,开始工作
}

// DMA发送函数(阻塞式)
void uart_send_dma(uint8_t *data, uint16_t length)
{
    // 1. 等待DMA通道空闲(通过检查CNDTR或EN位)
    // 方法1:检查DMA通道是否使能(读取CCR寄存器的EN位)
    while((DMA1_Channel4->CCR & DMA_CCR1_EN) != 0);
    // 2. 清除可能的完成标志
    DMA_ClearFlag(DMA1_FLAG_TC4);
    // 3. 复制数据到发送缓冲区
    if(length > 100) length = 100;  // 防止溢出
    memcpy(Serial_TxBuffer, data, length);
    // 4. 设置DMA传输数据长度
    DMA1_Channel4->CNDTR = length;  // 设置要发送的字节数
    // 5. 启动DMA传输
    DMA_Cmd(DMA1_Channel4, ENABLE);
    // 6. 等待传输完成(阻塞等待)
    while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET);
    // 7. 传输完成后禁用DMA通道,准备下一次发送
    DMA_Cmd(DMA1_Channel4, DISABLE);
}

// 发送字符串的辅助函数
void uart_send_string(char *str)
{
    uint16_t len = strlen(str);
    if(len > 0) {
        uart_send_dma((uint8_t*)str, len);
    }
}

//串口中断函数
void USART1_IRQHandler(void){
    //检查IDLE中断(空闲中断,表示一帧数据接收完成)
    if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET){
        //正确清除IDLE中断标志(必须的操作顺序)
        //注意:IDLE中断标志清除需要先读SR,再读DR
        (void)USART1->SR;  // 读取状态寄存器
        (void)USART1->DR;  // 读取数据寄存器(关键:清除IDLE标志)
        
        //关闭DMA接收通道(暂停DMA,防止数据在处理期间被覆盖)
        DMA_Cmd(DMA1_Channel5, DISABLE);

        //计算本次接收的数据长度
        //CNDTR:DMA通道传输数据量计数器,每传输一个字节减1
        //初始值=100,当前值=CNDTR,所以已接收字节数=100-CNDTR
        uint16_t data_len = 100 - DMA1_Channel5->CNDTR;

        //清空目标缓冲区并拷贝数据
        if(data_len > 0){
            memset(Serial_RxPacket, 0, 100); //清空整个目标数组
            memcpy(Serial_RxPacket, Serial_RxBuffer, data_len); //复制数据到用户缓冲区
            Serial_RxPacket[data_len] = '\0'; //添加字符串结束符
            Serial_RxLength = data_len; //记录数据长度
            Serial_RxFlag = 1; //标记有新数据需要处理
        }

        //重装DMA计数器,继续接收下一帧数据
        //注意:循环模式下,只需重置CNDTR即可
        DMA1_Channel5->CNDTR = 100;  // 重置传输计数器
        DMA_Cmd(DMA1_Channel5, ENABLE);  // 重新使能DMA接收

        //清IDLE中断标志(可选,已在读DR时清除)
        USART_ClearITPendingBit(USART1, USART_IT_IDLE);
        return;
    }
}

int main(void)
{
    uart_init();
    
    while(1){
        // 检查是否有新数据
        if(Serial_RxFlag == 1){
            // 回显接收到的数据(测试发送功能)
            uart_send_string("Echo: ");
            uart_send_string(Serial_RxPacket);
            uart_send_string("\r\n");
            // 清除接收标志,准备接收下一帧数据
            Serial_RxFlag = 0;
        }
    }
}

下载上面程序并烧录,打开keil的调试窗口,并按以下步骤进行设置,然后打开串口调试工具(网上有很多,随便一个都行,我这里也提供了我自己使用的,在我的gitee仓库也有,点击获取),设置好参数,发送想要的数据。


2. 中断方式(不使用DMA)

其实整体的流程差不多,中断方式要比DMA方式简单,但是DMA方式比较高效点,大家可以好好比对以下两种代码主要哪里不同,具体就不多说了,还是那句话,会用就行,下面就是中断方式的示例代码。

cpp 复制代码
#include "stm32f10x.h"
#include "string.h"

// 全局变量
char Serial_RxPacket[100];      // 接收数据包缓冲区
uint8_t Serial_RxFlag = 0;      // 接收完成标志
uint16_t Serial_RxLength = 0;   // 接收数据长度

// 发送单个字节函数
void USART_SendByte(uint8_t data)
{
    // 等待发送缓冲区为空
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    
    // 发送数据
    USART_SendData(USART1, data);
    
    // 等待发送完成
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}

// 发送字符串函数
void USART_SendString(char *str)
{
    while(*str)
    {
        USART_SendByte(*str++);
    }
}

void uart_init(void)
{
    // 1. 系统时钟初始化(必须)
    SystemInit();
    
    // 2. 开启AFIO时钟(重要)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    
    // 3. 开启USART1和GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 4. 初始化GPIO引脚
    GPIO_InitTypeDef GPIO_Initstructure;
    
    // TX - PA9(连接到PC的RX)
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP;    // 复用推挽输出
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_Initstructure);
    
    // RX - PA10(连接到PC的TX)
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_IPU;  // 浮空输入
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_Initstructure);

    // 5. 初始化USART参数
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 115200;    // 波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);

    // 6. 初始化中断(不用DMA,使用RXNE和IDLE中断)
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);  // 与DMA不同之处,接收数据中断(每个字节触发)
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);  // 空闲中断(检测帧结束)
    
    // 7. 配置中断优先级
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);

    // 8. 打开串口
    USART_Cmd(USART1, ENABLE);
    
}

// 串口中断函数(非DMA版本)
void USART1_IRQHandler(void)
{
    static uint8_t rx_buffer[100];      // 临时接收缓冲区(类似DMA的Serial_RxBuffer)
    static uint16_t rx_index = 0;       // 接收索引(类似DMA的CNDTR)
    
    // 1. 处理接收数据中断(RXNE)
    if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    {
        // 读取接收到的字节(类似DMA自动搬运)
        uint8_t rx_data = USART_ReceiveData(USART1);
        
       /*存储到缓冲区(DMA自动完成,这里需要手动)
			这里只针对字节数小于100的情况,if语句可写可不写
			当然规范情况要写*/
        if(rx_index < 100)  // 防止溢出
        {
            rx_buffer[rx_index] = rx_data;
            rx_index++;
        }
        else 
        {
            // 缓冲区溢出,重置
            rx_index = 0;
        }
        
        // 读取DR寄存器已自动清除RXNE标志
    }
    
    // 2. 处理IDLE中断(帧结束检测)
    if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET)
    {
        // 正确清除IDLE中断标志(必须)
        volatile uint32_t temp;
        temp = USART1->SR;  // 读取状态寄存器
        temp = USART1->DR;  // 读取数据寄存器 - 清除IDLE标志
        (void)temp;
        
        // 3. 计算接收长度(类似DMA的 data_len = 100 - CNDTR)
			  // 这里直接是索引值(规定数据数小于100字节的情况下!!)
        uint16_t data_len = rx_index;  
        
        // 4. 处理接收到的数据(类似DMA的中断处理)
        if(data_len > 0)
        {
            // 清空目标缓冲区并拷贝数据
            memset(Serial_RxPacket, 0, sizeof(Serial_RxPacket));
            memcpy(Serial_RxPacket, rx_buffer, data_len);
            Serial_RxPacket[data_len] = '\0';
            
            Serial_RxLength = data_len;
            Serial_RxFlag = 1;  // 标记有新数据
        }
        
        // 5. 重置接收缓冲区(类似DMA的重装CNDTR = 100)
        rx_index = 0;
        // 可选:清空接收缓冲区
        // memset(rx_buffer, 0, sizeof(rx_buffer));
    }
}

int main(void)
{
    // 初始化串口
    uart_init();
    
    // 主循环
    while(1)
    {
        // 检查是否收到数据(类似DMA版本的检查)
        if(Serial_RxFlag == 1)
        {
            Serial_RxFlag = 0;  // 清除标志
            
            // 处理接收到的数据
            USART_SendString(Serial_RxPacket);
        }
        
        // 简单延时
        for(volatile int i = 0; i < 1000; i++);
    }
}

同样烧录代码,跟上面DMA方式的操作流程一样,可以看到同样的效果,这里就不贴图了。

四、结束语

好了,stm32的基本使用------串口(USART)的使用先介绍到这,后续有时间我继续更新这方面的文章,敬请期待,我是"风行男孩",咱有缘再见!(码字不易,转载请标明出处!若是文章有帮助,点个赞留个痕迹即可。)

相关推荐
诺狞猫2 小时前
SF32LB52-i2c驱动TM1650
学习·sifli
点灯小铭2 小时前
基于单片机的多模式档位调节与过热保护风扇设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
却道天凉_好个秋2 小时前
音视频学习(八十二):mp4v
学习·音视频·mp4v
好奇龙猫2 小时前
【大学院-筆記試験練習:数据库(データベース問題訓練) と 软件工程(ソフトウェア)(6)】
学习
咚咚王者2 小时前
人工智能之核心基础 机器学习 第十一章 无监督学习总结
人工智能·学习·机器学习
0和1的舞者2 小时前
Python 中四种核心数据结构的用途和嵌套逻辑
数据结构·python·学习·知识
在路上看风景2 小时前
01. 学习教程链接
学习
星源~2 小时前
Zephyr - MCU 开发快速入门指南
单片机·嵌入式硬件·物联网·嵌入式开发·zephyr
星源~2 小时前
zephyr-开发环境配置疑难问题解决
单片机·嵌入式硬件·物联网·项目开发