嵌入式经典通信总线协议

一、串口通信

1、串口通信

串口通信是指外设和计算机间,通过数据信号线 、地线、控制线等,按位进行传输数据的一种通讯方式,如SPI通信、USART通信、EEPROM通信和CAN通信等。简单讲,串口通信实现了上位机(PC)与下位机(如STM32)之间的信息交互

2、串口通信的分类

处理器与外部设备通信有两种方式:

2.1、串行通信:数据按位顺序依次传输(只有一根数据线进行传输),如8个数据位依次传输,速度慢,但占用引脚资源少

按照数据传送方向,又分为:

  • 单工:数据传输只支持数据在一个方向上传输(只收不发或者只发不收,模式固定)

  • 单双工(半双工):允许数据在两个方向上传输,但是在某一时刻,只允许数据在一个方向上传输(能发能收,但不能同时进行)

  • 全双工:允许数据同时在两个方向上传输。(能发能收,且能同时进行)

示例图片

2.2、并行通信:数据各个位同时传输利用多根数据线进行数据传输),如8个数据位同时传输,占用引脚资源多,但速度快,一般会加一根时钟线,用于同步传输

示例图片

3、串行通信的分类

串行通信按通信的方式可分为:

  • 同步通信:带时钟同步信号传输,如SPI、IIC通信等

  • 异步通信:不带时钟同步信号,如UART(通用异步收发器)、USART(通用同步/异步收发器,两种模式可切换)、单总线等

示例图片 示例图片

4、串口通信的缺点

串口通信这种传递高低电平信号(TTL)的方式很微弱,如果直接用这种信号去通讯的话,非常容易收到干扰通讯距离是非常短的,所以为了增强串口通信的抗干扰能力增长通讯距离,一般会加一个电平转换芯片(比如MAX232EPE),将TTL信号转换成232信号

二、RS232(物理层)

1、RS232的由来:在串口通信的基础上,一般会加一个电平转换芯片(比如MAX232EPE),将TTL信号(电平信号)转换成232信号

2、TTL电平中+5V表示逻辑“1”,0V表示逻辑“0”(TTL是正逻辑),RS232电平中-15V ~ -3V表示逻辑“1”,+3V ~ +15V表示逻辑“0”(RS232是负逻辑),RS232电平适合干扰大、距离远的情况,最大传输距离为15米,用于工业上;TTL电平适合距离近且干扰小的情况,一般用在电路板内部的两个芯片之间

通讯标准 电平标准(发送端)
5V TTL 逻辑“1”:2.4V-5V 逻辑“0”:0~0.5V
RS-232 逻辑“1”:-15V — -3V 逻辑“0”:+3V — +15V

2、RS232是DB-9连接器(9根通信线),在工业控制中RS232接口一般只用RXD,TXD和GND三条线

3、RS232抗干扰能力强(提高点平幅度)、传输距离可达15米(比TTL更强)

4、RS232和串口通信只能进行点到点的简单通信(一对一),支持单工、半双工、全双工通信

示例图片

三、RS485(物理层)

1、RS485的由来:在串口通信的基础上,增加了一个485电平转换芯片,把输入的电平信号(TTL信号)转换成差分信号

2、差分信号的传输线中没有参考电平线,所有都是信号线,然后1和0的表达两根信号线之间的电压差。

逻辑 电压范围
逻辑1 2V < A-B <6V
逻辑0 -6V <A-B <-2V
示例图片

3、RS485采用双绞线,这种接线方式为总线式拓扑结构,在同一总线上可以同时存在多个节点。因为采用两线制数据的发送和接收都要使用这对差分信号线发送和接收不能同时进行,所以只能采用半双工的方式工作,编程时也需要加以处理

示例图片

4、RS485抗干扰能力强、通信速度快(传输速度快是因为抗干扰能力强,能采用很高的波特率)、传输距离可达1500米和可实现多节点组网

5、RS485可支持一对多组网,采用半双工的方式工作

