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