STM32单片机学习(15) —— PC串口通信实验

文章目录

在开始串口实验之前我们需要先了解一些OLED,因为在我们这个实验中会用到,并不复杂,简单了解怎么操作就可以,有了OLED可以让我们更直观的观察通信过程。

OLED实验

OLED

简介

OLED屏幕(Organic Light-Emitting Diode,有机发光二极管)是一种基于有机材料的自发光显示技术。

相比于传统的LCD(Liquid Crystal Display,液晶显示屏),OLED屏幕的每个像素都可以独立发光,因此不需要背光模块。

OLED显示器具有以下优点:

  1. 自发光,显示效果好。
  2. 响应速度快,适合动态显示。
  3. 视角广,色彩鲜艳。
  4. 轻薄柔性,设计灵活。
  5. 低功耗:尤其是显示黑色或深色内容时,OLED屏幕的功耗比较低。

当然OLED显示屏也有一些缺点,比如:

  1. 成本较高。
  2. 寿命相对较短。
  3. 容易烧屏(长时间显示静态图像会导致像素老化)。

基于上述特点,OLED显示屏广泛应用于消费电子、工业设备和嵌入式系统等领域。

在STM32等单片机开发中,OLED屏幕常用于显示调试信息、传感器数据或用户界面,是常用的扩展外设。

两种常见的OLED显示屏

在单片机领域,我们常使用的OLED屏幕主要有两种:四针脚版本、七针脚版本。如下所示:

四针脚版本一般都使用I2C作为通信协议,而七针脚版本往往额外支持SPI通信协议,除此之外它们的显示像素的颜色也有差别,比如可以显示白色、黄色、蓝色等。

屏幕的分辨率都比较小,只有128* 64,这对手机等复杂的电子设备来说显然是不够的,但对于我们使用单片机则是完全够用的。

我们的工具盒中使用的OLED显示屏是:0.96寸、四针脚、白色像素显示、供电电源为3.3V/5V。

针脚接线

我们使用的OLED显示屏,一种有四个针脚,如下图所示:

其中GND和VCC针脚就分别接地、以及接上3.3V供电。而SCL和SDA两个引脚是用于和单片机进行I2C通信的两个引脚,其中:

  1. SCL(Serial Clock Line):串行时钟线,用于同步时钟信号。
  2. SDA(Serial Data Line):串行数据线,用于传输实际的数据。

它们的具体作用,待到后续课程讲解I2C通信时,我们再详谈。一般情况下,这两个针脚就需要接入到单片机的I2C通信协议引脚上,但在我给大家的工具函数中,直接使用了GPIO口模拟了I2C协议,所以这两个针脚接到任意GPIO口都是可以的。这句话你可能也不太看得懂,没有关系,后面我们讲完I2C通信,就很容易理解了。下面我们主要关注OLED屏幕的使用,它的原理我们后面都会详细讲解。

OLED屏幕显示字符数量

OLED的分辨率是128*64,即屏幕有 128 列像素和 64 行像素,表示一共有8192个像素点。

而通常使用8列像素,16行像素来表示常见的一个英文字符(比如英文字母、数字等),即一个英文字符需要128个像素点来表示。

这样的话:整个OLED屏幕就可以显示4(64/ 16)行,16(128/8)列,总共64(8192/128)个常见英文字符。

可以参考下面的两张图来理解OLED显示字符的格式:

当然OLED还可以显示中文,甚至也可以各种简单的图案,但对我们本文不涉及,具体就是对像素点的操作用文字取模软件生成点阵,然后输出。

LED显示数据实验

首先需要将OLED硬件设备插入到面包板上,并且各个针脚也必须正确的接入。其接线电路图如下所示:

由于配件盒当中的导线没有长度恰好适配的,所以我们可以拿出两根最短的跳线,来连接SCL/SDA引脚和单片机。

实物接线图大家可以参考一下(颜色无所谓,找两个最短的跳线就行):

