9-USART串口

2023-12-24 18:02:41

1:串口通信协议

为了控制或读取外挂模块,stm32需要与外挂模块进行通信,来扩展硬件系统。通信双方需要遵守通信协议,也就是双方需要按照协议规则进行数据收发,不同的外挂模块会有不同的通信协议。

下面介绍一下引脚的全称:USART:TX(Transmit Exchange)数据发送脚、RX(Receive Exchange)数据接收脚。

  • IIC:SCL(Serial Clock)时钟线、SDA(Serial Data)数据线。
  • SPI:MOSI(Master Output Slave Input)主机输出数据脚、MISO(Master Input Slave Output)主机输入数据脚、CS(Chip Select)片选
  • USB:DP(Data Postive)差分线正、DM(Data Minus)差分线负

注:上述协议中,单端电平都需要共地。
注:使用差分信号可以抑制共模噪声,可以极大的提高信号的抗干扰特性,所以一般差分信号的传输速度和传输距离都非常高

?

下面介绍一些串口引脚的注意事项:

  • TX与RX:简单双向串口通信有两根通信线(发送端TX和接收端RX),要交叉连接。不过,若仅单向的数据传输,可以只接一根通信线。
  • GND:一定要共地。由于TX和RX的高低电平都是相对于GND来说的,所以GND严格来说也算是通信线。
  • VCC:相同的电平才能通信,如果两设备都有单独的供电,VCC就可以不接在一起。但如果某个模块没有供电,就需要连接VCC,注意供电电压要按照模块要求来,必要时需要添加电压转换电路。

串口协议的软件部分:

?

?

下面介绍串口的参数:

  • 波特率:串口通信的速率(bit/s),也就是通信双方所约定的通信速率(异步通信)。
  • 空闲状态:固定为高电平。
  • 起始位:固定为低电平,标志一个数据帧的开始。
  • 数据位:低位先行,数据帧的有效载荷,1为高电平,0为低电平。
  • 校验位(选填):用于数据验证,根据数据位计算得来。
  • 停止位:固定为高电平,用于表示数据帧的间隔,同时也可以使得通信线回归到空闲状态。可以配置停止位是1位/2位。

?

2:stm32上的片上外设-USART

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器 是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据 自动生成数据帧时序,从TX引脚发送出去,也可 自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。USART中的“S”表示同步,只支持时钟输出,不支持时钟输入,是为了兼容别的协议或特殊用途而设计的,并不支持两个USART之间进行同步通信,所以这个功能几乎不会用到,一般更常使用的是UART同步异步收发器。下面是一些参数:
?

  • 自带波特率发生器,最高达4.5Mbits/s,常用9600/115200。
  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)。
  • 可选校验位:无校验(常用)/奇校验/偶校验。
  • 支持同步模式(一般不用)、硬件流控制(指示从设备准备好接收的信号,一般不用)、DMA、智能卡、IrDA(手机红外通信,但并不是红外遥控,目前很少见)、LIN(局域网的通信协议)。
  • STM32F103C8T6 USART资源:USART1(APB2)、USART2(APB1)、USART3(APB1)。
    ?

?

上图给出USART最主要、最基本的结构:

  • 波特率发生器:用于产生约定的通信速率。时钟来源是PCLK2/PCLK1,经过波特率发生器分频后,产生的时钟通向发送控制器、接收控制器。
  • 发送控制器、接收控制器:用于控制发送移位、接收移位。
  • GPIO:发送端配置成复用推挽输出、接收端配置成上拉输入。
  • 标志位:TXE置位时写入数据、RXNE置位时接收数据。
  • 开关控制:用于开启整个USART外设。

?

小细节:计算分频系数DIV

?

?细节2:

USB转串口模块

?

