单片机数据传输概述

单片机(Microcontroller Unit, MCU)作为嵌入式系统的核心组件,其数据传输能力直接影响整个系统的性能和可靠性。数据传输是指单片机内部各模块之间或单片机与外部设备之间交换信息的过程。理解单片机数据传输的类型、串行与并行通信方式,以及常见协议如SPI、I2C和UART,对于嵌入式系统设计至关重要。

单片机数据传输主要分为内部传输和外部传输两大类。内部传输发生在芯片内部,如CPU与RAM、ROM、外设寄存器之间的交互,通常通过内部总线(如AHB、APB)实现,速度极快且延迟低。外部传输则涉及单片机与外部设备(如传感器、存储器、显示屏或其他MCU)的通信,这是设计中需要重点考虑的部分,因为外部传输受限于物理连接、噪声环境和功耗等因素。

从传输方向来看,数据传输可以是单工(Simplex,数据单向流动)、半双工(Half-Duplex,数据可双向但不能同时)或全双工(Full-Duplex,数据可同时双向流动)。从同步方式来看,可分为同步传输(需要时钟信号同步)和异步传输(无需时钟,使用起始/停止位)。这些分类帮助工程师根据应用场景选择合适的传输方式。

在实际应用中,单片机数据传输的选择取决于多个因素:数据速率需求、传输距离、连接线数量、功耗限制和成本。例如,高速数据采集系统可能需要并行传输或高速串行协议,而低功耗传感器网络则更适合I2C或UART。接下来,我们将详细探讨串行与并行通信方式,并深入解析SPI、I2C和UART协议。

串行与并行通信方式

并行通信方式

并行通信是一种同时传输多个数据位的通信方式,通常使用多根数据线(如8位数据总线使用8根线)。每个数据位都有独立的传输通道,因此在短距离内可以实现高吞吐量。并行通信的优点是速度快、实时性好,特别适合需要高速数据传输的场景,如单片机与外部存储器(SRAM、DRAM)或高速ADC/DAC的连接。

然而,并行通信也有显著缺点:随着传输距离增加,信号容易出现时钟偏移(Clock Skew)和电磁干扰(EMI),导致数据错误。此外,多根线增加了硬件复杂度和成本,不适合紧凑型设计。在单片机中,并行通信常用于内部总线或短距离外部接口,如8051系列的外部存储器接口。

并行通信示例:假设使用8位并行总线从单片机向外部RAM写入数据。单片机需要提供地址线(如16位地址需要16根线)、数据线(8根)和控制线(如读写信号、片选信号)。总线可能需要数十根线,传输一个字节只需一个时钟周期。

串行通信方式

串行通信是逐位传输数据的方式,使用一根或几根线即可完成通信。数据按顺序发送,通常需要时钟信号(同步)或起始/停止位(异步)。串行通信的优点是线缆少、成本低、抗干扰能力强,适合长距离传输和多设备连接。缺点是相对并行通信速度较慢,但现代高速串行协议(如USB、Ethernet)已大大弥补了这一不足。

串行通信可分为同步和异步两种:

  • 同步串行通信:需要共享时钟信号,数据传输连续高效,如SPI和I2C。
  • 异步串行通信:无需时钟,使用固定波特率(Baud Rate)和帧格式(起始位、数据位、奇偶校验位、停止位),如UART。

在单片机应用中,串行通信因其灵活性和低引脚占用而广泛使用。例如,Arduino Uno有14个数字I/O引脚,其中多个可用于串行通信,而并行通信可能占用所有引脚。

串行与并行比较表(以8位数据传输为例):

特性 并行通信 串行通信
数据线数量 8根(数据)+ 多根控制线 1-2根(数据+时钟/地)
速度 高(短距离) 中低(但可优化)
距离 短(米) 长(可达数米至千米)
成本 高(线缆和引脚多)
抗干扰 差(易受噪声影响)
应用场景 内部总线、存储器 传感器、通信模块

通过这些比较,我们可以看到串行通信在现代嵌入式设计中更受欢迎,因为它平衡了性能和资源消耗。

SPI协议详解

SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的串行通信协议,由Motorola公司开发。它主要用于单片机与外围设备(如闪存、SD卡、显示屏、传感器)之间的短距离通信。SPI支持多主从架构,但通常为单主多从模式。

SPI基本原理

