电子入门教程[04]—喂?zaima?

电子入门教程[04]—喂?zaima?

在浏览此文之前,可能你需要先康康这个:

电子入门教程[03]—带上它的眼睛

在正式开始学习本文之前,你需要先自学以下知识:

  • C语言或C++
  • 安装Arduino IDE
  • 认识Arduino Uno开发板
  • 懂得如何使用面包板连接电路
  • 电子入门教程系列的前作(们)

前言

上一节,我们学会了ADC和DAC的基本概念,并且能使用Arduino UNO的Analog引脚采集电位器中间脚位上的变化电压。

这一节我们来讲讲单片机和从属设备间的通信协议(protocol)。

一.Protocol?

人类在日常生活中的交流非常简单,说话,手语,甚至眼神都能够传递信息。但是电子系统间的交流可没那么简单——你说话它也听不懂啊:)

那么在什么时候我们会用到通信协议(Communication protocol)呢?
比如我们上一节里使用的Arduino IDE自带的串口监视器,他就是在串行通信协议 (Serial Communication) 的基础上进行数据采集显示的。
也就是说,当我们想要在远端获取一个电子系统发送过来的信息(无论是什么信息)时,我们就需要用到通信协议。
比如我们需要获取输电线母线上的电压电流数据,我们又不可能派人上去测量(过于危险),我们就可以部署一套监视系统,通过网线或者别的数据线把采集到的数据传输回我们的输电控制中心来监测它的工作状态。

既然我们确定了需求(需要采集数据并且以可视化的形式展现出来),那么该怎样传输呢?
举个例子,在单片机和电脑的通讯过程中,我们可以使用最简单、最方便、最广泛使用的串口协议通讯。

二.通信协议

人交流也分很多种,我们可以说话,对方不在身边时也可以用QQ等实时聊天工具。不同的方式是为了适用于 不同的交流环境和条件的
电子设备间的通信协议大致可分为 串行通信并行通信 两种。

2.1-串行通信 & 并行通信

如果集成电路(单片机等)具有更多的引脚的话,那么它的价格通常会更加昂贵。而我们的通信需要通过I/O口的电平变化信号来实现,所以在为了减少I/O引脚的占用的场合,我们比较喜欢使用串行总线来传输数据。

典型的串行通信协议有:

  • UART 通用异步收发传输器 (Universal Asynchronous Receiver/Transmitter)
  • SPI 序列外设接口总线 (Serial Peripheral Interface Bus)
  • IIC 集成电路总线(简写为I2C) (Inter Intergrated Circuit)
  • 1-Wire 单总线协议
  • RS-485、RS-232、RS-422等等…
  • PCI-Express
  • and so on

而并行通信,会 占用集成电路更多的引脚,因为它是多bit数据同时通过并行线进行传送,这样的好处是传输速度大大提高了,但是并行通信受到传送线路长度的限制。 (因为长度增加,线路间的互相干扰和空间电磁波的干扰就会增加,数据也就容易出错)


1TKcqI.jpg


1TKyMd.jpg

看这2张对比图也许你会更清楚,同样是传输一组8位(8bit)数据:01100010,串行传输需要传8次,而并行传输只需要传输1次就可以了。

典型的并行通信协议有:

  • 并行通信 (没逗你,我是认真的.jpg)

2.2-物理层 & 协议层

先挖在这不填(●ˇ∀ˇ●)

串口(UART)通讯协议

通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,通常称为UART)
在UART上追加同步方式的序列信号变换电路的产品,被称为USART(Universal Synchronous Asynchronous Receiver Transmitter)

以上内容了解即可。具体我们还要来看实例。
我们为什么喜欢Arduino呢…因为它实在是太简单了。
在Arduino种使用串口,让UNO板向电脑发送一句问候语的代码如下:

#include <Arduino.h>

void setup()    //  Initialize function
{
    Serial.begin(9600); //  Init UART at baud rate 9600
}

void loop()     //  Main function
{
    Serial.println("Hello emoe-studio!");   // say hello
    delay(1000);    //  delay 1000ms.
}

