以上视频是我对3种协议的讲解。

对比表

我们也可以看一下,UART、IIC和SPI对比表

特性 串口 IIC SPI
通信速率(最大) 230400 bps 400 kbps(快速) 高达72 MHz(主机)
通信线数量 2 根 2 根 4 根
通信距离 短距离 短距离 短距离
通信协议 异步串行 同步串行 同步串行
硬件复杂度 中等 中等
软件复杂度 中等 中等
主从设备支持 支持(从设备) 支持(主/从设备) 支持(主/从设备)
多设备通信 支持 支持 支持
数据传输完整性

UART其实也是可以进行多设备通信。在多设备通信时,需要使用分时复用技术或者基于协议的多点通信技术。其中分时复用技术将多个设备连接到同一串口,通过在不同时间间隔内交替发送数据来实现多设备通信;而基于协议的多点通信技术则使用特定的通信协议来允许多个设备连接到同一串口进行通信。因此,在选择通信协议时,需要考虑实际应用需求以及硬件和软件资源的限制。

6个demo

以下我用51单片机和STM32单片机分别做了6个demo,仅供参考

UART,51版本

这个程序使用定时器1控制串口通信的波特率,其中FREQ和BAUD分别表示单片机的工作频率和波特率。在初始化函数init_serial()中,将定时器1配置为模式2,并计算出需要设定的初值,然后启动定时器1。同时,将串口配置为模式1,允许接收,开启串口中断并开启总中断。在串口中断处理函数serial_interrupt()中,处理接收到的数据和发送下一条数据。主循环中可以进行其他任务的处理。注意,需要根据实际的硬件接口来配置串口相关的引脚和波特率等参数。

#include<reg52.h>

#define FREQ 11059200 // 单片机工作频率
#define BAUD 9600 // 波特率

void init_serial() {
  TMOD &= 0x0F;
  TMOD |= 0x20; // 定时器1工作在模式2
  TH1 = 256 - FREQ / (BAUD * 32);
  TR1 = 1; // 启动定时器1
  SCON = 0x50; // 串口工作在模式1,允许接收
  ES = 1; // 开启串口中断
  EA = 1; // 开启总中断
}

void serial_interrupt() interrupt 4 {
  if (RI) {
    RI = 0; // 清除接收中断标志
    // 处理接收到的数据
    // ...
  }
  if (TI) {
    TI = 0; // 清除发送中断标志
    // 发送下一条数据
    // ...
  }
}

void main() {
  init_serial();
  // 主循环
  while(1) {
    // ...
  }
}

UART,STM32版本

这个程序使用USART1控制串口通信的波特率,其中FREQ和BAUD分别表示单片机的工作频率和波特率。在初始化函数init_serial()中,先使能USART1和GPIOA的时钟,然后配置PA9为复用推挽输出模式,PA10为浮空输入模式。接着,配置USART1的参数,包括波特率、字长、停止位、校验位等等,并使能USART1。发送一个字节时,先等待发送缓冲区为空,然后调用USART_SendData()发送一个字节。接收一个字节时,先等待接收缓冲区非空,然后调用USART_ReceiveData()读取一个字节。

STm32F103单片机控制串口通信的简单程序,波特率为9600:

#include "stm32f10x.h"

#define FREQ 72000000 // 单片机工作频率
#define BAUD 9600 // 波特率

void init_serial() {
  // 使能USART1和GPIOA的时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

  // 配置PA9为复用推挽输出模式,PA10为浮空输入模式
  GPIO_InitTypeDef gpio_init;
  gpio_init.GPIO_Pin = GPIO_Pin_9;
  gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
  gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &gpio_init);

  gpio_init.GPIO_Pin = GPIO_Pin_10;
  gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(GPIOA, &gpio_init);

  // 配置USART1的参数
  USART_InitTypeDef usart_init;
  usart_init.USART_BaudRate = BAUD;
  usart_init.USART_WordLength = USART_WordLength_8b;
  usart_init.USART_StopBits = USART_StopBits_1;
  usart_init.USART_Parity = USART_Parity_No;
  usart_init.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  usart_init.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  USART_Init(USART1, &usart_init);

  // 使能USART1
  USART_Cmd(USART1, ENABLE);
}

void send_byte(uint8_t byte) {
  // 等待发送缓冲区为空
  while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
  // 发送一个字节
  USART_SendData(USART1, byte);
}

uint8_t receive_byte() {
  // 等待接收缓冲区非空
  while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
  // 读取一个字节
  return USART_ReceiveData(USART1);
}