SPI使用四根线(有时三根,如果单向传输):

  • SCLK(Serial Clock):时钟信号,由主设备(Master)生成,用于同步数据传输。时钟频率可高达数十MHz。
  • MOSI(Master Out Slave In):主设备输出,从设备输入的数据线。
  • MISO(Master In Slave Out):主设备输入,从设备输出的数据线。
  • CS/SS(Chip Select / Slave Select):片选信号,由主设备控制,用于选择哪个从设备通信。每个从设备需要一根独立的CS线。

SPI是全双工的,意味着数据可以同时发送和接收。传输过程由主设备发起,通过拉低CS线选中从设备,然后在SCLK的驱动下,MOSI和MISO同时交换数据。数据位宽通常为8位,但可配置为其他值。

SPI工作模式

SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:

  • Mode 0 (CPOL=0, CPHA=0):时钟空闲低电平,数据在SCLK上升沿采样。
  • Mode 1 (CPOL=0, CPHA=1):时钟空闲低电平,数据在SCLK下降沿采样。
  • Mode 2 (CPOL=1, CPHA=0):时钟空闲高电平,数据在SCLK下降沿采样。
  • Mode 3 (CPOL=1, CPHA=1):时钟空闲高电平,数据在SCLK上升沿采样。

主从设备必须匹配相同模式才能通信。

SPI代码示例

以下是一个使用Arduino(基于AVR单片机)实现SPI主设备通信的详细代码示例。假设我们连接一个SPI从设备(如25LC256 EEPROM),主设备发送一个字节并接收响应。代码使用Arduino的SPI库,但我们会详细解释每个步骤。

#include <SPI.h>  // 包含SPI库

// 定义片选引脚
const int csPin = 10;  // 数字引脚10作为CS

void setup() {
  Serial.begin(9600);  // 初始化串口监视器
  pinMode(csPin, OUTPUT);  // 设置CS为输出
  digitalWrite(csPin, HIGH);  // 初始高电平(未选中)
  
  // 初始化SPI:设置主模式、时钟分频(例如1MHz)、数据模式0
  SPI.begin();  // 启用SPI
  // SPI.setClockDivider(SPI_CLOCK_DIV16);  // 可选:设置时钟分频,16MHz/16=1MHz
  // SPI.setDataMode(SPI_MODE0);  // 模式0
  // SPI.setBitOrder(MSBFIRST);  // 先发高位
}

void loop() {
  // 假设发送命令0x03(读取数据)和地址0x0000
  byte command = 0x03;  // 读取命令
  byte addressHigh = 0x00;  // 地址高字节
  byte addressLow = 0x00;   // 地址低字节
  
  // 选中从设备:拉低CS
  digitalWrite(csPin, LOW);
  
  // 发送命令和地址(3字节),同时接收数据
  byte receivedByte1 = SPI.transfer(command);  // 发送命令,接收第一个字节(可能为0或状态)
  byte receivedByte2 = SPI.transfer(addressHigh);  // 发送地址高字节,接收第二个字节
  byte receivedByte3 = SPI.transfer(addressLow);   // 发送地址低字节,接收第三个字节
  byte receivedData = SPI.transfer(0x00);  // 发送0x00(哑字节),接收实际数据
  
  // 释放CS:拉高CS
  digitalWrite(csPin, HIGH);
  
  // 打印接收到的数据
  Serial.print("Received Data: ");
  Serial.println(receivedData, HEX);
  
  delay(1000);  // 每秒传输一次
}

代码详细解释

  1. 包含库和初始化SPI.h提供SPI功能。setup()中设置CS引脚为输出,初始高电平(从设备未选中)。SPI.begin()初始化SPI硬件。
  2. 传输过程SPI.transfer(byte)函数发送一个字节并返回接收到的字节。在循环中,我们模拟读取EEPROM:先发送读命令(0x03),然后发送2字节地址,最后发送哑字节(0x00)来获取数据。整个过程在CS拉低期间完成。
  3. 时序控制:CS拉低表示传输开始,拉高结束。时钟由硬件自动生成,基于SCLK引脚。
  4. 实际应用:这个示例可扩展为读写EEPROM。调试时,用示波器观察SCLK、MOSI和MISO波形,确保模式匹配。

SPI的优势在于高速(可达100Mbps以上)和简单,但缺点是需要多根线,且每个从设备需独立CS,不适合引脚紧张的场景。

I2C协议详解

I2C(Inter-Integrated Circuit,集成电路总线)是一种半双工、同步的串行通信协议,由Philips(现NXP)开发。它使用两根线连接多个设备,支持多主从架构,常用于低速传感器、EEPROM和实时时钟(RTC)等应用。I2C设计为低引脚数、低成本的解决方案。

