type
slug
status
summary
icon
category
date
tags
password
两个程序:
- 串口发送:串口的模块,通过串口通信,把一些数据发送到电脑上的串口助手来显示。
- 串口发送+接收:判断是否收到数据,如果收到数据,则读取数据。将数据回传到电脑。在OLED显示。
注意:在串口中,只能发送二进制数/十六进制数。发送字符,就需要一个数据到字符的映射表。ASCII码表。
eg:发送41。 以HEX模式显示,显示41;以文本模式显示,就会去查找映射表(0x41=65,对应ASCII码表的A)
通信接口

- 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
- STM32芯片中集成了很多功能模块,eg:定时器计数、PWM输出、AD采集……即读写操作芯片内部的寄存器。
- 很多芯片没有的功能eg:蓝牙无线遥控功能、陀螺仪加速计测量姿态……只能外挂芯片来实现。
- 外挂的芯片都在STM32外面。STM32如何获得这些数据呢?在设备之间连接一根/多根通信线路发送和接收数据,完成数据交换,从而实现控制外挂模块和读取外挂模块数据的目的。
- 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
USART:
- 引脚TX、RX(TXD、RXD)
- TX(Transmit Exchange):数据发送脚。RX(Receive Exchange):数据接收脚
I2C:
- 引脚:SCL、SDA
- SCL(Serial Clock):时钟。SDA:(Serial Data):数据
SPI:
- 引脚:SCLK、MOSI、MISO、CS
- SCLK(Serial Clock)时钟、MOSI(Master Output Slave Input)主机输出数据脚、MISO、CS(Chip Select)片选:指定通信的对象
CAN:
- 引脚:CAN_H、CAN_L
- 差分数据脚,用两个引脚代表一个差分数据
USB:
- 引脚:DP、DM
- DP(Data Positive)(D+),DM(Data Minus)(D-)。差分数据脚
双工
全双工通信:两根通信线,发送线路和接受线路互不影响。
- eg:串口:一根TX发送,一根RX接收。SPI:一根MOSI发送,一根MISO接收。
半双工通信:一根通信线。
- (I2C\CAN\USB,CAN和USB的两根差分线也是组合成一根数据线的。)
单工通信:数据只能从一个设备到另一个设备,不能反过来。
- 串口的RX去掉,就变成单工了。
时钟
eg:当发送方发送一个高低电平,接收方不知道发送的是(1和0)还是(1100)还是……
所以需要时钟信号让接收方知道:什么时候需要采集数据
同步通信:
- I2C和SPI有单独的时钟线,所以他们是同步的,接收方可以在时钟信号的指引下进行采样。
异步通信:
- 剩下的串口\CAN\USB没有时钟线,需要双方约定一个采样频率。并且需要加一些帧头帧尾等,进行采样位置的对齐。
电平特性
单端:
- 引脚的高低电平都是对GND的电压差。单端通信的双方必须要共地(把GND接在一起)。(USART\I2C\SPI引脚里面还需要接一个GND引脚)
差分:
- 靠两个差分引脚的电压差来传输信号的。通信的时候可以不需要GND。使用差分信号可以极大地提高抗干扰性,所以常被用来实现高速度和远距离通信。
设备
点对点通信:
- 双方直接传输数据就好
多设备通信:可以在总线上挂载多个设备
- 需要寻址以确定通信对象
串口通信

单片机和电脑通信是串口通信的优势,常用于接电脑屏幕,调试打印信息。(I2C和SPI等是芯片之间的通信,不会接在电脑上)
USB转串口模块:芯片CH340,将串口协议转化成USB协议。一口接电脑,一口接支持串口通信的芯片的引脚。
陀螺仪传感器模块,可以测量角速度、加速度等姿态参数。
蓝牙串口模块:下面引脚是串口通信的引脚,上面的芯片和手机互联。
硬件电路