示例图片

四、UART(物理层和链路层)

CH340:USB转串口模块,这里的串口就指的是UASRT(TTL电平)

示例图片

img

1、UART特点

  • 即通用异步收发器(Universal Asynchronous Receiver/Transmitter),是一种通用的串行异步通信总线。该总线有两条数据线(TX和RX),可以实现全双工的发送和接收
  • 具体实现可能依赖于底层的物理连接方式(例如RS-232, RS-485)等

2、数据传输的格式(通信基础)

当两个设备使用UART通讯时,双方要事先约定好波特率、数据位、奇偶校验位以及停止位后才能进行数据交互。

一帧串口数据包括以下内容:

1、起始位

当未有数据发送时,数据线处于逻辑“1”状态;先发出一个逻辑“0”信号,表示开始传输字符(数据线电平由高到底表示通信的开始,默认为高电平)

2、数据位

数据位表示真正要发送或接收的信息,位数一般有8位或9位(由低到高)

3、奇偶校验位

数据位末尾可以选择是否添加奇偶校验位,用于检测数据传输是否正确

4、停止位

代表信息传输结束的标志位,可以是1位,1.5位或2位。停止位的位数越多,数据传输的速率也越慢

5、波特率

波特率:描述串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位

约定好帧格式,就需要设置波特率:115200(一秒钟以内,串口可以传输115200个高低电平)、19200、9600

6、通信双方要知道对方的波特率、数据长度、开始位和停止位才可以开始通信

示例图片

3、配置驱动

3.1、裸机(裸核、STM32)

串口设置的一般步骤可以总结为如下几个步骤(利用中断触发):

  1. 串口时钟使能和GPIO 时钟使能
  2. 串口复位
  3. GPIO 端口模式设置
  4. 串口参数初始化
  5. 开启中断 并且初始化 NVIC(如果需要开启中断才需要这个步骤)
  6. 使能串口
  7. 编写中断处理函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//1、串口时钟使能 GPIO 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);

//2、串口复位
USART_DeInit(USART1); // 复位串口 1

//3、GPIO端口模式设置
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9

//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10

//4、串口参数初始化
//USART结构体初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
//使用USART_Init函数对串口进行初始化
USART_Init(USART1, &USART_InitStructure); //初始化串口1

//5、开启中断并且初始化NVIC(这里使用中断触发数据收发)
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断

//6、使能串口
USART_Cmd(USART1, ENABLE); //使能串口1

//7、编写中断处理函数
//数据收发逻辑的编写
UART_WriteByte和UART_ReadByte

参考链接:STM32之USART-串口通信(含串口实验详细解析)_stm32 串口11bit-CSDN博客

3.2、带操作系统(系统核,Linux,I.MX6ULL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//1、定义串口引脚以及引脚PAD属性
/* 定义 UART1 RX 引脚 */
#define UART1_RX_GPIOG PIO1
#define UART1_RX_GPIO_PIN (17U)
#define UART1_RX_IOMUXC IOMUXC_UART1_RX_DATA_UART1_RX
/* 定义 UART1 TX 引脚 */
#define UART1_TX_GPIO GPIO1
#define UART1_TX_GPIO_PIN (16U)
#define UART1_TX_IOMUXC IOMUXC_UART1_TX_DATA_UART1_TX

// 配置串口使用的引脚 (PAD属性)
#define UART_RX_PAD_CONFIG_DATA (SRE_0_SLOW_SLEW_RATE| \
DSE_6_R0_6| \
SPEED_1_MEDIUM_100MHz| \
ODE_0_OPEN_DRAIN_DISABLED| \
PKE_1_PULL_KEEPER_ENABLED| \
PUE_1_PULL_SELECTED| \
PUS_3_22K_OHM_PULL_UP| \
HYS_0_HYSTERESIS_DISABLED)
/*
转换速率: 转换速率慢
驱动强度: R0/6
带宽配置 : medium(100MHz)
开漏配置: 关闭
拉/保持器配置: 使能
拉/保持器选择: 上下拉
上拉/下拉选择: 22K 欧姆上拉 (选择了保持器此配置无效)
滞回器配置: 禁止
*/

