1.串口

(1)硬件电路

       1.简单双向串口通信有两根通信线(发送端TX和接收端RX

       2.TX与RX要交叉连接

       3.当只需单向的数据传输时,可以只接一根通信线

        4.当电平标准不一致时,需要加电平转换芯片(这里就可以了解RS232、485的通信协议,RS232简易优点就是增加了电压差,增加了通信距离。而RS485是用差分电压,既能增加通信距离又能强抗干扰。本质都是对于串口的扩展用于更复杂的场景)

这里补充一点的是,当两个设备进行通信时可以不接电源线,但必须要接底线,保证共同的电压基准。串口通信的信号电平(如TX、RX)是相对于地的。如果没有共地,两个设备的“0V”参考点不同,可能导致信号电平偏移、误码甚至损坏接口。

(2)串口参数及时序串口参数及时序

        波特率:串口通信的速率

        

        数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行

        校验位:用于数据验证,根据数据位计算得来

        停止位:用于数据帧间隔,固定为高电平

我们首先要保证Tx空闲状态时为高电平,将输出GPIO设置复用推挽输出。

起始位:标志一个数据帧的开始,Tx从空闲状态转换为Busy状态---我们将Tx状态拉低标志着我们开始接受数据

数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行

校验位:用于数据验证,根据数据位的1的奇偶个来判断发送是否正确(一般用不到,1位出错的概率可以检测的到,二位出错就检测不到)

停止位:标志一个数据帧的结束,Tx从Busy状态转换为空闲状态:我们将Tx状态抬高标志着我们接受数据完成

从上图可以清晰的了解波特率的计算,要得到我们想要的波特率只需要反求分频器的数就行。对于STM32硬件来说都是集成的,不需要计算。

这里我还需要介绍一下几个标志位以便于后续理解:

TXE:发送数据寄存器空 (Transmit data register empty)

TC:发送完成 (Transmission complete)

RXNE:读数据寄存器非空 (Read data register not empty),当接受数据寄存器非空时,标志位为1,否则为0。TXE同理

(3)串口发送模式

/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);					//将PA9引脚初始化为复用推挽输出
	
	/*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_Tx;			//模式,选择为发送模式
	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);				//将结构体变量交给USART_Init,配置USART1
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行

上述为使用标准库对USART发送模式进行初始化,波特率为9600,无校验位,一位停止位,数据为为8个字节。关于硬件流控不仔细讲,简单介绍一个,硬件流控主要是相当于一个缓冲区,对于通信双方的速度进行约束。避免数据丢失的自动流量控制机制。

/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}


/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}


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

对于上述三个函数,我重点讲解一下第三个函数int fputc(int ch,FILE *f),在STM32开发中,重写 int fputc(int ch, FILE *f) 函数可以实现 将标准输出(如 printf)重定向到串口(或其他自定义输出设备)。这之所以可行,本质上依赖于 C标准库的弱符号(weak symbol)机制 和 库函数的设计约定(其实在其他类型单片机也可重定向,我在初学51单片机时也可重定向)。

使用printf函数之前别忘了引用头文件stdio.h。

(3)串口接收模式

/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟

    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);					//将PA10引脚初始化为上拉输入

    /*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode =  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);				//将结构体变量交给USART_Init,配置USART1

    USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行

上述为使用标准库对USART接受模式进行初始化,波特率为9600,无校验位,一位停止位,数据为为8个字节。

/* 对于1个字节的数据接收 */

// #1.等待接受数据
while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == RESET)

// #2.接受数据
uint8_t btyeRcvd = USART_ReceiveData(USART1)


/* 对于多字节的数据接收 */
/**
 * @brief 通过USART1接收指定长度的数据(阻塞方式)
 * @param buffer 存放接收数据的缓冲区
 * @param len    要接收的字节数
 * @retval 实际接收到的字节数(正常情况下 == len)
 */
uint16_t USART1_ReceiveBytes(uint8_t *buffer, uint16_t len)
{
    uint16_t cnt = 0;
    
    for(cnt = 0; cnt < len; cnt++)
    {
        // 等待接收数据寄存器非空
        while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
        
        // 读取数据并存入缓冲区
        buffer[cnt] = USART_ReceiveData(USART1);
    }
    
    return cnt;
}

