51单片机&15单片机 时钟芯片DS1302

简介: 51单片机&15单片机 时钟芯片DS1302

前言

现在流行的串行时钟电路很多,如DS1302、 DS1307、PCF8485等。这些电路的接口简单、价格低廉、使用方便,被广泛地采用。

  • 本文介绍的实时时钟电路DS1302是DALLAS公司的一种具有涓细电流充电能力的电路
  • 主要特点是采用串行数据传输,可为掉电保护电源提供可编程的充电功能,并且可以关闭充电功能。采用普通32.768kHz晶振。

d8a9384cc1cf5c91ab4e86376b3a5984_ce723adf9f63448d9e6941a30793a1ab.jpeg


一、DS18B20芯片介绍

DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。

RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片。

特征

b33a10a38bbfd1dce7e12ffca3d849b1_f1577dd2c8eb4f09b93b01576124ad95.png

DS1302模块:

6c79881af38f464a43432651b866a04d_f0eeb660eec04975b0913034a48a86d0.png

c4fd9b62c8d8de74ca1f187817799919_6952f52ef56946c2a54fb7824905b6a7.png


二、芯片引脚定义与应用电路

f2be4f186b103037a3c19e1712793764_b12ae655145744e19ecca2ff227556eb.png

晶振给芯片提供一个时钟源

典型工作电路

731d30395bbdac9eaa820c9877cd386f_f54182bf10d04bf3a7102f455e23e9b3.png

51单片机DS1302电路图

83965654662a35e1627a5b2c92ca5bf2_9db9c3d9670a426983fe7d47be7c7474.png

蓝桥杯15单片机开发板实际电路

136b16386ca448b965686e114c069ce4_1e3fb75834f44221b18f8af0cdf9adef.png

注意51单片机与15单片机使用的I/O口是不同的

该开发板中没有接备用电池,因此掉电再上电之后是初始化设置的时间。

电池的DS1302模块可以存储时间

引脚名 作用
VCC2 主电源
VCC1 备用电池
GND 电源接地
X1,X2 32.768kHz晶振
CE 芯片使能
IO 数据输入/输出
SCLK 串行时钟


三、寄存器定义

DS1302相关寄存器如下:

7dd24c4f272925977d160a0e3c8b566f_bdd8035c34a7479ca4e86b2337fbee96.png

  • 第一行的 CH(BIT7)表示时钟暂停控制位,这一位置1代表时钟暂停,置0时钟正常运行
  • 倒数第二行的 WP(BIT7),这一位置1代表芯片写保护,写入操作无效,置0即可解除芯片写保护,就可以写入数据
  • 12/24 第四行  BIT7   =0,是24小时模式
  • 如果BIT7 = 1 是12小时模式  BIT5 = 0是AM, BIT5 = 1是PM

DS1302有12个寄存器,其中有7个寄存器与日历、时钟相关,存放的数据位为BCD码形式,其日历、时间寄存器如上图所示。

此外,DS1302 还有年份寄存器、控制寄存器、充电寄存器、时钟突发寄存器及与RAM相关的寄存器等。时钟突发寄存器可一次性顺序读写除充电寄存器外的所有寄存器内容。 DS1302与RAM相关的寄存器分为两类:一类是单个RAM单元,共31个,每个单元组态为一个8位的字节,其命令控制字为C0H~FDH,其中奇数为读操作,偶数为写操作;另一类为突发方式下的RAM寄存器,此方式下可一次性读写所有的RAM的31个字节,命令控制字为FEH(写)、FFH(读)。


四、命令字

2fb5dfc1ae93f2d199321094a3fe3717_4152fe3cefce42f3bbb1d39e514fcf20.png

  • 第0位表示读还是写(置1表示读,置0表示写);
  • 第1-5位表示地址(秒地址为0,分钟地址为10,…);
  • 第6位表示操作RAM还是时钟CK(置1表示操作RAM,置0表示操作CK);
  • 第7位:固定为1;

举个例子:

对时钟操作、秒地址操作、读:

10000001(0x81)

对时钟操作、秒地址操作、写:

10000000(0x80)

这与上面寄存器中的读地址和写地址是对应的。


五、数据流