- VCC和GND是供电,TX和RX是单端信号,即高低电平是相对于GND
- TX\RX\GND必须要接,如果两个设备都有独立供电,VCC可以不接。如果设备1为STM32有独立供电,设备2为蓝牙模块,没有独立供电。那么需要把STM32的VCC和蓝牙模块的VCC接在一起。STM32通过VCC线向设备2供电,注意电压。
- 如果只需要单向通信eg:设备1到设备2,那么只用接设备1的TX到设备2的RX即可。
- 一般为TTL电平,不同电平信号需要加电平转换芯片,转接一下。
- 协议规定:一个设备使用TX发送高低电平,一个设备用RX接受高低电平。3.3vTTL电平,相对GND为逻辑1。0v为逻辑0。
电平标准
电压和数据的对应关系。

逻辑1代表VCC,逻辑0代表GND。
串口参数及时序

- 时序图:串口发送一个字节的格式。
- 串口中每一个字节都装载在一个数据帧里面,每一个数据帧都由起始位、数据位、停止位组成。
- 数据位8位(即一个字节),数据位9位(8位有效载荷(1字节)+1位奇偶校验位)
- 波特率:串口通信的速率。串口使用异步通信,需要双方约定一个通信速率。eg:我每隔1s发送一位,你每隔1s接收一位。
- 比特率:每秒传输比特数,bit/s,bps,在二进制调制下:一个码元为一个bit,高电平为1,低电平为0,波特率=比特率
- eg:约定波特率为1000bps,1s要发1000位,每一位就是1ms。
- 起始位:表示一个数据帧的开始,固定为低电平。
- 串口空闲状态,引脚为高电平。需要传输时,必须要发送一个起始位(低电平),产生下降沿,告诉接收设备,这个数据帧要开始了。
- 停止位:表示数据帧间隔,固定为高电平。同时为下一个起始位做准备。如果没有数据了,空闲状态也是高电平。
- 如果没有停止位,当我数据的最后一位是0的时候,就无法产生下降沿。
- 数据位:低位先行。
- 发送0x0F,转化成二进制为00001111,低位先传输。高高高高低低低低。形成波形如下图。如果需要发送0x0F,就需要设置定时翻转引脚电平,产生这样的波形。

- 校验位:三种方式:无校验、奇校验、偶校验。判断数据传输是否出错,出错选择丢弃或重传。
- 波形组成:起始位,数据位,校验位,停止位。
- 奇校验:包括校验位在内的9位数据会出现奇数个1。
- eg:00001111,那么校验位需要再补充一个1,实现奇数个1。
- eg:00001110,那么校验位需要补充一个0。
- 发送方在发送数据后会补充一个校验位,保证1的个数为奇数。接收方会验证数据中1的个数判断传输是否出错。如果在传输过程中,因为干扰,一位1变为0/0变为1,则数据出错,要求丢弃或重传。
- 偶检验:包括校验位在内的9位数据会出现偶数个1。
- 准确性更高的校验:CRC校验。


- 0x55:01010101
- 波特率:9600,即一秒发送9600位,每一位就是1/9600s,大概是104us
- 波特率变为4800,波形时长就会变成原来的两倍。
- 波形解析:
- 空闲状态为高电平,数据帧开始,先发送起始位产生下降沿,表示数据开始。
- 数据转化为01010101,低位先行。10101010
- 8位数据1位停止无校验:8位数据之后引脚置为高电平。表示一个数据帧完成
- 停止位的位数是可以发生变化的:
- 连续发生两个0x55,停止位2位比1位数据分隔得更宽。

- TX发送数据的两种方式:
- 在STM32中,根据字节数翻转高低电平,是由USART外设自动翻转。
- 也可以软件模拟产生波形。定时器定一个104us的时间,根据数据帧要求,调用GPIO_WriteBit设置高低电平,进行高低电平的翻转。
- RX接收数据的两种方式:
- USART外设自动
- 软件模拟:定时调用GPIO_ReadInputDataBit来读取每一位。拼接为一个字节。
- 接收时,需要一个外部中断,在起始位的下降沿触发,进入接收状态,对齐采样时钟,依次采样8次。
总结:
TX定时输出高低电平。
RX定时接收高低电平。
USART外设