引脚插孔不要接错了,可以把板子翻过来仔细看看。

在进行STM32实验时,引脚接错了,是常见的导致实验失败的原因,大家一定要细心一点。

使用OLED模块驱动代码

接线完成后,你可以在链接: https://pan.baidu.com/s/1mVThv6JQw1I8C54aNZdAUw?pwd=njc4 提取码: njc4**" 软件环境 -- 函数模块 -- OLED驱动模块"**中找到头文件和源文件。

然后新建一个工程,将其中的源文件和头文件都复制到工程的Tools目录下,并且将这些文件都添加到Keil5的"Group--Tools"中。这些操作前面都讲过了,这里就不再赘述了。

这段代码的详细原理大家都可以不了解,仅需要了解一部分代码。在文件"OLED.c"源文件的开头,有这样一段代码:

c 复制代码
/*
    引脚配置宏,若改变了SCL和SDA接入引脚需要修改
    当前SCL针脚接入PB10引脚
    当前SDA针脚接入PB11引脚
    建议按照文档中的接线图接线, 这样就无需修改这里!
*/
#define OLED_SCL_GPIO_PORT      GPIOB
#define OLED_SCL_GPIO_PIN       GPIO_Pin_10
#define OLED_SDA_GPIO_PORT      GPIOB
#define OLED_SDA_GPIO_PIN       GPIO_Pin_11

这段代码通过宏定义的方式,配置了显示屏的SCL/SDA引脚和单片机的某两个引脚连接。

建议按照文档中的接线图接线,这样就无需修改这里!

如果你确实不想使用PB10/PB11这两个引脚,就可以修改这段代码,但一般没有这个必要。

然后你就可以直接在main.c中写以下测试代码:

c 复制代码
#include "stm32f10x.h"
#include "OLED.h"
#include "Delay.h"

int main(void) {
    // 初始化OLED
    OLED_Init();
    OLED_ShowChar(1, 1, 'X');
    OLED_ShowString(1, 3, "hello world!");
    OLED_ShowUnsignedNum(2, 1, 6666, 4);
    OLED_ShowSignedNum(2, 6, 1234, 4);
    OLED_ShowSignedNum(2, 12, -1234, 4);
    OLED_ShowHexNum(3, 1, 0xFC12, 4);
    OLED_ShowDouble(3, 8, 3.14159, 1, 5);
    OLED_ShowBinNum(4, 1, 1234, 16);

    // 延时3s后执行清屏
    Delay_S(3);

    OLED_Clear();
    while (1) {

    }
}

这样你就可以看到LED显示屏,显示以下内容:

PC串口通信实验

本文所用的观察实验结果的软件在之前发的网盘链接里,里面的串口助手文件夹里边:嵌入式软件环境

链接: https://pan.baidu.com/s/1mVThv6JQw1I8C54aNZdAUw?pwd=njc4 提取码: njc4

--来自百度网盘超级会员v7的分享

这里需要注意接线图,单片机的Rx,Tx与PC端的Rx,Tx是交叉相接的,这是很容易出现的错误,如果接错就不会有任何实验现象。

实验一:完成基于串口通信的单片机发送数据到PC端

1.单片机向PC发送1个字节数据

2.单片机向PC发送多个字节数据

3.单片机向PC发送字符串

4.单片机向PC发送无符号整数