总的来说,对于串口接收与发送,不要忘记两个标志位TXE:发送数据寄存器空 (Transmit data register empty)和RXNE:读数据寄存器非空 (Read data register not empty)

2.IIC

(1)硬件电路

两根通信线: SCL Serial Clock )、 SDA Serial Data )、同步,半双工、带数据应答
、支持总线挂载多设备(一主多从、多主多从)

  1. 所有I2C设备的SCL连在一起,SDA连在一起
  2. 设备的SCLSDA均要配置成开漏输出模式(保证SCL、SDA在空闲状态时都要保持高电平)
  3. SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右(对于绝大数外挂模块来说都两根线都自带上拉电阻、所以在大多数时单片机都不需要外加上拉电阻)

(2)I2C时序基本单元

  1. 起始条件:SCL高电平期间,SDA从高电平切换到低电平
  2. 终止条件:SCL高电平期间,SDA从低电平切换到高电平

发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行)

接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行)

在SCL低电平定义为写的时间,SCL高电平定义为读的时间

发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据 0 表示应答,数据 1 表示非应答(主机在接收之前,需要释放 SDA

(3)IIC发送格式

起始位+地址位(7位地址位+1位读写位,读1写0,方法(0x## << 1 & 0x00)+ACK应答位+数据位+ACK+......+停止位。

(4)IIC硬件读写

先提前介绍几个标志位以便于后续代码的理解:

ACK:应答使能 (Acknowledge enable)
软件可以设置或清除该位,或当PE=0时,由硬件清除。
0:无应答返回;
1:在接收到一个字节后返回一个应答(匹配的地址或数据)。(切记不要在这里搞混,ACK1是将电平拉高,使SDA线置于低电平,接受应答)

STOP:停止条件产生 (Stop generation)
软件可以设置或清除该位;或当检测到停止条件时,由硬件清除;当检测到超时错误时,硬件
将其置位。

START:起始条件产生 (Start generation)
软件可以设置或清除该位,或当起始条件发出后或PE=0时,由硬件清除。

AF:应答失败 (Acknowledge failure)

TxE:数据寄存器为空(发送时) (Data register empty (transmitters))

RxNE:数据寄存器非空(接收时) (Data register not empty (receivers))

BTF:字节发送结束 (Byte transfer finished)

ADDR:地址已被发送(主模式)/地址匹配(从模式) (Address sent (master mode)/matched
(slave mode))

SB:发送起始位

补充:我简易的讲述一下如何用标志位去写入数据,便于大家记忆更深

先附代码,我再讲解

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
    // 1. 产生起始条件
    I2C2->CR1 |= I2C_CR1_START;
    while(!(I2C2->SR1 & I2C_SR1_SB));  // 等待起始条件发送完成 (EV5)
    
    // 2. 发送设备地址 + 写方向 (地址左移1位,最后一位=0表示写)
    I2C2->DR = (MPU6050_ADDRESS << 1) | 0x00;
    while(!(I2C2->SR1 & I2C_SR1_ADDR));  // 等待地址发送完成 (EV6)
    // 读取SR1和SR2清除ADDR标志位
    (void)I2C2->SR1;
    (void)I2C2->SR2;
    
    // 3. 发送寄存器地址
    I2C2->DR = RegAddress;
    while(!(I2C2->SR1 & I2C_SR1_TXE));  // 等待数据寄存器空 (EV8)
    
    // 4. 发送要写入的数据
    I2C2->DR = Data;
    while(!(I2C2->SR1 & I2C_SR1_BTF));  // 等待字节传输完成 (EV8_2)
    
    // 5. 产生停止条件
    I2C2->CR1 |= I2C_CR1_STOP;
}

1.发送起始位,然后等待SB(发送起始位)

2.发送地址位,等待ADDR(寻址成功位)

**这里必须要读取SR1和SR2清除ADDR标志位才能开始下一步,这里我也带有疑问,但数据手册硬性要求,有懂的小伙伴可以在评论区分享一下

3.发送寄存器地址,等待发送寄存器(TxE)为空

**这一步其实不是硬性要求,只是对于复杂的外挂,如MPU6050是一个地址,其内又有多个地址

4.发送数据,等待字节发送位(BTF)为空

5.产生停止条件

下面是集成模块用法。

	/* IIC硬件初始化 */

    /*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);		//开启I2C2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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);					//将PB10和PB11引脚初始化为复用开漏输出
	
	/*I2C初始化*/
	I2C_InitTypeDef I2C_InitStructure;						//定义结构体变量
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;				//模式,选择为I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;				//时钟速度,选择为50KHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;		//时钟占空比,选择Tlow/Thigh = 2
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;				//应答,选择使能
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//应答地址,选择7位,从机模式下才有效
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;				//自身地址,从机模式下才有效
	I2C_Init(I2C2, &I2C_InitStructure);						//将结构体变量交给I2C_Init,配置I2C2
	
	/*I2C使能*/
	I2C_Cmd(I2C2, ENABLE);									//使能I2C2,开始运行


/*------------------------以江科大用IIC读取MPU6050历程源码为例---------------------------*/

/*------------------------发送多个字节---------------------------*/

/**
  * 函    数:MPU6050等待事件
  * 参    数:同I2C_CheckEvent
  * 返 回 值:无
  */
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)								//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;										//跳出等待,不等了
		}
	}
}

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t *Pdata, uint32_t len)
{
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送

	 MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
     MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8

     while(Len--)
    {
        I2C_SendData(I2C2, *Pdata++);												//硬件I2C发送数据
	    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2  
    }	
	
	
	I2C_GenerateSTOP(I2C2, ENABLE);											//硬件I2C生成终止条件
}

