16.1 关于I2C
1 M* t. w+ L( L, \1 M- |16.1.1 I2C协议
& A: o6 J8 f" L& `2 [0 b! C# v2 ?4 V& |# F
I²C(Inter-Integrated Circuit),常读作“I方C”,它是一种多主从架构串行通信总线。在1980年由飞利浦公司设计,用于让主板、嵌入式系统或手机连接低速周边设备。如今在嵌入式领域是非常常见通信协议,常用于MPU/MCU与外部设备连接通信、数据传输。: {& Q. D/ @- O1 [% o8 S
8 P+ q" ^4 L4 G5 |; ?. V8 s
I²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。每个连接到总线的设备都有一个独立的地址,主机可以通过该地址来访问不同设备。因为I²C协议比较简单,常常用GPIO来模拟I²C时序,这种方法称为模拟I²C。如果使用MCU的I²C控制器,设置好I²C控制器, I²C控制器就自动实现协议时序,这种方式称为硬件I²C。因为I²C设备的速率比较低,通常两种方式都可以,模拟I²C方便移植,硬件I²C工作效率相对较高。
2 s- e. Y) ~8 k) R5 y
; U8 n# y3 `2 g/ X关于I²C协议,通过下面例子进行一个形象的比喻方便大家理解,如图 16.1.1 所示,老师(MCU)将球(数据)传给众多学生中的一个(众多外设设备中的一个)。& C4 {# t1 R5 K6 G& d. e) b
9 ^4 _$ R" Y4 x
# `8 c, i! E( j1 \
* C" E4 D! U' Y( c, S
8 T" b) F: `# `8 {: @图 16.1.1 I²C协议比喻
$ d9 X/ i; v* c& n# u3 X
8 h, D( G2 s: D$ ]4 v& e% q1 \) J首先老师将球踢给某学生,即主机发送数据给从机,步骤如下:2 B7 L6 @) u+ ^7 P% q& \5 X. R9 B
% R4 F& N2 E3 l" G0 @
1) 老师:开始了(start);
5 J0 b8 F- m: a/ ^* n A# O; n$ |3 ]" {
2) 老师:A!我要发球给你!(地址/方向);, m/ B/ L% k* V Q
/ ?5 i8 r" C& W$ R/ ~& O- h, Y
3) 学生A:到!(回应);
* z6 h6 Q0 t9 C/ A# `- ~5 j( k' X; a/ Q# o, ^
4) 老师把球发出去(传输);
& u$ ?1 ?" n0 h v* z8 t1 k& {# b3 Z$ m4 }4 q
5) A收到球之后,应该告诉老师一声(回应);& \2 \5 m! g! {7 d( s5 t2 Z9 o
% ]3 p; @: v$ Q0 F- [/ {0 ]) A6) 老师:结束(停止);4 C2 [) }; `: o1 i# D& r0 ~
4 D9 K( u) B2 B
% M0 I4 N2 e( c' l
: W9 {$ l3 g u1 R* i接着老师让学生把球传给自己,即从机发送数据给主机,步骤如下:, _0 I- J1 I0 N
" ~0 z- `& ]2 O, F/ s2 X1) 老师:开始了(start);! t: e) r& M/ X* O T
7 x7 v5 U1 A, ?5 Z" p; D2) 老师:B!把球发给我!(地址/方向);5 {% V5 y! r l% o. n) \* f# `0 i
# p6 l+ L$ K' r( u( P" v3 K6 C: C
3) 学生B:到!
; u/ g6 ^- u8 E) R& _
3 h9 _% b1 C4 r9 ^4) B把球发给老师(传输);
# {; X& B8 }; i7 n/ h, L( l+ l) N/ u! _" C$ i& K8 C
5) 老师收到球之后,给B说一声,表示收到球了(回应);
2 D8 ]5 ]! _" ]3 `$ y( R' [- P1 {! @* A8 a- c
6) 老师:结束(停止)。) t, f; H& `0 S! B, J
+ a# L# O) u3 u1 ?$ [! o
: w" V8 f) a0 ~* v& H从上面的例子可知,都是老师(主机)主导传球,按照规范的流程(通信协议),以保证传球的准确性,收发球的流程总结如下:3 S* D6 M! U6 R) t g
" Q. z9 e7 _+ X. ^/ [/ ]
① 老师说开始了,表示开始信号(start);; e" n# s) T% g
: { h1 P: l2 d9 v" g( {% }② 老师提醒某个学生要发球,表示发送地址和方向(address/read/write);& r/ I N, J5 K7 O) E
% j, @% U: f( R8 Z4 `
③ 该学生回应老师(ack);
/ b; Q$ |/ l- C6 C
* g4 v% {3 |) b( I8 f7 E6 g3 i④ 老师发球/接球,表示数据的传输;
0 m2 y f5 \. N, Q9 u- D9 r+ [" i+ h' B$ R
⑤ 收到球要回应:回应信号(ACK);
- k( }0 Z3 }0 g- f9 e6 s! a; L! [$ f& W; {
⑥ 老师说结束,表示IIC传输结束(P)。
6 _" N0 R) D$ U! W3 W( t$ G- q! Y4 p, Z/ g
; J$ M0 b# j; e9 U/ i3 c
以上就是I²C的传输协议,如果是软件模拟I²C,需要依次实现每个步骤。因此,还需要知道每一步的具体细节,比如什么时候的数据有效,开始信号怎么表示。
+ E2 m8 r% k! L, w! r; v% z! \, V
2 R M- \% z0 u2 a; X5 r$ [4 \
数据有效性
! a% _/ ]$ x* ^' A2 l$ t* z
' A# }! `. Z/ l v2 WI²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。数据有效性示意图如图 16.1.2 所示。
8 `* |* z T6 c' m$ B! x' d; h; I6 J" @
6 @/ s: `; m" E3 h6 K8 O! @
; b# W/ R) Q3 |9 d
图 16.1.2 数据有效性
5 [5 o7 K/ x6 n1 H9 V( l1 V, a4 w1 l* |! W, M7 H* R
& {: P" i1 ]9 {; }# b1 f/ `; P开始信号和结束信号& r2 f6 E& d: d4 Q* L
% y4 Z; [( {5 r. o# O# nI²C起始信号(S):当SCL高电平时,SDA由高电平向低电平转换;% g2 K! g# K! _$ W: [
- @. g7 Y( o$ BI²C停止信号(P):当SCL高电平时,SDA由低电平向高电平转换;
y3 z$ \, O0 s* _; @
$ N5 O" J! _! v+ K" G1 F. N, {
& P2 m5 y& ?! I9 a6 P* ~# Z. ^2 S( J& K
4 I4 o! J$ L% s. q" x, i
图 16.1.3 开始信号和结束信号
3 O& h( P; H+ @6 C2 d1 Z% X: P& U) S, G' ^1 G2 m: E1 ^' J. G
1 w* e9 f/ C, N1 `+ p3 k1 l+ S1 O
应答信号
: H s9 d2 B( t$ X# d \9 ]6 @7 F
7 E, S; u a* m% e1 g6 l$ K0 _7 nI²C每次传输的8位数据,每次传输后需要从机反馈一个应答位,以确认从机是否正常接收了数据。当主机发送了8位数据后,会再产生一个时钟,此时主机放开SDA的控制,读取SDA电平,在上拉电阻的影响下,此时SDA默认为高,必须从机拉低,以确认收到数据。4 ], g9 p/ p# M, Z y6 a# q
9 d: o6 D2 F! K, X
, C- N/ ^( D+ ^
4 \1 r+ L1 {, L' ~
图 16.1.4 数据传输格式和应答信号; E. Q% Y8 f0 v6 T! o3 H: ?9 L
+ J: e2 {9 _& C7 D1 j( [
& S$ g- w* U( u) L( x
完整传输流程
6 Q! F5 q( O3 F5 B0 y% a' S: t
8 e4 O7 }/ b* o, x: OI²C完整传输流程如下:. l0 L V, k! u
8 }. m/ D- u8 l- c w. z
① SDA和SCL开始都为高,然后主机将SDA拉低,表示开始信号;
4 I. g" E" }0 s( t) t$ [% k. ^, D9 o5 h7 v8 `$ n4 @
② 在接下来的8个时间周期里,主机控制SDA的高低,发送从机地址。其中第8位如果为0,表示接下来是写操作,即主机传输数据给从机;如果为1,表示接下来是读操作,即从机传输数据给主机;另外,数据传输是从最高位到最低位,因此传输方式为MSB(Most Significant Bit)。
8 @- x, n: ^/ V- V& M% Y
) W7 a7 G$ I7 q7 u9 e* u③ 总线中对应从机地址的设备,发出应答信号;* e4 Q: X1 t# K! N5 r) {
3 L' {" L& p" h/ B④ 在接下来的8个时间周期里,如果是写操作,则主机控制SDA的高低;如果是读操作,则从机控制SDA的高低; }+ I U" V, K# L0 q
2 v$ w, q9 k5 u
⑤ 每次传输完成,接收数据的设备,都发出应答信号;- ^& k1 v4 X3 r# Q, P7 s# C
# b' h% `) [( o3 ?/ S
⑥ 最后,在SCL为高时,主机由低拉高SDA,表示停止信号,整个传输结束;
" y, b, a3 N+ g$ o% ~1 h) k/ e/ A( d
/ ?( i, B* C3 Q9 i
! q+ j# d3 N0 h# t( a图 16.1.5 I2C传输时序
7 q. f4 s4 G# G: N2 A; q8 T- o% [8 T2 U% G K6 Q1 R
* `+ c ^) y r. [; [+ e16.1.2 EEPROM介绍* F0 l% t# k2 `+ P# r
" n2 L2 Q! V; k! XEEPROM的全称是“电可擦除可编程只读存储器”,即Electrically Erasable Programmable Read-Only Memory。通常用于存放用户配置信息数据,比如在开发板首次运行时,需要屏幕校准,校准后的配置信息就可以保存在EEPROM里,开发板断电后配置信息不丢失,下次启动,开发板自动读取EEPROM的校准配置信息,就不需要重新校准。
! @1 g8 f, d# G* d( P' h$ k( g3 c
7 c3 X* Z' z4 H1 {0 r+ f. aEEPROM和Flash的本质上是一样的,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板就有一个SPI接口的外部Flash(W25Q64),在后面SPI接口再讲解。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。两者区别这里不再过多赘述,读者理解EEPROM在嵌入式中扮演的角色即可。: d# b- l& \. S6 H9 t/ E
4 X4 u4 F3 N6 o. W0 I% B3 G: C$ k" S
8 G! p0 J# [# r: e" ]- {, A& v1 T& V结构组成 H% b `# ~0 a) ?4 p: [7 L+ g
f* W; w6 {* ?# O2 LEEPROM类型众多,其中比较常见是AT24Cxx系列,从命名上看,AT24Cxx中xx的单位是K Bit,如AT24C08,其存储容量为8K Bit。本开发板上的EEPROM型号为AT24C02,其存储容量为2K Bit,2*1024=2048 Bit。
4 J4 A1 k$ r! ]) Y \& Z. d0 T/ R4 y
对于AT24C01/02,每页大小为8 Byte,对于AT24C04/08/16,每页大小为16 Byte。如图 16.1.6 所示,AT24C02由32页(Page)组成,每一页由8个字节(Byte)组成,每个Byte由8位(Bit)组成,Bit为最小存储单位,存放1个0或1。9 V! n+ c2 q( z
$ C9 j+ O# R, w N: e
, A1 W- J$ D% a. ]9 [5 o, {
g0 s% S1 O' l9 v1 j/ T! @
图 16.1.6 AT24C02结构示意图
$ b( G1 x9 |4 q. u3 c
7 H* p2 }7 ?" ^! t1 a8 L) m: R7 e+ R6 N+ d* z% R/ i! i' \9 |# d
设备地址
3 [& K& C9 V8 ^- O: e/ ~( p/ X" H
I²C设备都会有一个设备地址,不同容量的AT24C02,设备地址定义会有所差异,由芯片数据手册《AT24Cxx.pdf》可知,如图 16.1.7 所示。
* N: Z# y9 g6 G; M
( v0 U* S+ h [ B* f- U
; k4 |, Z- f# _# j
" w, E2 ]& a6 _. I/ a图 16.1.7 AT24Cxx设备地址定义
r& Z# ~- m- M5 E0 g0 l- n1 t+ f, o. W6 a- s# S5 C
7 @0 O/ M! J( o/ D: H$ E) m
AT24C02的容量为2K,对应上图中的第一行,高四位固定为“1010”,中间三位由A2、A1、A0引脚的电平决定,比如A2~0引脚全接地,则值为“000”,最后的最低位为读写位,0代表写命令,1代表读命令。7 V. [ g! e4 H# X; p
# h9 b8 }% i4 I3 C) a: D
A2、A1、A0引脚电平需要由原理图决定,假设全接电源地,则如果需要向AT24C02写数据,则发送地址“1010 0000”,如果需要向AT24C02读数据,则发送地址“1010 0001”。
2 a Q7 g; H6 G# g3 J# l9 f' z
3 R% |2 C% O. Y1 W8 I: h假设开发板有多个AT24C02挂在同一I²C总线上,通过这个规则,只需设计电路时,让A2、A1、A0引脚电平不同,即可区分两个AT24C02。
; s9 T' u# `: I" R- d4 l8 `. c6 N- B6 x; d$ `, ^" c8 m |8 W
对于容量再大一点的AT24Cxx系列,比如AT24C04,器件地址由A2、A1引脚决定,数据空间有P0决定。比如对AT24C04的0~2K空间操作,则P0为0,对2K~4K空间操作,则P0为1。
7 ^" p# `4 i* M6 N7 j0 T, }% [8 m G2 z- r' ?+ W
9 ^0 f7 p* R2 P写AT24Cxx 8 V5 B7 f: w0 c) v* y% L' a( |
+ D! F6 t; T; FAT24Cxx支持字节写模式和页写模式。字节写模式是一个地址一个数据的写;页写模式是连续写数据,一个地址多个数据的写,但是页写模式不能自动跨页,如果超出一页长度,超出的数据会覆盖原先写入的数据。
3 S2 _0 W4 h, i% U/ M: u) j( g' l/ }7 q
如图 16.1.8 所示,为AT24Cxx字节写模式的时序,在MCU发出开始信号(Start)后,发出8 Bit的设备地址信息(图中读写位为低电平,即写数据),待收到AT24Cxx应答信号后,再发出要写的数据地址,再次等待AT24Cxx应答,最后发出8 Bit数据写数据,待AT24Cxx应答后,发出停止信号(Stop),完成一次单字节写数据。# o0 b9 h2 r! k9 V' `
4 r8 W6 j! C3 K' Q8 q3 I2 C
6 a3 {4 w" `$ ?, \6 U e1 j
0 Y8 m7 G c4 q+ f \/ f图 16.1.8 AT24Cxx字节写模式时序 j3 B* w4 B' Z: u/ i0 ]
AT24C02容量为2K,因此数据地址范围为0x00~0xFF,即0~255,每个数据地址每次写1Byte,即8bit,也就刚好256*8=2048Bit。对于1K容量的产品,数据地址范围为0x00~0x7F,最高位不会用到,因此图中数据地址的最高位为“*”,意思是对于1K容量的产品,该位无需关心。) w4 N; M+ o( }/ I$ s
+ K$ Z2 ~0 J+ K: p
) i U& n! @: \* ?, {
( C( S7 {7 Q2 d2 D7 `4 X, b& A5 j7 O
图 16.1.9 单字节写模流程图7 B6 z% }$ b$ j" X+ q
, C8 e# i; e# |0 V+ B
图 16.1.10 为AT24Cxx的页写模式时序,与字节写模式的差异在于,不是只发送1Byte数据,而是任意多个。需要注意,该模式不能跨页写,遇到跨页时,需要重新发送完整的时序。
) O# h2 V+ ]2 Q
& y4 n# z. \( V+ v8 s. d9 F" N. v
) p% ?+ \5 x0 E% J; y
4 C$ F! g- H# U8 o( n5 I
图 16.1.10 AT24Cxx页写模式时序6 S( z+ d/ g# p+ b" S
% B# @8 e5 s$ B值得一提的是,《AT24Cxx.pdf》里提到每次写完之后,再到下次写之前,需要间隔5ms时间,以确保上次写操作在芯片内部完成,如图 16.1.11 所示。5 [" g# B7 m+ m# Z! }
7 ^* v6 s. i' w
) R+ _* ^3 o% y( s8 K; u$ c
4 K! Z" S7 H/ \图 16.1.11 AT24Cxx写间隔* l' H" A( C6 k1 `* O
) i2 n9 c# w' A3 f) m2 n7 ]+ p9 R- Z
读AT24Cxx7 C2 Q2 v( t4 Q/ W& |
: z5 W' |; u# s) ]: u1 O) aAT24Cxx支持当前地址读模式、随机地址读模式和顺序读模式。当前地址读模式就是在上一次读/写操作之后的最后位置,继续读出数据,比如上次读/写在地址n,接下来可以直接从n+1处读出数据;随机地址读模式是指定数据地址,然后读出数据;顺序读模式是连续读出多个数据。
`7 ?, @0 h% G; F' k5 @/ C& y1 d# M3 V* t) l3 t) J6 n
在当前地址读模式下,无需发送数据地址,数据地址为上一次读/写操作之后的位置,时序如图 16.1.12 所示,注意在结尾,主机接收数据后,无需产生应答信号。
' `/ S! O+ t$ z% F3 }/ `" ~
1 d8 G% j& M) V8 u. a0 }% a6 M7 t
% T& _9 a8 j$ V' i0 v
- a) b- Q/ W! c5 t1 j2 d
图 16.1.12 AT24Cxx当前地址读模式9 _/ M) Z j! @$ D2 r4 o/ o; ^" C
5 @- |0 ^4 W! ^; H) [4 g在随机地址读模式下,需要先发送设备地址,待读的数据地址,接着再重新发出开始信号,设备地址,读出数据,时序如图 16.1.13 所示。- W+ G& Q1 O* `/ g" [4 W' ]8 z
( ]1 `- h9 J3 e2 o& H9 ~ V
! v; S. s# ~9 o! D" H& r' s, @' ~0 E! \# K
图 16.1.13 AT24Cxx随机地址读模式% w) _& |/ [4 B' }7 I/ e# H2 y9 K( W
- a, L/ L% E- n0 w4 e' W& M, \# h5 c3 V+ ^+ E
在顺序读模式下,需要先从当前地址读模式或随机地址读模式启动,随后便可连续读多个数据,时序如图 16.1.14 所示
+ W. m2 }% ^' \' _+ W* D u1 Q
% ~3 G0 z3 m, i1 U
. w& z% g4 _) R4 f1 b6 x& {( O( F: h. X+ H
图 16.1.14 AT24Cxx顺序读模式/ e: u, ?- w" a2 O# e K l
$ ^- h+ J7 g, F9 J! H1 ~' ^1 e( T; A: M& d5 A6 ^$ f7 @7 j
16.2 硬件设计
* H& i, R0 N9 l6 Y: p8 [& u如图 16.2.1 为开发板EEPROM部分的原理图,U6为AT24C02芯片,它的A0、A1、A2都接地,因此该设备地址为“1010 000X”,当读该设备时,X为1,写该设备时,X为0。, c5 U4 m: C; M* R
7 y9 |, n+ ]9 n; [
U4的7脚为写保护引脚(Write Protect,WP),当该引脚为高,则禁止写AT24C02,这里直接拉低WP,任何时候都可直接写AT24C02。
Z; T7 m) Y" c: S, X7 N5 _( g" u
8 V: I, a6 Y& g( l" ]4 r: q此外,I2C的两个脚SCL和SDA都进行了上拉处理,从而保证I2C总线空闲时,两根线都必须为高电平。如果没有上拉,在主机发送完数据后,放开SDA,此时SDA的电平状态不确定,可能为高,也可能为低,无法确定是从机拉低给出应答信号。
. ^6 C9 U$ u: y7 y
( @7 J! u: l6 q' k( X8 a结合原理图可知,PB6作为了I2C1的SCL,PB7作为了I2C1的SDA。
+ M- i; ^% S* S; a0 n t+ f2 [
' P+ z: k* v' d: ]1 O+ S7 v
8 E( s k3 r5 V: s6 S. `% b
$ v- [. N5 m! L, z' E) ~图 16.2.1 EEPROM模块原理图
) G5 _5 J# o! F8 Y7 Z6 v* E( H
4 d8 U( M. J7 K0 z: s' ?! H6 D: A N! B4 r
16.3 软件设计
( P# T& B3 N; d. s1 F7 c; P16.3.1 软件设计思路9 A/ J6 n9 f4 }6 s# A ]
0 C0 |+ g$ d, F实验目的:本实验通过GPIO模拟I2C总线时序,对EEPROM设备AT24C02进行读写操作。* v- [( j/ ^8 G
% U# O& K: L' n/ D& i6 Y, ?2 ^7 i4 a
1) 引脚初始化:GPIO端口时钟使能、GPIO引脚设置为输入/输出模式(PB6、PB7);4 O0 x6 F6 u' P- V' ?/ i! ~& u
% P8 o) Q: k6 ^8 F4 s
2) 封装I2C每个环节的时序函数:起始信号、响应信号、读写数据、停止信号;
( s. l! x0 p5 N, }/ K% F+ Z0 [# O; ~) U3 h
3) 使用I2C协议函数,实现对AT24C02的读写;
. I7 k3 [( W- p3 P5 }" l
8 d7 J# n% N2 J1 |8 D3 r3 ~. i6 k4) 主函数,每按一次按键,写一次AT24C02,接着读出来验证是否和写的数据一致;- H. `% G% J+ l: a5 u
8 P, i1 B& v7 c! O6 c! |
本实验配套代码位于“5_程序源码\8_通信—模拟I2C\”。* w, |: k1 C- l. ]
) @9 s, O, x6 [6 x( |
1 N! I* z* f: C) w' c0 ~16.3.2 软件设计讲解* H7 Y1 }* d- V* q/ H! {
0 s7 T/ u6 e' d( q, \# S1) GPIO选择与接口定义
% a5 I; Q3 {0 z
6 u) i Y% O+ @+ {# l# C2 z3 C( o首先定义SCL和SDA引脚,引脚的高低电平宏定义,如代码段 16.3.1 所示。) D% N" K4 Y+ z
* d2 l8 W7 J8 t! b# \& t- d k/ w代码段 16.3.1 模拟I2C引脚相关定义(driver_i2c.h)" H0 `- `" J6 C C* E# m8 {& r3 z
3 g, }- @8 b3 e% i! a7 N- /************************* I2C 硬件相关定义 *************************/0 o" \8 o0 q# f
- 4 _, z. ~. r( F# V2 }: d4 E
- #define ACK (0)- U% m G8 A& ] {
* o" }" j+ V+ } i) ^- #define NACK (1)4 J% A+ [: c. k. F0 H# e3 ]
- " C. t4 J; `! S$ u7 R
- 7 X& \; _; e+ s# c" W1 w
* C9 d. N( L# d! L- #define SCL_PIN GPIO_PIN_6
3 } @$ u0 [: t" M# \0 \, e - ) d- f1 A3 m! d x- i- V6 o
- #define SCL_PORT GPIOB. l5 o# _/ v$ L
9 r. {3 `4 Y4 t# H% Z- #define SCL_PIN_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()9 s; `8 ^# T+ Z! C. M8 G0 C: ?
- ' w+ z# ~3 m$ b4 i, L5 q8 e
- $ J+ a4 Q! W3 ^8 _( d0 U. n
. ]! W1 s0 I" }- #define SDA_PIN GPIO_PIN_7# ]! h& [" _" d" n) d
& B, e9 {1 _: V: S% e9 F- #define SDA_PORT GPIOB. C$ \9 v; T+ R7 X4 S& l) X" W+ _) h
" v% E9 A8 M3 J8 R- #define SDA_PIN_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()7 O4 r* I3 h& [+ D2 V
3 j9 y0 t/ j9 B% K/ y4 Y; j
1 U4 s. |- s* D7 X+ V
5 m5 r+ A; B9 n! W3 J% H- #define SCL_H() HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET)
% |& h7 S7 s% }1 x3 d1 f
$ |; v7 a; l3 l: ?; Z- #define SCL_L() HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET)
" S+ J1 K: [5 O+ b - . M; N$ i5 O; `' I3 Q
- #define SCL_INPUT() HAL_GPIO_ReadPin(SCL_PORT, SCL_PIN)( y! m4 K3 y, {$ Z) u
) v! \6 L0 `' T( r! g8 h- ( t/ B) T$ k2 |
$ W& Q9 E) e! ]9 `- #define SDA_H() HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET), F6 {! u, c" S& d
, I0 }: ?; g( v% I x6 E5 g6 S- #define SDA_L() HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET)
: l% J& C( U* a* m J" U - * T, ]' m( f3 Z1 m
- #define SDA_INPUT() HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN)
复制代码
2 n3 f$ ?; D( z3 b |4 S% v接着将两个GPIO引脚初始化,使能引脚时钟,先默认设置为输出模式。SCL引脚为时钟信号,始终为输出模式,SDA引脚为数据引脚,可能输出或者输入,因此还需要编写函数实现输入、输出的切换,如代码段 16.3.2 所示。
: m1 _0 s: A+ B4 W* m# _3 N7 C
1 c' g$ I7 A# ?代码段 16.3.2 I2C引脚初始化(driver_i2c.c)
' g a) `/ y* f. U
% |, I/ ?# [" o; j& b" B- /*
0 e/ R0 m( K Y+ r7 D' F3 ^ - * 函数名:void I2C_Init(void)1 r+ D4 i) N c- ?% e8 W
- * 输入参数:5 L1 L( ]; t$ }- u
- * 输出参数:无
% w* O( x7 @6 F+ ]+ q - * 返回值:无% a5 F! n: @! l
- * 函数作用:初始化模拟I2C的引脚为输出状态且SCL/SDA都初始为高电平
, }& } i' B9 f: ~ - */
7 L9 o% x A) G2 l+ J, P- W - void I2C_Init(void), R$ v) R1 F) e+ I% o6 q
- {) u `4 P5 n: z; @0 D
- GPIO_InitTypeDef GPIO_InitStruct = {0};7 y( }" g9 q* z( M$ E
- : Z9 K4 A% Q9 A
- SCL_PIN_CLK_EN();, u. |& V/ K8 @9 n- |
- SDA_PIN_CLK_EN();. B) |3 F# D0 E4 i
7 K1 ?1 | Y6 x" z! a- GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
3 S/ g( j+ ^, }7 R2 `/ \ - GPIO_InitStruct.Pull = GPIO_NOPULL;# Y; Q# Y( u0 _/ n+ w
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;3 p2 I0 \6 d. \
3 t- Y, w- ^5 c& {- GPIO_InitStruct.Pin = SCL_PIN;7 q' I- u; r- Z
- HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);1 l' g3 N1 i0 l; m" T) C
: X# J& z6 F, e! f3 S- GPIO_InitStruct.Pin = SDA_PIN;
1 P3 D/ i! \+ S1 {( U' C - HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
+ ^: r- I5 Y) g& |: h+ E' P9 m" p+ x
' k" P8 R4 j. V: } a ]- SCL_H();
# d2 z) s' E2 q# q! f) q - SDA_H();- H8 S2 l" m" R9 \, z
- }
6 j) S& h% A' C5 v$ D: w - D A u$ C& h7 G1 J
- /*: K; ]: c1 R4 m
- * 函数名:static void I2C_SDA_OUT(void)) Q _* J8 C$ f
- * 输入参数:" H: m' ?7 T" {9 V* u9 P
- * 输出参数:无8 }! Q2 S9 f' f/ C! t9 T6 R
- * 返回值:无
; j; s$ y6 \* z+ t$ r) l0 M9 B - * 函数作用:配置SDA引脚为输出. p- I6 p9 W6 t
- */
$ R" U9 M7 H7 ?6 j# v { - static void I2C_SDA_OUT(void)
5 H1 H; `+ \" R* [8 {' O - {
6 V& J+ [" J7 K7 N) j W - GPIO_InitTypeDef GPIO_InitStruct = {0};/ N# ?% g: M4 L" K
Y, m$ i2 x+ S |$ j, G- GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
0 S0 W/ m7 d+ B4 E$ J. H: P% o' l - GPIO_InitStruct.Pull = GPIO_PULLUP;5 k0 k7 M0 W8 u3 h" a
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
3 h7 R' Q$ ]2 y/ o7 v. L4 A& X/ A
D" c( F" ?/ d6 a) j: b- GPIO_InitStruct.Pin = SDA_PIN;
# e; u/ Z( c" w& ?+ x - HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
, p5 w. C# L. Z' W - }8 K- n8 ~" X2 p, i2 q+ E( f
- + Z: E- F3 E' L S b+ p
- /*
9 ~4 t# a! G5 z6 {* v" Z: v- u9 ] - * 函数名:static void I2C_SDA_IN(void)
9 W: E$ m2 V9 A& U+ N - * 输入参数:" X/ {4 S# f0 S3 K7 c( c1 r
- * 输出参数:无8 J% ^: _2 j) a' ~$ f) y3 B- _
- * 返回值:无 h% e! w8 I L( P3 X! |
- * 函数作用:配置SDA引脚为输入7 `" o2 @* L! o" Q6 X% _: C
- */
' i2 V2 E7 [; I# B1 v/ c9 }% J! p - static void I2C_SDA_IN(void)
1 x% J$ I. G' D; H1 l - {6 R- R. _( q5 X$ L
- GPIO_InitTypeDef GPIO_InitStruct = {0};# _5 v+ c( [4 {6 ^' _( A3 l" \" `
- 9 ~. }: d2 D3 p* A/ V
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;) R: {7 l; G) W3 n* K4 Q
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
3 o' C; Y* S+ W- D' ^) N
$ q& o4 T3 T7 f: ~% a9 k6 C- GPIO_InitStruct.Pin = SDA_PIN;5 `) L. y! z X+ Y/ G( E+ W
- HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
% I- t z0 @* ] - }
复制代码 2 s3 Y2 \% }: u& U8 m& [* Q+ q* `
) {4 M$ b( u2 K- O0 O2) I2C时序函数
1 `" E' O* e; t$ F E' s6 Y
+ r9 l4 k6 P. z开始信号/结束信号/ U+ M" S( F, J0 M" }( X
4 y6 p$ S2 b( j/ n: n
参考前面图 16.1.3 所示的开始信号和结束信号编写程序。对于开始信号,首先将SDA和SCL都拉高,随后SDA拉低,再SCL拉低。对于结束信号,首先拉低SDA,拉高SCL,再拉低SDA,代码如代码段 16.3.3 所示。. m/ k, O P" ]4 [5 l0 ?$ i
% ?9 J" s/ U+ M3 i- b
代码段 16.3.3 I2C开始信号和结束信号(driver_i2c.c)
; Z0 f0 {( @( {) _2 w( _1 V# a1 O7 x! ?" Y7 Z5 ~
- /*
) v/ g2 @3 ?! ^2 p4 N - * 函数名:void I2C_Start(void) a1 p/ {: l& E8 L m6 `" `
- * 输入参数:9 r7 B2 s1 J% u/ y! S) Y
- * 输出参数:无7 ]8 _; G2 n+ K7 \7 ~' [8 X* p
- * 返回值:无
0 l5 v7 Z, T' T" L - * 函数作用:I2C开始信号' @& i3 K$ ~. |0 ?8 m0 v% X. `
- */
! t) x. E2 ^# y - void I2C_Start(void)1 m7 E5 |& E2 z. A! C5 ]2 s
- {3 U' \4 X& K3 \" H; e
- I2C_SDA_OUT();
; Y, @5 E0 N/ y6 } `# o - ) w& F; H6 t9 \3 F
- SCL_H();0 I* {6 X+ \. i: T" z( l( d: b
- I2C_Delay();
& a. u h4 h0 D& @% P" P2 n
2 X, t) d5 ]+ l& k+ z- SDA_H();+ @- W7 V2 v0 o1 d5 V
- I2C_Delay();
$ P! z0 l( @ A2 |! M% S3 g
9 ?& j0 e7 |% @) W/ E- SDA_L();
# n- a" ~+ b3 W# h: ^' g+ n - I2C_Delay();/ p) d W6 X' G' I& s3 f% n
- , n& b, @ [9 n8 Z: B' F
- SCL_L();) w# P) x- q' m5 u, Y; W( p8 e4 C
- I2C_Delay();
! e6 ?3 Z1 x# I2 {9 z - }# ?* r5 g4 z' a; ^7 s
9 S4 G! p9 g7 h2 W/ P- /*7 q1 x1 w L* u8 w& k- q1 P
- * 函数名:void I2C_Stop(void) n% N# k# Z' b# y; ~ R' ^
- * 输入参数:( r3 _. e/ @6 Y/ y/ g Q2 B! Z
- * 输出参数:无' H* @3 _9 v! r( D
- * 返回值:无) }( S9 J8 y1 {+ W* p3 \
- * 函数作用:I2C停止信号
" R: t9 E& C ^$ [ - */: R' N) ^ a) L9 |3 S! a
- void I2C_Stop(void)
! Z! P2 P0 Q0 @# ~4 i: F( | - {
) \9 X+ u, p' m7 z7 w9 \, l8 n1 p - I2C_SDA_OUT();/ n; C2 Y3 @0 T) I- w
- 8 |" A5 C1 U6 Z! ~: s7 q
- SDA_L();
- ^6 w) g, T* {+ X* B* b, a - I2C_Delay();% O- A2 b0 F* H m1 i
4 A5 K; U. \" g0 K3 p6 |6 P- SCL_H();/ U, u5 ?" q2 t5 H0 `. L
- I2C_Delay();: Q, S6 E8 q6 a6 {6 G' H
- ' h8 c, ~6 W6 n4 |) A4 l
- SDA_H();
# w1 o- N: x* f n) j. l - I2C_Delay();, i1 P, e' T3 t+ a/ `
- }
复制代码
! r o9 {7 l. l
1 B+ S% B. e- L$ t! B应答信号/非应答信号/等待应答信号; D1 L0 u3 D' K$ ^( \3 o- t
+ u, y. y9 S' G: i9 S3 T- d1 D
参考前面图 16.1.4 所示,编译应答信号,如代码段 16.3.4 所示。
m9 {; G8 l: }' z, h5 }: W, L/ Y* Y4 K1 E+ K
代码段 16.3.4 应答/非应答/等待应答信号(driver_i2c.c)
3 m3 c$ i9 `' s; {
* d, m9 A3 q2 d3 q1 z- /*
: k' ?' {. d: Z; d) Z5 x - * 函数名:void I2C_ACK(void)3 d4 l c: Y1 ]* Z
- * 输入参数:& ?4 L- q$ W5 J. P7 s, Y9 Z6 l
- * 输出参数:无
c/ Z1 `$ Q$ B8 m4 i6 d: Q* m; N! h - * 返回值:无! F' n/ L' J% y9 x D: y9 k
- * 函数作用:I2C发出应答信号
& }. Y$ n, C3 ] - */
6 f) S& [ G* J% Z - void I2C_ACK(void)7 Y+ l& o) C% u! i% O0 Q2 D; P
- {
9 X, a, M) f4 X3 D9 J9 | - I2C_SDA_OUT();3 \0 E: G+ f6 Q. H, A% ~7 j
- " h4 R, r' p! A1 [1 F
- SCL_L();3 i' u; O/ c D. v ?8 Y: K! b7 u
- I2C_Delay();) A; B: r' F! S0 R8 ]; [3 I
- K4 B9 w% Z, j- B; A) E- SDA_L();: q+ x! _" l6 o! Q1 C M
- I2C_Delay();
! v3 S% w- ]" ~# v
" m' M+ x8 E: r) f) N* X- SCL_H();
* c/ r% A6 R& k3 K - I2C_Delay();
$ b& S9 q- \( Y8 p$ H - 7 N; k+ U" r! L1 e5 P
- SCL_L();0 \0 S/ {3 z+ x6 Q9 `9 w
- I2C_Delay();1 L0 K/ b. k' J# x
- }4 A6 ?2 g1 u. x& C- ^- A
- : a2 d6 r# _9 v2 p) r
- /*
+ ?4 w- j! C Y: n2 k: Q+ s - * 函数名:void I2C_NACK(void)- h+ S, @: C6 k( T9 w7 m
- * 输入参数:
' V% N" o/ K- x - * 输出参数:无, H) a) G" k6 ]
- * 返回值:无- ?6 c# z3 e0 Q0 ~: }; I5 c5 N
- * 函数作用:I2C发出非应答信号
' m Q$ [* l4 e2 S# y; A- e/ W$ e - */
/ V' B2 ?) u' _* X - void I2C_NACK(void), V( x9 F! c7 Z4 n. R% n9 w, V
- {
( }1 a" z* S, G9 y" Y, r - I2C_SDA_OUT();
* b( n I0 G+ I8 j+ ~3 H
9 P2 h: S* P+ e, r/ L+ p: w! K- SCL_L();
/ X& _& ?9 a! O - I2C_Delay(); L: z0 ^% F" T$ j/ i
- 7 S2 T( z0 O) O8 q2 g
- SDA_H();, u# L Q; ~% K( X7 N: a
- I2C_Delay();: S' V2 O z8 h$ @8 x! H
2 x! i! I, p @- SCL_H();
6 e% f' |1 Q b6 ^" _ - I2C_Delay();5 }6 n! l* E6 T
- 4 M8 k6 ^% d. V. M4 H z! Q1 y
- SCL_L();
" U* B \0 m0 B! e - I2C_Delay();; a8 j) c0 R. H8 x n. H2 s, M
- }
1 }' I0 d7 i1 B0 t; X4 ]$ \% h
- c& H- g2 _( j' B" q( q+ {5 n- /*
8 ^0 j/ W5 |. B1 P - * 函数名:uint8_t I2C_GetACK(void); D; F( W& G5 N; C! u# a" E
- * 输入参数:% r! r9 V) ?" a, s8 X
- * 输出参数:无) l$ S1 _7 g, x2 x- U' l1 H8 v
- * 返回值:1无应答,0有应答. }9 G: v3 e/ D) k7 H N' F4 R
- * 函数作用:I2C等待从机的应答信号
4 i8 b0 n x# K2 ~+ Z - */
5 Q/ X: c2 K2 @3 e C - uint8_t I2C_GetACK(void)
6 @1 r2 @3 ?; c1 A/ r; P% ]$ T! t - {
+ z9 j7 Z J' l! d# h8 h$ q3 f - uint8_t time = 0;
/ @( m" X8 J" E8 H - I2C_SDA_IN();
# h8 E% ?( F1 [, d- N& f
. r' B: `* e2 U2 u+ m- SCL_L();1 [; U: q8 ~2 a. b- K2 b
- I2C_Delay();
. m% O* C) z$ n7 h/ x - . k k7 |/ m2 ], T+ f- ]
- SDA_H();
# e6 X, O5 A# ^+ o! Y - I2C_Delay();
% ?1 y; ^# F) y1 Y( f - ( I; u/ X* _1 D: t
- SCL_H();( }% e* Q0 O$ R2 T2 A) j3 v5 h
- I2C_Delay();
( }3 I8 w: z( L1 ]. n
6 u. M4 d) A8 e. \; S" L" X/ z- while(SDA_INPUT())
8 Z) Z; ^% u* d6 c e' n - {
2 J) l( W, C5 D8 E/ e - time++;" u+ j2 P; D) r) ]. `7 Z1 J2 S( r6 t
- if(time>250)
i) _6 E5 X M0 r$ h9 F X5 }, \. @ - {
& i! C) Y5 W2 f3 Z6 T* J/ N" a - SCL_L();1 q4 V5 {6 W% e0 P
- return 1;
' _ Y9 W7 S+ _ - }9 C; Y! K0 Y4 `% X+ S
- }. e( m, l- I% D4 ~' h: f
- SCL_L();: X9 M Z1 B3 s
: F( v0 F1 D! u8 H5 \0 \! l& n; O- return 0;
0 n! C; _. z) F - }
复制代码
, k" k1 I& h/ J2 T8~23行:应答信号,在一个SDA时钟周期里,将SCL拉低;7 B/ o% c5 t u0 g4 Z9 V
+ k |! E+ q7 l4 T; e- X32~47行:非应答信号,在一个SDA时钟周期里,将SCL拉高;$ l8 h$ c8 p0 |; s0 ~
' Y, _. _* [8 G. \6 I. v
56~82行:等待应答信号,拉高SDA后放开SDA,读取SDA是否被拉低,如果拉低返回0,否则返回1;
9 D5 ]7 p( f* y
. g( {9 U5 q3 ~ V0 A; M0 h! e6 l2 M4 Q5 |( P
发送/接收函数 Z @2 C. R( G# f7 ~% R
+ @. P; R4 ^/ D6 q! w% V8 }
最后还剩发送/接收函数,如代码段 16.3.5 所示。对于发送函数,控制SDA产生8个时钟周期,每个时钟周期里控制SDA高低电平发送1位数据。对于接收函数,控制SDA产生8个时钟周期,每个时钟周期里读取SDA高低电平接收1位数据。
0 K% t) W& ?6 ]' \
7 R' a j [" f# W5 R( P代码段 16.3.5 发送/接收函数(driver_i2c.c)) J0 T; T2 E1 Q- `1 ~
* [, q9 W$ \, N: k ^& r
- /*( _& f% {1 `* a5 p `; K% \$ v
- * 函数名:void I2C_SendByte(uint8_t data)$ P1 [2 P% Z3 n
- * 输入参数:data->发送的数据. F# w) q+ {- x0 d: C0 F7 y4 J
- * 输出参数:无1 I6 ~. ~$ k0 L( D0 {
- * 返回值:无" b B0 {2 [& g$ T, d
- * 函数作用:I2C发送一个字节 f5 z* S3 V, O9 C* _0 _+ k
- */
2 H, Z# ]5 |) z5 u1 n! w% _; w - void I2C_SendByte(uint8_t data)( ^6 c! u$ M# j
- {1 O( Z# }4 W y" w
- uint8_t cnt = 0;6 b* Y! b$ u6 s+ N
- ( I* [' H, j% x$ p3 s/ i" Q- F2 T
- I2C_SDA_OUT();
/ J$ ]' b! `) a4 m p - * V) G( I& l R6 B
- for(cnt=0; cnt<8; cnt++)
& Y, c$ G" N; c6 M6 ^$ k$ O) B - {9 g) F3 k$ ]* _
- SCL_L();
' Y/ N& z) a$ v3 P - I2C_Delay();
) i9 s) X- C. o
( v4 D2 M6 n' d% v e- if(data & 0x80)8 ~: \. _. G: l0 [$ M
- {
- K- r) F, k& L2 {5 f. Q: ? - SDA_H();
( r3 j! A: y" s u* d9 z - }
" \4 v, ^ h" a& b: }1 G/ x7 m8 J - else! M( d6 b% R: R0 Z# ?! X& h9 x" ]
- {2 W; Y6 k* |' c9 A+ o" ] T$ W% v! K
- SDA_L();
& S2 Q8 P6 w& F - }5 K1 l6 p" `- L
- data = data<<1;
* j- K( W u$ m' m N" ?7 k) J - SCL_H();9 T# \. \; e) E7 Z! f: }5 K
- I2C_Delay();
( U) Y7 {4 n7 n/ q - }
9 |; m' i. c# z0 F; e
9 R' I; t! R$ k- SCL_L();
& N# o5 ?! E. {3 @0 e2 i - I2C_Delay();* K! `3 z L b
- I2C_GetACK();# G9 A s3 \: E$ K" R
- }
/ E3 [, w; Q7 z. ?1 p$ n* Z
+ ~7 W( M( C/ K+ {- /*
4 l: _' v6 G; \$ d% z - * 函数名:uint8_t I2C_ReadByte(uint8_t ack)
4 b! f( `9 H0 x% C9 u' h( Z8 \ - * 输入参数:ack->发送的应答标志,1应答,0非应答% B7 @ [( {3 P, h: O0 U7 g
- * 输出参数:无! w+ e; N, p+ R2 j4 {, f) q1 N
- * 返回值:返回读到的字节
3 Q! L- v* L0 F7 Y9 E+ t7 z - * 函数作用:I2C读出一个字节
, O8 s3 u6 n1 k. H" l - */+ ?5 s0 j( v( w2 v. a' g' H( q
- uint8_t I2C_ReadByte(uint8_t ack)" I' ^7 r3 Z; r% I# U
- {! b. L4 H' A0 {+ e! b: m8 |
- uint8_t cnt;
' G! q, Z! F/ b' K( E. N- x - uint8_t data = 0xFF;& k3 c0 ~4 M. e/ q
- ; g/ d1 L+ A* }9 f$ D
- SCL_L();% Y) Q5 a4 D7 R2 K) y r. Z
- I2C_Delay();5 v& s2 {* R1 ?/ h& n
- * E5 D! t* p" S6 o
- for(cnt=0; cnt<8; cnt++)
" o% f5 ?2 S, \ _4 O' ?0 i+ p9 B! | - {$ H1 _* f+ y/ c/ u
- SCL_H(); //SCL高(读取数据)% ?% S4 R- Y1 Q" S! }
- I2C_Delay();
! N5 Z" C' R; ?# g2 P% X- e - 8 {% t' Y- n0 U, h" N$ E
- data <<= 1;+ E! E; W* z( K u( r. \2 R5 V! P/ O S2 n
- if(SDA_INPUT())
2 |3 W7 D, z: u - {
- E/ z+ c! H9 Y G7 L - data |= 0x01; //SDA高(数据为1)
5 Z: m# |$ b. s1 u S0 \ o! i - }
- F/ G3 L& {9 ?) O! I7 s - SCL_L();: o, D, N4 q: y5 h* Z
- I2C_Delay();
2 s' J1 \1 \8 {. b - }' T% ^/ M2 S( d' Y
- //发送应答信号,为低代表应答,高代表非应答
. M% _2 i I/ c2 L - if(ack == 0)
; c/ P, f% Z) L1 A - {
2 r% _- O0 g& d, a6 s7 Z - I2C_ACK();' R, F* A7 L- i8 b
- }% n, n# F; {" t' h. |
- else9 q. C7 |1 ?2 x0 j
- {
% T/ f2 \5 w( i5 a" q - I2C_NACK();$ m+ M% C$ d. i$ j3 e% ^# t R- U8 Y
- }: t. _8 u% Q& P/ f7 @
- return data; //返回数据
0 w' A- A( y, u$ ]3 i. r4 u% W - }
复制代码
2 t, F& |) p% B" L# X7 u14~31行:循环8次,每次循环:
2 W: T# O& n- O1 D8 d( Q- `4 ]1 ~" G2 Q G: X2 ~ a! h2 N
16行:先拉低SCL;
! l/ ]: t" g: i5 e2 U$ N
" B1 o* O8 j% v* p) j9 |! K7 _19~26行:将输入的数据data与0x08且运算,得到最高位的值,从而控制SDA输出对应的高、低电平;+ h q9 s4 ?( t& _9 i
: V/ {0 i4 S' A0 d8 ^7 V! M% J
27行:将data左移一位,得到次高位;
6 ~0 v8 T: v& Y, b' e v/ P/ K
+ L' P1 C& O& t" _5 ]' b29行:拉高SCL,让SDA处于稳定期,从设备即可获取SDA的值;
/ Z* H, S( b( m0 o ]
& v5 s1 B8 ~& x7 }5 z# u2 C35行:等待从设备的应答信号;
* q: _9 }0 _1 F- [+ v0 X6 c! x6 t8 r* E3 F9 [
53~65行:循环8次,每次循环:& a( o/ \ p6 B! J/ ^+ |7 N/ y6 i$ B
& D- @: N" x$ o
55行:先拉高SCL,此时认为从设备控制SDA电平,处于稳定期;
]9 ]/ M% J7 H: O3 L1 h: x4 a+ N7 F$ _/ v: m/ g/ c
58行:将data左移1位,以确保收到数据按最高位在前存放;
' B( W% J$ u4 u' s" a) F4 C7 V6 a8 n4 s2 F7 x, N: k
59~62行:读取SDA电平,如果为高,保存到data当前最低位,否则data最低位默认为0;6 a; c$ [6 o+ ^; v% Q9 ~
+ {# s$ F" p# F# r63行:SCL拉低,此时从设备继续控制SDA电平变化* @* W4 y% O' v: L
3 C& ?: F0 t9 O+ p: J
66~74行:根据传入的参数,决定是否发送应答信号;
" u. w& ?7 O6 h6 T2 e
" i- x! }( ~* B2 V' W+ O% a整个I2C协议函数中,经常用到“ I2C_Delay()”来实现SCL时钟周期。对于AT24Cxx,由其芯片手册可知,时钟脉冲宽度(Clock Pulse Width)需要大于5us,也就是SCL如果刚变为高电平,需要等待至少5us才能变为低电平,因此定义“ I2C_Delay()”为5us以上即可。
0 K. s& M( p/ R( J8 ], }0 S( }' c$ c @7 x
- #define I2C_Delay() us_timer_delay(5) // Clock Pulse Width >5us
复制代码 , M B6 k- _3 u5 j& x% j
这里的“us_timer_delay()”可以由定时器提供,也可以使用循环提供,前者精度更高,效果更好。定时器的介绍在后面章节,本章不作分析,延时函数的两者方式如代码段 16.3.6 所示。
. p4 c+ c) D$ f7 \# ], \/ @
$ w& r+ L/ m( I$ F. Z代码段 16.3.6 延时函数的实现(driver_timer.c)
6 P: Y& F- B# h: D/ i
; l# `' _; l, h2 s; Y+ N d- #if 0
3 }7 q4 j' X: o" s7 T4 ^" U2 p - /*
* {- V& |* w) c( F3 ]# | - * 函数名:void us_timer_delay(uint16_t t)( I* f: f' X, Z' P& ^: u) ~1 r/ ?
- * 输入参数:t-延时时间us
0 @ e; J1 q4 e8 n - * 输出参数:无3 w: g% q' f* w2 \6 C- m: P$ U
- * 返回值:无
2 ~' U/ e& c1 i2 r - * 函数作用:定时器实现的延时函数,延时时间为t us,为了缩短时间,函数体使用寄存器操作,用户可对照手册查看每个寄存器每一位的意义+ M& _- N2 N: V% d0 f1 X; v
- */
2 o. E+ R/ |% I8 R1 D. W; l" T - void us_timer_delay(uint16_t t)
- e7 t' r$ K C, h S4 ?; {5 ?4 P - {) L8 q j6 y1 b8 w2 h
- uint16_t counter = 0;
& d% J1 }4 v9 S$ s+ T - __HAL_TIM_SET_AUTORELOAD(&htim, t);
9 W4 L( F3 l! a" H, U2 A" e - __HAL_TIM_SET_COUNTER(&htim, counter);2 I: x. P- O) h' g3 ~; U7 L, j! G
- HAL_TIM_Base_Start(&htim);
7 [6 x0 a; A! d: {; A# S - while(counter != t)
6 Q/ z" Y+ f; Q! P5 Z& B4 h I - {. J, E- }) E2 L! s
- counter = __HAL_TIM_GET_COUNTER(&htim);
1 {* [9 N! x; S& f2 h6 @ - }
- a/ U2 q4 e) m4 y; U! [ - HAL_TIM_Base_Stop(&htim);
: I R5 t* b9 y9 _7 x( M - }
+ ]- [" \% y+ ]5 p3 M m) p - #else
8 _% |3 s! p9 z6 { - /*
3 u- \; e0 s" x - * 函数名:void us_timer_delay(uint16_t t)
9 C! h! `& ~5 X - * 输入参数:t-延时时间us
* u' W4 P& Y! T - * 输出参数:无5 c* B) S9 f6 @' M: T
- * 返回值:无
; {- l- a8 ^- w4 g0 {5 w( r - * 函数作用:延时粗略实现的延时函数,延时时间为t us( G3 L1 \* O, ~0 x; p+ W
- */
$ |9 _+ R: r: L* a a( Z3 {7 k \ - void us_timer_delay(uint16_t t)
' s" ^3 g4 S) F' c% s - {7 }" Y; \) I5 p+ |
- uint16_t counter = 0;: ?- @, D% s+ q b7 F ^) A5 d
/ Y" L3 G" _$ t7 J- while(t--)! ?9 v* W# D/ G% y4 d, S$ W
- {: E$ U7 I) s$ m m% _2 a) J3 i
- counter=10;
5 N& }: u- I' P$ u# O; W - n$ g7 l9 {$ T- F) k$ Q
- while(counter--) ;, E0 z6 b: F, ] j: V: B. q- \
- }3 D- D& r* |) [
- }2 c& h/ J6 ?; g0 w7 ^
- #endif
复制代码 7 F5 @) Y, ~" ?! @& l& o
5 A( n; ?! F% X2 _3) AT24C02读写函数: o/ ?% |# k: H7 u X4 F( z
& P' C! Y: a" p, u) X0 ~/ G: j
编写好I2C协议函数后,参考AT24C02手册编写读写数据函数,如代码段 16.3.7 所示。, A+ J+ v+ H2 O" L1 A& L' p o1 O
, `% \8 U, n- l3 {9 ]+ y2 r) g代码段 16.3.7 读写AT24C02一字节数据(driver_eeprom.c)
" ?, n# ~' _& P1 D6 t8 J& D' _- A1 U8 E
- /*) b6 \3 I! I% x
- * 函数名:uint8_t EEPROM_WriteByte(uint16_t addr, uint8_t data)2 b! _9 ^/ ?* P h
- * 输入参数:addr -> 写一个字节的EEPROM初始地址7 B: p, }% ~% g9 R+ v6 t
- * data -> 要写的数据) [, m5 l6 [* C4 r3 M
- * 输出参数:无1 Q# c T4 O+ H
- * 返回值:无; N) \% l" l7 j- u* e* b3 |$ y1 `
- * 函数作用:EEPROM写一个字节+ Y2 m ~8 n$ n0 W
- */" t& f1 K$ w, s b' Y3 Y' d# h. ?
- void EEPROM_WriteByte(uint16_t addr, uint8_t data)
! z* u/ M* P$ f- \ - {7 v- v0 N+ [) G6 h
- /* 1. Start */
: d( M* q/ @) x8 Q5 c! C7 v8 X - I2C_Start();% g9 v( y9 q0 K, z/ z2 |
- M7 D7 ^0 z8 o+ i
- /* 2. Write Device Address */8 ~2 t6 R# E$ H6 S; R* d9 M5 P: j
- I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );
! d2 ?7 k$ j5 j" [# l0 h1 ? ^
* E/ s$ e8 f# _! N& J, j; g2 N4 S- /* 3. Data Address */
+ M6 K2 ]& T8 r% F( Z) l) n - if(EEPROM_WORD_ADDR_SIZE==0x08)
+ [, j; J \* S2 r - {) ]) g, }, @- R; a- _% b
- I2C_SendByte( (uint8_t)(addr & 0x00FF) );3 Y4 w) O7 X, [
- }% H1 T2 Q3 r) ?) y
- else
# c! P# {% H8 @( C7 F7 | - {/ }2 b, ]5 j) a3 d" t7 x! G k/ X
- I2C_SendByte( (uint8_t)(addr>>8) );
+ o- s: ]0 @$ P" [% J; q - I2C_SendByte( (uint8_t)(addr & 0x00FF) );0 U8 s/ t7 U, f# A5 o
- }
3 B9 Y" u2 I! e# m% w* K% N" `( s9 Q
8 w- B3 I2 S. ^9 j* _- /* 4. Write a byte */8 B# C) A2 k- l2 Q$ v4 j# v- j4 U/ R
- I2C_SendByte(data);
2 c: e( l- f- P4 k9 t3 b: Z
/ B) f" } ]" ?; v w* d, d- /* 5. Stop */$ l+ h7 Q9 f! R# ^
- I2C_Stop();
7 P: Y+ l$ g: x% b& f8 D - }, l) |* F% v. H/ y7 R/ `! W6 G
2 j# t5 P! S' T `$ l: l- /*
& t3 z2 C: C. @/ P* W( G) z - * 函数名:uint8_t EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)$ n0 M; V- ?/ M: e
- * 输入参数:addr -> 读一个字节的EEPROM初始地址: m" X+ d8 @( Z& t R. @
- * data -> 要读的数据指针2 U* {2 u. ~; e
- * 输出参数:无1 V( ^2 E# b# l0 f6 @
- * 返回值:无0 P3 _% W/ O4 q- O
- * 函数作用:EEPROM读一个字节% A# c7 u$ X: I# h5 S. u* i
- */* u j; J5 r# Q. q! J: L* [" G' r
- void EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)% v' k( W: M2 u
- {
( D1 `- g2 |% p' [) ]3 ~% z& } - /* 1. Start *// e$ a4 S0 m7 t: N
- I2C_Start();: O" w+ {* a4 J8 x
- " u5 d1 c8 E0 T& _
- /* 2. Write Device Address */) [! W1 `# e# q
- I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );8 L% q; j$ X+ U& V( P; _& K. j% a4 z
/ B, {' F7 C1 k" h2 I' o- /* 3. Data Address */" o, w2 C6 K! H$ e0 Q# @5 Q
- if(EEPROM_WORD_ADDR_SIZE==0x08)9 B( [3 K/ y7 Y& a* s# n
- {9 O- u6 | z" W( g B+ D
- I2C_SendByte( (uint8_t)(addr & 0x00FF) );3 z+ f ?9 O+ M2 T+ h; f
- } g6 y( E6 Q8 K' K/ x g, ?
- else
' s+ w+ Q [) n: D+ f - {& t8 T& E- ]7 w5 ] N
- I2C_SendByte( (uint8_t)(addr>>8) );6 r) K) b& J9 ?. p+ ]6 n& D( ~
- I2C_SendByte( (uint8_t)(addr & 0x00FF) );6 i4 h |4 S: `& K7 D4 Z/ j. w
- }
# z. } f! R: |1 _8 c) i/ h4 o
$ X8 L* r) M. G- /* 4. Start Again */6 e! ~5 A4 E l$ X% {
- I2C_Start();
& S3 Z! m4 ~: j% @7 ?
# j+ e- s/ F+ G7 x7 i" r# o- /* 5. Write Device Address Read */
% Q, z4 s: K" w8 q4 @ L - I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_RD );
: A$ i; m% A: n- g* {, |3 e( W, W
8 k& h4 p1 \/ f0 ?% ] n- /* 6.Read a byte */
$ b5 D/ E) F p0 z8 j7 w% s - *pdata = I2C_ReadByte(NACK);, t* p, Y* l) l, s, @
% B; w0 U6 }2 |: {' H- /* 7. Stop */
2 q% k+ J: X1 A! e/ }! ?: V - I2C_Stop();: ?' B( R5 L0 _1 I. v. a
- }
复制代码 : Y6 d$ ^8 _& O, X& C
, T O; z$ p( P; i7 @ b; l参加前面图 16.2.1 和图 16.1.13 所示的介绍时序,编写AT24C02一字节读写程序。
1 x, H, q; q! ]3 L4 D7 o
4 X4 L n: n2 x6 \; m0 h9~33行:写AT24C02一字节数据;
: F# K4 d4 U9 s2 L. J# `( d% {. V7 w/ _3 Q
12行:发送I2C开始信号;
) p& y, g7 h" i6 R. Y4 p' c) h% O* C- [" D
15行:发送AT24C02的设备地址,最后一位表示写操作;
; C O$ k* |% p4 L5 x3 P
' v# L' R8 ?1 w& r# ]9 T2 T18~26行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16为16位);
$ Q0 ^' W2 `% i! {& B' Y2 ?" J6 z9 v) w% D2 x, O! j; a
29行:发送数据;/ F1 [1 m" i" b- @! G2 t
# ]; f l* ], k5 ^: t/ @, e32行:发送I2C停止信号;, G& N& {1 s* c5 F/ x& o* F$ p Z
6 p9 i; D0 J5 h# ~
43~73行:读AT24C02一字节数据;
& M0 q6 J8 E6 }' R4 N) ^) r
5 F4 H" p! L% t- a* s& U6 ^8 Q- w% o46行:发送I2C开始信号;
2 w: s( W! O8 u5 K6 c8 P- C4 b# p3 `$ {/ `3 h
49行:发送AT24C02的设备地址,最后一位表示写操作(接下来要写数据地址);
( R/ e1 E1 z, u8 W l4 ^8 l+ T+ l
# e5 I. s* X' W4 q3 @$ b! d! u52~60行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16为16位);
. `) Z2 B% d R: Z) u- U
1 U$ o' W( l& q63行:再次发送I2C开始信号;
# {7 c( d; I4 r* U$ B: l/ C8 |3 U3 E( n0 U" B
66行:发送AT24C02的设备地址,最后一位表示读操作;8 m' p+ W7 R; J' _8 H
7 x! ], m* L2 e/ |* g7 R) `, `69行:读取AT24C02数据,且无需ACK;
% J' P* F( r# d. X& P9 h4 S& s, N7 H" V3 }" m. v' r K0 c
72行:发送I2C停止信号;
9 I* F3 U7 K" m8 u; b& I' f' E: m2 _- J! n& V
实现了对AT24C02单字节的读写,还需要实现多字节的读写。多字节读写可以通过AT24Cxx的页写模式和顺序读模式,实现多个数据的连续读写。在页写模式时,需要程序上设置,不能跨页写,这里简单处理,直接多次调用前面的单次读写即可,如代码段 16.3.8 所示。: K# i) B4 E; j& F9 P. f
, L# z$ |! z, n% j
代码段 16.3.8 读写AT24C02多字节数据(driver_eeprom.c)5 w2 O! A+ ~ ]1 T9 U
% T, W. h5 v& Z9 F+ ?$ w4 Y2 R
- /*2 H9 W: t0 X- Q& Q% t: F) f
- * 函数名:void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
, |6 N) ~5 t J7 y$ [2 k) H! u# r8 O5 C - * 输入参数:addr -> 写一个字节的EEPROM初始地址
# c; m9 B1 o! }( A4 m! T! v2 w - * data -> 要写的数据指针" T$ X; X( w+ A6 G( l" N8 o2 i: U0 g- i
- * sz -> 要写的字节个数
9 l7 |( D2 C1 W5 F/ c4 u - * 输出参数:无' ]9 E0 `+ [, X; h2 Y& g
- * 返回值:无+ M! d6 e% x/ M2 ?1 k6 d5 Y5 u
- * 函数作用:EEPROM写N个字节1 I! R$ t. H7 `% o3 x" Q0 T$ g
- */
: R6 M7 ^5 @* V1 W" l - void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)8 @0 Q4 `9 n- G- @% m3 D
- {
$ [1 c% T+ b6 B+ s! Z4 z4 n% q- |' j6 J6 A - uint16_t i = 0;
( i. }3 t# W( H0 T7 C1 o1 i; o! q - 6 y4 z' Z* R7 `
- for(i=0; i<sz; i++)/ c) v& x% O2 d! b8 N& M
- {
2 @% B! ^: ?' M) R( M - EEPROM_WriteByte(addr, pdata<i>);$ D0 v; x# T9 I5 R( c' Y
- addr++; z/ }1 f. B' V& V& p
- HAL_Delay(10); // Write Cycle Time 5ms+ j5 r3 i0 d/ E# }5 j) W* H# p( e
- }
* D8 B1 X5 Q0 C2 G3 p3 J - }
# R& [0 ]) }! Z9 H% R0 \- S - : o* I0 }2 |/ x/ N" A( e
- /*# _7 q3 \- o: w
- * 函数名:void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)4 Y5 V/ G5 I, d1 k
- * 输入参数:addr -> 读一个字节的EEPROM初始地址8 J# r% X J* V+ i/ j) @/ o6 O' n
- * data -> 要读的数据指针7 _- P) U7 G4 A# l* A: ~2 S' v8 C6 f, K
- * sz -> 要读的字节个数& t3 T) w* w. c9 T; z/ d
- * 输出参数:无
( `3 v9 U( a5 C - * 返回值:无: g+ i, u: E8 P
- * 函数作用:EEPROM读N个字节
6 I2 W# s9 L- C# s+ ^& T8 s' t - */
. X7 Y0 ]. ~8 l& I( e2 c2 B - void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)! n1 W% i, } ?( I# c/ Q
- { D3 A( ]) r8 {1 E p: C3 W3 y
- uint16_t i = 0;( O" E5 @& v5 P2 y! \5 o
. J$ Z0 a8 Z- ^2 L- for(i=0; i<sz; i++)
( y! L5 E# K# b/ M1 F - {
1 c: e' Z1 O3 G& a7 Y5 R - EEPROM_ReadByte(addr, &pdata<i>);- {; f) Z9 P1 E8 U ^
- addr++;
G1 |5 A+ v! e/ v - }
& x0 b+ c2 O5 Q( E+ ?4 Z y8 } - }</i></i>
复制代码 0 ]; ?, P* [3 j
需要注意的是,AT24Cxx每次写操作后,有一个写间隔,需要间隔5ms以上,因此在写多个字节时,每次写完都需要延时5ms以上。0 ]$ N5 f' O: g3 Q$ u% C N: A
8 O# i9 d% D: c7 y" d$ @
* M8 e9 [9 e# F. d! ~9 ?& A4) 主函数控制逻辑
& w# @" }& b0 g9 b' z, l8 P$ s" t. j) E- f) s# I
在主函数里,每按一下按键,调用“EEPROM_Write_Nbytes()”对AT24C02写一串数据,再调用“EEPROM_Read_Nbytes()”读出该数据,如代码段 16.3.9 所示。7 d0 _7 [0 @) m5 z0 c" a
9 J' ?2 K5 D% O4 }' U$ G6 j6 P1 a代码段 16.3.9 主函数控制逻辑(main.c), K R& ?( F! Y9 P @% y2 ^* m! G0 e0 t
4 Y6 t: t B* E
- // 初始化I2C' D% r3 @) ]1 p- q- `6 M# s. W. V G/ _
- I2C_Init();0 u5 @' N- A6 c- o/ Q; ~
- , A7 F7 w' W; @: u. l( \
- while(1): N7 ]% N# B9 q! \. r% ~ x+ x
- {
: d0 X. u2 z9 _, z; g$ [ - if(key_flag) // 按键按下
0 L4 ~/ {8 u7 J1 d. i - {
" J& ^7 K* k' {) r8 b - key_flag = 0;
* T6 L, m- W' ^) g - 4 T9 w2 r! Q$ y$ ?, c ?
- printf("\n\r");, K% }3 ]* J" x
- printf("Start write and read eeprom.\n\r");/ ]9 {* o D- \
- 4 }6 `0 c5 F5 S+ S
- // 读写一串字符,并打印4 N. }' r# G! M( A
- EEPROM_Write_NBytes(0, tx_buffer, sizeof(tx_buffer)); // 写数据% D( f ^* C+ Y# s3 a, ?
- HAL_Delay(1);
# G) m( K. a2 u6 ?5 ^ - : b; @2 p5 v, m8 O
- EEPROM_Read_NBytes(0, rx_buffer, sizeof(tx_buffer)); // 读数据# T/ S7 Q/ o3 W$ G1 L+ V1 u8 d
- HAL_Delay(1);0 g6 H( h1 {9 `8 F S
1 q s2 ~. x6 I5 l- printf("EEPROM Write: %s\n\r", tx_buffer);6 L; X4 x* c1 l6 S
- printf("EEPROM Read : %s\n\r", rx_buffer);
# e# U8 T* M. N - + Y4 }% l6 t& h0 W: K2 ~% P
- memset((uint8_t*)rx_buffer, 0, sizeof(rx_buffer)); // 清空接收的数据) v. d' ?( f. i! @3 E; I2 Z: F4 O
- }
1 s, f) e0 N, z+ { - }
复制代码 5 {2 \7 O7 i! v' W! R. V' i
16.4 实验效果2 [5 F& g. y) R( n& n9 m0 p; W3 v
本实验对应配套资料的“5_程序源码\8_通信—模拟I2C\”。打开工程后,编译,下载,按下按键KEY,即可看到串口如图 16.4.1 所示。5 l2 J) N; g' o; V
# n2 A2 z6 z& D( a
5 A' G5 V$ p6 [' `8 l+ e0 a7 B: ?! Q2 e; U
图 16.4.1 模拟I2C读写AT24C02数据 , r& G+ a4 ~' [8 x6 S$ Q" s8 |
* F. g7 s) S# Y/ x( u0 o
7 ?1 d- o/ S; k1 X2 h6 U. v |