分析问题

  1. 我们采用USART1来进行串口通信(之前我们讲到APB2外设总线上挂载的外设某种类型外设中的1号选手,比如USART1、TIM1、SPI1、ADC1等)

  2. USART1外设引脚在PA9(TX)和PA10(RX)

  3. 该题中我们只需要发送数据,所以可以只开启USART1_Tx(PA9)

  4. PA9引脚输出模式,串口通信我们需要通过USART外设实现,这里我们使用了USART1。PA9引脚的控制权应该交给USART1来管理,所以输出模式从通用输入输出改变为复用通用输入输出

  5. 我们需要PA9能够输出高电平和低电平,显然开漏模式不能做到这一点(输出高电平时,引脚输出高阻抗,输出低电平时,引脚输出低电平)。所以我们选则复用推挽模式

  6. PA10用来接收PC端的信息(这题用不到可以不用初始化,下一道题涉及到),所以设置为浮空输入模式。

    扩展:

    如果在PA10设置为上拉输入模式也是可以的,但是设置为下拉输入模式则不可以,可以思考一下为什么?

    之前我们已经学到当通信导线上保持为高电平时,表示串口通信处于空闲状态。

    单片机与 PC 之间的串口通信 为例:

    1. 单片机的Tx引脚持续输出高电平,即表示通信还未开始,单片机当前没有向 PC 发送数据。
    2. 当单片机的 Rx 引脚持续检测到输入高电平 时,表示通信尚未开始,PC 当前没有向单片机发送数据。

    这就是为什么上拉可以而下拉却不行的原因,持续的高电平,让其保持为空闲状态。

  7. 数据的发送与接收,我们操作数据寄存器,并且操作前要获得寄存器的状态

    c 复制代码
    // 获取寄存器状态的外设库函数
    FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
    // 返回类型为枚举类型
    typedef enum {RESET = 0, SET = !RESET} FlagStatus, ITStatus;

代码实现

初始化GPIOA 和 USART1
c 复制代码
void init_GPIO(){
		// 开时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
		GPIO_InitTypeDef GPIO_InitStruct;
		// 初始化PA9
		GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
		GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
		GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
		GPIO_Init(GPIOA, &GPIO_InitStruct);
		// 初始化PA10
		GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
		GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
		GPIO_Init(GPIOA, &GPIO_InitStruct);
		
}
void init_USART(){
		// 开时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
		USART_InitTypeDef USART_InitStruct;
		// 设置波特率
		USART_InitStruct.USART_BaudRate = 115200;
		// 设置硬件流控制
		USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
		// 设置USART的模式
		USART_InitStruct.USART_Mode = USART_Mode_Tx;
		// 设置校验位
		USART_InitStruct.USART_Parity = USART_Parity_No;
		// 停止位
		USART_InitStruct.USART_StopBits = USART_StopBits_1;
		// 设置数据位长度
		USART_InitStruct.USART_WordLength = USART_WordLength_8b;
		// 初始化USART1
		USART_Init(USART1, &USART_InitStruct);
    	// 使得USART1可用
		USART_Cmd(USART1, ENABLE);
}
结构体成员 常见取值(宏定义) 备注说明
USART_BaudRate 9600、115200 串口波特率,表示通信速率(单位:bps)
USART_WordLength USART_WordLength_8b 数据位是8位(推荐无校验时使用)
USART_WordLength_9b 数据位是9位(推荐有校验时使用)
USART_StopBits USART_StopBits_1 1 位停止位(推荐)
USART_StopBits_0_5 0.5 位停止位(不推荐)
USART_StopBits_2 2 位停止位(不推荐)
USART_StopBits_1_5 1.5 位停止位(不推荐)
USART_Parity USART_Parity_No 无校验位(推荐)
USART_Parity_Even 偶校验
USART_Parity_Odd 奇校验
USART_Mode USART_Mode_Rx 只接收模式
USART_Mode_Tx 只发送模式
`USART_Mode_Tx USART_Mode_Rx`
USART_HardwareFlowControl USART_HardwareFlowControl_None 不使用硬件流控(推荐)
USART_HardwareFlowControl_RTS RTS(请求发送)流控
USART_HardwareFlowControl_CTS CTS(允许发送)流控
USART_HardwareFlowControl_RTS_CTS RTS + CTS 双向流控
发送功能实现