这里我要清晰的解释一下I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)函数

I2C_CheckEvent 是 STM32 标准外设库中用于 I2C 通信的核心函数,它的主要作用是检查指定的 I2C 通信事件是否已经发生并自动清除相关标志位(这里注意一下,调用这个函数是会自动清除标志位的,我初写文章,这里就暂时不解释IIC用标志位具体写入的流程,这里简单叙述怎么用,后续我会在上文补充讲述I2C不用集成函数去讲解,如何用标志位写入)函数执行时对状态寄存器的读操作会自动清除大部分事件标志位(如 ADDR、SB、BTF 等),省去了手动清除标志位的步骤。

现在我要讲述一下这个函数的参数作用:

事件宏定义 对应事件 含义
I2C_EVENT_MASTER_MODE_SELECT EV5 起始条件已发送,已进入主模式
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED EV6 地址已发送(写方向),已进入发送模式
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED EV6 地址已发送(读方向),已进入接收模式
I2C_EVENT_MASTER_BYTE_TRANSMITTED EV8_2 一个数据字节已发送完成
I2C_EVENT_MASTER_BYTE_RECEIVED EV7 一个数据字节已接收完成

对于此函数就可以轻易的,发送一个或多个数据了。

下面开始对于接受数据的讲解。这里就不再在讲解寄存器的写法了,下面有详解,大家可以去看看,也是类似的写法。

嵌入式铁头山羊STM32-各章节详细笔记-查阅传送门-CSDN博客

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重复起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C发送从机地址,方向为接收
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一个字节之前提前将应答失能
	I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一个字节之前提前申请停止条件
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7
	Data = I2C_ReceiveData(I2C2);											//接收数据寄存器
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);									//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
	
	return Data;
}



1.起始位

2.发送初始地址(I2C_Send7bitAddress + 写方向)

3.发送内部地址(意为我要对这个地址进行操作)

4.重新起始位(这里重始起始位是为了方便开始对寄存器读数据)

5.发送内部地址,我要读取这个数据

6.读取数据

7.提前失能,不再应答

8.停止位

9.接受数据并返回

(5)IIC软件读写

这里我是推荐有硬件资源就用硬件去写,这里不得不说发送天才的人真是个stm32,迫不得已再用软件。而且用软件读取IIC会有很多精度问题,初学者是很难排除原因的。

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue为1时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}

/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);							//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);							//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);							//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);							//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
	{
		/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
		MyI2C_W_SDA(!!(Byte & (0x80 >> i)));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)				//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);						//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA()){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
													//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

这里我直接引用江科大的软件时序了,因为他可写的太棒了。

