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