在控制指令字输入后的下一个SCLK时钟的上升沿时,数据被写入DS1302,数据输入从低位即位0开始。同样,在紧跟8位的控制指令字后的下一个SCLK脉冲的下降沿读出DS1302的数据,读出数据时从低位0位到高位7。


六、时序图与数据读写

读写时序图:

05c236df4eb6fc59520369a10b098bba_34153039166243f39e1771b2fcee33d6.png

从时序图中也能看出DS1302使用了三个引脚:CE(使能端)、SCLK(时钟)、I/O(数据)

规定:在时钟的上升沿,I/O口的数据将会被写入,在时钟的下降沿,时钟芯片的数据将会被读出。

初始化

1. //上电默认1,先初始化为0
2. void DS1302_Init(void)
3. {   
4.  DS1302_CE    = 0; //
5.  DS1302_SCLK  = 0;
6. }

6.1 单字节写(Write)

5ff3a56011892eab56827d0e5d850153_60c2f45176d64f128a2b70601e6594b1.png

从时序图中可以看到,要发两个字节:第一个字节是命令字(先发最低位R/W),第二个字节是数据;【一位一位依次发送】,一共发送16位数据,产生16个脉冲

单字节写数据步骤

将CE置1;

将命令字最低位放在I/O线上,等待SCLK的上升沿写入单片机

SCLK给上升沿,发送最低位

SCLK给下降沿

将命令字次低位放在I/O线上

SCLK给上升沿,发送次低位

SCLK给下降沿

…(依次循环8次,将命令字发送出去)

将要写入的数据最低位放在I/O线上,等待SCLK的上升沿写入单片机

SCLK给上升沿,发送最低位

SCLK给下降沿

将数据次低位放在I/O线上

SCLK给上升沿,发送次低位

SCLK给下降沿

…(依次循环8次,将数据发送出去)

将CE置0;

示例代码:

1. //单字节写(Command命令字(地址),Data数据)
2. void DS1302_WriteByte(unsigned char Command, unsigned char Data)
3. {
4.  unsigned char i;
5.  DS1302_CE = 1;  
6.  for (i = 0; i < 8; i ++)  //写命令字
7.  {
8.    DS1302_IO = Command & (0x01<<i);//将命令字放在I/O线上,等待写入
9.    DS1302_SCLK = 1;  //上升沿写入
10.     DS1302_SCLK = 0;  // (准备下一次写入)
11.   }
12.   for (i = 0; i < 8; i ++)  //写数据
13.   {
14.     DS1302_IO = Data & (0x01<<i);//将数据放在I/O线上,等待写入
15.     DS1302_SCLK = 1;  //上升沿写入
16.     DS1302_SCLK = 0;
17.   } 
18.   DS1302_CE = 0;  
19. }