- USART:通用同步/异步收发器,UART:通用异步收发器。
- 同步模式多了个时钟输出。只支持时钟输出,不支持时钟输入。同步模式更多是为了兼容其他协议/特殊用途。并不支持两个USART之间同步通信。所以一般看来USART和UART没有区别。
- 串口主要通过波形来收发信息,USART外设就是串口通信的硬件支持电路。
- 当我们配置好USART电路,直接读写寄存器,就可以自动发送和接收数据了。
- 波特率发生器:即一个分频器,eg:APB2总线72MHz的频率。然后波特率发生器进行一个分频,得到我们想要的波特率时钟,在这个时钟下进行收发,即我们指定的通信波特率。
- 同步模式:多一个CLK时钟线。
- 硬件流控制:防止B设备因为处理数据慢而导致的数据丢失问题。 硬件电路上多出一根线,A设备向B设备高速发送数据。如果B没准备好接收,就置高电平。如果准备好了就置低电平。A接收到B反馈的准备信号,就只会在B准备好的时候才发送数据。
- STM32F103C8T6一共有三个USART外设资源,USART1是APB2总线上的设备,USART2和USART3是APB1总线上的设备。
USART框图

- 程序上只表现为一个寄存器,数据寄存器:DR。实际硬件里面有两个寄存器:TDR/RDR(一个用于发送,一个用于接收。TDR只写,RDR只读。)
- 发送移位寄存器/接收移位寄存器:将数据一位一位发送/接收,正好对应串口协议的波形数据位。
- 发送移位寄存器
- 工作原理:如果给TDR写入0x55数据,寄存器里为01010101。硬件检测到数据写入,就会检查当前移位寄存器是否有数据正在移位,如果没有:01010101就会全部移入发送移位寄存器准备发送。当数据从TDR移入发送移位寄存器时,会置一个标志位(TXE:TX Empty)。如果置1,那么就可以在TDR写入下一个数据了。
- 发送移位寄存器就可以在下面的发生器控制的驱动下,一位一位将数据输入到TX引脚。向右移位和串口协议规定的低位先行一致。
- 两个寄存器缓存,有利于连续接收。
- 接收移位寄存器:
- 在接收器控制的驱动下一位一位接收高低电平,向右移位。当接收8位后,将数据整体传给RDR。转移过程中也设置一个标志位RXNE(RX Not Empty)(接收数据寄存器非空)当我们检测到RXNE置1,就可以把数据读走了。
- SCLK控制:配合发送移位寄存器输出。每移一位,同步时钟电平就跳变一个周期。
- 中断控制:配置中断能不能通向NVIC
- 中断控制位

- 波特率发生器
- fPCLKx:APB1/APB2

串口引脚(F103)
引脚复用以及重映射




USART基本结构

时钟来源:PCLK2/1,波特率发生器分频,产生必要的时钟(通信速率)发送给发送接收器和接收控制器。
软件层面只有一个DR寄存器供我们读写。
数据帧
- 字长设置

最好选择9位字长有校验,8位字长无校验(保证一个字节的数据)。
- 配置停止位
- 一位停止位时长=一位数据位时长

- 采样
- 采样的高电平在每一位的正中间,使得读入的高低电平是稳定的。过前或过后会使得可能高低电平正在翻转导致采样误差。
- 侦测到一个起始位后,就会以波特率的频率,连续采样一帧数据。起始位开始,采样位置就要对齐到位的正中间,后面就会都对齐正中间了。


一个数据位有16个采样时钟,起始位侦测对齐采样时钟。直接在8,9,10位采样数据(即在该位的正中间采样)连续采样三次,全为1,就为1;全为0,就为0。其他以2:1的标准来执行,同时噪声标志位NE置1.

波特率发生器

- DIV分为整数部分和小数部分
- /16是因为还有一个16倍波特率的采样时钟,所以输入时钟/DIV=16倍的波特率。计算波特率就得/16
- eg:USART1波特率为9600,9600=72M/(16*DIV),解得DIV=468.75,转化为二进制111010100.11数据写入寄存器。多出部分补0,
- 库函数只用输入波特率,会自动帮我们转换。

USB转串口原理图

经过CH340转换,输出为TXD和RXD,串口协议。
注意供电图:USB为5V供电,经过稳压电路也有3.3v。都引出。其中5号引脚为CH340VCC,是CH340的电源输入脚。

状态寄存器SR(存放状态)/数据寄存器DR(存放最关键的数据)/配置寄存器CR(存放各自配置参数)
代码1:串口发送
接线图


