SPI总线是微处理器和外设之间进行数据交互的常用串行总线接口。本文主要介绍了SPI的接口类型、数据传输(时钟极性、时钟相位)、读、写操作的具体代码,最后是一个完整的xpt2046实现ADC的代码实现及注意事项。
串行外设接口(SPI)是微控制器(MCU)和外围IC(如传感器、ADC、DAC、移位寄存器、SRAM等)之间使用较广泛的接口之一。SPI是一种同步、全双工、主从式接口。来自主机或者从机的数据在时钟上升沿或下降沿同步。主机和从机可以同时传输数据。SPI接口可以是三线式或者4线式。本文主要介绍4线式SPI接口。
图1 SPI总线:单一主机对单一从机模式
4线SPI总线有四个信号:
时钟(SPI CLK,SCLK)。
片选(CS)。
主机输出、从机输入(MOSI)。
主机输入、从机输出(MISO)。
产生时钟信号的器件称为主机(MCU)。主机和从机之间传输的数据与主机产生的时钟同步。SPI接口只能有一个主机,但可以有多个从机。
图2 SPI总线:单一主机对多从机模式(2个从机)
来自主机的片选信号用于选择从机。这通常是一个低电平有效的信号,拉高时,从机与主机断开连接。当使用多个从机时,主机需要为每个从机提供单独的片选信号。MOSI和MISO是数据线。MOSI将数据从主机发送到从机,MISO将数据从从机发送到主机,实现主机和从机的数据交互。
要开始SPI通信,主机通过使能CS信号选择从机,发送时钟信号。片选(CS)一般是低电平有效信号。因此,主机必须在该信号上发送逻辑“0”以选择从机。SPI是全双工通信接口,主机和从机可以分别通过MOSI和MISO线路发送和接收数据(数据交互)。在SPI通信期间,数据的发送(串行移出到MOSI/MISO)和接收(MISO/MOSI)可以同时进行。串行时钟沿同步数据的移位和采样。SPI接口允许用户灵活选择上升沿或者下降沿采样(读操作,输入)或者移位(写操作,输出)。读、写操作字节都是从最高位(MSB)开始的。
图3 SPI通信读、写的bit顺序
在SPI通信接口中,主机可以选择时钟极性和时钟相位。空闲状态就是片选(CS)无效时的状态。在空闲状态期间,CPOL位设置时钟信号(SCLK)的极性。
表1 CPOL极性规定
CPHA位选择时钟相位,规定主机采样数据时的时钟边沿,包括第一个跳变沿和第二个跳变沿。
表2 CPHA极性规定
根据CPOL位和CPHA位的不同组合,有四种SPI模式可用。
表3 CPOL位和CPHA位的不同组合条件下的SPI模式
图4 CPOL和CPHA时序关系
主机在与采用SPI通信接口的从机(ADC(xpt2046))数据交互过程中,要严格按照从机的时序要求进行读写操作,且从机的SPI通信时序变体很多,具体有从机采用只读模式、从机采用只写模式、先写后读模式、数据位数不同等配置非常灵活。并不是只有图4中的模式类别,具体读/写操作要参考从机的数据手册。
void send_byte(dat) //主机发送数据到从机 写操作
//数据传输顺序:MSB→LSB
{
unsigned char i;
cs_n = 0;
for(i=0;i<8;i++)
{
sclk = 0;
if(dat & 0x80)
{
mosi = 1;
}
else
{
mosi = 0;
}
sclk = 1;//上升沿锁存数据
dat = (dat << 1);
}
sclk = 0;//拉低dclk,方便后续操作
}
unsigned char receive_byte()
{
unsigned char dat = 0x00;
unsigned char shifter = 0x80;
unsigned char i;
cs_n = 0;
sclk = 0;
for(i=0;i<8;i++)
{
sclk = 1;
sclk = 0;//下降沿dout数据移出 miso从机发送数据到主机
if(miso)
{
dat = (dat | shifter);
}
else
{
dat = dat;
}
shifter = (shifter >> 1);
}
sclk = 0;//拉低dclk,方便后续操作。
cs_n = 1;//结束主机、从机数据交互过程。
return dat;
}
图5 ADC(XPT2046 Vbat)时序(12bit)
控制字节的具体含义见xpt2046数据手册。
代码实现:
1 /*
2 程序功能:A/D转换 采集电位器的电压值。
3 state:success!!
4
5 */
6 #include
7 //led灯
8 sbit led_10 = P1^0;
9 sbit led_11 = P1^1;
10 sbit led_12 = P1^2;
11 sbit led_13 = P1^3;
12 sbit led_14 = P1^4;
13 sbit led_15 = P1^5;
14 sbit led_16 = P1^6;
15 sbit led_17 = P1^7;
16 //A/D
17 sbit dclk = P2^1;
18 sbit xpt_in = P2^0;
19 sbit xpt_out = P2^5;
20 sbit xpt_cs_n = P3^7; //低电平有效 _n表示低电平有效
21 //数码管显示
22 //位选
23 sbit wei_enable = P2^7; //与锁存器配合使用
24 //个位→0x7f;十位→0xbf;百位→0xdf;千位→0xef;万位→0xf7;十万位→0xfb;百万位→0xfd;千万位→0xfe;
25 sbit duan_enable = P2^6; //与锁存器配合使用
26 //0x3f→0;0x06→1;0x5b→2;0x4f→3;0x66→4;0x6d→5;0x7d→6;0x07→7;0x7f→8;0x6f→9;
27 //0x77→A;0x7c→B;0x39→C;0x5e→D;0x79→E;0x71→F;0x76→H;0x38→L;0x40→-;0x00→熄灭
28 unsigned char byte_ctrl = 0xa4; //1010_0100 0xa4 配置成低功耗模式,不然ADC结果不准,至于原因,不清楚。
29 unsigned int receive_dat = 0x0000;
30 //自定义函数声明:
31 //0.delay
32 void delay_us(unsigned int z);
33 //1.send_byte_ctrl
34 void send_byte_ctrl(dat);
35 //2.receive_data
36 unsigned int receive_data_AD();
37 //3.led_display_dec_3
38 void led_display_dec_3(unsigned int dat);//小数点后两位
39
40 //main函数
41 void main()
42 {
43
44 //display
45 while(1)
46 {
47 send_byte_ctrl(byte_ctrl);
48 receive_dat = receive_data_AD();
49 led_display_dec_3(receive_dat);
50 led_10 = 0;
51 }
52
53 }
54 //
55 //0.delay
56 void delay_us(unsigned int z)
57 {
58 unsigned int x;
59 for(x=0;x> 1);
111
112 }
113 dclk = 0;//拉低dclk,方便后续操作。
114 for(i=0;i<3;i++)
115 {
116 dclk = 0;
117 dclk = 1;
118 }
119 xpt_cs_n = 1;//结束A/D转换操作。
120 dat = (dat * 8.4); //显示小数点后两位 实测:VCC=4.3,VCC/4=1.075V,对应得数值是1280(十进制),1.075/1280=8.4e-4。
121 return dat;
122 }
123 //
124 //3.led_display_dec_3
125 void led_display_dec_3(unsigned int dat)//dec表示10进制
126 {
127 unsigned char wei_point_2;
128 unsigned char wei_point_1;
129 unsigned char wei_point;
130 unsigned char wei_ge;
131 wei_enable = 0;
132 duan_enable = 0;
133 wei_ge = dat / 10000; //12345 1 个位
134 wei_point_1 = dat % 10000 /1000; //12345 2 小数点后第一位
135 wei_point_2 = dat % 1000 / 100; //12345 3 小数点后第二位
136 //wei_point_2
137 //位选
138 P0 = 0x7f;//个位
139 wei_enable = 1;
140 wei_enable = 0;
141 //段选
142 switch(wei_point_2)
143 {
144 case 0 : { P0 = 0x3f; }break;
145 case 1 : { P0 = 0x06; }break;
146 case 2 : { P0 = 0x5b; }break;
147 case 3 : { P0 = 0x4f; }break;
148 case 4 : { P0 = 0x66; }break;
149 case 5 : { P0 = 0x6d; }break;
150 case 6 : { P0 = 0x7d; }break;
151 case 7 : { P0 = 0x07; }break;
152 case 8 : { P0 = 0x7f; }break;
153 case 9 : { P0 = 0x6f; }break;
154 default: { P0 = 0x3f; }break;
155 }
156 duan_enable = 1;
157 duan_enable = 0;
158 delay_us(300);
159 //wei_point_1
160 //位选
161 P0 = 0xbf; //十位
162 wei_enable = 1;
163 wei_enable = 0;
164 //段选
165 switch(wei_point_1)
166 {
167 case 0 : { P0 = 0x3f; }break;
168 case 1 : { P0 = 0x06; }break;
169 case 2 : { P0 = 0x5b; }break;
170 case 3 : { P0 = 0x4f; }break;
171 case 4 : { P0 = 0x66; }break;
172 case 5 : { P0 = 0x6d; }break;
173 case 6 : { P0 = 0x7d; }break;
174 case 7 : { P0 = 0x07; }break;
175 case 8 : { P0 = 0x7f; }break;
176 case 9 : { P0 = 0x6f; }break;
177 default: { P0 = 0x3f; }break;
178 }
179 duan_enable = 1;
180 duan_enable = 0;
181 delay_us(300);
182 //wei_ge
183 //位选
184 P0 = 0xdf; //百位
185 wei_enable = 1;
186 wei_enable = 0;
187 //段选
188 switch(wei_ge)
189 {
190 case 0 : { P0 = 0xbf; }break;//0xbf
191 case 1 : { P0 = 0x86; }break;//0x86
192 case 2 : { P0 = 0xdb; }break;//0xdb
193 case 3 : { P0 = 0xcf; }break;//0xcf
194 case 4 : { P0 = 0xe6; }break;//0xe6
195 case 5 : { P0 = 0xed; }break;//0xed
196 case 6 : { P0 = 0xfd; }break;//0xfd
197 case 7 : { P0 = 0x87; }break;//0x87
198 case 8 : { P0 = 0xff; }break;//0xff
199 case 9 : { P0 = 0xef; }break;//0xef
200 default: { P0 = 0x3f; }break;
201 }
202 duan_enable = 1;
203 duan_enable = 0;
204 delay_us(300);
205 }
①缺乏数据接收确认机制。
②单主机。
③xpt2046进行ADC时,要配置成低功耗模式(PD1=0,PD0=0),不然ADC结果不准,至于为什么,不清楚。
1.https://aticleworld.com/spi-communication-protocol/
2.https://www.analog.com/cn/analog-dialogue/articles/introduction-to-spi-interface.html
本人水平有限,如有错误,望大佬多多指教。非常感谢。