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