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