串行通信协议用于芯片间的数据交互。常用的串行通信协议包括单总线、I2C、SPI、UART。最初,串行通信协议种类太多,给芯片设计者和使用方都造成了很大的不便。后来,经过飞利浦等公司的共同努力,才形成了I2C、SPI、UART等通用串行通信协议。而单总线因为其通信效率低,只在少数场合中有应用,并未形成通用的协议,仍需要根据具体的芯片手册进行协议的软件实现。
通用串行通信协议具有完善而灵活的通信功能,很多外围芯片都根据通用串行通信协议设计其通信接口电路,所以大部分微控制器/微处理器也都集成了通用串行通信协议的硬件实现电路。在此基础上,程序员通常只需要简单配置微控制器/微处理器中的串行通信协议的工作方式就可以和外围芯片进行数据交互了。而在一些情况下(比如微控制器/微处理器没有集成所需的通用串行通信协议或外围芯片的串行通信协议和通用串行通信协议有些出入),程序员就需要通过软件去实现串行通信协议。
通用串行通信协议用于交互一个或多个字节,这些字节对于不同的外围芯片具有不同的含义。因此,在设计程序时,需要设计通用驱动代码和专用驱动代码两个部分。通用驱动代码就是通用串行通信协议的代码,专用驱动代码就是利用具体芯片内部各种寄存器进行读写操作的代码。
第一部分 单总线
参考资料:《DS18B20.pdf》《http://c.biancheng.net/cpp/html/1958.html》
单总线通信协议因芯片不同有明显差异,这一部分以常见的DS18B20为例介绍其单总线通信协议的实现。
1.1 协议说明
1.1.1 初始化
和 I2C 的寻址类似,1-Wire 总线开始也需要检测这条总线上是否存在 DS18B20这个器件。如果这条总线上存在 DS18B20,总线会根据时序要求返回一个低电平脉冲,如果不存在的话,也就不会返回脉冲,即总线保持为高电平,所以习惯上称之为检测存在脉冲。此外,获取存在脉冲不仅仅是检测是否存在 DS18B20,还要通过这个脉冲过程通知 DS18B20准备好,单片机要对它进行操作了。
存在脉冲检测过程,首先单片机要拉低这个引脚,持续大概 480us 到 960us 之间的时间即可,我们的程序中持续了 500us。然后,单片机释放总线,就是给高电平,DS18B20 等待大概 15 到 60us 后,会主动拉低这个引脚大概是 60 到 240us,而后 DS18B20 会主动释放总线,这样 IO 口会被上拉电阻自动拉高。
有的同学还是不能够彻底理解,程序列出来逐句解释。首先,由于 DS18B20 时序要求非常严格,所以在操作时序的时候,为了防止中断干扰总线时序,先关闭总中断。然后第一步,拉低 DS18B20 这个引脚,持续 500us;第二步,延时 60us;第三步,读取存在脉冲,并且等待存在脉冲结束。
1.1.2 ROM操作指令
我们学 I2C 总线的时候就了解到,总线上可以挂多个器件,通过不同的器件地址来访问不同的器件。同样,1-Wire 总线也可以挂多个器件,但是它只有一条线,如何区分不同的器件呢?
在每个 DS18B20 内部都有一个唯一的 64 位长的序列号,这个序列号值就存在 DS18B20内部的 ROM 中。开始的 8 位是产品类型编码(DS18B20 是 0x10),接着的 48 位是每个器件唯一的序号,最后的 8 位是 CRC 校验码。DS18B20 可以引出去很长的线,最长可以到几十米,测不同位置的温度。单片机可以通过和 DS18B20 之间的通信,获取每个传感器所采集到的温度信息,也可以同时给所有的 DS18B20 发送一些指令。这些指令相对来说比较复杂,而且应用很少,所以这里大家有兴趣的话就自己去查手册完成吧,我们这里只讲一条总线上只接一个器件的指令和程序。
Skip ROM(跳过 ROM):0xCC。当总线上只有一个器件的时候,可以跳过 ROM,不进行 ROM 检测。
1.1.3 RAM操作指令
Read Scratchpad(读暂存寄存器):0xBE。这里要注意的是,DS18B20 的温度数据是 2 个字节,我们读取数据的时候,先读取到的是低字节的低位,读完了第一个字节后,再读高字节的低位,直到两个字节全部读取完毕。
Convert Temperature(启动温度转换):0x44。当我们发送一个启动温度转换的指令后,DS18B20 开始进行转换。从转换开始到获取温度,DS18B20 是需要时间的,而这个时间长短取决于 DS18B20 的精度。前边说 DS18B20 最高可以用 12 位来存储温度,但是也可以用 11 位,10 位和 9 位一共四种格式。
1.1.4 位读写时序
当要给 DS18B20 写入 0 的时候,单片机直接将引脚拉低,持续时间大于 60us 小于 120us就可以了。图上显示的意思是,单片机先拉低 15us 之后,DS18B20 会在从 15us 到 60us 之间的时间来读取这一位,DS18B20 最早会在 15us 的时刻读取,典型值是在 30us 的时刻读取,最多不会超过 60us,DS18B20 必然读取完毕,所以持续时间超过 60us 即可。
当要给 DS18B20 写入 1 的时候,单片机先将这个引脚拉低,拉低时间大于 1us,然后马上释放总线,即拉高引脚,并且持续时间也要大于 60us。和写 0 类似的是,DS18B20 会在15us 到 60us 之间来读取这个 1。
当要读取 DS18B20 的数据的时候,我们的单片机首先要拉低这个引脚,并且至少保持1us 的时间,然后释放引脚,释放完毕后要尽快读取。从拉低这个引脚到读取引脚状态,不能超过 15us。主机采样时间,也就是 MASTER SAMPLES,是在 15us 之内必须完成的,读取一个字节数据的程序如下。
1.2 协议实现
/****************************************************** //主要内容:DS18B20驱动代码 //补充说明:12位分辨率时的最大转换时间为750ms,启动转换后不能立即读取,因此启动转换和读取两个函数是分开写的 ******************************************************/ #include <reg52.h> #include <intrins.h> typedef unsigned char uint8 typedef unsigned int uint16 sbit DQ = P3^2; //DS18B20 通信引脚 /****************************************************** //功 能:软件延时,延时时间(t*10)us //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void DelayX10us(uint8 t) { do { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); }while (--t); } /****************************************************** //功 能:复位总线,获取存在脉冲,以启动一次读写操作 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ bit Get18B20Ack(void) { bit ack; EA = 0; //禁止总中断 DQ = 0; DelayX10us(50); //产生 500us 复位脉冲 DQ = 1; DelayX10us(6); //延时 60us ack = DQ; //读取存在脉冲 while(!DQ); //等待存在脉冲结束 EA = 1; //使能总中断 return ack; } /****************************************************** //功 能:向 DS18B20 写入一个字节 //输入参数:待写入字节 //返 回 值:void //补充说明:无 ******************************************************/ void Write18B20(uint8 dat) { uint8 i ; EA = 0; //禁止总中断 for(i=0;i<8;i++) { DQ = 0; //拉低总线至少1us,表示写时序开始。 _nop_(); DQ = dat & 0x01; DelayX10us(6); //延时 60us DQ = 1; //写完后释放总线 dat >>= 1; _nop_(); _nop_(); } EA = 1; //使能总中断 } /****************************************************** //功 能:从 DS18B20 读取一个字节 //输入参数:void //返 回 值:读到的字节 //补充说明:无 ******************************************************/ uint8 Read18B20(void) { uint8 i; uint8 dat = 0; EA = 0; //禁止总中断 for(i=0;i<8;i++) { DQ = 0; //拉低总线至少1us,表示读时序开始。单片机在此下降沿内15us内读取的数据是有效的。 _nop_(); dat >>= 1; DQ = 1; //释放总线,由DS18B20控制总线并传输一位数据 _nop_(); _nop_(); if(DQ) dat |= 0x80; DelayX10us(6); //延时60us } EA = 1; //重新使能总中断 return dat; } /****************************************************** //功 能:启动一次 18B20 温度转换 //输入参数:void //返 回 值:表示是否启动成功 //补充说明:无 ******************************************************/ bit Start18B20(void) { bit ack; ack = Get18B20Ack(); //执行总线复位,并获取 18B20 应答 if(ack == 0) //如 18B20 正确应答,则启动一次转换 { Write18B20(0xCC); //跳过 ROM 操作 Write18B20(0x44); //启动一次温度转换 } return ~ack; //ack==0 表示操作成功,所以返回值对其取反 } /****************************************************** //功 能:读取 DS18B20 转换的温度值 //输入参数:void //返 回 值:表示是否读取成功 //补充说明:温度有正有负,以补码的形式存储 ******************************************************/ bit Get18B20Temp(int *temp) { bit ack; uint8 LSB, MSB; //16bit 温度值的低字节和高字节 ack = Get18B20Ack(); //执行总线复位,并获取 18B20 应答 if(ack == 0) //如 18B20 正确应答,则读取温度值 { Write18B20(0xCC); //跳过 ROM 操作 Write18B20(0xBE); //发送读命令 LSB = Read18B20(); //读温度值的低字节 MSB = Read18B20(); //读温度值的高字节 *temp = ((int)MSB << 8) + LSB; //合成为 16bit 整型数 } return ~ack; //ack==0 表示操作应答,所以返回值为其取反值 }
第二部分 Inter-IC(Standard-mode)
参考资料:《I2C-bus specification and user manual V.6.pdf》《AT24C02.pdf》《http://c.biancheng.net/cpp/html/1939.html》《STM32英文参考手册_V15.pdf》《STM32中文参考手册_V10.pdf》
2.1 协议说明
飞利浦半导体(现为恩智浦半导体)开发了一种简单的双向 2 线总线,用于高效的 IC 间控制,称为 Inter-IC 或 I2C 总线。I2C总线数据传输速率在标准模式下最高可达 100 kbit/s,在快速模式下最高可达 400 kbit/s,在快速模式 Plus 下最高可达 1 Mbit/s,或在高速模式下高达 3.4 Mbit/s。超快速模式是一种单向模式,数据传输速度高达 5 Mbit/s。
串行数据 (SDA) 和串行时钟 (SCL) 这两条线在连接到总线的设备之间传输信息。 每个设备都由一个唯一的地址识别(无论是微控制器、LCD 驱动程序、存储器还是键盘接口),并且可以作为发送器或接收器运行,具体取决于设备的功能。 LCD 驱动器可能只是一个接收器,而存储器既可以接收数据,也可以传输数据。 除了发送器和接收器之外,设备在执行数据传输时也可以被视为主机或从机(见表 1)。主机是在总线上启动数据传输并生成时钟信号以允许传输的设备,此时任何被寻址的设备都视为从机。
2.1.1 SDA and SCL signals
SDA 和 SCL 都是双向线路,通过电流源或上拉电阻连接到正电源电压。
- 当总线空闲时,SDA 和 SCL 都是高电平。
- SCL 为高电平时,SDA 线上的高电平到低电平转换定义了启动条件。SCL 为高电平时,SDA 线上的低电平到高电平转换定义了停止条件。
- 当 SCL 为高电平时,SDA 上的数据必须是稳定的。只有 SCL 为低电平时,SDA上的数据才可以改变(见图 4)。每传输一个数据位都会产生一个时钟脉冲。
2.1.4 START and STOP conditions
所有事务都以 START (S) 开始,并以 STOP (P) 结束(见图 5)。
START 和 STOP 条件始终由主机生成。在 START 条件之后,总线被认为是忙碌的。在 STOP 条件之后的某个时间,总线被认为再次空闲。
如果生成重复的 START (Sr) 而不是 STOP 条件,则总线保持忙碌。在这方面,START (S) 和重复 START (Sr) 条件在功能上是相同的。
因此,对于本文档的其余部分,S 符号用作通用术语来表示 START 和重复的 START 条件,除非 Sr 特别相关。
2.1.5 Byte format
SDA 线上的每个字节都必须是 8 位长。 每次传输可传输的字节数不受限制。 每个字节后必须跟一个确认位。 数据首先以最高有效位 (MSB) 传输(参见图 6)。 如果从机在执行其他功能(例如服务内部中断)之前无法接收或发送另一个完整字节的数据,则它可以将时钟线 SCL 保持为低电平以强制主机进入等待状态。 当从设备准备好接收另一个字节的数据并释放时钟线 SCL 时,数据传输将继续。
2.1.6 Acknowledge (ACK) and Not Acknowledge (NACK)
确认发生在每个字节之后。确认位使接收器能够通知发送器该字节已成功接收并且可以发送另一个字节。主机生成所有时钟脉冲,包括确认第九个时钟脉冲。
确认信号定义如下:发送器在确认时钟脉冲期间释放 SDA 线,因此接收器可以将 SDA 线拉低,并在该时钟脉冲的高电平期间保持稳定的低电平(见图 4)。还必须考虑建立和保持时间(在第 6 节中指定)。
当 SDA 在第 9 个时钟脉冲期间保持高电平时,这被定义为未确认信号。然后,主机可以生成一个停止条件来中止传输,或者生成一个重复的 START 条件来开始新的传输。有五个条件会导致 NACK 的产生:
1. 总线上的接收器的地址都不是发送器发送的地址,因此没有设备响应确认。
2. 接收器无法接收或发送,因为它正在执行一些实时功能,还没有准备好开始与主设备的通信。
3. 在传输过程中,接收方得到了它不理解的数据或命令。
4. 在传输过程中,接收方不能再接收任何数据字节。
5. 主接收器必须向从发送器发出传输结束信号。
2.1.7 The slave address and R/W bit
数据传输遵循图 9 所示的格式。在 START 条件 (S) 之后,发送从地址。 该地址有 7 位长,后跟第 8 位,即数据方向位 (R/W)——“0”表示传输 (WRITE),“1”表示请求数据 (READ)(参见图 10)。 数据传输总是由主机产生的停止条件 (P) 终止。 但是,如果主机仍然希望在总线上进行通信,它可以生成重复的 START 条件 (Sr) 并寻址另一个从机,而无需首先生成 STOP 条件。 在这样的传输中,读/写格式的各种组合是可能的。
2.2 协议实现
I2C 通信分为低速模式 100kbit/s、快速模式 400kbit/s 和高速模式3.4Mbit/s。因为所有的 I2C 器件都支持低速,但却未必支持另外两种速度,所以作为通用的I2C 程序我们选择 100k 这个速率来实现,也就是说实际程序产生的时序必须小于等于 100k的时序参数。很明显也就是要求 SCL 的高低电平持续时间都不短于 5us,因此我们在时序函数中通过插入 I2CDelay()这个总线延时函数(它实际上就是 4 个 NOP 指令,用 define 在文件开头做了定义),加上改变 SCL 值语句本身占用的至少一个周期,来达到这个速度限制。如果以后需要提高速度,那么只需要减小这里的总线延时时间即可。
/****************************************************** //主要内容:I2C协议实现 //补充说明:无 ******************************************************/ #include <reg52.h> #include <intrins.h> #define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7; sbit I2C_SDA = P3^6; /****************************************************** //功 能:产生总线起始信号 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void I2CStart(void) { I2C_SDA = 1; //首先确保 SDA、SCL 都是高电平 I2C_SCL = 1; I2CDelay(); I2C_SDA = 0; //先拉低 SDA I2CDelay(); I2C_SCL = 0; //再拉低 SCL } /****************************************************** //功 能:产生总线停止信号 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void I2CStop(void) { I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平 I2C_SDA = 0; I2CDelay(); I2C_SCL = 1; //先拉高 SCL I2CDelay(); I2C_SDA = 1; //再拉高 SDA I2CDelay(); } /****************************************************** //功 能:I2C 总线写操作 //输入参数:待写入字节 //返 回 值:从机应答位的值 //补充说明:无 ******************************************************/ bit I2CWrite(uint8 dat) { bit ack; //用于暂存应答位的值 uint8 i; for(i=0;i<8;i++) { if ((dat & 0x80) == 0) I2C_SDA = 0; else I2C_SDA = 1; dat <<= 1; I2CDelay(); I2C_SCL = 1; //拉高 SCL I2CDelay(); I2C_SCL = 0; //再拉低 SCL,完成一个位周期 } I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答 I2CDelay(); I2C_SCL = 1; //拉高 SCL ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值 I2CDelay(); I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线 return (~ack); //应答值取反以符合通常的逻辑 } /****************************************************** //功 能:I2C 总线读操作,并发送非应答信号 //输入参数:void //返 回 值:读到的字节 //补充说明:无 ******************************************************/ uint8 I2CReadNAK(void) { uint8 i; uint8 dat = 0; I2C_SDA = 1; //首先确保主机释放 SDA for(i=0;i<8;i++) { I2CDelay(); I2C_SCL = 1; //拉高 SCL dat <<= 1; if(I2C_SDA != 0) dat |= 0x80; I2CDelay(); I2C_SCL = 0; //再拉低 SCL,以使从机发送出下一位 } I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号 I2CDelay(); I2C_SCL = 1; //拉高 SCL I2CDelay(); I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线 return dat; } /****************************************************** //功 能:I2C 总线读操作,并发送应答信号 //输入参数:void //返 回 值:读到的字节 //补充说明:无 ******************************************************/ uint8 I2CReadACK(void) { uint8 i; uint8 dat; I2C_SDA = 1; //首先确保主机释放 SDA for(i=0;i<8;i++) { I2CDelay(); I2C_SCL = 1; //拉高 SCL dat <<= 1; if(I2C_SDA != 0) dat |= 0x80; I2CDelay(); I2C_SCL = 0; //再拉低 SCL,以使从机发送出下一位 } I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号 I2CDelay(); I2C_SCL = 1; //拉高 SCL I2CDelay(); I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线 return dat; }
2.3 芯片说明
2.3.1 EEPROM 写数据流程
第一步,首先是 I2C 的起始信号,接着跟上首字节,也就是我们前边讲的 I2C 的器件地址,并且在读写方向上选择“写”操作。
第二步,发送数据的存储地址。24C02 一共 256 个字节的存储空间,地址从 0x00~0xFF,我们想把数据存储在哪个位置,此刻写的就是哪个地址。
第三步,发送要存储的数据第一个字节、第二个字节„„注意在写数据的过程中,EEPROM 每个字节都会回应一个“应答位 0”,来告诉我们写 EEPROM 数据成功,如果没有回应答位,说明写入不成功。
在写数据的过程中,每成功写入一个字节,EEPROM 存储空间的地址就会自动加 1,当加到 0xFF 后,再写一个字节,地址会溢出又变成了 0x00。
2.3.2 EEPROM 读数据流程
第一步,首先是 I2C 的起始信号,接着跟上首字节,也就是我们前边讲的 I2C 的器件地址,并且在读写方向上选择“写”操作。这个地方可能有同学会诧异,我们明明是读数据为何方向也要选“写”呢?刚才说过了,24C02 一共有 256 个地址,我们选择写操作,是为了把所要读的数据的存储地址先写进去,告诉 EEPROM 我们要读取哪个地址的数据。这就如同我们打电话,先拨总机号码(EEPROM 器件地址),而后还要继续拨分机号码(数据地址),而拨分机号码这个动作,主机仍然是发送方,方向依然是“写”。
第二步,发送要读取的数据的地址,注意是地址而非存在 EEPROM 中的数据,通知EEPROM 我要哪个分机的信息。
第三步,重新发送 I2C 起始信号和器件地址,并且在方向位选择“读”操作。
这三步当中,每一个字节实际上都是在“写”,所以每一个字节 EEPROM 都会回应一个“应答位 0”。
第四步,读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位 ACK(0)”,如果不想读了,告诉 EEPROM,我不想要数据了,别再发数据了,那就发送一个“非应答位 NAK(1)”。
和写操作规则一样,我们每读一个字节,地址会自动加 1,那如果我们想继续往下读,给 EEPROM 一个 ACK(0)低电平,那再继续给 SCL 完整的时序,EEPROM 会继续往外送数据。如果我们不想读了,要告诉 EEPROM 不要数据了,那我们直接给一个 NAK(1)高电平即可。这个地方大家要从逻辑上理解透彻,不能简单的靠死记硬背了,一定要理解明白。梳理一下几个要点:
A、在本例中单片机是主机,24C02 是从机;
B、无论是读是写,SCL 始终都是由主机控制的;
C、写的时候应答信号由从机给出,表示从机是否正确接收了数据;
D、读的时候应答信号则由主机给出,表示是否继续读下去。
2.3.3 EEPROM 多字节读写操作
我们读取 EEPROM 的时候很简单,EEPROM 根据我们所送的时序,直接就把数据送出来了,但是写 EEPROM 却没有这么简单了。给 EEPROM 发送数据后,先保存在了 EEPROM的缓存,EEPROM 必须要把缓存中的数据搬移到“非易失”的区域,才能达到掉电不丢失的效果。而往非易失区域写需要一定的时间,每种器件不完全一样,ATMEL 公司的 24C02 的这个写入时间最高不超过 5ms。在往非易失区域写的过程,EEPROM 是不会再响应我们的访问的,不仅接收不到我们的数据,我们即使用 I2C 标准的寻址模式去寻址,EEPROM 都不会应答,就如同这个总线上没有这个器件一样。数据写入非易失区域完毕后,EEPROM 再次恢复正常,可以正常读写了。
在向 EEPROM 连续写入多个字节的数据时,如果每写一个字节都要等待几 ms 的话,整体上的写入效率就太低了。因此 EEPROM 的厂商就想了一个办法,把 EEPROM 分页管理。24C01、24C02 这两个型号是 8 个字节一个页,而 24C04、24C08、24C16 是 16 个字节一页。我们开发板上用的型号是 24C02,一共是 256 个字节,8 个字节一页,那么就一共有 32 页。
分配好页之后,如果我们在同一个页内连续写入几个字节后,最后再发送停止位的时序。EEPROM 检测到这个停止位后,就会一次性把这一页的数据写到非易失区域,就不需要像上节课那样写一个字节检测一次了,并且页写入的时间也不会超过 5ms。如果我们写入的数据跨页了,那么写完了一页之后,我们要发送一个停止位,然后等待并且检测 EEPROM 的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以在很大程度上提高数据的写入效率。
2.4 驱动实现
/****************************************************** //版权说明:AT24C02驱动代码 //主要内容: //补充说明:无 ******************************************************/ #include <reg52.h> /****************************************************** //功 能:AT24C02读取函数 //输入参数:buf-数据接收指针,addr-E2 中的起始地址,len-读取长度 //返 回 值:void //补充说明:无 ******************************************************/ void E2Read(uint8 *buf, uint8 addr, uint8 len) { do //用寻址操作查询当前是否可进行读写操作 { I2CStart(); if(I2CWrite(0x50<<1)) break; //应答则跳出循环,非应答则进行下一次查询 I2CStop(); }while(1); I2CWrite(addr); //写入起始地址 I2CStart(); //发送重复启动信号 I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作 while (len > 1) //连续读取 len-1 个字节 { *buf++ = I2CReadACK(); //最后字节之前为读取操作+应答 len--; } *buf = I2CReadNAK(); //最后一个字节为读取操作+非应答 I2CStop(); } /****************************************************** //功 能:AT24C02写入函数 //输入参数:buf-源数据指针,addr-E2 中的起始地址,len-写入长度 //返 回 值:void //补充说明:无 ******************************************************/ void E2Write(uint8 *buf, uint8 addr, uint8 len) { while(len > 0) //等待上次写入操作完成 { do //用寻址操作查询当前是否可进行读写操作 { I2CStart(); if(I2CWrite(0x50<<1)) break; //应答则跳出循环,非应答则进行下一次查询 I2CStop(); } while(1); //按页写模式连续写入字节 I2CWrite(addr); //写入起始地址 while(len > 0) { I2CWrite(*buf++); //写入一个字节数据 len--; //待写入长度计数递减 addr++; //E2 地址递增 if ((addr&0x07) == 0) break; //检查地址是否到达页边界,24C02 每页 8 字节,所以检测低 3 位是否为零即可。到达页边界时,跳出循环,结束本次写操作 } I2CStop(); } }
第三部分 SPI
参考资料:《SPI Block Guide V4.01.pdf》《W25Q64FV.pdf》《STM32英文参考手册_V15.pdf》《STM32中文参考手册_V10.pdf》
3.1 协议介绍
SPI 模块允许在 MCU 和外围设备之间进行双工、同步、串行通信。 软件可以轮询 SPI 状态标志或 SPI 操作可以由中断驱动。
通过设置 SPI 控制寄存器 1 中的 SPI 启用 (SPE) 位来启用 SPI 系统。当 SPE 位被设置时,四个相关的 SPI 端口引脚专用于 SPI 功能。
SPI 系统的主要组成部分是 SPI 数据寄存器。 主机中的 8 位数据寄存器和从机中的 8 位数据寄存器通过 MOSI 和 MISO 引脚链接,形成一个分布式的 16 位寄存器。 当执行数据传输操作时,这个 16 位寄存器从主机的 S 时钟串行移位 8 位位置,因此在主机和从机之间交换数据。 写入主机SPI数据寄存器的数据成为输出到从机的数据,传输操作后从主机SPI数据寄存器读取的数据是来自从机的输入数据。
读取 SPTEF=1 的 SPISR,然后写入 SPIDR 会将数据放入发送数据寄存器。 当传输完成且 SPIF 清零时,接收到的数据被移入接收数据寄存器。 该 8 位数据寄存器用作读取的 SPI 接收数据寄存器和写入的 SPI 发送数据寄存器。 单个 SPI 寄存器地址用于从读取数据缓冲区读取数据并将数据写入发送数据寄存器。
SPI 控制寄存器 1 (SPICR1) 中的时钟相位控制位 (CPHA) 和时钟极性控制位 (CPOL) 选择 SPI 系统使用的四种可能时钟格式之一。 CPOL 位只是选择一个非反相或反相时钟。 CPHA 位用于通过在奇数 SCK 边沿或偶数 SCK 边沿上采样数据来适应两种根本不同的协议(请参阅传输格式)。
SPI 可配置为作为主机或从机运行。 当 SPI 控制寄存器 1 中的 MSTR 位置位时,选择主机模式,当 MSTR 位清零时,选择从机模式。
Master Mode
当 MSTR 位置位时,SPI 以主模式运行。 只有主 SPI 模块可以启动传输。 发送通过写入主 SPI 数据寄存器开始。 如果移位寄存器为空,字节立即传送到移位寄存器。 在串行时钟的控制下,字节开始在 MOSI 引脚上移出。
S-clock:SPR2、SPR1和SPR0波特率选择位与SPI波特率寄存器中的SPPR2、SPPR1和SPPR0波特率预选位一起控制波特率发生器并决定传输速度。 SCK 引脚是 SPI 时钟输出。 主机的波特率发生器通过 SCK 引脚控制从机外设的移位寄存器。
MOSI、MISO 引脚:在主模式下,串行数据输出引脚(MOSI)和串行数据输入引脚(MISO)的功能由 SPC0 和 BIDIROE 控制位决定。
SS 引脚:如果设置了 MODFEN 和 SSOE 位,则 SS 引脚被配置为从选择输出。 SS 输出在每次传输期间变为低电平,而在 SPI 处于空闲状态时为高电平。
当对主机中的 SPI 数据寄存器进行写操作时,会有半个 SCK 周期延迟。 延迟后,SCK 在主设备内启动。 传输操作的其余部分略有不同,具体取决于 SPI 控制寄存器 1 中的 SPI 时钟相位位 CPHA 指定的时钟格式(请参阅传输格式)。
Slave Mode
当 SPI 控制寄存器 1 中的 MSTR 位清零时,SPI 以从机模式运行。
SCK 时钟:在从机模式下,SCK 是来自主机的 SPI 时钟输入。
MISO、MOSI 引脚:在从机模式下,串行数据输出引脚(MISO)和串行数据输入引脚(MOSI)的功能由 SPI 控制寄存器 2 中的 SPC0 位和 BIDIROE 位决定。
SS 引脚:SS 引脚是从选择输入。 在数据传输发生之前,从 SPI 的 SS 引脚必须为低电平。 SS 必须保持低电平直到传输完成。 如果 SS 变高,SPI 将被强制进入空闲状态。
SS 输入还控制串行数据输出引脚,如果 SS 为高电平(未选择),则串行数据输出引脚为高阻抗,如果 SS 为低电平,则 SPI 数据寄存器中的第一位被驱动出串行数据 输出引脚。 此外,如果未选择从机(SS 为高电平),则 SCK 输入将被忽略,并且不会发生 SPI 移位寄存器的内部移位。
尽管 SPI 能够进行双工操作,但某些 SPI 外设只能在从模式下接收 SPI 数据。 对于这些更简单的设备,没有串行数据输出引脚。
只要不超过一个从设备驱动系统从设备的串行数据输出线,几个从设备就可以从一个主设备接收相同的传输,尽管主设备不会收到所有接收从设备的返回信息。
如果 SPI 控制寄存器 1 中的 CPHA 位清零,则 SCK 输入上的奇数边沿会导致串行数据输入引脚上的数据被锁存。 偶数边缘导致先前从串行数据输入引脚锁存的值移入 SPI 移位寄存器的 LSB 或 MSB,具体取决于 LSBFE 位。
如果设置了 CPHA 位,SCK 输入上的偶数边沿会导致串行数据输入引脚上的数据被锁存。 奇数边沿导致先前从串行数据输入引脚锁存的值移入 SPI 移位寄存器的 LSB 或 MSB,具体取决于 LSBFE 位。
当 CPHA 置位时,第一个边沿用于将第一个数据位传送到串行数据输出引脚。 当 CPHA 清零且 SS 输入为低电平(选择从机)时,SPI 数据的第一位被驱动出串行数据输出引脚。 在第 8 个移位之后,传输被认为完成,接收到的数据被传输到 SPI 数据寄存器。 为了指示传输完成,SPI 状态寄存器中的 SPIF 标志被设置。
Transmission Formats
在 SPI 传输期间,数据同时传输(串行移出)和接收(串行移入)。 串行时钟 (SCK) 同步两条串行数据线上信息的移位和采样。 从选择线允许选择单个从 SPI 设备,未选择的从设备不会干扰 SPI 总线活动。 或者,在主 SPI 设备上,从选择线可用于指示多主总线争用。
主 SPI 设备和通信从设备的时钟相位和极性应该相同。 在某些情况下,在传输之间改变相位和极性以允许主设备与具有不同要求的外围从设备通信。
3.2 协议实现
/****************************************************** //主要内容:SPI协议实现 //补充说明:无 ******************************************************/ sbit SPI_SCK = P3^2; sbit SPI_SI = P3^3; sbit SPI_SO = P3^4; sbit SPI_CS = P3^5; /****************************************************** //功 能:SPI发送一个字节数据 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void SPI_SendOneByte(uint16 Sdata) { uint8 i; for(i=0;i<8;i++) { SPI_SCK = 0; //上升沿发送数据,提前拉低电平 _nop_(); if(Sdata&0x80) SPI_SO = 1; //判断高位是否为1 else SPI_SO = 0; _nop_(); SPI_SCK = 1; Sdata <<= 1; } } /****************************************************** //功 能:SPI接收一个字节数据 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ uint8 SPI_ReadOneByte(void) { uint8 i; uint8 Temp_data; for(i=0;i<8;i++) { SPI_SCK = 1; //下降沿接收数据,提前拉高电平 _nop_(); if(SPI_SI) Temp_data |= 0x01; _nop_(); SPI_SCK = 0; Temp_data <<= 1; } return Temp_data; }
3.3 芯片说明
存储型矩阵: 一共有65,536页,每页256字节。每次只能写入一页,也就是每次只能写入256个字节。擦除时,只能以16页为最小单位,也就是一个扇区:16*256bits,一次就要擦8KB。写入时可以以字节为单位写入,但是擦除时只能以扇区为单位擦除。芯片支持标准的串行外设接口,比如SPI,也支持双IO-SPI或者四IO-SPI。
3.4 驱动实现
/****************************************************** //功 能:写使能 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void Write_Enable(void) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x06); //发送写使能指令 _nop_(); SPI_CS = 1; } /****************************************************** //功 能:写禁止 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void Write_Disable(void) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x04); _nop_(); SPI_CS = 1; } /****************************************************** //功 能:字节写 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void Byte_Write(uint8 Addr,uint16 Wdata) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x02);//发送写数据止指令 SPI_SendOneByte(Addr); SPI_SendOneByte(Wdata); _nop_(); SPI_CS = 1; } /****************************************************** //功 能:页写 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void Page_Write(uint8 Addr,uint8 *s) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x02); //发送写数据指令 SPI_SendOneByte(Addr); SPI_SendOneByte(*s++); _nop_(); SPI_CS = 1; } /****************************************************** //功 能:读一个字节 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ uint8 Byte_Read(uint8 Addr) { uint8 Rdata; SPI_CS = 0; _nop_(); SPI_SendOneByte(0x03); //发送读数据指令 SPI_SendOneByte(Addr); Rdata = Data_Rece(); _nop_(); SPI_CS = 1; return Rdata; }
第四部分 UART
参考资料:《http://c.biancheng.net/cpp/html/1921.html》《STM32英文参考手册_V15.pdf》《STM32中文参考手册_V10.pdf》
4.1 协议说明
首先是对通信的波特率的设定,在这里我们配置的波特率是 9600,那么串口调试助手也得是 9600。配置波特率的时候,我们用的是定时器 T0 的模式 2。模式 2 中,不再是 TH0 代表高 8 位,TL0 代表低 8 位了,而只有TL0 在进行计数,当 TL0 溢出后,不仅仅会让 TF0 变 1,而且还会将 TH0 中的内容重新自动装到 TL0 中。这样有一个好处,就是我们可以把想要的定时器初值提前存在 TH0 中,当 TL0溢出后,TH0 自动把初值就重新送入 TL0 了,全自动的,不需要程序中再给 TL0 重新赋值了,配置方式很简单,大家可以自己看下程序并且计算一下初值。
波特率设置好以后,打开中断,然后等待接收串口调试助手下发的数据。接收数据的时候,首先要进行低电平检测 while (PIN_RXD),若没有低电平则说明没有数据,一旦检测到低电平,就进入启动接收函数 StartRXD()。接收函数最开始启动半个波特率周期,初学可能这里不是很明白。大家回头看一下我们的图 11-2 里边的串口数据示意图,如果在数据位电平变化的时候去读取,因为时序上的误差以及信号稳定性的问题很容易读错数据,所以我们希望在信号最稳定的时候去读数据。除了信号变化的那个沿的位置外,其它位置都很稳定,那么我们现在就约定在信号中间位置去读取电平状态,这样能够保证我们读的一定是正确的。
一旦读到了起始信号,我们就把当前状态设定成接收状态,并且打开定时器中断,第一次是半个周期进入中断后,对起始位进行二次判断一下,确认一下起始位是低电平,而不是一个干扰信号。以后每经过 1/9600 秒进入一次中断,并且把这个引脚的状态读到 RxdBuf 里边。等待接收完毕之后,我们再把这个 RxdBuf 加 1,再通过 TXD 引脚发送出去,同样需要先发一位起始位,然后发 8 个数据位,再发结束位,发送完毕后,程序运行到 while (PIN_RXD),等待第二轮信号接收的开始。
4.2 协议实现
/****************************************************** //版权说明:UART协议实现 //主要内容: //补充说明:无 ******************************************************/ #include <reg52.h> sbit PIN_RXD = P3^0; //接收引脚定义 sbit PIN_TXD = P3^1; //发送引脚定义 bit RxdOrTxd = 0; //指示当前状态为接收还是发送 bit RxdEnd = 0; //接收结束标志 bit TxdEnd = 0; //发送结束标志 uint8 RxdBuf = 0; //接收缓冲器 uint8 TxdBuf = 0; //发送缓冲器 void ConfigUART(uint16 baud); void StartTXD(uint8 dat); void StartRXD(); /****************************************************** //功 能:主函数,用来测试协议实现效果 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void main() { EA = 1; //开总中断 ConfigUART(9600); //配置波特率为 9600 while(1) { while(PIN_RXD); //等待接收引脚出现低电平,即起始位 StartRXD(); //启动接收 while(!RxdEnd); //等待接收完成 StartTXD(RxdBuf+1); //接收到的数据+1 后,发送回去 while(!TxdEnd); //等待发送完成 } } /****************************************************** //功 能:串口配置 //输入参数:baud-通信波特率 //返 回 值:void //补充说明:无 ******************************************************/ void ConfigUART(uint16 baud) { TMOD &= 0xF0; //清零 T0 的控制位 TMOD |= 0x02; //配置 T0 为模式 2 TH0 = 256 - (11059200/12)/baud; //计算 T0 重载值 } /****************************************************** //功 能:启动串行接收 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void StartRXD(void) { TL0 = 256 - ((256-TH0)>>1); //接收启动时的 T0 定时为半个波特率周期 ET0 = 1; //使能 T0 中断 TR0 = 1; //启动 T0 RxdEnd = 0; //清零接收结束标志 RxdOrTxd = 0; //设置当前状态为接收 } /****************************************************** //功 能:启动串行发送 //输入参数:dat-待发送字节 //返 回 值:void //补充说明:无 ******************************************************/ void StartTXD(uint8 dat) { TxdBuf = dat; //待发送数据保存到发送缓冲器 TL0 = TH0; //T0 计数初值为重载值 ET0 = 1; //使能 T0 中断 TR0 = 1; //启动 T0 PIN_TXD = 0; //发送起始位 TxdEnd = 0; //清零发送结束标志 RxdOrTxd = 1; //设置当前状态为发送 } /****************************************************** //功 能:T0 中断服务函数,处理串行发送和接收 //输入参数:void //返 回 值:void //补充说明:无 ******************************************************/ void InterruptTimer0(void) interrupt 1 { static uint8 cnt = 0; //位接收或发送计数 if(RxdOrTxd) //串行发送处理 { cnt++; if(cnt <= 8) //低位在先依次发送 8bit 数据位 { PIN_TXD = TxdBuf & 0x01; TxdBuf >>= 1; } else if (cnt == 9) PIN_TXD = 1; //发送停止位 else //发送结束 { cnt = 0; //复位 bit 计数器 TR0 = 0; //关闭 T0 TxdEnd = 1; //置发送结束标志 } } else //串行接收处理 { if(cnt == 0) //处理起始位 { if(!PIN_RXD) //起始位为 0 时,清零接收缓冲器,准备接收数据位 { RxdBuf = 0; cnt++; } } else TR0 = 0; //起始位不为 0 时,中止接收。关闭 T0 else if (cnt <= 8) //处理 8 位数据位 { RxdBuf >>= 1; //低位在先,所以将之前接收的位向右移 if (PIN_RXD) RxdBuf |= 0x80; //接收脚为 1 时,缓冲器最高位置 1,而为 0 时不处理即仍保持移位后的 0 cnt++; } else //停止位处理 { cnt = 0; //复位 bit 计数器 TR0 = 0; //关闭 T0 if (PIN_RXD) RxdEnd = 1; //停止位为 1 时,方能认为数据有效,置接收结束标志 } } }
第五部分 串行通信协议汇总比较
单总线 | IIC | SPI | UART | |
总线数量(不含电源VDD和GND) | 1 | 2 | 4 | 2 |
数据同步方式 | 异步 | 同步 | 同步 | 异步 |
数据传输方向与时间的关系 | 半双工 | 半双工 | 全双工 | 全双工 |
应答与校验 | 未知 | 有应答,无校验 | 无应答,无校验 | 无应答,有校验 |
数据长度限制 | 未知 | 8 | 8 | 8 |
通信终端 | 未知 | 一主多从 | 一主多从 | 无主从之分 |
通信速率 | 未知 | MB/s | MB/s | KB/s |
特性 | 未知 | 有地址位,以及传送方向位 | 片选信号 | 波特率 |
RS232 串口和 UART 串口,它们的协议类型是一样的,只是电平标准不同而已,而 MAX232 这个芯片起到的就是中间人的作用,它把 UART 电平转换成 RS232 电平,也把 RS232 电平转换成 UART 电平,从而实现标准 RS232接口和单片机 UART 之间的通信连接。随着技术的发展,工业上还有 RS232 串口通信的大量使用,但是商业技术的应用上,已经慢慢的使用 USB 转 UART 技术取代了 RS232 串口,绝大多数笔记本电脑已经没有串口这个东西了,那我们要实现单片机和电脑之间的通信该怎么办呢?我们只需要在电路上添加一个 USB 转串口芯片(如CH340),就可以成功实现 USB 通信协议和标准UART 串行通信协议的转换。