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