主要关注的是该模块的供电情况。

  • USB插座:直接插在电脑USB端口上,注意整个模块的供电来自于USB的VCC+5V。
  • CON6插针座:
  • 引脚2、引脚3:用于连接到stm32上进行串口通信。
  • 引脚5【CH340_VCC】:通过跳线帽可以选择 接入+3.3V(stm32) 或者+5V。CH340芯片的供电引脚,同时决定了TTL,所以也就是串口通信的TTL电平。神奇的是,即使不接跳线帽CH340也可以正常工作,TTL为3.3V,但是显然接上电路以后更加稳定。
  • 通信和供电的选择:CON6插针座选择引脚4/6进行通信后,剩下的引脚可以用于给从设备供电,但是剩下的这个脚显然与TTL电平不匹配。此时需要注意 优先保证供电电平的正确,通信TTL电平不一致问题不大。当然,若从设备自己有电源,那么就不存在这个问题了。
  • TXD指示灯、RXD指示灯:若相应总线上有数据传输,那么指示灯就会闪烁。

实验1:串口发送

?

在软件代码中定义要发送的信息,然后通过串口发送到电脑端,使用“串口助手”小工具查看。要求依次发送单字节数据、数组、字符串、数据的每一位。
注:串口助手可以切换“文本模式”/“HEX模式”。
注:数字和字符的对应关系可以参考ASCII码表。

main.c?

#include "stm32f10x.h"                  // Device header
#include "SerialPort.h"
    
int main(void){
    uint8_t send_byte = 0x42;
    uint8_t send_array[6] = {0x30,0x31,0x32,0x33,0x34,0x35};
    //串口初始化
    SerialPort_Init();
    //发送单个字节
    SerialPort_SendByte('A');//可以直接发送字符
    SerialPort_SendByte(send_byte);
    SerialPort_SendByte('\r');
    SerialPort_SendByte('\n');
    //发送数组
    SerialPort_SendArray(send_array,6);
    SerialPort_SendByte('\r');
    SerialPort_SendByte('\n');
    //发送字符串
    SerialPort_SendString("Hello World!\r\n");
    //发送数字的每一位
    SerialPort_SendNum(65535, 5);
    SerialPort_SendString("\r\n");
    while(1){
//        //循环发送数字
//        SerialPort_SendByte(send_byte);
//        OLED_ShowHexNum(1,9,send_byte,2);
//        send_byte++;
//        Delay_ms(1000);
    };
}

??SerialPort.h

#ifndef __SERIALPORT_H
#define __SERIALPORT_H

void SerialPort_Init(void);
void SerialPort_SendByte(uint8_t send_byte);
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array);
void SerialPort_SendString(char *send_string);
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len);

#endif

SerialPort.c

#include "stm32f10x.h"                  // Device header

//串口初始化-USART1
void SerialPort_Init(void){
    //1.开启RCC外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    //2.初始化GPIO-PA9
    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);
    //3.初始化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;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);
    //4.配置中断,开启NVIC(接收数据使用)
    //5.开启外设
    USART_Cmd(USART1, ENABLE);
}

//串口发送1字节数据
void SerialPort_SendByte(uint8_t send_byte){
    //向发送数据寄存器TDR中写入数据
    USART_SendData(USART1, send_byte);
    //确认数据被转移到发送移位寄存器(等待标志位TXE置位)
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
}

//发送一个数组
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array){
    uint8_t i=0;
    for(i=0;i<size_array;i++){
        SerialPort_SendByte(send_array[i]);
    }
}

//发送一个字符串
void SerialPort_SendString(char *send_string){
    uint8_t i=0;
    for(i=0; send_string[i]!='\0'; i++){
        SerialPort_SendByte(send_string[i]);
    }
}

//非外部调用函数-幂次函数
uint32_t SerialPort_Pow(uint32_t X, uint32_t Y){
    uint32_t result = 1;
    while(Y--){
        result *= X;
    }
    return result;
}

//发送数字的每一位-先发高位
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len){
    uint16_t i;
    for(i=0;i<send_len;i++){
        SerialPort_SendByte((send_num/SerialPort_Pow(10,send_len-i-1))%10+'0');
    }
}

实验2:移植printf函数

将C语言自带函数printf进行封装,默认成将需要打印的数据发送到串口,进而可以显示在电脑端串口助手上。