函数1.void MyI2C_W_SCL(uint8_t BitValue):对时钟线写高低电平(对于不同的高速芯片需要不同的延时时间,要不然短时间读取不到电平状态,72Mhz的主频的芯片可以不用延时)

函数2.void MyI2C_W_SDA(uint8_t BitValue)、uint8_t MyI2C_R_SDA(void):对于数据线读写高低电平

函数3.void MyI2C_Init(void):注意要用复用开漏模式

函数4.void MyI2C_Start(void):起始位,这里注意一个问题,起始位要在SCL线保持高电平,SDA产生下降沿才能算一个起始位。起始后拉低SCL方便后续时序的拼接

函数5.void MyI2C_Stop(void):停止位,在SCL线为高电平时,SDA产生一个下降沿

函数6.void MyI2C_SendByte(uint8_t Byte)、uint8_t MyI2C_ReceiveByte(void):发送一个字节、接受一个字节,不解释了,具体看注释吧

函数7.void MyI2C_SendAck(uint8_t AckBit)、uint8_t MyI2C_ReceiveAck(void):发送应答、接受应答

上述是重点,下述就是与套着时序图读取,写入的函数了

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();						//I2C起始
	MyI2C_SendByte(MPU6050_ADDRESS);	//发送从机地址,读写位为0,表示即将写入
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(RegAddress);			//发送寄存器地址
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(Data);				//发送要写入寄存器的数据
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_Stop();						//I2C终止
}

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	MyI2C_Start();						//I2C起始
	MyI2C_SendByte(MPU6050_ADDRESS);	//发送从机地址,读写位为0,表示即将写入
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(RegAddress);			//发送寄存器地址
	MyI2C_ReceiveAck();					//接收应答
	
	MyI2C_Start();						//I2C重复起始
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);	//发送从机地址,读写位为1,表示即将读取
	MyI2C_ReceiveAck();					//接收应答
	Data = MyI2C_ReceiveByte();			//接收指定寄存器的数据
	MyI2C_SendAck(1);					//发送应答,给从机非应答,终止从机的数据输出
	MyI2C_Stop();						//I2C终止
	
	return Data;
}

3.SPI

(1)硬件电路

四根通信线:SCKSerial Clock)、MOSIMaster Output Slave Input)、MISOMaster Input Slave Output)、SSSlave Select

同步,全双工

支持总线挂载多设备(一主多从)

所有SPI设备的SCKMOSIMISO分别连在一起

主机另外引出多条SS控制线,分别接到各从机的SS引脚

输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

(2)  SPI时序单元

  1. 片选信号 (SS/CS):通信开始前,主设备将 CS 拉低(激活低有效),选中从设备。结束后拉高。

  2. 时钟极性 (CPOL):决定空闲时 SCK 的电平。

    • CPOL = 0:空闲低电平。

    • CPOL = 1:空闲高电平。

  3. 时钟相位 (CPHA):决定数据采样发生在时钟的哪个边沿。

    • CPHA = 0:在第一个时钟边沿采样(前沿采样)。

    • CPHA = 1:在第二个时钟边沿采样(后沿采样)。

  4. 数据收发在同一时钟脉冲内,主设备通过 MOSI 线发送数据,从设备通过 MISO 线回复数据。这个过程是同时完成的(全双工),每位数据通常需要 半个时钟周期 建立稳定,另半个周期供对方采样。(这里注意一下,数据发送和接送都是同时进行,发出一个bite就要接受一个bite)

这里再介绍两个标志位(可见是比IIC简单多了哈)

TXE:发送缓冲为空 (Transmit buffer empty)
0:发送缓冲非空;
1:发送缓冲为空。

RXNE:接收缓冲非空 (Receive buffer not empty)
0:接收缓冲为空;
1:接收缓冲非空。

(3)  SPI交换字节

  * 函    数:SPI写SS引脚电平,SS仍由软件模拟
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}