#defineUART_TX_PAD_CONFIG_DATA (SRE_0_SLOW_SLEW_RATE|\
DSE_6_R0_6|\
SPEED_1_MEDIUM_100MHz|\
ODE_0_OPEN_DRAIN_DISABLED|\
PKE_1_PULL_KEEPER_ENABLED|\
PUE_0_KEEPER_SELECTED|\
PUS_3_22K_OHM_PULL_UP|\
HYS_0_HYSTERESIS_DISABLED)
/*
转换速率:转换速率慢
驱动强度:R0/6
带宽配置:medium(100MHz)
开漏配置:关闭
拉/保持器配置:使能
拉/保持器选择:保持器
上拉/下拉选择:22K欧姆上拉(选择了保持器此配置无效)
滞回器配置:禁止
*/
//2、串口初始化配置
void uart_init(void)
{
/********************** 第一部分 ********************/
/* 时钟初始化,设置 UART 根时钟,并设置为 40MHz*/
CCM->CSCDR1 &= ~(0x01 << 6); //设置 UART 选择 PLL3 / 6 = 80MHz
CCM->CSCDR1 &= ~(0x3F);
//清零
//设置串口根时钟分频值为 1,UART 根时钟频率为:80M / (dev + 1) = 40MHz
CCM->CSCDR1 |= (0x01 << 0);
/********************** 第二部分 ********************/
/* 开启 UART1 的时钟 */
CCM->CCGR5 |= CCM_CCGR5_CG12(0x3); //开启 UART1 的时钟
UART1->UCR1 &= ~UART_UCR1_UARTEN_MASK; //禁用 UART1
/* 软件复位 */
UART1->UCR2 &= ~UART_UCR2_SRST_MASK;
while ((UART1->UCR2 & UART_UCR2_SRST_MASK) == 0)
{
}
/********************** 第三部分 ********************/
UART1->UCR1 = 0x0;
UART1->UCR2 = UART_UCR2_SRST_MASK;
UART1->UCR3 = UART_UCR3_DSR_MASK | UART_UCR3_DCD_MASK | UART_UCR3_RI_
→MASK;
UART1->UCR4 = UART_UCR4_CTSTL(32);
UART1->UFCR = UART_UFCR_TXTL(2) | UART_UFCR_RXTL(1);
UART1->UESC = UART_UESC_ESC_CHAR(0x2B);
UART1->UTIM = 0x0;
UART1->ONEMS = 0x0;
UART1->UTS = UART_UTS_TXEMPTY_MASK | UART_UTS_RXEMPTY_MASK;
UART1->UMCR = 0x0;
/********************** 第四部分 ********************/
/* 引脚初始化 */
IOMUXC_SetPinMux(UART1_RX_IOMUXC, 0);
IOMUXC_SetPinConfig(UART1_RX_IOMUXC, UART_RX_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(UART1_TX_IOMUXC, 0);
IOMUXC_SetPinConfig(UART1_TX_IOMUXC, UART_TX_PAD_CONFIG_DATA);
/********************** 第五部分 ********************/
/*******uart 初始化 ******/
/* 设置控制寄存器到默认值 */
UART1->UCR2 |= (1 << 5); //8 位数宽度
UART1->UCR2 &= ~(1 << 6); //一位停止位
UART1->UCR2 &= ~(1 << 8); //禁用奇偶校验位
UART1->UCR2 |= (1 << 2); //使能发送
UART1->UCR2 |= (1 << 1); //使能接收
UART1->UCR2 |= (1 << 14); //忽略流控
/********************** 第六部分 ********************/
/* For imx family device, UARTs are used in mode,
so that this bit should always be set.*/
UART1->UCR3 |= UART_UCR3_RXDMUXSEL_MASK;
UART1->UFCR = \
(UART1->UFCR & ~UART_UFCR_TXTL_MASK) | UART_UFCR_TXTL(1); //设置发送FIFO 阀值
UART1->UFCR = \
(UART1->UFCR & ~UART_UFCR_TXTL_MASK) | UART_UFCR_TXTL(1); //设置接收FIFO 阀值
UART1->UCR1 &= ~UART_UCR1_ADBR_MASK; //禁用可变波特率
// UART1->UCR1 |= UART_UCR1_ADBR_MASK;

/**********************第七部分********************/
/*波特率设置方式1。使用官方SDK设置波特率函数*/
//UART_SetBaudRate(UART1,115200,40000000);

/*波特率设置方式2。手动计算,填入寄存器*/
/*设置串口波特率
*RefFreq时钟40MHz
*UFCR RFDIV 110 0x067分频5.714MHz
*BaudRate 115200bps
*UBMR 31-1=0x09
*UBIR 10-1=0x1E
*/
UART1->UFCR&=~(0x07 << 7); //清零分频值
UART1->UFCR|=(0x06<< 7); //设置分频值,40MHz/7= 5.714MHz
UART1->UBIR= 0x09;
UART1->UBMR= 0x1E;

/**********************第八部分********************/
/*开启串口*/
UART1->UCR1|=UART_UCR1_UARTEN_MASK;
}

五、SPI (物理层和链路层)

1、SPI特点

  • SPI(Serial Peripheral lnterface):串行外设接口,SPI是一种高速的、全双工、同步的串行通信总线
  • SPI采用主从方式工作,一般有一个主设备和一个或多个从设备(一主一从一主多从
  • SPI需要至少4根线,分别是MISO(主设备输入从设备输出,接收信号线)、MOSI(主设备输出从设备输入)、SCLK(时钟)、CS(片选)
示例图片

2、SPI4根逻辑线的作用

  • MISO:接收信号线,主机接受,从机发送(从机发送数据到主机)
  • MOSI:发送信号线,主机发送,从机接受(主机发送数据到从机)
示例图片

3、SPI的工作模式

SPI总线有四种不同的工作模式,取决于极性(CPOL)和相位(CPHL)这两个因素

1、CPOL表示SCLK空闲时的状态

  • CPOL=O,空闲时SCLK为低电平
  • CPOL=1,空闲时SCLK为高电平

2、CPHA表示采样时刻

  • CPHA=0,每个周期的上升沿(第一个时钟沿)采样
  • CPHA=1.每个周期的下降沿(第二个时钟)沿采样

例如:CPOL=0,CPHA=0

采集的数据:取决于采样时刻MOSI线上的电平状态

示例图片

4、配置驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//以下是SPI模块的初始化代码,配置成主机模式 						  
//SPI口初始化
//这里针是对SPI1的初始化
void SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;

/* SPI的IO口和SPI外设打开时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

/* SPI的IO口设置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; //PA5-SPI1_SCK PA6-SPI1_MISO PA7-SPI1_MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小:SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿(下降)数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI1, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器

SPI_Cmd(SPI1, ENABLE); //使能SPI外设

SPI1_ReadWriteByte(0xff);//启动传输
}

参考链接:【STM32】SPI程序示例_stm32 spi例程-CSDN博客

六、IIC(物理层和链路层)

1、IIC特点

  • IIC总线(lnter-Integrated Circuit):集成电路总线,是一种串行半双工总线,主要用于近距离、低速的芯片之间的通信;
  • IIC总线:有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步
  • IIC通信一般采用一主多从的工作模式。
示例图片

2、数据传输格式

2.1、写数据帧
示例图片

1、空闲状态:空闲状态的时候,SCL线和SDA线都处于高电平

示例图片

2、起始位:时钟信号处于高电平期间,数据信号完成由高到低的跳变,这样起始信号就发送完成了

示例图片

3、设备地址:表明主机与那个从机进行通信

示例图片

设备地址的传输时SCL和SDA的状态表示:SCL时钟线始终处于高平点的时候,读取SDA数据线上的内容

示例图片

4、读/写数据位:读数据置1,写数据置0(针对的是主机)

5、应答信号位:从机发给主机的,从机收到主机发送的设备地址,收到置0,没收到置1(确认设备地址是否正确)——>(应答信号位:0:数据被正确接受,1:从机忙;接受错误;主机读取完成)

6、寄存器地址位:设备的存储数据的内部寄存器地址

7、应答信号位:从机发给主机的,从机收到主机发送的内部寄存器地址,收到置0,没收到置1(确认设备内部寄存器地址是否正确)

8、要写入的数据:设备存储器的寄存器要写入的八位数据

9、应答信号位:从机发给主机的,告诉主机发送成功,收到置0,没收到置1(确认写入是否成功)

10、停止位:时钟信号为高,数据信号完成由低到高的跳变

示例图片
2.2、读数据帧
示例图片

1、起始位

2、设备地址:读取目标数据的设备地址

3、读写位:写,主机写入

4、应答位:从机发给主机的,从机收到主机发送的设备地址,收到置0,没收到置1(确认设备地址是否正确)

5、寄存器地址:读取目标数据的寄存器地址

6、应答信号:从机发给主机的,从机收到主机发送的内部寄存器地址,收到置0,没收到置1(确认设备内部寄存器地址是否正确)

7、起始位

8、设备地址:读取目标数据的设备地址

9、读写位:读数据,主机读

10、要接受的数据:从设备的存储数据的寄存器中取出数据

11、应答位:主机发送给从机的,表示收到从机的数据

12、停止位

示例图片

3、配置驱动

3.1、软件模拟IIC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

//参数给1或0,就可以释放或拉低SCL了。
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
Delay_us(10);
}

//写数据
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
Delay_us(10);
}

//读与写不是一个寄存器
//读数据
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return BitValue;
}

//IIC的SDA与SCL引脚初始化
void MyI2C_Init(void)
{
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);

//将两根线置为高电平,初始化为开漏输出且释放总线
GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);
}

//开始
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}

//结束
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}

//发送单元
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for(i = 0; i < 8; i++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);

}
}

//接收单元
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i,Byte = 0x00;
//MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
for (i = 0; i< 8;i++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1)
{
Byte |= (0x80>>i);
}
MyI2C_W_SCL(0);
}
return Byte;

}

//发送应答
void MyI2C_SendAck(uint8_t AckBit)
{

MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);

}

//接收应答
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
//主机释放SDA
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;

}
3.2、硬件模拟IIC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"
#define MPU6050_ADDRESS 0XD0

//带有超时退出机制的WaitEvent函数
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout;
Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
}

//主机给从机写数据(单片机给MPU6050写数据)
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{

I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
//在库函数中,发送数据自带接收应答,接收数据自带发送应答。
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
I2C_SendData(I2C2,RegAddress);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);
I2C_SendData(I2C2,Data);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);
I2C_GenerateSTOP(I2C2,ENABLE);

}

//主机读从机的数据(单片机读MPU6050的数据)
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2,ENABLE);

MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
//在库函数中,发送数据自带接收应答,接收数据自带发送应答。
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
I2C_SendData(I2C2,RegAddress);
//问题:如果使用硬件操作I2C,在重复开始条件时,EVENT_MASTER_BYTE_TRANSMITTING这个状态正在发送数据(数据并没有发生完成),但是实验结果显示数据也能正常的接收,这是为什么?
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);

I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
I2C_AcknowledgeConfig(I2C2,DISABLE);
I2C_GenerateSTOP(I2C2,ENABLE);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE);
return Data;

}

//获取数据寄存器的函数
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
uint8_t DataH,DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}


//初始化MPU6050
void MPU6050_Init(void)
{
//第一步:开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//第二步:配置pin引脚为复用开漏输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//第三步:初始化I2C外设
I2C_InitTypeDef I2C_InitStructure;
//接收一个字节后是否给从机应答
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
//stm32从机可以响应几位的地址(stm32作为从机才会用到)
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 10000;
//占空比:这里指的是SCL低电平与高电平的比,因为SCL是弱上拉,所以在快速状态下,SDA波形翻转需要一些时间(低电平需要更多的时间)
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
//自身地址1(stm32作为从机才会用到),指定stm32的自身地址,方便别的主机呼叫。
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C2,&I2C_InitStructure);

//第三步:I2C使能
I2C_Cmd(I2C2,ENABLE);

MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);

}


//获取MPU6050的ID
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

参考链接:STM32——IIC(I2C)附代码_stm32 i2c demo-CSDN博客

七、CAN(物理层和链路层)

1、CAN特点

  • CAN总线(Controller Area Network):控制器(在汽车领域称为ECU,Electronic Control Unit)域网络,CAN总线应用最多的是汽车领域
  • CAN通信采用的是差分信号,是两根线共同作用的,是双绞线缠绕的,这样的结构,抗干扰能力强,可进行长距离传输

2、CAN总线上的电平信号

CAN采用的是双绞线,用的是差分信号表示逻辑“1”和逻辑“0”

逻辑 CAN_H CAN_L 两条线上的电压差
逻辑1 2.5V 2.5V 0V
逻辑0 3.5V 1.5V 2V
示例图片

3、数据传输格式

示例图片

1、起始位:占1位,一定是0

2、识别码:占11位,数据发送至哪个设备的识别码

3、RTR位:占1位,数据帧为0,远程请求帧为1

3、控制码:占6位,控制数据长度

  • IDE位:第1位,标准帧(识别码11位)为0,拓展帧(识别码29位)为1
  • 空闲位:第2位,保留
  • DLC(Data Link Control)位:剩下4位,决定了数据码的所占字节数(DLC=1,则数据码只占1个字节,DLC=8,则数据码占8个字节)

4、数据码:所占位数由DLC决定

5、CRC:占16位,循环冗余校验位,确保数据的准确性

  • CRC校验码:占15位,
  • CRC界定符:占1位,固定为1,为了与后面的数据隔开

6、ACK码:占两位, 第一位为ACK确认槽,发送端发送逻辑1,接收端回复逻辑0,第二位为ACK界定位,作用也是隔开,固定为1

7、结束位,占7位,全都是1,表示数据帧的结束

4、数据传输优先级

根据数据帧的识别码来判断,识别码中0多的优先级高。如果识别码一致,RTR位为0的数据帧具有优先权

5、配置驱动

1、配置CAN的基础设置

  • 波特率的配置:数据的传输速率
  • 中断开启:打开中断是为了CPU能够及时接受和处理放在 “接受邮箱” 中的报文

2、配置CAN的过滤器

2.1、CAN过滤器有两种工作模式:列表模式和掩码模式

  • 列表模式:列出ID名字,过滤器通过判断报文ID与其是否一致来决定是接受还是舍弃这份报文。
  • 掩码模式:通过确定ID特定位的值来判断报文的接受与丢弃。

2.2、32位列表模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief 32位列表模式过滤器配置(这里举一个标准ID一个扩展ID)
* @param hcan CAN的句柄
* @param StdId 标准ID
* @param ExtId 扩展ID
*/
void CANFilterConfig_Scale32_IdList(CAN_HandleTypeDef * hcan,uint32_t StdId,uint32_t ExtId,uint8_t Filter_Nunber )
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber ; //使用过滤器编号
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; //设为列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //配置为32位宽
sFilterConfig.FilterIdHigh = StdId<<5; //基本ID放入到STID中
sFilterConfig.FilterIdLow = 0|CAN_ID_STD; //设置IDE位为0
sFilterConfig.FilterMaskIdHigh = ((ExtId<<3)>>16)&0xffff;
sFilterConfig.FilterMaskIdLow = (ExtId<<3)&0xffff|CAN_ID_EXT;//设置IDE位为1
sFilterConfig.FilterFIFOAssignment = 0; //接收到的报文放入到FIFO0中
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(hcan, &sFilterConfig) ;
}

2.3、16位列表模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief 16位列表模式过滤器配置
* @param hcan CAN的句柄
* @param StdId1~StdId4 4个标准ID
*/
void CANFilterConfig_Scale16_IdList(CAN_HandleTypeDef * hcan,uint32_t StdId1,uint32_t StdId2,uint32_t StdId3,uint32_t StdId4,uint8_t Filter_Nunber)
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber; //使用过滤器1
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; //设为列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; //位宽设置为16位
sFilterConfig.FilterIdHigh = StdId1<<5; //4个标准CAN ID分别放入到4个存储中
sFilterConfig.FilterIdLow = StdId2<<5;
sFilterConfig.FilterMaskIdHigh = StdId3<<5;
sFilterConfig.FilterMaskIdLow = StdId4<<5;
sFilterConfig.FilterFIFOAssignment = 0; //接收到的报文放入到FIFO0中
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
}

