基于STM32F103C8T6学习USART串口通信(CH340)

基于STM32学习USART串口通信及应用

前言:

学习笔记,若有侵犯请告知删改
参 考:[9-1] USART串口协议_哔哩哔哩_bilibili
串口通信原理与实践-优快云博客
江科大学习笔记——USART串口通信_江科大串口通信程序-优快云博客
STM32江科大————USART串口通信_32江科大usart串口-优快云博客

通信接口

【通信目的】:一对或者多设备之间进行数据传输,拓展功能
【通信协议】:制定通信的规则,通信双方按照协议规则进行数据收发
 

常见的数据通信协议

USART:TX(数据发送脚)、RX(数据接收脚)

I2C:SCL(时钟)、SDA(数据)

SPI:SCLK(时钟)、MOSI(主机输出数据脚)数据发送、MISO(主机输入数据脚)数据接收、CS(片选,用于指定通信的对象)

CAN:CAN_H、CAN_L(差分数据脚,用2个引脚表示差分数据)

USB:DP(也叫D+)、DM(D-)也是一对差分数据脚。

【双工模式】

全双工:通信双方能够同时双向通信。通常两根数据线,发送线路和接收线路互不影响(打电话)

半双工:只有一根数据线,I2C、CAN、USB。CAN和USB两根差分线组合成一根线(对讲机)

单工:数据只能从一个设备到另一个设备,不能反着来(广播)。

【时钟特性】(对的时间做对的事)

同步:同一时钟节拍下工作。I2C、SPI有单独的时钟线,所以同步,接收方可以在时钟的指引下进行采样。

异步:在相同的频率下工作,只看起始和结束。USART、CAN、USB没有时钟线,需要双方约定一个采样频率,并还需加帧头帧尾等,进行采样位置的对齐

【电平特性】

单端:USART、I2C、SPI,引脚的高低电平都是对GND的电压差,所以单端信号通信双方必须要共地,就是把GND接在一起。所以这3个引脚前必须加GND引脚。

差分:没有参考电平线,靠2个差分引脚的电压差来传输信号,是差分信号,在通信的时候,不需要GND。不过USB协议也有一些地方需要单端信号,所以USB还是需要供地的,性能优,距离远,抗干扰强。

【设备通信】

点对点:只有两个设备

多设备:需要有寻址的过程,确定对象是谁

串口通信USART

      USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器  是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备点对点的互相通信,并且串口通信每次同时只能传输1个二进制位(与并行的区别)。

        STM32内部集成了USART硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。USART自带波特率发生器,最高达4.5Mbits/s 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2) 可选校验位(无校验/奇校验/偶校验) 支持同步模式(CLK输出)、硬件流控制(防止发送太快导致接收数据丢弃或覆盖)、DMA、智能卡、IrDA、LIN

单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力。常见硬件有蓝牙模块、语言模块、陀螺仪模块。

STM32F103C8T6 中的USART资源包括: USART1、 USART2、 USART3(USART1在APB2上72M,USART2和3在APB1上36M
STM32江科大————USART串口通信_32江科大usart串口-优快云博客

硬件电路

串口通信lian'xian

  1. 简单双向串口通信有两根通信线(发送端TX和接收端RX) TX与RX要交叉连接
  2. 当只需单向的数据传输时,可以只接一根通信线(只用收或者发)
  3. 电平标准不一致时,需要加电平转换芯片(前面说了,单端、共地参考)

【电平标准】

        电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

TTL电平+3.3V(STM32)或+5V(51)表示1,0V表示0
RS232电平:-3~-15V表示1,+3~+15V表示0
RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)

串口参数以及时序

十位和十一位(多个校验位)数据帧

十位和十一位(多个校验位)数据帧

【波特率】

         波特率,是串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位。波特率就是9600说明每秒钟可以传输9600个二进制位数据(传输一个二进制位需要的时间是1/9600秒,也就是104us)。串口通信的波特率不能随意设定,一般最常见的波特率是9600或115200(发送和接收波特率必须一致)。