法一:

  1. 首先点击“魔术棒”,在Target界面的“Code Generation”方框中勾选“USE MicroLIB”。MicroLIB是Keil为嵌入式平台优化的一个精简库,要使用printf函数就会用到这个MicroLIB精简库。
  2. 对printf函数重定向,将printf函数打印的东西输出到串口。于是在 SerialPort.c 模块中添加下列代码:
    #include <stdio.h>
    
    //对printf函数重定向-将fputc函数原型重定向到串口
    //注:ptintf函数本质上就是循环调用fputc,将字符一个一个输出
    int fputc(int ch, FILE *f){
        SerialPort_SendByte(ch);
        return ch;
    }
    

    在?SerialPort.h?模块中添加下列代码

  3. #include <stdio.h>
    

    于是就可以在?main.c?中调用printf函数,将数据输出到串口了。

    //使用重定向的printf函数
        printf("%d\r\n",666);
    

法2:

若多个串口都想使用printf函数,那么就可以使用sprintf函数。sprintf函数可以将格式化字符输出到一个字符串里,然后再调用相应的“串口发送字符串”函数发送这个字符串,整个过程不涉及重定向,于是就实现了所有USART外设都可以打印信息到串口了。所以下面可以直接在 main.c 中定义:

char String[100];//定义一个足够长的字符串数组
sprintf(String, "Num=%d\r\n", 666);//将格式化字符串存储在String中
SerialPort_SendString(String);//串口发送字符串

实验3:串口发送与接收

程序整体思路:

  1. 查询。主函数不断查询RXNE标志位,但是会占用很多的CPU资源,所以不推荐。
  2. 中断。推荐方法,下面的演示也是基于此方法。

main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "SerialPort.h"

int main(void){    
    uint8_t Rx_byte = 0;//串口接收的单比特数据
    
    //设置中断分组
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    //OLED初始化
    OLED_Init();
    OLED_ShowString(1,1,"Rx_byte:");
    
    //串口初始化
    SerialPort_Init();

    while(1){
        if(SerialPort_GetRxFlag()==1){
            Rx_byte = SerialPort_GetRxData();
            OLED_ShowHexNum(1,9,Rx_byte,2);
            SerialPort_SendByte(Rx_byte);
        }
    };
}

?SerialPort.c

uint8_t SerialPort_RxData = 0;
uint8_t SerialPort_RxFlag = 0;

//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){
    if(SerialPort_RxFlag==1){
        SerialPort_RxFlag = 0;
        return 1;
    }else{
        return 0;
    }
}

//获取接收的数据
uint8_t SerialPort_GetRxData(void){
    return SerialPort_RxData;
}

//USART1_RXNE中断函数
void USART1_IRQHandler(void){
    if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
        SerialPort_RxFlag = 1;
        SerialPort_RxData = USART_ReceiveData(USART1);
        //读操作可以自动清零标志位,但加上也没事
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}
//不要忘了将前两个函数在头文件中声明

重点:USART串口数据包

数据包的作用是将一个个单独的数据打包,方便进行多字节的数据通信。因为实际应用中,经常需要进行数据打包。比如陀螺仪传感器需要将数据发送到stm32,其中包括X轴、Y轴、Z轴三个字节,循环不断的发送;若采用一个一个进行发送的方式,接收方就有可能分不清对应的顺序,进而出现数据错位现象。此时,若能将同一批数据进行分割和打包,就可以方便接收方识别。
1:数据包格式定义:

HEX数据包

若载荷数据与包头、包尾一样怎么办呢?有三种解决思路:?

  1. 限制载荷数据的范围。使其不会与包头、包尾重复。
  2. 尽量使用固定长度的数据包。只要数据长度固定,那么就可以通过包头、包尾定位数据。
  3. 增加包头包尾的数量,使其尽量呈现出载荷数据不会出现的状态。

注:包尾可以去除。但是这样会使得载荷数据和包头重复的问题更加严重。

若想发送16位整型数据、32位整型数据、float、double、结构体等,只需使用?uint8_t型指针?指向这些数据,就可以进行发送(将各种数据转换成字节流)。?文本数据包

?文本数据包中,每个数据都经过了一层编码和译码。
由于包头包尾非常容易唯一确定,文本数据包基本不用担心载荷数据和包头包尾重复的问题。

