通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
通信接口区别
| 名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 |
|---|---|---|---|---|---|
| USART | TX、RX | 全双工 | 异步 | 单端 | 点对点 |
| I2C | SCL、SDA | 半双工 | 同步 | 单端 | 多设备 |
| SPI | SCLK、MOSI、MISO、CS | 全双工 | 同步 | 单端 | 多设备 |
| CAN | CAN_H、CAN_L | 半双工 | 异步 | 差分 | 多设备 |
| USB | DP、DM | 半双工 | 异步 | 差分 | 点对点 |
以下是一些常见概念:
- 双工:通信设备是否同时能进行双方通信,一般全双工的都有两根通讯线,如 USART 就有TX/RX这两根通讯线。 I2C 只有一根通讯线SDA(另外一根是时钟线),所以 I2C 半双工的通信
- 时钟:分为同步异步
同步:有单独的时钟信号线,保证通讯时用的是同一个时钟
异步:没有单独的时钟信号线,只能双方规定制定的时钟频率。如串口发送方中可以指定波特率来实现间隔多少时间向TX发送一个数据(变化一次高低电平),那么接收方也要根据这个波特率来接收数据(间隔多长时间读取一次RX的电平) - 电平:
单端:电平参考需要一样,即如串口通讯中,发送方与接收方需要共地,保证它们的参考电压是一样的。
差分:不需要共同的参考电平,是根据两根通讯线的电平差异来获取结果的。如两根通讯线的电平不同则表示结果0,相同则表示结果1。 - 电平标准:
即通讯协议中,双方数据1和0的表达方式标准。即传输过程中人为规定电压与数据的对应关系,常用的有以下3种,抗干扰性RS485 > RS232 > TTL- TTL电平:+3.3V或+5V表示1,0V表示0
- RS232电平:(-3 , -15V)表示1,(+3 , +15V)表示数据0
- RS485标准:使用的不是绝对电压,而是两根信号线的相对电压(差分信号)作为标准。两根线电压差(+2, +6V)表示1,相差(-2, -6V)表示0。抗干扰信号非常强,距离可达到上千米
- 当电平标准不一致时,需要加电平转换芯片
- 波特率
用于指定发送的频率和接收的频率,假如发送方1秒发送1位,那么接收方也必须1秒接收(读取一次RX的电平)1位,假如接收方频率更快,那么有可能相同一个数据被接收方多次接收。其单位是bps,即1秒发送的位数。1000bps就是1秒发送1000位数据,1位数据的发送耗时是1ms(二进制下,1位=1baud) - 数据模式
- HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
比如
- HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
| Hex | 字符 | 解释 |
|---|---|---|
| 0x41 | A | 大写字母A |
| 0x0D | (CR) | 回车符(\r) 回到当前行的开头 |
| 0x0A | (LF) | 换行符(\n) 另起一行 |
- 文本模式/字符模式:以原始数据编码后的形式显示
- 如果要显示汉字,就得制定汉字的字符集如GB2312、GBK,另外Unicode字符集:全球的语言,最常用的传输形式是UTF8
一、串口通信
1、参数
- 波特率
- 1位起始位(标志一个数据帧的开始,固定为低电平)
- 1位停止位(标志一个数据帧的结束,固定为高电平)一般停止位为1位,也可设置为0.5、1、1.5、2,停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
- 8/9位数据位(是否奇偶校验,如偶校验:第9位保证9位数据中1的个数为偶数),数据低位先行
因此,在无奇偶校验的情况下一帧的数据长度为10位。
2、串口通信理解
它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。 使用多缓冲器配置的DMA方式,可以实现高速数据通信。