int main() {
  init_serial();
  // 主循环
  while(1) {
    // 发送数据
    send_byte(0x55);
    // 接收数据
    uint8_t received = receive_byte();
    // 处理接收到的数据
    // ...
  }
  return 0;
}

IIC,51单片机

这个程序使用P1_0和P1_1分别控制I2C总线的数据线SDA和时钟线SCL。在初始化函数init_i2c()中,先将SDA和SCL置为高电平。发送起始信号时,将SDA置为高电平,SCL置为高电平,等待一段时间,然后将SDA置为低电平,等待一段时间后再将SC置为低电平,以此来发送起始信号。发送停止信号时,先将SDA置为低电平,SCL置为高电平,等待一段时间,然后将SDA置为高电平,等待一段时间后再将SCL置为高电平,以此来发送停止信号。

发送一个字节时,将字节的每一位从高位到低位依次写入SDA,然后将SCL置为高电平,等待一段时间,再将SCL置为低电平,等待一段时间后将字节向左移位。写入字节的同时,可以将ACK信号读取出来,ACK为0表示接收到数据,ACK为1表示接收数据出现了错误。

读取一个字节时,先将SDA置为高电平,然后从高位到低位依次读取SDA上的数据,并将它们组成一个字节,最后向左移位。在读取每一位数据时,先将SCL置为高电平,等待一段时间后再将SCL置为低电平,等待一段时间后再读取SDA上的数据。最后,将ACK或NAK信号写入SDA,然后再将SCL置为高电平,等待一段时间后再将SCL置为低电平,以此来发送ACK或NAK信号。

注意,在每次读取和写入数据之前,都需要发送起始信号。这里给出的是一个简单的I2C通信程序,可以根据具体的应用需求进行修改和扩展。

#include <reg51.h>

#define FREQ 11059200 // 单片机工作频率
#define SDA P1_0 // I2C数据线
#define SCL P1_1 // I2C时钟线

void delay_us(unsigned int t) {
  while (t--) {
    /* 空循环 */
  }
}

void start_i2c() {
  SDA = 1;
  SCL = 1;
  delay_us(5);
  SDA = 0;
  delay_us(5);
  SCL = 0;
}

void stop_i2c() {
  SDA = 0;
  SCL = 1;
  delay_us(5);
  SDA = 1;
  delay_us(5);
}

unsigned char write_byte(unsigned char byte) {
  unsigned char ack;
  unsigned char i;
  for (i = 0; i < 8; i++) {
    if (byte & 0x80) {
      SDA = 1;
    } else {
      SDA = 0;
    }
    SCL = 1;
    delay_us(5);
    SCL = 0;
    byte <<= 1;
  }
  SDA = 1;
  SCL = 1;
  delay_us(5);
  ack = SDA;
  SCL = 0;
  return ack;
}

unsigned char read_byte(unsigned char ack) {
  unsigned char byte = 0;
  unsigned char i;
  SDA = 1;
  for (i = 0; i < 8; i++) {
    byte <<= 1;
    SCL = 1;
    delay_us(5);
    if (SDA) {
      byte |= 0x01;
    }
    SCL = 0;
  }
  if (ack) {
    SDA = 0;
  } else {
    SDA = 1;
  }
  SCL = 1;
  delay_us(5);
  SCL = 0;
  SDA = 1;
  return byte;
}

void init_i2c() {
  SDA = 1;
  SCL = 1;
  delay_us(5);
}

int main() {
  init_i2c();
  start_i2c();
  write_byte(0xA0); // 写器件地址
  write_byte(0x00); // 写寄存器地址
  write_byte(0x55); // 写数据
  stop_i2c();
  start_i2c();
  write_byte(0xA0); // 写器件地址
  write_byte(0x00); // 写寄存器地址
  start_i2c();
  write_byte(0xA1); // 读器件地址
  unsigned char data = read_byte(0); // 读数据
  stop_i2c();
  return 0;
}

IIC,STM32单片机

以下是一个简单的I2C通信程序,使用STM32 HAL库来实现。这个程序可以向I2C设备发送数据,并从I2C设备读取数据。
首先需要在STM32的CubeMX中配置I2C外设,并将其设置为主机模式。在代码中,使用了默认的I2C1外设。

#include "main.h"
#include "stm32f1xx_hal.h"