HEX数据包:
优点:传输最直接,解析数据非常简单,比较适合一些模块发送最原始的数据。如使用串口通信的陀螺仪、温湿度传感器。
缺点:灵活性不足,载荷容易和包头包尾重复。
文本数据包:
优点:数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合。如蓝牙模块常用的AT指令、CNC和3D打印机常用的G代码,都是文本数据包的格式。
缺点:解析效率低。

2:数据包的收发流程

?数据包发送是非常简单的,直接发就完事儿了。但是接收数据包的过程比较复杂,这是就要考虑使用状态机

图9-19 接收HEX数据包-状态机

实验4:串口收发HEX数据包

?自定义数据包格式,使用串口完成指定格式的数据包收发,并将收发结果显示在OLED上。另外按键的功能是将发送的当前存储的发送数据全部加1再发送出去。

  • 数据包头:0xFF。
  • 载荷数据:固定数据段长度为4个字节。
  • 数据包尾:0xFE。

?

?main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "SerialPort.h"
#include "Key.h"

int main(void){
    //存储串口接收的HEX数据包
    uint8_t *Rx_Packet = SerialPort_GetRxPacket();
    //存储串口发送的HEX数据包
    uint8_t Tx_Packet[4] = {0x01,0x02,0x03,0x04};
    
    //设置中断分组
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    //OLED初始化
    OLED_Init();
    OLED_ShowString(1,1,"Rx_Packet:");
    OLED_ShowString(3,1,"Tx_Packet:");
    //串口初始化
    SerialPort_Init();
    //按键初始化
    Key_Init();
        
    while(1){
        //显示接收到的数据
        if(SerialPort_GetRxFlag()==1){
            OLED_ShowHexNum(2,1,*Rx_Packet,2);
            OLED_ShowHexNum(2,4,*(Rx_Packet+1),2);
            OLED_ShowHexNum(2,7,*(Rx_Packet+2),2);
            OLED_ShowHexNum(2,10,*(Rx_Packet+3),2);
        }
        
        //检测按键,发送数据包到电脑
        if(Key_GetNum()==1){
            Tx_Packet[0]++;
            Tx_Packet[1]++;
            Tx_Packet[2]++;
            Tx_Packet[3]++;
            SerialPort_SendPacket(Tx_Packet);
            OLED_ShowHexNum(4,1,Tx_Packet[0],2);
            OLED_ShowHexNum(4,4,Tx_Packet[1],2);
            OLED_ShowHexNum(4,7,Tx_Packet[2],2);
            OLED_ShowHexNum(4,10,Tx_Packet[3],2);
        }
    };
}

?SerialPort.c新增函数

uint8_t SerialPort_RxPacket[4];
uint8_t SerialPort_RxPacketFlag = 0;

//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){
    if(SerialPort_RxPacketFlag==1){
        SerialPort_RxPacketFlag = 0;
        return 1;
    }else{
        return 0;
    }
}

//获取接收的HEX数据包
uint8_t* SerialPort_GetRxPacket(void){
    return SerialPort_RxPacket;
}

//发送HEX数据包
void SerialPort_SendPacket(uint8_t *send_array){
    SerialPort_SendByte(0xFF);
    SerialPort_SendArray(send_array, 4);
    SerialPort_SendByte(0xFE);
}

//USART1_RXNE中断函数
void USART1_IRQHandler(void){
    uint8_t rec_byte;
    static uint8_t rx_state;
    static uint8_t rx_index;
    if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
        rec_byte = USART_ReceiveData(USART1);
        
        //利用状态机,接收HEX数据包
        if(rx_state==0){
            if(rec_byte==0xFF){
                rx_index = 0;
                rx_state = 1;
            }
        }else if(rx_state==1){
            SerialPort_RxPacket[rx_index] = rec_byte;
            rx_index++;
            if(rx_index>=4){
                rx_state = 2;
            }
        }else if(rx_state==2){
            if(rec_byte==0xFE){
                SerialPort_RxPacketFlag = 1;
                rx_state = 0;
            }
        }
        
        //读操作可以自动清零标志位,但加上也没事
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}
//除了中断函数,其余函数还要在头文件SerialPort.h中声明