/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);	//开启SPI1的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA4引脚初始化为推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA5和PA7引脚初始化为复用推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA6引脚初始化为上拉输入
	
	/*SPI初始化*/
	SPI_InitTypeDef SPI_InitStructure;						//定义结构体变量
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//模式,选择为SPI主模式
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//数据宽度,选择为8位
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//先行位,选择高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;				//SPI极性,选择低极性
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;			//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;				//NSS,选择由软件控制
	SPI_InitStructure.SPI_CRCPolynomial = 7;				//CRC多项式,暂时用不到,给默认值7
	SPI_Init(SPI1, &SPI_InitStructure);						//将结构体变量交给SPI_Init,配置SPI1
	
	/*SPI使能*/
	SPI_Cmd(SPI1, ENABLE);									//使能SPI1,开始运行
	
	/*设置默认电平*/
	MySPI_W_SS(1);											//SS默认高电平
}



/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);				//拉低SS,开始时序
}

/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);				//拉高SS,终止时序
}

/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);	//等待发送数据寄存器空
	
	SPI_I2S_SendData(SPI1, ByteSend);								//写入数据到发送数据寄存器,开始产生时序
	
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);	//等待接收数据寄存器非空
	
	return SPI_I2S_ReceiveData(SPI1);								//读取接收到的数据并返回
}

这里我直接引用了江科大的代码,大家可清晰的了解是江科大非常喜欢封装函数,这是非常好的习惯。

一、下面介绍一下初始化函数(挑选几个容易概念重的,其他的大家可行了解哈):

1.SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;    //波特率分频,选择128分频:

这里是挂载总线的时钟频率 ÷ 分频,这里有个原则,你的实际波特率必须 ≤ 从设备的最高频率。

2.SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                //SPI极性,选择低极性
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;            //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0

这两个函数是关于时序的读取,总共四个模式,按道理来说,无论那个模式选择都是可以读取外挂设备的,只不过代码不一罢了。但为了大家,我还是建议用模式0,因为关于SPI大多数代码都是用模式1

交换一个字节(模式 0
CPOL=0 :空闲状态时, SCK 为低电平
CPHA=0 SCK 第一个边沿移入数据,第二个边沿移出数据

3.SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                //NSS,选择由软件控制

这里如果用硬件控制就可以直接外加上拉电阻,如果选用软件模式,我的建议是在SPI下外加SPI_NSSInternalSoftwareConfig(SPI1, SPI_NSSInternalSoft_Set);函数,这句话的意思内部软件使NSS置于高电位,但我看江科大没写,我是为了保险起见。

二、下面介绍一下封装的函数,防止大家第一次看的时候比较懵

void MySPI_W_SS(uint8_t BitValue)这个函数就是对单个片选信号线进行封装,0选中从机

在初始化中写入MySPI_W_SS(1),为了方便后续代码管理,这样理解就是后续有多个从机设备,而再初始化中全都拉高片选信号,方便后续再写单个模块。

三、下面介绍一下uint8_t MySPI_SwapByte(uint8_t ByteSend)函数

         1. 等待发送缓冲区为空 (TXE 标志)

         2.写入数据,触发时序生成

         3.等待接收缓冲区非空 (RXNE 标志)

         4.读取接收到的数据并返回

         四、我引入一个多字节交换函数

// 更高效的连续发送(适合大量数据)
void SPI_SendMultiBytes_Fast(SPI_TypeDef* SPIx, uint8_t* txData, uint8_t* rxData, uint16_t len)
{
    uint16_t i;
    
    // 先发送第一个字节
    while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPIx, txData[0]);
    
    // 循环处理中间的数据(利用流水线)
    for(i = 1; i < len; i++)
    {
        // 等待上一个数据发送完成(缓冲区空)
        while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
        
        // 立即发送下一个数据
        SPI_I2S_SendData(SPIx, txData[i]);
        
        // 此时读取上一个数据的结果
        while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
        if(rxData != NULL)
            rxData[i-1] = SPI_I2S_ReceiveData(SPIx);
        else
            SPI_I2S_ReceiveData(SPIx);
    }
    
    // 读取最后一个字节
    while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
    if(rxData != NULL)
        rxData[len-1] = SPI_I2S_ReceiveData(SPIx);
    else
        SPI_I2S_ReceiveData(SPIx);
}

这种代码写入方式可以有效地节省交换时间。先介绍一下函数吧

void SPI_SendMultiBytes_Fast(SPI_TypeDef* SPIx, uint8_t* txData, uint8_t* rxData, uint16_t len)