I2C基本原理

I2C使用两根线:

  • SDA(Serial Data):双向数据线,所有设备共享。
  • SCL(Serial Clock):时钟线,由主设备控制,但所有设备可拉低它以实现时钟同步(时钟拉伸)。

I2C是半双工的,数据在SCL的驱动下逐位传输。每个设备有唯一的7位或10位地址(通常7位,支持128个地址)。传输以起始条件(Start,SCL高时SDA从高到低)开始,以停止条件(Stop,SCL高时SDA从低到高)结束。

数据帧格式:起始位 + 7位地址 + 读/写位(0写,1读) + ACK/NACK(应答/非应答) + 数据字节 + ACK/NACK + … + 停止位。每个字节后都有ACK(接收方拉低SDA表示确认)。

I2C速度模式:

  • 标准模式:100kbps
  • 快速模式:400kbps
  • 高速模式:3.4Mbps

I2C工作流程

主设备发起通信,发送地址选中从设备,然后交换数据。从设备通过ACK确认。如果总线忙,主设备等待。I2C支持广播地址(全1)和通用调用。

I2C代码示例

以下是一个使用Arduino作为主设备,与I2C从设备(如DS3231 RTC模块)通信的详细代码示例。主设备向RTC写入时间,然后读取当前时间。代码使用Wire库。

#include <Wire.h>  // 包含Wire库(I2C库)

// RTC设备地址(7位):0x68 (1101000)
#define RTC_ADDR 0x68

void setup() {
  Serial.begin(9600);
  Wire.begin();  // 初始化I2C作为主设备
}

void loop() {
  // 步骤1:向RTC写入时间(设置秒、分、时等,假设初始设置)
  Wire.beginTransmission(RTC_ADDR);  // 开始传输到RTC
  Wire.write(0x00);  // 寄存器地址0(秒寄存器)
  Wire.write(0x00);  // 秒=0
  Wire.write(0x30);  // 分=30
  Wire.write(0x12);  // 时=12 (24小时制)
  Wire.endTransmission();  // 结束传输,发送停止条件
  
  delay(100);  // 短暂延迟
  
  // 步骤2:从RTC读取当前时间
  Wire.beginTransmission(RTC_ADDR);  // 开始传输
  Wire.write(0x00);  // 指定读取寄存器0(秒)
  Wire.endTransmission(false);  // 结束传输但不发送停止(重复起始)
  
  Wire.requestFrom(RTC_ADDR, 3);  // 请求3字节(秒、分、时)
  
  if (Wire.available() == 3) {
    byte seconds = Wire.read();  // 读取秒
    byte minutes = Wire.read();  // 读取分
    byte hours = Wire.read();    // 读取时
    
    // 打印时间(BCD格式,需转换)
    Serial.print("Time: ");
    Serial.print(hours >> 4, DEC); Serial.print((hours & 0x0F), DEC); Serial.print(":");
    Serial.print(minutes >> 4, DEC); Serial.print((minutes & 0x0F), DEC); Serial.print(":");
    Serial.print(seconds >> 4, DEC); Serial.println((seconds & 0x0F), DEC);
  } else {
    Serial.println("I2C Read Error");
  }
  
  delay(5000);  // 每5秒读取一次
}

代码详细解释

  1. 包含库和初始化Wire.h提供I2C功能。Wire.begin()初始化主设备。
  2. 写入过程beginTransmission(addr)开始传输,write(data)发送数据(包括寄存器地址和值),endTransmission()结束传输,发送停止条件。这里我们设置RTC时间(BCD格式)。
  3. 读取过程:先发送寄存器地址,然后requestFrom(addr, numBytes)请求数据。Wire.available()检查可用字节,Wire.read()逐字节读取。注意endTransmission(false)用于重复起始(不发送停止),这是读取多字节的常见技巧。
  4. 数据处理:RTC数据是BCD(二进制编码十进制),需移位和掩码转换。调试时,可用逻辑分析仪捕获SDA/SCL波形,观察ACK位。
  5. 实际应用:这个示例适用于时钟同步系统。I2C的多设备能力允许连接多个传感器,总线长度可达几米,但需上拉电阻(通常4.7kΩ)。

I2C的优点是引脚少、支持多设备,但缺点是速度较低、半双工,且总线仲裁复杂(多主时)。

UART协议详解

UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是一种异步、全双工的串行通信协议,用于单片机与PC、模块(如GPS、蓝牙)或另一个UART设备的通信。它不需要共享时钟,使用固定波特率传输。

UART基本原理

UART使用两根线(或一根,如果单向):

  • TX(Transmit):发送线,从单片机输出。
  • RX(Receive):接收线,输入到单片机。

数据帧格式:起始位(1位,低电平) + 数据位(5-9位,通常8位) + 可选奇偶校验位(1位) + 停止位(1-2位,高电平)。传输以起始位开始,停止位结束。波特率(如9600、115200)必须匹配两端。

UART是全双工的,支持异步通信,适合点对点连接。常见变体包括RS-232(长距离,负逻辑)和TTL电平(单片机内部,正逻辑)。

UART工作流程

发送方按波特率逐位发送数据,接收方采样RX线。起始位检测后,采样数据位,检查停止位。如果校验失败,可丢弃数据。UART通常有缓冲区(FIFO)处理多字节。

UART代码示例

以下是一个使用Arduino实现UART通信的详细代码示例。主设备(Arduino)通过串口向PC发送字符串,并从PC接收命令控制LED。代码使用Serial库。

// 无额外库,Serial已内置

const int ledPin = 13;  // 内置LED引脚

void setup() {
  Serial.begin(9600);  // 初始化串口,波特率9600
  pinMode(ledPin, OUTPUT);  // 设置LED为输出
  digitalWrite(ledPin, LOW);
  
  // 等待串口连接(仅USB串口需要)
  while (!Serial) {
    ;  // 等待
  }
  
  Serial.println("UART Ready - Send 'ON' to turn LED on, 'OFF' to turn off");
}

void loop() {
  // 步骤1:发送数据到PC(模拟周期性报告)
  static unsigned long lastSend = 0;
  if (millis() - lastSend > 2000) {  // 每2秒发送一次
    Serial.print("LED Status: ");
    Serial.println(digitalRead(ledPin) ? "ON" : "OFF");
    lastSend = millis();
  }
  
  // 步骤2:从PC接收数据
  if (Serial.available() > 0) {
    String received = Serial.readStringUntil('\n');  // 读取直到换行符
    received.trim();  // 去除空白
    
    if (received == "ON") {
      digitalWrite(ledPin, HIGH);
      Serial.println("LED turned ON");
    } else if (received == "OFF") {
      digitalWrite(ledPin, LOW);
      Serial.println("LED turned OFF");
    } else {
      Serial.println("Invalid command - Send 'ON' or 'OFF'");
    }
  }
  
  delay(10);  // 小延迟避免忙等
}

代码详细解释

  1. 初始化Serial.begin(9600)设置波特率。while(!Serial)等待PC连接(仅USB)。Serial.println()发送带换行的字符串。
  2. 发送数据:在loop()中,使用millis()定时器每2秒发送LED状态。Serial.print()println()用于格式化输出。
  3. 接收数据Serial.available()检查接收缓冲区字节数。Serial.readStringUntil('\n')读取整行(直到换行)。然后解析命令:如果”ON”,点亮LED并回复;”OFF”熄灭;否则报错。
  4. 调试与应用:在Arduino IDE的串口监视器中测试,确保波特率匹配。实际应用中,UART常用于日志输出、命令行接口或与模块通信(如ESP8266 WiFi模块)。如果长距离,使用RS-232转换器。

UART的优点是简单、异步,无需时钟线,但缺点是需要精确匹配波特率,且不适合多设备总线(需额外硬件如多路复用器)。

总结与应用建议

单片机数据传输类型包括内部总线和外部通信,外部传输以串行为主,因其高效和灵活。串行通信(如SPI、I2C、UART)优于并行通信在大多数嵌入式场景中,因为它节省引脚和成本。

  • SPI:高速、全双工,适合存储器和显示屏。选择时注意时钟模式和CS线数量。
  • I2C:低引脚、多设备,适合传感器网络。需上拉电阻,避免总线冲突。
  • UART:异步、全双工,适合点对点调试和模块通信。波特率匹配是关键。

在设计时,评估需求:高速用SPI,低引脚用I2C,简单调试用UART。结合示例代码,从简单原型开始测试,逐步集成到系统中。实际项目中,还需考虑噪声抑制(如屏蔽线)和错误处理(如CRC校验)。通过这些协议,单片机可高效连接外部世界,实现复杂功能如物联网设备或机器人控制。