- 跳线帽接VCC:3.3V和VCC,两个设备要把GND接在一起,进行共地。
- TX和RX交叉连接:TXD接板子的RX即PA10,RXD接板子的TX即PA9
- 串口模块和STLINK都要插在电脑上,保证两个模块都有独立供电。
- 保证串口模块的驱动已安装

库函数
(很多事增强功能的函数,需要这个功能再去了解相关的函数)



SendData:写DR寄存器,RecieveData:读DR寄存器
如何发送和接收,软件一律不管。

标志位相关的函数。
代码思路
串口初始化:
第一步:开启时钟,将USART和GPIO的时钟都打开。
第二步:GPIO初始化,将TX配置成复用输出,将RX配置为输入。
第三步:配置USART,直接使用一个结构体。
第四步:如果只需要发送功能,直接开启USART。如果既需要发送又需要接收功能,就还得配置一下中断。
接收数据:调用接收数据的函数
发送数据:调用发送数据的函数
获取发送和接收的状态:调用获取标志位的函数
代码分析

- 第一步:开启时钟,将USART和GPIO的时钟都打开。
- 第二步:GPIO初始化,将TX配置成复用输出,将RX配置为输入。
- TX引脚是USART外设的数据输出脚,选用复用推挽输出。
- RX引脚是USART外设的数据输入脚,选择输入模式。输入模式并不分普通/复用输入。一根线只能有一个输出,但可以有多个输入。所以输入脚外设和GPIO可以同时用。RX配置是浮空输入/上拉输入
- 第三步:配置USART,直接使用一个结构体。
- 当写入波特率后,该函数会自动帮我们算好分频系数等,写入BRR寄存器
- 定义一个结构体,将所有参数引出,将结构体的地址传入初始化。
- 复制名称,crtl+alt+空格选择
- 选择模式,如果即想要发送又需要接收,那就USART_Mode_RX|USART_Mode_TX(或符号连接),和GPIO_Pin_2|GPIO_Pin_1用法相同
- 选择校验位
- 选择停止位
- 选择字长(8位/9位)






第四步:如果只需要发送功能,直接开启USART。

接收数据:调用接收数据的函数

发送到TXD后,等待TXE置1(即数据移动到移位寄存器后),才又写入数据(否则会发生数据覆盖)
条件循环等待:TXE==RESET,等待。TXE=SET,循环结束,结束等待。
标志位自动清0(标志位置1后不需要手动清0)





HEX模式:以原始数据的形式显示
文本模式:显示字符串,41数据解析成A
数据模式

HEX模式:只能显示一个个十六进制的数据,不能显示文本/符号
显示文本,需要对一个个数据进行编码,查找字符集,。

空字符:对应原始数据是0x00,保留位,不映射任何字符。一般作为字符串的结束位,字符串遇到数据0x00后,就代表字符串结束了。
想显示和存储汉字,也得制定汉字的字符集(GB2312\GBK\GB18030)
将全球的字符收录到一个统一的字符集Unicode字符集,最常用的传输形式是UTF8

字符和数据在发送和接收的转换关系。线路中传输的必须是十六进制数,0x41。
Serial_SendArray
注意:
- 第一个参数为数组的首地址,传递一个指针。第二个参数为length,可以根据长度定义为uint16_t,uint32_t。
- 遍历数组(循环length次)。不断调用Serial_SendByte(Array[i])。
main.c

- 一般使用HEX模式
Serial_SendString
注意:
- char * == uint8_t *
- 字符串自带一个结束标志位,所以就不需要传递长度参数了。这里的数据0,对应刚刚所说的空字符,是字符串结束的标志位。也可以写成字符的形式’\0’(空字符的转义字符)
- 头文件声明
main.c
注意:
- 字符串用双引号,写完这个字符串之后,编译器会自动补上结束标志位。所以字符串的存储空间=字符串长度+1;
- 一般用文本模式
- 想要换行:转义字符\r\n