实际上我们只需要实现发送一字节的功能,后续功能调用该函数即可

  • 单片机向PC发送1个字节数据

    c 复制代码
    void Send_Byte(uint8_t data){
        	// 等待发送缓冲区空
    		while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    		// 发送一个字节
        	USART_SendData(USART1, data);
    }

    效果图:

  • 单片机向PC发送多个字节数据

    c 复制代码
    void Send_Bytes(uint8_t *data, uint8_t len){
        // 遍历数组
        for(int i = 0; i < len; i++){
            // 逐字节发送
            Send_Byte(data[i]);
        }
    }
  • 单片机向PC发送字符串

    c 复制代码
    void Send_Str(char *ch){
        while(*ch) Send_Byte(*ch++);
    }
  • 单片机向PC发送无符号整数

    c 复制代码
    #include<stdio.h>
    void Send_Uint(uint16_t num){
        char str[20];
        sprintf(str, "%u", num);
        Send_Str(str);
    }

效果图:

可以看到已经全部实现了

实验二:完成基于串口通信的单片机接收PC端发送的数据:

1.PC端发送0点亮LED, 发送1熄灭LED

2.PC端发送一行字符串, 以'\n'作为结束, 展示在OLED显示屏上

分析问题(上面分析过的不再分析):

  1. 当接收数据寄存器非空时,程序员控制STM32将接收数据寄存器中的数据读出来,接收数据寄存器被清空。

    c 复制代码
    // 等待接收数据寄存器非空
    while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
  2. 与发送数据不同,接收到数据之后我们要有返回值,返回给单片机再打印到OLED上。

  3. 把LED接在PA0 PA2 PA4 PA6口,以下是LED灯的控制方法

    c 复制代码
    #include "stm32f10x.h"
    #include "../tools/Delay.h"
    
    void LED_AllInit(void){
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
        GPIO_InitTypeDef GPIO_InitStruct;
    	
    	//初始化PA0 PA2 PA4 PA6
    	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP ; 
        GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0| GPIO_Pin_2 | GPIO_Pin_4 | GPIO_Pin_6;
    	GPIO_Init(GPIOA  , &GPIO_InitStruct);
    	GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_2 | GPIO_Pin_4 | GPIO_Pin_6 );
    }	
       
    void LED_AllOn(void){
    			
    	GPIO_SetBits(GPIOA, GPIO_Pin_0); 
    	GPIO_SetBits(GPIOA, GPIO_Pin_2);
    	GPIO_SetBits(GPIOA, GPIO_Pin_4);
    	GPIO_SetBits(GPIOA, GPIO_Pin_6);
    }
    void LED_AllOff(void){
    	GPIO_ResetBits(GPIOA, GPIO_Pin_0); 
    	GPIO_ResetBits(GPIOA, GPIO_Pin_2);
    	GPIO_ResetBits(GPIOA, GPIO_Pin_4);
    	GPIO_ResetBits(GPIOA, GPIO_Pin_6);
    }

代码实现

首先要先实现接收一个字符串

c 复制代码
char Receive_Byte(){
    // 等待接收数据寄存器非空
	while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
	// 返回寄存器中的内容
    return USART_ReceiveData(USART1);
}

我们可以将读取到的内容再发送给PC,来验证。可以看到下面运行的示意图没有任何问题。

实现点亮灯泡就比较简单了,就只需要在主函数中加两条判断语句就可以了,但是需要注意的是,单片机接收的数据默认是字符形式的,所以在判断时要用字符类型。

那么如何返回一个字符串呢?我们需要进一步考虑这个问题