-
发送数据时,Stm32会先判断TXE(Transmit Buffer Empty)信号,假如为1,表示发送数据寄存器
TDR寄存器(只写寄存器)为空。此时STM32写入一帧数据到TDR上,并设置TXE为0,写入后系统会
判断移位寄存器是否正在移位,假如没有在移位说明前一帧数据已经发送完成。数据寄存器TDR寄
存器的数据会全部转移到发送移位寄存器中,然后数据数由数据移位寄存器按照波特率传输出去(即
按波特率设置TX引脚的电平)。与此同时,会设置TXE为1,发送数据寄存器TDR寄存器也会写入新的
数据。也就是第一帧从TDR寄存器转移到移位寄存器后,马上设置TXE为1。这时候同时会发生两件
事情:- 移位寄存器负责一位位发送第一帧的数据
- stm32读取到TXE为1,会把第二帧数据写入到TDR上。
根据TXE信号判断是否写TDR->写入根据发送移位寄存器的状态是否转移数据->发送移位寄存器发送数据+再次写TDR -
数据接收时,接收移位寄存器会一位位的从RX引脚读取数据(按照波特率的频率读取RX引脚的电平) ,假如读取完一帧后,数据会被转运到接收数据寄存器RDR中,转运完成后会设置RXNE标志位。stm32读取到这个标志位后就把RDR寄存器的数据读走。同时接收移位寄存器会再次读取下一帧数据。
接收移位寄存器接收数据->数据转移到RDR中>转移完成则读取RDR,接收移位寄存器再次接收数据 -
下图为数据手册里的参考图,主要注意3部分:数据读写部分、中断、时钟
发送和接收的波特率由波特率寄存器BRR里的DIV决定,而波特率由内部时钟72MHZ分频得到的。所以DIV的值需要满足一下公式 波特率 = fPCLK2/1 / (16 * DIV),所以假如设置3600的波特率,那么DIV就需要设置成
72000000/3600/16 = 468.75。这个是带小数的值,会被写两个寄存器。

-
Hex数据包
实际应用中,往往不可能是单个字节的,一般都是多个字节组合成的数据包。
数据包根据长度是否固定分为固定包长和可变包长:固定包长的接收利用数据包的首尾有效载荷以及已经接收到的数据个数。可变包长利用数据包的首尾有效载荷,注意首尾有效载荷不能出现在数据中。

利用状态机的思维编写接收过程的代码。
3、利用CubMX配置串口通信
配置只需要一步,关键掌握如何收发各种格式的数据。