Serial_SendNumber
注意:
- 在函数内我们需要把Number的位数按十进制分开。
- eg:12345
- 取万位:12345/10000%10=1%10=1
- 取千位:12345/1000%10=12%10=2
- 取百位:12345/100%10=123%10=3
- 取十位:12345/10%10=1234%10=4
- 取个位:12345/1%10=12345%10=5
- 总结:取x位,即数字/10^x%10(/10^x即将右边x-1位去掉,%10将左边去掉,保留目标位)
- 优先级位图法:prio/8去掉低三位。
- 次方函数
Serial_SendByte(Number / Serial_Pow(10,i) % 10);
i=0,即第一次取出来的是个位- 第一次取出来最高位,即
Length-i-1
; - 因为10000=10^4(即4个0)
- 注意临界情况:第五次循环,i=4,Length-i-1=0,10^0=1,取到个位。
- 修改代码为:
Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) % 10);
- 最终以字符的形式显示,则需要对照ASCII表加上偏移量:
- 字符0对应的数据是0x30,所以+0x30/+’0’
Serial_SendByte(Number / Serial_Pow(10, Length - i -1) % 10 + 0x30);
printf

MicroLIB:Keil精简库
将printf进行重定向,将printf函数打印的东西输出到串口。(printf默认将输出到屏幕,单片机没有屏幕)
在Serial.c里面加入
#include <stdio.h>
,重写printf函数(fputc为printf的底层函数)int ch
:这个参数是你需要输出的字符,它是一个整数类型,但只占用 1 字节存储。字符是以 ASCII 码的形式传递给fputc
,比如字符'A'
对应的 ASCII 值是 65,字符'a'
对应的是 97。
FILE *f
:这个参数是指向FILE
类型的指针。FILE
是标准 I/O 库用来表示文件或设备的结构。在嵌入式开发中,我们不关心它的具体内容,通常传递的是一个NULL
或者一个已经打开的文件句柄(如果你有文件输出的需求)。在重定向printf
的输出时,这个参数实际上不会被用到,但它是标准fputc
函数的一部分。
- 例如,调用
printf("Hello")
后,实际上它会依次调用: - 而每次调用
fputc
时,它会将字符通过串口输出(或其他设备)。如果你没有做任何重定向,默认情况下,fputc
会将字符输出到标准输出(通常是屏幕)。
- main.c:
出现warning:printf隐式声明

解决方案:Serial.h中 #include <stdio.h>(其中有printf的声明,而main.c中又包含串口头文件)
sprintf
sprintf
是 C 标准库中的一个函数,用于将格式化的数据输出到字符串中。与 printf
不同,printf
是将格式化数据输出到标准输出(如终端或屏幕),而 sprintf
则是将格式化的数据写入到一个字符串(字符数组)中。
函数原型:str
:指向要写入格式化数据的字符数组的指针。
format
:格式化字符串,包含普通字符和格式说明符(如%d
、%s
等)。
...
:要格式化的参数,根据格式说明符的数量和类型提供相应的参数。
- 返回值:
sprintf
函数返回写入到字符串中的字符数,不包括字符串结束符\0
。
函数示例:
输出:
关于串口(不涉及重定向的问题,每个串口都可以用springf格式化打印)
再次封装(可变参数)

发送汉字
如果汉字以UTF8的方式编码发送,也要以UTF8的方式接收。同时在c/c++杂项控制栏写入:—no-multibyte-chars

Serial_printf(”你好,世界!”);才不会报错。
Encoding选择GB2312(汉字的编码方式),接收处选择GBK编码
代码2:串口接收
USART_RX复用在了PA10引脚,需要初始化PA10(浮空输入/上拉输入)

同时开启发送和接收的部分

串口接收可以使用查询和中断两种方式
查询方式:
流程:在主函数里面不断判断RXNE标志位,如果置1,就说明收到数据。
再调用RecieveData读取寄存器即可。

清除标志位的问题:

使用中断:
开启中断,配置NVIC

当RXNE一旦置1,就会向NVIC申请中断。

对Serial_RxData和RXFlag封装/extern

串口数据包收发
发送;文本数据包和HEX数据包
HEX数据包

数据包:把一个个单独的数据打包和分割,进行多字节通信。
eg:陀螺仪的X\Y\Z轴数据,需要连续不断的打包发送。出现问题,接收方接收数据时候的分割。X数据置一个标志位,最高位为1。检查标志位,得到X数据,接着的两个数据是Y和Z的数据。