返回字符串思路分析
  1. 我们刚才验证Receive_Byte的时候发现,我们输入abc时,也输出了abc,那这是不是说明,它本身就可以输出字符串呢。

    答案是否定的,先从我们的函数定义上来说,我们返回值为char而非char*,所以不可能返回字符串。出现这样的结果是因为,输入一个字符到数据寄存器,单片机就马上取了出来,过程比较快,所以看起来像字符串。

    另外可以从参考手册可以看到,数据寄存器只有一个字节的数据用来收发,所以不能直接返回字符串

  2. 所以需要我们自己构建一个缓冲区,将读入的字符逐个读入到缓冲区中,之后再输出。定义之后合并字符构建字符串我们需要做两次判断,一个是判断越界,另外一个则是判断结束。最后输出到OLED

    c 复制代码
    #define BUFFER_SIZE
    // 字符缓冲区
    char Buffer[BUFFER_SIZE];
    
    typedef enum{
    		FINISH = 0,
    		UNFINISH
    }BuildStrStatus;
    // 构建字符串
    BuildStrStatus Build_Str(char *Buffer) {
        
        char ReceiveChar = Receive_Byte();
    
        static uint8_t BufferIdx = 0;   
        // 判断越界
        if (BufferIdx == BUFFER_SIZE - 1) {
            Buffer[BufferIdx] = 0;
            BufferIdx = 0;
            return FINISH;
        }
        // 判断结束符
        if (ReceiveChar == 'E') {
            Buffer[BufferIdx] = '\0';
            BufferIdx = 0;
            return FINISH;
        }
        Buffer[BufferIdx++] = ReceiveChar;
        return UNFINISH;
    }
    // 合并字符串
    void Merge_Byte(){
    	while(Build_Str(Buffer) == UNFINISH);
    
    }
    //输出到OLED
    void OLED_SHOW(){
    	static uint8_t line= 2;
    	if(line > 4) {
    		line = 2;
    		OLED_Clear();	
    		OLED_ShowString(1,1,"waiting string");
    	}
    		OLED_ShowString(line++,1,Buffer);
    }
完整的代码展示
c 复制代码
#include "stm32f10x.h"
#include "../tools/Delay.h"
#include "../tools/OLED.h"
#include "../tools/LED.h"
#include<stdio.h>
#define BUFFER_SIZE 100
void init_Tx_and_Rx(){
		// 初始化GPIO
		// 开时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
		GPIO_InitTypeDef GPIO_InitStruct;
		// 初始化PA9
		GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
		GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
		GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
		GPIO_Init(GPIOA, &GPIO_InitStruct);
		// 初始化PA10
		GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
		GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
		GPIO_Init(GPIOA, &GPIO_InitStruct);
		// 输出高电平表示空闲状态
		GPIO_SetBits(GPIOA, GPIO_Pin_9);
		
}
void init_USART(){
		// 开时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
		USART_InitTypeDef USART_InitStruct;
		// 设置波特率
		USART_InitStruct.USART_BaudRate = 115200;
		// 设置硬件流控制
		USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
		// 设置USART的模式
		USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
		// 设置校验位
		USART_InitStruct.USART_Parity = USART_Parity_No;
		// 停止位
		USART_InitStruct.USART_StopBits = USART_StopBits_1;
		// 设置数据位长度
		USART_InitStruct.USART_WordLength = USART_WordLength_8b;
		// 初始化USART1
		USART_Init(USART1, &USART_InitStruct);
		// 使得USART1可用
		USART_Cmd(USART1, ENABLE);
}

void Send_Byte(uint8_t data){
		while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
		USART_SendData(USART1, data);
}
void Send_Bytes(char *data, uint8_t len){
    // 遍历数组
    for(int i = 0; i < len; i++){
        // 逐字节发送
        Send_Byte(data[i]);
    }
}

void Send_Str(char *ch){
    while(*ch) Send_Byte(*ch++);
}

void Send_Uint(uint16_t num){
    char str[20];
    sprintf(str, "%u", num);
    Send_Str(str);
}

char Receive_Byte(){
		while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
		return USART_ReceiveData(USART1);
}
// 字符缓冲区
char Buffer[BUFFER_SIZE];

typedef enum{
		FINISH = 0,
		UNFINISH
}BuildStrStatus;

