stm32基础学习------串口的基本使用
所有可用资源可在我的gitee仓库查找,点击即可前往。
一、前言
本篇文章继续介绍stm32的一些基本使用,文章主要是介绍串口的基本使用,包括使用DMA和不使用DMA的方式发送数据和接收数据,关于串口的使用在代码量上较之前来说确实多了不少,说实话让我从头到尾写过去我也做不到,大家开始只需要了解基本的使用流程,用得多了有经验后自然能熟练使用。
三、准备工作
硬件:
- stm32F103C8T6开发板
- ST-LINK V2下载器一个
- USB转TTL模块一个
- 杜邦线若干+面包板
软件:
- Keil 5(没安装可以点击进入我的另一篇博客有详细下载及配置教程)
其他(需要自行下载,点击前往,下载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)的使用先介绍到这,后续有时间我继续更新这方面的文章,敬请期待,我是"风行男孩",咱有缘再见!(码字不易,转载请标明出处!若是文章有帮助,点个赞留个痕迹即可。)