数据混叠。若电脑端连续发送数据包,而stm32处理不及时,会导致数据错位。但是一般像传感器模块等的数据都具有连续性,所以就算数据错位也没关系。
发送数据不匹配。注意发送字节数据一定要写成0x11的形式,而不是直接写一个11进行发送。
收发数据没反应。注意一定要在最开始声明的地方赋初值,否则有可能读不出数据。当然,还有一种可能是串口连接不稳定,可以重新拔插一下串口。
?

实验5:串口收发文本数据

?使用式的文本数据包,来控制单片机点亮或熄灭LED灯,单片机完成指令后再将接收的状态回传到电脑。电脑端发送指定格

  • 数据包头:@。
  • 数据包:有效指令为"@LED_ON\r\n"、“@LED_OFF\r\n”。(不定字长)
  • 数据包尾:\r\n。

?

main.c?】

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "SerialPort.h"
#include "LED.h"
#include <string.h>

int main(void){
    //存储串口接收的HEX数据包
    char *Rx_Packet = SerialPort_GetRxPacket();
    
    //设置中断分组
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    //OLED初始化
    OLED_Init();
    OLED_ShowString(1,1,"Rx_Packet:");
    OLED_ShowString(3,1,"Tx_Packet:");
    //串口初始化
    SerialPort_Init();
    //LED初始化
    LED_Init();
        
    while(1){
        //对接收到的文本进行判断
        if(SerialPort_GetRxFlag()==1){
            //OLED显示接收到的文本
            OLED_ShowString(2,1,"                ");
            OLED_ShowString(2,1,Rx_Packet);
            //根据接收的内容执行相应的动作
            if(strcmp(Rx_Packet, "LED_ON")==0){
                LED1_ON();
                OLED_ShowString(4,1,"                ");
                OLED_ShowString(4,1,"LED_ON_OK");
                SerialPort_SendString("LED_ON_OK\r\n");
            }else if(strcmp(Rx_Packet, "LED_OFF")==0){
                LED1_OFF();
                OLED_ShowString(4,1,"                ");
                OLED_ShowString(4,1,"LED_OFF_OK");
                SerialPort_SendString("LED_OFF_OK\r\n");
            }else{
                OLED_ShowString(4,1,"                ");
                OLED_ShowString(4,1,"ERROR_COMMAND");
                SerialPort_SendString("ERROR_COMMAND\r\n");
            }
        }
    };
}

SerialPort.c新增函数(将上一节HEX数据包部分全部删除)

char SerialPort_RxPacket[100];
uint8_t SerialPort_RxPacketFlag = 0;

//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){
    if(SerialPort_RxPacketFlag==1){
        SerialPort_RxPacketFlag = 0;
        return 1;
    }else{
        return 0;
    }
}

//获取接收的HEX数据包
char* SerialPort_GetRxPacket(void){
    return SerialPort_RxPacket;
}

//USART1_RXNE中断函数
void USART1_IRQHandler(void){
    uint8_t rec_byte;
    static uint8_t rx_state;
    static uint8_t rx_index;
    if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
        rec_byte = USART_ReceiveData(USART1);
        
        //利用状态机,接收HEX数据包
        if(rx_state==0){
            if(rec_byte== '@'){
                rx_index = 0;
                rx_state = 1;
            }
        }else if(rx_state==1){
            if(rec_byte != '\r'){
                SerialPort_RxPacket[rx_index] = rec_byte;
                rx_index++;
            }else{
                rx_state = 2;
            }
        }else if(rx_state==2){
            if(rec_byte == '\n'){
                SerialPort_RxPacket[rx_index] = '\0';//字符串结束标志符
                SerialPort_RxPacketFlag = 1;
                rx_state = 0;
            }
        }
        
        //读操作可以自动清零标志位,但加上也没事
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}
//除了中断函数,其他函数还要在头文件SerialPort.h中声明

LED.c新增函数

/**
  * @brief  LED1亮
  */
void LED1_ON(void){
    GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}

/**
  * @brief  LED1灭
  */
void LED1_OFF(void){
    GPIO_SetBits(GPIOA, GPIO_Pin_1);
}
//注意还要在头文件LED.h中声明

文章来源:https://blog.csdn.net/hvbkbb/article/details/135182821
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。