串口数据包的分割方法:额外增添包头和包尾
- 固定包长,含包头包尾:即每个数据包的长度是一样的。
- 四个字节为一个数据包。0xFF为包头,0xFE为包尾。
- 可变包长,含包头包尾:即每个数据包的长度不一样。
Q1:包头包尾和数据载荷重复的问题。(即如果数据是0xFF和0xFE怎么办?)
S1:对数据进行限幅。如XYZ3个数据,范围都在0~100.
S2;尽量使用固定包长的数据。对齐几个数据包之后,之后的数据包也是对齐,只用判断包头包尾而不会判断数据内部。
S3:增加包头包尾的数量,使得它尽量呈现出载荷数据表示不了的状态。
Q2:包长可变性的选择。根据需求和是否会有严重的数据载荷重复问题。
Q3:各种数据转化为字节流的问题。
S:只需要一个uint8_t的指针指向它,当成字节数组发送就可以了。
优点:传输简单,解析数据简单。比较适合一些模块发送原始数据。串口通信的陀螺仪/温湿度传感器。
缺点:灵活性不足,载荷容易和包头包尾重复。
文本数据包

在HEX数据包里,数据都是原始数据。文本数据包中,每个字节都经过了一层编码和译码。其实本质还是一个字节的HEX数据。由于字符作为包头包尾,可以有效避免包头包尾重复和数据载荷的问题。
eg:@作为包头,\r\n换行的字符作为包尾。
优点:数据清晰可见,适合人机交互。蓝牙模块常用的AT指令。有效避免重复和载荷问题。
缺点:需要编码和译码,解析数据低。
HEX数据包接收

发送HEX数据包:SendArray
发送文本数据包:SendString
上图是固定包长的HEX数据包的接收。
每收到一个字节,程序都会进一遍中断。中断函数里,可以拿到一个字节。但是拿到字节后,就需要退出中断。所以具有独立性。但是数据包其实是有前后关联性的。包头之后是数据,数据之后为包尾。—>不同类型数据需要不同的处理方式,还要一个记住不同状态、操作、合理状态转移的机制。
“状态机”

设计状态转换图如下:
第一个状态:等待包头。第二个状态:接收数据。第三个状态:等待包尾。
每个状态需要一个变量来标志。(S=0/S=1/S=2)
流程如下:
- 最开始S=0,收到一个数据进行中断。判断数据是不是包头0xFF。
- 如果是包头0xFF,那么将S=1,退出中断结束。
- 如果不是0xFF,那么数据包头没有对齐。继续等待包头(仍然在状态一,S=0)。下次仍然进中断,判断包头。
- 下次再进入中断,就可以根据S=1,就可以进行第二个状态:数据接收。
- 将数据存入数组。还有一个变量,记录数据的个数。
- 如果没有收够4个数据,那么继续接收数据。
- 收够4个数据,S=2。
- 下次再进入中断,就可以根据S=2,就可以进行第三个状态:等待包尾。判断数据是不是0xFE
- 如果是包尾0xFE,那么将S=0.
- 如果不是,继续等待包尾。
设计状态转换图:状态,转移条件。—>编程。
文本数据包接收

可变包长的文本数据包的接收。
串口收发数据包的代码
HEX发送


main.c中:

HEX接收
接收数据包,将载荷数据存在RxPacket数组里面。然后设置标志位,进行状态变化。
Serial.c
Serial.h
main.c
第4行,1,4,7,10列显示数据
即使出现重复问题时也不会报错,因为接收载荷数据的时候,并不会判断包头包尾。
问题:RxPacket[]在中断函数里被写入,在主函数中被读出。如果数据收发频率出现问题,可能会导致数据混杂。(这个数据包的一部分和下一个数据包的一部分形成一个新的数据包)
解决办法:加入判断。当每个数据包接收处理完毕之后再接收下一个数据包。/传感器的独立数据连续性,即使出现数据混杂那么还是可以分辨的。
文本接收
可变包长,含包头包尾
Serial.c
Serial.h
main.c
发送:@ABC+换行符
屏幕显示ABC
发送:@LED_ON+换行符
回传:LED_ON_OK
如果收发数据包的速度太快了,可以定义一个缓存区进行接收,来解决数据错误的问题。
启动配置和存储器映像


- 作者:🐟🐟
- 链接:https://www.imyuyu.top//article/Quadcopter1-4-1
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。