代码中,Serial就是Arduino库中的串口类,关于串口的reference,可以在 这里 找到。

在这里列出几个常用的函数

  • begin() ->初始化函数
  • end() ->禁用串口通讯。释放RX和TX端口
  • print(), println() ->输出数据(以ASCII码形式)
  • read(), readBytes(), readBytesUntil() ->读取串口另一端发送过来的数据
  • find(), findUntil() ->从串口缓存读取数据直到读到指定的数据
  • avaliable() ->在串口缓存中查询可以被读取的bytes数量

那么在这个发送过程中,我们是怎样将数据传输出去的呢?

0. Connection

如果2个具有UART接口的芯片需要建立UART通讯,那么他们应该这样接线:


1zKy8J.jpg

RX 和 TX 分别代表 Receive(接收) 和Transmit(发送),两个系统的GND必须连在一起(因为需要有相同的参考电位)
理论上来说,如果左边的设备只发送不接收,而右边的设备只接收不发送的话,我们可以把连接左边设备RX端口和右边设备TX端口的那根线去掉,这样这个系统只需要一根数据线和一根共地线就可以达到我们的需求。

1st. Character -> ASCII Code

第一步,在单片机上完成—我们先将字符转换成硬件能够识别的ASCII码,就拿 ‘hello’ 来举例吧。


1zeMYd.png

查ASCII码表,得到’hello’对应的十进制数分别为’104 101 108 108 111′
‘hello’对应的二进制码流为’01101000 01100101 01101100 01101100 01101111’

(这一步不是我们完成的,是编译器帮我们解决的)
程序在进行编译时,遇到字符会自动把它们转换为ASCII码并存储到单片机的程序存储器或者数据存储器中。(具体存到哪里还是看代码怎么写)

2nd. 将二进制码通过数据帧发送出去

在这里我们开始揭露UART通信的底层过程。
Arduino的UART端口把数据的字节按照bit顺序发送(通常1字节=8bit),然后对面的UART接收端把bit组装成字节。具体怎么发送呢?还记得我们之前说的单片机的I/O口和二进制的对应关系么?
是的,我们就根据二进制码流来控制I/O口的电平高低来实现硬件传输数据。

但这时就有一个问题,我怎么知道我的通信何时开始,何时结束,哪几个脉冲组成1字节呢?
我们说它是通信协议,重点当然是在协议上:p

数据帧


1zuv9J.png

在UART的空闲状态(没有信号传输的时候),信号线上保持高电平(这是从电报时代的历史遗存,线路保持高电平表明它至少没有损坏(如果线路断了,那就是绝对的低电平))。
每个字节表示为一个,以逻辑低电平为开始位(Start bit),然后紧跟着的是8bit的数据比特流(bit stream),数据之后是一位可选的奇偶校验位(这个我们先不管),最后的是1位或者多位停止位(逻辑高电平)(如果只传送了一帧数据,后续不再有数据传输时线路将一直保持高电平,也就等效为多个停止位了,但是在数据源源不断连续传输时,停止位只有1位。)

如果线路长期保持低电平,这被UART检测为Break condition
奇偶校验位可选的意思是,你可以选择启用他或者禁用他,但是他一直都占着那一位(无论他被不被你认可),也就是说,UART的一个数据帧的长度基本是固定的。。。

时间!时间!时间!

我们刚刚似乎忽略了一个问题—每一位的传输时间是多长?
这个时间是由 波特率(Baud Rate) 决定的。
依据波特率,单片机会在内部产生一个时钟信号,通常是数据传输率的倍频(8或16倍)。
UART的发送和接收动作都由这个时钟信号控制。


1zQsXR.png

波形图

像这种以时间为横坐标,电压信号为纵坐标的图被称作 信号的时域波形图 (你没猜错,还有频域波形图),这是我们以后分析电路和程序工作的最重要也是最有力的手段之一,请务必要牢记呀!
虽然这个图中你看不到坐标轴,但你应该知道向右是时间流动的方向(怪怪的说法…)
另外,在逻辑电路(通常是数字电路)中,多个信号画在一起做对比时的图通常被称作 时序图(据说这个叫法不标准,应该叫顺序图,我也不是很清楚orz)