BuildStrStatus Build_Str(char *Buffer) {
    
    char ReceiveChar = Receive_Byte();

    static uint8_t BufferIdx = 0;   
    if (BufferIdx == BUFFER_SIZE - 1) {
        Buffer[BufferIdx] = 0;
        BufferIdx = 0;
        return FINISH;
    }
    if (ReceiveChar == 'E') {
        Buffer[BufferIdx] = '\0';
        BufferIdx = 0;
        return FINISH;
    }
    Buffer[BufferIdx++] = ReceiveChar;
    return UNFINISH;
}

void Merge_Byte(){
		while(Build_Str(Buffer) == UNFINISH);

}
void OLED_SHOW(){
	static uint8_t line= 2;
	if(line > 4) {
		line = 2;
		OLED_Clear();	
		OLED_ShowString(1,1,"waiting string");
	}
		OLED_ShowString(line++,1,Buffer);
}

int main() {
		init_Tx_and_Rx();
		init_USART();
		OLED_Init();
		LED_AllInit();
		OLED_Clear();
		OLED_ShowString(1,1,"waiting string");
		char temp;
		while(1){
			
			temp = Receive_Byte();
			if(temp == '0')LED_AllOn();
			else if(temp == '1') LED_AllOff();
			else{
					Merge_Byte();
					OLED_SHOW();
			}
		}
}
优化

如果这样去运行的话,会发现一个问题,LED的控制只能控制一下,因为一旦进入 else 分支,调用 Merge_Byte(),程序就陷入了长时间的阻塞等待(等待 'E' 结束符)。在这个等待期间,程序无法响应任何其他的操作(比如无法响应新的 '0' 或 '1' 命令)。所以这个程序还是无法运行的,但是如果把模块分开的话都是可以独自运行的。我们已经遇到两个阻塞函数了,但是我们始终不能很好的解决,那么从下一文章中我们开始学习如何应对阻塞------中断。

或者我们可以通过这种方式避免阻塞现象,但是在传输的字符串中不能包含0或1

c 复制代码
int main() {
	init_Tx_and_Rx();
	init_USART();
	OLED_Init();
	LED_AllInit();
	OLED_Clear();
	OLED_ShowString(1,1,"waiting string");
  	char temp;
    while(1){
        temp = Receive_Byte(); // 获取字符
        
        if(temp == '0') {
            LED_AllOn();
        }
        else if(temp == '1') {
            LED_AllOff();
        }
        else {

            static uint8_t BufferIdx = 0;

            if (temp != 'E') {
                 if(BufferIdx < BUFFER_SIZE - 1) {
                     Buffer[BufferIdx++] = temp;

				 }
            } else {
                 // 收到了结束符 'E'
                 Buffer[BufferIdx] = '\0'; // 添加字符串结束符
                 BufferIdx = 0; // 重置索引 
                 OLED_SHOW(); 
            }
        }
    }

}
相关推荐
网络工程小王1 小时前
【大模型vLLM 使用】学习笔记
笔记·学习·llama
星夜夏空992 小时前
STM32单片机学习(14) —— STM32的串口外设
stm32·单片机·学习
栉甜2 小时前
APIs学习
前端·javascript·css·学习·html
吃好睡好便好2 小时前
说说梳头的保健作用
学习
都在酒里2 小时前
STM32标准库驱动L298N双H桥电机驱动模块(调速/正反转/多模式实战,附完整工程代码)
stm32·单片机·嵌入式硬件
wuxinyan1232 小时前
工业级大模型学习之路013:RAG零基础入门教程(第九篇):RAG幻觉治理
人工智能·学习·rag
Hello_Embed2 小时前
USB 学习指南+软硬件框架
网络·笔记·stm32·嵌入式·ai编程
99乘法口诀万物皆可变2 小时前
Simscape 学习路径图:从入门到精通的多物理域仿真指南
学习
wuxinyan1233 小时前
工业级大模型学习之路015:RAG零基础入门教程(第十一篇):系统重构与代码规范化
人工智能·python·学习·重构·rag