【串口通信的通信单元

            串口通信时,收发是一个周期一个周期进行的,每个周期传输n个二进制位。这一个周期就叫做一个通信单元,一个通信单元由:起始位+数据位+奇偶校验位+停止位组成的。通信单元再通信时双方事需要先约定好

            起始位:表示发送方要开始发送一个通信单元,起始位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。(串口通信固定为低电平0

            数据位:是一个通信单元中发送的有效信息位(低位先行),是本次通信真正要发送的有效数据,串口通信一次发送多少位有效数据是可以设定的(可选的有6、7、8、9,一般都是选择8位数据位,因为一般通过串口发送的文字信息都是ASCII码编码,而ASCII码中一个字符刚好编码为8位

            校验位:是用来校验数据位,以防止数据位出错的。常见有无校验(10位数据帧)、奇或偶标志位(11位数据帧,第八位奇0偶1判断 二进制 1 的个数)

            停止位:是发送方用来表示本通信单元结束标志的,停止位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。常见的有1位停止位、1.5位停止位、2位停止位等(图中有),一般使用的是1位停止位。(串口通信固定为高电平1,  通信空闲状态也为1)。

USART内部框图

  • 发送寄存器和接受寄存器占用同一个地址。
  • 进行写操作时,数据写到TDR。进行读操作,数据从RDR读出来。
  • 发送寄存器:把一个字节的数据一位一位地移出去。
  • 若硬件检测到发送寄存器没有数据移位,TDR地数据就会一位一位移动到发送寄存器。当数据从TDR移动到发送寄存器就会置标志位。TXE为1,说明可以在TDR写入下一个数据。但TXE置1了并不是前面的数据已经从发送寄存器移出去。
  • TX从发送移位寄存器移位,RX从接收移位寄存器移位。
  • 硬件数据流控,简称流控。nRTS(Request To Send)是请求发送,是输出脚,nCTS(Clear To Send)是清除发送,是输入脚,这里的n都表示低电平有效。nRTS用于告诉别人能不能传输信号,nCTS用于接收nRTS信号。
  • 可以触发串口中断实现

数据发送过程:首先写操作过后,数据来到TDR后会一整个字节一次性进入发送移位寄存器,此时会在SR状态寄存器里面置一个标志位TXE(TX Empty发送数据寄存器空),如果该标志位置1了,就可以在TDR立马写入下一个数据,此时数据还在发送移位寄存器里面,数据并未发送出去,然后发送移位寄存器会在绿色发生器控制的控制下,向右移位将数据从TX引脚发送出去,刚好对应串口协议低位先行,比如写操作写入0x55给TDR,此时寄存器里面就是原数据0101 0101,那么原数据向右移位就把最低位1先发送出去了,作为发送数据的最高位(低位先行,高位填充),然后依次发送剩下的010 1010,最终得到发送数据1010 1010,这就是串行发送。
数据接收过程:从RX引脚接收到数据,然后一位一位的读取引脚电平,读取到的第一个电平放在接收移位寄存器最高位,然后接收器控制器会控制接收移位寄存器向右移位,也就是将高位数据移到低位,移位之后再读取第二个电平放在最高位再右移,以此类推移位8次接受一个字节,置位SR状态寄存器RXNE(接收数据寄存器非空)。比如发送0x55,接收时RX引脚接收到的数据是1010 1010,把一个电平0放在最高位,然后移位原数据,再读取第二个电平1再移位,移位8次得到一个字节0101 0101之后,整个数据会一下到RDR里面并在状态寄存器里置标志位RXNE(RX Not Empty)=1,也就是并行接收
注意:发送时候加帧头帧尾和接收时候去帧头帧尾外设自己完成了,我们不用管

【简化结构图】

        USART的PCLKx分频得到发送控制器和接收控制器的波特率,写操作可以判断标志位TXE,由发送控制器控制从TDR到移位寄存器到TX,读操作依据判断标志位RXNE,由接收控制器控制从RX到移位寄存器到RDR,最后Cmd开启外设即可。

【波特率发生器】

        波特率发生器中,对APBx时钟分频得到对应发送和接收的时钟,fPCLKx(x=1或2),USART1挂载在APB2上是PCLK2的72MHZ时钟,USART2和3在APB1上是PCLK1的36MHZ时钟,USARTDIV是分频系数,可以支持到小数点后四位,确保分频更加精准,分频完成之后还要再除以16(内部使用了16倍波特率的采样时钟)才能得到发生器和接收器时钟频率。 
发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
计算公式:波特率 = fPCLK2/1 / (16 * DIV) ,一般定波特率需要求解的数值是DIV

USART数据帧

【字长配置】

字长

        通常是9位数据位带校验或者是8位数据位不带校验的方式,为上升沿采样,上升沿对应每一个数据的中间,时钟速率和数据速率一致。
空闲帧就是从头到尾都是高电平,断开帧就是从头到尾都是低电平

        串口输出定时翻转TX电平即可,输入时不仅要保证采样频率和波特率一致,还要保证每次输入采样的位置正好在没一位的正中间,这样子采样才是最可靠的,另外输入还要判断噪声干捞,因此复杂的多。        

【停止位配置】

停止位

通常停止位一位即可

【起始位侦测】


        当输入电路侦测到一个数据帧的起始位后(也就是一个下降沿)就会以波特率的频率连续采样一帧数据。同时,从起始位开始采样位置(时钟上升沿)就要对齐到位的正中间,一般只要第一位对齐了后面就都齐了。

        输入电路对起始位侦测的采样时钟进行细分,会以波特率的16倍进行采样,使一位数据之间要采样16次(1位的时间里采集16次数据)。

        首先要要检测起始位,在空闲状态时候检测的全部都是1,而当检测到0的时候,产生下降沿,如果这个下降沿不是由于噪声产生的,那么后面的就应该是起始位了,这个时候就对起始位进行16次采样,采取每三位(3、5、7)至少两位是0这个才是起始位,否则舍弃这几次采样,再次寻找下一个下降沿再次判断是否是起始位,如果有噪声则会置标志位NE(Noisy Error),数据能够使用但是提醒你谨慎使用。当确定这是起始位之后,接收状态就会由空闲变成接收起始位,这个图中在8,9,10次采样(至少两位是0)才开始变为接收起始位状态,8,9,10三次采样肯定是在起始位正中间的,那么后续数据的16次采样,我们采样8,9,10这三次采样的地方肯定也位于后续数据正中间。

【数据采样】


在一个数据位有16个采样时钟的数据长度,由于起始位侦测已经对齐了采样时钟,所以直接在中间的第8、9、10次连续采样三次,没有噪声影响便全为0或1,或者是受到了噪声的影响两位是0或1,同时置为NE标志,告诉你存在噪声。

数据格式

HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
文本模式/字符模式:以原始数据编码后的形式显示
 

编码

CH340

CH340内部原理图

硬件接线:
CH340中短接VCC-3.3V
PA9(USART1_TXD)----RXD
PA10(USART1_TXD)--TXD
GND----------------------------GND共地
电脑USB-----------------------CH340USB

连接好后出现出现COM9,若没有要么就是串口烧了识别不出,要么就是没有安装串口驱动(网上有很多教程,江科大视频最前面几个很详细)

蓝牙

编程实现

代码源于江科大STM32!!!库函数位于stm32f10x_usart.h,用什么学什么

串口发送+接收

1. 初始化
        1.1开启USART1时钟APB2,开启GPIOA时钟APB2
             RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);    
             RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
        1.2初始化GPIOA PA9输出TXD--复用推挽  PA10输入RXD--浮空或者上拉  
             GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;(IPU)
              1.3初始化USART        (8位字长无校验、1位停止位、波特率9600)
             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);    