发送数据
//1、发送一个字节
void hhSerialSendByte(uint8_t Byte){
HAL_UART_Transmit(&huart1, &Byte, 1, HAL_MAX_DELAY);
}
//2、发送一个数组
void hhSerialSendArray(uint8_t *Array,uint16_t Length){
for(uint16_t i=0;i<Length;i++){
hhSerialSendByte(Array[i]);
}
}
//3、发送一个字符串
void hhSerialSendString(char * mString){
for(uint16_t i=0;mString[i]!='\0';i++){
hhSerialSendByte(mString[i]);
}
}
//4、发送一个数字
uint32_t Serial_Pow(uint32_t X, uint32_t Y){
uint32_t Result = 1;
while (Y --){
Result *= X;
}
return Result;
}
void hhSerial_SendNumber(uint32_t Number, uint8_t Length){
uint8_t i;
for (i = 0; i < Length; i ++)
{
hhSerialSendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
//5、发送浮点数 前设置
#include <stdarg.h>
#include "stdio.h"
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
hhSerialSendString(String);
}
接收数据
1、查询法接收
在前面配置好USART的波特率等信息后,循环不断查询是否有数据传输过来
uint8_t ByteRecv;
int main(void){
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
OLED_Init();
OLED_Clear();
while (1){
HAL_UART_Receive(&huart1, &ByteRecv, 1, HAL_MAX_DELAY);
hhSerialSendByte(ByteRecv);
}
}
2、中断法接收
开启中断,串口中断里读取数据。
//1.单字节发送
void hhSerialSendByte(uint8_t Byte){
HAL_UART_Transmit(&huart1, &Byte, 1, HAL_MAX_DELAY);
}
uint8_t Serial_RxFlag;
uint8_t Serial_GetRxFlag(void){
if (Serial_RxFlag == 1){
Serial_RxFlag = 0;
return 1;
}
return 0;
}
uint8_t ByteRecv;
//接收中断函数
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if (huart == &huart1){
Serial_RxFlag=1;//已接收标志位,说明已经接收完一次
HAL_UART_Receive_IT(&huart1, &ByteRecv, 1);//接收了一次后需要再次打开接收中断为下次中断接收做准备
}
}
int main(void){
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
OLED_Init();
OLED_Clear();
HAL_UART_Receive_IT(&huart1, &ByteRecv, 1);//启动中断接收一个字节
OLED_ShowString(1, 1, "RxData:");
while (1){
if (Serial_GetRxFlag() == 1){
hhSerialSendByte(ByteRecv);//将接收到的数据重新发送返回给电脑串口
OLED_ShowHexNum(1, 8, ByteRecv, 2);
}
}
}
轮询模式和中断模式
HAL_UART_Transmit():串口发送数据,使用超时管理机制
HAL_UART_Receive(): 串口接收数据,使用超时管理机制
HAL_UART_Transmit_IT():串口中断模式发送
HAL_UART_Receive_IT(): 串口中断模式接收
轮询模式 其用于在没有中断机制或DMA机制的情况下,主动等待并处理外设的状态变化。在轮询模式下,CPU不断地检查外设的状态寄存器,以确定是否有数据可供处理。这种方式简单易用,但效率较低,因为CPU在等待期间不能处理其他任务。
二、SPI通信
1、基本概念
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线,同步,全双工 ,支持总线挂载多设备(一主多从),有四根通信线:
SCK(Serial Clock)、MOSI(Master Output Slave Input)MISO(Master Input Slave Output)SS (SDA)(Slave Select,每个从机都有一根线)
所有SPI设备的SCK、MOSI、MISO分别连在一起,主机另外引出多条SS控制线分别接到各从机的SS引脚,MOSI输出引脚配置为推挽输出,、MISO输入引脚配置为浮空或上拉输入。

主机和从机先在SCK的控制下,都同时先移出数据,然后同时移入数据,实现数据交换
- 与IIC相比:
- SPI传输速度更快,可以达80M或更大
- SPI硬件开销大,需要4根线,I2C只需要2根
- SPI只支持一主多从模式,I2C支持一主多从和多主多从的模式
- SPI没有应答机制,I2C有应答机制
- I2C:读取寄存器地址+数据, SPI:指令码+读写指令的模型
2、SPI通信时序
通信起始停止信号:SS 下降沿选择了某个从机,通信过程中一直保持低电平;SS上升沿,通信结束。
通信过程中,根据SPI_CR 控制寄存器里的CPOL(Clock Polarity 时钟极性) ,CPHA(Clock Phase 时钟相位),这两者的配置,交换数据有4种模式。
CPOL:控制空闲状态SCK的电平,如CPOL=0,也就是无通信时,SCK为低电平。
CPHA:控制SCK变化时,主机从机先移入数据还是移出数据。如CPHA=1,SCK第一个边沿移出数据,第二个边沿移入数据。CPHA=0,SCK第一1个边沿移入数据(SS下降沿时移出的数据),第二个边沿移出数据。
模式1 CPOL=0,CPHA=1
模式0 CPOL=1,CPHA=0,最常用
三、IIC通信
IIC通信:一种半双工通信,有SCL、SDA两根通信线,可以一主多从。
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式,所有引脚都禁止输出强上拉的高电平
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
IO输出模式下也可以读取输入,因为输出输入寄存器不一样。输入电平是由输出电平和io外部电平一起决定,
同时读取SDA前要释放这根线把SDA拉到高电平,因为线与特性避免影响外部数据的写入。协议定的好
一、时序基本单元
IIC组成部分:起始和停止单元 发送、接收单元 接收、发送应答
1、起始、停止单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平(数据线SDA下降沿)
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平(SDA上升沿)结束时读取
SCL低电平期间,主机放数据到SDA(高位先行,串口低位先行),释放SCL控制权(SCL为高电平),SCL高电平期间主机读取数据.循环上述过程8次,即可发送或接收一个字节. 因此SCL高电平期间数据也不能变化
2、发送单元
SCL低电平期间,放数据到SDA(高位先行,串口低位先行),释放SCL控制权(SCL为高电平),循环上述过程8次,即可发送一个字节.