从低位向高位写数据,嵌套一个for循环,用Command & (0x01<

6.2 单字节读(Read)

a5e960fcade2afbf84ea23668e54d995_1ea6edc8486f469c950398aeeafb6d6d.png

注意:从上述时序图中可以看出,单字节写(Write)有16个脉冲,而单字节读(Read)只有15个脉冲,因为当最后一个命令字的上升沿之后的下降沿数据马上就读出来了,如下图所示。

ca71622bcc6904f5832c01e4ac54d9ea_fd5a246fefc54fb48a7bbe0f69f0ad21.png

读时序的SCLK有15个脉冲,相比单字节写少了一个脉冲,写代码要注意

如果写命令字和之前一样,错误示例代码如下

1. for (i = 0; i < 8; i ++) //写命令字
2.  {
3.    DS1302_IO = Command & (0x01<<i);
4.    DS1302_SCLK = 1;
5.    DS1302_SCLK = 0;
6. 
7.  }

写完8位数据,要读取的第一位数据已经出来了,这样写是有问题的

我们需要的时序是这样的:先给0再给1(蓝线),这样写完命令字就到了图示位置(红线)

24fda46c865afa39f1933895d227350e_e895aef937a246be86c52afff4bcc80a.png

后面读取的时候,从红线位置开始,先给1再给0(蓝线),循环8次就能全部读取出来

769f0aa335b7fff11bd20819fb14b380_7d6e6432008049ebaf53a074bfeb1dd0.png

单字节读取数据步骤

将CE置1;

将命令字最低位放在I/O线上,等待SCLK的上升沿写入单片机

先SCLK给下降沿

SCLK给上升沿,发送最低位

将命令字次低位放在I/O线上

SCLK给下降沿

SCLK给上升沿,发送次低位

…(依次循环8次,将命令字发送出去)

SCLK给上升沿,保持高电平,保证时序正确

SCLK给下降沿

此时下降沿读取I/O口数据

SCLK给上升沿,发送次低位

SCLK给下降沿

下降沿读取I/O口数据

…(依次循环8次,将数据读取出来)

将CE置0;

最后要讲I/O也置0

读取数据的操作:如果检测到I/O口是1,就用或运算,取出1,如果读出0,就不操作(默认当前位是0,进入下一次for循环)

示例代码:

1. //单字节读,用上面的宏定义即可
2. unsigned char DS1302_ReadByte(unsigned char Command)
3. {
4.  unsigned char Data = 0x00;
5.  unsigned char i;
6.  Command |= 0x01;//变成读的地址
7. 
8.  DS1302_CE = 1;  
9.  for (i = 0; i < 8; i ++)  //写命令字
10.   {
11.     DS1302_IO = Command & (0x01<<i);    
12.     DS1302_SCLK = 0;  //先给0后给1,因为时序图上少一个脉冲
13.     DS1302_SCLK = 1
14.   }
15.   for (i = 0; i < 8; i ++)
16.   {
17.     DS1302_SCLK = 1;  //先给1后给0,模拟时序图
18.     DS1302_SCLK = 0;
19.     if(DS1302_IO == 1)  //读取端口是1
20.     { Data |= (0x01<<i); }  //读数据操作
21.   }
22.   DS1302_CE = 0;  
23.   DS1302_IO = 0;  //修改的地方
24.   return Data;  
25. }

Command |= 0x01;//变成读的地址

这句代码是啥意思,别急,看宏定义

宏定义

67006bae25f067ee4a1f1e514c406d21_127ffe9f7fa24c6fa8f964f3837fd0d7.png

读数据比写数据的命令码大1

0x80 | 0x01 = 0x81

0x82 | 0x01 = 0x83

将写数据的命令码与0x01进行或运算,即得到读数据的命令码

所以我们将写数据的命令码用宏定义封装一下

1. #define DS1302_SECOND   0x80 //秒
2. #define DS1302_MINTUE   0x82 //分钟
3. #define DS1302_HOUR     0x84 //小时
4. #define DS1302_DATE     0x86 //日
5. #define DS1302_MONTH    0x88 //月
6. #define DS1302_DAY      0x8A //星期
7. #define DS1302_YEAR     0x8C //年
8. #define DS1302_WP       0x8E //写保护的

Command(写) | 0x01 = Command(读)

让写的地址 |= 0x01,就是读数据的地址,减少一半的宏定义!!!


实际测试一下

秒寄存器写个3,然后读取看结果

1. #include <STC15F2K60S2.H>
2. #include "smg.h"
3. #include "DS1302.h"
4. 
5. unsigned char Sec;
6. 
7. void main()
8. {
9.  All_Init();
10.   DS1302_Init();
11. 
12.   DS1302_WriteByte(0x80,0x03);
13. 
14.   while(1)
15.   {
16.     Sec = DS1302_ReadByte(0x81);
17.     Nixie(1, Sec / 10);
18.     Nixie(2, Sec % 10);
19.   }
20. }

效果

3 4 5 6 7 8 9之后变成16了

原因?

上面提到过BCD码,放在这提一嘴

  BCD码  

bee1dc6534da7598aae21dcf02fd63f2_7583143489cc4c70881e2cbcb81ef956.png

十进制 十六进制 BCD码
9 0000 1001 09
16 0001 0000 10

介绍BCD码与十进制互相转换的文章:

【C语言】BCD码、十进制互相转换

明白了BCD码,就可以对DS1302模块读写时间进行封装了

年 月 日 时 分 秒 星期

数据比较多,用数组存放各个数据

1. unsigned char DS1302_Time[9]={23,2,11,23,59,55,4};//年 月 日 时 分 秒 星期
2. 
3. void DS1302_SetTime(void)
4. {
5.  DS1302_WriteByte(DS1302_WP, 0x00);//解除芯片写保护
6.  DS1302_WriteByte(DS1302_YEAR,   DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码
7.  DS1302_WriteByte(DS1302_MONTH,  DS1302_Time[1]/10*16+DS1302_Time[1]%10);  
8.  DS1302_WriteByte(DS1302_DATE,   DS1302_Time[2]/10*16+DS1302_Time[2]%10);
9.  DS1302_WriteByte(DS1302_HOUR,   DS1302_Time[3]/10*16+DS1302_Time[3]%10);
10.   DS1302_WriteByte(DS1302_MINTUE, DS1302_Time[4]/10*16+DS1302_Time[4]%10);
11.   DS1302_WriteByte(DS1302_SECOND, DS1302_Time[5]/10*16+DS1302_Time[5]%10);
12.   DS1302_WriteByte(DS1302_DAY,    DS1302_Time[6]/10*16+DS1302_Time[6]%10);  //星期
13.   DS1302_WriteByte(DS1302_WP, 0x80);//此句WP给0x80,打开芯片写保护
14. }
15. 
16. void DS1302_ReadTime(void) //此函数读取时钟芯片的数据,写到数组DS1302_Time里
17. {
18.   unsigned char Temp;
19.   Temp = DS1302_ReadByte(DS1302_YEAR);//进去就被 或运算 置1,变成读,读BCD码
20.   DS1302_Time[0] = Temp/16*10+Temp%16;  
21.   Temp = DS1302_ReadByte(DS1302_MONTH);//月
22.   DS1302_Time[1] = Temp/16*10+Temp%16;
23.   Temp = DS1302_ReadByte(DS1302_DATE);
24.   DS1302_Time[2] = Temp/16*10+Temp%16;
25.   Temp = DS1302_ReadByte(DS1302_HOUR);
26.   DS1302_Time[3] = Temp/16*10+Temp%16;
27.   Temp = DS1302_ReadByte(DS1302_MINTUE);
28.   DS1302_Time[4] = Temp/16*10+Temp%16;
29.   Temp = DS1302_ReadByte(DS1302_SECOND);
30.   DS1302_Time[5] = Temp/16*10+Temp%16;
31.   Temp = DS1302_ReadByte(DS1302_DAY);
32.   DS1302_Time[6] = Temp/16*10+Temp%16;
33. }

6.3 DS1302时钟实现部分

给DS1302模块也封装起来

DS1302.c

1. #include "DS1302.h"
2. 
3. //对端口重新定义,位变量,只进行逻辑判断,非零即真
4. sbit DS1302_CE   = P1^3;
5. sbit DS1302_IO   = P2^3;
6. sbit DS1302_SCLK = P1^7;
7. 
8. #define DS1302_SECOND   0x80 //秒
9. #define DS1302_MINTUE   0x82 //分钟
10. #define DS1302_HOUR     0x84  //小时
11. #define DS1302_DATE     0x86  //日
12. #define DS1302_MONTH    0x88  //月
13. #define DS1302_DAY      0x8A  //星期
14. #define DS1302_YEAR     0x8C  //年
15. #define DS1302_WP       0x8E  //写保护的
16. 
17. unsigned char DS1302_Time[9]={23,2,9,23,59,55,4};//年 月 日 时 分 秒 星期
18. 
19. //上电默认1,先初始化为0
20. void DS1302_Init(void)
21. {   
22.   DS1302_CE    = 0; //
23.   DS1302_SCLK  = 0;
24. }
25. 
26. //单字节写(命令字,数据)
27. void DS1302_WriteByte(unsigned char Command, unsigned char Data)
28. {
29.   unsigned char i;
30.   DS1302_CE = 1;  
31.   for (i = 0; i < 8; i ++)  //写命令字
32.   {
33.     DS1302_IO = Command & (0x01<<i);
34.     DS1302_SCLK = 1;
35.     DS1302_SCLK = 0;
36.   }
37.   for (i = 0; i < 8; i ++)  //写数据
38.   {
39.     DS1302_IO = Data & (0x01<<i);
40.     DS1302_SCLK = 1;
41.     DS1302_SCLK = 0;
42.   } 
43.   DS1302_CE = 0;  
44. }
45. 
46. //单字节读,用上面的宏定义即可
47. unsigned char DS1302_ReadByte(unsigned char Command)
48. {
49.   unsigned char Data = 0x00;
50.   unsigned char i;
51.   Command |= 0x01;//变成读的地址
52. 
53.   DS1302_CE = 1;  
54.   for (i = 0; i < 8; i ++)  //写命令字
55.   {
56.     DS1302_IO = Command & (0x01<<i);
57.     DS1302_SCLK = 0;  //先给0后给1,因为时序图上少一个脉冲
58.     DS1302_SCLK = 1;
59.   }
60.   for (i = 0; i < 8; i ++)
61.   {
62.     DS1302_SCLK = 1;  //先给1后给0,模拟时序图
63.     DS1302_SCLK = 0;
64.     if(DS1302_IO == 1)  //读取端口是1
65.     { Data |= (0x01<<i); }  //读数据操作
66.   }
67.   DS1302_CE = 0;  
68.   DS1302_IO = 0;  //修改的地方
69.   return Data;  
70. }
71. 
72. void DS1302_SetTime(void)
73. {
74.   DS1302_WriteByte(DS1302_WP, 0x00);//解除芯片写保护
75.   DS1302_WriteByte(DS1302_YEAR,   DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码
76.   DS1302_WriteByte(DS1302_MONTH,  DS1302_Time[1]/10*16+DS1302_Time[1]%10);  
77.   DS1302_WriteByte(DS1302_DATE,   DS1302_Time[2]/10*16+DS1302_Time[2]%10);
78.   DS1302_WriteByte(DS1302_HOUR,   DS1302_Time[3]/10*16+DS1302_Time[3]%10);
79.   DS1302_WriteByte(DS1302_MINTUE, DS1302_Time[4]/10*16+DS1302_Time[4]%10);
80.   DS1302_WriteByte(DS1302_SECOND, DS1302_Time[5]/10*16+DS1302_Time[5]%10);
81.   DS1302_WriteByte(DS1302_DAY,    DS1302_Time[6]/10*16+DS1302_Time[6]%10);  //星期
82.   DS1302_WriteByte(DS1302_WP, 0x80);//此句WP给0x80,打开芯片写保护
83. }
84. 
85. void DS1302_ReadTime(void) //此函数读取时钟芯片的数据,写到数组DS1302_Time里
86. {
87.   unsigned char Temp;
88.   Temp = DS1302_ReadByte(DS1302_YEAR);//进去就被 或运算 置1,变成读,读BCD码
89.   DS1302_Time[0] = Temp/16*10+Temp%16;  
90.   Temp = DS1302_ReadByte(DS1302_MONTH);//月
91.   DS1302_Time[1] = Temp/16*10+Temp%16;
92.   Temp = DS1302_ReadByte(DS1302_DATE);
93.   DS1302_Time[2] = Temp/16*10+Temp%16;
94.   Temp = DS1302_ReadByte(DS1302_HOUR);
95.   DS1302_Time[3] = Temp/16*10+Temp%16;
96.   Temp = DS1302_ReadByte(DS1302_MINTUE);
97.   DS1302_Time[4] = Temp/16*10+Temp%16;
98.   Temp = DS1302_ReadByte(DS1302_SECOND);
99.   DS1302_Time[5] = Temp/16*10+Temp%16;
100.  Temp = DS1302_ReadByte(DS1302_DAY);
101.  DS1302_Time[6] = Temp/16*10+Temp%16;
102. }

DS1302.h

1. #ifndef __DS1302_H__
2. #define __DS1302_H__
3. 
4. #include <STC15F2K60S2.H>
5. 
6. extern unsigned char DS1302_Time[9];//年月日时分秒星期
7.  //外部变量必须加extern,外部数组或函数可以不加extern
8. 
9. void DS1302_Init(void);
10. void DS1302_WriteByte(unsigned char Command, unsigned char Data);
11. unsigned char DS1302_ReadByte(unsigned char Command);
12. 
13. void DS1302_SetTime(void);
14. void DS1302_ReadTime(void);
15. 
16. #endif

main.c

用数码管和独立按键做个界面显示,默认显示时间界面,按键7也显示时间界面,按键6显示年月日界面,按键5显示星期界面

1. #include <STC15F2K60S2.H>
2. #include "smg.h"
3. #include "Key.h"
4. #include "Delay.h"
5. #include "DS1302.h"
6. 
7. unsigned char Keynum;
8. unsigned char Show_Flag = 0;
9. void main()
10. {
11.   All_Init();
12.   DS1302_Init();
13. 
14.   DS1302_SetTime();
15. 
16.   while(1)
17.   {
18.     Keynum = Duli_Anjian();
19.     DS1302_ReadTime();
20.     if    (Keynum == 7) Show_Flag = 0;  //按键7
21.     else if (Keynum == 6) Show_Flag = 1;  //按键6
22.     else if (Keynum == 5) Show_Flag = 2;
23. 
24.     if (0 == Show_Flag)//按键7 或默认状态
25.     {
26.       Nixie(1, DS1302_Time[3]/10);  //时
27.       Nixie(2, DS1302_Time[3]%10);
28.       Nixie(3, 16); //显示 -
29.       Nixie(4, DS1302_Time[4]/10);  //分
30.       Nixie(5, DS1302_Time[4]%10);
31.       Nixie(6, 16); //显示 -
32.       Nixie(7, DS1302_Time[5]/10);  //秒
33.       Nixie(8, DS1302_Time[5]%10);
34.     }
35.     else if (1 == Show_Flag)//按键6
36.     {
37.       Nixie(1, DS1302_Time[0]/10);  //年
38.       Nixie(2, DS1302_Time[0]%10);
39.       Nixie(3, 16); //显示 -
40.       Nixie(4, DS1302_Time[1]/10);  //月
41.       Nixie(5, DS1302_Time[1]%10);
42.       Nixie(6, 16); //显示 -
43.       Nixie(7, DS1302_Time[2]/10);  //日
44.       Nixie(8, DS1302_Time[2]%10);
45.     }
46.     else if (2 == Show_Flag)
47.     {
48.       Nixie(1, DS1302_Time[6]);
49.       Nixie(2,17);  //熄灭码
50.       Nixie(3,17);  
51.       Nixie(4,17);
52.       Nixie(5,17);
53.       Nixie(6,17);
54.       Nixie(7,17);
55.       Nixie(8,17);
56.     }
57. 
58.   }
59. }

界面显示的亮度差强人意,还是用定时器刷新数码管比较好

613e0b68c6fc3e27ea645db23781028c_446989099a824f52a7d5517af07a1bfc.jpeg


参考资料:

DS1302详解

51单片机DS1302实时时钟

单片机常用芯片总结(二)——DS1302时钟芯片

B站视频(链接打不开)名称如下:

江科大自化协-51单片机入门教程-2020版 程序全程纯手打 从零开始入门【10-1】【10-2】


相关文章
|
6月前
|
监控
单片机的时钟系统
单片机的时钟系统
75 1
|
6月前
|
C语言
基于单片机的简易电子时钟
基于单片机的简易电子时钟
94 0
|
1月前
基于51单片机的proteus数字时钟仿真设计
基于51单片机的proteus数字时钟仿真设计
108 1
|
存储 芯片
51单片机--DS1302时钟
51单片机--DS1302时钟
127 0
|
6月前
|
芯片 开发者
单片机中时钟分析与快速读懂时序图的方法
单片机中时钟分析与快速读懂时序图的方法
167 0
|
监控 芯片
单片机如何才能不死机之内外部时钟
单片机如何才能不死机之内外部时钟
|
测试技术 C语言 芯片
基于51单片机的自动打铃打鸣作息报时系统AT89C51数码管三极管时钟电路
基于51单片机的自动打铃打鸣作息报时系统AT89C51数码管三极管时钟电路
248 0
基于单片机的多功能数字时钟设计
基于单片机的多功能数字时钟设计
228 1
基于单片机的多功能数字时钟设计
蓝桥杯之单片机学习(十七)——DS1302的基本应用
蓝桥杯之单片机学习(十七)——DS1302的基本应用
498 0
蓝桥杯之单片机学习(十七)——DS1302的基本应用
|
传感器 编解码 芯片
51单片机驱动步进电机——使用ULN2003芯片
51单片机驱动步进电机——使用ULN2003芯片
372 0
51单片机驱动步进电机——使用ULN2003芯片