2. 发送一个字节函数
        2.1 调用发送数据函数  //将字节数据写入数据寄存器,写入后USART自动生成时序波形
             USART_SendData(USART1, Byte);    
        2.2 等待发送完成  //利用TXE发送标志位空判断,等其置为1(自动清0)发送完成一个字节
             while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);    
3. 发送一个数组
       用指针传递数组, 设定长度,循环利调用发送一个字节函数发送

4. 发送字符串 (文本模式)
        同理使用指针传递字符串,利用字符串末尾停止位为\0的特性判断字符串发送循环完成


5. 发送数字
        利用数字长度以及对应位对10取模取出每一位数据,加上0数字的ASCII码‘0’或者0x30作为偏移
        5.1创建一个次方函数,用于数字/10的y次方,取出该位

        5.2 循环发送

6. 接收方 
       6.1 方法1:主函数中循环判断标志位RXNE(接收寄存器非空,即当数据从移位寄存器移动到RDR时置为1,对DR读取时硬件自动清零)的状态,然后读DR,使用OLED显示出来。
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)   
   {
       Serial_RxData = USART_ReceiveData(USART1);//读取数据寄存器,存放在接收的数据变量
         }
        6.2 方法2:串口中断
       (1) 首先通过USART_IT_RXNE开启串口接收IT中断
        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);       //开启串口接收数据的中断
        (2)NVIC中断分组
            NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);            //配置NVIC为分组2
       (3)NVIC配置
          NVIC_InitTypeDef NVIC_InitStructure;                    //定义结构体变量
          NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;      //选择配置NVIC的USART1线
          NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;            //指定NVIC线路使能
          NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定抢占优先级为1
          NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;      //指定NVIC线路的响应优先级为1
          NVIC_Init(&NVIC_InitStructure); 
        (3)调用中断函数  (在启动文件md.h中)    

        其中两个变量是为了方便调用和写主函数,建议extern作为全局变量或者是定义函数调用,可不清除RXNE标志位。为验证数据接收与发送的是否一致,可以进行数据回传,再发送RxData回去

