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