DS1302时钟1.1 简介DS1302是一个低功耗、实时时钟（RTC）芯片，如下图所示：1.2 引脚功能32.768kHz分频15次即频率为1hz，一秒一次引脚号名称方向功能说明1VCC2电源输入主电源（+5V 或 +3.3V），通常是系统电源供给2X1输入32.768kHz 晶振的第一端（接晶振）3X2输出32.768kHz 晶振的第二端4GND电源地线（0V）5CE输入芯片使能信号（Chip Enable，高电平有效），通信时需先拉高6I/O双向数据线（串行输入/输出），命令、数据都通过它传输7SCLK输入串行时钟信号（上升沿或下降沿采样，DS1302在上升沿接收数据）8VCC1电源输入备用电源（接纽扣电池，掉电时维持时钟运行）1.3 基本工作原理基本组成结构工作流程1.4 通信协议​ 根据上述的工作流程可知，使用DS1302时，核心操作就是读写寄存器。1.4.1 写操作​ 以下是向寄存器写入单个字节的时序图。发送数据前，需要将CE拉高，使能通信。需要注意的是，拉高CE时，需确保SCLK处于低电平需要先发送命令字节，再发送数据字节，发送时都是低位优先命令字节用于指定寄存器地址以及声明读/写操作，各位的含义如下数据字节为发送的具体数据，每位数据都需要在一个时钟周期上升沿发送，意味着在上升沿之前要准备数据1.4.2 读操作​ 以下是从寄存器读取单个字节的时序图：接收数据前，需要将CE拉高，使能通信。需要注意的是，拉高CE时，需确保SCLK处于低电平需要先发送命令字节，再接受数据字节，发送和接收时都是低位优先发送每位数据要在时钟信号的上升沿，读取每位数据要在时钟信号的下降沿​ 注意:第8个时钟周期，需要完成最后1位数据的发送和第1位数据的接收1.4.3 常用寄存器​ 以下是各时间寄存器的地址和每位的定义：1）中文版2）英文版#寄存器名称读地址写地址位分布说明范围或含义1秒寄存器81h80hBIT7: CH（1=停止，0=运行）BIT6~4: 十秒位（0–5）BIT3~0: 个位秒（0–9）00–59 秒2分钟寄存器83h82hBIT6~4: 十分钟位（0–5）BIT3~0: 个位分钟（0–9）00–59 分钟3小时寄存器85h84hBIT7: 12/24 小时制（1=12hr，0=24hr）；BIT6: 保留，固定0； BIT5: AM/PM（12hr制下，1=PM，0=AM）；BIT4~0: 小时数值1–12 / 0–23 小时4日期（日）寄存器87h86hBIT5~0: 日期1–315月份寄存器89h88hBIT4~0: 月份1–126星期寄存器8Bh8AhBIT2~0: 星期1–77年寄存器8Dh8ChBIT7~0: 年份（低两位）00–998写保护寄存器（WP）8Fh8EhBIT7: WP（1=禁止写操作，0=允许写）—注意：在向DS1302写入数据前需要先关闭写保护，写完再开启写保护。1.5 相关资料（1）购买链接：https://item.szlcsc.com/15455.html（2）实物图、原理图、封装如何设置和显示时间信息？1.6 硬件原理图1.7 软件设计1.7.1 初始化初始化 DS1302 RTC 芯片 ，上电时先给它一个已知的稳定状态void Int_DS1302_Init(void) { SCLK = 0; // 时钟信号 IO = 0; // 数据 CE = 0; // 使能 } 1.7.2 写数据向 DS1302 写 1 个字节数据static void Int_DS1302_WriteByte(u8 byte) { u8 i; for (i = 0; i < 8; i++) { // 执行 8 次，依次把这个字节的 最低位（LSB）到最高位（MSB） 依序送给 DS1302 IO = byte & 0x01; // 准备数据，取当前字节的最低位 SCLK = 1; SCLK = 0; // 先将 SCLK 时钟线拉高再拉低，产生一个上升沿/下降沿组合 byte >>= 1; // 把 byte 右移一位，这样下一次循环时最低位就是原来的第二位 } }1.7.3 读数据从 DS1302 读取一个字节数据static u8 Int_DS1302_ReadByte() { u8 i, byte = 0; for (i = 0; i < 8; i++) { // 循环 8 次，每次从 DS1302 读 1 位, 读到的第一个位应该放在最低位 byte >>= 1; // 右移一位，为即将读入的新位腾出最高位,第一次无所谓，反正都是0 if (IO == 1) { // 如果是高电平（1），就把当前字节的 最高位 置 1 byte = byte | 0x80; } SCLK = 1; // 产生一个时钟脉冲，通知 DS1302 送出下一位数据 SCLK = 0; } return byte; }1.7.4 按寄存器地址操作定义写寄存器地址，读寄存器地址在写寄存器地址上加1即可#define SECOND 0x80 #define MINUTE 0x82 #define HOUR 0x84 #define DAY 0x86 #define MONTH 0x88 #define DAY_OF_WEEK 0x8A #define YEAR 0x8C #define WP 0x8E 写寄存器函数static void Int_DS1302_SetRegister(u8 addr, u8 byte) { CE = 1; Int_DS1302_WriteByte(addr); // 先发寄存器地址 Int_DS1302_WriteByte(byte); // 再发要写的数据 CE = 0; } 读寄存器函数static u8 Int_DS1302_GetRegister(u8 addr) { u8 byte; CE = 1; Int_DS1302_WriteByte(addr | 1); /// addr | 1 → 把寄存器地址最低位（BIT0）设置为 1，变成 读地址。 byte = Int_DS1302_ReadByte(); // 调用 Int_DS1302_ReadByte() 读取 1 个字节 CE = 0; return byte; } 1.7.5 设置 DS1302 日期时间结构体定义, 用于存放时间日期的普通十进制值typedef struct { u8 second; // 0–59 u8 minute; // 0–59 u8 hour; // 0–23 u8 day; // 1–31 u8 month; // 1–12 u8 year; // 0–99 u8 day_of_week; // 1–7 } Struct_Date; 设置函数void Int_DS1302_SetDate(Struct_Date *p_st_date) { u8 temp; // 关闭写保护 Int_DS1302_SetRegister(WP, 0x00); // 设置秒 temp = (p_st_date->second % 10) | ((p_st_date->second / 10) << 4); // % 10 取个位，/ 10 取十位，<< 4 把十位移到 BCD 的高 4 位 Int_DS1302_SetRegister(SECOND, temp); // 设置分钟 temp = (p_st_date->minute % 10) | ((p_st_date->minute / 10) << 4); Int_DS1302_SetRegister(MINUTE, temp); // 设置小时 temp = (p_st_date->hour % 10) | ((p_st_date->hour / 10) << 4); Int_DS1302_SetRegister(HOUR, temp); // 设置日 temp = (p_st_date->day % 10) | ((p_st_date->day / 10) << 4); Int_DS1302_SetRegister(DAY, temp); // 设置月 temp = (p_st_date->month % 10) | ((p_st_date->month / 10) << 4); Int_DS1302_SetRegister(MONTH, temp); // 设置年 temp = (p_st_date->year % 10) | ((p_st_date->year / 10) << 4); Int_DS1302_SetRegister(YEAR, temp); // 设置星期 Int_DS1302_SetRegister(DAY_OF_WEEK, p_st_date->day_of_week % 10); // 开启写保护 Int_DS1302_SetRegister(WP, 0x80); }1.7.6 读取当前时间/日期void Int_DS1302_GetDate(Struct_Date *p_st_date) { u8 temp; // 获取秒 temp = Int_DS1302_GetRegister(SECOND); p_st_date->second = (temp >> 4) * 10 + (temp & 0x0F); // (temp >> 4) → 取高 4 位（十位数）(temp & 0x0F) → 取低 4 位（个位数）最终得到十进制的实际数值。 temp = Int_DS1302_GetRegister(MINUTE); p_st_date->minute = (temp >> 4) * 10 + (temp & 0x0F); temp = Int_DS1302_GetRegister(HOUR); p_st_date->hour = (temp >> 4) * 10 + (temp & 0x0F); temp = Int_DS1302_GetRegister(DAY); p_st_date->day = (temp >> 4) * 10 + (temp & 0x0F); temp = Int_DS1302_GetRegister(MONTH); p_st_date->month = (temp >> 4) * 10 + (temp & 0x0F); temp = Int_DS1302_GetRegister(YEAR); p_st_date->year = (temp >> 4) * 10 + (temp & 0x0F); temp = Int_DS1302_GetRegister(DAY_OF_WEEK); p_st_date->day_of_week = (temp >> 4) * 10 + (temp & 0x0F); }1.7.7 写入读取测试定义星期格式化数组：// WEEK_NAME[0] → "Mon"，WEEK_NAME[6] → "Sun" code char *WEEK_NAME[] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; u8 xdata str[17]; // 时间字符串缓存 Struct_Date st_date; // st_date 结构体为一个固定的时间（2025 年 8 月 7 日 17:28:30，星期四 st_date.year = 25; st_date.month = 8; st_date.day = 7; st_date.day_of_week = 4; st_date.hour = 17; st_date.minute = 28; st_date.second = 30; // 初始化时钟并设置时间 Int_DS1302_Init(); Int_DS1302_SetDate(&st_date); while (1) { Int_DS1302_GetDate(&st_date); // // 从 DS1302 读取当前时间 // 组合成 "2025/08/07 Thu" 这样的字符串。可以将str显示在oled屏幕上 sprintf(str, "20%02d/%02d/%02d %s", (int)st_date.year, (int)st_date.month, (int)st_date.day, WEEK_NAME[st_date.day_of_week - 1]); Delay_1MS(1000); }
OLED模块-I2C协议OLED模块概述1.1 OLED简介​ OLED（Organic Light-Emitting Diode，有机发光二极管）显示屏由许多微小的发光二极管构成，每个发光二极管就对应着屏幕上的一个像素，通过控制二极管的亮灭就能在屏幕上显示图像或文字。​ 按照驱动方式划分，OLED显示屏分可分为两类，分别是PMOLED（Passive Matrix，被动矩阵）和AMOLED（Active Matrix，主动矩阵）。PMOLED的驱动方式和LED点阵类似，每次只能点亮一行或者一列，需要快速的逐行或者逐列扫描来显示内容。AMOLED屏幕上的每个像素点都有一个单独的控制电路，可以独立进行控制。​ 本文使用的是一块PMOLED（Passive Matrix，被动矩阵）屏，其分辨率为128x64。1.2 驱动芯片​ 市面上的多数OLED显示屏都会带有驱动芯片，驱动芯片的核心作用是控制每个像素点的亮灭以显示图像或文字，此外它还会提供数据接口（例如I2C），用于接收屏幕上要显示的数据。驱动芯片的存在使得我们可以通过简单的接口控制OLED显示屏，大大简化了开发和控制流程。​ 本文所使用的这块OLED屏的驱动芯片为SSD1315，由于我们需要通过它来控制OLED显示的内容，所以需要重点学习它的用法。1.2.1 基本工作原理工作流程1.2.2 IIC接口​ 虽然SSD1315的MCU Interface支持多种通信协议，但我们所选用的OLED显示模块只暴露出了IIC接口，如下图所示。​ 由于OLED显示屏的作用是显示数据，它对于MCU来讲更多的是一个输出设备，因此我们只需关注如何向SSD1315写入数据即可。下面是写数据的时序图：​ 说明：SSD1315的地址，高6位是固定的011110，最后1位SA0可由用户自定义。参考OLED模块手册，可得最后1位的默认值为0R/W#(读写标识)应为0。（增加或移出相应电阻可以更改高低电平实现修改地址）​ 结论：地址 + 读写标识应为01111000(0x78)。说明：MCU向SSD1315发送的数据可分为两类 -- Controlbyte和Data byte。Data byte: 数据字节，其内容可能是Data(显示数据)，也可能是Command(控制命令)。Control byte: 控制字节，在Data byte之前发送，用于声明后续Data byte内容的类型和格式，该字节只有高2位有效，其余6位均为0;D/C#(Data/Command): 用于声明后续的Data byte是Data还是Command，1表示Data，0表示Command。Co(Contination Bit): 用于声明后内容是否连续。0表示连续。此时后边可以连续发送多个Data bvte；1表示不连续，此时后边只能发送一个Data byte。需要注意的是，当Co设置0时，本次通信中后续所有的字节都会被视作Databyte，co位的具体用法如下:1.2.3 GDDRAM​ GDDRAM的作用是存储显示数据，其容量是128×64 bit，每个bit对应着屏幕上的一个像素，该bit的值就能决定该像素的亮灭。1.2.3.1 内存组织结构​ 分为8页，每一页128x8个字节：每一页的结构写一个Data byte：从上往下，而不是从左往右1.2.3.2 内存寻址​ 在了解了内存组织结构之后，我们还需掌握内存的寻址方式，这样才能在我们指定的位置写入指定的内容。​ 向GDDRAM写入数据时是以byte为单位的，按照上述的内存组织结构， 定位一个byte，至少需要明确两点，分别是第几页（Page Address，页地址）和第几列（Column Address，列地址）。因此GDDRAM内部设有两个指针， Page Address Pointer和Column Address Pointer，分别指向当前要写入的页和列。为方便连续写入多个字节，每当成功写入一个字节后，Page Address Pointer或者Column Address Pointer会自动递增，指向下一个要写入的位置。​ 根据指针递增逻辑的不同，GDFRAM提供了三种不同的寻址方式，分别是页寻址，水平寻址和垂直寻址，具体使用哪种模式，可以通过向SSD1315发送控制命令进行选择，具体命令如下:​ 下面逐一介绍三种寻址方式的工作原理。页寻址这种寻址方式下，用户需要先向SSD1315发送控制命令设置页地址（例如Page1）和列地址（例如COL3），此时Page Address Pointer和Column Address Pointer就会指向对应的位置。之后便可开始连续发送数据，每成功写入一个字节，Column Address Pointer都会自动+1，而Page Address Pointer始终不变，如果写到当前页末尾，Column Address Pointer会回到当前页的第1列。设置页地址的命令如下: X三位表示0-7行设置列地址的命令如下: 两个字节拆开分别表示高位和低位，7位可以表示0-127列水平寻址这种寻址方式下，用户需要先向SSD1315发送控制命令，设置一个写入区域，具体来讲就是分别指定页的起始地址和结束地址（例如Page1~Page5）以及列的起始地址和结束地址（例如COL2~COL125）。此时Page Address Pointer和Column Address Pointer会指向该区域的左上角。之后便可开始连续发送数据，每成功写入一个字节，Column Address Pointer会自动+1，当到达目标区域的右侧边界时，Page Address Pointer便会自动+1。当写到目标区域的右下角时，两个指针会再次回到左上角。如下图所示。设置页地址范围的命令如下。如： 00100010 00000001 00000011 为1-3页设置列地址范围的命令如下如：00100001 00000001 00000011 为1-3列垂直寻址这种寻址方式下，用户需要先向SSD1315发送控制命令，设置一个写入区域，具体来讲就是分别指定页的起始地址和结束地址（例如Page1~Page5）以及列的起始地址和结束地址（例如COL2~COL125）。此时Page Address Pointer和Column Address Pointer会指向该区域的左上角。之后便可开始连续发送数据，每成功写入一个字节，Page Address Pointer会自动+1，当到达目标区域的底部边界时，Column Address Pointer便会自动+1。当写到目标区域的右下角时，两个指针会再次回到左上角。如下图所示:1.2.3.3 内存bit与屏幕像素映射关系​ GDDRAM中的每个bit都对应着屏幕上的一个像素，但具体哪个bit对应着哪个像素，目前仍是不明确的，最理想的方式当然是每个bit和每个像素的位置完全对应，但默认情况下并非如此。​ bit和像素之间的对应关系受两个环节的影响，第一个环节是GDDRAM和SSD1315输出引脚（64个COM和128个SEG）的对应关系，第二个环节是SSD1315的输出引脚和OLED屏幕的连线方式。下面逐一分析。GDDRAM和SSD1315输出引脚对应关系，默认映射关系如下：一一对应SSD1315输出引脚和OLED屏幕对应关系，行和列都与GDDRAM和SSD1315相反：为了保证设计的灵活性，SSD1315提供了重映射功能，用户可以通过控制命令来重新定义GDDRAM和输出引脚的对应关系，如下映射过后，GDDRAM行的0对应oled屏幕行的0，GDDRAM列的0对应oled屏幕的列0，我们让左上角的二极管亮，只需要将SEG0和COM0定位的二极管亮即可；设置Segment重映射的命令如下:设置1即可让127映射为SEG0，从左到右127到0设置Common重映射的命令如下:X3设置为1即可从上到下为63到0
I2C总线1.1 概述​ I2C（Inter-Integrated Circuit），通常简称为IIC，是一种用在集成电路（IC）之间的串行通信总线。它是由Philips（现在的NXP半导体）在上世纪80年代开发的，并在之后广泛应用于各种电子设备和嵌入式系统中。1.2 信号线​ I2C为同步串行通信，使用两根线路进行通信，分别是数据线（SDA）和时钟线（SCL），SDA线用于数据传输，SCL线用于数据传输的同步。SCL的每个时钟周期，SDA传输一位数据。​ I2C规定，数据接收方会在每个时钟周期的高电平期间读取数据，具体来讲就是在SCL处于高电平时，读取SDA上的数据，如下图所示:​ 因此，SDA必须在SCL低电平期准备好要发送的下一位数据，然后在SCL高电平期间保持稳定。1.3 主从架构​ I2C采用主从架构，一个主设备可连接多个从设备。主设备负责发起通信和控制总线，而从设备负责响应主设备的请求。如下图所示:​ I2C总线中的每个设备都有一个唯一的地址（用7位二进制数字表示），用于在总线上标识自己。主设备可以根据地址选择性的与特定的从设备进行通信。​ 需要注意的是，SCL信号线上的时钟信号始终由主设备产生，而SDA信号线上的数据信号既可由主设备产生，也可由从设备产生。当主设备向从设备发送数据时，SDA信号由主设备产生，从设备接收信号；当主设备从从设备读取数据时，SDA信号由从设备产生，主设备接收信号。1.4 通信协议1.4.1 空闲状态​ I2C协议规定，当SDA和SCL均为高电平时，总线为空闲状态。1.4.2 起始和结束信号​ 主设备和从设备间的每次通信，都需要以一个起始信号开始，以一个结束信号终止。起始信号和结束信号的定义如下：​ 起始信号：当SCL处于高电平时，SDA由高变低。​ 停止信号：当SCL处于高电平时，SDA由低变高。 ​ 如下图：​ 起始信号和结束信号，都只能由主设备产生。1.4.3 确认信号​ I2C协议规定，发送方每发送一个字节（8位）的数据，接收方都要向发送方回复一个1位的确认信号，如下图所示。​ ​ 如果该确认信号为0表示接收方已成功接收到该字节，发送方可继续发送下一字节，这个信号在I2C协议中称为ACK（Acknowledge）；如果该信号为1，则表示接收方未能成功接收到该字节，或者不希望接收更多数据，该信号在I2C协议中称为NACK（Not Acknowledge）。1.4.4 从机地址和读写标识​ 由于一个I2C总线上可能有多个从设备，所以开始通信前，主设备需要先与目标设备取得联系，然后再进行数据传输，除此之外，主机还需要向目标设备明确本次通信的操作是写数据还是读数据。​ 上述操作的实现思路如下：当主设备发送起始信号之后，会向所有设备发送一个字节的数据，这一个字节中，前7位为目标设备地址，第8位为读/写标识（1表示读，0表示写），如下图所示。当各从设备收到这个字节的数据后，会将7位地址与自身进行对比，相同则会向主设备回复确认信号，不相同则不做任何回应。当主设备收到目标设备的确认信号后，便会开始与该设备进行通信。1.4.5 完整通信流程​ 综上所述，当主设备想要与某个从设备进行通信时，需要经历如下流程。发送起始信号;发送目标从设备地址+读写标识位;接收从设备回复的确认信号;与从设备进行数据传输（发送/接收）;发送终止信号;1.5 基础驱动编写1.5.1功能分析​ 为方便后序使用I2C协议通信，此处先编写一个通用的基础的驱动，该驱动包含的具体函数如下图所示:1.5.2 发送起始信号确保在空闲状态：当SDA和SCL均为高电平时，总线为空闲状态SCL = 1; SDA = 1;拉低SDA，发送起始信号SDA = 0;拉低SCL，准备后面的数据位:SCL = 0;代码：void Dri_IIC_Start(){ SCL = 1; SDA = 1; SDA = 0; SCL = 0; }1.5.3 发送一个字节SCL低电平期间准备数据位，例如:SDA = 1拉高SCL，发送数据:SCL = 1拉低SCL，准备下一位：SCL = 0准备下一位数据：SDA = 0拉高SCL，发送数据:SCL = 1SCL = 0至此，发送了两个数据位，不难发现，发送每个数据位的操作是相同的，因此，想要发送一个字节，只需将发送一个数据位的操作循环执行8次即可：需要注意的是I2C协议规定，发送一个字节的数据时，要从高位开始，具体代码如下：void Dri_IIC_SendByte(u8 byte) { u8 i; for (i = 0; i < 8; i++) { // SDA = (byte & (0x80 >> i)) == 0 ? 0 : 1; // 0x80: 1000 0000 if((byte & 0x80) == 0x00) { SDA = 0; }else { SDA = 1; } byte <<= 1; SCL = 1; SCL = 0; } }1.5.4 接收确认信号主设备释放SDA如何释放SDA?I2C协议规定，所有设备的SCL和SDA引脚都需要以开漏(Open-Drain)模式接入总线，如下图所示:该模式的工作原理是:设备输出低电平时，MOS管导通，信号线接地为低电平; 设备输出高电平时，MOS管关闭，信号线被上拉电阻拉至高电平。因此主设备在接受从设备数据时，需要将SDA输出置为1，此时主设备的SDA输出和SDA信号线呈断开状态，从设备便可控制SDA信号线向主设备发送确认信号了。释放步骤：SDA = 1;设置SDA = 1 后，从设备会在SCL电平期间准备好确认信号拉高SCL， 读取确认信号：SCL = 1; ack = SDA;拉低SCL，准备下一位数据SCL = 0;代码：bit Dri_IIC_ReceiveAck() { bit ack; SDA = 1; SCL = 1; ack = SDA; SCL = 0; return ack; }1.5.5 接收一个字节主设备释放SDA，以允许从设备驱动SDASDA = 1拉高SCL，并在SCL高电平期间读取SDA的值SCL = 1; current_bit = SDA;拉低SCL，以便从设备准备下一位数据：SCL = 0;SCL = 1; current_bit = SDA;完整代码:u8 Dri_IIC_ReceiveByte() { u8 byte = 0; u8 i; SDA = 1; for (i = 0; i < 8; i++) { SCL = 1; byte = (byte << 1) | SDA; SCL = 0; } return byte; }1.5.6 发送确认信号SDA = 0SCL = 1;SCL = 0;代码：void Dri_IIC_SendAck(bit ack) { SDA = ack; SCL = 1; SCL = 0; }1.5.7 发送停止信号SDA = 0SCL = 1;SDA = 1代码：void Dri_IIC_Stop() { SDA = 0; SCL = 1; SDA = 1; }1.5.8 完整代码Dri_IIC.h#ifndef __DRI_IIC_H__ #define __DRI_IIC_H__ #include <STC89C5xRC.H> #include "Util.h" #define SCL P17 #define SDA P16 void Dri_IIC_Start(); void Dri_IIC_Stop(); void Dri_IIC_SendByte(u8 byte); u8 Dri_IIC_ReceiveByte(); void Dri_IIC_SendAck(bit ack); bit Dri_IIC_ReceiveAck(); #endif /* __DRI_IIC_H__ */Dri_IIC.c#include "Dri_IIC.h" void Dri_IIC_Start() { SCL = 1; SDA = 1; SDA = 0; SCL = 0; } void Dri_IIC_Stop() { SDA = 0; SCL = 1; SDA = 1; } void Dri_IIC_SendByte(u8 byte) { u8 i; for (i = 0; i < 8; i++) { // SDA = (byte & (0x80 >> i)) == 0 ? 0 : 1; // 0x80: 1000 0000 if((byte & 0x80) == 0x00) { SDA = 0; }else { SDA = 1; } byte <<= 1; SCL = 1; SCL = 0; } } u8 Dri_IIC_ReceiveByte() { u8 byte = 0; u8 i; SDA = 1; for (i = 0; i < 8; i++) { SCL = 1; byte = (byte << 1) | SDA; SCL = 0; } return byte; } void Dri_IIC_SendAck(bit ack) { SDA = ack; SCL = 1; SCL = 0; } bit Dri_IIC_ReceiveAck() { bit ack; SDA = 1; SCL = 1; ack = SDA; SCL = 0; return ack; }
UART（四）发送多字节命令1.1 具体要求当PC向单片机发送字符串turn on时，单片机需要令LED亮起，并向PC回复：OK: LED turned on\r\n当PC向单片机发送字符串off时，单片机需要令LED熄灭，并向PC回复：OK: LED turned off\r\n当PC向单片机发送其他字符串时，单片机不做任何操作，只需向PC回复：Error:Unknown command1.2 实现方式1特定结束符判断是否接收完毕数据，如回车符：定义接收缓冲区和接收到的数据索引以及是否接收完毕的变量：u8 buffer_str[32]; // 接收缓冲区 u8 buffer_str_len = 0; bit recv_line_ready;中断函数， 接收到\r时将是否接收完毕的变量置1void Dri_UART_Func() interrupt 4 { // 收到命令,将数据放入缓冲区 if (RI == 1) { unsigned char ch = SBUF; // 避免数组越界 if (buffer_str_len < sizeof(buffer_str) - 1) { if (ch == '\r') { recv_line_ready = 1; // 不处理'\n' } else if (ch != '\n') { buffer_str[buffer_str_len++] = ch; } } RI = 0; } ... }外界循环调用一个函数判断是否接收完毕的变量置1情况，外界提供数组，函数内部将接收到的数组内容循环赋值给外部数组：bit Dri_UART_RecvStr(char *str) { if(recv_line_ready == 1){ u8 i = 0; while (i < buffer_str_len) { str[i] = buffer_str[i]; i++; } str[i] = '\0'; buffer_str_len = 0; // 清空缓冲区 recv_line_ready = 0; // 重置接收状态 return 1; }else { return 0; // 没有接收到完整的字符串 } }外界循环调用，收到完整字符串后判断是否为需要的内容，并做相应的处理逻辑：while (1) { if(Dri_UART_RecvStr(buffer_strs) == 1){ if (strcmp(buffer_strs, "turn on") == 0) { Dri_UART_SendStr("OK: LED turned on\r\n"); } else if (strcmp(buffer_strs, "off") == 0) { Dri_UART_SendStr("OK: LED turned off\r\n"); } else { Dri_UART_SendStr("ERROR: Unknown command\r\n"); } } }完整代码Dri_UART.h#ifndef __DRI_UART_H__ #define __DRI_UART_H__ #include <STC89C5xRC.H> #include "Util.h" void Dri_UART_Init(); void Dri_UART_Func(); void Dri_UART_SendChar(char str); void Dri_UART_SendStr(char *str); bit Dri_UART_RecvStr(char *str); #endifDri_UART.c#include "Dri_UART.h" u8 buffer_str[32]; // 接收缓冲区 u8 buffer_str_len = 0; bit recv_line_ready; // 发送状态,1:正在发送,0:未在发送 static bit is_sending; void Dri_UART_Init() { // 1.设置串口工作模式 SM0 = 0; SM1 = 1; // 2.设置波特率 // 2.1 设置SMOD控制位 PCON &= ~0x80; // 2.2设置定时器1工作模式 TMOD &= 0x0F; TMOD |= 0x20; // 2.3 设置定时器1初值 TH1 = 0xFD; TL1 = 0xFD; // 2.4 启动定时器1 TR1 = 1; // 3.串口接收相关配置 REN = 1; SM2 = 0; // 打开中断总开关和串口中断开关 EA = 1; ES = 1; TI = 0; RI = 0; is_sending = 0; } void Dri_UART_SendChar(char ch) { // 如果有数据在发送，等待发送完成 while (is_sending); is_sending = 1; SBUF = ch; } void Dri_UART_SendStr(char *str) { while (*str) { Dri_UART_SendChar(*str); str++; } } bit Dri_UART_RecvStr(char *str) { if(recv_line_ready == 1){ u8 i = 0; while (i < buffer_str_len) { str[i] = buffer_str[i]; i++; } str[i] = '\0'; buffer_str_len = 0; // 清空缓冲区 recv_line_ready = 0; // 重置接收状态 return 1; }else { return 0; // 没有接收到完整的字符串 } } /** * @brief 串口中断函数，进入这个函数有两个触发条件：发送完成和接收完成 * */ void Dri_UART_Func() interrupt 4 { // 收到命令,将数据放入缓冲区 if (RI == 1) { unsigned char ch = SBUF; if (buffer_str_len < sizeof(buffer_str) - 1) { if (ch == '\r') { recv_line_ready = 1; } else if (ch != '\n') { buffer_str[buffer_str_len++] = ch; } } RI = 0; } // 发送完成,将发送状态置为0 if (TI == 1) { is_sending = 0; TI = 0; // 清除发送中断标志 } } main.c#include "Dri_Timer0.h" #include "Dri_UART.h" #include <STRING.H> #define BUZZ P46 #define LED P00 #define LED_OPEN P34 void Int_Buzzer_Buzz() { unsigned char counter = 50; // 500Hz的方波 while (counter) { BUZZ = ~BUZZ; Delay_1MS(1); --counter; } } void main() { u8 i = 0; u8 buffer_strs[32]; BUZZ = 1; // 打开蜂鸣器 LED_OPEN = 1; Dri_UART_Init(); while (1) { if(Dri_UART_RecvStr(buffer_strs) == 1){ if (strcmp(buffer_strs, "turn on") == 0) { // 点亮LED P0 = 0x00; Dri_UART_SendStr("OK: LED turned on\r\n"); } else if (strcmp(buffer_strs, "off") == 0) { // 熄灭LED P0 = 0xFF; Dri_UART_SendStr("OK: LED turned off\r\n"); } else { Dri_UART_SendStr("ERROR: Unknown command\r\n"); } // 蜂鸣器响一声 Int_Buzzer_Buzz(); } } } 1.3 实现方式2空闲超过特定时间就表示接收完毕，特点时间内接收数据，每收到一个字符开启新的计时；Dri_UART.h#ifndef __DRI_UART_H__ #define __DRI_UART_H__ #include <STC89C5xRC.H> #include "Util.h" /** * @brief 串口初始化 * */ void Dri_UART_Init(void); /** * @brief 发送字符串 * * @param str 待发送字符串 */ void Dri_UART_SendStr(char *str); /** * @brief 接收字符串 * * @param str 用于接收字符串的数组 * @return bit 0为读取失败，1为读取成功 */ bit Dri_UART_ReceiveStr(char str[]); #endif /* __DRI_UART_H__ */Dri_UART.c#include "Dri_UART.h" #include "Dri_Timer0.h" #define BAUD_RATE 9600 #define T2TEMP 256 - (FOSC / NT / 32 / BAUD_RATE) #include <STDIO.H> / 0:未在发送 1:正在发送 static bit s_is_sending = 0; static char s_buffer[10] = {0}; static u8 s_index = 0; static bit s_is_complete = 0; static u8 s_idle_count = 0; void Dri_UART_Timer0Callback() { s_idle_count++; if (s_index > 0 && s_idle_count >= 10) { s_is_complete = 1; s_idle_count = 0; } } void Dri_UART_Init() { // 1.选择工作模式 SM0 = 0; SM1 = 1; // 2.波特率 // 2.1 SMOD PCON &= 0x7F; // 2.2 Timer1 // 2.2.1 工作模式 TMOD &= 0x0F; TMOD |= 0x20; // 2.2.2 初始值 TH1 = 0xFD; TL1 = 0xFD; // 2.2.3 开启 TR1 = 1; // 3.接受相关 REN = 1; SM2 = 0; // 4. 中断相关 EA = 1; ES = 1; RI = 0; TI = 0; // 5. 注册空闲监测回调 Dri_Timer0_RegisterCallback(Dri_UART_Timer0Callback); } void Dri_UART_SendChar(char c) { while (s_is_sending == 1); s_is_sending = 1; SBUF = c; } void Dri_UART_SendStr(char *str) { while (*str != 0) { Dri_UART_SendChar(*str); str++; } } bit Dri_UART_ReceiveStr(char *cmd) { u8 i; if (s_is_complete) { for (i = 0; i < s_index; i++) { cmd[i] = s_buffer[i]; } cmd[s_index] = '\0'; s_index = 0; s_is_complete = 0; return 1; } else { return 0; } } void Dri_UART_Handler() interrupt 4 { if (RI == 1) { // 接收数据的逻辑 s_idle_count = 0; s_buffer[s_index++] = SBUF; RI = 0; } if (TI == 1) { // 发送数据的逻辑 s_is_sending = 0; TI = 0; } }main.c#include "Dri_UART.h" #include <STRING.H> void main() { char command[10] = {0}; Dri_UART_Init(); while (1) { if (Dri_UART_ReceiveStr(command)) { if (strcmp(command, "on") == 0) { P0 = 0x00; Dri_UART_SendStr("Ok:LED is on"); } else if (strcmp(command, "off") == 0) { P0 = 0xFF; Dri_UART_SendStr("Ok:LED is off"); } else { Dri_UART_SendStr("Error:Unhnown command"); } } } }
UART（三）发送单字节命令​ 使用UART与PC进行通信，通过PC向单片机发送命令，控制LED的亮灭​ 当前需求是实现PC单片机的串口通讯，但是现在的PC基本都不再提供串口，因此需要使用一个USB转串口的芯片来实现PC与单片机的通讯，如下图所示。1.1 具体要求当PC向单片机发送字符A时，单片机需要令LED亮起，并向PC回复：Ok: LED is on。当PC向单片机发送字符B时，单片机需要令LED熄灭，并向PC回复：Ok: LED is off。当PC向单片机发送其他字符时，单片机不做任何操作，只需向PC回复：Error:Unknown command。1.2 实现思路​ 实现当前需求，需要先对串口进行初始化，具体操作如下。1.2.1 选择串口工作模式​ 本案例选择模式1，因此需要将SM0和SM1做出如下配置。SM0 = 0; SM1 = 1;1.2.2 设置波特率​ 本案例波特率选用9600，需要做如下配置。（1）SMOD控制位​ 按照前文的计算，将SMOD设置为0即可，由于SMOD不可进行位寻址，因此我们需要对其所在的寄存器PCON进行整体赋值，如下。PCON &= 0x7F;（2）定时器1​ 按照前文的计算，定时器1应工作在模式2（8位自动重装载），每次重装载的初始值应为253。具体设置如下。// 定时器1工作模式 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1的初值 TH1 = 0xFD; TL1 = 0xFD; // 启动定时器1 TR1 = 1;（3）串口接收相关配置​ 串口默认不接收数据，因此需要先使能接受，另外还需将SCON寄存器中的SM2控制位设置为0，表示接受数据时不校验数据帧的停止位。REN = 1; SM2 = 0;（4）启动串口中断// 开启中断 EA = 1; // 开启串口中断 ES = 1; // 复位中断标志位 RI = 0; TI = 0;​ 完成串口的初始化之后，根据需求编写响应的业务逻辑即可。1.3 代码1.3.1 初始化void Dri_UART_Init() { // 1.设置串口工作模式 SM0 = 0; SM1 = 1; // 2.设置波特率 // 2.1 设置SMOD控制位 PCON &= ~0x80; // 2.2设置定时器1工作模式 TMOD &= 0x0F; TMOD |= 0x20; // 2.3 设置定时器1初值 TH1 = 0xFD; TL1 = 0xFD; // 2.4 启动定时器1 TR1 = 1; // 3.串口接收相关配置 REN = 1; SM2 = 0; // 打开中断总开关和串口中断开关 EA = 1; ES = 1; TI = 0; RI = 0; }1.3.2 串口中断函数当收到上位机发送的'A'命令后，打开led，收到'B'命令后，关闭led：void Dri_UART_Func() interrupt 4 { // 收到命令,将数据放入缓冲区 if (RI == 1) { if(SBUF == 'A'){ P0 = 0x00; }else if(SBUF == 'B'){ P0 = 0xFF; } RI = 0; // 清除接收中断标志 } if (TI == 1) { TI = 0; // 清除发送中断标志 } } 1.3.3 发送一个字符数据void Dri_UART_SendChar(char ch) { SBUF = ch; }1.3.4 发送一个字符串void Dri_UART_SendStr(char *str) { while (*str) { Dri_UART_SendChar(*str); str++; } }循环调用发送一个字符的函数，直接写 SBUF，但没有检查上一个字符是否已经发完，这样可能会丢字符！修改发送单个字符函数，添加全局变量is_sending避免了数据在未发送完成时被覆盖；// 发送状态,1:正在发送,0:未在发送 static bit is_sending; void Dri_UART_SendChar(char ch) { // 如果有数据在发送，等待发送完成 while (is_sending); is_sending = 1; SBUF = ch; } // 初始化函数 void Dri_UART_Init() { ... // 初始化函数添加 is_sending = 0; } // 中断函数 void Dri_UART_Func() interrupt 4 { ... // 发送完成,将发送状态置为0 if (TI == 1) { is_sending = 0; ... } }1.3.5 收到指令后回复字符串中断函数添加逻辑：void Dri_UART_Func() interrupt 4 { // 收到命令,将数据放入缓冲区 if (RI == 1) { if(SBUF == 'A'){ ... Dri_UART_SendStr("LED is on") }else if(SBUF == 'B'){ ... Dri_UART_SendStr("LED is off") }else{ Dri_UART_SendStr("Error:Unknown command") } RI = 0; // 清除接收中断标志 } ... }结果会发生什么？调用 Dri_UART_SendStr()，第一个字符调用 SendChar；is_sending == 0，于是 SBUF 写入第一个字节；等待发送完成 —— 但 你在中断里，TI == 1 不能触发新的中断；所以 中断函数死等 is_sending == 0 永远不会成功；程序卡死在中断里，后面所有中断（包括接收）都不来了，系统死锁。修改代码// 接收到的数据存放在buffer中 char buffer; // 调用 Dri_UART_RecvChar() 从 buffer 中取出接收到的数据； bit Dri_UART_RecvChar(char *p_ch) { // 如果缓冲区有数据,则将值符传给p_ch,并清空缓冲区 if (buffer) { *p_ch = buffer; buffer = 0; return 1; } else { return 0; } } // 中断函数 void Dri_UART_Func() interrupt 4 { if (RI == 1) { buffer = SBUF; // 收到命令,将数据放入缓冲区 ... } ... } // 初始化函数 void Dri_UART_Init() { ... // 初始化函数添加 buffer = 0; }函数使用void main() { char command; Dri_UART_Init(); while (1) { if (Dri_UART_RecvChar(&command)) { if (command == 'A') { // 点亮LED P0 = 0x00; Dri_UART_SendStr("Ok:LED is on"); } else if (command == 'B') { // 熄灭LED P0 = 0xFF; Dri_UART_SendStr("Ok:LED is off"); } else { // 报错 Dri_UART_SendStr("Error:Unknown command"); } } } }1.3.6 单字节接收完整代码Dri_UART.h#ifndef __DRI_UART_H__ #define __DRI_UART_H__ #include <STC89C5xRC.H> #include "Util.h" /** * @brief 串口初始化方法，需要先调用 * */ void Dri_UART_Init(); /** * @brief 通过串口发送一个字符串 * * @param ch 要发送的字符串 */ void Dri_UART_SendStr(char *str); /** * @brief 通过串口接收一个字符 * * @param p_ch 要接收的字符指针 * * @return 0为读取失败，1为读取成功 */ bit Dri_UART_RecvChar(char *p_ch); #endifDri_UART.c#include "Dri_UART.h" #define BAUD_RATE 9600 #define T2TEMP 256 - (FOSC / NT / 32 / BAUD_RATE) char buffer; // 发送状态,1:正在发送,0:未在发送 static bit is_sending; void Dri_UART_Init() { // 1.设置串口工作模式 SM0 = 0; SM1 = 1; // 2.设置波特率 // 2.1 设置SMOD控制位 PCON &= ~0x80; // 2.2设置定时器1工作模式 TMOD &= 0x0F; TMOD |= 0x20; // 2.3 设置定时器1初值 TH1 = 0xFD; TL1 = 0xFD; // 2.4 启动定时器1 TR1 = 1; // 3.串口接收相关配置 REN = 1; SM2 = 0; // 打开中断总开关和串口中断开关 EA = 1; ES = 1; TI = 0; RI = 0; is_sending = 0; buffer = 0; } void Dri_UART_SendChar(char ch) { // 如果有数据在发送，等待发送完成 while (is_sending); is_sending = 1; SBUF = ch; } void Dri_UART_SendStr(char *str) { while (*str) { Dri_UART_SendChar(*str); str++; } } bit Dri_UART_RecvChar(char *p_ch) { // 如果缓冲区有数据,则将值符传给p_ch,并清空缓冲区 if (buffer) { *p_ch = buffer; buffer = 0; return 1; } else { return 0; } } /** * @brief 串口中断函数，进入这个函数有两个触发条件：发送完成和接收完成 * */ void Dri_UART_Func() interrupt 4 { // 收到命令,将数据放入缓冲区 if (RI == 1) { buffer = SBUF; RI = 0; } // 发送完成,将发送状态置为0 if (TI == 1) { is_sending = 0; TI = 0; } }main.c#include <STC89C5xRC.H> #include "Dri_UART.h" void main() { char command; Dri_UART_Init(); while (1) { if (Dri_UART_RecvChar(&command)) { if (command == 'A') { // 点亮LED P0 = 0x00; Dri_UART_SendStr("Ok:LED is on"); } else if (command == 'B') { // 熄灭LED P0 = 0xFF; Dri_UART_SendStr("Ok:LED is off"); } else { // 报错 Dri_UART_SendStr("Error:Unknown command"); } } } }
UART（二）使用说明STC89C52系列单片机内部集成了一个功能强大的全双工串行通信口，以下是相关引脚。该通信口具有四种工作模式，如下。其中模式0为同步通讯，该模式下，TxD引脚会作为时钟信号线，RxD作为数据信号线，该模式下只能实现半双工通讯。模式1、2、3为经典的UART通讯，三者的区别在与是否有校验位，以及波特率是否可变。模式1的一个数据帧包含1个起始位，8个数据位和1个停止位。模式2和模式3的一个数据帧包含1个起始位，8个数据位，1个校验位和1个停止位。另外模式1和模式3的波特率可由定时器1进行配置，因而可以自由设置，而方式2的波特率直接由系统时钟决定，因而不可自由配置。1.1 设置工作模式​ 工作模式需要通过SCON（Serial Control，串行口控制）寄存器中的M0和SM1两个控制位进行设置，如下图所示。​ 如果使用方式1，则应将SM0置为0，SM1置为1。1.2 设置波特率1.2.1 设置原理以方式1为例:方式1的波特率会受到两个因素的影响，分别是SMOD控制位和定时器1的溢出频率，具体作用如下图所示。以中间水平线大致分割，上面为发送模块，下面为接收模块：时钟脉冲决定了发送一帧数据的频率：最初时钟来源为定时器1的溢出频率，每溢出一次向外输出一次时钟信号，经过SMOD根据配置是否分频，再往后分频16次即为波特率：溢出频率由定时器初始值决定，SMOD由配置决定​ 根据结构图可以得出公式，模式一的波特率为：1.2.2 设置步骤​ UART常用的波特率有4800、9600、19200、38400、57600、115200等，此处以9600为例。1.2.2.1 设置SMOD控制位SMOD控制位位于PCON（Power Control，电源控制）寄存器，如下图所示。此处选择0，所以此时的波特率应为（定时器1的溢出率）/ 32。1.2.2.2 设置定时器1​ 由于定时器1当前的作用仅仅是为串口提供时钟信号，因此可不开启定时器中断，只需完成以下配置即可。（1）选择定时器工作模式​ 定时器1的工作模式需要通过TMOD寄存器中的C/T控制位，以及M1和M0两个控制位进行设置，如下图所示。C/T用于设置计数/定时的工作方式，此处应选择定时模式，因此需将C/T设置为0。M1和M0，用于配置具体的工作模式，相关配置如下。由于没有启用定时器中断，因此无法在中断中重新设置定时器脉冲计数器的初始值，所以此处选择模式2（8位自动重装载）最为合理。（2）设置脉冲计数器初始值模式2的最大计数为2⁸=256，因此定时器1的溢出频率等于SYSclk / 12 /（256 - TH1）或者是SYSclk / 6 /（256 - TH1），假如当前MCU工作在12T模式，所以溢出频率就等于SYSclk / 12 /（256 - TH1）。SYSclk为系统时钟频率，工作模式为12t，则时钟频率分频12，即系统时钟频率除以12定时器计数+1；寄存器为8位，可以存256个数，当TH1为255时，此时寄存器再来一次脉冲就溢出了；每秒定时器溢出几次 = 定时器每秒加了多少次（SYSclk / 12），再除以它每次溢出需要多少次（256 - TH1）​ 因此最终的波特率就应该等于SYSclk / 12 /（256-TH1）/32，假如当前的系统时钟频率为11059200Hz，目标的波特率为9600，那么就能得到以下方程​ 所以可计算出TH1的值应为253。（3）启动定时器​ 定时器1的启动也无需外部引脚控制，因此应将GATE控制位设置为0，并将TR1控制位设置为1。1.3 发送数据​ STC89C52的串口的发送模块示意图如下。​ 数据发送的大致流程如下：​ 开发者将待发送的8位数据写入发送缓冲器（SBUF），此时发送控制器（Tx Control）就会开始工作，它会自动为数据添加起始位和结束位，从而构成一个完整的UART数据帧，然后逐位通过TxD引脚输出出去。当完成一个数据帧的输出之后，发送控制器会将发送中断控制位TI置1，向CPU请求中断，CPU检测到中断请求后就执行相应的中断服务程序。​ 总结：发送数据只需要将待发送的数据写入SBUF即可。1.4 接收数据​ STC89C52的串口的接收模块示意图如下。​ 默认情况下，串口并不会接收数据。如需接收数据，需要先将REN（Receive Enable）控制位置为1，REN控制位位于SCON寄存器，如下图所示。​ 当REN置为1后，上图中的1到0跳变检测器（1-To-0 Transition Detector）就会开始工作，具体来讲就是不断检测RxD引脚的起始位。当检测到1到0的跳变后，就会启动接收控制器（Rx Control），接收控制器会将接收到数据逐位移入到输入移位寄存器（Input Shift REG），直到接收到停止位，就算完成了一帧数据的接收。​ 正常情况下，接下来，接收控制器会将输入移位寄存器（Input Shift REG）中的数据加载到读取缓冲器（SBUF）中，并将读取中断控制位RI置1，向CPU请求中断，CPU检测到中断请求后就执行相应的中断服务程序，开发者就能在中断服务程序中读取SBUF获取当前帧的数据了。​ 但是上述操作（加载数据到SBUF和RI置位）的执行是有条件的，满足条件才会执行，不满足，那么当前数据帧就会被丢弃，具体条件如下：1）结束位正常​ 开发者可以配置是否检测停止位的有效性（高电平有效）。是否检测是由SCON寄存器中的SM2控制位来决定的。SM2=1时，接收控制器就会检测控制停止位，当SM2=0时，则不会检测停止位，建议将SM2设置为0。1）读取中断标志位为复位状态​ 读取中断标志位RI必须等于0，也就说要保证上一帧数据已经被读取或处理完毕，才能处理当前帧。​ 总结：接收数据需要先使能接收，也就是将REN控制位置1，然后开启串口中断，并在中断服务程序中读取SBUF。1.5 串口中断注意事项​ 根据前文的描述，当使用串口发送完一帧数据后，会将发送中断标志位TI置1；当串口接收到一帧数据后，会将接收中断标志位RI置1。需要注意的是两个控制位请求的是同一个中断——串口中断（中断号为4）。​ 也就是说发送完一帧数据和接收完一帧数据之后，执行的都是串口中断的中断服务程序，因此，再编写该中段服务程序时，需要注意判断当前中断到底是由发送操作触发的，还是由接收操作触发的，代码示例如下。/* * 串口中断的中断号为4 */ void Dri_UART_Handler() interrupt 4 { /* 检查接收中断标志位RI，如果为1，表示有一帧数据接收完成 */ if (RI == 1) { } /* 检查发送中断标志位TI，如果为1，表示有一帧数据发送完成 */ if (TI == 1) { } }​ 另外RI和TI标志位，只能由软件复位，也就是需要在中断服务程序中将其设置为0，如下。/* * 串口中断的中断号为4 */ void Dri_UART_Hander() interrupt 4 { /* 检查接收中断标志位RI，如果为1，表示有一帧数据接收完成 */ if (RI == 1) { RI = 0; } /* 检查发送中断标志位TI，如果为1，表示有一帧数据发送完成 */ if (TI == 1) { TI = 0; } }
UART（一）概述1.1 串行通讯和并行通讯串行通讯和并行通讯是数据传输的两种主要方式，两者的区别如下：串行通讯是指数据通过一条数据线逐位传输并行通讯是指数据通过多条数据线同时传输 串行并行传输速率低高硬件成本低高抗干扰能力强弱通讯距离远近1.2 单工通讯和双工通讯​ 单工和双工是通讯领域中用于描述数据传输方向的术语，它们定义了两个设备之间的数据通信方式。​ 单工通信，只允许数据在一个方向上传输，即数据只能从发送端传输到接收端，接收端无法向发送端传输数据。简单来说，就是一种“单向”通信模式，可以类比电视广播。​ 双工通信，允许数据在两个方向上传输，其下又分为两种类型：半双工和全双工 。​ 半双工通信允许数据在两个方向上传输，但不能同时进行。在任何时刻，数据只能在一个方向上传输。这意味着通信的两端可以轮流发送和接收数据，但不能同时进行。可以类比对讲机。​ 全双工通信允许数据同时在两个方向上传输。这种通信方式最为高效，因为它允许通信双方同时发送和接收数据，可以类比电话。1.3 同步通讯和异步通讯​ 同步通讯和异步通讯的区别在于发送方和接收方如何对数据进行协调统一，详细内容如下图所示：​ 假如发送方和接收方正在以串行的方式进行通信，发送方发出了这样一段信号，请思考，接收方应该如何解析这段信号？这样？还是这样？应该如何协调发送方和接收方？​ 方案一: 发送方发送数据的同时，发送一个时钟信号，并规定，接收方在每个时钟周期的下降沿对数据进行采样，这种方案叫做同步通讯(发送方和接收方使用相同的时钟信号，因此两者的时钟的是同步的)​ 方案二: 统一发送方发送数据的频率和接收方的接收频率，这样就能保证数据的正确解析，同时还不需要额外的时钟信号，这种方案叫做异步通讯(发送方和接收方的时钟不一定同步)同步通信（Synchronous Communication）： 发送方和接收方共享同一个时钟信号，数据的发送和接收都依赖这个共同的时钟来协调。常见于 I²C、SPI 等通信方式。异步通信（Asynchronous Communication）： 发送方和接收方不共享时钟，而是通过事先约定的波特率（每秒传输的比特数）和起始位、停止位等机制来实现数据同步。常见于 UART 通信。1.4 UART定义​ UART（Universal Asynchronous Receiver/Transmitter）是一种异步、全双工的串行通信接口，常用于微控制器与计算机、其他微控制器或外部设备之间的数据交换，下图是UART通信所需的信号线，其中Tx用于发送数据，Rx用于接受数据。1.5 UART通讯协议1.5.1 数据格式​ 在UART通信中，数据是逐帧（Frame）发送的，每个数据帧通常包括起始位、数据位、校验位（可选）和停止位，具体结构如下图所示。​ 下面对每个部分进行详细介绍。1）空闲状态​ 协议规定，在空闲状态下，也就是没有数据传输时，应为高电平。2）起始位​ 起始位表示一个数据帧的开始，起始位为低电平（区别于空闲状态）。3）数据位​ 传输的主体内容，位于起始位之后，长度可以是5到9位，一般都是8位。低电平表示0，高电平表示1。低位在前。4）校验位（可选）​ 用于校验当前帧的正确性，校验算法可以是奇校验或偶校验，该算法的思想如下。​ 奇校验（odd parity）：如果数据位中1的数目是偶数，则校验位为1，如果1的数目是奇数，校验位为0，目的是保证数据位+校验位中的1的总个数是奇数。​ 偶校验（even parity）：如果数据为中1的数目是偶数，则校验位为0，如果1的数目为奇数，校验位为1，目的是保证数据位+校验位中的1的个数是偶数。5）停止位​ 停止位表示数据帧的结束，通常为1位或2位，停止位为高电平。1.5.2 发送方和接收方的约定​ 为保证UART通信能够正常工作，发送方和接收方必须提前做好如下约定。1）波特率​ 波特率（Baud Rate）用于表示数据的传输速率，发送方和接收方必须约定好传输速率，才能保证数据被正确的发送和接收。​ 需要注意波特率（Baud Rate）和比特率（Bit Rate）的区别，比特率表示每秒传输的位（bit）数，而波特率表示每秒传输的符号（symbol）数。但是串口通信中，只有0和1这两个符号，因此1个符号用1位就能表示，所以此处的波特率和比特率是等价的。​ 波特率和比特率的关系取决于每个符号（symbol）携带的比特数。如果每个符号只携带1个比特，那么波特率和比特率是相等的。但如果每个符号携带多个比特（比如通过调制技术），比特率就会高于波特率。​ 公式：比特率 = 波特率 × 每个符号的比特数​ 例如：波特率是9600，每个符号携带2个比特，那么比特率就是9600 × 2 = 19200 bps。2）数据位​ 发送方和接收方需要明确数据位的位数。3）校验位​ 发送方和接收方需要明确是否有校验位，如果有，需要明确校验算法是哪个。4）停止位​ 发送方和接收方需要明确停止位的位数。
74HC595​ 74HC595芯片的相关资料如下。（1）购买链接：https://item.szlcsc.com/520614.html （2）实物图、原理图、封装工作原理​ 74HC595是一种常用的8位串行输入并行输出芯片，简单来说就是一位输入，多位输出​ 74HC595内部有两个寄存器，分别是移位寄存器(Shfit Register)和存储寄存器(storage Register)​ 移位寄存器用于接受串行数据输入，芯片会根据SCK时钟信号(上升沿)将串行输入逐位移入移位寄存器。存储寄存器与输出引脚相连，因此存储寄存器中的数据，就是芯片输出的数据，芯片会根据RCK时钟信号(上升沿)，将移位寄存器接受的串行数据一次性加载到存储寄存器进行并行输出。使用演示将SER、SCK、RCK接到单片机引脚，QA-QH接到输出引脚（如led引脚），如何输出01000000？其他引脚G#输出使能，低电平有效；SCLR#给低电平清除移位寄存器；QH‘为输出移位寄存器移出的位；
相关资料（1）STC单片机官网：https://www.stcaimcu.com/（2）STC89C52RC-40I-LOFP44购买链接：https://item.szlcsc.com/9213.html （3）STC89C52RC系列单片机手册 https://pan.baidu.com/s/1jrlVVyeKlaDejVkKXsJqwQ?pwd=syfc51单片机概述单片机是一种将中央处理器(CPU)、存储、输入/输出接口等集成在一个芯片上的微型计算机。它具有体积小、价格便宜、易于编程等特点，广泛应用于家用电器的智能控制模块，无人售卖系统、医疗设备中的监测系统等。1. 单片机概述51单片机（或称8051单片机）原是指1980年由Intel公司推出的一种8位微控制器系列，现在更多的指实现了8051指令集（单片机的“语言”）的一系列单片机。2. STC89C52RC概述型号STC89C52RC-40I-LOFP44，其内部同样采用了8051指令集。3. 命名规范4. 封装类型单片机的封装是指单片机的外形尺寸及材质，目前STC89C52RC有两种常见封装，如下图所示：两种封装的芯片并无性能差异，只是外观不同，具体选择哪一种需要看PCB板的设计要求5. 引脚功能概述STC89C52RC-40I-LOFP44共有44个引脚，各引脚的功能如下图所示。6. 最小系统概述电源: 和电脑电源一样，起到供电作用。电容104即10×10^4pF（0.1μF），还有一个稍大的电容10μF，电源接一大一小电容用来过滤不同频率的干扰。复位电路: 相当于电脑的重新启动功能。10μF电容串联一个10k电阻，一般称为rc复位。给单片机通电，电容开始充电，没电相当于导通，充满相当于断路，可以将这里的电容等效看做可变电阻，充的电越来越多，阻值越大。电压变化5 - 0，相当于持续一段时间是高电平，实现上电复位；在电容两侧并联一个瞬时开关，开关按下，电容短路放电，此时类似于单片机上电状态实现复位；晶振电路: 是电脑处理器的主频，2.3GHz，主频越快，处理速度越快。。不同最小系统看对应芯片手册。