2.4、32位掩码模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* @brief 32位掩码模式过滤器配置
* @param hcan CAN的句柄
* @param ID 验证码
* @param Mask 屏蔽码
* @param ID_Type ID类型(标准帧为0,其他则为扩展帧)
*/
void CANFilterConfig_Scale32_IdMask(CAN_HandleTypeDef * hcan,uint32_t ID,uint32_t Mask,uint8_t ID_Type,uint8_t Filter_Nunber)
{

if(ID_Type==0) //标准ID
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber; //使用过滤器2
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; //配置为掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //设置为32位宽
sFilterConfig.FilterIdHigh =ID<<5;
sFilterConfig.FilterIdLow =0;
sFilterConfig.FilterMaskIdHigh =(Mask<<5);
sFilterConfig.FilterMaskIdLow =0|0x02; //只接收数据帧
sFilterConfig.FilterFIFOAssignment = 0; //设置通过的数据帧进入到FIFO0中
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(hcan, &sFilterConfig);
}
else //扩展ID
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber; //使用过滤器3
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; //配置为掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //设为32位宽
sFilterConfig.FilterIdHigh =((ID<<3) >>16) &0xffff;
sFilterConfig.FilterIdLow =((ID<<3)&0xffff) | CAN_ID_EXT;
sFilterConfig.FilterMaskIdHigh = (Mask>>16)&0xffff;
sFilterConfig.FilterMaskIdLow = (Mask&0xffff)|0x02; //只接收数据帧
sFilterConfig.FilterFIFOAssignment = 0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(hcan, &sFilterConfig);
}

}