3、接收单元
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位

4、发送、接收应答
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA,从机应答后再释放SDA,因此SDA最终会有一小段的高电平)
二、IIC时序
1、 指定地址写
对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

2、 当前地址读(不常用)
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

3、指定地址读
- 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
- 主机如果想要读取多个字节,就需要在最后一个字节给一个非应答,此时从机就会释放SDA;否则如果主机应答,从机认为主机还要继续,就继续发送

三、软件实现IIC
对SCL写、SDA读写函数、配置I2C IO口
#include "stm32f10x.h"
#include "Delay.h"
//对SCL写
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10);
}
//对SDA写
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
//对SDA读
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
//初始化GPIO
void MyI2C_Init(void)
{
//打开GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//初始化为开漏输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//初始时都设置成高电平,因为I2C空闲时两根线都是高电平
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}
1、起始条件
//起始或重复起始位
void MyI2C_Start(void)
{
//为了兼容重复起始位,这里需先将SDA拉高。假如SCL先拉高,而此时的SDA为低电平(由前时序决
定),将SDA拉高,就会造成SCL高电平期间SDA上升沿。从机会误判为终止条件
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
//起始条件后SCL为低电平
MyI2C_W_SCL(0);
}
2、主机发送字节
//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
//1.发送前SCL是低电平
uint8_t i;
for (i = 0; i < 8; i ++)
{
//2.SCL低电平时SDA放数据
MyI2C_W_SDA(Byte & (0x80 >> i));//0x80的二进制十1000 0000
//3.SCL设置高高电平,让从机读取SDA的数据
MyI2C_W_SCL(1);
//4.SCL再设置成低电平(所以发完数据后SCL仍然是低电平)
MyI2C_W_SCL(0);
}
}
3、主机接收应答
uint8_t MyI2C_ReceiveAck(void)
{
//1.接收前SLC是低电平
uint8_t AckBit;
//2.主机释放SDA,从机检测到后会把数据放在SDA上
MyI2C_W_SDA(1);
//3.主机设置SCL为高电平
MyI2C_W_SCL(1);
//4.主机读取SDA
AckBit = MyI2C_R_SDA();
//5.主机再设置成低电平(所以收完数据后SCL仍然是低电平)
MyI2C_W_SCL(0);
return AckBit;
}
4、主机接收字节
//接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
//1.接收前SLC是低电平
uint8_t i, Byte = 0x00;
//2.主机释放SDA,从机检测到后会把数据放在SDA上
MyI2C_W_SDA(1);
for (i = 0; i < 8; i ++)
{
//3.主机设置SCL为高电平
MyI2C_W_SCL(1);
//4.主机读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);
}
//5.主机再设置成低电平(所以收完数据后SCL仍然是低电平)
MyI2C_W_SCL(0);
}
return Byte;
}
5、主机发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
//1.发送前SCL是低电平
//2.SCL低电平时SDA放数据
MyI2C_W_SDA(AckBit);
//3.SCL设置高高电平,让从机读取SDA的数据
MyI2C_W_SCL(1);
//4.SCL再设置成低电平(所以发完数据后SCL仍然是低电平)
MyI2C_W_SCL(0);
}
6、终止条件
SCL高电平期间,SDA上升沿(从低电平切换到高电平)
void MyI2C_Stop(void)
{
//SDA在结束前的电平时不确定的,由前时序决定。假如前时序输出的是高电平,那么就做不了结束条件
(SCL高电平期间SDA上升沿)。所以这里需要先降SDA拉低
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
1万+

被折叠的 条评论
为什么被折叠?