SPI_TypeDef* SPIx,选SPI

uint8_t* txData,要发送的数据

uint8_t* rxData,发送后交换过来的数据

uint16_t len、要交换的程度

介绍一下这种代码的优势:请看下图,不解释了

(4)W25Q64介绍

这里我介绍一下W25Q64FLASH存储模块吧,以便后续有小伙伴用SPI进行实际操作

1.W25Q64的存储结构

W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景

存储介质:Nor Flash(闪存)

时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

存储容量24位地址)

对W25Q64进行写入分为六步

1.写使能

2.扇区擦除(这里可以小伙伴自己查看一下手册,其实是可以块擦除的)

3.等待空闲

4.写使能

5.页编程(这里写入只能按页写入)

6.等待空闲

下面列出几个W25Q64的基础命令定义

// ============ W25Q64 基础命令定义 ============
#define W25X_WriteEnable         0x06
#define W25X_WriteDisable        0x04
#define W25X_ReadStatusReg       0x05
#define W25X_SectorErase         0x20  // 4KB扇区擦除
#define W25X_ChipErase           0xC7
#define W25X_PageProgram         0x02  // 页编程(256字节)
#define W25X_ReadData            0x03

小伙伴们自行理解哈。

现在开始进行写操作

// ============ W25Q64 基础命令定义 ============
#define W25X_WriteEnable         0x06
#define W25X_WriteDisable        0x04
#define W25X_ReadStatusReg       0x05
#define W25X_SectorErase         0x20  // 4KB扇区擦除
#define W25X_ChipErase           0xC7
#define W25X_PageProgram         0x02  // 页编程(256字节)
#define W25X_ReadData            0x03

// CS引脚定义(根据你的实际硬件修改)
#define W25Q64_CS_LOW()    GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define W25Q64_CS_HIGH()   GPIO_SetBits(GPIOA, GPIO_Pin_4)


// 等待W25Q64空闲(读取状态寄存器)
void W25Q64_WaitBusy(void)
{
    uint8_t status;
    uint8_t cmd = W25X_ReadStatusReg;  // cmd = 0x05


    W25Q64_CS_LOW();
    
    // 发送读状态寄存器命令
    SPI_SendMultiBytes_Fast(SPI1, &cmd, NULL, 1);
    
    // 循环读取状态,直到BUSY位为0
    do {
        SPI_SendMultiBytes_Fast(SPI1, (uint8_t*)"\xFF", &status, 1);
    } while(status & 0x01);  // BUSY位在bit0
    
    W25Q64_CS_HIGH();
}


// 写使能
void W25Q64_WriteEnable(void)
{
    W25Q64_CS_LOW();
    uint8_t cmd = W25X_WriteEnable;
    SPI_SendMultiBytes_Fast(SPI1, &cmd, NULL, 1);
    W25Q64_CS_HIGH();
}


// 扇区擦除(4KB对齐,地址必须是4096的倍数)
void W25Q64_SectorErase(uint32_t sector_addr)
{
    uint8_t cmd[4];
    
    // 扇区地址必须是4096的倍数
    if(sector_addr % 4096 != 0) {
        // 错误处理:地址未对齐
        return;
    }
    
    W25Q64_WriteEnable();  // 擦除前需要写使能
    
    W25Q64_CS_LOW();
    
    cmd[0] = W25X_SectorErase;
    cmd[1] = (sector_addr >> 16) & 0xFF;
    cmd[2] = (sector_addr >> 8) & 0xFF;
    cmd[3] = sector_addr & 0xFF;
    
    SPI_SendMultiBytes_Fast(SPI1, cmd, NULL, 4);
    
    W25Q64_CS_HIGH();
    
    W25Q64_WaitBusy();  // 等待擦除完成
}


// ============ 页编程(一次最多256字节)============
void W25Q64_PageProgram(uint32_t addr, uint8_t* data, uint16_t len)
{
    uint8_t cmd[4];
    uint16_t i;
    
    
    W25Q64_WriteEnable();  // 编程前需要写使能
    
    W25Q64_CS_LOW();
    
    cmd[0] = W25X_PageProgram;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;
    
    // 发送页编程命令和地址
    SPI_SendMultiBytes_Fast(SPI1, cmd, NULL, 4);
    
    // 发送要写入的数据
    SPI_SendMultiBytes_Fast(SPI1, data, NULL, len);
    
    W25Q64_CS_HIGH();
    
    W25Q64_WaitBusy();  // 等待编程完成
}


