[{"content":"第一部分：C语言基础概念详解 1. C语言的起源与特点 C语言诞生于20世纪70年代初，最初用于开发UNIX操作系统。它的设计目标是使得代码更高效且能够直接操作硬件，因此C语言也被称为“低级语言的高级化”。以下是C语言的几个主要特点：\n高效性：C语言编写的程序运行速度非常快，尤其适合底层开发，如操作系统、驱动程序等。 可移植性：C语言代码具有良好的跨平台移植性，只需少量修改就可以在不同平台上编译运行。 灵活性：C语言允许直接访问内存，可以进行精细的内存操作（如指针操作）。 丰富的运算符：C语言提供了丰富的运算符集，能够进行各种类型的数据运算和逻辑运算。 2. 程序的基本结构 每个C语言程序都由以下几个主要部分组成：\n预处理指令：使用 #include来引入标准库，如 stdio.h，为输入输出提供函数。 主函数 main()：每个C程序的入口点是 main()函数，程序的执行从 main()开始。其返回类型通常是 int，表示程序的执行状态（0通常表示成功）。 一个简单的C语言程序如下：\n1 2 3 4 5 6 #include \u0026lt;stdio.h\u0026gt; int main() { printf(\u0026#34;Hello, World!\\n\u0026#34;); return 0; } 3. 变量和数据类型 C语言是一种静态类型语言，这意味着在使用变量之前，必须声明变量的类型。主要的数据类型有：\n整数类型 (int)：用于存储整型数值，通常占用4字节。 例：int a = 10; 浮点类型 (float, double)：用于存储小数，float通常占用4字节，double占用8字节。 例：float pi = 3.14; 字符类型 (char)：用于存储单个字符，占用1字节。 例：char letter = 'A'; C语言允许使用修饰符来改变基本类型的特性，如：\nshort：表示短整型，减少内存占用； long：表示长整型，增加数值范围； unsigned：表示无符号类型，仅存储非负数。 例如：\n1 2 unsigned int positive_num = 100; // 无符号整数 long long large_num = 1234567890; // 长整型 4. 常量 常量是指在程序运行期间其值不可改变的量。C语言通过 const关键字来声明常量。例如：\n1 const int days_in_week = 7; // 一周的天数不会改变 5. 基本输入输出 输出函数 printf()：用于向屏幕输出信息，常见的占位符有： %d：输出整数； %f：输出浮点数； %c：输出字符； %s：输出字符串。 1 printf(\u0026#34;Integer: %d, Float: %.2f\\n\u0026#34;, 10, 3.1415); 输入函数 scanf()：用于从键盘读取输入，常见的格式符号与 printf()相似，使用 \u0026amp;符号来获取变量的地址。 1 2 int num; scanf(\u0026#34;%d\u0026#34;, \u0026amp;num); // 读取整数 6. 运算符 C语言的运算符种类繁多，主要分为以下几类：\n算术运算符：用于基本的数学运算，包括： +：加法； -：减法； *：乘法； /：除法（整数除法时，结果为整数）； %：取模运算（只适用于整数）。 例如：\n1 int result = 10 % 3; // result = 1 关系运算符：用于比较两个值，返回布尔结果（1表示真，0表示假）。 ==：等于； !=：不等于； \u0026gt;：大于； \u0026lt;：小于； \u0026gt;=：大于等于； \u0026lt;=：小于等于。 例如：\n1 2 3 if (a == b) { printf(\u0026#34;a equals b\u0026#34;); } 逻辑运算符：用于处理逻辑关系。 \u0026amp;\u0026amp;：逻辑与，只有两个表达式都为真时，结果为真； ||：逻辑或，任一表达式为真，结果为真； !：逻辑非，将表达式的结果取反。 例如：\n1 2 3 if (a \u0026gt; 0 \u0026amp;\u0026amp; b \u0026gt; 0) { printf(\u0026#34;Both a and b are positive numbers.\u0026#34;); } 7. 条件控制结构 C语言中的条件控制结构用于控制程序执行的流程，主要包括：\nif语句：用于条件判断，当条件成立时执行特定代码块。 else语句：在 if条件不成立时执行。 else if语句：用于多条件判断。 例如：\n1 2 3 4 5 6 7 8 int num = 5; if (num \u0026gt; 0) { printf(\u0026#34;Positive number\u0026#34;); } else if (num == 0) { printf(\u0026#34;Zero\u0026#34;); } else { printf(\u0026#34;Negative number\u0026#34;); } 8. 循环控制结构 C语言提供了三种常用的循环结构：\nfor循环：常用于已知循环次数的情况。 1 2 3 for (int i = 0; i \u0026lt; 5; i++) { printf(\u0026#34;%d\\n\u0026#34;, i); } while循环：在条件成立时重复执行代码块，适用于未知循环次数的情况。 1 2 3 4 5 int i = 0; while (i \u0026lt; 5) { printf(\u0026#34;%d\\n\u0026#34;, i); i++; } do\u0026hellip;while循环：先执行一次代码块，然后在条件成立时继续循环。 1 2 3 4 5 int i = 0; do { printf(\u0026#34;%d\\n\u0026#34;, i); i++; } while (i \u0026lt; 5); 9. 函数与作用域 函数是C语言中的重要组成部分，用于将代码逻辑封装成可重用的代码块。函数的声明和定义包括返回类型、函数名、参数列表和函数体。例如： 1 2 3 int add(int a, int b) { return a + b; } 在调用函数时，参数会传递到函数内，函数执行完毕后将返回一个值。例如：\n1 int result = add(5, 3); // result为8 局部变量与全局变量：C语言中的变量分为局部变量和全局变量。局部变量是在函数或代码块内部声明的变量，只能在其定义的范围内使用；全局变量是在函数外部声明的，整个程序都可以访问。 第二部分：数组与字符串的详细介绍 在上一部分中，我们讲到了C语言的基本概念、控制结构和函数等内容。现在，我们将深入探讨C语言中的数组与字符串，它们是编程中用于处理多个数据的一些关键工具。\n1. 数组概念 数组是存储相同数据类型元素的连续内存块。它使得我们可以用同一个变量名来处理一组值，而不是为每个值都声明一个独立的变量。数组的元素通过下标（索引）进行访问，索引从 0开始。\n一维数组 一维数组可以看作是一个线性的数据集合。其语法为： 1 data_type array_name[array_size]; 例如，声明一个存储5个整数的数组：\n1 int numbers[5]; // 创建一个包含5个整数的数组 我们可以通过下标来给数组元素赋值和访问元素：\n1 2 numbers[0] = 10; // 给第一个元素赋值10 int first_value = numbers[0]; // 获取第一个元素的值 也可以使用循环来遍历数组：\n1 2 3 4 5 6 7 8 9 #include \u0026lt;stdio.h\u0026gt; int main() { int numbers[5] = {1, 2, 3, 4, 5}; // 初始化数组 for (int i = 0; i \u0026lt; 5; i++) { printf(\u0026#34;%d \u0026#34;, numbers[i]); // 依次输出每个元素 } return 0; } 数组初始化 数组可以在声明时直接初始化，也可以在运行时赋值：\n静态初始化（在声明时指定初始值）： 1 int numbers[5] = {1, 2, 3, 4, 5}; // 初始化5个值 部分初始化时，未指定的元素会自动被初始化为 0： 1 int numbers[5] = {1, 2}; // 数组的剩余元素被初始化为0 多维数组 C语言支持多维数组，常用的有二维数组。二维数组可以看作是一个矩阵，即行列形式的数据结构。声明语法如下：\n1 data_type array_name[rows][cols]; 例如：\n1 int matrix[3][3]; // 3行3列的二维数组 二维数组的元素可以通过两个下标进行访问：\n1 matrix[0][1] = 5; // 将第一行第二列的元素赋值为5 二维数组的遍历通常使用嵌套循环：\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; int main() { int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}}; for (int i = 0; i \u0026lt; 2; i++) { for (int j = 0; j \u0026lt; 3; j++) { printf(\u0026#34;%d \u0026#34;, matrix[i][j]); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 在上述例子中，matrix[2][3]是一个2行3列的数组，遍历时通过嵌套的 for循环访问每个元素。\n2. 字符串 字符串在C语言中被视为字符数组。C语言中没有专门的字符串数据类型，因此字符串通常是一个以空字符 \\0 结尾的字符数组。该空字符标志着字符串的结束，且不会被显示输出。\n字符串的声明与初始化 字符串可以通过字符数组来声明，并且可以直接初始化为字符串常量：\n1 char str[6] = \u0026#34;Hello\u0026#34;; // 自动在末尾添加\u0026#39;\\0\u0026#39; 注意，这里的 str数组长度为6，因为 \u0026quot;Hello\u0026quot;有5个字符，加上末尾的空字符 \\0总共6个字节。我们也可以手动初始化字符串：\n1 char str[6] = {\u0026#39;H\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;, \u0026#39;\\0\u0026#39;}; // 手动添加结束符 字符串输入输出 可以使用 printf()输出字符串，也可以使用 scanf()读取字符串。特别地，scanf()读取字符串时，不需要写 \u0026amp;符号，因为字符串本质上是指针（稍后我们会详细讨论指针）。\n输出字符串： 1 printf(\u0026#34;%s\u0026#34;, str); // 输出字符串 输入字符串： 1 scanf(\u0026#34;%s\u0026#34;, str); // 从用户输入获取字符串 不过，使用 scanf()时有一个重要问题：它只能读取空格之前的内容，遇到空格会停止输入。如果需要读取包含空格的字符串，可以使用 gets()函数（已废弃，不推荐）或 fgets()函数：\n1 fgets(str, sizeof(str), stdin); // 读取带有空格的输入 常用字符串处理函数 C语言的 string.h库提供了一系列处理字符串的函数，如：\nstrlen()：计算字符串长度。 strcpy()：复制字符串。 strcat()：拼接字符串。 strcmp()：比较两个字符串。 示例：\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; int main() { char str1[20] = \u0026#34;Hello\u0026#34;; char str2[20] = \u0026#34;World\u0026#34;; strcat(str1, str2); // 将str2拼接到str1后 printf(\u0026#34;%s\\n\u0026#34;, str1); // 输出\u0026#34;HelloWorld\u0026#34; return 0; } 3. 数组与字符串的常见操作 数组与指针 在C语言中，数组名实际上是一个指向数组第一个元素的指针。我们可以通过指针运算来访问数组元素：\n1 2 3 int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // p指向数组的第一个元素 printf(\u0026#34;%d\\n\u0026#34;, *(p + 1)); // 输出第二个元素的值，结果为2 字符串的常见问题 数组越界：如果字符串数组的大小不足以容纳字符串和结束符 \\0，可能会导致内存越界的问题。因此在声明字符串数组时要确保有足够的空间。 空字符问题：字符串末尾必须有一个 \\0，否则可能导致程序在输出或操作字符串时出现错误或崩溃。 4. 字符串与数组的区别 尽管字符串是以字符数组的形式存在，但处理方式上仍有细微的区别：\n字符数组可以包含任意字符，不必以 \\0结尾； 字符串必须以 \\0结尾，printf等函数遇到 \\0时才会停止输出。 第三部分：指针详解 在前面的部分，我们讨论了数组和字符串，并简要提到了指针的概念。现在我们将深入讨论指针，这是C语言中最强大、但也最难掌握的概念之一。指针在C语言中的作用非常重要，尤其是在处理内存、数组、函数参数和动态分配内存时。\n1. 指针的基本概念 指针是存储另一个变量的地址的变量。也就是说，指针指向存储在内存中的一个特定位置，而这个位置存储了某个值。通过指针，可以间接地访问和修改存储在该位置的值。\n指针声明 在C语言中，指针通过类型名和一个星号（*）声明。它的基本语法为：\n1 data_type *pointer_name; data_type：指针所指向的变量的类型，例如 int, float等。 pointer_name：指针变量的名称。 例如：\n1 int *p; // 声明一个指向整数的指针p 指针的地址和解引用 \u0026amp;运算符：取地址运算符，返回变量的内存地址。 *运算符：解引用运算符，返回指针指向的变量的值。 例如：\n1 2 3 4 int a = 10; int *p = \u0026amp;a; // p存储变量a的地址 printf(\u0026#34;Address of a: %p\\n\u0026#34;, p); // 输出变量a的地址 printf(\u0026#34;Value of a: %d\\n\u0026#34;, *p); // 通过指针访问变量a的值，结果为10 在这个例子中，p存储了 a的地址，而 *p则访问 a的值。\n2. 指针与数组的关系 在C语言中，数组名实际上就是一个指向数组第一个元素的指针。因此，指针和数组之间有非常密切的关系。\n例如：\n1 2 3 4 5 int arr[3] = {10, 20, 30}; int *p = arr; // p指向数组的第一个元素 printf(\u0026#34;%d\\n\u0026#34;, *p); // 输出10，相当于arr[0] printf(\u0026#34;%d\\n\u0026#34;, *(p+1)); // 输出20，相当于arr[1] 这里，p指向 arr[0]，p + 1指向 arr[1]，通过解引用 *(p+1)可以访问数组的第二个元素。\n总结指针与数组的关系：\narr实际上是一个指向 arr[0]的指针； arr[i]等价于 *(arr + i)； 指针可以通过指针运算遍历数组。 3. 指针与函数 在C语言中，指针与函数有着紧密的关系。通过指针，我们可以实现以下两种主要功能：\n函数参数传递：通过传递指针，可以使得函数内部对传递参数的修改在函数外部生效。 指向函数的指针：通过指针来引用和调用函数。 通过指针传递参数 C语言中函数的参数传递是值传递，也就是说，函数接收到的是参数的副本。如果希望函数修改原始参数，可以通过指针来传递参数的地址。\n例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;stdio.h\u0026gt; void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main() { int x = 5, y = 10; swap(\u0026amp;x, \u0026amp;y); // 传递x和y的地址 printf(\u0026#34;x = %d, y = %d\\n\u0026#34;, x, y); // 输出：x = 10, y = 5 return 0; } 在这个例子中，swap函数通过指针修改了 x和 y的值，使得函数外的 x和 y被交换。\n函数指针 指针不仅可以指向变量，还可以指向函数。一个函数指针可以存储某个函数的地址，并通过这个指针调用该函数。\n函数指针的声明语法如下：\n1 return_type (*pointer_name)(parameter_types); 例如，声明一个指向返回 int类型、接受两个 int参数的函数指针：\n1 int (*func_ptr)(int, int); 我们可以将一个函数的地址赋给函数指针，并通过指针调用该函数：\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; int add(int a, int b) { return a + b; } int main() { int (*func_ptr)(int, int) = add; // 将函数add的地址赋给指针 int result = func_ptr(3, 4); // 通过指针调用函数 printf(\u0026#34;Result: %d\\n\u0026#34;, result); // 输出：Result: 7 return 0; } 4. 指针的分类 指针在C语言中有多个分类，每种指针类型在不同的场景下使用。以下是几种常见的指针类型：\n空指针（NULL指针） 空指针是指向**\u0026ldquo;无效\u0026quot;地址**的指针。通常将一个指针赋值为 NULL来表明它不指向任何有效的内存地址。在实际编程中，空指针常用于指针初始化或检查指针是否有效。\n1 int *p = NULL; // p不指向任何有效地址 使用空指针可以避免一些未初始化指针带来的错误。\n悬空指针 悬空指针是指一个指针指向了被释放或不存在的内存地址。它是造成内存访问错误的常见原因。为了避免悬空指针，在释放动态分配的内存后，应该将指针设为 NULL。\n1 2 3 int *p = (int *)malloc(sizeof(int)); // 动态分配内存 free(p); // 释放内存 p = NULL; // 避免悬空指针 野指针 野指针指的是指向未知地址或非法内存地址的指针。野指针的行为是不可预料的，可能导致程序崩溃或产生未定义的行为。在使用指针时，要确保指针指向合法的内存区域。\n5. 指针与动态内存管理 C语言提供了一套标准库函数来进行动态内存分配，它们包括：\nmalloc()：分配指定大小的内存块，返回指向该内存块的指针。如果分配失败，返回 NULL。 calloc()：与 malloc()类似，但会初始化分配的内存为0。 free()：释放之前用 malloc()或 calloc()分配的内存块。 例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int main() { int *p = (int *)malloc(5 * sizeof(int)); // 分配5个int大小的内存 if (p == NULL) { printf(\u0026#34;Memory allocation failed\\n\u0026#34;); return 1; } // 初始化数组 for (int i = 0; i \u0026lt; 5; i++) { p[i] = i * 10; } // 输出数组内容 for (int i = 0; i \u0026lt; 5; i++) { printf(\u0026#34;%d \u0026#34;, p[i]); } free(p); // 释放内存 return 0; } 在这个例子中，malloc函数分配了足够存储5个整数的内存空间，并通过指针 p访问和操作这些内存。\n6. 指针的常见错误 未初始化的指针：指针在使用前必须初始化。如果未初始化就使用指针，可能会导致程序崩溃或产生不确定的行为。 内存泄漏：使用 malloc或 calloc动态分配内存后，必须使用 free释放内存，否则会导致内存泄漏。 解引用空指针：解引用空指针会导致程序崩溃。在解引用指针之前，必须确保指针指向有效的内存区域。 第四部分：结构体、联合体与指针的结合使用 在上一部分中，我们深入讨论了C语言中的指针。现在我们继续探讨结构体和联合体，并进一步学习它们如何与指针结合使用。在C语言中，结构体和联合体是非常重要的数据类型，常用于定义复杂的数据结构，例如链表、树等。\n1. 结构体（Struct） 结构体是一种用户自定义的数据类型，它允许将不同类型的变量组合成一个单一的实体。结构体特别适合用于描述复杂的对象或记录，例如描述一名学生的学号、姓名和成绩等信息。\n结构体的声明 结构体的声明格式如下：\n1 2 3 4 5 struct structure_name { data_type1 member1; data_type2 member2; ... }; 例如，定义一个表示学生信息的结构体：\n1 2 3 4 5 struct Student { int id; // 学号 char name[50]; // 姓名 float grade; // 成绩 }; 结构体变量的定义与访问 定义结构体变量时，可以使用结构体名称。例如：\n1 struct Student s1; // 定义结构体变量s1 访问结构体的成员时使用点运算符（.）：\n1 2 3 s1.id = 1001; strcpy(s1.name, \u0026#34;Alice\u0026#34;); // 将字符串\u0026#34; Alice\u0026#34;复制到name成员 s1.grade = 95.5; 通过这种方式，我们可以操作和访问结构体中的各个成员。\n结构体初始化 结构体变量可以在声明时初始化：\n1 struct Student s1 = {1001, \u0026#34;Alice\u0026#34;, 95.5}; 结构体数组 与基本类型数组类似，我们可以定义结构体数组来存储多个结构体变量。例如，定义一个存储多名学生信息的结构体数组：\n1 2 3 4 5 struct Student students[3] = { {1001, \u0026#34;Alice\u0026#34;, 95.5}, {1002, \u0026#34;Bob\u0026#34;, 89.0}, {1003, \u0026#34;Charlie\u0026#34;, 92.3} }; 2. 结构体与指针 指针可以与结构体结合使用，以更高效地操作结构体，尤其是在函数参数传递和动态内存分配时。\n结构体指针的声明 结构体指针的声明与普通指针相同，只是类型为结构体类型。例如，声明一个指向 Student结构体的指针：\n1 struct Student *p; 我们可以让指针 p指向某个结构体变量的地址：\n1 p = \u0026amp;s1; // p指向结构体变量s1的地址 通过指针访问结构体成员 通过指针访问结构体成员时，不能直接使用点运算符，而是使用箭头运算符（-\u0026gt;）。例如：\n1 2 3 printf(\u0026#34;ID: %d\\n\u0026#34;, p-\u0026gt;id); // 访问结构体成员id printf(\u0026#34;Name: %s\\n\u0026#34;, p-\u0026gt;name); // 访问结构体成员name printf(\u0026#34;Grade: %.2f\\n\u0026#34;, p-\u0026gt;grade); // 访问结构体成员grade 这种方式避免了结构体的大量复制，特别是在传递结构体作为函数参数时，指针更加高效。\n结构体指针与动态内存分配 结构体指针可以与 malloc()函数结合，动态分配结构体内存。例如：\n1 2 3 4 5 6 struct Student *p = (struct Student *)malloc(sizeof(struct Student)); // 动态分配结构体内存 if (p != NULL) { p-\u0026gt;id = 1004; strcpy(p-\u0026gt;name, \u0026#34;David\u0026#34;); p-\u0026gt;grade = 88.5; } 分配完成后，可以像普通结构体指针一样操作和访问结构体成员，使用完成后需要记得释放内存：\n1 free(p); // 释放内存 3. 联合体（Union） 联合体与结构体类似，也是一种自定义的数据类型，但它允许多个成员共用同一块内存空间。联合体中的所有成员共享同一个内存地址，因此在某一时刻，联合体只能存储一个成员的值。\n联合体的声明 联合体的声明格式与结构体类似：\n1 2 3 4 5 union UnionName { data_type1 member1; data_type2 member2; ... }; 例如：\n1 2 3 4 5 union Data { int i; float f; char str[20]; }; 联合体的使用 定义联合体变量时，可以像结构体一样操作。因为所有成员共享同一块内存，修改一个成员的值会覆盖其他成员的值：\n1 2 3 4 5 6 7 union Data d; d.i = 10; printf(\u0026#34;d.i = %d\\n\u0026#34;, d.i); // 输出：d.i = 10 d.f = 3.14; printf(\u0026#34;d.f = %.2f\\n\u0026#34;, d.f); // 输出：d.f = 3.14 printf(\u0026#34;d.i = %d\\n\u0026#34;, d.i); // 输出的d.i值可能是未定义的，因为d.f覆盖了内存 联合体的大小 由于联合体的所有成员共享同一个内存，因此联合体的大小等于它所有成员中最大类型的大小。例如：\n1 2 union Data d; printf(\u0026#34;Size of union: %ld\\n\u0026#34;, sizeof(d)); // 输出联合体的大小，取决于成员中最大的类型 4. 结构体与联合体的比较 特性 结构体 联合体 内存分配 每个成员都有独立的内存空间 所有成员共享同一块内存 使用场景 需要同时存储多个不同类型的数据 需要节省内存且一次只使用一个成员 成员访问 可以同时访问多个成员 只能访问最后一次赋值的成员 大小 总大小等于所有成员的大小之和 大小等于最大成员的大小 5. 嵌套结构体 在C语言中，结构体可以嵌套，即一个结构体的成员可以是另一个结构体。例如：\n1 2 3 4 5 6 7 8 9 10 11 struct Date { int day; int month; int year; }; struct Student { int id; char name[50]; struct Date birthdate; // 嵌套Date结构体 }; 通过这种方式，可以创建更复杂的数据结构。例如：\n1 2 3 4 5 6 struct Student s1; s1.id = 1001; strcpy(s1.name, \u0026#34;Alice\u0026#34;); s1.birthdate.day = 15; s1.birthdate.month = 6; s1.birthdate.year = 2000; 6. 指向结构体数组的指针 指针不仅可以指向单个结构体变量，还可以指向结构体数组。例如：\n1 2 struct Student students[3]; struct Student *p = students; // p指向结构体数组的第一个元素 可以通过指针和下标一起操作结构体数组：\n1 2 3 for (int i = 0; i \u0026lt; 3; i++) { p[i].id = i + 1001; // 通过指针访问数组元素 } 7. 位域（Bit Fields） 位域是一种特殊的结构体成员，用于控制某个成员占用的比特位数。位域用于节省内存空间，通常在需要精确控制内存分配的情况下使用，例如嵌入式系统中。\n位域的声明 位域声明时，需要指定成员占用的位数：\n1 2 3 4 struct { unsigned int a : 4; // a占用4位 unsigned int b : 4; // b占用4位 } bitField; 在这个例子中，a和 b各占用4位，总共占用1个字节。\n第五部分：文件操作与预处理器指令 在前面四部分的基础上，本部分将讨论文件操作和预处理器指令，它们是C语言编程中非常实用且重要的功能。文件操作使得程序可以读写外部文件，而预处理器指令则帮助程序员更灵活地管理代码和进行条件编译。\n1. 文件操作 文件操作是指通过程序与外部文件进行交互，比如读取文件内容或向文件中写入数据。C语言中，文件操作主要通过标准库函数来实现，这些函数定义在 stdio.h头文件中。\n文件指针 在C语言中，文件通过文件指针进行操作。FILE类型是一个结构体，表示文件流，而 FILE *则是文件指针，用来引用和操作文件。\n1 FILE *file_pointer; 打开和关闭文件 使用 fopen()函数来打开文件，并返回一个 FILE *指针。如果文件打开失败，fopen()会返回 NULL。其基本语法如下：\n1 FILE *fopen(const char *filename, const char *mode); filename：文件名。 mode：文件打开模式，有以下几种常见模式： \u0026quot;r\u0026quot;：以只读模式打开文件，文件必须存在。 \u0026quot;w\u0026quot;：以写入模式打开文件，若文件不存在则创建，若文件存在则清空文件内容。 \u0026quot;a\u0026quot;：以追加模式打开文件，文件必须存在。 \u0026quot;r+\u0026quot;：以读写模式打开文件，文件必须存在。 \u0026quot;w+\u0026quot;：以读写模式打开文件，若文件不存在则创建，若文件存在则清空文件内容。 例如：\n1 2 3 4 FILE *fp = fopen(\u0026#34;data.txt\u0026#34;, \u0026#34;r\u0026#34;); // 打开文件data.txt用于读取 if (fp == NULL) { printf(\u0026#34;Error: Could not open file.\\n\u0026#34;); } 当操作完文件后，必须使用 fclose()函数关闭文件，以释放系统资源：\n1 fclose(fp); 读写文件 C语言提供了多种函数用于文件的读写操作，包括单字符、字符串和格式化数据的读写。\n字符读写 fgetc()：从文件中读取一个字符。 fputc()：将一个字符写入文件。 1 2 char ch = fgetc(fp); // 从文件中读取一个字符 fputc(\u0026#39;A\u0026#39;, fp); // 向文件写入字符\u0026#39;A\u0026#39; 字符串读写 fgets()：从文件中读取一行字符串，最多读取 n-1个字符（n是指定的大小），并在末尾添加 '\\0'。 fputs()：向文件写入一个字符串。 1 2 3 char buffer[100]; fgets(buffer, 100, fp); // 从文件中读取一行，存储在buffer中 fputs(\u0026#34;Hello, World!\u0026#34;, fp); // 向文件写入字符串 格式化读写 fprintf()：将格式化的数据写入文件。 fscanf()：从文件中读取格式化的数据。 1 2 fprintf(fp, \u0026#34;ID: %d, Name: %s\\n\u0026#34;, 1001, \u0026#34;Alice\u0026#34;); // 向文件写入格式化数据 fscanf(fp, \u0026#34;%d %s\u0026#34;, \u0026amp;id, name); // 从文件中读取格式化数据 二进制文件操作 对于需要精确控制数据格式的场景，C语言提供了二进制文件的读写函数：\nfread()：从文件中读取二进制数据。 fwrite()：向文件中写入二进制数据。 1 2 3 4 int arr[5] = {1, 2, 3, 4, 5}; fwrite(arr, sizeof(int), 5, fp); // 写入5个整数到文件中 fread(arr, sizeof(int), 5, fp); // 从文件中读取5个整数 文件定位 文件定位函数用于在文件中移动读写指针：\nfseek()：将文件指针移动到指定位置。 ftell()：返回文件指针的当前偏移量。 rewind()：将文件指针重置到文件开头。 1 2 3 fseek(fp, 0, SEEK_SET); // 移动到文件开头 long position = ftell(fp); // 获取当前指针位置 rewind(fp); // 重置到文件开头 2. 预处理器指令 预处理器指令是由**#**号开头的指令，它们在编译器编译代码之前执行预处理工作。常见的预处理器指令包括文件包含、宏定义、条件编译等。\n#include：文件包含 #include指令用于将外部文件的内容包含到当前文件中。常见的有两种包含形式：\n尖括号形式：用于包含标准库文件。 1 #include \u0026lt;stdio.h\u0026gt; 双引号形式：用于包含用户自定义的头文件。 1 #include \u0026#34;myheader.h\u0026#34; #define：宏定义 宏定义用于定义常量或简单的函数。在预处理阶段，所有宏的使用都会被替换为定义的内容。\n定义常量： 1 #define PI 3.14159 使用 PI时，编译器会将其替换为 3.14159。\n宏函数： 1 #define SQUARE(x) ((x) * (x)) 使用 SQUARE(5)时，编译器会将其替换为 ((5) * (5))。\n条件编译 条件编译指令用于控制代码的编译行为，允许编译器根据条件选择性地编译某些代码。\n#if、#elif、#else、#endif：根据条件选择性地编译代码。 1 2 3 4 5 6 7 #define DEBUG 1 #if DEBUG printf(\u0026#34;Debug mode is on.\\n\u0026#34;); #else printf(\u0026#34;Release mode is on.\\n\u0026#34;); #endif #ifdef和 #ifndef：判断某个宏是否被定义。 1 2 3 4 5 6 7 #ifdef PI printf(\u0026#34;PI is defined.\\n\u0026#34;); #endif #ifndef PI printf(\u0026#34;PI is not defined.\\n\u0026#34;); #endif #undef：取消宏定义 使用 #undef可以取消某个宏的定义：\n1 #undef PI #pragma：特殊指令 #pragma指令是编译器特定的指令，用于设置编译器的特性。例如：\n1 #pragma pack(1) // 设置结构体成员的对齐方式为1字节 3. 宏与内联函数的区别 宏在预处理阶段展开，不执行类型检查，可能会导致意外的错误。 内联函数在编译时替换成函数代码，执行类型检查，通常比宏更安全。 例如：\n1 2 #define MAX(a, b) ((a) \u0026gt; (b) ? (a) : (b)) // 宏 inline int max(int a, int b) { return (a \u0026gt; b) ? a : b; } // 内联函数 ","date":"2024-10-12T00:00:00Z","permalink":"https://zuoliangyu.github.io/hugo_blog/p/c%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%9F%BA%E7%A1%80/","title":"C语言入门基础"},{"content":"在系统理论中，判断一个系统是否具备线性、时不变性、因果性、稳定性和记忆性等性质是分析和设计系统的基础。下面将详细说明每种性质的定义以及相应的判断方法。\n1. 线性（Linearity） 定义 线性系统满足叠加原理，即对于任意输入信号\n$$ x_1(t) $$\n和\n$$ x_2(t) $$\n，以及任意常数 $$ a$$ 和 $$\nb$$，系统的响应满足：\n$$ S[a x_1(t) + b x_2(t)] = a S[x_1(t)] + b S[x_2(t)] $$\n判断方法 要判断系统是否线性，可以通过验证叠加原理是否成立。具体步骤包括：\n齐次性（比例性）：\n对于任意输入信号\n$$ x(t) $$\n和任意常数 $$a$$，验证： $$ S[a x(t)] = a S[x(t)] $$\n如果等式成立，系统满足齐次性。\n叠加性：\n对于任意两个输入信号\n$$ x_1(t) $$\n和 $$ x_2(t) $$\n，验证： $$ S[x_1(t) + x_2(t)] = S[x_1(t)] + S[x_2(t)] $$\n如果等式成立，系统满足叠加性。\n如果系统同时满足齐次性和叠加性，则系统是线性的。\n示例 假设系统的输入输出关系为\n$$ y(t) = 2x(t) $$\n。验证线性：\n齐次性： $$ S[a x(t)] = 2(a x(t)) = a (2x(t)) = a S[x(t)] $$\n叠加性： $$ S[x_1(t) + x_2(t)] = 2(x_1(t) + x_2(t)) = 2x_1(t) + 2x_2(t) = S[x_1(t)] + S[x_2(t)] $$\n因此，该系统是线性的。\n2. 时不变性（Time-Invariance） 定义 时不变系统的特性不随时间的推移而改变。即，如果输入信号延迟\n$$ t_0 $$\n后，系统的输出也延迟 $$ t_0 $$\n，则系统是时不变的。数学表达为：\n$$ S[x(t - t_0)] = y(t - t_0) $$\n判断方法 要判断系统是否时不变，可以进行以下步骤：\n输入延迟：\n对输入信号\n$$ x(t) $$\n延迟 $$ t_0 $$\n，得到 $$ x(t - t_0) $$\n。\n系统响应：\n计算系统对延迟后的输入信号的响应 $$ S[x(t - t_0)] $$\n。\n输出延迟：\n将原始输出\n$$ y(t) = S[x(t)] $$\n延迟 $$ t_0 $$\n，得到 $$ y(t - t_0) $$\n。\n比较：\n判断\n$$ S[x(t - t_0)] $$\n是否等于 $$ y(t - t_0) $$\n。\n如果对所有\n$$ t_0 $$\n均成立，则系统是时不变的；否则，系统是时变的。\n示例 考虑系统\n$$ y(t) = x(t) + t $$\n：\n输入延迟后： $$ S[x(t - t_0)] = x(t - t_0) + t $$\n输出延迟： $$ y(t - t_0) = x(t - t_0) + (t - t_0) $$\n显然\n$$ S[x(t - t_0)] \\neq y(t - t_0) $$\n，因此该系统是时变的。\n3. 因果性（Causality） 定义 因果系统的输出在任意时刻\n$$ t$$ 仅依赖于当前及过去的输入值，即 $$\ny(t) $$ 不依赖于未来的 $$\nx(t\u0026rsquo;) $$ （ $$\nt\u0026rsquo; \u0026gt; t$$）。\n判断方法 要判断系统是否因果，可以分析系统的输入输出关系，具体方法包括：\n系统方程分析：\n如果系统的输出\n$$ y(t) $$\n仅由 $$ x(\\tau) $$\n（ $$ \\tau \\leq t $$\n）决定，则系统是因果的。\n脉冲响应：\n对于线性时不变系统，检查脉冲响应\n$$ h(t) $$\n。如果 $$ h(t) = 0 $$\n对于所有 $$ t \u0026lt; 0 $$\n，则系统是因果的。\n系统分类：\n实时系统通常是因果的，而预测系统通常是非因果的。 示例 考虑系统\n$$ y(t) = x(t) + x(t - 1) $$\n：\n输出\n$$ y(t) $$\n仅依赖于当前 $$ x(t) $$\n和过去 $$ x(t - 1) $$\n的值。\n因此，该系统是因果的。\n相反，系统\n$$ y(t) = x(t + 1) $$\n依赖于未来的输入，故为非因果系统。\n4. 稳定性（Stability） 定义 在系统理论中，通常讨论 BIBO 稳定性（Bounded Input, Bounded Output）。即对于所有有界输入\n$$ |x(t)| \\leq M_x $$\n，系统的输出也有界 $$ |y(t)| \\leq M_y $$\n。\n判断方法 判断系统是否 BIBO 稳定的方法包括：\n脉冲响应：\n对于线性时不变系统，若脉冲响应\n$$ h(t) $$\n满足 $$ \\int_{-\\infty}^{\\infty} |h(t)| dt \u0026lt; \\infty $$\n，则系统是 BIBO 稳定的。\n系统方程：\n分析系统的输入输出关系，确保输出不会因为输入的有界性而发散。 极点分析（针对差分方程或传递函数）：\n检查系统的极点位置，确保所有极点位于单位圆内（离散系统）或左半平面（连续系统）。 示例 对于系统\n$$ y(t) = \\int_{-\\infty}^{t} e^{-(t - \\tau)} x(\\tau) d\\tau $$\n：\n脉冲响应\n$$ h(t) = e^{-t}u(t) $$\n（其中 $$ u(t) $$\n为单位阶跃函数）。\n$$\n$$\n\\int_{-\\infty}^{\\infty} |h(t)| dt = \\int_{0}^{\\infty} e^{-t} dt = 1 \u0026lt; \\infty\n$$\n因此，该系统是 BIBO 稳定的。 5. 记忆性（Memory） 定义 记忆性指系统的输出是否仅依赖于当前输入值。若系统的输出 $$y(t)$$ 仅由 $$x(t)$$ 决定，则称系统无记忆（memoryless）；否则，系统有记忆（memory）。\n判断方法 要判断系统是否有记忆，可以分析系统的输入输出关系：\n系统方程：\n若 $$y(t)$$ 仅与 $$x(t)$$ 有关，则系统无记忆。 若 $$y(t)$$ 依赖于 $$x(t\u0026rsquo;)$$ （$$t\u0026rsquo; \\neq t$$），则系统有记忆。 脉冲响应：\n对于线性时不变系统，若脉冲响应 $$h(t)$$ 是一个冲激函数 $$h(t) = k\\delta(t)$$，则系统无记忆。 否则，系统有记忆。 示例 系统 $$y(t) = 3x(t)$$：\n输出仅依赖于当前输入，系统无记忆。 系统 $$y(t) = x(t) + x(t - 1)$$：\n输出依赖于当前和过去的输入，系统有记忆。 $$ ","date":"2024-09-15T00:00:00Z","permalink":"https://zuoliangyu.github.io/hugo_blog/p/%E5%A4%A7%E5%AD%A6%E5%AD%A6%E4%B9%A0/","title":"性质的判断"},{"content":"[toc]\n前言 小小的bb几句 这里我说明一下我个人在学习单片机的时候的一些心得体会，以及大模板的最新建立，会持续更新。\n最基础的模版来自于B站Up Alice_西风的个人空间-Alice_西风个人主页-哔哩哔哩视频 (bilibili.com)\n我的B站为左-岚的个人空间-左-岚个人主页-哔哩哔哩视频 (bilibili.com)\n这篇文章对应着我B站的视频为 【蓝桥杯】【单片机】大模板构建喂饭教程\n建议搭配着视频使用，不然可能有些地方比较迷糊。\n对应的CSDN链接为【喂饭】【速成】蓝桥杯模版手把手建立-CSDN博客\n对应的知乎链接为【喂饭】【速成】蓝桥杯模版手把手建立\n这两个仓库可以删除后面的东西（一直删除到master，可以看到我的全部的东西）\nGithub的仓库为 lanqiaobei_study/模板/demo_zuolan at master · zuoliangyu/lanqiaobei_study (github.com) Gitee的仓库为 模板/demo_zuolan · zuoliangyu/蓝桥杯学习 - 码云 - 开源中国 (gitee.com)\n锁存器与三八译码器介绍 这里我们需要有一个基础知识，就是我们的锁存器和三八译码器介绍，这是一个比较关键的点，我们看到下面这两个图，可以看到我们的锁存器是用P25，P26，P27进行控制，即P2的高三位进行的控制，我们只需要按照如下的命令进行书写即可。\n1 2 P2 = P2 \u0026amp; 0x1f | 0x?0; P2 \u0026amp;= 0x1f; 其中的P2\u0026amp;=0x1f，表示我们将高三位置为0，| 0x?0的操作表明我们对高三位进行书写，比如Y4C，那么就代表我们的高三位要表示为4，即0100，我们将其放在高位，即1000_0000，翻译过来就是0x80\nNOP延时函数 这里要注意一个问题，就是我们延时函数大部分都有NOP，包括Ds1302，IIC都使用了这个，这个函数存在于头文件intrins.h中，需要注意一下引用，一般来说，一个NOP在12MHz的单片机下是1us，其余的自己去换算一下。\nLED，蜂鸣器，继电器，MOTOR模版建立 Led.c 这里虽然名称叫做Led.c，但是里面书写LED，蜂鸣器，继电器，MOTOR的代码\nLed底层 我们可以看到Led的代码，我们可以注意到的是，这个地方使用了static这个关键词，因为我们需要关注内存的使用~~（这里不得不提C51的垃圾内存容量）~~，所以我们将其变为函数静态内存，可以有效节省。\n传入的参数有两个，一个是addr，代表我们需要控制的Led的地址，（0-7）对应了（L1-L8），另一个是enable，用于控制选中的Led灯的亮灭。\n1 void Led_Disp(unsigned char addr, unsigned char enable) 我们定义了两个参数，一个是temp，一个是temp_old，你们可能会好奇，为什么我要在这里用两个变量来进行编写，明明逻辑上只需要用一个变量对P0进行赋值就行了，这里我说明一下，我们一般情况下不会一直操控锁存器，否则有概率影响数码管的显示，当我们状况出现变化（temp!=temp_old）的时候，我们才对锁存器P2的高三位进行书写。当我们需要点亮的时候，就让相应的addr变为1；当我们需要熄灭的时候就让相应的addr变为0；\n这里你可能会有疑问，因为我们在原理图上面如果想让Led亮，那么应该给0，这里做一个说明，我们后面会直接取反，这里用1是为了符合我们的直觉~~（因为正常人就是1亮0灭）~~\n1 2 3 4 5 6 static unsigned char temp = 0x00; static unsigned char temp_old = 0xff; if (enable) temp |= 0x01 \u0026lt;\u0026lt; addr; else temp \u0026amp;= ~(0x01 \u0026lt;\u0026lt; addr); 我们通过查看原理图可以看到，用于控制的Load为Y4C，我们通过计算二进制可以发现结果为1000_0000，即0x80，所以我们在这里代码中的P2设置为P2 = P2 \u0026amp; 0x1f | 0x80;在运行后别忘记使用P2 \u0026amp;= 0x1f来进行高位清零，但是在我们操作P2锁存器之前，我们需要先对P0进行赋值（要记得先取反）\n1 2 3 4 5 6 if (temp != temp_old) { P0 = ~temp; P2 = P2 \u0026amp; 0x1f | 0x80; P2 \u0026amp;= 0x1f; temp_old = temp; } 完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /// @brief Led扫描 /// @param addr 需要控制的Led的地址（0-7） /// @param enable 控制该地址的Led是否点亮 void Led_Disp(unsigned char addr, unsigned char enable) { static unsigned char temp = 0x00; static unsigned char temp_old = 0xff; if (enable) temp |= 0x01 \u0026lt;\u0026lt; addr; else temp \u0026amp;= ~(0x01 \u0026lt;\u0026lt; addr); if (temp != temp_old) { P0 = ~temp; P2 = P2 \u0026amp; 0x1f | 0x80; P2 \u0026amp;= 0x1f; temp_old = temp; } } 蜂鸣器，继电器，MOTOR底层 这里我就一起讲了，因为他们三是一个芯片的不同输出，和上方的Led类似，但是这几个需要一个全局变量（在函数外申明）\n1 2 unsigned char temp_0 = 0x00; unsigned char temp_old_0 = 0xff; 我们可以看到，BUZZ、MOTOR、RELAY为一个芯片的不同位的输出\nRELAY对应I5-\u0026gt;Q4-\u0026gt;D4-\u0026gt;0001_0000-\u0026gt;0x10\nMOTOR对应I6-\u0026gt;Q5-\u0026gt;D5-\u0026gt;0010_0000-\u0026gt;0x20\nBUZZ对应I7-\u0026gt;Q6-\u0026gt;D6-\u0026gt;0100_0000-\u0026gt;0x80\n这里后我们就知道如果我们需要对应的工作，那么就要给P0赋相应的值，并且下方的Y5C也告诉我们，我们需要给锁存器为1010_0000-\u0026gt;0xa0,即P2 = P2 \u0026amp; 0x1f | 0xa0;这个之后也别忘了使用P2 \u0026amp;= 0x1f来进行高位清零。\n1 2 3 4 5 6 if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 \u0026amp; 0x1f | 0xa0; P2 \u0026amp;= 0x1f; temp_old_0 = temp_0; } 总体代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 unsigned char temp_0 = 0x00; unsigned char temp_old_0 = 0xff; /// @brief 蜂鸣器控制 /// @param enable void Beep(bit enable) { if (enable) temp_0 |= 0x40; else temp_0 \u0026amp;= ~(0x40); if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 \u0026amp; 0x1f | 0xa0; P2 \u0026amp;= 0x1f; temp_old_0 = temp_0; } } /// @brief 继电器控制 /// @param enable void Relay(bit enable) { if (enable) temp_0 |= 0x10; else temp_0 \u0026amp;= ~(0x10); if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 \u0026amp; 0x1f | 0xa0; P2 \u0026amp;= 0x1f; temp_old_0 = temp_0; } } /// @brief MOTOR控制 /// @param enable void MOTOR(bit enable) { if (enable) temp_0 |= 0x20; else temp_0 \u0026amp;= ~(0x20); if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 \u0026amp; 0x1f | 0xa0; P2 \u0026amp;= 0x1f; temp_old_0 = temp_0; } } Led.h 申明代码如下\n1 2 3 4 5 #include \u0026lt;STC15F2K60S2.H\u0026gt; void Led_Disp(unsigned char addr, unsigned char enable); void Beep(bit enable); void Relay(bit enable); void MOTOR(bit enable); Key模版建立 key.c 基础代码 在这里我们可以看到原理图里面的行列显示，最新版的原理图把row和col单独拿出来了，所以可能对应有点麻烦~~（其实也不麻烦）~~，如下图所示，我们直接按照一列一列进行扫描，给列低电平后判断指定行是否为低电平，如果指定行为低电平，那么我们就认为这个按键被按下。并且我们在这里不需要实现消抖等操作，我们会在main.c里面实现我们的功能，这里只实现最基础的扫描。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 P44 = 0;P42 = 1;P35 = 1;P34 = 1; if (P33 == 0) temp = 4; if (P32 == 0) temp = 5; if (P31 == 0) temp = 6; if (P30 == 0) temp = 7; P44 = 1;P42 = 0;P35 = 1;P34 = 1; if (P33 == 0) temp = 8; if (P32 == 0) temp = 9; if (P31 == 0) temp = 10; if (P30 == 0) temp = 11; P44 = 1;P42 = 1;P35 = 0;P34 = 1; if (P33 == 0) temp = 12; if (P32 == 0) temp = 13; if (P31 == 0) temp = 14; if (P30 == 0) temp = 15; P44 = 1;P42 = 1;P35 = 1;P34 = 0; if (P33 == 0) temp = 16; if (P32 == 0) temp = 17; if (P31 == 0) temp = 18; if (P30 == 0) temp = 19; 在我们把行列扫描写完后就可以完成工作了，但是，我们要注意一个问题，局部变量一定要注意初始化为0\n1 unsigned char temp = 0; 注意事项 在这里要注意一个问题，我们的串口进行发送的时候会占用P30和P31口，这个是硬件的冲突，所以我们在进入这个扫描函数的时候，需要对其关掉轮询（就是在main函数里面用的什么来进行各个函数定时的定时器，叫做轮询定时器）。这里我用的是定时器1，所以我进入后就要使用ET1=0关掉，结束后用P3=0xff；ET1=1开启。\n1 2 3 4 ET1 = 0; ---- P3 = 0xff; ET1 = 1; 还有一个比较重要的问题，今年（2024）不管是省赛还是国赛都考了ne555的测量，我们ne555的测量要用到P34引脚，也就是我们的按键的最后一列，那么这里就要注意一下，这是一个硬件冲突，且无法解决，所以我们将所有P34删除，就当我们用的按键为4行3列\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 P44 = 0;P42 = 1;P35 = 1; if (P33 == 0) temp = 4; if (P32 == 0) temp = 5; if (P31 == 0) temp = 6; if (P30 == 0) temp = 7; P44 = 1;P42 = 0;P35 = 1; if (P33 == 0) temp = 8; if (P32 == 0) temp = 9; if (P31 == 0) temp = 10; if (P30 == 0) temp = 11; P44 = 1;P42 = 1;P35 = 0; if (P33 == 0) temp = 12; if (P32 == 0) temp = 13; if (P31 == 0) temp = 14; if (P30 == 0) temp = 15; 整体代码如下（没有包括ne555，如果有ne555需要把按键扫描换成上面的）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include \u0026#34;key.h\u0026#34; unsigned char Key_Read() { unsigned char temp = 0; ET1 = 0; P44 = 0;P42 = 1;P35 = 1;P34 = 1; if (P33 == 0) temp = 4; if (P32 == 0) temp = 5; if (P31 == 0) temp = 6; if (P30 == 0) temp = 7; P44 = 1;P42 = 0;P35 = 1;P34 = 1; if (P33 == 0) temp = 8; if (P32 == 0) temp = 9; if (P31 == 0) temp = 10; if (P30 == 0) temp = 11; P44 = 1;P42 = 1;P35 = 0;P34 = 1; if (P33 == 0) temp = 12; if (P32 == 0) temp = 13; if (P31 == 0) temp = 14; if (P30 == 0) temp = 15; P44 = 1;P42 = 1;P35 = 1;P34 = 0; if (P33 == 0) temp = 16; if (P32 == 0) temp = 17; if (P31 == 0) temp = 18; if (P30 == 0) temp = 19; P3 = 0xff; ET1 = 1; return temp; } key.h 1 2 #include \u0026lt;STC15F2K60S2.H\u0026gt; unsigned char Key_Read(); 数码管模板建立 Seg.c 数码管编码推导 这里我单独开一个标题来做数码管的编码推导，因为这个很重要，我们看到下方的截图\n可以观察到每一段的数码管都被编号，这里由于我们用的是共阳极数码管，即0亮1灭，下面我给一个🌰来进行说明\n我们标好后按照dp-\u0026gt;g-\u0026gt;…-\u0026gt;a的顺序写出，即1010_0100-\u0026gt;0xa4，这个值在数码管的显示中就代表着“2”\n所以我们就可以根据这个规律来编写一个段选表\n1 2 code unsigned char seg_dula[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0xff, 0x88}; 数码管代码编写 这是我们最最重要的玩意，因为这个涉及到我们的显示，我们看原理图可以看到，我们需要对数码管进行位选（选择哪一个数码管进行显示），段选（选择数码管显示的数据）。\n其中ABCDEFG和DP为我们的段选，COM1-8为我们的位选\n我们需要先对P0进行段选显示后，使用P2将锁存器打开（Y7C-\u0026gt;1110_0000-\u0026gt;0xe0）;在对P0进行位选配置后，使用P2将锁存器打开（Y6C-\u0026gt;1100_0000-\u0026gt;0xc0）\n经过我们的分析后我们就可以知道我们需要传入的数据为wela位选，dula段选，point是否显示小数点\n1 void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point) 我们为了避免闪烁，所以我们需要先对数码管进行消隐，即\n1 2 3 P0 = 0xff; P2 = P2 \u0026amp; 0x1f | 0xe0; P2 \u0026amp;= 0x1f; 然后我们对位置进行选择后使用P2打开相应锁存器\n1 2 3 P0 = 0x01 \u0026lt;\u0026lt; wela; P2 = P2 \u0026amp; 0x1f | 0xc0; P2 \u0026amp;= 0x1f; 由于我们可以知道，要显示小数点的话，我们就要让DP为0，其他不变，即\u0026amp;0x7f即可完成\n1 2 3 4 P0 = seg_dula[dula]; if (point) P0 \u0026amp;= 0x7f; P2 = P2 \u0026amp; 0x1f | 0xe0; P2 \u0026amp;= 0x1f; 总体代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026#34;seg.h\u0026#34; // 0-9 灭 // A code unsigned char seg_dula[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0xff, 0x88}; void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point) { // 消隐 P0 = 0xff; P2 = P2 \u0026amp; 0x1f | 0xe0; P2 \u0026amp;= 0x1f; // 位选 P0 = 0x01 \u0026lt;\u0026lt; wela; P2 = P2 \u0026amp; 0x1f | 0xc0; P2 \u0026amp;= 0x1f; // 段选 P0 = seg_dula[dula]; if (point) P0 \u0026amp;= 0x7f; P2 = P2 \u0026amp; 0x1f | 0xe0; P2 \u0026amp;= 0x1f; } Seg.h 1 2 #include \u0026lt;STC15F2K60S2.H\u0026gt; void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point); Ds1302时钟模版建立 Ds1302.c 这里有官方提供的参考代码，所以我们上面的时序可以不用书写，我们直接对下方的时间读取/写入进行操作\n官方的参考代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /*\t# DS1302代码片段说明 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。 2. 参赛选手可以自行编写相关代码或以该代码为基础，根据所选单片机类型、运行速度和试题 中对单片机时钟频率的要求，进行代码调试和修改。 */ // void Write_Ds1302(unsigned char temp) { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { SCK = 0; SDA = temp \u0026amp; 0x01; temp \u0026gt;\u0026gt;= 1; SCK = 1; } } // void Write_Ds1302_Byte(unsigned char address, unsigned char dat) { RST = 0; _nop_(); SCK = 0; _nop_(); RST = 1; _nop_(); Write_Ds1302(address); Write_Ds1302(dat); RST = 0; } // unsigned char Read_Ds1302_Byte(unsigned char address) { unsigned char i, temp = 0x00; RST = 0; _nop_(); SCK = 0; _nop_(); RST = 1; _nop_(); Write_Ds1302(address); for (i = 0; i \u0026lt; 8; i++) { SCK = 0; temp \u0026gt;\u0026gt;= 1; if (SDA) temp |= 0x80; SCK = 1; } RST = 0; _nop_(); SCK = 0; _nop_(); SCK = 1; _nop_(); SDA = 0; _nop_(); SDA = 1; _nop_(); return (temp); } 我们需要补充的代码 首先是我们需要绑定SDA，RST，SCK的引脚，在原理图中可以看到，并且要注意一个问题，我们用到了NOP函数，所以需要引入头文件intrins.h\n1 2 3 4 #include \u0026#34;intrins.h\u0026#34; sbit SDA = P2 ^ 3; sbit RST = P1 ^ 3; sbit SCK = P1 ^ 7; 然后就是我们的读写时钟，观察芯片手册可以看到\n我们如果需要读取时钟的话，只需要读取地址0x85-\u0026gt;小时 0x83-\u0026gt;分钟 0x81-\u0026gt;秒钟\n我们如果需要写入时钟的话，需要先将0x8E位置的最高位WP置0，解除写保护后才能进行写入，写入的地址为0x84-\u0026gt;小时 0x82-\u0026gt;分钟 0x80-\u0026gt;秒钟\n所以我们的代码编写就很简单了,我们传入数据为数组的指针\n1 2 void Set_Rtc(unsigned char *ucRtc) void Read_Rtc(unsigned char *ucRtc) 这里给一个调用的示例\n1 2 3 unsigned char time[3] = {11,12,13};//11:12:13 Set_Rtc(time); Read_Rtc(time); 我们在写入之前需要进行解锁写保护，写完后就进行写保护\n1 2 3 Write_Ds1302_Byte(0x8e, 0x00); ----- Write_Ds1302_Byte(0x8e, 0x80); 我们在使用的过程中可以直接使用for循环进行读取/写入，由于我们为了方便，所以我们在这里使用十进制进行存储和读取（本质上存储和读取的都是二进制BCD码，比如0x11我们读取为11，写入为11，在里面内部使用变量进行转换）\n举个🌰，我们假设要写入的时间为23:59:58，那么我们实际上要写入芯片的数据为0x23 0x59 0x58\n但是我们为了方便（因为有时候需要我们是设置时钟，加减的那种，直接使用bcd码不方便，需要我们判断是否达到0xa0然后进行转换），这里我们可以在里面写一个十进制转为十进制bcd码的公式就行，以写入0x23为例，我们输入给我们Set_Rtc函数的数据为23，我们要先将其转换为0x23才能进行写入，那么我们可以观察一下，本质上就是把十位和个位分开，然后再存入即可，我们分开很简单，直接/10和%10就可以取出十位和个位，然后再使用\u0026laquo;4来进行高位移位，将我们的十位移动到高四位去，然后|上我们的个位（低四位）就可以将其拼接完成。\n23-\u0026gt;2,3-\u0026gt;0010,0011-\u0026gt;0100_0011\n1 temp = ucRtc[i] / 10 \u0026lt;\u0026lt; 4 | ucRtc[i] % 10; 同理，我们读取的时候读取到的都是十进制的bcd码，那么我们将其转换后再读出\n0x23-\u0026gt;0x2,0x3-\u0026gt;2*10+3-\u0026gt;23\n1 ucRtc[i] = temp \u0026gt;\u0026gt; 4 * 10 + temp \u0026amp; 0x0f; 读取和写入的代码如下\n1 2 3 4 5 6 7 8 9 10 //写入 for (i = 0; i \u0026lt; 3; i++) { temp = ucRtc[i] / 10 \u0026lt;\u0026lt; 4 | ucRtc[i] % 10; Write_Ds1302_Byte(0x84 - 2 * i, temp); } //读取 for (i = 0; i \u0026lt; 3; i++) { temp = Read_Ds1302_Byte(0x85 - 2 * i); ucRtc[i] = temp \u0026gt;\u0026gt; 4 * 10 + temp \u0026amp; 0x0f; } 需要补充的完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 十进制 11 -\u0026gt; 0x11 void Set_Rtc(unsigned char *ucRtc) { unsigned char i; unsigned char temp; Write_Ds1302_Byte(0x8e, 0x00); for (i = 0; i \u0026lt; 3; i++) { temp = ucRtc[i] / 10 \u0026lt;\u0026lt; 4 | ucRtc[i] % 10; Write_Ds1302_Byte(0x84 - 2 * i, temp); } Write_Ds1302_Byte(0x8e, 0x80); } // 十进制 0x11 -\u0026gt; 11 void Read_Rtc(unsigned char *ucRtc) { unsigned char i; unsigned char temp; for (i = 0; i \u0026lt; 3; i++) { temp = Read_Ds1302_Byte(0x85 - 2 * i); ucRtc[i] = temp \u0026gt;\u0026gt; 4 * 10 + temp \u0026amp; 0x0f; } } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 /*\t# DS1302代码片段说明 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。 2. 参赛选手可以自行编写相关代码或以该代码为基础，根据所选单片机类型、运行速度和试题 中对单片机时钟频率的要求，进行代码调试和修改。 */ #include \u0026#34;ds1302.h\u0026#34; #include \u0026#34;intrins.h\u0026#34; sbit SDA = P2 ^ 3; sbit RST = P1 ^ 3; sbit SCK = P1 ^ 7; // void Write_Ds1302(unsigned char temp) { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { SCK = 0; SDA = temp \u0026amp; 0x01; temp \u0026gt;\u0026gt;= 1; SCK = 1; } } // void Write_Ds1302_Byte(unsigned char address, unsigned char dat) { RST = 0; _nop_(); SCK = 0; _nop_(); RST = 1; _nop_(); Write_Ds1302(address); Write_Ds1302(dat); RST = 0; } // unsigned char Read_Ds1302_Byte(unsigned char address) { unsigned char i, temp = 0x00; RST = 0; _nop_(); SCK = 0; _nop_(); RST = 1; _nop_(); Write_Ds1302(address); for (i = 0; i \u0026lt; 8; i++) { SCK = 0; temp \u0026gt;\u0026gt;= 1; if (SDA) temp |= 0x80; SCK = 1; } RST = 0; _nop_(); SCK = 0; _nop_(); SCK = 1; _nop_(); SDA = 0; _nop_(); SDA = 1; _nop_(); return (temp); } // 十进制 11 -\u0026gt; 0x11 void Set_Rtc(unsigned char *ucRtc) { unsigned char i; unsigned char temp; Write_Ds1302_Byte(0x8e, 0x00); for (i = 0; i \u0026lt; 3; i++) { temp = ucRtc[i] / 10 \u0026lt;\u0026lt; 4 | ucRtc[i] % 10; Write_Ds1302_Byte(0x84 - 2 * i, temp); } Write_Ds1302_Byte(0x8e, 0x80); } // 十进制 0x11 -\u0026gt; 11 void Read_Rtc(unsigned char *ucRtc) { unsigned char i; unsigned char temp; for (i = 0; i \u0026lt; 3; i++) { temp = Read_Ds1302_Byte(0x85 - 2 * i); ucRtc[i] = temp \u0026gt;\u0026gt; 4 * 10 + temp \u0026amp; 0x0f; } } Ds1302.h 1 2 3 #include \u0026lt;STC15F2K60S2.H\u0026gt; void Set_Rtc(unsigned char *ucRtc); void Read_Rtc(unsigned char *ucRtc); Onewire温度模版建立 Onewire.c 官方的参考代码 这里有官方提供的参考代码，所以我们上面的时序可以不用书写，我们直接对下方的进行温度读取操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 /*\t# 单总线代码片段说明 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。 2. 参赛选手可以自行编写相关代码或以该代码为基础，根据所选单片机类型、运行速度和试题 中对单片机时钟频率的要求，进行代码调试和修改。 */ // void Delay_OneWire(unsigned int t) { unsigned char i; while (t--) { for (i = 0; i \u0026lt; 12; i++); } } // void Write_DS18B20(unsigned char dat) { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { DQ = 0; DQ = dat \u0026amp; 0x01; Delay_OneWire(5); DQ = 1; dat \u0026gt;\u0026gt;= 1; } Delay_OneWire(5); } // unsigned char Read_DS18B20(void) { unsigned char i; unsigned char dat; for (i = 0; i \u0026lt; 8; i++) { DQ = 0; dat \u0026gt;\u0026gt;= 1; DQ = 1; if (DQ) { dat |= 0x80; } Delay_OneWire(5); } return dat; } // bit init_ds18b20(void) { bit initflag = 0; DQ = 1; Delay_OneWire(12); DQ = 0; Delay_OneWire(80); DQ = 1; Delay_OneWire(10); initflag = DQ; Delay_OneWire(5); return initflag; } 我们需要补充的代码 首先是我们需要绑定DQ的引脚，在原理图中可以看到\n1 sbit DQ = P1 ^ 4; 我们不需要管上方的时序的书写，只需要进行温度读取的代码书写就行，\n下方是温度芯片的使用顺序（官方手册），首先我们需要运行init，然后再检查rom（可跳过），最后执行我们的功能函数，注意，一次只能使用一个功能。\n我们可以在官方手册上找到需要运行的指令\n当我们发送0xcc的时候，就会跳过ROM检查，并且这里也说明了，我们发送0x44就可以开启温度转换，发送0xbe就可以开始读取温度，但是要注意一个事情，我们同样可以在我们的官方的手册上面找到一段读取的语句，这里表明我们先读取低位，再读取高位，最后将其拼接后再*精度就是我们的数据了\n温度的精度在文档中对应如下\n精度为9-\u0026gt;0.5;10-\u0026gt;0.25;11-\u0026gt;0.125;12（默认）-\u0026gt;0.0625\n我们就可以根据上面的描述来进行代码书写\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 float rd_temperature() { unsigned char low, high; //开启温度转换 init_ds18b20(); Write_DS18B20(0xcc); Write_DS18B20(0x44); Delay_OneWire(200); //开启温度读取 init_ds18b20(); Write_DS18B20(0xcc); Write_DS18B20(0xbe); low = Read_DS18B20(); high = Read_DS18B20(); return (float)(high \u0026lt;\u0026lt; 8 | low) * 0.0625; } 这里给一个调用示例\n1 float temp = rd_temperature(); 完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 /*\t# 单总线代码片段说明 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。 2. 参赛选手可以自行编写相关代码或以该代码为基础，根据所选单片机类型、运行速度和试题 中对单片机时钟频率的要求，进行代码调试和修改。 */ #include \u0026#34;onewire.h\u0026#34; sbit DQ = P1 ^ 4; // void Delay_OneWire(unsigned int t) { unsigned char i; while (t--) { for (i = 0; i \u0026lt; 12; i++); } } // void Write_DS18B20(unsigned char dat) { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { DQ = 0; DQ = dat \u0026amp; 0x01; Delay_OneWire(5); DQ = 1; dat \u0026gt;\u0026gt;= 1; } Delay_OneWire(5); } // unsigned char Read_DS18B20(void) { unsigned char i; unsigned char dat; for (i = 0; i \u0026lt; 8; i++) { DQ = 0; dat \u0026gt;\u0026gt;= 1; DQ = 1; if (DQ) { dat |= 0x80; } Delay_OneWire(5); } return dat; } // bit init_ds18b20(void) { bit initflag = 0; DQ = 1; Delay_OneWire(12); DQ = 0; Delay_OneWire(80); DQ = 1; Delay_OneWire(10); initflag = DQ; Delay_OneWire(5); return initflag; } float rd_temperature() { unsigned char low, high; init_ds18b20(); Write_DS18B20(0xcc); Write_DS18B20(0x44); Delay_OneWire(200); init_ds18b20(); Write_DS18B20(0xcc); Write_DS18B20(0xbe); low = Read_DS18B20(); high = Read_DS18B20(); return (float)(high \u0026lt;\u0026lt; 8 | low) * 0.0625; } Onewire.h 1 2 #include \u0026lt;STC15F2K60S2.H\u0026gt; float rd_temperature(); IIC（PCF8591(AD/DA)与AT24C02(EEPROM)）模版建立 iic.c 官方的参考代码 这里有官方提供的参考代码，所以我们上面的时序可以不用书写，我们直接进行AD读取与DA输出还有AT24C02的存储操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 /*\t# I2C代码片段说明 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。 2. 参赛选手可以自行编写相关代码或以该代码为基础，根据所选单片机类型、运行速度和试题 中对单片机时钟频率的要求，进行代码调试和修改。 */ #define DELAY_TIME 10 // static void I2C_Delay(unsigned char n) { do { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); } while (n--); } // void I2CStart(void) { sda = 1; scl = 1; I2C_Delay(DELAY_TIME); sda = 0; I2C_Delay(DELAY_TIME); scl = 0; } // void I2CStop(void) { sda = 0; scl = 1; I2C_Delay(DELAY_TIME); sda = 1; I2C_Delay(DELAY_TIME); } // void I2CSendByte(unsigned char byt) { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { scl = 0; I2C_Delay(DELAY_TIME); if (byt \u0026amp; 0x80) { sda = 1; } else { sda = 0; } I2C_Delay(DELAY_TIME); scl = 1; byt \u0026lt;\u0026lt;= 1; I2C_Delay(DELAY_TIME); } scl = 0; } // unsigned char I2CReceiveByte(void) { unsigned char da; unsigned char i; for (i = 0; i \u0026lt; 8; i++) { scl = 1; I2C_Delay(DELAY_TIME); da \u0026lt;\u0026lt;= 1; if (sda) da |= 0x01; scl = 0; I2C_Delay(DELAY_TIME); } return da; } // unsigned char I2CWaitAck(void) { unsigned char ackbit; scl = 1; I2C_Delay(DELAY_TIME); ackbit = sda; scl = 0; I2C_Delay(DELAY_TIME); return ackbit; } // void I2CSendAck(unsigned char ackbit) { scl = 0; sda = ackbit; I2C_Delay(DELAY_TIME); scl = 1; I2C_Delay(DELAY_TIME); scl = 0; sda = 1; I2C_Delay(DELAY_TIME); } 我们需要补充的代码 首先是我们需要绑定sda和scl的引脚，在原理图中可以看到，并且要注意一个问题，我们用到了NOP函数，所以需要引入头文件intrins.h\n1 2 3 #include \u0026#34;intrins.h\u0026#34; sbit scl = P2 ^ 0; sbit sda = P2 ^ 1; 我在这里需要说明一下IIC通信的方式，他是一个一个八位的数据，其中第一位是我们的发送地址，永远都是1，然后后面挂载了不同的外设，我们的蓝桥杯的板子挂了两个外设，一个是PCF8591，对应着AD/DA的功能；另一个是AT24C02，对应着EEPROM的存储功能。在我们板子上就长我们下面这个样子~~（我画的有点抽象）~~，所以我们就可以知道，如果我们要对AT24C02芯片写入数据的话，我们就需要使用地址为（1010_0000-\u0026gt;0xa0），要读取数据的化，我们就要使用地址为（1010_0001-\u0026gt;0xa1）;要写入DA输出的数据的话，我们就需要使用地址为（1001_0000-\u0026gt;0x90），要读取AD采样的数据的话，我们就需要使用地址为（1001_0001-\u0026gt;0x91）\n在我们使用IIC进行通信的时候，我们要遵循下面的运行逻辑\n1 2 3 4 5 I2CStart(); I2CSendByte(地址); I2CWaitAck();\tI2CSendByte(功能); I2CWaitAck();\tAD与DA 从上面说明的内容的我们可以知道，如果我们要对PCF8591进行操作，那么就需要对地址0x90和0x91进行操作\n我们现在先说一下AD的代码，通过观察原理图我们可以发现，AD有4个通道可以进行读取\n其中AIN0通道是我们的外部测量通道（由于蓝桥杯现在还没有涉及到信号发生器，所以我们可以先暂时不用管这个通道）\nAIN1通道很明显就是光明电阻的分压读取（光照越强，分压越高，采集到的结果越大），\nAIN2通道为差分输入（我们也是不常用到的输入，因为没有信号发生器）\nAIN3通道为滑动变阻器输入（我们测量滑动变阻器的分压，这个你左右旋钮看一下就知道哪边变大了）\n我们查看一下PCF8591的手册可以看到，我们读取的地址的书写方式，这里我统一给一个说明\n位次 情况说明 情况 第一位 固定位 0 第二位 是否需要开启DA输出 开启DA输出（1）\n不开启DA输出（0） 第三四位 选择输入通道的模式\n差分输入具体要看下面的图 通道与编号对应输入（常用）（00）\n通道差分输入（01,10,11） 第五位 固定位 0 第六位 自动递增标志\n（用于一个一个读取AD输入口） 自我递增（没考过，因为我们一般都是单一读取）（1）\n不自我递增（0） 第七八位 用于指定我们读取的通道 通道0（00），在我们正常的对应输入下为外部信号输入\n通道1（01），在我们正常的对应输入下为光明电阻分压输入\n通道2（10），在我们正常的对应输入下为差分信号输入\n通道3，在我们正常的对应输入下为滑动变阻器分压输入（11） 我们那么这个时候常考的就是下面的形式，注意一下题目中是否需求DA输出，如果需要DA输出的话，这里使用地址的时候必须使用允许DA输出，\n需要的数据 输入的地址 外部电压输入（开启DA） 0100_0000-\u0026gt;0x40 外部电压输入（不开启DA） 0000_0000-\u0026gt;0x00 光敏电阻分压输入（开启DA） 0100_0001-\u0026gt;0x41 光敏电阻分压输入（不开启DA） 0000_0001-\u0026gt;0x01 差分信号输入（开启DA） 0100_0010-\u0026gt;0x42 差分信号输入（不开启DA） 0000_0010-\u0026gt;0x02 滑动变阻器分压输入（开启DA） 0100_0011-\u0026gt;0x43 滑动变阻器分压输入（不开启DA） 0000_0011-\u0026gt;0x03 有了这样一个基础，我们就可以开始着手编写我们的AD代码了\n我们只需要指定一下我们需要读取的通道的地址就行\n1 unsigned char Ad_Read(unsigned char addr) 这里给一个调用示例，这里需要注意的是，如果你同时读取光敏电阻和滑动变阻器的值，他们会地址交换，你们可以自己去试试。\n1 unsigned char temp = Ad_Read(0x01);//读取光敏电阻结果 首先我们需要选中我们的PCF芯片并且写入我们需要读取的地址,这里需要注意的就只有每次发送Byte后我们需要Wait一下\n1 2 3 4 5 6 // 选择芯片为PCF I2CStart(); I2CSendByte(0x90); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); 然后重新开启一下通话（因为我们上面是进行写操作，现在要进行读操作），读取后我们需要发送一个Ack为1，表明我们这边接收数据完成，后续不用接受了，结束后就可以关闭对话Stop了\n1 2 3 4 5 6 I2CStart(); I2CSendByte(0x91); I2CWaitAck(); temp = I2CReceiveByte(); I2CSendAck(1); I2CStop(); AD完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 unsigned char Ad_Read(unsigned char addr) { unsigned char temp; // 选择芯片为PCF I2CStart(); I2CSendByte(0x90); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); I2CStart(); I2CSendByte(0x91); I2CWaitAck(); temp = I2CReceiveByte(); I2CSendAck(1); I2CStop(); return temp; } 说完AD我们就要说到DA了\n和AD一样，我们只需要传入我们需要写入的数据就行，但是要注意一下我们的数据为数字电压，即0-255，我们输出后的电压是模拟电压，即0-5V，所以我们是一一映射过去的\n1 void Da_Write(unsigned char dat) 举个🌰，我们如果想要输出电压为2V,那么我们就应该\n1 Da_Write(2*51); 如果想要输出的电压为2.5V，那么我们就应该，这里不用关心他会出现不匹配的情况，C语言底层会自动转换为unsigned char类型的,这类有啊注意\n1 Da_Write(2.5*51); 首先我们需要选中我们的PCF芯片并且给他我们的地址信息（前面提到过如果要开启DA输出，那么我们应该写入的地址）,这里需要注意的就只有每次发送Byte后我们需要Wait一下，这里的0x41可以更换为0x40-0x43的任何一个，但是最好和题目中要求AD需要读取的通道一致（这里也要注意在你读取AD的时候加上0x4）\n1 2 3 4 5 6 7 8 9 10 void Da_Write(unsigned char dat) { // 选择芯片为PCF I2CStart(); I2CSendByte(0x90); I2CWaitAck(); I2CSendByte(0x41); I2CWaitAck(); I2CSendByte(dat); I2CWaitAck(); } AD与\nEEPROM 从上面说明的内容的我们可以知道，如果我们要对AT24C02进行操作，那么就需要对地址0xa0和0xa1进行操作\n本质上我们的EEPROM写入与读取和AD/DA是一致的，但是我们需要注意一个点，因为我们通常写入/读取的不止一个数据，我们一般是一个数组写进去和读取，所以我们就需要注意这个点，我们的操作如下，传递的数据都是数组的首地址（如果存单个数据只需要使用\u0026amp;name就行），传入参数为需要写入/读取数组首地址名称））str，需要写入/读取的数组地址addr，数组的大小num\n1 2 void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) 这里给一个调用的示例，需要注意一下如果写入为unsigned int，可以不用分为高低位存储，但是需要分为高低位读取\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 unsigned char arr_input[5]={1,2,3,4,5}; unsigned char arr_output[5]; EEPROM_Write(arr_input,0,5); EEPROM_Read(arr_output,0,5); unsigned char input = 5; unsigned char output; EEPROM_Write(\u0026amp;input,0,1); EEPROM_Read(\u0026amp;output,0,1); unsigned int input = 5; unsigned int output; unsigned char high,low; EEPROM_Write(\u0026amp;input,0,2); EEPROM_Read(\u0026amp;high,0,1); EEPROM_Read(\u0026amp;low,1,1); output = high\u0026lt;\u0026lt;4 | low; 现在我来说一下实现细节\n先说明一下EEPROM_Read函数，和上文的AD读取一样，我们需要先选中AT24C02，并且功能为写，然后进行位置的传输，确认我们需要读取的地址\n1 2 3 4 5 I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); 在我们读取的时候，和上文AD一样，我们需要重新开启一个对话进行读取\n1 2 3 I2CStart(); I2CSendByte(0xa1); I2CWaitAck(); 但是在我们读取的时候需要注意一个点，我们使用的数组进行的数据读取，并且在我们读取的时候对指针进行++操作，当我们还没有读完数据的时候，我们发送Ack为0，代表我们需要继续读取，当我们读取数据结束后，我们发送Ack为1，表示读取结束，最后也别忘记需要进行Stop停止交流。\n1 2 3 4 5 6 7 8 9 10 11 I2CStart(); I2CSendByte(0xa1); I2CWaitAck(); while (num--) { *str++ = I2CReceiveByte(); if (num) I2CSendAck(0); else I2CSendAck(1); } I2CStop(); 在这里说明一下*str++代表的含义，这是c的一个写法，等价于下面的代码\n1 2 3 4 5 6 7 8 9 unsigned char arr[5]; unsigned char i = 0; unsigned char num = 5; while(num--) { arr[i] = I2CReceiveByte(); i++; ----- } 完整的代码EEPROM_READ如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) { I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); I2CStart(); I2CSendByte(0xa1); I2CWaitAck(); while (num--) { *str++ = I2CReceiveByte(); if (num) I2CSendAck(0); else I2CSendAck(1); } I2CStop(); } 我们可以从上面看到，AD和EEPROM_READ可以进行类比；同理，我们的DA和EEPROM_Write也可以进行类比，但是EEPROM_Write有一个需要注意的点，我会在下面进行说明\n我们第一步还是选择我们的芯片，选择我们需要写入的地址\n1 2 3 4 5 I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); 第二步就是使用一个循环进行写入，这里需要注意一个问题，就是我们每写入一个数据，都需要I2C_Delay一下，注意不是Delay，而是I2C_Delay，经过我们测试，当这个值为200 的时候效果是可以的，并且这里有个最最重要的点，在我们Stop结束后，我们需要进行4个255时间的I2C_Delay~~（其实我在群里面说过，但你们一直觉得我在讲乐子）~~\n1 2 3 4 5 6 7 8 9 10 while (num--) { I2CSendByte(*str++); I2CWaitAck(); I2C_Delay(200); } I2CStop(); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); 完整的代码EEPROM_Write如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) { I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); while (num--) { I2CSendByte(*str++); I2CWaitAck(); I2C_Delay(200); } I2CStop(); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); } 完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 /*\t# I2C代码片段说明 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。 2. 参赛选手可以自行编写相关代码或以该代码为基础，根据所选单片机类型、运行速度和试题 中对单片机时钟频率的要求，进行代码调试和修改。 */ #include \u0026#34;iic.h\u0026#34; #include \u0026#34;intrins.h\u0026#34; #define DELAY_TIME 5 sbit scl = P2 ^ 0; sbit sda = P2 ^ 1; // static void I2C_Delay(unsigned char n) { do { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); } while (n--); } // void I2CStart(void) { sda = 1; scl = 1; I2C_Delay(DELAY_TIME); sda = 0; I2C_Delay(DELAY_TIME); scl = 0; } // void I2CStop(void) { sda = 0; scl = 1; I2C_Delay(DELAY_TIME); sda = 1; I2C_Delay(DELAY_TIME); } // void I2CSendByte(unsigned char byt) { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { scl = 0; I2C_Delay(DELAY_TIME); if (byt \u0026amp; 0x80) { sda = 1; } else { sda = 0; } I2C_Delay(DELAY_TIME); scl = 1; byt \u0026lt;\u0026lt;= 1; I2C_Delay(DELAY_TIME); } scl = 0; } // unsigned char I2CReceiveByte(void) { unsigned char da; unsigned char i; for (i = 0; i \u0026lt; 8; i++) { scl = 1; I2C_Delay(DELAY_TIME); da \u0026lt;\u0026lt;= 1; if (sda) da |= 0x01; scl = 0; I2C_Delay(DELAY_TIME); } return da; } // unsigned char I2CWaitAck(void) { unsigned char ackbit; scl = 1; I2C_Delay(DELAY_TIME); ackbit = sda; scl = 0; I2C_Delay(DELAY_TIME); return ackbit; } // void I2CSendAck(unsigned char ackbit) { scl = 0; sda = ackbit; I2C_Delay(DELAY_TIME); scl = 1; I2C_Delay(DELAY_TIME); scl = 0; sda = 1; I2C_Delay(DELAY_TIME); } unsigned char Ad_Read(unsigned char addr) { unsigned char temp; // 选择芯片为PCF I2CStart(); I2CSendByte(0x90); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); I2CStart(); I2CSendByte(0x91); I2CWaitAck(); temp = I2CReceiveByte(); I2CSendAck(1); I2CStop(); return temp; } // 数字电压255-\u0026gt;5V void Da_Write(unsigned char dat) { // 选择芯片为PCF I2CStart(); I2CSendByte(0x90); I2CWaitAck(); I2CSendByte(0x41); I2CWaitAck(); I2CSendByte(dat); I2CWaitAck(); } void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) { I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); while (num--) { I2CSendByte(*str++); I2CWaitAck(); I2C_Delay(200); } I2CStop(); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); } void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) { I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); I2CStart(); I2CSendByte(0xa1); I2CWaitAck(); while (num--) { *str++ = I2CReceiveByte(); if (num) I2CSendAck(0); else I2CSendAck(1); } I2CStop(); } iic.h 1 2 3 4 5 #include \u0026lt;STC15F2K60S2.H\u0026gt; unsigned char Ad_Read(unsigned char addr); void Da_Write(unsigned char dat); void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num); void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num); Uart串口模版建立 uart.c 在这里我们需要注意，串口官方是没有给底层的，我们需要自己手搓，在这里我给的代码为重定向代码。\n首先是使用isp生成一个用定时器2计时的串口1的代码，波特率为9600（比赛的时候可能不是9600，要根据题目来看），定时器时钟选用12T，数据位为8位\n我们直接把他复制到我们代码里面就行，最后别忘记加一个EA=1来开启总中断，还有些isp版本可能比较老，没有使能中断的选项，所以需要自己加上ES=1\n1 2 3 4 5 6 7 8 9 10 11 void Uart1_Init(void) // 9600bps@12.000MHz { SCON = 0x50; // 8位数据,可变波特率 AUXR |= 0x01; // 串口1选择定时器2为波特率发生器 AUXR |= 0x04; // 定时器时钟1T模式 T2L = 0xC7; // 设置定时初始值 T2H = 0xFE; // 设置定时初始值 AUXR |= 0x10; // 定时器2开始计时 ES = 1; // 使能串口1中断 EA = 1; } 我们这里不需要实现复杂的函数，只需要实现一个重定向就行，使用printf直接将数据发送到串口就行，我现在说一下步骤\n我们通过查看c51里面（注意是c51，不是标准的c语言函数库，直接在keil里面右键打开），我们可以看到下方有一个putchar的一个extern的函数定义，这就是我们需要重载的函数，因为我们printf本质上就是调用putchar的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 /*-------------------------------------------------------------------------- STDIO.H Prototypes for standard I/O functions. Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc. All rights reserved. --------------------------------------------------------------------------*/ #ifndef __STDIO_H__ #define __STDIO_H__ #ifndef EOF #define EOF -1 #endif #ifndef NULL #define NULL ((void *) 0) #endif #ifndef _SIZE_T #define _SIZE_T typedef unsigned int size_t; #endif #pragma SAVE #pragma REGPARMS extern char _getkey (void); extern char getchar (void); extern char ungetchar (char); extern char putchar (char); extern int printf (const char *, ...); extern int sprintf (char *, const char *, ...); extern int vprintf (const char *, char *); extern int vsprintf (char *, const char *, char *); extern char *gets (char *, int n); extern int scanf (const char *, ...); extern int sscanf (char *, const char *, ...); extern int puts (const char *); #pragma RESTORE #endif 我们可以知道，串口在发送的时候，会将数据放进缓冲区SBUF（如果你用串口2那就是S2BUF，但是这里没涉及到，就不用管了），当我们正在发送的时候TI标志位为0，当我们发送完成后，他就会被置为0，然后我们需要将ch返回（这个是函数本身就需要返回的char）\n1 2 3 4 5 6 extern char putchar(char ch) { SBUF = ch; while (TI == 0); TI = 0; return ch; } uart.h 1 2 3 4 #include \u0026lt;STC15F2K60S2.H\u0026gt; #include \u0026#34;stdio.h\u0026#34; void Uart1_Init(void); Ul超声波模版建立 ul.c 在这里我们需要注意，串口官方是没有给底层的，我们需要自己手搓。我们通过查询原理图可以看到，超声波的T为P10，R为P11，所以我们需要定义一下\n1 2 sbit Tx = P1 ^ 0; sbit Rx = P1 ^ 1; 现在我们来进行逻辑的书写，我们首先要知道超声波测距的原理，我发送数据后，等待接收数据，那么我通过计算发送和接收中的间隔时间，就可以得到我们的距离，这里用数学公式可以进行计算，一般来说声速都是340m/s（也不排除题目里面和你说要对其进行变换啥的【比如第十四届国赛】，这里就按照声速为340m/s来进行说明）\n$$ d = \\frac{vt}{2} = \\frac{340 \\text{ m/s} \\times t}{2} = 170 \\text{ m/s} \\times t = 1.7 \\times 10^4 \\text{ cm/s} \\times t $$\n我们这里有一个单位换算需要进行注意，t的单位为us，也就是 $$ {10^{ - 6}}s $$ 我们进行完整的单位换算就可以知道d的距离了（单位为cm） $$ d = 1.7*{10^4}*{10^{ - 6}}t = 0.017t $$\n图来自微信群友chmod-wrx绘制（我绘图太🌶︎🐔了）\n根据我们上方的分析，我们可以知道我们首先需要进行发送，我们在这里发送8个40kHz的方波进行测量，那么周期就是 $$ {1 \\over {40k}} = 25 \\cdot {10^{ - 6}}s = 25us $$ 也就是我们需要 $ 12us $ 将发送的电平进行反转\n代码如下，我们首先使用isp生成一个12us的延时函数,这里需要注意的是要正确使用系统频率为12MHz；如果这里测量结果有问题，可以尝试将33修改为38或者其他的数据，因为软件延时不准。\n1 2 3 4 5 6 7 8 9 void Delay12us(void)\t//@12.000MHz { unsigned char data i; _nop_(); _nop_(); i = 33;//可以修改为其他数据，比如38 while (--i); } 然后就是我们的超声波发送函数，发送8个4kHz的方波\n1 2 3 4 5 6 7 8 9 void Ut_Wave_Init() { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { Tx = 1; Delay12us(); Tx = 0; Delay12us(); } } 最后是我们最重要的数据接收与转换函数的书写，在这里我们使用的pca定时器，可以节省定时器来进行其他的操作（因为ne555强制占用定时器0，串口考到就会占用一个定时器，还有一个定时器需要用来进行函数轮询，所以我们就必须用其他的定时器进行超声波的操作）,其实PCA本质上还是一个定时器，所以他也存在一些定时器的参数，比如TLx-\u0026gt;CL;THx-\u0026gt;CH;TMOD-\u0026gt;CMOD;TRx-\u0026gt;CR;TFx-\u0026gt;CF\n我们首先需要对定时器的CH和CL清零，为了我们计数做准备；然后将CMOD配置为0x00，十六位不重载定时器。\n1 2 CH = CL = 0; CMOD = 0x00; 在我们发送超声波的时候，我们需要将总中断关闭，在我们发送完毕后就关掉，在我们发送完成后，就打开CR开始计时，等待我们收到回应（收到回应后Rx为0）或者计数溢出（溢出后CF为1），我们就关闭CR开始读取数据\n1 2 3 4 5 6 7 EA = 0; Ut_Wave_Init(); EA = 1; CR = 1; while (Rx \u0026amp;\u0026amp; !CF); CR = 0; 如果我们是因为收到回应而结束（那么就代表我们测量到了数据），那么我们就进行时间读取，如下所示，你们可能会好奇为什么+3，这是因为我们实际测量发现，+3后在远程数据（测量结果\u0026gt;10的时候）误差会比较小，而近程数据（测量数据\u0026lt;10）满足误差范围，我们也不可能测量得到0这个数据（题目中一般都不会测到0）（所以我们才把他视为测量错误的返回结果）\n1 2 time = CH \u0026lt;\u0026lt; 8 | CL; return (0.017 * time + 3); 如果我们是因为计数溢出（没有收到返回的信号，代表测量错误），我们就直接返回为0，表示错误结果\n1 return 0; 转换的完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned char Ut_Wave_Data() { unsigned int time; CH = CL = 0; CMOD = 0x00; EA = 0; Ut_Wave_Init(); EA = 1; CR = 1; while (Rx \u0026amp;\u0026amp; !CF); CR = 0; if (!CF) { // us -\u0026gt; s 10^(-6) // m -\u0026gt; cm 10^2 // 10^(-4) // L = V*T/2=340*time/2=170*10^(-4)*time=0.017*time time = CH \u0026lt;\u0026lt; 8 | CL; return (0.017 * time + 3); } else { CF = 0; return 0; } } 完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include \u0026#34;ul.h\u0026#34; #include \u0026#34;intrins.h\u0026#34; sbit Tx = P1 ^ 0; sbit Rx = P1 ^ 1; void Delay12us(void) //@12.000MHz { unsigned char data i; _nop_(); _nop_(); i = 33; // 38 while (--i); } void Ut_Wave_Init() { unsigned char i; for (i = 0; i \u0026lt; 8; i++) { Tx = 1; Delay12us(); Tx = 0; Delay12us(); } } unsigned char Ut_Wave_Data() { unsigned int time; CH = CL = 0; CMOD = 0x00; EA = 0; Ut_Wave_Init(); EA = 1; CR = 1; while (Rx \u0026amp;\u0026amp; !CF); CR = 0; if (!CF) { // us -\u0026gt; s 10^(-6) // m -\u0026gt; cm 10^2 // 10^(-4) // L = V*T/2=340*time/2=170*10^(-4)*time=0.017*time time = CH \u0026lt;\u0026lt; 8 | CL; return (0.017 * time + 3); } else { CF = 0; return 0; } } ul.h 1 2 #include \u0026lt;STC15F2K60S2.H\u0026gt; unsigned char Ut_Wave_Data(); init初始化模版建立 init.c 我们在启动开发板的时候最好对其进行初始化，关闭蜂鸣器和继电器，MOTOR和全部LED灯，避免上电情况有误\n1 2 3 4 5 6 7 8 9 10 #include \u0026#34;init.h\u0026#34; void System_Init() { P0 = 0xff; P2 = P2 \u0026amp; 0x1f | 0x80; P2 \u0026amp;= 0x1f; P0 = 0x00; P2 = P2 \u0026amp; 0x1f | 0xa0; P2 \u0026amp;= 0x1f; } init.h 1 2 #include \u0026lt;STC15F2K60S2.H\u0026gt; void System_Init(); main主函数书写 main.c 现在我们来进行主函数的编写，我也拆分为模块，但是最后还是会给一个整体的代码\n全部变量的归纳 这里我把全部的全局变量放上来\n定义了Led的变量数组，用于控制8个灯的亮灭情况 定义了Seg_Pos的变量，用于控制数码管位选情况 定义了Seg_Buf的变量数组，用于控制每个数码管显示第数据 定义了Seg_Point的变量数组，用于控制某一位数码管是否显示小数点 定义了Uart_Buf的变量数组，用于获取串口接收的数据 定义了Uart_Rx_Index的变量，用于对串口接收后的数组进行索引 定义了Uart_flag的变量，用于判断当前是否正处于串口接收状态 定义了Sys_Tick的变量，用于嘀嗒计时，判断串口是否接收状态超过了10ms 定义了ucRtc的变量数组，用于存放时钟数据，时分秒 定义了int型的计时变量time_all_1s，用于全局定时 定义了int型的频率变量Freq，用于测频结果的存放 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /* LED与数码管 */ unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0}; unsigned char Seg_Pos; unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10}; unsigned char Seg_Point[8] = {0, 0, 0, 0, 0, 0, 0, 0}; /* 串口数据*/ unsigned char Uart_Buf[10]; unsigned char Uart_Rx_Index; bit Uart_flag; unsigned char Sys_Tick; /* 时间*/ unsigned char ucRtc[3] = {11, 11, 11}; unsigned int time_all_1s; /* 数据 */ unsigned int Freq;//ne555测频 数据读取模块 我们单独写一个模块对数据进行读取，方便我们进行不同数据的操作，这里使用%的操作来进行时间的定时，比如我们的时间读取，就每隔50ms（因为我定时器是1ms）进行一次；AD读取每隔100ms一次；温度读取每隔500ms一次（这里仅作参考，后续还是要自己搓一下试试）\n1 2 3 4 5 6 7 8 9 10 11 void Data_Proc() if (time_all_1s % 50 == 0) { // 时间读取 } if (time_all_1s % 100 == 0) { // AD读取 } if (time_all_1s % 500 == 0) { // 温度读取 } } Key键盘模块 这个模块是重中之重，因为用到了一个很精妙的三行代码消抖方法，这个在我以前的视频蓝桥杯单片机按键考点里面提到过，点击这行字体就可以跳转过去，我这里简单说明一下，我们的逻辑如下，Key_Val,Key_Old,Key_Down,Key_Up,这几个变量都是下面的逻辑，我们这里对其的原理进行解析\n瞬时值获取-\u0026gt; 获取按下的值，获取抬起的值-\u0026gt;更新⼀下值\nDown和Up都是瞬时值，Old比Val延后10ms，基于这个特性我们可以进行书写代码\n对应的代码如下，我们将这四个变量设置为静态局部变量，可以有效节省内存空间，使用if语句那一段表明我们选择10ms跑一次代码（因为我们定时是1ms的）\n1 2 3 4 5 6 7 8 void Key_Proc() { static unsigned char Key_Val, Key_Down, Key_Up, Key_Old; if (time_all_1s % 10) return; Key_Val = Key_Read(); Key_Down = Key_Val \u0026amp; (Key_Old ^ Key_Val); Key_Up = ~Key_Val \u0026amp; (Key_Old ^ Key_Val); Key_Old = Key_Val; } 如果理解不了上面的图和代码，我就拿一个🌰来进行验证，比如我们按下/抬起S4来做一下验证\n键盘状态 Key_Old Key_Val Key_Old^Key_Val Key_Down Key_Up 未按下 0000 0000 0000^0000=0000 0000 0000 按下过程中（10ms） 0000 0100 0000^0100=0100 0100\u0026amp;0100=0100 1011\u0026amp;0100=0000 按下稳定（10ms后） 0100 0100 0100^0100=0000 0100\u0026amp;0000=0000 1011\u0026amp;0000=0000 抬起过程（10ms） 0100 0000 0100^0000=0100 0000\u0026amp;0100=0000 1111\u0026amp;0100=0100 我们可以从这里看出，当我们按下的时候，那一瞬间Down会是4（注意是一瞬间），其余时候都是0；同理Up也是一样，当我们抬起的瞬间Up会是4（注意是一瞬间），其余时候是0\n那么我们如何在这个代码里面进行书写呢，其实很简单，我们一般判断按键都是判断的按键按下，即Down的数据，那么我们按照下面这样写就行，下面就是按下S4的一个示例\n1 2 3 4 5 6 7 8 9 10 void Key_Proc() { static unsigned char Key_Val, Key_Down, Key_Up, Key_Old; if (time_all_1s % 10) return; Key_Val = Key_Read(); Key_Down = Key_Val \u0026amp; (Key_Old ^ Key_Val); Key_Up = ~Key_Val \u0026amp; (Key_Old ^ Key_Val); Key_Old = Key_Val; if(Key_Down == 4) //执行按下S4的函数 } Seg数码管模块 这个和V2模版不一样，他将数据读取放出去了，所以我们的数码管里面不过多涉及到数据读取，这里选择的是20ms进行一次数码管状态更新\n1 2 3 void Seg_Proc() { if (time_all_1s % 20) return; } 如何使用呢，其实也非常简单，我们只需要按照下方的写法即可（因为一般来说我们都有多个界面进行切换，我们定义一个变量Seg_Show_Mode来进行书写，如果有子界面的话，拿我们就再定义一个mode就行）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 void Seg_Proc() { if (time_all_1s % 20) return; switch(Seg_Show_Mode) { case 0: //xx界面 Seg_Buf[0] = xx; Seg_Buf[1] = xx; break; case 1: //xx界面 break; } } Led灯模块 这里面我的话经常放的是Led，蜂鸣器，继电器，MOTOR口的相关逻辑，这里不需要设置定时进入\n1 void Led_Proc() {} 使用示例如下\n1 2 3 4 5 6 7 8 9 10 void Led_Proc() { ucLed[0] = 1;//L1点亮 ucLed[1] = 0;//L2熄灭 Relay(1);//打开继电器（L10亮） Beep(1);//蜂鸣器开启 MOTOR(1);//MOTOR输出高电平 Relay(0);//关闭继电器（L10灭） Beep(0);//蜂鸣器关闭 MOTOR(0);//MOTOR输出低电平 } Uart串口模块 我们在这里使用了串口超时解析，即10ms内没有收集到串口的信息的时候我们才进入并进行解析（后续在定时器里面会提到这个Sys_Tick），在我们解析结束后使用memset函数将接收的数组置为0（为了防止后续解析被干扰），并将索引置为0，为下一次接收解析进行准备\n1 2 3 4 5 6 7 8 9 void Uart_Proc() { if (Uart_Rx_Index == 0) return; if (Sys_Tick \u0026gt;= 10) { Sys_Tick = Uart_flag = 0; memset(Uart_Buf, 0, Uart_Rx_Index); Uart_Rx_Index = 0; } } 使用示例，这里发送小写的ok返回hello，注意一下，判断的时候用的是单引号而不是双引号\n1 2 3 4 5 6 7 8 9 10 void Uart_Proc() { if (Uart_Rx_Index == 0) return; if (Sys_Tick \u0026gt;= 10) { Sys_Tick = Uart_flag = 0; if(Uart_Buf[0]==\u0026#39;o\u0026#39;\u0026amp;\u0026amp;Uart_Buf[1]==\u0026#39;k\u0026#39;) printf(\u0026#34;hello\u0026#34;); memset(Uart_Buf, 0, Uart_Rx_Index); Uart_Rx_Index = 0; } } 定时器0初始化（NE555模块） 这个模块是最有意思的，首先我们需要注意一下，P34和singal的跳线帽要插在一起，并且按键方面需要删除P34，然后才能开始测，这里依然用isp生成一下初始化的定时器0，但是这里要注意，新版的不能生成0ms，我们就随便生成个时间，然后按照我这样勾选后复制粘贴就行\n我们需要将TL0和TH0的值改为0，并且在TMOD \u0026amp;= 0xF0;下面对其进行配置 TMOD |= 0x05;将其变为十六位不自动重载（这里具体可以参考手册里面对于定时器的配置，我之前的视频有提到蓝桥杯单片机ne555模块，点击即可观看），这里别忘了EA开启总中断（这是我的习惯），频率的读取放在定时器1那里讲\n1 2 3 4 5 6 7 8 9 10 11 void Timer0_Init(void) // 1毫秒@12.000MHz { AUXR \u0026amp;= 0x7F; // 定时器时钟12T模式 TMOD \u0026amp;= 0xF0; // 设置定时器模式 TMOD |= 0x05; TL0 = 0; // 设置定时初始值 TH0 = 0; // 设置定时初始值 TF0 = 0; // 清除TF0标志 TR0 = 1; // 定时器0开始计时 EA = 1; } 定时器1初始化 这是我们轮询的定时器的初始化代码，我们使用isp生成1ms定时器1的代码（记得勾选上使能），老版本没有使能，所以记得加上ET1 = 1开启\n当然，不要忘记EA = 1，\n1 2 3 4 5 6 7 8 9 10 11 void Timer1_Init(void) // 1毫秒@12.000MHz { AUXR \u0026amp;= 0xBF; // 定时器时钟12T模式 TMOD \u0026amp;= 0x0F; // 设置定时器模式 TL1 = 0x18; // 设置定时初始值 TH1 = 0xFC; // 设置定时初始值 TF1 = 0; // 清除TF1标志 TR1 = 1; // 定时器1开始计时 ET1 = 1; // 使能定时器1中断 EA = 1; } 定时器1中断（重点） 我们在这里放了一堆东西，这里我做一下说明，我们每个中断都有对应的中断号（可以在官方的手册查到），这里定时器1的中断号是3\n然后是我们的一个全局定时器（1s，注意time_all_1s的类型要是unsigned int，不然会溢出），在这个定时器里面我们对频率进行了读取，将TH0 \u0026laquo; 8 | TL0的结果给了Freq（注意一下Freq的类型要是unsigned int，不然会溢出），在我们读取完数据后，将TH0和TL0进行了置0手动重载，方便下一次进行测量\n接着就是一个和Sys_Tick相关的代码，当我们接收到数据的时候Uart_flag会置为1，然后将Sys_Tick置为0，也就是说我们一段时间不接受数据的话，那么Sys_Tick的值会一直累加，直到达到10ms（我们上面在Uart串口模块里面的设定），Uart_flag置为0，Sys_Tick的值置为0，为下一轮串口解析做准备\n然后就是我们的一个位选的代码，因为我们数码管本质上是一位一位显示，所以我们需要写一个位选的变量来进行位选，这里使用的Seg_Pos = (++Seg_Pos) % 8将Seg_Pos这个变量在0~7中间进行循环\n接着就是我们的数码管显示函数，这里不需要多说了，保持他不变就行，传入为位选、位选对应的段选显示、位选对应的小数点显示\n最后就是我们的Led显示函数，这里我并没有选择和数码管同步，而是选择在中断里面写了个for循环来进行显示，因为它不涉及到扫描等操作，本质上就是可以直接一起显示。\n1 2 3 4 5 6 7 8 9 10 11 12 void Timer1_Isr(void) interrupt 3 { unsigned char i; if (++time_all_1s == 1000) { time_all_1s = 0; Freq = TH0 \u0026lt;\u0026lt; 8 | TL0; TH0 = TL0 = 0; } if (Uart_flag) Sys_Tick++; Seg_Pos = (++Seg_Pos) % 8; Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]); for (i = 0; i \u0026lt; 8; i++) Led_Disp(i, ucLed[i]); } 串口中断（这里代码写死） 我们串口有一个接收中断，串口1的中断号查询后发现为4\n我们第一行就是判断是否RI为1，因为RI为1则代表有信息过来了，当我们收集到信息时，我们将Uart_flag置为1，Sys_Tick置为0，开始嘀嗒计时，当我们10ms没有接收到新的数据的时候，我们就认为数据接收完成，可以开始进行串口处理，后面的接收就不需要多讲了，我们是一位一位进行接收的，而读写都是用的SBUF这个缓冲区（串口1，如果是串口2则是S2BUF，但是串口2不会考，所以这里不用管），我们接收完成后，将RI置为0，为下一次接收做准备，最后一行的\u0026gt;10清零，则是为了避免出现传入数据溢出，导致程序异常，这里的10取决于你的字符串数组的位数，我在最上面定的是10位，所以这里写的10\n1 2 3 4 5 6 7 8 9 10 void Uart1_Isr(void) interrupt 4 { if (RI) { Uart_flag = 1; Sys_Tick = 0; Uart_Buf[Uart_Rx_Index] = SBUF; Uart_Rx_Index++; RI = 0; } if (Uart_Rx_Index \u0026gt; 10) Uart_Rx_Index = 0; } main主函数书写 这是我们的一个最初的函数，没什么好说明的，就是需要进行一些初始化而已，但是顺序不要写错了，首先是系统初始化，然后是定时器0（ne555），最后是定时器1（轮询）的初始化。\n你可以发现，我在这里进行了一次温度的空读取，因为温度上电后会显示85（温度转换的时间），如果在后面才给他开始温度转换的话，那么很有可能跳85，导致测量结果错误，这里的延时函数也是用isp直接生成的，注意系统时间别选错了就行\n我们写完初始化之后就需要写一下循环了，51单片机里面必须将循环写死，所以我们用了while(1)，这里的顺序倒没什么特殊的要求，但是尽量按照我这个顺序就行，不会出问题的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void main() { System_Init(); Timer0_Init(); Timer1_Init(); Set_Rtc(ucRtc); rd_temperature(); Delay750ms(); while (1) { Data_Proc(); Key_Proc(); Seg_Proc(); Uart_Proc(); Led_Proc(); } } 完整的main.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 #include \u0026#34;main.h\u0026#34; /* LED与数码管 */ unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0}; unsigned char Seg_Pos; unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10}; unsigned char Seg_Point[8] = {0, 0, 0, 0, 0, 0, 0, 0}; /* 串口数据*/ unsigned char Uart_Buf[10]; unsigned char Uart_Rx_Index; bit Uart_flag; unsigned char Sys_Tick; /* 时间*/ unsigned char ucRtc[3] = {11, 11, 11}; unsigned int time_all_1s; /* 数据 */ unsigned int Freq; void Data_Proc() { if (time_all_1s % 50 == 0) { // 时间读取 } if (time_all_1s % 100 == 0) { // AD读取 } if (time_all_1s % 500 == 0) { // 温度读取 } } /* 键盘处理*/ void Key_Proc() { static unsigned char Key_Val, Key_Down, Key_Up, Key_Old; if (time_all_1s % 10) return; Key_Val = Key_Read(); Key_Down = Key_Val \u0026amp; (Key_Old ^ Key_Val); Key_Up = ~Key_Val \u0026amp; (Key_Old ^ Key_Val); Key_Old = Key_Val; } /* 数码管处理*/ void Seg_Proc() { if (time_all_1s % 20) return; } void Led_Proc() {} void Uart_Proc() { if (Uart_Rx_Index == 0) return; if (Sys_Tick \u0026gt;= 10) { Sys_Tick = Uart_flag = 0; memset(Uart_Buf, 0, Uart_Rx_Index); Uart_Rx_Index = 0; } } void Timer0_Init(void) // 1毫秒@12.000MHz { AUXR \u0026amp;= 0x7F; // 定时器时钟12T模式 TMOD \u0026amp;= 0xF0; // 设置定时器模式 TMOD |= 0x05; TL0 = 0; // 设置定时初始值 TH0 = 0; // 设置定时初始值 TF0 = 0; // 清除TF0标志 TR0 = 1; // 定时器0开始计时 EA = 1; } void Timer1_Init(void) // 1毫秒@12.000MHz { AUXR \u0026amp;= 0xBF; // 定时器时钟12T模式 TMOD \u0026amp;= 0x0F; // 设置定时器模式 TL1 = 0x18; // 设置定时初始值 TH1 = 0xFC; // 设置定时初始值 TF1 = 0; // 清除TF1标志 TR1 = 1; // 定时器1开始计时 ET1 = 1; // 使能定时器1中断 EA = 1; } void Timer1_Isr(void) interrupt 3 { unsigned char i; if (++time_all_1s == 1000) { time_all_1s = 0; Freq = TH0 \u0026lt;\u0026lt; 8 | TL0; TH0 = TL0 = 0; } if (Uart_flag) Sys_Tick++; Seg_Pos = (++Seg_Pos) % 8; Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]); for (i = 0; i \u0026lt; 8; i++) Led_Disp(i, ucLed[i]); } void Uart1_Isr(void) interrupt 4 { if (RI) { Uart_flag = 1; Sys_Tick = 0; Uart_Buf[Uart_Rx_Index] = SBUF; Uart_Rx_Index++; RI = 0; } if (Uart_Rx_Index \u0026gt; 10) Uart_Rx_Index = 0; } void Delay750ms(void) //@12.000MHz { unsigned char data i, j, k; _nop_(); _nop_(); i = 35; j = 51; k = 182; do { do { while (--k); } while (--j); } while (--i); } void main() { System_Init(); Timer0_Init(); Timer1_Init(); Set_Rtc(ucRtc); rd_temperature(); Delay750ms(); while (1) { Data_Proc(); Key_Proc(); Seg_Proc(); Uart_Proc(); Led_Proc(); } } main.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;STC15F2K60S2.H\u0026gt; #include \u0026#34;ds1302.h\u0026#34; #include \u0026#34;iic.h\u0026#34; #include \u0026#34;init.h\u0026#34; #include \u0026#34;intrins.h\u0026#34; #include \u0026#34;key.h\u0026#34; #include \u0026#34;led.h\u0026#34; #include \u0026#34;onewire.h\u0026#34; #include \u0026#34;seg.h\u0026#34; #include \u0026#34;stdio.h\u0026#34; #include \u0026#34;string.h\u0026#34; #include \u0026#34;uart.h\u0026#34; #include \u0026#34;ul.h\u0026#34; ","date":"2024-06-02T00:00:00Z","image":"https://zuoliangyu.github.io/hugo_blog/p/%E8%93%9D%E6%A1%A5%E6%9D%AF/test_hu2849714102897378774.jpg","permalink":"https://zuoliangyu.github.io/hugo_blog/p/%E8%93%9D%E6%A1%A5%E6%9D%AF/","title":"蓝桥杯模板建立"}]