3、报文的发送和接受

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* @brief 发送标准ID的数据帧
* @param hcan CAN的句柄
* @param ID 数据帧ID
* @param pData 数组指针
* @param Len 字节数0~8
*/
uint8_t CANx_SendStdData(CAN_HandleTypeDef* hcan,uint16_t ID,uint8_t *pData,uint16_t Len)
{
static CAN_TxHeaderTypeDef Tx_Header;

Tx_Header.StdId=ID;
Tx_Header.ExtId=0;
Tx_Header.IDE=0;
Tx_Header.RTR=0;
Tx_Header.DLC=Len;
/*找到空的发送邮箱,把数据发送出去*/
if(HAL_CAN_AddTxMessage(hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX0) != HAL_OK) //
{
if(HAL_CAN_AddTxMessage(hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX1) != HAL_OK)
{
HAL_CAN_AddTxMessage(hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX2);
}

/**
* @brief 发送扩展ID的数据帧
* @param hcan CAN的句柄
* @param ID 数据帧ID
* @param pData 数组指针
* @param Len 数据长度0~8
*/
uint8_t CANx_SendExtData(CAN_HandleTypeDef* hcan,uint32_t ID,uint8_t *pData,uint16_t Len)
{
static CAN_TxHeaderTypeDef Tx_Header;

Tx_Header.RTR=0;
Tx_Header.DLC=Len;
Tx_Header.StdId=0;
Tx_Header.ExtId=ID;
Tx_Header.IDE=CAN_ID_EXT;
/*找到空的发送邮箱,把数据发送出去*/
if(HAL_CAN_AddTxMessage(&hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX0) != HAL_OK) //
{
if(HAL_CAN_AddTxMessage(&hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX1) != HAL_OK)
{
HAL_CAN_AddTxMessage(&hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX2);
}
}

/**
* @brief CAN FIFO0的中断回调函数,在里面完成数据的接收
* @param hcan CAN的句柄
*/

uint8_t date_CAN1[8];//设为全局变量,用于接收CAN1数据
uint8_t date_CAN2[8];//设为全局变量,用于接收CAN2数据

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
if(hcan->Instance ==CAN1)
{
CAN_RxHeaderTypeDef RxHeader; //接受句柄
HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &RxHeader, date_CAN1); //接收,CAN邮箱为0
return ;
}
else if(hcan->Instance == CAN2)
{
CAN_RxHeaderTypeDef RxHeader; //接受句柄
HAL_CAN_GetRxMessage(&hcan2, CAN_RX_FIFO0, &RxHeader, date_CAN2); //接收,CAN邮箱为0
}
}

void CAN_Start(CAN_HandleTypeDef *hcan)
{
HAL_CAN_ActivateNotification(hcan ,CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_CAN_Start(hcan);
}

参考链接:CAN通信知识梳理及在Stm32上的应用(HAL库)-CSDN博客

八、Modbus、IEC61850、IEC104、IEC103、IEC101

1、modbus

  • Modbus协议主要运行在应用层
  • 支持多种通信方式,包括ModBus RTUModBus ASCIIModBus TCP
  • ModBus RTU:采用二进制数据传输,使用串行通信,支持RS232和RS485两种接口(物理传输介质:物理上通过串口线)
  • ModBus ASCII:采用ASCII码传输文本信息,使用串行通信,并支持RS232和RS485两种接口(物理传输介质:物理上通过串口线)
  • ModBus TCP:使用TCP/IP协议进行通信(物理传输介质:物理层上通过网口)

2、IEC61850

  • 作用:IEC 61850 用于变电站自动化系统的通信。它旨在提供一种通用的框架,以实现不同制造商设备之间的互操作性
  • 协议组成:MMS+GOOSE+SV(61850将通过对象分为三层),站控层、间隔层、过程层
  • MMS:用于装置和后台之间的数据交互
  • GOOSE:用于装置之间的通讯
  • SV:用于采样值传输

3、IEC103

  • 作用:IEC 103专门针对保护继电器的应用,用于监控和记录电力系统保护装置的动作行为。它可以用来配置保护设备、读取故障报告等。
  • 通信方式:运行在应用层的报文,IEC 103可以使用不同的物理传输介质,如串行接口或以太网

4、IEC104和IEC101

  • 作用:IEC 101是一种面向遥测和遥控应用的基本远动任务配套标准。它规定了如何在远动设备之间进行简单而高效的数据交换
  • 101通信方式:主要是在数据链路层(Layer 2)和应用层(Layer 7)。IEC 101通常通过串行通信链路工作,例如RS-232或RS-485(物理传输介质:物理上通过串口线)
  • IEC 104是基于IEC 60870-5-101标准,并使用TCP/IP网络协议进行传输的扩展版本
  • TCP(传输控制协议)和UDP(用户数据报协议)是两种不同的传输层协议,它们并不直接指定物理传输媒介(网线(以太网)、Wi-Fi、4G/5G蜂窝网络还是其他任何类型的网络连接)