写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做!
本文写于:2025.04.08
STM32开发板学习——第27节: [9-3] USART串口发送&串口发送+接收
- 前言
- 开发板说明
- 引用
- 解答和科普
- 一、串口发送
- 二、发送+接收
- 问题
- 总结
前言
本次笔记是用来记录我的学习过程,同时把我需要的困难和思考记下来,有助于我的学习,同时也作为一种习惯,可以督促我学习,是一个激励自己的过程,让我们开始32单片机的学习之路。
欢迎大家给我提意见,能给我的嵌入式之旅提供方向和路线,现在作为小白,我就先学习32单片机了,就跟着B站上的江协科技开始学习了.
在这里会记录下江协科技32单片机开发板的配套视频教程所作的实验和学习笔记内容,因为我之前有一个开发板,我大概率会用我的板子模仿着来做.让我们一起加油!
另外为了增强我的学习效果:每次笔记把我不知道或者问题在后面提出来,再下一篇开头作为解答!
开发板说明
本人采用的是慧净的开发板,因为这个板子是我N年前就买的板子,索性就拿来用了。另外我也购买了江科大的学习套间。
原理图如下
1、开发板原理图
2、STM32F103C6和51对比
3、STM32F103C6核心板
视频中的都用这个开发板来实现,如果有资源就利用起来。另外也计划实现江协科技的套件。
下图是实物图
引用
【STM32入门教程-2023版 细致讲解 中文字幕】
还参考了下图中的书籍:
STM32库开发实战指南:基于STM32F103(第2版)
数据手册
解答和科普
一、串口发送
这是USB转串口模块,这里有个跳线帽,说过这个跳线帽要接在VCC和3.3V上,因为VCC是给CH340芯片供电,选择通信的TTL电平为3.3V, 然后通信引脚TXD和RXD要接在STM32的PA9和PA10口,为什么是这两个口呢,看一下引脚定义表,计划用USART1进行通信,所以选择这两个引脚。TX和RX交叉连接,不要接错了。在接线图里,接A9(TX)接的就是串口模块的RXD(接受), 然后串口模块的TXD(发送)要接在STM32的PA10(RX接收)。然后,两个设备之间要把负极接在一起,进行共地,一般多个系统之间互联,都要进行共地,这样电平才能有高低的参考。最后这个串口和STlink都要插在电脑上,这样STM32和串口模块都要独立供电,所以这里通信的电源正极就不需要接了,直接3根线就行了。
初始化流程:
第一步,开启时钟,把需要用的USART和GPIO的时钟打开;
第二部,GPIO初始化,把TX配置成复用输出,RX配置成输入;
第三步,配置USART,直接使用一个结构体,就可以把这里所有参数都配置好了;
第四步,如果你只需要发送的功能,就直接开启USART,初始化就结束了,如果你需要接受的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就行了。
那初始化完成之后,如果要发送数据,调用一个发送函数就行了,如果要接受数据,就调用接受的函数,如果要获取发送和接受的状态,就调用获取标志位的函数,这就是USART外设的使用思路。
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
配置同步时钟输出的,包括时钟是不是要输出,时钟的极性相位等参数,因为参数比较多,也是用结构体配置的;
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
开启USART到DMA的触发通道;
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
这两个函数,在我们发送和接收的时候会用到;写和读DR寄存器,DR寄存器内部有4个寄存器,控制发送与接收,执行细节上一节已经分析过了,写DR就是发送,读DR就是接收,至于怎么产生波形。怎么判断输入,软件不管;
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
标志位相关函数
首先引脚模式:TX引脚是USART外设控制的输出脚,所以要选用复用推挽输出;
RX引脚是USART外设数据输入脚,所以要选择输入模式,输入模式并不分什么普通输入、复用输入,一根线只能有一个输出,但可以有多个输入,所以输入脚外设和GPIO都可以同时用,一般RX配置是浮空输入或者上拉输入,因为串口波形空闲状态时高电平,所以不使用下拉输入,引脚模式不清楚的话,还是可以看一下手册,GPIO那一节有个推荐的配置表,可以参考一下;目前只需要数据发送,所以只初始化TX就行了,引脚模式这里,选择AF_PP;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;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);
这样就是把PA9配置为复用推挽输出,供USART1的TX使用,那引脚就初始化好了;
配置USART
USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600; //波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //控制流USART_InitStructure.USART_Mode=USART_Mode_Tx; //串口模式要想接收再|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);USART_Cmd(USART1,ENABLE); //开启
第一个参数波特率:可以直接写个9600就行,写完之后,这个Init函数内部会自动算好9600对应的分频系数,然后写入到BRR寄存器;
第二个参数是硬件流控制:我们不使用所以选择None;
第三个参数是串口模式:我们放到这里,这里可以选择TX发送模式和RX接收模式,如果你继续要发送有需要接收,那就用或符号把TX和RX或起来,
第四个参数是校验位:
第五个参数是停止位;
第六个参数哦是8位数据;
发送一个字节
void Serial_SendByte(uint8_t Byte) //发送一个字节
{USART_SendData(USART1,Byte);while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);}
main.C
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Serial.h"int main(void)
{OLED_Init();Serial_Init();Serial_SendByte(0x66);OLED_ShowString(1,2,"Hello STM32 MCU");while(1){}
}
Serial
#include "stm32f10x.h" // Device headervoid Serial_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;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); USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600; //波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //控制流USART_InitStructure.USART_Mode=USART_Mode_Tx; //串口模式要想接收再|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);USART_Cmd(USART1,ENABLE); //开启
}void Serial_SendByte(uint8_t Byte) //发送一个字节
{USART_SendData(USART1,Byte);while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);}
#ifndef __SERIAL_H
#define __SERIAL_Hvoid Serial_Init(void);
void Serial_SendByte(uint8_t Byte);#endif
实验现象
HEX模式:只能显示一个个的十六进制数,不能显示文本0x41;
文本模式;以原始数据编码后的形式显示 0x41对应A;
UFT8
Serial_SendByte('A');也是发送0x41;
发送数组
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{uint16_t i;for(i=0;i<Length;i++){Serial_SendByte(Array[i]);}}
发送字符串
void Serial_SendString(char *String)
{uint8_t i;for(i=0;String[i]!='\0';i++){Serial_SendByte(String[i]);}}
Serial_SendString("Hello STM32 MCU\r\n");
这个可以执行换行:\r\n,
发送数字
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{uint32_t Result=1;while (Y--){Result *=X;}return Result;}
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{uint8_t i;for (i=0;i< Length;i++){Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+'0');}}
使用printf函数
printf函数默认输出到屏幕,我们单片机没有屏幕,所以要进行重定向:
在串口模块里加上#include <stdio.h>
int fputc(int ch,FILE *f)
{Serial_SendByte(ch);return ch;
}
printf("Num= %d\r\n",666);
那么重定向fput跟printf有什么关系呢:fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fput函数一个个打印的,我们把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了。
这种方法只能重定向一个,你定向到串口1了,那串口2再用就没有用了,如果多个串口都想用printf怎么办呢,这时候就可以用Sprintf,可以把格式化字符输出到一个字符串里,所以这里可以先定义一个字符串:
char String[100];sprintf(String,"Num= %d\r\n",666);Serial_SendString(String);
定义字符串,打印字符串;再发送字符串。我们要是能封装这个过程,就再好不过了。
封装Sprintf;可变参数
void Serial_Printf(char *format,...)
{char String[100];va_list arg;va_start(arg,format);vsprintf(String, format ,arg);va_end(arg);Serial_SendString(String);}
Serial_Printf("Num= %d\r\n",666);
显示汉字
1、UTF8都是(加上–no-multibyte-chars)
Serial_Printf(“你好,世界”);
2、GB2312
然后再写汉字,选择GBK编码,复位也可以显示了。
二、发送+接收
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);
USART_InitStructure.USART_Mode=USART_Mode_Tx |USART_Mode_Rx;
可以选择查询或者中断,如果使用查询,那初始化就结束了,如果使用中断,那还需要在这里开启中断,配置NVIC;
查询:
查询的流程是,在主函数里不断判断RXNE标志位,如果置1了,就说明收到数据了, 那再调用ReceiveData,读取DR寄存器,这样就行了。主函数演示不封装了,
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Serial.h"uint8_t RxData;
int main(void)
{OLED_Init();Serial_Init();while(1){if(USART_GetFlagStatus(USART1 ,USART_FLAG_RXNE)== SET) //收到数据{RxData=USART_ReceiveData(USART1);}OLED_ShowHexNum(1,1,RxData,2);//清除标志位}
}
中断方法
首先,初始化这里,要加上开启中断的代码:
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);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);
这里RXNE标志位一但置1了,就会向NVIC申请中断,之后我们可以在中断函数里接收数据;
要不要清楚标志位呢,如果你读取了DR,就可以自动清除,如果没读取DR,就可以手动清除。
其实在中断里,只是进行了数据转存一下,最终还是要扫描查询这个RxFlag,来接收数据的;对于单字节接收来说,可能转存一下意义不大,主要展示一下中断接收的写法和多字节数据包接收;
在各自的函数中分别用了;
void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET){Serial_RxData=USART_ReceiveData(USART1);Serial_RxFlag=1;USART_ClearITPendingBit(USART1,USART_IT_RXNE);}}
uint8_t Serial_GetRxFlag(void)
{if(Serial_RxFlag == 1){Serial_RxFlag=0;return 1;}return 0;
}uint8_t Serial_GetRxData (void)
{return Serial_RxData;
}
main.C
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Serial.h"uint8_t RxData;
int main(void)
{OLED_Init();Serial_Init();OLED_ShowString(1,1,"Hello STM32 MCU");OLED_ShowString(2,1,"RxData:");while(1){if(Serial_GetRxFlag()==1) //收到数据{RxData=Serial_GetRxData();Serial_SendByte(RxData);}OLED_ShowHexNum(2,8,RxData,2);//清除标志位}
}
Serial.ch
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdarg.h"uint8_t Serial_RxData;
uint8_t Serial_RxFlag;void Serial_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;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); 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); USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600; //波特率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);USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);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);USART_Cmd(USART1,ENABLE); //开启
}void Serial_SendByte(uint8_t Byte) //发送一个字节
{USART_SendData(USART1,Byte);while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);}void Serial_SendArray(uint8_t *Array,uint16_t Length)
{uint16_t i;for(i=0;i<Length;i++){Serial_SendByte(Array[i]);}}void Serial_SendString(char *String)
{uint8_t i;for(i=0;String[i]!='\0';i++){Serial_SendByte(String[i]);}}
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{uint32_t Result=1;while (Y--){Result *=X;}return Result;}
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{uint8_t i;for (i=0;i< Length;i++){Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+'0');}}int fputc(int ch,FILE *f)
{Serial_SendByte(ch);return ch;
}void Serial_Printf(char *format,...)
{char String[100];va_list arg;va_start(arg,format);vsprintf(String, format ,arg);va_end(arg);Serial_SendString(String);}uint8_t Serial_GetRxFlag(void)
{if(Serial_RxFlag == 1){Serial_RxFlag=0;return 1;}return 0;
}uint8_t Serial_GetRxData (void)
{return Serial_RxData;
}void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET){Serial_RxData=USART_ReceiveData(USART1);Serial_RxFlag=1;USART_ClearITPendingBit(USART1,USART_IT_RXNE);}}
现象
问题
1、只是发送和接收一个字节,如何接收大量数据;
总结
本节课主要学会了如何用串口进行接收和发送,在串口模式可以|起了,发送和接收,然后本次只完成了单字节的接收和发送。