如上图所示,上面代表着一个UART数据帧,下面的波形信号就是时钟信号,从低电平变成高电平又变回低电平的过程,被称作 脉冲(Pulse)
在UART接收方,接收器在每个时钟脉冲到来时测试接收到的信号状态是否为Start bit,如果起始位的持续时间占到了传输一个bit所需时间的一半以上,就认为是开始了传输(这样判断是有依据的,想想为什么?)
在每个脉冲到来时,接收方都会检测数据传输线上的电平并记录下来,一位一位地保存到一个寄存器中(暂存数据),在计数达到8个之后,这时数据传输完毕,再进行可选的奇偶校验和结束位判别,一个数据帧结束时,单片机会产生一个信号 (通常是中断) 来通知CPU取走寄存器中的数据,为下一个数据帧的到来做准备。

而我们的波特率具体有什么实际意义吗?当然是代表传输数据的快慢啦。波特率越高,数据传输越快。
如何计算?比如我们的波特率是9600,那么每一位传输的时间是1/9600 s,意思就是说如果接连不断地发送数据帧,按照11bit长度计算,1秒钟可以发送9600/11=872.7,也就是差不多872个数据帧,也就是872字节。按照ASCII字母来说,872个英文字母/s,够直观了8~

常用的波特率有300,600,1200,2400,4800,9600,14400,19200,38400,56000,57600,115200,128000,256000,460800,921600,1500000.

了解了串口的原理,剩下的就是应用啦。

串口通信的Example

比如我们上一节教程就用到了串口。

int sensorPin = A0;     // ADC输入端口A0口
int digitalValue = 0;   // 一个用来储存ADC读书的变量

void setup() {
  Serial.begin(9600);   //初始化串口,波特率9600
}

void loop() {
  digitalValue = analogRead(sensorPin); //读取sensorPin脚上的电压
  Serial.print("digital value = ");     //向串口输出字符串"digital value = " 
  Serial.println(digitalValue);         //通过串口输出ADC得到的原始数字(以十进制),并换行  
  delay(1000);                          //延时1000ms(1s)
}

在这里我们用串口把ADC读取的原始数据通过串口发送到电脑上显示出来,提供了数据可视化的方式。串口在调试时非常有用,当你不知道一个数据是否正常时,你可以用串口把他输出到电脑上以便能直观地看到。
在做一些更复杂的工作时,比如你要采集10000个有物理意义的电压数据,然后用计算机工具或者算法对他们进行分析处理时,输出到串口是一个不错的选择。

其他的串行通信—IIC


1zd4Cn.png

其实呐,在单片机方面常用的通信协议就是串口,SPI,IIC,并行通信,等等…
IIC也是一个神奇的通信协议,我们一般读作 ‘I方C’ (简写是I2C嘛) ,IIC全程是集成电路总线(IIC Bus),它也是一种典型的串行通信总线,不过他跟串口在通讯机制上有着天壤之别~
1980年代,飞利浦 (对就是那个卖灯卖剃须刀的) 公司为了让主板、嵌入式系统或手机连接更多的低速周边设备,开发出了一种总线通信协议,就是窝们的I2C。自2006年起,使用I2C协议已经不需要支付专利费了,但是制造商仍然需要付费以获取I2C从属设备地址。

窝们现在可以在NXP半导体(前身就是飞利浦半导体,2006年改名恩智浦)的官网找到IIC的技术规格手册

I2C的特殊机制

在之前我们了解到,UART可以支持2个设备点对点的通信,但是在应对多个设备的通信需求时,显得有些力不从心。为什么呢ˋ( ° ▽、° )