Printf函数实现(可发文字)

1.打开Micro LIB精简库

2.重定向fputc

#include <stdio.h>

/**
  * 函    数:使用printf需要重定向的底层函数
  * 参    数:保持原始格式即可,无需变动
  * 返 回 值:保持原始格式即可,无需变动
  */
int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);            //将printf的底层重定向到串口中自己的发送字节函数
    return ch;
}

此时可以直接调用printf("\r\nNum2=%d", 222);   输出,但是只能有一个printf。不使用重定向而是使用sprintf打印到字符数组,再用串口发送字符数组,此方法打印到字符数组,之后想怎么处理都可以,可在多处使用
    char String[100];                    //定义字符数组
    sprintf(String, "\r\nNum3=%d", 333);//使用sprintf,把格式化字符串打印到字符数组
    Serial_SendString(String);            //串口发送字符数组(字符串)

3.封装的prinf函数(抄就完了)

#include <stdarg.h>

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)//C语言可变参数
{
    char String[100];                //定义字符数组


    va_list arg;                    //定义可变参数列表数据类型的变量arg
    va_start(arg, format);            //从format开始,接收参数列表到arg变量
    vsprintf(String, format, arg);    //使用vsprintf打印格式化字符串和参数列表到字符数组中
    va_end(arg);                    //结束变量arg


    Serial_SendString(String);        //串口发送字符数组(字符串)
}

4.print输出格式乱码问题
(1)串口助手和Keil文字编码格式保持一致(中文推荐GB2312--GBK)
(2)若一定相使用UTF-8则Keil中修改参数

(3)批量转码工具
        UltraCodingSwitch.exe进行转码


拓展:串口数据包

作用:把一个个单独的数据给打包起来方便进行多字节的通信

HEX数据包-数据以本身的形式出现

添加包头包尾进行数据判断,可以自由定义,若数据与包头包尾重复,怎么办?
(1)固定包长,对齐包头包尾,就能分清楚数据与包头尾了
(2)数据限幅,只发生非包头包尾的数据
(3)添加包头包尾的数量,减少重复

