写个IIC的主机来玩一玩。
- 仅100M时钟输入
- SCL波形工整,任意两个上升沿之间均为整数倍周期,占空比50%
- 发送数据时SDA严格对其到SCL低电平正中间
- 尽可能少的状态机
- 不浪费资源
- 数据逻辑和时序逻辑分离
接口设计中,我的思路是将数据与时序分离开,将和输入输出时序相关的逻辑放在最后面的模块中。使用stream接口实现数据交互。
找一个EEPROM来当从机:
仿真文件:
MicroChip 24XX04
先仿真一个IIC主机:
interface iic_master_sim (
inout sda,
inout scl
);
task init;
task start;
task re_start;
task stop;
task write;
task read;
endinterface
通过delay实现完美的主机,仿真:
观察波形,分割出有规律的部分:
在这一部分,可以看出1和0明显不一样。
显然可以得到:
DATA1:SDA 1111
SCL 0110
DATA0: SDA 0000
SCL 0110
那么,Start,ReStart,Stop:
Start : SDA 1100
SCL 1110
Restart: SDA 1100
SCL 0110
Stop : SDA 0011
SCL 0111
为了方便描述,将这些称呼为iic符号。每个IIC符号均为2*4bit
那么这里就是串化了,接下来借助GTX的思路来设计。
先用逻辑实现一个oserdes,ODDR模式。数据内部存储1小拍。
发送一些东西:
ACK在发送侧看来就是DATA1。由于整个系统都是100M来驱动的,所以这里的phy_sync其实是逻辑,并不是真正的时钟。
用phy_sync[1]产生ready,往前送,整一组数据:
效果不错,波形很完美。由于SCL的生成依赖于phy_sync,所以SCL一定是频率稳定的。
一个字节+ACK一共9个IIC符号,而其他控制位均为1个IIC符号。
需要一个编码模块,将符号与用户数据分离开:
iic_master_encode
再将字节的串化也做进去。
"Start" encode => 11001110
"ReStart" encode => 11000110
"Stop" encode => 00110111
"0xA0" encode => "DATA1", "DATA0", "DATA1", "DATA0", "DATA0", "DATA0", "DATA0", "DATA0", "DATA1"
=> 11110110 00000110 11110110 00000110 00000110 00000110 00000110 00000110 11110110
数据就用Stream接口传入,8bit数据放不下,用user来标识是数据还是控制符。
user
000 未定义
001 写数据,不带ACK,SDA让从机去拉SDA
010 读数据,数据自动改为0xFF,带ACK,连续读取
011 读数据,数据自动改为0xFF,不带ACK,结束连续读取
100 Start
101 ReStart
110 Stop
111 未定义
用ready信号做反压,控制,反压一个IIC符号的时间,数据,反压9个IIC符号时间:
这样就实现发送部分。接下来就是接收部分。
接收部分,iserdes->decode->data。
数据采样时钟比发送时钟晚了半周期:
这样就能完美的采样到数据。
decode无法分辨是发送的数据还是接收的数据,所以一律按读数据来算:
user
010 读数据,带ACK
011 读数据,不带ACK
100 Start
101 ReStart
110 Stop
仿真:
下面需要一个模块来产生phy_sync信号。这个也是非常简单,用计数器就可以生成了。
case (sync)
{2'b00, 2'b00} : sync <= {2'b11, 2'b00};
{2'b11, 2'b00} : sync <= {2'b11, 2'b11};
{2'b11, 2'b11} : sync <= {2'b10, 2'b11};
{2'b10, 2'b11} : sync <= {2'b10, 2'b10};
{2'b10, 2'b10} : sync <= {2'b01, 2'b10};
{2'b01, 2'b10} : sync <= {2'b01, 2'b01};
{2'b01, 2'b01} : sync <= {2'b00, 2'b01};
{2'b00, 2'b01} : sync <= {2'b00, 2'b00};
endcase
assign {tx_sync, rx_sync} = sync;
软件配置时,需要100K,那么就直接在寄存器中写入12500/100=(125)即可。
整个IIC最核心的部分已经完成了,没有状态机。
考虑软件层如何操作代码,把底层做到最简单:
iic_start();
iic_write(0b10100000);
iic_write(0x00);
iic_write(0x11);
iic_write(0x22);
iic_write(0x33);
iic_write(0x44);
iic_write(0x55);
iic_stop();
iic_start();
iic_write(0b10100000);
iic_write(0x01);
iic_restart();
iic_write(0b10100001);
rd_data[0] = iic_read(CONTINUE); // 0x22
rd_data[1] = iic_read(CONTINUE); // 0x33
rd_data[2] = iic_read(CONTINUE); // 0x44
rd_data[3] = iic_read(STOP); // 0x55
iic_stop();
每条指令均会产生一个tx_stream数据,并接收到一个rx_stream数据。
addr
0x00 w 0x01 总线发送Start
r 0x00 总线未检测到Start
r 0x01 总线已完成Start发送,读取清零
0x01 w 0x01 总线发送Restart
r 0x00 总线未检测到Restart
r 0x01 总线已完成Restart发送,读取清零
0x02 w 0x01 总线发送Stop
r 0x00 总线未检测到Stop
r 0x01 总线已完成Stop发送,读取清零
0x03 w 0x00 此时通过0x04发送数据的ACK被主机拉低
w 0x01 此时通过0x04发送数据的ACK被主机释放
r 0x00 此时通过0x06读出的数据ACK为低电平
r 0x01 此时通过0x06读出的数据ACK为高电平
0x04 w 0x55 发送数据0x55
0x05 w 0x01 清空发送FIFO
r 0x02 发送FIFO中有2个数据
0x06 r 0x55 读取数据为0x55
0x07 w 0x01 清空读取FIFO
r 0x02 读取FIFO中有2个数据
写操作流程:
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b10100000 发送器件地址
0x04 w 0x00 寄存器地址
0x04 w 0x11 写数据
0x04 w 0x22 写数据
0x04 w 0x33 写数据
0x04 w 0x44 写数据
0x04 w 0x55 写数据
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
读操作流程:
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b10100000 发送器件地址,写
0x04 w 0x01 寄存器地址
0x01 w 0x01 发送ReStart
0x04 w 0b10100001 发送器件地址,读
0x03 w 0x00 接下来发送的数据拉低ACK
0x04 w 0xFF 写数据
0x04 w 0xFF 写数据
0x04 w 0xFF 写数据
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0xFF 写数据
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
0x06 r 0x22 读取到数据0xA0
0x06 r 0x22 读取到数据0x01
0x06 r 0x22 读取到数据0xA1
0x06 r 0x22 读取到数据0x22
0x06 r 0x33 读取到数据0x33
0x06 r 0x44 读取到数据0x44
0x06 r 0x55 读取到数据0x55
看样子需要增加一些机制,能把接收到的数据做区分?
整理一下:
┌────────┐ ┌─────────┐ ┌────────┐
───►│ encode ├───►│ oserdes ├───►│ │
└────────┘ └─────────┘ │ open │
│ drain │◄───►
┌────────┐ ┌─────────┐ │ buffer │
◄───┤ decode │◄───┤ iserdes │◄───┤ │
└────────┘ └─────────┘ └────────┘
最简单的方发,在oserdes上发送sda的辅助信号,经过iserdes让decode解码出是读还是写。
┌────────┐ ┌─────────┐ ┌────────┐
───►│ encode ├───►│ iserdes ├──┬─►│ │
└────────┘ └─────────┘ │ │ open │
▼ │ drain │◄───►
┌────────┐ ┌─────────┐ │ │ buffer │
◄───┤ decode │◄───┤ oserdes │◄─┴──┤ │
└────────┘ └─────────┘ └────────┘
解码器的user这次多了状态:
3'b000 WriteData & ack
3'b001 WriteData & Nack
3'b010 ReadData & Ack
3'b011 ReadData & Nack
3'b100 Start
3'b101 Re-Start
3'b110 Stop
把rx的数据做一下解码:
52665 ns, rx: Start
142665 ns, rx: WriteData a0, with Ack
232665 ns, rx: WriteData 00, with Ack
322665 ns, rx: WriteData 11, with Ack
412665 ns, rx: WriteData 22, with Ack
502665 ns, rx: WriteData 33, with Ack
592665 ns, rx: WriteData 44, with Ack
682665 ns, rx: WriteData 55, with Ack
692665 ns, rx: Stop
5752665 ns, rx: Start
5842665 ns, rx: WriteData a0, with Ack
5932665 ns, rx: WriteData 01, with Ack
5942665 ns, rx: Re-Start
6032665 ns, rx: WriteData a1, with Ack
6122665 ns, rx: ReadData 22, with Ack
6212665 ns, rx: ReadData 33, with Ack
6302665 ns, rx: ReadData 44, with Ack
6392665 ns, rx: ReadData 55, Without Ack
6402665 ns, rx: Stop
这样发出的数据和读回的数据就可以区别了。
寄存器说明
addr
0x00 w 0x01 总线发送Start
r 0x00 总线未检测到Start
r 0x01 总线已完成Start发送,读取清零
0x01 w 0x01 总线发送Restart
r 0x00 总线未检测到Restart
r 0x01 总线已完成Restart发送,读取清零
0x02 w 0x01 总线发送Stop
r 0x00 总线未检测到Stop
r 0x01 总线已完成Stop发送,读取清零
0x03 w 0x00 此时通过0x04,0x06发送数据的ACK被主机拉低
w 0x01 此时通过0x04,0x06发送数据的ACK被主机释放
r 0x00 此时通过0x06读出的数据ACK为低电平
r 0x01 此时通过0x06读出的数据ACK为高电平
0x04 w 0x55 发送数据0x55
0x05 w 0x01 清空发送FIFO
r 0x02 发送FIFO中有2个数据
0x06 w 0x00 回读一个数据
r 0xAA 读取数据为0xAA
0x07 w 0x01 清空读取FIFO
r 0x02 读取FIFO中有2个数据
回读流程
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b10100000 发送器件地址,写
0x04 w 0x01 寄存器地址
0x01 w 0x01 发送ReStart
0x04 w 0b10100001 发送器件地址,读
0x03 w 0x00 接下来发送或读取的数据拉低ACK
0x06 w 0x00 读数据
0x06 w 0x00 读数据
0x06 w 0x00 读数据
0x03 w 0x01 接下来发送或读取的数据释放ACK
0x06 w 0x00 读数据
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
0x07 r 0x04 读取FIFO中由4个数据
0x06 r 0x22 读取到数据0x22
0x06 r 0x33 读取到数据0x33
0x06 r 0x44 读取到数据0x44
0x06 r 0x55 读取到数据0x55
0x03 r 0x01 0x55数据没有ACK
读取数据的过程中,读取0x03不是必须的。流程可以
保险起见,可以在回读前清空回读FIFO。
如果有超长的IIC操作,为了防止FIFO溢出,需要读取0x05,0x07寄存器已确认操作空间。
当然这个大小也取决于FIFO的大小。一般情况下操作数量不会超过256。
扫描主机的操作:
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b00000000 发送器件地址,写
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
0x03 r 0x01 没有ACK
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b00000010 发送器件地址,写
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
0x03 r 0x01 没有ACK
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b00000100 发送器件地址,写
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
0x03 r 0x01 没有ACK
0x00 w 0x01 发送Start
0x03 w 0x01 接下来发送的数据释放ACK
0x04 w 0b00000110 发送器件地址,写
0x02 w 0x01 发送Stop
0x02 r 0x00 还未检测到Stop
0x02 r 0x01 检测到Stop
0x03 r 0x00 有ACK
具体总线与FIFO部分之后再说。