假设我们有A、B、C三个设备需要通信,它们都带有UART接口,那么我们将AB的串口通信线和GND对应连接起来,这时AB可以通信,但如果想要C加入这个通信网络,我们会发现这是很困难的一件事情。

  • 如果将C的通信线直接对应接入AB的通信网络,所有的设备都会混乱
  • 如果我们用一个2位双向开关来切换连接到A设备的通信线,那么我们可以通过这个开关来切换B连接到A的串口或者C连接到A的串口,但这样做很麻烦,并且也不能满足ABC两两之间相互通信的需求。
  • 如果根据上一条的思路设计复杂的开关切换网络,确实有可能做到ABC两两之间的相互通信,但是整个系统的复杂度将呈直线上升(因为你需要更多的IO来控制那些额外的切换开关√)

这时我们需要新的解决方案。答案就是——I2C

想象过在3根线上挂载多达100个设备组成的数据传输链路吗?I2C能做到。
串口使用GND,RX,TX三条线来进行通信,并且使用通信设备内部产生的时钟信号来作为数据收发的基准(并且这个取决于波特率的时钟信号的速度必须提前协商好)
I2C的设计也只需要三条线,它们分别是SCL(串行时钟)、SDA(串行数据)、GND(电源地)。I2C的好处在于在一组总线上,它可以支持最多112个节点的通信(等会讲为什么是112),并且可以达到很高的通信速率。(超高速模式下5Mbit/s)

下图截自I2C官方文档,列出了一种I2C的典型应用


3d7syR.png

其实这个图还是挺复杂的。反正你只要知道 IIC总线上能挂一堆设备 就可以啦
比如,我有20多个使用I2C协议的数字传感器,如果我需要让他们同时工作并把数据发送到我的单片机上进行处理分析显示,我们只需要3根线:

  • SDA(Serial Data)
  • SCL(Serial Clock),有人写作SCK之类的,都是表示时钟线的意思
  • GND 共地,这个没什么好说的啦

0. 总线(BUS)

我们将所有的传感器的SDA、SCL、GND分别对应的连接起来,再接到单片机分配的SDA、SCL脚(就是2个IO)上,这时我们称这3根线组成了 IIC总线 ,再在SDA、SCL总线上分别接一个大小合适的上拉电阻(1k-10k之间,通常取4.7k,1k为强上拉,10k为弱上拉),因为I2C的端口是双向开漏输出结构,需要使用电阻上拉。

这里暂时不讲开漏输出,如果你学过模电和三极管的知识应该就懂了√
开漏输出如果不接上拉电阻,那么这个输出的电平状态是无法确定的,也就是I2C不能工作

1st. Address(地址)

我们在2条数据传输线上连接了这么多的从设备(Slave Device),我们该如何做到1对1的通信呢…
我们不妨来类比一下,我们的城镇街道是这样定位的:解放路1号、2号、233号…

这是什么? 这就是地址

也就是说我们使用一个独一无二的标识地址来确定一个地方的位置,同样在I2C通信的机制中,地址就是帮助主机(Master Device)与从机建立点对点通信的"指南针"

I2C的地址空间有7位或10位两种标准,具体取决于使用的设备,我们一般使用的时候是用7bit地址空间,也就是理论上多达128个从设备(0x00-0x7F)
但是实际没有这么多,I2C设计的时候保留了16个地址,所以一组I2C总线最多可以和112个节点通信。
实际使用时,每个I2C设备都将拥有一个独立的地址,如果有冲突,一般可以在一定范围内修改。如果实在不能修改(或者可修改的空间也被用完了),你可以再开一个新的I2C总线了~

2nd. 通信过程

我们不会像UART那样从最底层的时序讲起,这里只是讲个大概,有兴趣的同学可以去查更详细的资料~

1. I2C总线的操作模式

就像任何一个电子设备一样,I2C总线也有自己的小脾气。它有如下4种模式:

  • 主节点发送 – 主节点发送数据给从节点
  • 主节点接收 – 主节点接收从节点数据
  • 从节点发送 – 从节点发送数据给主节点
  • 从节点接收 – 从节点接收主节点数据