I2C_HandleTypeDef hi2c1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_I2C1_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_I2C1_Init();

  uint8_t data_to_send = 0xAA;  // 要发送的数据
  uint8_t data_received;        // 接收到的数据

  while (1)
  {
    // 发送数据
    HAL_I2C_Master_Transmit(&hi2c1, 0x50, &data_to_send, 1, 1000);

    // 等待一段时间
    HAL_Delay(100);

    // 读取数据
    HAL_I2C_Master_Receive(&hi2c1, 0x50, &data_received, 1, 1000);
    …………
  }
}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                              | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_I2C1_Init(void)
{
  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 100000;
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler();
  }
}

SPI,51单片机

下面是一个简单的SPI程序示例,使用51单片机的SPI接口与一个SPI设备进行通信。假设单片机时钟频率为12MHz,SPI时钟频率为1MHz,SPI设备的CS引脚接在P1.4上,发送的数据为0x55,接收的数据存储在一个变量中:

在这个程序中,首先使用SPI_Init函数初始化SPI接口,设置了波特率为1MHz,时钟极性为低电平,时钟相位为第一边沿,SPI时钟分频为4,主机模式。然后在主函数中,使用SPI_Transfer函数向SPI设备发送一个字节的数据,同时接收从设备返回的数据,并将接收到的数据存储在一个变量中。在SPI_Transfer函数中,首先选中SPI设备(即拉低CS引脚),然后通过串口发送数据,等待发送和接收完成,并取消SPI设备的选中状态,最后返回接收到的数据。

需要注意的是,这个程序仅发送和接收了一个字节的数据,如果需要发送更多的数据,可以通过循环调用SPI_Transfer函数来完成。此外,程序中的波特率和SPI时钟分频需要根据具体应用需要进行调整,还需要根据实际连接的SPI设备的时钟特性进行调整。

#include <reg52.h>

sbit CS = P1^4;  // 定义CS引脚

void SPI_Init(void)
{
  TMOD = 0x00;  // 配置为模式0,同时清除定时器/计数器的设置
  SCON = 0x50;  // 配置为模式1
  PCON |= 0x80; // 将SMOD置1,使串口波特率翻倍
  TH1 = 0xFA;   // 设置波特率为9600bps
  TL1 = 0xFA;
  TR1 = 1;      // 启动定时器/计数器1
  ES = 0;       // 禁止串口中断
  SS = 1;       // 禁止从机模式
  CPHA = 0;     // 时钟极性为低电平
  CPOL = 0;     // 时钟相位为第一边沿
  SPR1 = 0;     // 设置SPI时钟分频为4
  SPR0 = 0;
  MSTR = 1;     // 主机模式
  SPIEN = 1;    // 启动SPI
}

unsigned char SPI_Transfer(unsigned char data)
{
  CS = 0;       // 选中SPI设备
  SBUF = data;  // 发送数据
  while(!TI);   // 等待发送完成
  TI = 0;
  ……
  }

SPI,STM32单片机

使用STM32单片机的SPI接口与一个SPI设备进行通信。假设单片机时钟频率为72MHz,SPI时钟频率为1MHz,SPI设备的CS引脚接在PA4上,发送的数据为0x55,接收的数据存储在一个变量中:

在这个程序中,首先使用SPI_Init函数初始化SPI接口,设置了SPI模式为主机模式,数据传输大小为8位,时钟极性为低电平,时钟相位为第一边沿,SPI时钟分频为32。然后在主函数中,使用SPI_Transfer函数向SPI设备发送一个字节的数据,同时接收从设备返回的数据,并将接收到的数据存储在一个变量中。在SPI_Transfer函数中,首先选中SPI设备(即拉低CS引脚),然后通过HAL库的HAL_SPI_TransmitReceive函数发送和接收数据,并取消SPI设备的选中状态,最后返回接收到的数据。

需要注意的是,这个程序仅发送和接收了一个字节的数据,如果需要发送更多的数据,可以通过循环调用SPI_Transfer函数来完成。此外,程序中的SPI时钟分频需要根据实际连接的SPI设备的时钟特性进行调整。

#include "stm32f1xx_hal.h"

SPI_HandleTypeDef hspi1;

void SPI_Init(void)
{
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 10;
  HAL_SPI_Init(&hspi1);
}

unsigned char SPI_Transfer(unsigned char data)
{
  unsigned char recv_data;
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
  HAL_SPI_TransmitReceive(&hspi1, &data, &recv_data, 1, 100);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
  return recv_data;
}

int main(void)
{
  unsigned char data;
  HAL_Init();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_SPI1_CLK_ENABLE();
  GPIO_InitTypeDef GPIO_InitStruct;
  GPIO_InitStruct.Pin = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  SPI_Init();
  data = SPI_Transfer(0x55);
  while (1)
  {
  ………………
  }
}