文章目录
iic总线协议介绍
iic: inter integrated circuit,集成电路总线,是一种同步 串行 半双工通信协议
iic总线结构图
①总线由数据线 sda 和时钟线 scl 构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发。
②总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信。
③数据线 sda 和时钟线 scl 都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平。
④总线上数据的传输速率在标准模式下可达 100kbit/s 在快速模式下可达 400kbit/s,在高速模式下可达 3.4mbit/s。
⑤总线支持设备连接。在使用 iic 通信总线时,可以有多个具备 iic 通信能力的设备挂载在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容 400pf 的限制决定。
iic协议
可以归纳为
- 三个信号:起始信号、停止信号、应答信号
-
①起始信号:当 scl 为高电平期间,sda 由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
-
②停止信号:当 scl 为高电平期间,sda 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
-
③应答信号:发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ack 简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(nack),一般表示接收器接收该字节没有功。
- 两个注意:数据有效性、数据传输
-
④数据有效性:iic 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 scl 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
-
⑤数据传输:在 i2c 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 scl 串行时钟的配合下,在 sda 上逐位地串行传送每一位数据。数据位的传输是边沿触发。
- 一个状态:空闲状态
- ⑥空闲状态:iic 总线的 sda 和 scl 两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
iic读写通讯过程
- step1:主机首先在 iic 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。
- step2:主机接着发送从机地址+0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。
- step3:主机在总线上接收到有应答信号后,才能继续向从机发送数据。iic 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
- step1:主机发出起始信号,接着发送从机地址+1(读操作)组成的 8bit 数据
- step2:从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回 8bit 数据,发送完之后从机就会等待主机的应答信号。
- step3:假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
24c02 简介
24c02 是一个 2k bit 的串行 eeprom 存储器,内部含有 256 个字节。在 24c02 里面还有一个 8 字节的页写缓冲器。该设备的通信方式 iic,通过其 scl 和 sda 与其他设备通信。
单片机内部的rom只能在程序下载时进行擦除和改写,程序运行时本身是不能改写的,单片机内部的ram中的数据程序运行时可以修改,但掉电丢失,所以需要用到数据存在系统中且掉电不丢失时需要用到eeprom。
- wp引脚:写保护引脚,高电平时只读,接地时允许读写。
- 24c02设备地址:可编程部分+不可编程部分
可编程部分由a0,a1,a2决定,最后一位设置数据传输方向,即0–>写操作,1–>读操作,其具体格式为:
24c02读写时序图
写时序图
- step1:主机在iic总线发送第1个字节的数据为设备地址0xa0,用于寻找总线上的24c02
- step2:在获得应答信号后,主机继续发送第2个字节的数据,为24c02的内存地址
- step3:在获得应答信号后,主机发送第3个字节的数据,为写入在第二字节内存地址处的数据。
上面的写操作只能单字节写入到24c02,效率低下,24c02具有页写时序如图所示。
在页写时序中,只需要告诉 24c02 第一个内存地址 1,后面数据会按照顺序写入到内存地址 2,内存地址 3等。
读时序
24c02 读取数据的过程是一个复合的时序,其中包含写时序和读时序。
- step1:起始信号后,主机向24c02发送设备地址0xa0,获取从机应答信号后,接着发送需要读取内存地址
- step2:起始信号产生后主机发送24c02设备地址0xa1,获取从机应答信号后,接着从机返回刚刚写入内存地址中的数据。如果主机获得数据后返回的是应答信号,那么会一直获取从机返回的数据,当主机返回非应答信号时,从机结束传输
实验
实现功能
每按下 key1,mcu 通过 iic 总线向 24c02 写入数据,通过按下 key0 来控制 24c02 读取数据。同时在 lcd 上面显示相关信息。led0 闪烁用于提示程序正在运行。
实验原理
24c02 的 scl 和 sda 分别连接在 stm32 的 pb8 和 pb9 上。本实验通过软件模拟 iic 信号建立起与 24c02 的通信,进行数据发送与接收,使用按键 key0 和 key1 去触发,lcd 屏幕进行显示。
用软件模拟 iic,最大的好处就是方便移植,同一个代码兼容所有 mcu,任何一个单片机只要有 io 口,就可以很快的移植过去,而且不需要特定的 io 口。
流程图
代码
iic底层驱动代码
- myiic.h
#ifndef __myiic_h
#define __myiic_h
#include "./system/sys/sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define iic_scl_gpio_port gpiob
#define iic_scl_gpio_pin gpio_pin_8
#define iic_scl_gpio_clk_enable() do{ __hal_rcc_gpiob_clk_enable(); }while(0) /* pb口时钟使能 */
#define iic_sda_gpio_port gpiob
#define iic_sda_gpio_pin gpio_pin_9
#define iic_sda_gpio_clk_enable() do{ __hal_rcc_gpiob_clk_enable(); }while(0) /* pb口时钟使能 */
/******************************************************************************************/
/* io操作 */
#define iic_scl(x) do{ x ? \
hal_gpio_writepin(iic_scl_gpio_port, iic_scl_gpio_pin, gpio_pin_set) : \
hal_gpio_writepin(iic_scl_gpio_port, iic_scl_gpio_pin, gpio_pin_reset); \
}while(0) /* scl */
#define iic_sda(x) do{ x ? \
hal_gpio_writepin(iic_sda_gpio_port, iic_sda_gpio_pin, gpio_pin_set) : \
hal_gpio_writepin(iic_sda_gpio_port, iic_sda_gpio_pin, gpio_pin_reset); \
}while(0) /* sda */
#define iic_read_sda hal_gpio_readpin(iic_sda_gpio_port, iic_sda_gpio_pin) /* 读取sda */
/* iic所有操作函数 */
void iic_init(void); /* 初始化iic的io口 */
void iic_start(void); /* 发送iic开始信号 */
void iic_stop(void); /* 发送iic停止信号 */
void iic_ack(void); /* iic发送ack信号 */
void iic_nack(void); /* iic不发送ack信号 */
uint8_t iic_wait_ack(void); /* iic等待ack信号 */
void iic_send_byte(uint8_t txd);/* iic发送一个字节 */
uint8_t iic_read_byte(unsigned char ack);/* iic读取一个字节 */
#endif
- myiic.c
①初始化iic
sda开漏模式,不用规定io方向,也可以读取外部信号的高低电平。
void iic_init(void)
{
gpio_inittypedef gpio_init_struct;
iic_scl_gpio_clk_enable(); /* scl引脚时钟使能 */
iic_sda_gpio_clk_enable(); /* sda引脚时钟使能 */
gpio_init_struct.pin = iic_scl_gpio_pin;
gpio_init_struct.mode = gpio_mode_output_pp; /* 推挽输出 */
gpio_init_struct.pull = gpio_pullup; /* 上拉 */
gpio_init_struct.speed = gpio_speed_freq_very_high; /* 快速 */
hal_gpio_init(iic_scl_gpio_port, &gpio_init_struct);/* scl */
gpio_init_struct.pin = iic_sda_gpio_pin;
gpio_init_struct.mode = gpio_mode_output_od; /* 开漏输出 */
hal_gpio_init(iic_sda_gpio_port, &gpio_init_struct);/* sda */
/* sda引脚模式设置,开漏输出,上拉, 这样就不用再设置io方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
iic_stop(); /* 停止总线上所有设备 */
}
②iic延时函数用于控制iic读写速度
static void iic_delay(void)
{
delay_us(2); /* 2us的延时, 读写速度在250khz以内 */
}
③起始信号
void iic_start(void)
{
iic_sda(1);
iic_scl(1);
iic_delay();
iic_sda(0); /* start信号: 当scl为高时, sda从高变成低, 表示起始信号 */
iic_delay();
iic_scl(0); /* 钳住i2c总线,准备发送或接收数据 */
iic_delay();
}
④停止信号
void iic_stop(void)
{
iic_sda(0); /* stop信号: 当scl为高时, sda从低变成高, 表示停止信号 */
iic_delay();
iic_scl(1);
iic_delay();
iic_sda(1); /* 发送i2c总线结束信号 */
iic_delay();
}
⑤发送函数
将需要发送的数据作为形参,形参大小位1字节,在iic中一个时钟信号发送1bit,故该函数需要循环8次,模拟8个时钟信号。
void iic_send_byte(uint8_t data)
{
uint8_t t;
for (t = 0; t < 8; t++)
{
iic_sda((data & 0x80) >> 7); /* 高位先发送 */
iic_delay();
iic_scl(1);
iic_delay();
iic_scl(0);
data <<= 1; /* 左移1位,用于下一次发送 */
}
iic_sda(1); /* 发送完成, 主机释放sda线 */
}
⑥iic读取函数
首先需要一个变量 receive 存放接收到的数据,在每一次循环开始前都需要对 receive 进行左移 1 位操作,那么 receive 的 bit0 位每一次赋值前都是空的,用来存放最新接收到的数据位,然后在 scl 线进行高低电平切换时输出 iic 时钟,在 scl 高电平期间加入延时,确保有足够的时间能让数据发送并进行处理,使用宏定义 iic_read_sda 就可以判断读取到的高低电平,假如 sda 为高电平,那么 receive++即在 bit0 置 1,否则不做处理即保持原来的0 状态。当 scl 线拉低后,需要加入延时,便于从机切换 sda 线输出数据。在 8 次循环结束后,我们就获得了 8bit 数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应答或者非应答信号,去回复从机。
uint8_t iic_read_byte(uint8_t ack)
{
uint8_t i, receive = 0;
for (i = 0; i < 8; i++ ) /* 接收1个字节数据 */
{
receive <<= 1; /* 高位先输出,所以先收到的数据位要左移 */
iic_scl(1);
iic_delay();
if (iic_read_sda)
{
receive++;
}
iic_scl(0);
iic_delay();
}
if (!ack)
{
iic_nack(); /* 发送nack */
}
else
{
iic_ack(); /* 发送ack */
}
return receive;
}
⑦等待从机应答信号
该函数主要用在写时序中,当启动起始信号,发送完8bit 数据到从机时,我们就需要等待以及处理接收从机发送过来的响应信号或者非响应信号,一般就是在 iic_send_byte 函数后面调用。
在这个等待读取的过程中加入了超时判断,假如超过这个时间没有接收到数据,那么主机直接发出停止信号,跳出循环,返回等于 1的变量。在正常等待到应答信号后,主机会把 scl 时钟线拉低并延时,返回是否接收到应答信号。
uint8_t iic_wait_ack(void)
{
uint8_t waittime = 0;
uint8_t rack = 0;
iic_sda(1); /* 主机释放sda线(此时外部器件可以拉低sda线) */
iic_delay();
iic_scl(1); /* scl=1, 此时从机可以返回ack */
iic_delay();
while (iic_read_sda) /* 等待应答 */
{
waittime++;
if (waittime > 250)
{
iic_stop();
rack = 1;
break;
}
}
iic_scl(0); /* scl=0, 结束ack检查 */
iic_delay();
return rack;
}
⑧产生ack应答
void iic_ack(void)
{
iic_sda(0); /* scl 0 -> 1 时 sda = 0,表示应答 */
iic_delay();
iic_scl(1); /* 产生一个时钟 */
iic_delay();
iic_scl(0);
iic_delay();
iic_sda(1); /* 主机释放sda线 */
iic_delay();
}
⑨不产生ack应答
void iic_nack(void)
{
iic_sda(1); /* scl 0 -> 1 时 sda = 1,表示不应答 */
iic_delay();
iic_scl(1); /* 产生一个时钟 */
iic_delay();
iic_scl(0);
iic_delay();
}
24c02驱动代码
- 24cxx.h
#ifndef __24cxx_h
#define __24cxx_h
#include "./system/sys/sys.h"
#define at24c01 127
#define at24c02 255
#define at24c04 511
#define at24c08 1023
#define at24c16 2047
#define at24c32 4095
#define at24c64 8191
#define at24c128 16383
#define at24c256 32767
/* 开发板使用的是24c02,所以定义ee_type为at24c02 */
#define ee_type at24c02
void at24cxx_init(void); /* 初始化iic */
uint8_t at24cxx_check(void); /* 检查器件 */
uint8_t at24cxx_read_one_byte(uint16_t addr); /* 指定地址读取一个字节 */
void at24cxx_write_one_byte(uint16_t addr,uint8_t data); /* 指定地址写入一个字节 */
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen); /* 从指定地址开始写入指定长度的数据 */
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen); /* 从指定地址开始读出指定长度的数据 */
#endif
- 24cxx.c
①指定地址写入一个数据
主机发送的设备地址和内存地址共同确定了要写入的地方, iic_send_byte(0xa0+((addr>>8)<<1))和 iic_send_byte(addr % 256)确定写入位置,由于它内存大小一共 2048 字节,所以只需要定义 11 个寻址地址线,2048 = 2^11。主机下发读写命令的时候带了 3 位,后面再跟 1 个字节(8 位)的地址,正好 11 位,就不需要再发后续的地址字节了。
void at24cxx_write_one_byte(uint16_t addr, uint8_t data)
{
/* 原理说明见:at24cxx_read_one_byte函数, 本函数完全类似 */
iic_start(); /* 发送起始信号 */
if (ee_type > at24c16) /* 24c16以上的型号, 分2个字节发送地址 */
{
iic_send_byte(0xa0); /* 发送写命令, iic规定最低位是0, 表示写入 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ack */
iic_send_byte(addr >> 8); /* 发送高字节地址 */
}
else
{
iic_send_byte(0xa0 + ((addr >> 8) << 1)); /* 发送器件 0xa0 + 高位a8/a9/a10地址,写数据 */
}
iic_wait_ack(); /* 每次发送完一个字节,都要等待ack */
iic_send_byte(addr % 256); /* 发送低位地址 */
iic_wait_ack(); /* 等待ack, 此时地址发送完成了 */
/* 因为写数据的时候,不需要进入接收模式了,所以这里不用重新发送起始信号了 */
iic_send_byte(data); /* 发送1字节 */
iic_wait_ack(); /* 等待ack */
iic_stop(); /* 产生一个停止条件 */
delay_ms(10); /* 注意: eeprom 写入比较慢,必须等到10ms后再写下一个字节 */
}
②
uint8_t at24cxx_read_one_byte(uint16_t addr)
{
uint8_t temp = 0;
iic_start(); /* 发送起始信号 */
/* 根据不同的24cxx型号, 发送高位地址
* 1, 24c16以上的型号, 分2个字节发送地址
* 2, 24c16及以下的型号, 分1个低字节地址 + 占用器件地址的bit1~bit3位 用于表示高位地址, 最多11位地址
* 对于24c01/02, 其器件地址格式(8bit)为: 1 0 1 0 a2 a1 a0 r/w
* 对于24c04, 其器件地址格式(8bit)为: 1 0 1 0 a2 a1 a8 r/w
* 对于24c08, 其器件地址格式(8bit)为: 1 0 1 0 a2 a9 a8 r/w
* 对于24c16, 其器件地址格式(8bit)为: 1 0 1 0 a10 a9 a8 r/w
* r/w : 读/写控制位 0,表示写; 1,表示读;
* a0/a1/a2 : 对应器件的1,2,3引脚(只有24c01/02/04/8有这些脚)
* a8/a9/a10: 对应存储整列的高位地址, 11bit地址最多可以表示2048个位置, 可以寻址24c16及以内的型号
*/
if (ee_type > at24c16) /* 24c16以上的型号, 分2个字节发送地址 */
{
iic_send_byte(0xa0); /* 发送写命令, iic规定最低位是0, 表示写入 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ack */
iic_send_byte(addr >> 8); /* 发送高字节地址 */
}
else
{
iic_send_byte(0xa0 + ((addr >> 8) << 1)); /* 发送器件 0xa0 + 高位a8/a9/a10地址,写数据 */
}
iic_wait_ack(); /* 每次发送完一个字节,都要等待ack */
iic_send_byte(addr % 256); /* 发送低位地址 */
iic_wait_ack(); /* 等待ack, 此时地址发送完成了 */
iic_start(); /* 重新发送起始信号 */
iic_send_byte(0xa1); /* 进入接收模式, iic规定最低位是1, 表示读取 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ack */
temp = iic_read_byte(0); /* 接收一个字节数据 */
iic_stop(); /* 产生一个停止条件 */
return temp;
}
③检查at24cxx是否正常
这里利用的是 eeprom 芯片掉电不丢失的特性,在第一次写入了某个值之后,再去读一下是否写入成功,这种方式去检测芯片是否正常工作。
{
uint8_t temp;
uint16_t addr = ee_type;
temp = at24cxx_read_one_byte(addr); /* 避免每次开机都写at24cxx */
if (temp == 0x55) /* 读取数据正常 */
{
return 0;
}
else /* 排除第一次初始化的情况 */
{
at24cxx_write_one_byte(addr, 0x55); /* 先写入数据 */
temp = at24cxx_read_one_byte(255); /* 再读取数据 */
if (temp == 0x55)return 0;
}
return 1;
}
main.c
#include "./system/sys/sys.h"
#include "./system/usart/usart.h"
#include "./system/delay/delay.h"
#include "./bsp/led/led.h"
#include "./bsp/lcd/lcd.h"
#include "./usmart/usmart.h"
#include "./bsp/key/key.h"
#include "./bsp/24cxx/24cxx.h"
/* 要写入到24c02的字符串数组 */
const uint8_t g_text_buf[] = {"stm32 iic test"};
#define text_size sizeof(g_text_buf) /* text字符串长度 */
int main(void)
{
uint8_t key;
uint16_t i = 0;
uint8_t datatemp[text_size];
hal_init(); /* 初始化hal库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(84); /* 初始化usmart */
led_init(); /* 初始化led */
lcd_init(); /* 初始化lcd */
key_init(); /* 初始化按键 */
at24cxx_init(); /* 初始化24cxx */
lcd_show_string(30, 50, 200, 16, 16, "stm32", red);
lcd_show_string(30, 70, 200, 16, 16, "iic test", red);
lcd_show_string(30, 90, 200, 16, 16, "atom@alientek", red);
lcd_show_string(30, 110, 200, 16, 16, "key1:write key0:read", red); /* 显示提示信息 */
while (at24cxx_check()) /* 检测不到24c02 */
{
lcd_show_string(30, 130, 200, 16, 16, "24c02 check failed!", red);
delay_ms(500);
lcd_show_string(30, 130, 200, 16, 16, "please check! ", red);
delay_ms(500);
led0_toggle(); /* 红灯闪烁 */
}
lcd_show_string(30, 130, 200, 16, 16, "24c02 ready!", red);
while (1)
{
key = key_scan(0);
if (key == key1_pres) /* key1按下,写入24c02 */
{
lcd_fill(0, 150, 239, 319, white); /* 清除半屏 */
lcd_show_string(30, 150, 200, 16, 16, "start write 24c02....", blue);
at24cxx_write(0, (uint8_t *)g_text_buf, text_size);
lcd_show_string(30, 150, 200, 16, 16, "24c02 write finished!", blue); /* 提示传送完成 */
}
if (key == key0_pres) /* key0按下,读取字符串并显示 */
{
lcd_show_string(30, 150, 200, 16, 16, "start read 24c02.... ", blue);
at24cxx_read(0, datatemp, text_size);
lcd_show_string(30, 150, 200, 16, 16, "the data readed is: ", blue); /* 提示传送完成 */
lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, blue); /* 显示读到的字符串 */
}
i++;
if (i == 20)
{
led0_toggle(); /* 红灯闪烁 */
i = 0;
}
delay_ms(10);
}
}
发表评论