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