2. 跑起来

  1. 系统上电,初始化
  2. 主机处于主节点发送模式,发送起始位(START),紧跟着发送希望与之通信的从机的7bit地址,最后再发送一个1bit的读写位,告诉从机我是想要你还是想你要我(的数据)
  3. 如果从节点存在于总线上,且主机发送的地址跟从节点的物理地址对上了,它将以ACK位应答(低电平有效)。Master收到应答信号之后,根据之前发送的读写位,处于发送/接收数据模式,从机则处于对应的相反模式(接收/发送)。
  4. 地址和数据首先发送最高有效位。 起始位在SCL位高时,由SDA上电平从高变低表示;停止位在SCL为高时,由SDA上电平从低变高表示。其他SDA上的电平变化在SCL为低时发生。
  5. 如果主节点想要向从节点写数据,它将发送一个字节,然后从节点以ACK位应答,如此重复。此时,主节点处于主节点发送模式,从节点处于从节点接收模式。
  6. 如果主节点想要读取从节点数据,它将不断接收从节点发送的一个个字节,在收到每个字节后发送ACK进行应答,除了接收到的最后一个字节。此时,主节点处于主节点接收模式,从节点处于从节点发送模式。
  7. 此后,主节点要么发送停止位终止传输,要么发送另一个START比特以发起另一次传输(即“组合消息”)。

狂飙开始!

I2C总线的通信速度有上限,没有下限(其实还是有的,比如0.0001HZ的通信速率,你自己也想掐死自己吧…)

常见的I²C总线依传输速率的不同而有不同的模式:标准模式(100 kbit/s)、低速模式(10 kbit/s),但时钟频率可被允许下降至零,这代表可以暂停通信。
而新一代的I²C总线可以和更多的节点(支持10比特长度的地址空间)以更快的速率通信:快速模式(400 kbit/s)、快速plus模式(1 Mbit/s)高速模式(3.4 Mbit/s)超高速模式(5 Mbit/s)。

I2C通信的Example

在多数我们使用这些通信协议的时候,我们是不必要去深究他们的原理和细节的。(至少在用Arduino的时候不用)
现在的单片机都会有成熟的开发语言和工具,大大简化了开发流程、降低了开发难度,就如UART,我们可以很轻松地调用库函数来实现UART的全部功能,甚至它还把一些常用的功能也整合进去了(比如在接收的信息中查找特定关键字)

在Arduino中,Wire Library是I2C通信库。
你可以在 这里 找到一个Arduino的I2C通信示例,电路连接如下:


JCOEBn.png

I2C主机

// Wire Master Reader
// by Nicholas Zambetti 

// Demonstrates use of the Wire library
// Reads data from an I2C/TWI slave device
// Refer to the "Wire Slave Sender" example for use with this
// Created 29 March 2006
// This example code is in the public domain.
#include <Wire.h>

void setup()
{
  Wire.begin();        // join i2c bus (address optional for master)
  Serial.begin(9600);  // start Serial for output
}

void loop()
{
  Wire.requestFrom(2, 6);    // request 6 bytes from slave device #2

  while(Wire.available())    // slave may send less than requested
  { 
    char c = Wire.read(); // receive a byte as character
    Serial.print(c);         // print the character
  }

  delay(500);
}

I2C从机

// Wire Slave Receiver
// by Nicholas Zambetti 

// Demonstrates use of the Wire library
// Receives data as an I2C/TWI slave device
// Refer to the "Wire Master Writer" example for use with this
// Created 29 March 2006
// This example code is in the public domain.
#include <Wire.h>

void setup()
{
  Wire.begin(4);                // join i2c bus with address #4
  Wire.onReceive(receiveEvent); // register event
  Serial.begin(9600);           // start serial for output
}

void loop()
{
  delay(100);
}

// function that executes whenever data is received from master
// this function is registered as an event, see setup()
void receiveEvent(int howMany)
{
  while(1 < Wire.available()) // loop through all but the last
  {
    char c = Wire.read(); // receive byte as a character
    Serial.print(c);         // print the character
  }
  int x = Wire.read();    // receive byte as an integer
  Serial.println(x);         // print the integer
}

下节课啊!

我太鸽了,我反省.jpg 鸽子永不承认自己咕咕!
下一篇文章我打算讲讲通信过程中的物理层和协议层,拓展一下单片机的知识√

敬请期待。
(希望不鸽)

发表评论