// ============ 写入任意长度数据(自动处理跨页)============
void W25Q64_Write(uint32_t addr, uint8_t* data, uint32_t len)
{
    uint32_t page_offset = addr & 0xFF;      // 页内偏移
    uint32_t page_remaining = 256 - page_offset;  // 当前页剩余空间
    uint32_t write_len;
    uint32_t current_addr = addr;
    uint32_t current_len = len;
    uint8_t* pData = data;
    
    // 按照流程图顺序执行
    // 步骤1-4:扇区擦除(如果目标区域未擦除,需要先擦除)
    // 注意:实际应用中需要判断是否需要擦除,这里简化处理
    
    while(current_len > 0)
    {
        // 计算本次写入的数据长度(不超过页边界)
        if(current_len < page_remaining) {
            write_len = current_len;
        } else {
            write_len = page_remaining;
        }
        
        // 步骤5:页编程
        W25Q64_PageProgram(current_addr, pData, write_len);
        
        // 更新地址和长度
        current_addr += write_len;
        pData += write_len;
        current_len -= write_len;
        
        // 重置页偏移(下一页从头开始)
        page_remaining = 256;
    }
}


这里小编不想解释了,对于上述代码迷惑的可以问一下ai,其实也蛮好理解,这里交给你们了。

下面是读取数据

void W25Q64_ReadData(uint32_t addr, uint8_t* rxData, uint32_t len)
{
    uint8_t cmd[4];
    
    W25Q64_CS_LOW();
    
    // 发送读命令和地址
    cmd[0] = W25X_ReadData;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;
    
    SPI_SendMultiBytes_Fast(SPI1, cmd, NULL, 4);
    
    // 连续读取数据(不需要等待,直接读)
    for(uint32_t i = 0; i < len; i++) {
        // 发送任意数据(通常是0xFF),同时接收数据
        uint8_t dummy = 0xFF;
        SPI_SendMultiBytes_Fast(SPI1, &dummy, &rxData[i], 1);
    }
    
    W25Q64_CS_HIGH();
}

这里小编也不解释,小编相信你们能够自行理解

4.总结对比

下面是我根据AI生成的,基本正确。

对比维度 UART SPI I2C (IIC)
引脚数量 2根 (TX, RX) 4根 (SCK, MOSI, MISO, SS) 2根 (SCL, SDA)
连接拓扑 点对点 (一对一) 一主多从 (共享SCK/MOSI/MISO,从机独占SS) 多主多从 (所有设备并接到两根总线上)
同步方式 异步 (无时钟线) 同步 (主设备提供时钟) 同步 (主设备提供时钟)
传输方向 全双工 全双工 半双工
典型速度 低速:9600 ~ 115200 bps
(上限约5Mbps)
高速:10 ~ 50 MHz
(上限可达100MHz+)
中低速:标准100kbps,快速400kbps,高速3.4Mbps
寻址方式  (硬件连接确定)  (硬件片选SS确定)  (7位/10位设备地址)
硬件应答  (需软件协议补充)  (无应答位)  (每字节后含ACK/NACK位)
总线仲裁 不支持 不支持 支持 (多主设备可抢占总线)
电气特性 TTL电平 (需转换长距离) TTL/CMOS电平 (板内短距) 开漏输出,需上拉电阻 (板内短距)
协议复杂度 ⭐ 简单 ⭐⭐ 较简单 ⭐⭐⭐ 较复杂
抗干扰能力 较强 (差分RS-422/485时强) 一般 (高速时对布线敏感) 一般 (开漏结构抗干扰稍弱)
通信距离 中长 (配合驱动芯片可达千米) 短 (板内厘米级) 短 (板内厘米级)
典型外设 调试串口、GPS、蓝牙模块 Flash、LCD屏、ADC、SD卡 传感器(温度/湿度)、EEPROM、RTC

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