通信的目的那一点的解析:STM32虽然功能丰富,但是有一些功能是没有的,需要外挂芯片功能实现,比如蓝牙无线遥控、陀螺仪加速度计测量姿态功能,这时候就需要外接通信线连接外挂芯片,与外挂芯片交换数据,实现功能,所以又说扩展了硬件系统。
TX与RX有的地方也叫TXD以及RXD,分别是数据发送脚以及数据接受脚。
SCL(sclock)是时钟,SDA(series data)是数据。
SCLK是时钟,MOSI是主机输出引脚, MISO主机输入数据脚,CS(chip select)片选,用于指定通信对象。
CAN_H,CAN_L这两个是差分数据脚,用两个表示差分数据。
DP DM也是一堆差分数据脚
数据按照协议的规定,在这些引脚上输入输出,从而实现通信。
全双工指的是发送和接收引脚互不影响,半双工只能进行发送或者进行接收。单工指的是数据只能从一个设备到另一个设备,不能反着来。
时钟同步通信指的是时钟接收方有一个时钟规定接收信号采样的时间。而异步通信则需要发送以及接收方共同接收一个采样时间,并且还要增加一些帧头帧尾进行采样对齐,比如信号时高电平到低电平,到底是1100还是10,则看采样频率采样的是什么。
单端电平指的是高低电平对应的是对GND的电压差,故此单端通信双方必须共地,差分电平靠两个引脚的电压差来传送信号,通信时候可以不接GND,不过USB中也有一些信号需要单端信号,所以USB还是需要共地的。差分信号极大的提高了抗干扰特性,故此传送速度以及距离都会非常高,性能也不错。
点对点就是一对一进行传送,多设备就是一对多,可以再总线上挂载很多的设备,
第一个小图,是USB转串口模块,上面的小芯片是CH340,芯片的作用是把串口协议转换为USB协议,引脚端接支持串口通信的引脚,另一端接USB,
中间是陀螺仪传感器模块,可以测量角速度加速度这种姿态参数,左右四个引脚,一边是串口引脚,一边是I2C引脚
右边是蓝牙串口模块,下面四个引脚是串口通信引脚,芯片是可以与手机互联,实现手机遥控单片机的功能,
VCC要保证两个模块都有供电,并且电压要在适当范围之内。
复杂的嘻哈UN口通信还会有时钟引脚,硬件流控制引脚。
RS485抗干扰能力强,可以传送上千米,而RS232只能传送几十米。
RS232电平一般在大型机器中使用,环境恶劣,静电干扰大,所以电平电压都比较大,波动也比较大。
单片机这种低压设备,一般使用TTL电平。
如果通信两个设备之间的信号电平使用不一致的话,应该使用电平转换芯片进行电平转换。
我们看一下下面的这两个时序图啊,这就是串口发送一个字节的格式,这个格式是串口协议规定的,串口中每一个字节都装载在一个数据帧里面。每个数据帧都有起始位、数据位和停止位组成,这里数据位有八个,代表一个自己的八位,在右边这个数据帧里面,还可以在数据位的最后加一个奇偶校验位。这样数据位总共就是九位,其中有效载荷是前八位,代表一个字节,校验位跟在有效载荷后面占一位,这就是串口数据帧的整体结构,那我们来看一下串口的参数,第一个参数就是波特率,它的用途是规定串口通信的速率呀,串口一般是使用异步通信,所以需要双方约定一个通信数据,比如我每隔一秒发送一位,那你就也得每隔一秒接受一位。接收快了就重复了,如果你接受慢了,那就会漏掉某些位,所以说发送和接收必须要约定好速率,这个数据参数就是波特率,波特率本来的意思是每秒传输码元的个数,单位是码元每秒,或者直接叫波特(Baud)。另外还有个数据表示叫比特率,比特率的意思是每秒传输的比特数,单位是bit/s,或者叫bps。在二进制调制的情况下,一个码元就是一个bit特,此时波特率就等于比特率。
像我们单片机的串口中心基本都是二进制调制,也就是高电频表示1,低电频表示0, 一位就是1bit,所以说这个串口的波特率经常会和比特律混用。不过这也是没关系的,因为这两个说法的数值相等。如果是多进制调试,那波特就是不一样的,那反映到波形上,这个了解一下,那反映到波形上,比如我们双方规定波特率为1000bps,那就表示一秒要发1000位,每一位的时间就是一毫秒。也就是这里,这一段时间是一毫秒,发送方每隔一毫秒发送一位,接收方每隔一毫秒接收一位,这就是波特率,它决定了每隔多久发送一位。接下来是起始位,它是标志一个数据帧的开始固定为低电平,我们看一下下面这个波形啊。首先串口儿的空闲状态是高电平,也就是没有数据传输的时候儿,引脚必须要置高电平作为空闲状态,然后需要传输的时候,必须要先发送一个起始位,这个起始位必须是低电平来打破空闲状态的高电平,产生一个下降沿,这个下降沿就告诉接收设备,这一帧数据要开始了。
如果没有起始位,那当我发送八个1的时候,是不是数据线就一直都是高点平啊,没有任何波动对吧?这样接收方怎么知道我发送数据了呢?所以这里必须要有一个固定为低电平的起始位。产生下降眼来告诉接收设备,我要发送数据啦。同理,在一个字节数据发送完成后,必须要有一个停止位,这个停止位的作用是用于数据跟间隔固定为高电平。同时这个停止位啊,也是为下一个起始位做准备的。如果没有停止位,那当我数据最后一位是零的时候,下次再发动新的一帧,是不是就没法产生下降沿了是吧?这就是起始位和停止位的作用。起始位固定为零,产生下降沿,表示传输开始。停止位固定为1,把引脚恢复成高端平,方便下一次的下降沿,如果没有数据了,正好引脚也为高电平,代表空闲状态。
继续看数据位,这里数据位表示数据帧的有效,1为高电品,0为低电品,低位先行啊,比如我要发送一个字节0X0F,那就首先把0F转换为二进制,就是00001111。然后低位先行。所以数据要从第一位开始发送,也就是1111,0000,现在依次放在发送引脚上,所以最终引角的波形就是这样的。所以说,如果你想发送0F这一个字节数据,那就按照波特率要求定时翻转引脚电瓶,产生一个这样的模型就行了。好,最后看一下校验位,它的作用是用于数据验证,根据数据位计算得来的,这里串口使用的是一种叫奇偶校验的数据验证方法,奇偶校验可以判断数据传输是不是出错了?如果数据出错了,可以选择丢弃或者要求重传。校验可以选择三种方式啊,无校验,奇校验和偶校验,无校验就是不需要校验位,波形就是左边这个起始位,数据位,停止位,总共三个部分。奇校验和偶校验的波形就是右边这个。起始位、数据位、校验位、停止位。如果使用了奇校验,那么包括校验位在内的九个数据,会出现奇数的1。比如,如果你传输00001111。目前总共四个一是偶数个,那么校验位就需要再补一个一,连同校验位就是000011111,总共五个1保证一为奇数,如果数据是00001110,是3个1是奇数,那么校验位就补一个零,连同校验位就是000011100。总共还是三个1,接收方在接收数据后会验证数据位和校验位。如果一的个数还是奇数。证明数据没有出错,如果在传送方中因为干扰有一位由一变成零。或者由零变成一了。那么整个数据的接偶特性就会变化,接收方一验证发现一的个数不是奇数。那就认为传输出错,就可以选择丢弃或者要求重传。偶校验也是一样。
但是这种方法检出率不是很高。
如果有两位数据同时出错,既有特性不变,那就校验不出来,所以奇偶校验只能保证一定程度上的数据校验,如如果想要更高的减出率,可以了解一下CRC校验,这个校验会更加好用,当然也会更复杂,我们这个STM3内部也有CRC检测,可以了解一下。那到这里串口的持续我们就了解了,最后再说一下啊,我们这里的数据位有两种表示方法。一种是把校验位作为数据位的一部分,就像下面这个时序一样,分为八位数据和九位数据。其中九位数据就是八位有效代喝和一位校验为。另一种就是把数据位和校验位独立开,数据位就是有效载荷儿,校验位就是独立的一位,像我上面的文字描述啊,就是把数据位和校验位分开描述。在串口助手软件里也是用的这种分开描述的方法,数据位八位,校验位一位,总之无论是合在一起还是分开描述,描述的都是同一个东西啊,这个应该也好理解。
那最后我们来看几个串口中心的实际波形啊,看完这些波形,相信你就能理解串口是如何来传输数据的了。这些波形我是用示波器实测的,操作方法是把探头的GND接到负极探头,接待发送设备的TX引件,然后发送数据,就能捕捉到这些波型了。那我们先看一下第一个模型。这个模型是发送一个字节数据0X55在TX引角输出的波形。波特率是9600,所以每位的时间就是一除9600,大概是104微秒,可以看到这里一位就是100微秒,多一点儿就是104微秒。
对于第一个图,没发送数据的时候是空闲状态,高电平数据帧开始先发送,起始位产生下降沿,代表数据帧开始啊,数据0X55转为二进制,低位先行就是。是一次发送1010,然后是1010。然后这个参数是八位数。没有校验位,所以之后就是停止位,把引脚置回高电平。这样一个数据帧就完成了,在STM32中,这个根据字节数据翻转高低电频是由USART外设自动完成的,你也可以软件模拟产生这样的模型。那就是定时器定一个104微秒的时间。时间到之后,按照数据帧的要求调用GPIO_Writebit函数是写入高低电瓶,产生一个和这一模一样的波形。
你的TX引脚发送就是置高低电频,那在RX引脚接收显然就是读取高低电平了,这也可以由USART外设自动来完成。如果想软件模拟的话,那就是定时调用函数1GPIO_ readinputdata来读取。最终拼接成一个字节。当他接收的时候应该还需要一个外部中断啊,但其实为的下降沿触发进入接收状态,并且对其采用时钟,然后一次采样八次,这就是接收的逻辑。
接着看一下下面的波形,如果发送你波形就是这样的。起始位,然后01010101停止位结束。在下面如果发送0XFF就是八个一,那波形就是这样。起始位八个一停那不行,就是这样。其实我发个一停止位结束在起始位下降沿之后的一个数据帧的时间内,这个高电平就是作为数据一来看的。当数据帧结束后,这里虽然还是一没有任何变化,但此时的一已经是属于空闲状态了。他需要等待下一个下降沿来开启。之后再看下面,如果发送0X0000,也是一样的,然后继续看右边儿的模型。这里如果把波特率改成4800的,也就是波特率变为一半儿,那相应的波形时长就会变为原来的二倍,可以看到这里十位数据总共大概两毫秒多一点,具体应该在2.08毫秒,那一位就是208微秒是之前的二倍,数据波形的时间拉宽,波形的变化趋势是不变的,之后看下面这个模型。这里加了一个,偶数校验位。数据是0X55。数下来1的个数是四个,已经是偶数了,所以输出的校验位是0。最后看一下停止位的变化,停止位是可以进行配置的,可以选择一位,1.5位,两位的。看一下这上面的波形是连续发送两个0X5。两个数据都会接在一起,中间没有空闲状态,下面这个波形是两位停止位,连续发送两个0x5。可以看到这里停止位就是两位的宽度,中间也没有空间状态。不过这样数据间隔的就更宽一些了,这就是不同长度停止位的现象。
总结一下就是TX引角输出电平,RX引脚定时读取引角的高低电平。每个字节的数据加上起始位、停止位,可选的校验位打包成数据帧,依次输出在TX引角另一端RX角依次接收,这样就完成了字节数据的传递,这就是串口通信。
先看一下第一句usart,它的英文全称是这一大串,就是通用同步异步收发器,这个S就是同步的意思,另外我们今天还会遇到串口,叫UART,这里少了个S,就是异步收发器,一般我们串口很少使用这个同步功能啊,所以usart和uart使用起来没多区别,其实这个USART只是多了个时钟输出而已,它只支持时间输出,不支持时钟输入,所以这个同步模式更多的是为了兼容别的协议,并不支持两个USART之间进行同步,所以我们学习串口主要还是异步通信。
然后继续看下一条,我们之前学习了串口的协议,串口主要就是靠收发这样的约定好的波形来进行通信的,那这个USART外设就是串口的硬件支持电路。USART大体可以分为发送和接收两部分。发送部分就是就是将数据寄存器的一个字节数字自动转换为协议规定的波形,从TX发动出去,接收部分就是自动接收RX引脚的波形,按照协议规定解码为一个字节数据存放在数据寄存器里,这就是USART电路的功能。当我们配置好了USART电路,就可以直接读写数据寄存器,就能自动发送和接收数据了,使用还是非常方便的。
波特率发生器就是用来配置波特率,它其实就是一个分频器,比如我们AB2总线给个72兆赫兹的频率,然后波特率发生器进行一个分频,得到我们想要的波特率。最后在这个时钟下进行收发,就是我们指定的通信波特率。
可配置数据位长度(8/9),停止位长度0.5或1或1.5或2,这些就是STM32 USART支持配置的参数了,这个数据位长度就是我们前面这里的参数,有八位和九位,是包含奇偶校验位的长短。一般不需要校验位就是八位,需要是九位。
停止位长度也就是这里的在进行连续发送时,停止位长度决定了帧的间隔,我们最常用的就是1位停止位。
然后下面就是可选校验位,无校验,奇校验或偶校验,有关奇偶校验我们之前也讲过,这里我们最常用的是无校验,那以上这些所有的参数啊,都是可以通过配置寄存器来完成的,使用库函数配置的话就更简单了,直接给结构体赋值就行。
串口参数我们最常用的就是波特率9600,或者115200,数据位八位,停止位1位,校验位无校验。一般我们都选这种常用的参数。
同步模式就是多了个时钟CLK的输出。
硬件流控制,这个是比如a设备有个TX向B设备的X发动数据,A设备一直在发,发的太快了,B处理不过来,如果没有硬件流控制,那B就只能抛弃新数据或者覆盖原数据了。如果有硬件流控制。在硬件电路上会多出一根线,如果B没准备好接收就是高电平,如果准备好了就是低电平,A接收到了B反馈的准备信号,只会在B准备好的时候才发数据,如果B没准备好,那数据就不会发送。这就是硬件流控的,可以防止因为B处理慢而导致数据丢失的问题。硬件里的控制。STM32也是有。不过我们一般不用.
DMA进行数据转信,如果有大量的数据进行收发,可以使用DMA转运数据,减轻CPU负担,最后智能卡,irda,lin这些是其他的一些协议,因为这些协议和串口是非常的像。所以STM32就对USART加上一些小改动,就能兼容这么多协议了,不过我们一般不使用这些协议。这个智能卡应该是跟我们刷的饭卡公交卡。IRDA是用于红外通信的啊,这个红外通信就是一个红外发光管,另一边是红外接收管,然后靠闪烁红外光通信。Lin是局域网的通信协议。
SM32F103876的usart资源有usart1 usart2 usart3,可以挂载很多串口设备,其中这里uSART1是APB2总线上的设备,剩下的都是1中的设备。
发送寄存器连接的是TX引脚,接收寄存器连接的是RX引脚,TDR以及RDR的地址是一样的,其实也就是DR寄存器(数据寄存器),但是在硬件上表现为两个部分。TDR是只写的寄存器,RDR是只读的寄存器。读操作的时候,就是从RDR中读出来,写操作就是写入TDR。
下面是两个移位寄存器,一个用于发送一个用于接收,前者的作用是把一个字节的数据一位一位的移动出去,对应串口协议波形的数据位。
TDR与发送移位寄存器如何工作?工作举例:
比如你在某时刻给TDR写入了0X55,这个数据在寄存器器里就是二进制数01010101。那么此时硬件检测到你写入数据了,他就会检查当前移位寄存器是不是有数据正在移位,如果没有,这个01010101就会立刻全部移动到发送移位起存器准备发送,当数据从TDR移动到发送移位寄存器时,会置一个标志位叫TXE(TX empty)发送寄存器空,我们检查这个标志位,如果置1了,我们就可以在TDR写入下一个数据了。
当TXE标志位置1数据其实还没有发送数据,只要数据从TDR转移到发送移位寄存器,TXE就会置1,我们就可以写入新的数据了,然后发送移位寄存器就会在下面这里的发送器控制的驱动下向右移位,然后一位一位的把数据输出到TX引脚。
这里是向右移位的,所以正好儿和串口协议规定的低位先行是一致的。当数据移位完成后,新的数据就会再次自动的从TDR转移到发送移位净准器里来,如果当前移位寄存器器移位还没有完成,TDR的数据就会进行等待,一旦一未完成就会立刻转移过来,有了TDR和一位净器的双重缓存,可以保证连续发送数据的时候,数据帧之间不会有空闲,提高了工作效率,简单来说啊,就是你数据一旦从TDR转移到移位寄存器了,管你有没有一位完成,我就立刻把下一个数据放在TDR等着,一旦移完了,新的数据就会立刻跟上,这样做效率就会比较高。
然后看一下接收段,这里也是类似的,数据从RX引脚通向接收移位寄存器,在接收器控制的驱动下,一位一位的读取RX电平,先放在最高位,然后往右移,移位八次之后就能接受一个字节了。同样因为串口协议规定是低位先行,所以接收移位寄存器是从高位往低位这个方向移动的。之后当一个字己移位完成之后,这一个字节的数据就会整体的一下子转移到接收数据进去RDR。在转移的过程中也会置一个标志位啊,叫RXNE(RX NOT Empty)接收数据寄存器非空,当我们检测到RXNE置1之后,就可以把数据读走了。
同样,这里也是两个寄存器进行缓存,当数据从移位寄存器转移到RDR时,就可以直接移位接收下一帧数据了,这就是USART外设整个的工作流程。
当然发送的时候需要加上帧头帧尾,接受的时候要去除帧头帧尾,内部电路会自动执行。
发送器控制,它就是用来控制发送移位净行器的工作的。接收器控制用来控制接收移位进器的工作。然后左边这里有一个硬件数据流控,简称流控,如果发送设备发的太快,接收设备来不及处理,就会出现丢弃或覆盖数据的现象,那有了流控就可以避免这个问题了。这里流控有两个引角,一个是NRTS(requst to send)是请求发送,是输出脚,也就是告诉别人我当前能不能接收。
NCTS(clear to send)是清除发送,是输入角,也就是用于接收别人NRTS的信号的,这里前面加个N,意思是低点平有效。
那这两个夹怎么玩的呢?首先得找另一个支持流控的串口,它CTS接到我的RTS,然后我的RTS要输出一个能不能接收的反馈信号,接到对方的CTS。当我能接受的时候,RTS就是低电平请求对方发送。对方的CTS接收到之后就可以一直发,当我处理不过来时,比如接收数据寄存器我一直没有读,又有新的数据过来了,现在就代表我没有及时处理,那RTS就会是高电平,对方CTS接收到之后就会暂停发送。直到接收数据器被读取,RTS置低电平新的数据才会继续发送。那反过来,当我的TX给别人发送数据时,我们CTS就要接到对方的RTS,用于判断对方能不能接受。TX和CTS是一对的,RX和RTS是一对的,这就是流控的工作模式。
接着继续看右边儿这个模块,这部分电路用于产生同步的时钟信号儿,它是配合发送移位计数器输出的啊,发送计数器每移位一次,同步时钟电频就跳变一个周期。当然这个始终只支持输出,不支持输入,所以两个USART之间不能实现同步的串口通信,那这个时钟信号儿有什么用呢?第一个用途就是兼容别的协议,比如串口加上时钟之后,就跟spi协议特别像,所以有了时钟输出的串口就可以兼容spi。
另外这个时钟也可以做自适应波特率,比如接收设备不确定发送设备给的是什么波特率。那就可以测量一下这个时钟的周期,然后再计算得到波特率,不过这就需要另外写程序来实现这个功能了。
然后继续看中间这个唤醒单元,这部分的作用是实现串口挂载多设备,串口一般是点对点的通信,两个设备互相通信,想发数据直接发就行,而多设备在一条总线上可以接多个同设备,每个设备分配一个地址,我想跟某个设备通信,就先进行寻址,确定通讯对象,再进行数据收发。这个唤醒单元可以用来实现多设备的功能在这里可以给串口分配一个地址。当你发送指定地址时,此设备唤醒开始工作,当你发送别的设备地址时,别的设备就换醒工作。这个设备没收到地址就会保持沉默,这样就可以实现多设备的通信了。
接着看下面这部分是中断输出控制,中断申请位,就是状态器这里的各种标志位,状态接入器这里有两个标志位比较重要,一个是TXE发送寄存器空,另一个是RXNE接收寄存器非空,这两个是判断发送状态和接收状态的必要标志。中断输出控制这里就是配置中断是不是能通向NVIC.
然后最下面这里是波特率发生器部分,波特率发生器其实就是分频器,APB时钟进行分频,得到发送和接收移位的时钟。时钟输入是FPCLKX,x等于一或2,USAR1挂载在APB2,所以就是PCLK2的时钟一般是72兆,其他的usARt都挂载在APB1,所以是PCLK1,一般是36MHz。之后这个时钟进行一个除以一个USARTDIV分频系数。USARTDIV里面就是右边这样,是一个数值。并且分为了整数部分和小数部分。因为有些波特率用72兆除一个整数的话,可能除不尽会有误差,所以这里分别系数是支持小数点后四位的分屏就更加精准。
之后分频完之后还要再除个16,得到发送器时钟和接收器时钟通向控制部分。然后右边这里如果TE(Tex Enable)为一,就是发送器使能,发送部分的波特率就有效,如果RX为1,就是接收器使能,接收部分的波特率就有效。
更具体的可以看手册描述。
具体引脚看引脚结构表。
波特率发生器,生成约定的通信速率。 时钟来源是PCLK2/1。
时钟在每一位的中间都有一个上升沿,时钟频率与数据传输的速率一致,接收端可以再上升沿的时候进行采样,这样可以精准定位每一位数据,靠谱。时钟的极性相位可以通过寄存器配置。
如果选用8位字长,一般就选用无校验,不然一个字节都不够,不方便,这样每一帧的有效载荷都是一字节。
这个时钟就是同步输出功能用的,在每一次上升沿的时候,就会进行采样,跟数据传输一致。输入要保证采样频率以及波特率(数据传输速率)一致。就是上面所说。还要保证采样位置刚好在正中间。中间最可靠,如果靠前或者靠后,可能高低电平还在反转。
STM32如何设计输入电路:
当输入电路侦测到一个数据帧的起始位的时候,就会以波特率的频率连续采样一帧数据,从起始位开始,采样位置就要对齐到位的正中间,只要第一位对齐了,后面就一定对齐了。为了实现这些功能,输入电路对采样频率进行了细分,意波特率的十六倍进行采样,也就是在一位的时间里进行十六次采样,最开始空闲状态高电平,采样就一直是1,某一位突然采样到0,那就说明在两次采样之间出现了下降沿,没有任何噪声的话,之后就是起始位了,在起始位会连续进行十六次采样,无噪声的情况下都会是0,但是实际一般都会有噪声,所以在十六次采样之前还要进行几次确定,根据上图所示,还会在下降沿的第三次,5,7次进行采样,8.9.10在进行一次采样,这两批采样中,都必须至少有两个0,如果没有噪声,就会全是0,如果有轻微噪声,有两个0,也算是符合要求检测到了起始位,但是会在状态寄存器中噪声标志位NE(noice erro)中置1,数据接收到了,但是有噪声,悠着点。
如果只有一个0,那就是噪声引起了,STM32会忽略前面的数据,重新进行检测。
这是在检测到起始位置后的检测,起始位已经对齐了,直接在8.9.10位进行数据采样,在两次几两次以上都是同一个的时候,就会置那个电平,假设两个0一个1,就是0,同时NE也会进行标记。
波特率发生器就是分频器, 为什么除以16,因为在他内部还有一个十六倍波特率的采样时钟,所以输入时钟经过分频之后,要等于十六倍的波特率,因为这个采样时钟就是这个外部输入的时钟,需要这个采样时钟!!
不过库函数配置的时候很简单,直接输入波特率就行,库函数会自动帮我们算。
USB转串口D+D-通信线走的时候USB协议,经过CH340转换之后才能输出串口协议,VCC五伏经过稳压管得到VCC3.3V,然后用两个线引出,也就是4.6号引脚,经过跳线帽可以选择TTL电平,也就是通信电平,详细请看文字说明。跳线帽是给CH340供电的,拔掉也可以,但是不稳定。
手册在25.同步异步收发器
准备工作:
CH340驱动,连线RX与TX,TX与RX,以及接线帽要按照之前的搞好。
步骤:
同步时钟输入的相关函数:包括时钟是不是要输出,极性以及相位等参数,参数较多,一般就用结构体进行配置。
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);//发送数据。写DR寄存器
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);//接收数据,读DR寄存器
DR寄存器内部有四个寄存器,控制发送与接收
1.开启时钟,需要把USART以及GPIO的时钟开启
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
2.GPIO初始化,把TX配置成复用输出,RX配置成输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//TX引脚是USART控制的输出引脚,选用复用推挽输出;RX引脚是USART控制的输入引脚,选择输入模式,输入没哟us很么普通输入复用输入,一根线仅仅可以一个输出,但是有很多个输入。所以输入脚,外设,GPIO可以同时用。一般RX配置浮空输入或者上拉输入,因为空闲时是高电平,所以不用下拉输入。GPIO那一章节的手册参照表可以看一下。
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;//硬件流控制,复制函数名,然后(CTRL+ALT+空格联想代码,没有合适的函数再跳转定义查看,可以方便很多,不用挑来跳出。
/*********************/
#define USART_HardwareFlowControl_None ((uint16_t)0x0000)//不用流控
#define USART_HardwareFlowControl_RTS ((uint16_t)0x0100)//只RTS
#define USART_HardwareFlowControl_CTS ((uint16_t)0x0200)//只CTS
#define USART_HardwareFlowControl_RTS_CTS ((uint16_t)0x0300)//两者都
/*********************/
USART_InitStructure.USART_Mode = USART_Mode_Tx;//串口模式,如果要输入输出的话可以或
USART_InitStructure.USART_Parity = USART_Parity_No;//校验位odd奇校验,ENVEN偶校验
USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位宽
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
4.如果仅仅使用发送功能,就直接打开USART然后初始化就行了,如果需要接收功能,要进行中断配置,就是在USART之前,加上Itconfig以及NVIC函数即可。
如果想接收或者发送的状态,接调用获取标志位的函数
发送函数:
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);//把Byte传入函数内部,跳转得:
/***********************/
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
/* Check the parameters */
assert_param(IS_USART_ALL_PERIPH(USARTx));
assert_param(IS_USART_DATA(Data));
/* Transmit Data */
USARTx->DR = (Data & (uint16_t)0x01FF);//把数据传入Data中,然后与那个数,清除无关高位数值,然后直接赋值给DR寄存器,由于是写操作,就给了TDR,然后移动到移位寄存器,一位一位从TX口发送数据。
}
/********************/
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//等待数据全部发送到移位寄存器。
}
也就是说TXE不用手动清零,在下次sendData的时候,标志位会自动清零。,这一句的含义就是,等待TXE置1,然后再重新写入数据。
串口助手的配置要与程序的USART码配置一致,不然会解析错误。
接收模式中,HEX模式就是以原始数据的形式显示,发送41,就是41本身。文本模式就是字符串。
左下角是字符以及ascall码等,右下角:当发送0x41的时候,传送也是0x41,如果接收方采用HEX模式,就显示原始数据,如果选择文本模式,就译码成A,如果发送的是A,就编码成0x41,然后传输。文本模式就是不用译码“A”,HEX模式就是0x41。
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
传输数组,注意指针指向数组首地址。格式问题。
/*********/
//发送字符串,char *String其实就是定义了一个数组,
- ‘\r’ (回车):将光标回到当前行的行首(而不会换到下一行),之后的输出会把之前的输出覆盖。
- ‘\n’ (换行):将光标换到当前位置的下一位置,而不会回到行首。
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;
}//x的y次方
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');//假设12345,就是12345/10000%10=1,然后12345/1000%10=2,以此类推。
}
}
/*************************************/
默认是十进制无符号的输出。
printf函数移植的方法。
1.魔术笔,勾选sue mcrolib,它是keil为嵌入式平台优选的精简库,等会要用的printf函数就可以用这个mrcolib。
2.对printf进行重定向,将printf打印的东西输出到串口,因为printf是默认输出到屏幕的,但是单片机没有屏幕,所以要进行重定向。
2.1重定向步骤:#include <stdio.h>
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
fput是printf的底层函数,printf就是不断调用fput一个个进行打印的,重写之后就是不断的在串口打印输出了。此文件在.c文件中写,不用.h,在main函数之中直接调用printf就行。主义还要在.h文件中加上#include<stdio.h>,然后主函数中调用.h函数。主函数中: printf("\r\nNum2=%d", 222);
但是这种重定义有弊端,只能重定义一个串口,也就是说printf只能在一个串口中打印出来。
可以使用sprintf,把把格式化字符输出到一个字符串里,第一个参数,是打印输出的位置
char String[100];//定义一个字符串数组
sprintf(String, "\r\nNum3=%d", 333);//存入数组
Serial_SendString(String);//发送
该方法不涉及重定向,每一个串口都可以使用。
但是一行一行写太过麻烦,下面是封装sprintf函数。
.h文件中。
#include <stdarg.h>
void Serial_Printf(char *format, ...)//第一个参数是格式化字符串,第二个参数是接收可变参数列表
{
char String[100];//定义字符串
va_list arg;//定义参数列表变量
va_start(arg, format);//从format开始接收参数表,放在arg中
vsprintf(String, format, arg);//1.打印位置2.格式化字符串3.参数表,sprintf只能接受直接打印的参数,这里用v
va_end(arg);//释放参数表
Serial_SendString(String);//传输数据
}
显示汉字
这个是在工具钳打开。选择UTF8.,这里改了,那么在串口助手也要相应更改
但是UTF8有些设备兼容性不好,可以切换GB2312编码
更改之后需要重新刷新,也就是把汉字删掉,然后右键文件->close all,再重新打开,这时候串口助手要更改为GBK了。
发送与接收(中断)
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);
/****多增加一个PA10引脚************/
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;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启RXNE到NVIC的输出,一旦RXNE置1了,就会进入中断,然后再在中断中读取数据。
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);
}
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);//清除RXNE
}
}
对于串口接收来说,可以使用查询以及中断两种方法,如果使用查询,初始化到此结束,使用中断,还需要开启中断,配合NVIC,在 USART_Cmd(USART1, ENABLE);前面那里。
查询就是不断在主程序中查询RXNE标志位,如果置1了,就说明受到了数据,在调用ReceiveData读取DR寄存器数据,不用手动清零RXNE,在进行读操作的时候会自动清零。
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "RxData:");
Serial_Init();
while (1)
{
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);//电脑发送给单片机,单片机回传给电脑。
OLED_ShowHexNum(1, 8, RxData, 2);
}
}
}
HEX模式下,串口助手中发送区只能使用十六进制,不用写0x,但是数值是0~9,A~F。
发送与接收(查询):是电脑发送过来,然后单片机查询接收
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "RxData:");
Serial_Init();
while (1)
{
if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
{
RxData = USART_ReceiveData(USART1);
OLED_ShowHexNum(1, 1, RxData, 2);
}
}
}
#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;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
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);
}
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);
}
}
很多数据模块都需要回传大量数据,选择串口收发Hex数据包,接收部分也要按照数据包的形式接收,这样才能接收多字节数据包:
数据包的作用就是把一个个单独字节打包起来,方便实现多字节的数据通信。之前的串口通信,是收发一个字节,这没问题,但是实际应用中有时候需要多个字节大包围一个整体发送。
包头就是数据包的开始,包尾就是结束,以0xFF作为包头,如果数据需要传送这个数怎么办?
解决方法一:规定数据的传输范围,让包头包尾在数据范围之外。
二.无法比避免载荷数据和包头包尾重复,那我们就尽量使用固定长度的数据包,这样由于载荷数据是固定的,只要我们通过头包尾对齐了数据,我们就可以严格知道哪个数据应该是包头包尾,哪个数据应该是载荷数据,在接收载荷数据的时候,我们并不会判断他是否是包头包尾,而在接受包头包尾的时候,我们会判断他是不是确实是包头包尾。用于数据对齐。这样在经过几个数据包的对齐之后,剩下的数据包儿应该就不会出现问题了。
三.第三种就是增加包头包围的数量,并且让他尽量呈现出载荷数据出现不了的状态。比如我们使用FF,FE作为包头,FD,FC作为包尾,这样也可以避免载荷数据和包头包尾重复的情况发生啊。
这个包头包尾并不是全部都需要的,比如我们可以只要一个包头,把包尾删掉,这样数据包的格式就是一个包头FF加四个数据,当检测到FF开始接收四个字节后,不过这样的话,载荷和包头重复的问题会更严重一些。比如最严重的情况下,载荷跟包头都是FF,那你肯定不知道哪个是包头的,而加上fe作为包尾,无论数据怎么变化,都是可以分辨包尾的。
然后第三个问题就是。就是固定包长和可变包长的选择问题,对于HEX数据包来说,如果你的载荷会出现和包头包尾同步的情况。那就最好选择固定包长,这样可以避免接受错误,如果你又会重复,又选择可变包装,那数据很容易就乱套了。如果载荷不会和包头包尾同步,那可以选择可变包长数据长度,像这样四位啊,三位等等,一位十位来回认一遍肯定都没问题,因为包头包尾是唯一的,只要出现包头就开始数据,只要出现包尾就结束数据包。
最后一个问题就是各种数据转换位的问题,这里数据都是一个字节一个字节组成的啊。如果你想发送16位的整形数据,32位的整形数据。Float double,甚至是结构体,都没问题,因为他们内部其实都是由一个字节一个字节组成的。只需要用一个UNIT8杠七的指针指向它,把它们当做一个字节数组发送就行了。
文本数据包跟HEX数据包分别对应了文本模式跟HEX两种模式。前者是原始数据,后者是经过一层编码和译码的文本格式,但是其实在每一个文本数据背后,都是一个自己的HEX数据。
@是包头,\r\n是包尾。
HEX数据包优点:传输直接,解析数据简单,适合一些原始数据的发送,比如一些串口通信的陀螺仪,温湿度传感器。缺点是载荷容易和包头包尾重复。发送的时候就是发送数组,用哪个发送数组的函数。
文本数据包:数据直观易理解,比较适合一些输入指令进行人机交互的场景,比如蓝牙模块常用的AT指令,CNC和3D打印机常用的代码都是文本数据包的格式。那缺点就是解析效率低,比如你发送一个数100派的数字包,HEX数据包就是一个字节100完事儿。文本数据包就得是三个字节的字符‘1’‘0’‘0’,收到之后还要把字符转换的数据,才能得到100。所以说我们需要根据实际场景来选择和设计数据包格式。发送的时候用发送字符串的函数。
固定包长的HEX数据包接收:
首先,根据之前的代码,我们知道每收到一个字节,程序都会进一遍中断,在中断函数里,我们可以拿到这一个字节,但拿到之后我们就得退出中断了。所以每拿到一个数据都是一个独立的过程。而对于数据包来说,很明显它具有前后关联性,包头之后是数据,数据之后是包尾。对包头、数据和包尾这三种状态,我们需要有不同的处理逻辑,所以在程序中我们需要设计一个能记住不同状态的机制。在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计思维就叫做状态机,在这里我们就使用状态机的方法来接收一个数据包儿,要想设计一个好的状态机程序,画一个这样的状态转移图是必要的,我们看一下。对于上面这样一个固定的转移图是必要的,我们看一下。对于上面这样一个固定包长数据包来说,我们可以定义三个状态,第一个状态是等待包头,第二个状态是接收数据,第三个状态是等待包尾。每个状态需要用一个变量来标志一下,比如我这里用变量S来标示,三个状态依次为S等于零,S等于一,S等于二,这一点类似于置标志位,只不过标志位只有零和一,而状态机是多标志位状态的一种方式啊。然后执行流程是最开始S等于零,收到一个数据,进中断,根据S等于零进入第一个状态的程序。判断数据是不是包头FF,如果是FF,则代表收到包头,之后置S等于1,退出中断结束。
这样下次再进中断,根据S等于1就可以进行接收数据的程序来,那在第一个状态,如果收到的不是FF就证明数据包没有对齐,我们应该等待数据包包头的出现。这时状态就仍然是你下次进中断就还是判断包头的逻辑,直到出现反复,才能转到下一个状态。那之后出现了FF,我们就可以转移到接收数据的状态,这时再收到数据,我们就直接把它存在数组中,另外再用一个变量记录收纳多少个数据。如果没收够四个数据就一直是接收状态,如果收够了就是S等于2,下次进中断时就可以进入下一个状态了。那最后一个状态就是等待包尾了,判断数据是不是fe,正常情况应该是fe,这样就可以置S等于0回到最初的状态,开始下一个轮回。
那也有可能这个数据不是FE,比如数据和包头重复,导致包头位置判断错了,那这个包尾位置就有可能不是FE。这时就可以进入重复等待包尾的状态,直到接收到真正的包尾。这样加入包尾的判断更能预防因数据和包头重复造成的错误。
这就是使用状态机接收数据包的思路。这个状态机其实是一种很广泛的编程思路,在很多地方都可以用到,使用的基本步骤是先根据项目要求定义状态,画几个圈儿。然后考虑好各个状态在什么情况下会进行转移,如何转移,画好线和转移条件,最后根据这个图来进行编程。这样思维就会非常清晰了,比如你要做个菜单,就可以用到状态机的思维,按什么键,切换什么菜单,执行什么样的程序。还有一些芯片内部逻辑也会用到状态机,比如芯片,什么情况下进入待机状态,什么情况下进入工作状态,这也是状态机的应用,希望大家可以研究一下,对你的编程肯定会有帮助。
那接下来继续我们来看一下这个可变包长文本数据包的接收流程。同样也是利用定义三个状态,第一个状态,等待包头,判断收到的是不是我们规定的@符号儿,如果收到@就进入接收状态,在这个状态下依次接收数据。同时这个状态还应该要兼具等待包尾的功能,因为这是可变包长,我们接收数据的时候也要时刻监视是不是收到包尾了,一旦收到包尾了就结束。那这里这个状态的逻辑就应该是收到一个数据,判断是不是\r,如果不是就正常接收,如果是则不接收,同时跳到下一个状态,等待包尾,判断下一个是不是\n。因为我这里数据包有两个包位,\r,\n,所以需要第三个状态,如果只有一个包尾,那在出现包围之后就可以直接回到初始状态了啊,只需要两个状态就行,因为接收数据和等待包尾需要在一个状态里同时进行。由于串口的包头包尾不会出现在数据中,所以基本不会出现数据错位的现象,这就是使用状态机接收文本数据包的方法。
代码:
//发送数据包的函数
uint8_t Serial_TxPacket[4]; //FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);//发送包头
Serial_SendArray(Serial_TxPacket, 4);//发送数据
Serial_SendByte(0xFE);//发送包尾
}
中断函数配置:当符合要求的时候就会进入中断。
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if (RxState == 0)
{
if (RxData == 0xFF)
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
if (pRxPacket >= 4)
{
RxState = 2;
}
}
else if (RxState == 2)
{
if (RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
然后单片机与电脑的串口助手实现相应的发送以及接收,即便是发送与包头相同的数据的时候,也没有影响,因为发送数据部分的时候,是不检查是否与包头相同的。
但是存在一个问题,就是读入数据存储位置以及读出数据存储位置是一个数组,这就会导致有时候数据发送以及接收速度不一样,数据包可能会混在一起。比如读取太慢了,前面两个数据读取的是上一个数据包,过了一段时间又读取了,就变成下一个数据包的内容了。
解决办法,可以在接收部分加入判断,就是在每一个数据包读取处理完毕之后,再接收下一个数据包,HEX数据包多是传输各种传感器的独立数据,比如陀螺仪的XYZ轴数据,以及温湿度传感器数据,相邻数据包的数据具有连续性,即便是相邻数据搞错了,也没什么问题。
相关代码是:串口收发HEX数据包:总体代码逻辑就是电脑在发送区发送数据单片机会显示接收到的数据,单片机发送数据,单片机会显示发送数据,然后电脑会在接收区显示。
串口收发文本数据包:
根据这个逻辑编写:以下代码的逻辑就是,电脑发送区发送指令控制单片机的灯亮灭
.c文件文章来源:https://www.toymoban.com/news/detail-776080.html
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
char Serial_RxPacket[100]; //"@MSG\r\n" 定义字符串数组
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;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
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);
}
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if (RxState == 0)
{
if (RxData == '@' && Serial_RxFlag == 0)
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
if (RxData == '\r')
{
RxState = 2;
}
else
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
}
}
else if (RxState == 2)
{
if (RxData == '\n')
{
RxState = 0;
Serial_RxPacket[pRxPacket] = '\0';
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
main文件文章来源地址https://www.toymoban.com/news/detail-776080.html
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include "string.h"
int main(void)
{
OLED_Init();
LED_Init();
Serial_Init();
OLED_ShowString(1, 1, "TxPacket");
OLED_ShowString(3, 1, "RxPacket");
while (1)
{
if (Serial_RxFlag == 1)
{
OLED_ShowString(4, 1, " ");//清除第四行的数据
OLED_ShowString(4, 1, Serial_RxPacket);
if (strcmp(Serial_RxPacket, "LED_ON") == 0)
{
LED1_ON();
Serial_SendString("LED_ON_OK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "LED_ON_OK");
}
else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)
{
LED1_OFF();
Serial_SendString("LED_OFF_OK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "LED_OFF_OK");
}
else
{
Serial_SendString("ERROR_COMMAND\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "ERROR_COMMAND");
}
Serial_RxFlag = 0;
}
}
}
到了这里,关于【STM32】标准库 USART通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!