文本数据包-数据每个字节都要经过一层编码和译码,最终表现出文本形式

由于文本内容数据过多,不用在意包头包尾重复的形式,但是解析效率低

发送较为简单无疑发送HEX就是发送数组,发送文本就约等于发送字符串,接收却难

固定包长 HEX数据包接收

引入状态机的思维,定义三个状态,S=0、1、2进行逻辑编程,在对应的状态干对应的事情
发送时:

Serial_TxPacket可以设置为数组extern出去或者是定义为数组指针变量,这里本人觉得加入形参替代4不就可以实现可变长度了,不过接收部分也要做更多的变化,蛮难。

接收部分:

void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;        //定义表示当前状态机状态的静态变量
    static uint8_t pRxPacket = 0;    //定义表示当前接收数据位置的静态变量
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)        //判断是否是USART1的接收事件触发的中断
    {
        uint8_t RxData = USART_ReceiveData(USART1);                //读取数据寄存器,存放在接收的数据变量
        
        /*使用状态机的思路,依次处理数据包的不同部分*/
        
        /*当前状态为0,接收数据包包头*/
        if (RxState == 0)
        {
            if (RxData == 0xFF)            //如果数据确实是包头
            {
                RxState = 1;            //置下一个状态
                pRxPacket = 0;            //数据包的位置归零
            }
        }
        /*当前状态为1,接收数据包数据*/
        else if (RxState == 1)
        {
            Serial_RxPacket[pRxPacket] = RxData;    //将数据存入数据包数组的指定位置
            pRxPacket ++;                //数据包的位置自增
            if (pRxPacket >= 4)            //如果收够4个数据
            {
                RxState = 2;            //置下一个状态
            }
        }
        /*当前状态为2,接收数据包包尾*/
        else if (RxState == 2)
        {
            if (RxData == 0xFE)            //如果数据确实是包尾部
            {
                RxState = 0;            //状态归0
                Serial_RxFlag = 1;        //接收数据包标志位置1,成功接收一个数据包
            }
        }
        
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);        //清除标志位
    }
}

可变包长 文本数据接收


接收方:

void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;        //定义表示当前状态机状态的静态变量
    static uint8_t pRxPacket = 0;    //定义表示当前接收数据位置的静态变量
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)    //判断是否是USART1的接收事件触发的中断
    {
        uint8_t RxData = USART_ReceiveData(USART1);            //读取数据寄存器,存放在接收的数据变量
        
        /*使用状态机的思路,依次处理数据包的不同部分*/
        
        /*当前状态为0,接收数据包包头*/
        if (RxState == 0)
        {
            if (RxData == '@' && Serial_RxFlag == 0)        //如果数据确实是包头,并且上一个数据包已处理完毕
            {
                RxState = 1;            //置下一个状态
                pRxPacket = 0;            //数据包的位置归零
            }
        }
        /*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/
        else if (RxState == 1)
        {
            if (RxData == '\r')            //如果收到第一个包尾
            {
                RxState = 2;            //置下一个状态
            }
            else                        //接收到了正常的数据
            {
                Serial_RxPacket[pRxPacket] = RxData;        //将数据存入数据包数组的指定位置
                pRxPacket ++;            //数据包的位置自增
            }
        }
        /*当前状态为2,接收数据包第二个包尾*/
        else if (RxState == 2)
        {
            if (RxData == '\n')            //如果收到第二个包尾为换行
            {
                RxState = 0;            //状态归0
                Serial_RxPacket[pRxPacket] = '\0';            //将收到的字符数据包添加一个字符串结束标志
                Serial_RxFlag = 1;        //接收数据包标志位置1,成功接收一个数据包
            }
        }
        
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);        //清除标志位
    }
}
        

若想使用文本模式字符串判断,可以调用#include "string.h"头文件中的strcmp()函数,对比串口接收到的数据与字符串,if(strcmp(Serial_RxPacket, "LED_ON") == 0),从而执行操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值