EDUCBA-嵌入式系统笔记-全-

EDUCBA 嵌入式系统笔记(全)

001:C语言中的十进制数值操作 💻

在本节课中,我们将学习如何在C语言中操作十进制数值。理解十进制数(或称实数)在计算机中的存储和处理方式,是进行嵌入式系统编程的基础。

什么是十进制数?

十进制数是指任何包含小数点的数字。例如,数字 170.345 就是一个十进制数。在编程中,这类数字也被称为实数。

为什么需要特殊的表示方法?

在计算机内存中,一些实数(例如非常小、非常大或带有小数部分的数字)无法用标准的整数类型来精确表示。为了解决这个问题,业界采用了 IEEE 754标准 来存储这些数字。

这种表示方法被称为 浮点数表示法,它是实数在计算机中的一种恰当表示形式。如今,所有的计算机系统和微控制器都使用这个标准来在内存中存储实数。

何时使用浮点数?

如果你处理的数字包含小数部分,或者使用的整数超出了 long 数据类型能表示的范围,那么就可以使用浮点数表示法。

到目前为止,我们讨论的主要是整数数据类型,它们并非按照此标准存储。请记住,浮点数主要用于以下情况:

  • 处理非常小的数字,例如 电子的电荷量
  • 处理非常大的数字,例如 地球与某个星系之间的距离

这类数字要么太大,无法放入常规的整数类型;要么包含小数部分。因此,它们需要使用浮点表示法。

C语言中的浮点数据类型

在C语言中,我们主要有两种浮点数据类型:

  • float:单精度浮点数。
  • double:双精度浮点数。

你可以使用这些数据类型来声明和操作浮点数值。

本节总结

在本节中,我们一起学习了:

  1. 十进制数(实数)的定义。
  2. 计算机使用 IEEE 754标准(浮点数表示法)来存储实数的原因。
  3. 在需要处理 极大极小带有小数部分 的数字时,应使用浮点数。
  4. C语言中用于操作浮点数的主要数据类型:floatdouble

在下一节视频中,我们将深入探讨存储浮点数的具体标准。敬请期待,祝你今天愉快!

002:单精度与双精度浮点数 🧮

在本节课中,我们将学习用于存储实数的 IEEE 754 浮点数格式。这是一种被所有现代计算机系统和微控制器遵循的标准。

上一节我们介绍了整数类型,本节中我们来看看如何存储带有小数部分的数字。

IEEE 754 浮点数格式

IEEE 754 是一种用于表示和操作浮点数的标准。它解决了如何高效存储大数字的问题。

例如,考虑数字 8.435 x 10^49。这是一个非常大的数字。如果将其直接转换为二进制存储,会消耗大量内存。因此,该标准建议不直接存储数字本身,而是存储其近似值以及必要的信息。

这些信息包括:

  • 符号:表示正负。
  • 指数:表示数字的缩放比例。
  • 尾数:也称为有效数字,表示数字的精度部分。

以下是存储这些信息的两种主要格式。

单精度与双精度格式

根据精度和存储空间的不同,IEEE 754 定义了两种主要格式。

单精度格式

单精度格式使用 32 位 来存储一个浮点数。
其位分配如下:

  • 23 位 用于尾数。
  • 8 位 用于指数。
  • 1 位 用于符号。

这种 32 位的表示方式就是单精度表示法。

双精度格式

双精度格式使用 64 位 来存储一个浮点数。
其位分配如下:

  • 52 位 用于尾数。
  • 11 位 用于指数。
  • 1 位 用于符号。

与单精度实现相比,双精度实现提供了更高级的近似,因此结果也更精确。

C 语言中的浮点数据类型

了解了存储格式后,我们来看看如何在编程中使用它们。在 C 语言中,我们有专门的数据类型来处理带小数点的数字。

考虑数字 567.89。它包含整数部分 567 和小数部分 .89。你不能使用 intcharlong 来存储它,因为这些类型只会存储整数部分,导致小数部分丢失。

因此,你需要使用以下数据类型:

  • float:用于 32 位的单精度表示。
  • double:用于 64 位的双精度表示。

格式说明符

在 C 语言中,通过标准输入输出函数读写这些小数时,需要使用正确的格式说明符。

以下是常用的格式说明符:

  • %f:用于读写 float 类型变量。
  • %lf:用于读写 double 类型变量。
  • %e:用于以科学计数法读写 float 类型变量。
  • %le:用于以科学计数法读写 double 类型变量。

需要注意的是,在 C 语言中,所有带小数点的常量默认都被编译器视为 double 类型。

总结与预告

本节课中我们一起学习了 IEEE 754 浮点数标准,了解了单精度和双精度格式如何通过存储符号、指数和尾数来高效表示实数。我们还学习了在 C 语言中如何使用 floatdouble 数据类型以及对应的格式说明符。

在下一个视频中,我们将通过一些实际例子和练习,来看看如何具体使用 floatdouble,并实践它们的实现。

003:使用浮点型和双精度型变量 第一部分 🧮

在本节课中,我们将学习C语言中两种用于存储小数的数据类型:float(浮点型)和double(双精度型)。我们将通过创建项目、编写代码来了解它们的基本用法、存储差异以及如何正确地打印它们的值。


项目创建与设置

首先,我们需要创建一个新的C/C++项目来实践。我们将使用MinGW GCC编译器,并将项目命名为 float_double_EX。项目创建完成后,在项目中新建一个名为 main.c 的源文件。

浮点型 (float) 介绍

上一节我们完成了项目设置,本节中我们来看看 float 数据类型。float 用于存储单精度浮点数。

以下是关于 float 的关键信息:

  • 存储大小4 字节。
  • 精度:最多可精确到 6 位小数。

main.c 文件中,我们首先包含标准输入输出头文件 stdio.h,然后在 main 函数中声明一个 float 类型的变量并赋值。

#include <stdio.h>

int main() {
    // float 存储大小为4字节,精度最多6位小数
    float f_num = 78.123456789;
    printf("Number is %f\n", f_num);
    return 0;
}

编译并运行此程序,你会发现打印出的数值被四舍五入到了大约6位小数,这是因为 float 的精度限制。

双精度型 (double) 介绍

了解了 float 之后,我们再来看看精度更高的 double 类型。double 意为“双倍”,它提供了比 float 更高的精度。

以下是关于 double 的关键信息:

  • 存储大小8 字节。
  • 精度:最多可精确到 15 位小数。

现在,我们在程序中添加一个 double 类型的变量进行对比。

#include <stdio.h>

int main() {
    // float 存储大小为4字节,精度最多6位小数
    float f_num = 78.123456789;
    // double 存储大小为8字节,精度最多15位小数
    double d_num = 78.123456789;

    printf("Float Number is %.9f\n", f_num);
    printf("Double Number is %.9f\n", d_num);

    return 0;
}

注意,在 printf 函数中,我们使用了格式说明符 %.9f 来指定打印9位小数。运行程序,你会观察到 double 类型变量能更准确地保留更多位小数。

格式化输出控制

为了更清晰地展示两种类型的精度差异,我们可以通过格式说明符精确控制输出的小数位数。

以下是调整输出精度的示例:

printf("Float Number is %.14f\n", f_num); // 尝试打印14位,但float只能精确约6位
printf("Double Number is %.14f\n", d_num); // double可以精确打印更多位

运行修改后的代码,float 类型的输出在超出其精度的部分会出现不准确的数字,而 double 类型则能正确显示。

科学计数法表示

除了常规小数格式,我们还可以使用科学计数法来打印浮点数。这通过格式说明符 %e 实现。

以下是如何使用科学计数法:

printf("Scientific notation: %e\n", d_num);

此外,我们也可以控制科学计数法表示中的小数位数:

printf("Scientific notation (2 decimals): %.2e\n", d_num);

使用 %.2e 会将数值四舍五入到小数点后两位,再以科学计数法形式输出。


本节课中我们一起学习了 floatdouble 两种浮点数类型的基本概念、存储精度差异,以及如何使用 printf 函数配合 %f%e 等格式说明符来控制它们的输出格式。正确理解这些数据类型对于进行精确的数值计算至关重要。在下一部分,我们将继续探讨与浮点数相关的更多操作和注意事项。

004:处理浮点型与双精度变量(第二部分) 🧮

在本节课中,我们将继续学习如何在C语言中处理浮点数和双精度数。我们将通过一个具体的编程示例,演示如何声明、赋值和打印一个极小的数值(如电子电荷),并比较使用floatdouble类型时的精度差异。

上一节我们介绍了浮点型变量的基本概念和打印格式。本节中,我们来看看如何在实际编程中应用这些知识,并处理高精度需求。

编程示例:存储并打印电子电荷

我们将编写一个程序,创建一个变量来存储电子的电荷值(约为 1.602 × 10^-19 库仑),并使用不同的格式打印它。

以下是创建和打印浮点变量的步骤:

  1. 首先,我们创建一个float类型的变量。
  2. 使用科学计数法(E表示法)为其赋值。
  3. 使用printf函数,配合%f%e格式说明符来打印这个值。
// 创建一个浮点变量存储电子电荷
float electron_charge = 1.602E-19;

// 使用 %f 格式打印(默认显示6位小数)
printf("Charge of electron is: %f\n", electron_charge);

// 使用 %e 格式打印(科学计数法)
printf("Charge of electron is: %e\n", electron_charge);

运行这段代码后,使用%f格式可能会输出0.000000,这是因为数值太小,默认的6位小数不足以显示它。而%e格式则可以正确显示其科学计数法形式。

控制输出精度

为了更精确地控制输出的小数位数,我们可以在格式说明符中指定精度。

以下是如何指定输出精度的示例:

  • %.8f: 输出浮点数,并保留8位小数。
  • %.8e: 以科学计数法输出,并保留8位小数。

将代码修改为指定精度后再次运行,可以观察到输出的小数位数发生了变化。

使用双精度类型提升精度

float类型通常提供约6-7位有效数字的精度。对于需要更高精度的场景,我们可以使用double类型。

以下是使用double类型的修改:

  1. 将变量类型从float改为double
  2. printf中使用对应的格式说明符%lf(用于double%f)和%le(用于double%e)。
// 创建一个双精度变量存储电子电荷
double electron_charge_double = 1.602E-19;

// 使用 %lf 格式打印,并指定28位小数以显示这个极小值
printf("Charge of electron (double) is: %.28lf\n", electron_charge_double);

使用double类型后,由于其提供约15位有效数字的更高精度,当我们指定显示足够多的小数位(例如28位)时,就能清晰地看到这个极小数值的细节,而不仅仅是零。

本节课中我们一起学习了如何声明和打印浮点数与双精度数。通过电子电荷的例子,我们实践了使用科学计数法赋值,并使用%f%e及其精度控制格式进行输出。关键点在于理解了floatdouble在精度上的差异:对于需要高精度的计算,应优先选择double类型,并在打印时指定足够的精度来正确显示数据。

005:scanf 函数简介

概述

在本节课程中,我们将学习C语言中另一个重要的库函数:scanf。我们将了解它的作用、基本用法,以及如何用它从标准输入(如键盘)读取数据。这对于编写能与用户交互的程序至关重要。

scanf 函数简介

scanf 是一个标准库函数,它允许你从标准输入读取数据。在个人计算机上,标准输入通常指键盘。在嵌入式系统中,输入设备可能是触摸屏或按键板等。使用 scanf 函数,你可以从键盘读取字符和数字。

基本语法与用法

上一节我们介绍了 scanf 的基本概念,本节中我们来看看它的具体语法和如何编写代码。

scanf 函数的基本语法格式如下:

scanf("格式说明符", &变量名);

以下是 scanf 使用时的两个核心要点:

  1. 格式说明符:指定要读取的数据类型,例如 %d 表示整数,%c 表示字符。
  2. 地址运算符 &:在变量名前使用 & 符号,表示将读取的值存储到该变量的内存地址中。

代码示例解析

让我们通过一个具体的代码片段来理解 scanf 的用法。

假设我们创建一个变量 age,并提示用户输入年龄:

int age;
printf("Enter your age: ");
scanf("%d", &age);

在这段代码中:

  • printf 函数向用户显示提示信息。
  • scanf("%d", &age) 会等待用户输入一个整数。
  • 用户输入的数字(例如 25)会被读取,并通过 &age 存储到变量 age 的内存地址中。
  • 这样,用户输入的数据就被成功读取到程序里了。

相关函数:getchar

除了 scanf,C语言还提供了 getchar 函数用于读取单个字符。

getchar 函数用于从标准输入读取一个字符。它不接受参数,并返回一个整数值,该值是所按键的ASCII码。

例如:

int a;
a = getchar();

程序执行到 getchar() 时会暂停,直到用户按下一个键。该键的ASCII码值会被获取并存储在变量 a 中,然后程序继续执行。

实践任务预告

在理解了 scanf 的基本用法后,下一节视频我们将进行实践。

你需要创建一个程序,该程序使用 scanf 从用户那里接收四个整数,计算它们的平均值,并将结果打印给用户。这将巩固你对 scanf 函数用法的理解。

总结

本节课中我们一起学习了 scanf 函数。我们了解到它是一个用于从标准输入读取数据的库函数,其核心在于正确使用格式说明符(如 %d%c)和地址运算符 &。我们还简单了解了 getchar 函数用于读取单个字符的用途。掌握这些输入函数是进行用户交互式编程的基础。

006:构建嵌入式系统 p06 01_02_02_scanf练习实现第一部分

在本节课中,我们将学习如何在嵌入式系统项目中实际使用 scanf 函数。我们将创建一个简单的C语言程序,用于计算四个数字的平均值。通过这个练习,你将掌握使用 scanf 接收用户输入、处理浮点数以及管理控制台输出的基本方法。

项目创建与变量声明

上一节我们介绍了 scanf 函数的基本概念,本节中我们来看看如何将其应用于一个实际项目。

首先,你需要创建一个新的C++项目。为项目命名,并在其中添加一个新的 main.c 文件。在该文件中,创建 main 函数作为程序的入口点。

接下来,在 main 函数内部,我们需要声明变量来存储用户输入的数字和计算结果。由于平均值可能是小数,我们将使用 float 类型。

以下是变量声明的代码示例:

float number1, number2, number3, number4;
float average;

使用 printfscanf 获取用户输入

变量声明完成后,我们需要提示用户输入数据,并使用 scanf 函数读取这些输入。

我们将使用 printf 函数在控制台显示提示信息。为了让用户在同一行输入,我们不在提示信息末尾添加换行符 \n。然后,使用 scanf 读取用户输入,并将其存储到相应的变量中。%f 是用于读取浮点数的格式说明符。

以下是获取四个数字输入的代码:

printf("Enter the first number: ");
scanf("%f", &number1);

printf("Enter the second number: ");
scanf("%f", &number2);

printf("Enter the third number: ");
scanf("%f", &number3);

printf("Enter the fourth number: ");
scanf("%f", &number4);

计算并输出平均值

获取所有输入后,我们可以计算这四个数字的平均值。计算公式为总和除以数量。

计算平均值的代码如下:

average = (number1 + number2 + number3 + number4) / 4;

计算完成后,我们需要将结果显示给用户。使用 printf 函数并配合 %f 格式说明符来输出 average 变量的值。

输出结果的代码如下:

printf("The average is: %f\n", average);

处理控制台输出缓冲问题

在集成开发环境(如Eclipse)的控制台中运行程序时,你可能会遇到提示信息没有立即显示的问题。这是因为输出内容被缓冲在操作系统的输出缓冲区中,没有立即刷新到控制台显示。

为了解决这个问题,我们需要在每次使用 printf 后,显式地刷新输出缓冲区。这可以通过调用 fflush(stdout) 函数来实现。

修改后的提示输入代码如下:

printf("Enter the first number: ");
fflush(stdout);
scanf("%f", &number1);

// ... 对其他三个数字重复此模式

在Windows中直接运行程序

有时,IDE内置的控制台对 scanf 的支持并不理想。作为替代方案,你可以直接运行编译生成的可执行文件(.exe)。

以下是找到并运行可执行文件的步骤:

  1. 在项目文件夹中,导航到 Debug 子目录。
  2. 找到与项目同名的 .exe 文件(例如 scanf_example.exe)。
  3. 双击该文件,它将在独立的Windows命令提示符窗口中运行。在这个窗口中,输入和输出通常会按预期工作。

总结

本节课中我们一起学习了 scanf 函数的实际应用。我们创建了一个计算四个数字平均值的程序,涵盖了从项目创建、变量声明、用户输入获取、数据计算到结果输出的完整流程。我们还探讨了在特定开发环境中可能遇到的输出缓冲问题及其解决方案,并介绍了直接运行可执行文件作为备选测试方法。掌握这些基础知识是进行更复杂嵌入式系统编程的重要一步。

007:scanf练习实现 第二部分 🖥️

在本节课中,我们将继续学习如何使用 scanf 函数,并解决程序执行后终端窗口立即关闭的问题。我们将探讨如何让程序等待用户输入后再退出,并学习如何优化代码结构。


程序暂停问题与 getchar 函数

上一节我们介绍了使用 scanf 读取多个输入。但在程序执行最后的 printf 后,控制台窗口会立即关闭,导致用户看不到输出结果。

为了解决这个问题,我们需要让程序在打印结果后暂停。一个常见的方法是使用 getchar 函数。getchar 函数会使程序等待,直到用户从键盘按下任意键。当用户按下键时,该函数会捕获这个字符,然后程序可以继续执行或退出。

以下是使用 getchar 的基本语法:

getchar();

将此语句放在打印平均值的 printf 语句之后,程序就会在显示结果后暂停。


初次尝试与问题

我们首先在打印平均值的代码后添加一个 getchar()

printf("The average is: %f\n", average);
getchar(); // 等待用户按键

构建并运行程序后,我们输入数字。然而,程序在显示平均值后并没有如预期般暂停,而是直接退出了。这是因为之前的 scanf 函数在读取数字后,在输入缓冲区中留下了一个换行符 \n。第一个 getchar() 会立刻读取这个残留的换行符,因此不会等待用户的新输入。


解决方案:使用多个 getchar 或循环

一个直接的解决方法是使用两个 getchar() 调用。第一个用于消耗缓冲区中残留的换行符,第二个则真正等待用户的新输入。

printf("The average is: %f\n", average);
printf("Press any key to exit the application.\n");
getchar(); // 消耗残留的换行符
getchar(); // 等待用户按键

这种方法有效,但并非通用解决方案,因为它依赖于特定的输入情况。

更通用的方法是使用一个循环。我们可以让程序持续等待,直到用户输入一个非换行符的键。

printf("Press any key to exit the application.\n");
while(getchar() != '\n') {
    // 循环等待,直到输入的字符不是换行符
    // 此处可以留空或添加其他语句
}
getchar(); // 等待用户最后一次按键确认

这个 while 循环会清空输入缓冲区中所有字符,直到遇到换行符。然后,最后一个 getchar() 会等待用户进行一次新的按键操作,从而实现稳定的暂停效果。


优化输入:单次 scanf 读取多个值

在之前的练习中,我们使用了多个 printfscanf 语句来分别提示和读取四个数字。实际上,我们可以简化这个过程。

我们可以使用一个 printf 进行提示,然后用一个 scanf 语句同时读取所有四个数字。

以下是优化后的代码示例:

#include <stdio.h>

int main() {
    float num1, num2, num3, num4, average;

    // 单次提示
    printf("Enter four numbers separated by space: ");
    // 单次scanf读取四个值
    scanf("%f %f %f %f", &num1, &num2, &num3, &num4);

    average = (num1 + num2 + num3 + num4) / 4;

    printf("The average is: %f\n", average);

    // 通用暂停方法
    printf("Press any key to exit the application.\n");
    while(getchar() != '\n'); // 清空输入缓冲区
    getchar(); // 等待用户按键

    return 0;
}

这种方法大大减少了代码行数,使程序更加简洁高效。用户可以在提示后一次性输入所有数字(用空格分隔),然后按回车键。


总结

本节课中我们一起学习了以下内容:

  1. 使用 getchar 暂停程序:解决了程序输出后立即关闭的问题。
  2. 处理输入缓冲区:理解了残留换行符对 getchar 的影响,并学会了使用循环来清空缓冲区,从而获得更稳健的暂停逻辑。
  3. 优化 scanf 的使用:掌握了如何用单个 scanf 语句读取多个输入值,使代码更加简洁明了。

通过尝试和修改这些程序,你可以更深入地理解C语言中标准输入/输出函数的工作方式。请尝试运行这些示例,并观察它们的行为。我们将在后续课程中继续探索更多嵌入式系统编程的基础知识。

008:scanf练习2第1部分

概述

在本节课程中,我们将学习如何编写一个C语言程序。该程序需要从用户处接收六个字符输入,并打印出这些字符对应的ASCII码值。我们将使用scanfgetchar函数来获取输入,并通过整数格式输出字符的ASCII码。

创建新项目

首先,我们需要创建一个新的C/C++项目来编写我们的程序。

以下是创建项目的具体步骤:

  1. 创建一个新的C/C++项目。
  2. 将项目命名为 ScanfExercise2
  3. 点击“下一步”并完成项目创建。
  4. 关闭项目创建向导。
  5. 在项目中添加一个新的源文件,命名为 main.c

练习目标

现在,我们来看看本次练习的具体要求。

你需要编写一个程序,该程序能够从用户处读取六个字符。这些字符可以是字母、数字或除换行符外的任何特殊字符。程序需要打印出每个输入字符对应的ASCII码值。

程序的预期输出应为一组数字,即每个输入字符的ASCII码。例如,如果你输入了字符 abcd12,那么程序应该输出这些字符对应的ASCII码值,如 9798991004950

为了实现这个功能,你需要将输入的字符存储到变量中,然后以整数格式打印这些变量,从而显示其ASCII码值。

后续步骤

在下一节视频中,我们将动手编写这个程序。我们将尝试使用C语言实现上述功能。

总结

本节课中,我们一起学习了如何规划一个C语言程序,该程序需要读取用户输入的六个字符并输出其ASCII码。我们创建了项目文件,并明确了程序的功能需求。下一节我们将开始具体的代码实现。

009:scanf练习2第二部分 🖥️

在本节课中,我们将继续学习如何在STM32嵌入式系统中使用scanf函数。我们将基于上一节创建的项目,编写一个程序来接收用户输入的六个字符,并打印出它们对应的ASCII码值。

概述

上一节我们介绍了如何使用scanf函数接收用户输入。本节中,我们来看看如何扩展这个程序,使其能够接收多个字符输入,并显示每个字符的ASCII码值。

项目准备

项目已在之前的视频中创建完成。首先,我们将使用SDDio.h库,并创建main函数。

#include "SDDio.h"

int main(void) {
    // 程序代码将写在这里
}

实现步骤

以下是实现该练习的具体步骤。

1. 提示用户输入

我们使用printf函数向用户发送一条消息,提示输入六个字符。

printf("请输入六个字符:");

2. 声明变量

我们需要六个字符变量来存储用户输入的字符。

char c1, c2, c3, c4, c5, c6;

3. 接收用户输入

使用scanf语句接收六个字符。由于%c用于读取单个字符,我们需要重复六次。

scanf("%c%c%c%c%c%c", &c1, &c2, &c3, &c4, &c5, &c6);

4. 打印ASCII码值

接下来,我们使用printf函数打印每个字符对应的ASCII码值。ASCII码是数字,可以使用%d(有符号整数)或%u(无符号整数)格式说明符。这里我们使用%u

printf("ASCII码值为:%u, %u, %u, %u, %u, %u\n", c1, c2, c3, c4, c5, c6);

5. 完整代码示例

将以上步骤组合起来,完整的代码如下。

#include "SDDio.h"

int main(void) {
    char c1, c2, c3, c4, c5, c6;

    printf("请输入六个字符:");
    scanf("%c%c%c%c%c%c", &c1, &c2, &c3, &c4, &c5, &c6);

    printf("ASCII码值为:%u, %u, %u, %u, %u, %u\n", c1, c2, c3, c4, c5, c6);

    return 0;
}

编译与调试

保存代码后,编译项目。编译成功后,进入调试模式并运行程序。程序会提示输入六个字符。例如,输入A BC D56(注意空格也是一个字符),程序将输出每个字符对应的ASCII码值。

注意scanf会读取输入缓冲区的内容,包括空格和换行符。如果输入后没有立即显示结果,可能需要清除输入缓冲区。可以使用一个额外的scanf语句来读取并忽略换行符。

// 读取并忽略输入缓冲区中的剩余内容(包括换行符)
scanf("%*[^\n]");
scanf("%*c");

替代方案

除了使用scanf,也可以使用getchar函数来获取单个字符。getchar每次读取一个字符,适合简单的字符输入场景。但本教程中,我们选择使用scanf来实现。

c1 = getchar();
c2 = getchar();
// ... 以此类推

总结

本节课中我们一起学习了如何扩展scanf函数的使用,以接收多个字符输入并显示它们的ASCII码值。我们回顾了变量声明、输入输出函数的使用,以及编译调试的基本流程。通过这个练习,你应能更熟练地在嵌入式系统中处理用户输入。


提示:本程序仅用于读取输入缓冲区并显示ASCII值。在实际应用中,可能需要根据具体需求调整输入处理逻辑。

010:C语言中的指针

在本节课中,我们将要学习C语言中一个非常核心且强大的概念——指针。指针是嵌入式C编程中与硬件交互、读写外设和配置内存的关键工具。我们将从理解指针的基本定义开始,逐步探索其重要性。

什么是指针?🧭

上一节我们介绍了本课程的学习目标,本节中我们来看看指针到底是什么。

指针本质上就是内存地址。计算机内存由许多可以存储数据(例如1字节)的单元组成,每个内存单元都有一个唯一的地址。这个地址就被称为指针,因为它“指向”存储在该内存位置的数据。

为了更直观地理解,我们可以想象内存是一系列连续的存储格子:

[地址: 0x1000] -> 存储数据 A
[地址: 0x1001] -> 存储数据 B
[地址: 0x1002] -> 存储数据 C
...

在这里,0x10000x1001等就是指针,它们指向了具体的数据A、B、C。

指针的作用与特性 ⚙️

理解了指针的定义后,我们来看看使用指针可以做什么,以及它有哪些重要特性。

通过指针,我们可以执行多种操作:

  • 写入数据:向指针所指向的内存位置写入新值。
  • 读取数据:从指针指向的内存位置读取值到程序中。
  • 指针运算:对指针进行递增等操作,使其指向下一个内存位置(例如从0x1000移动到0x1001)。

指针的大小取决于处理器架构:

  • 64位系统中,指针大小通常为 8字节
  • 32位ARM微控制器(如许多STM32)中,指针大小为 4字节
  • 在一些8位微控制器中,指针大小可能为 2字节

这个大小决定了指针变量能寻址的内存空间范围。

总结与预告 📚

本节课中我们一起学习了指针的基础概念。我们了解到指针就是内存地址,它是访问和操作内存中数据的直接方式,在嵌入式编程中至关重要。指针的大小随处理器架构而变化。

在下一个视频中,我们将开始在具体的C程序里探索指针的声明和使用方法。请继续关注接下来的内容。


课程名称:构建嵌入式系统 | ARM Cortex (STM32) Fundamentals
章节编号:P10
章节名称:C语言中的指针

011:指针变量与初始化 📝

在本节课中,我们将学习指针变量的定义与初始化。我们将了解指针变量在内存中如何分配空间,以及指针数据类型的作用。


上一节我们介绍了指针的基本概念,本节中我们来看看如何定义和初始化一个指针变量。

指针变量的定义使用星号(*)符号。例如,定义一个字符型指针变量:

char *ptr;

这行代码定义了一个名为 ptr 的指针变量,它用于存储一个字符类型数据的地址。

当编译器看到指针变量的定义时,它会为该变量分配内存空间。在64位机器上,无论指针指向何种数据类型,编译器都会为指针变量本身预留 8字节 的内存。这是因为在64位架构中,一个内存地址需要8字节来存储。

指针变量存储的值是一个内存地址。这个地址指向计算机内存中的某个数据。指针变量的地址(即存储这个地址值的内存位置)和它存储的地址值(即它指向的数据的地址)是两个不同的概念。

既然指针变量的大小固定为8字节,那么定义指针时指定的数据类型(如 char *int *)有何作用呢?其核心目的是控制通过该指针进行操作时的行为

以下是这些操作的具体说明:

  • 读取操作:当通过指针读取数据时,指针的数据类型决定了从目标地址开始读取多少字节的数据。例如,char * 会读取1字节,而 int * 在大多数系统上会读取4字节。
  • 写入操作:同理,写入操作也会根据指针的数据类型来决定写入多少字节的数据。
  • 算术运算:对指针进行加1(ptr++)或减1(ptr--)操作时,指针移动的字节数由其指向的数据类型大小决定。char * 移动1字节,int * 则移动4字节。

因此,char *ptr; 定义了一个字符型指针。对该指针进行读取操作将产生1字节的数据。这样的变量也称为“指向字符类型数据的指针”或“字符型指针”。

在下一个视频中,我们将通过实例来创建指针变量和普通变量,并尝试为指针变量赋值,这将使你对指针变量的概念更加清晰。


本节课中我们一起学习了指针变量的定义与初始化。我们了解到指针变量本身固定占用8字节内存,而指针的数据类型则决定了通过该指针进行操作(如读写、算术运算)时访问内存的粒度。下一节我们将通过代码实践来巩固这些概念。

012:指针变量与指针数据类型 📌

在本节课中,我们将学习如何在C程序中存储和操作内存地址,即指针。我们将重点理解如何声明指针变量,以及如何通过指针变量来操作数据。


概述

在之前的课程中,我们了解到内存地址就是指针。本节我们将通过一个具体的例子,学习如何在程序中存储指针,并理解指针变量与普通变量的区别。


变量与数据存储

在C程序中,要存储一个数据值,我们首先需要创建一个变量。例如,要存储温度值,我们可以创建一个float类型的变量temp,并将其初始化为17。变量temp用于操作数据,例如比较、增减或打印温度值。

代码示例:

float temp = 17;

变量的核心作用是操作数据。同样,要操作一个指针,我们也需要创建一个指针变量。


指针变量的声明与操作

指针变量用于存储内存地址,并允许我们对该地址进行读写、递增或递减等操作。声明指针变量时,需要使用特定的指针数据类型。

以下是声明指针变量的步骤:

  1. 指定指针所指向的数据类型。
  2. 在变量名前使用星号*,以区分普通变量和指针变量。

代码示例:

char *ptr;  // 声明一个指向字符类型的指针变量
int *intPtr; // 声明一个指向整数类型的指针变量

星号*是区分普通变量和指针变量的关键。例如,int表示整数类型,而int *表示指向整数的指针类型。


在程序中存储指针

现在,让我们在具体的程序中看看如何存储一个指针。首先,我们假设有一个内存地址0x7ffeeb5b9a3c。虽然这是一个地址,但对于编译器来说,它只是一个数值。

初始尝试(错误示范):

long long addressVar = 0x7ffeeb5b9a3c;

上面的代码只是将一个很大的数值存储在一个long long类型的变量中。对于编译器而言,addressVar只是一个普通变量,而不是指针变量。

为了告诉编译器这是一个内存地址,我们需要使用指针变量并进行显式类型转换。

正确方法:

char *addressPtr = (char *)0x7ffeeb5b9a3c;

在这行代码中:

  • char * 是指针数据类型,表示指向char类型的指针。
  • addressPtr 是指针变量名。
  • (char *) 是显式类型转换,它告诉编译器后面的数值0x7ffeeb5b9a3c是一个地址,而不是普通的long long数值。

如果不进行显式转换,编译器会报错,因为它无法自动将一个大整数识别为指针。


核心概念总结

  • 指针:即内存地址。
  • 指针变量:用于存储和操作内存地址的变量。其声明需要在数据类型后加星号*,例如 int *ptr
  • 类型转换:将一个数值(如内存地址)转换为指针类型时,必须使用显式类型转换,例如 (int *)0x1000

总结

本节课我们一起学习了指针的核心概念。我们了解到,要操作内存地址,必须声明指针变量,并使用正确的数据类型和显式类型转换来存储地址值。指针变量使我们能够对特定的内存位置进行读写和修改操作,这是嵌入式系统编程中直接操作硬件的基础。在接下来的课程中,我们将进一步学习如何使用指针进行实际的读写操作。

013:指针读写操作 📝

在本节课中,我们将学习如何通过指针进行数据的读取和写入操作。理解指针的读写是掌握C语言内存管理的关键步骤。

上一节我们介绍了指针的基本概念和地址操作,本节中我们来看看如何通过指针来访问和修改内存中的数据。

读取指针数据

要从指针读取数据,必须对指针进行解引用操作。解引用意味着访问指针所指向的内存地址中存储的实际值。

以下是读取指针数据的基本语法:

data_type variable_name = *pointer_name;

在这个上下文中,星号 * 也被称为解引用运算符。编译器会根据指针的类型(例如 char*)从指针指向的地址获取相应大小的数据(例如1字节),并将其存储到指定的变量中。

指针读写实践示例

为了更好地理解,我们创建一个具体的示例程序。该程序将演示如何声明指针、为其赋值、通过指针读取值,以及查看不同变量的地址。

以下是完整的示例代码:

#include <stdio.h>

int main() {
    // 声明并初始化三个整型变量
    int m = 10;
    int n;
    int o;

    // 声明一个整型指针变量,并将变量m的地址赋给它
    int* z = &m;

    // 打印指针z中存储的地址(即m的地址)
    printf("z 存储的地址 = %p\n", z);

    // 通过解引用指针z来读取并打印m的值
    printf("*z 存储的值 = %d\n", *z);

    // 直接使用&运算符打印变量m的地址
    printf("&m 是 m 的地址 = %p\n", &m);

    // 打印变量n和o的地址
    printf("&n 是 n 的地址 = %p\n", &n);
    printf("&o 是 o 的地址 = %p\n", &o);

    // 打印指针变量z自身的地址
    printf("&z 是 z 的地址 = %p\n", &z);

    return 0;
}

代码输出与分析

运行上述程序后,你将在控制台看到类似以下的输出:

z 存储的地址 = 0x7ffd4ffa456c
*z 存储的值 = 10
&m 是 m 的地址 = 0x7ffd4ffa456c
&n 是 n 的地址 = 0x7ffd4ffa4570
&o 是 o 的地址 = 0x7ffd4ffa4574
&z 是 z 的地址 = 0x7ffd4ffa4560

对输出结果的分析如下:

  1. 指针存储的地址:指针变量 z 中存储的值是变量 m 的内存地址(例如 0x7ffd4ffa456c)。
  2. 通过指针读取的值:通过对指针 z 解引用(*z),我们得到了变量 m 中存储的整数值 10
  3. 变量地址的一致性:直接通过 &m 获取的地址与指针 z 中存储的地址完全相同,这验证了 z = &m; 这条赋值语句的作用。
  4. 不同变量的地址:变量 noz 都拥有各自独立且不同的内存地址。每个变量在内存中都有其专属的位置。
  5. 打印地址的格式说明符:在 printf 函数中,使用 %p 作为格式说明符来打印内存地址。

核心要点总结

本节课中我们一起学习了指针的核心读写操作:

  • 读取数据:通过对指针使用解引用运算符 * 来获取其指向地址中的数据。
  • 地址与值的区分:指针变量本身存储的是一个地址,通过解引用才能得到该地址处的值。
  • &* 运算符& 用于获取变量的地址,* 用于获取指针所指地址的值,两者互为逆操作。
  • 内存视图:每个变量都有唯一的内存地址,可以使用 & 运算符查看。指针使我们能够间接地通过这些地址来操作数据。

理解这些概念是进行底层内存操作、数据结构和高效嵌入式编程的基础。

014:指针练习1与练习2理解

在本节课程中,我们将学习并完成两个关于指针的编程练习。指针是C语言中一个核心且强大的概念,理解它对于嵌入式系统开发至关重要。我们将通过动手实践来加深对指针操作的理解。

练习一:基础指针操作

上一节我们介绍了指针的基本概念,本节中我们来看看第一个练习的具体要求。这个练习旨在帮助你熟悉如何声明指针、如何通过指针访问和修改变量的值。

以下是练习一的预期输出步骤:

  1. 打印一个整型变量的地址。
  2. 打印该整型变量的值。
  3. 声明一个指针变量,将其指向该整型变量。
  4. 打印指针变量自身的地址。
  5. 打印指针变量所存储的内容(即它所指向的变量的地址)。
  6. 通过指针变量,将一个新的值赋给原始整型变量。
  7. 再次打印指针变量的地址和内容,观察变化。

这个练习的关键在于理解:通过指针修改变量值后,变量本身的值会改变,但指针变量自身的地址以及它存储的地址(指向关系)通常不会改变。

完成此练习后,你将得到类似下方的输出结果:

地址 of number: 0x7ffc7d2b5a34
值 of number: 10
地址 of pointer: 0x7ffc7d2b5a38
内容 of pointer: 0x7ffc7d2b5a34
地址 of pointer: 0x7ffc7d2b5a38
内容 of pointer: 0x7ffc7d2b5a34
值 of number (通过指针修改后): 20

练习二:多类型指针操作

在掌握了基础指针操作后,我们将进行一个稍复杂的练习。这个练习将演示如何为不同数据类型的变量使用对应的指针。

以下是练习二的具体任务描述:

  • 声明三个不同类型的变量:一个整型 (int)、一个浮点型 (float)、一个字符型 (char),并为它们赋予初始值(例如:1003.14‘A’)。
  • 使用取地址运算符 & 打印这三个变量的地址。
  • 使用解引用运算符 * 打印这些地址上的值。例如:值 at 地址 0x... 是 100
  • 声明三个对应的指针变量:一个整型指针 (int *)、一个浮点型指针 (float *)、一个字符型指针 (char *)。
  • 使用这些指针变量,再次打印三个原始变量的地址。
  • 最后,仅使用指针变量和 * 运算符,打印出各个变量的值。

这个练习的代码框架可能如下所示:

#include <stdio.h>

int main() {
    int intVar = 100;
    float floatVar = 3.14;
    char charVar = ‘A’;

    int *intPtr;
    float *floatPtr;
    char *charPtr;

    // 你的代码将在这里实现上述步骤
    // 例如: printf(“地址 of intVar: %p\n”, &intVar);
    // 例如: intPtr = &intVar; // 将指针指向变量

    return 0;
}

完成此练习后,你将得到类似下方的输出,它清晰地展示了变量、地址和指针之间的关系:

地址 of intVar: 0x7ffc5a4b8b34
地址 of floatVar: 0x7ffc5a4b8b38
地址 of charVar: 0x7ffc5a4b8b3c
值 at 地址 0x7ffc5a4b8b34 是 100
值 at 地址 0x7ffc5a4b8b38 是 3.140000
值 at 地址 0x7ffc5a4b8b3c 是 A
通过指针打印地址...
intPtr 指向的地址: 0x7ffc5a4b8b34
floatPtr 指向的地址: 0x7ffc5a4b8b38
charPtr 指向的地址: 0x7ffc5a4b8b3c
通过指针打印值...
值 via intPtr: 100
值 via floatPtr: 3.140000
值 via charPtr: A

总结

本节课中我们一起学习了两个指针练习。第一个练习巩固了指针的声明、赋值以及通过指针修改变量的基础操作。第二个练习则扩展了应用,演示了如何为不同数据类型的变量使用对应的指针,并熟练运用 &* 运算符。通过这两个练习,你应该对指针如何作为“内存地址的持有者”以及如何通过它间接操作数据有了更直观的理解。请准备好你的开发环境,我们将在接下来的视频中开始动手编写代码。

015:指针练习一实现 🔧

在本节课程中,我们将通过一个具体的编程练习,来学习如何在C语言中声明、初始化和使用指针变量。我们将创建一个简单的程序,观察如何通过变量本身和指针来访问与修改变量的地址和值。


概述

指针是C语言中一个核心且强大的概念,它允许我们直接操作内存地址。理解指针对于嵌入式系统开发至关重要。本节课,我们将编写一个程序,完成以下任务:

  1. 声明一个整数变量和一个指向它的指针。
  2. 打印变量的地址和值。
  3. 将变量的地址赋值给指针。
  4. 通过指针访问和修改变量的值。
  5. 观察修改变量值或指针内容时,地址和值的变化情况。

代码实现步骤

以下是实现上述功能的完整步骤和代码。

首先,我们创建一个新的C项目,并添加一个源文件 main.c

1. 包含头文件与主函数框架

我们首先包含标准输入输出头文件,并建立主函数的基本结构。

#include <stdio.h>

int main() {
    // 后续代码将写在这里
    return 0;
}

2. 声明变量并初始化

在主函数内部,我们声明一个整数变量和一个指向整数的指针变量,并为整数变量赋初值。

    int *ptr_num; // 声明一个整数指针
    int num = 50; // 声明并初始化一个整数变量

3. 打印变量的原始信息

使用 printf 函数,我们首先打印变量 num 自身的地址和值。

    printf("\nAddress of num: %p", &num);
    printf("\nValue of num: %d", num);

4. 将变量地址赋值给指针

接下来,我们将变量 num 的地址赋值给指针变量 ptr_num

    ptr_num = &num; // 将 num 的地址赋给指针 ptr_num
    printf("\n\nNow, ptr_num is assigned with the address of num.");

5. 通过指针访问信息

赋值后,我们通过指针 ptr_num 来打印它自身存储的地址(即 num 的地址)以及它所指向的值。

    printf("\nAddress of pointer ptr_num: %p", ptr_num);
    printf("\nContent of pointer ptr_num: %d", *ptr_num);

6. 修改变量的值

现在,我们直接修改变量 num 的值,然后再次通过指针查看其内容是否同步变化。

    num = 70; // 直接修改 num 的值
    printf("\n\nThe value of num assigned to 70.");
    printf("\nAddress of pointer ptr_num: %p", ptr_num);
    printf("\nContent of pointer ptr_num: %d", *ptr_num);

7. 通过指针修改变量的值

最后,我们通过解引用指针 *ptr_num 来修改它所指向的内存内容(即 num 的值),并检查变量 num 的地址和值是否受到影响。

    *ptr_num = 100; // 通过指针修改 num 的值
    printf("\n\nThe pointer variable ptr_num is assigned the value 100 now.");
    printf("\nAddress of num: %p", &num);
    printf("\nValue of num: %d", num);

程序运行结果分析

运行上述程序,你将观察到类似以下的输出:

Address of num: 0x7ffc065fc8
Value of num: 50

Now, ptr_num is assigned with the address of num.
Address of pointer ptr_num: 0x7ffc065fc8
Content of pointer ptr_num: 50

The value of num assigned to 70.
Address of pointer ptr_num: 0x7ffc065fc8
Content of pointer ptr_num: 70

The pointer variable ptr_num is assigned the value 100 now.
Address of num: 0x7ffc065fc8
Value of num: 100

从输出中可以总结出以下关键点:

  • 地址不变:变量 num 的地址(0x7ffc065fc8)在整个程序运行期间始终不变。
  • 指针存储地址:指针 ptr_num 存储的值就是 num 的地址。
  • 值同步变化:无论是直接修改 num,还是通过 *ptr_num 修改,另一方访问到的值都会同步更新。这证明指针 ptr_num 和变量 num 指向同一块内存。
  • 地址独立于值:修改变量的值不会改变其内存地址。

总结

在本节课中,我们一起完成了一个基础的指针练习。我们学习了:

  1. 如何声明指针变量(int *ptr;)。
  2. 如何使用取地址运算符(&)获取变量的地址并赋值给指针。
  3. 如何使用解引用运算符(*)通过指针访问或修改其指向的变量值。
  4. 理解了变量地址的固定性,以及通过变量本身或指针修改变量值的等价性。

这个练习清晰地展示了指针作为“内存地址引用”的本质。在下一节中,我们将继续进行第二个指针练习,以加深对这一核心概念的理解。

016:指针练习2实现部分1 🎯

在本节课程中,我们将完成关于指针变量的第二个练习。我们将创建一个新的C/C++项目,编写代码来声明和初始化变量及其对应的指针,并学习如何使用&(取地址)运算符来获取变量的内存地址。


项目与文件创建

首先,创建一个新的C/C++项目。项目名称可以定为 pointer_exercise2。其他设置保持不变。

在新项目中,创建一个源文件,命名为 main.c,并开始编写代码。


代码实现步骤

以下是实现该练习的具体步骤。

1. 包含头文件与主函数框架

代码从包含标准输入输出头文件开始,并定义主函数 main

#include <stdio.h>

int main()
{
    // 代码将写在这里
    return 0;
}

2. 声明并初始化变量

在主函数内部,我们声明并初始化三个不同类型的变量。

    int number1 = 400;        // 整型变量
    float f1 = 9.8;           // 浮点型变量
    char chr = 'F';           // 字符型变量

3. 声明指针变量

接下来,声明对应类型的指针变量。

    int *ptr;      // 整型指针
    float *fptr;   // 浮点型指针
    char *cptr;    // 字符型指针

4. 为指针变量赋值

将之前声明的普通变量的地址赋值给对应的指针变量。

    ptr = &number1;  // ptr 存储 number1 的地址
    fptr = &f1;      // fptr 存储 f1 的地址
    cptr = &chr;     // cptr 存储 chr 的地址

5. 打印变量的值

使用变量本身直接打印它们的值。

    printf("number1 = %d\n", number1);
    printf("f1 = %f\n", f1);
    printf("chr = %c\n", chr);

6. 使用 & 运算符打印地址

接下来,我们使用 &(取地址)运算符来获取并打印每个变量的内存地址。

    printf("\nUsing & operator:\n");
    printf("Address of number1 = %p\n", &number1);
    printf("Address of f1 = %p\n", &f1);
    printf("Address of chr = %p\n", &chr);

本节总结

在本节课中,我们一起学习了指针练习的第二部分。我们创建了整型、浮点型和字符型变量及其对应的指针,并实践了如何将变量的地址赋值给指针。最后,我们使用 printf 函数和 & 运算符打印了这些变量的值和内存地址。

下一节视频中,我们将继续完成本练习的剩余部分,学习如何使用 *(解引用)运算符。


注意:本教程严格遵循原文每一句话的含义,删除了所有语气词,并按照指定的Markdown格式、标题风格和行文结构进行组织,以确保内容清晰、流畅且适合初学者理解。

017:指针练习2 - 实现部分2

在本节课程中,我们将继续完成指针练习的第二部分。我们将学习如何在不使用指针变量的情况下,直接通过地址运算符和间接运算符来访问变量的地址和值,并对比使用指针变量进行相同操作的方法。

上一节我们介绍了指针的基本概念和声明。本节中,我们来看看如何通过不同的方式操作指针。

以下是实现代码的核心步骤。

首先,我们声明并初始化三个不同类型的变量。

int numb_one = 10;
float f_one = 20.5;
char chr_one = 'A';

接下来,我们打印这些变量的初始值。

printf("Value of numb_one: %d\n", numb_one);
printf("Value of f_one: %f\n", f_one);
printf("Value of chr_one: %c\n", chr_one);

然后,我们使用地址运算符 & 和间接运算符 * 的组合,直接打印变量地址处的值,而不借助指针变量。

printf("Value at address of numb_one is: %d\n", *(&numb_one));
printf("Value at address of f_one is: %f\n", *(&f_one));
printf("Value at address of chr_one is: %c\n", *(&chr_one));

之后,我们声明对应的指针变量,并将变量的地址赋值给它们。

int *ptr = &numb_one;
float *f_ptr = &f_one;
char *c_ptr = &chr_one;

现在,我们仅使用指针变量来打印各变量的地址。

printf("Address of numb_one (using pointer): %p\n", ptr);
printf("Address of f_one (using pointer): %p\n", f_ptr);
printf("Address of chr_one (using pointer): %p\n", c_ptr);

最后,我们通过指针变量,使用间接运算符 * 来访问并打印变量的值。

printf("Value at address of numb_one (using pointer): %d\n", *ptr);
printf("Value at address of f_one (using pointer): %f\n", *f_ptr);
printf("Value at address of chr_one (using pointer): %c\n", *c_ptr);

编译并运行程序后,输出结果将验证我们的操作:

  1. 变量的初始值被正确打印。
  2. 使用 *(&variable) 直接获取的地址处的值与变量值一致。
  3. 通过指针变量打印的地址与直接使用 & 运算符获取的地址完全相同。
  4. 通过指针变量使用 * 运算符解引用获得的值也与变量原始值一致。

本节课中我们一起学习了指针的多种访问方式。我们实践了如何不通过指针变量而直接操作地址,也对比了使用指针变量进行寻址和解引用的标准方法。通过这个练习,你应该对C语言中指针的基本用法有了更扎实的理解。

018:什么是stdint头文件 📚

在本节课中,我们将要学习 stdint.h 这个标准库头文件的重要性。理解它如何帮助我们编写可移植性更强、更可靠的嵌入式C语言代码。

为什么需要 stdint.h? 🤔

上一节我们介绍了嵌入式编程的基础,本节中我们来看看代码可移植性面临的一个具体挑战。

假设你编写了一个C程序,其中使用了特定的数据类型(如 int, long)。当你更换编译器时,这些数据类型的大小可能会发生变化。代码虽然可能编译通过,但会引入潜在的、难以发现的错误,这就是可移植性问题

可移植性问题的根源

C语言标准(如C99)并没有严格规定 intlong 等基本数据类型的确切大小(例如是2字节还是4字节)。标准只定义了这些类型的最小值范围。具体的大小由编译器设计者根据目标硬件平台(如8位、32位微控制器)的架构和效率来决定。

以下是两个例子:

  • XC8编译器(针对8位PIC MCU)可能将 int 定义为2字节。
  • ARM编译器(针对32位Cortex-M MCU)可能将 int 定义为4字节。

一个具体的例子

让我们通过一段代码来直观地理解这个问题。

unsigned int count = 0;
count++;
if (count > 65536) {
    // 执行某些任务
}

这段代码在不同编译器下的行为:

  • 在将 unsigned int 视为 4字节 的编译器上,count 可以超过65536,条件判断可能为真,任务得以执行。
  • 在将 unsigned int 视为 2字节 的编译器上,count 的最大值是65535。当增加到65536时,值会回绕到0。因此,count > 65536 这个条件永远为假,预设的任务永远不会执行。

这就导致了同一份代码在不同平台/编译器下行为不一致的严重问题。

解决方案:使用 stdint.h

为了解决上述因数据类型大小不明确导致的问题,我们需要停止直接使用 intlong 这类“模糊”的标准类型。取而代之的是使用定义在 stdint.h 头文件中的固定宽度整数类型

这些类型通过 typedef(类型别名)的方式,为C语言的标准数据类型赋予了新的、含义明确的名字。它们不是新的数据类型,而是已有类型的别名,但其名称直接指明了确切的位宽。

你需要将 stdint.h 包含到你的项目中才能使用这些别名。

以下是 stdint.h 中定义的一些常用固定宽度整数类型别名:

  • 8位整数
    • int8_t: 精确的8位有符号整数。
    • uint8_t: 精确的8位无符号整数。
  • 16位整数
    • int16_t: 精确的16位有符号整数。
    • uint16_t: 精确的16位无符号整数。
  • 32位整数
    • int32_t: 精确的32位有符号整数。
    • uint32_t: 精确的32位无符号整数。
  • 64位整数
    • int64_t: 精确的64位有符号整数。
    • uint64_t: 精确的64位无符号整数。

使用这些类型,我们可以将前面有问题的代码重写为可移植的形式:

#include <stdint.h> // 包含头文件

uint32_t count = 0; // 明确使用32位无符号整数
count++;
if (count > 65536) {
    // 执行某些任务
}

现在,无论使用哪个编译器,count 都被明确定义为32位无符号整数,代码的行为在所有平台上都将保持一致。

总结 📝

本节课中我们一起学习了 stdint.h 头文件的核心价值。我们了解到,直接使用 intlong 等原生C类型会导致代码可移植性差,因为它们的位宽依赖于编译器。通过引入并使用 stdint.h 中定义的固定宽度整数类型(如 uint32_t),我们可以明确指定变量的位宽,从而编写出在不同硬件平台和编译器之间都能行为一致、可靠的嵌入式C代码。这是编写专业嵌入式软件的重要基石。在接下来的视频中,我们将继续深入探讨 stdint.h 的更多细节。

019:p19 02_01_02_理解 stdint.h 📚

在本节课中,我们将要学习 stdint.h 头文件。这个文件定义了标准整数类型的别名,是编写可移植嵌入式代码的关键。我们将了解它的作用、如何找到它,以及其中定义的一些有用类型。

上一节我们介绍了标准数据类型别名,本节中我们来看看这些别名是如何在 stdint.h 文件中被定义和管理的。

核心概念:stdint.h 的作用

stdint.h 头文件管理所有标准整数类型的别名。使用这些别名定义变量,可以确保变量在不同编译器和架构下具有确定的大小,从而避免程序错误或漏洞。

例如,使用 int32_t 定义变量 count

int32_t count;

这行代码会确保 count 变量始终占用 32 位,无论使用何种编译器。

如何定位 stdint.h 文件

stdint.h 是标准库头文件,位于工具链的安装目录中。以下是查找方法:

  • 对于 MinGW 编译器,路径通常类似于:C:\tools\MinGW\i686-w64-mingw32\include\stdint.h
  • 对于 ARM Cortex 工具链(如 STM32CubeIDE),路径通常类似于:...\STM32CubeIDE\plugins\...\tools\arm-none-eabi\include\stdint.h

不同编译器的 stdint.h 文件内容可能不同,这是由编译器自身定义的。

stdint.h 中的有用类型别名

除了基本的固定宽度整数类型(如 int8_t, uint16_t),stdint.h 还定义了一些其他有用的类型和宏。

以下是几个关键的类型和宏定义:

  • UINT8_MAX, UINT16_MAX, UINT32_MAX, UINT64_MAX:这些宏定义了对应无符号整数类型能表示的最大值。
  • INT8_MAX, INT8_MIN:这些宏定义了对应有符号整数类型能表示的最大值和最小值。
  • uintptr_tUINTPTR_MAXuintptr_t 是一个无符号整数类型,其宽度足以存储一个指针的值。UINTPTR_MAX 是其最大值。当你不确定目标架构的指针大小时,可以使用这个类型。

使用建议

在为未知架构(如 PIC、RISC-V)编写代码时,如果无法确定指针的大小,可以使用 uintptr_t 类型来定义指针变量。stdint.h 头文件会负责将其映射到当前系统合适的指针数据类型,你无需担心具体的指针大小。

在后续的实际编程中开始使用这些类型,你会更深刻地理解它们的便利性。

本节课中我们一起学习了 stdint.h 头文件的重要性、如何找到它,以及其中定义的核心类型别名。掌握这些知识是编写跨平台、可移植嵌入式代码的基础。下一节我们将继续学习其他相关内容。

构建嵌入式系统:ARM Cortex (STM32) 基础:第 02 章:第 02 节:C语言中的运算符

概述

在本节中,我们将学习C语言中的运算符。运算符是告诉编译器执行特定数学或逻辑操作的符号。理解运算符及其优先级对于编写正确的C语言程序至关重要。

运算符类型

C语言中的运算符主要分为三类:一元运算符、二元运算符和三元运算符。

上一节我们介绍了C语言的基础知识,本节中我们来看看具体的运算符。

以下是主要的运算符分类:

  • 一元运算符:作用于单个操作数。例如:
    • ++:自增运算符
    • --:自减运算符
  • 二元运算符:作用于两个操作数。它包含多个子类:
    • 算术运算符+(加), -(减), *(乘), /(除), %(取模)
    • 关系运算符<(小于), <=(小于等于), >(大于), >=(大于等于), ==(等于), !=(不等于)
    • 逻辑运算符&&(逻辑与), ||(逻辑或), !(逻辑非)
    • 位运算符&(按位与), |(按位或), ^(按位异或), ~(按位取反), <<(左移), >>(右移)
    • 赋值运算符=(基本赋值), +=-=*=/=%=
  • 三元运算符:作用于三个操作数。只有一个:
    • ? ::条件运算符

运算符优先级

当表达式中包含多个运算符时,编译器需要决定运算的先后顺序,这个规则就是运算符优先级。

为了理解优先级,我们来看一个例子。首先,我们创建一个简单的C程序。

#include <stdio.h>

int main(void) {
    int num1 = 34;
    int num2 = 56;
    int num3 = 67;
    int result;

    result = num1 + num2 * num3;
    printf("The result is: %d\n", result);

    return 0;
}

运行这段代码,输出结果是 3786。这是因为乘法运算符 * 的优先级高于加法运算符 +。所以表达式 num1 + num2 * num3 等价于 num1 + (num2 * num3)

如果我们想改变运算顺序,可以先计算加法,可以使用圆括号 ()

result = (num1 + num2) * num3;
printf("The result is: %d\n", result);

此时,输出结果会变为 (34 + 56) * 67 = 6030。圆括号拥有最高的优先级。

复杂表达式计算

现在,我们来看一个包含更多运算符的复杂表达式,以深入理解优先级规则。

result = 34 + 56 * 67 - 20 / 4 % 8;
printf("The result is: %d\n", result);

计算这个表达式 34 + 56 * 67 - 20 / 4 % 8 的步骤如下:

  1. 处理乘、除、取模:在 +- 之前,先计算 */%。并且 /% 的优先级相同,按从左到右的顺序结合。

    • 先计算 56 * 67 = 3752。表达式变为 34 + 3752 - 20 / 4 % 8
    • 接着计算 20 / 4 = 5。表达式变为 34 + 3752 - 5 % 8
    • 最后计算 5 % 8 = 5。表达式变为 34 + 3752 - 5
  2. 处理加法和减法+- 的优先级相同,按从左到右的顺序结合。

    • 先计算 34 + 3752 = 3786
    • 再计算 3786 - 5 = 3781

因此,最终结果是 3781。运行程序验证,输出结果正是 3781

提示:你不需要死记硬背所有运算符的优先级顺序。在实际编程中,如果对运算顺序不确定,最清晰、最安全的方法是使用圆括号 () 来明确指定计算顺序。你也可以随时查阅C语言标准或参考资料来确认优先级。

总结

本节课中我们一起学习了C语言中的运算符。我们首先了解了运算符的主要类型:一元、二元和三元运算符。然后,通过具体的代码示例,重点讲解了运算符优先级的概念,即当表达式中存在多个运算符时,编译器决定运算先后次序的规则。我们通过计算 34 + 56 * 67 - 20 / 4 % 8 这个表达式,演示了乘法、除法、取模运算符优先于加法和减法,以及同优先级运算符从左到右结合的规则。记住,使用圆括号可以灵活地控制运算顺序,这是编写清晰、无误代码的好习惯。在下一节,我们将探讨一元运算符的具体用法。

021:C语言中的一元运算符

在本节课程中,我们将学习C语言中的一元运算符。我们将通过具体的例子,详细探讨一元递增和递减运算符的工作原理,包括它们的前置与后置形式。

概述

一元运算符是仅需要一个操作数的运算符。在C语言中,最常见的两种一元运算符是递增运算符 ++ 和递减运算符 --。它们分别用于将变量的值增加1或减少1。理解这些运算符的前置与后置形式对于编写正确的程序逻辑至关重要。

一元递增运算符

一元递增运算符 ++ 用于将变量的值增加1。根据运算符相对于操作数的位置,它分为后置递增前置递增两种形式。

以下是两种形式的定义和区别:

  • 后置递增:运算符位于操作数之后,格式为 变量++。其执行顺序是:先使用变量的当前值,然后再将变量的值增加1
  • 前置递增:运算符位于操作数之前,格式为 ++变量。其执行顺序是:先将变量的值增加1,然后再使用这个新值

为了清晰地展示这一区别,我们来看一个代码示例。

#include <stdio.h>

int main() {
    int u_in1 = 34; // 定义并初始化变量 u_in1
    int result2, rs2; // 定义结果变量

    // 后置递增示例
    result2 = u_in1++; // 先将 u_in1 的值 (34) 赋给 result2,然后 u_in1 自增为 35
    printf("后置递增 - result2 的值: %d\n", result2);
    printf("后置递增 - u_in1 的值: %d\n", u_in1);

    // 前置递增示例
    rs2 = ++u_in1; // 先将 u_in1 的值 (35) 自增为 36,然后将新值 (36) 赋给 rs2
    printf("前置递增 - rs2 的值: %d\n", rs2);
    printf("前置递增 - u_in1 的值: %d\n", u_in1);
}

运行上述代码,输出结果将验证我们的分析:

  • 后置递增后,result2 的值为 34,而 u_in1 的值变为 35
  • 前置递增后,rs2 的值为 36u_in1 的值也变为 36

一元递减运算符

理解了递增运算符后,递减运算符 -- 就很容易掌握了。它的逻辑与递增运算符完全一致,只是作用是将变量的值减少1。

同样,递减运算符也分为两种形式:

  • 后置递减:格式为 变量--。执行顺序是:先使用变量的当前值,然后再将变量的值减少1
  • 前置递减:格式为 --变量。执行顺序是:先将变量的值减少1,然后再使用这个新值

让我们通过另一个代码示例来巩固理解。

#include <stdio.h>

int main() {
    int u_in2 = 56; // 定义并初始化变量 u_in2
    int result2_dec, rs2_dec; // 定义结果变量

    // 后置递减示例
    result2_dec = u_in2--; // 先将 u_in2 的值 (56) 赋给 result2_dec,然后 u_in2 自减为 55
    printf("后置递减 - result2_dec 的值: %d\n", result2_dec);
    printf("后置递减 - u_in2 的值: %d\n", u_in2);

    // 前置递减示例
    rs2_dec = --u_in2; // 先将 u_in2 的值 (55) 自减为 54,然后将新值 (54) 赋给 rs2_dec
    printf("前置递减 - rs2_dec 的值: %d\n", rs2_dec);
    printf("前置递减 - u_in2 的值: %d\n", u_in2);
}

这段代码的输出将清晰地展示:

  • 后置递减后,result2_dec 的值为 56,而 u_in2 的值变为 55
  • 前置递减后,rs2_dec 的值为 54u_in2 的值也变为 54

总结

本节课我们一起学习了C语言中的一元运算符。我们重点探讨了:

  1. 一元递增运算符 (++):用于将操作数的值加1。分为后置递增(先取值,后自增)和前置递增(先自增,后取值)。
  2. 一元递减运算符 (--):用于将操作数的值减1。同样分为后置递减(先取值,后自减)和前置递减(先自减,后取值)。

掌握这些运算符的前置与后置区别,对于避免程序中的逻辑错误和编写高效的嵌入式C代码非常重要。在接下来的课程中,我们将继续学习其他类型的运算符,例如与指针变量相关的运算符。

嵌入式系统构建:ARM Cortex (STM32) 基础:P22:指针与一元运算符

在本节课程中,我们将学习如何在C语言中对指针变量使用一元运算符(自增和自减)。我们将通过具体的代码示例,理解这些操作如何影响指针的值。


概述

指针是嵌入式C编程中的核心概念。理解如何操作指针对于直接访问和控制内存至关重要。本节将重点介绍对指针使用一元自增 (++) 和自减 (--) 运算符的行为。


指针变量的定义与初始化

首先,我们来看一个指针变量的定义和初始化示例。

uint32_t* p_address = (uint32_t*)0xFFFF0000;

在这行代码中:

  • uint32_t* 声明了一个指向 uint32_t 类型数据的指针变量。
  • p_address 是指针变量的名称。
  • (uint32_t*) 是一个类型转换,将后面的地址值转换为指向 uint32_t 的指针类型。
  • 0xFFFF0000 是赋予指针的初始内存地址。

此时,指针 p_address 指向内存地址 0xFFFF0000


使用算术运算符操作指针

如果我们想将指针移动到下一个内存位置,可以使用算术加法。

p_address = p_address + 1;

执行此操作后,p_address 的值不会简单地变为 0xFFFF0001。因为 p_address 是指向 uint32_t 的指针,而 uint32_t 类型通常占用4个字节。所以,“加1”意味着向前移动一个 uint32_t 数据单元的大小,即4个字节。

因此,最终结果将是:
p_address = 0xFFFF0004


使用一元自增运算符操作指针

上一节我们介绍了使用算术加法来移动指针。实际上,有一种更简洁的写法可以达到完全相同的目的,即使用一元自增运算符。

p_address++;

这行代码 p_address++p_address = p_address + 1 完全等效。它同样会使指针的值增加一个其所指数据类型的大小(本例中为4字节)。

执行结果同样是:
p_address = 0xFFFF0004

重要提示: 无论是使用 p_address + 1 还是 p_address++,指针的增量都取决于其指向的数据类型(uint32_t 为4字节)。两种方法的结果完全相同。


核心概念总结

对指针进行自增或自减操作时,其值的变化量由指针的类型决定。编译器会根据 sizeof(指针所指向的类型) 来计算实际要增加或减少的字节数。

公式表示:
新地址 = 旧地址 ± (n * sizeof(数据类型))
其中,n 是自增或自减的数值(例如 ++ 对应 n=1)。

以下是指向不同数据类型的指针进行 p++ 操作后的结果示例:

  • char *p; (sizeof(char)=1) -> 地址增加 1 字节。
  • uint32_t *p; (sizeof(uint32_t)=4) -> 地址增加 4 字节。
  • float *p; (sizeof(float)=4) -> 地址增加 4 字节。

总结

本节课中,我们一起学习了指针与一元运算符的使用:

  1. 我们回顾了指针变量的定义和初始化方法。
  2. 我们理解了使用算术运算符(如 +)操作指针时,其步长由所指数据类型决定。
  3. 我们掌握了使用一元自增 (++) 和自减 (--) 运算符来移动指针,这是更简洁的语法。
  4. 我们明确了关键点:指针算术的步进单位是其指向类型的字节大小,而非固定的1字节。

掌握指针的算术运算是进行有效内存访问和数组操作的基础。在下一节视频中,我们将探讨C语言中的关系运算符。


023:C语言中的关系运算符

在本节课中,我们将学习C语言中的关系运算符。关系运算符用于比较两个值,并根据比较结果返回一个布尔值。这些运算符是编写条件判断语句的基础,在嵌入式系统编程中至关重要。

关系运算符概述

上一节我们介绍了运算符的基本概念。本节中,我们来看看专门用于比较的关系运算符。关系运算符对操作数进行某种评估,并返回一个值。这个返回值在C语言中以整数形式表示:1 代表“真”(True),0 代表“假”(False)。

关系运算符也被称为二元运算符,因为它们需要至少两个操作数才能进行运算。这些运算符的求值顺序总是从左到右。

关系运算符列表

以下是C语言中可用的关系运算符列表。我们之前在运算符的入门介绍中已经简要提及过它们。

  • 等于 (==): 用于比较两个操作数是否相等。注意,单个等号 (=) 是赋值运算符,而双等号 (==) 才是比较运算符。
  • 大于 (>): 检查左侧操作数是否大于右侧操作数。
  • 小于 (<): 检查左侧操作数是否小于右侧操作数。
  • 大于或等于 (>=): 检查左侧操作数是否大于或等于右侧操作数。
  • 小于或等于 (<=): 检查左侧操作数是否小于或等于右侧操作数。
  • 不等于 (!=): 检查两个操作数是否不相等。这是一个否定运算符。

运算符使用示例与解释

为了更好地理解,让我们通过一些代码示例来看看这些运算符是如何工作的。

假设我们定义两个变量并赋值:

int number1 = 10;
int number2 = 20;

现在,我们可以使用关系运算符来比较它们:

  • 检查相等性number1 == number2。因为10不等于20,所以这个表达式的结果是 0(假)。
  • 检查不相等number1 != number2。因为10确实不等于20,所以这个表达式的结果是 1(真)。
  • 检查大于number1 > number2。因为10不大于20,所以结果是 0
  • 检查小于number1 < number2。因为10小于20,所以结果是 1
  • 检查大于或等于number1 >= 10。因为number1等于10,满足“等于”的条件,所以结果是 1。如果只使用 >number1 > 10 的结果将是 0,因为它不大于10。这就是 >=> 的区别。
  • 检查小于或等于number1 <= 10。同理,因为等于10,所以结果是 1

在C语言中,任何求值结果为 0 的表达式都被视为“假”,而任何非零值(包括1, -1, 100等)都被视为“真”。

关系运算符的应用场景

使用关系运算符的表达式会求值为真或假。关系运算经常与 if 条件语句结合使用,用于控制程序的流程。我们将在后续的实践演示中更详细地看到它们的应用。

就像上面的例子一样,如果你想检查变量的值,你需要先创建变量并赋值,然后使用关系运算符来检查操作数之间的关系。

总结

本节课中我们一起学习了C语言中的关系运算符。我们了解了六种基本的关系运算符:==!=><>=<=。它们用于比较两个值,并返回1(真)或0(假)。理解这些运算符是掌握条件逻辑和程序控制流的第一步。在接下来的视频中,我们将继续探讨更多相关内容。

024:C语言中的逻辑运算符

在本节课中,我们将要学习C语言中的逻辑运算符。逻辑运算符是编程中用于组合或修改条件判断结果的重要工具,它们能帮助我们构建更复杂的逻辑表达式。

上一节我们介绍了关系运算符,本节中我们来看看如何将多个条件组合起来。

逻辑运算符概述

C语言提供了三种主要的逻辑运算符:逻辑与(AND)、逻辑或(OR)和逻辑非(NOT)。这些运算符用于对布尔值(真或假,在C语言中通常用非零值表示真,零表示假)进行操作。

逻辑与运算符(&&)

逻辑与运算符(&&)是一个二元运算符,它要求两个操作数。其核心规则是:只有当两个操作数的值都为真(非零)时,整个表达式的结果才为真(1)。如果其中任何一个操作数为假(0),结果就为假(0)。

以下是逻辑与运算符的真值表,它清晰地展示了所有可能的输入组合及其对应的输出结果:

  • A 为真,B 为真A && B 结果为
  • A 为真,B 为假A && B 结果为
  • A 为假,B 为真A && B 结果为
  • A 为假,B 为假A && B 结果为

让我们通过一个代码示例来理解:

int num1 = -10;
int num2 = 20;
int result = num1 && num2; // 因为num1和num2都是非零值(真),所以result的值为1(真)

如果我们将 num1 改为 0

int num1 = 0;
int num2 = 20;
int result = num1 && num2; // 因为num1为0(假),所以无论num2为何值,result都为0(假)

逻辑或运算符(||)

逻辑或运算符(||)也是一个二元运算符。它的规则是:只要至少有一个操作数的值为真(非零),整个表达式的结果就为真(1)。只有当两个操作数都为假(0)时,结果才为假(0)。

以下是逻辑或运算符的真值表:

  • A 为真,B 为真A || B 结果为
  • A 为真,B 为假A || B 结果为
  • A 为假,B 为真A || B 结果为
  • A 为假,B 为假A || B 结果为

逻辑非运算符(!)

逻辑非运算符(!)是一个一元运算符,它只对一个操作数进行运算。它的功能非常简单:对操作数的布尔值进行取反。如果操作数为真(非零),则结果为假(0);如果操作数为假(0),则结果为真(1)。

以下是逻辑非运算符的真值表:

  • A 为真!A 结果为
  • A 为假!A 结果为

运算符小结

理解这三种逻辑运算符的真值表有助于我们预测复杂逻辑表达式的最终结果。不过,更重要的是理解其核心规则:&& 要求两者皆真,|| 要求至少一真,! 则直接取反。

总结

本节课中我们一起学习了C语言的三种逻辑运算符:逻辑与(&&)、逻辑或(||)和逻辑非(!)。我们了解了它们各自的作用规则,并通过真值表直观地看到了不同输入下的输出结果。这些运算符是构建条件判断和程序控制流的基础。在接下来的课程中,我们将开始学习 if 条件语句,届时会实际运用这些逻辑运算符来编写更强大的程序逻辑。

嵌入式系统构建:ARM Cortex (STM32) 基础:构建嵌入式系统 p25 02_03_01:if语句

在本节课中,我们将要学习C语言中的决策控制,具体从if语句开始。决策控制是编程的核心,它允许程序根据特定条件执行不同的代码块。


决策控制概述

编写程序时,经常需要让某些代码仅在特定条件满足时才执行。这意味着程序必须根据内部或外部的事件或条件来做出决策。

例如,如果用户按下某个特定按键,则执行一组语句;如果用户没有按下任何按键,则执行另一组语句。在嵌入式编程中,这同样重要。

以水位指示与控制程序为例。如果传感器检测到水位超过阈值,程序就执行关闭水泵的代码;否则,程序不关闭水泵。

在C语言中,有五种实现决策控制的方式:

  1. if语句
  2. if-else语句
  3. if-else-if阶梯语句
  4. 条件运算符
  5. switch-case语句

我们将在后续视频中逐一探讨。本节我们专注于if语句。


if语句详解

if语句如其名,用于检查一个特定条件。如果条件为真(true),则执行给定的语句块;如果条件为假(false),则不会执行该块内的任何语句。

if语句有两种常见的书写格式。

第一种是单语句执行:如果条件为真,则执行紧随其后的一条语句,并以分号结束。

if (表达式)
    语句;

第二种是多语句执行:如果条件为真,则执行花括号 {} 内的所有语句。每个语句都以分号结束。

if (表达式) {
    语句1;
    语句2;
    // ... 更多语句
}

选择哪种格式取决于你需要执行的语句数量。如果只有一条语句,可以省略花括号;如果有多条语句,则必须使用花括号将它们括起来。


代码示例

让我们通过代码示例来理解这两种格式。首先,我们创建一个简单的程序。

#include <stdio.h>
#include <stdint.h>

int main() {
    uint8_t number1 = 34; // 定义一个变量并赋值为34

    // 示例1:单语句if
    if (number1 > 35)
        printf("变量值 %d 大于 35\n", number1);

    return 0;
}

在这个例子中,number1的值是34,不满足 number1 > 35 的条件,因此printf语句不会执行,程序没有输出。

现在,我们将变量值改为36,并再次运行程序。

uint8_t number1 = 36; // 修改变量值为36

此时条件为真,程序会输出:变量值 36 大于 35

接下来,我们看一个多语句执行的例子。假设在条件满足时,我们不仅要打印信息,还要增加变量的值。

#include <stdio.h>
#include <stdint.h>

int main() {
    uint8_t number1 = 36;

    // 示例2:多语句if(使用花括号)
    if (number1 >= 35) {
        printf("变量值 %d 大于或等于 35\n", number1);
        number1++; // 增加变量的值
        printf("增加后的值:%d\n", number1);
    }

    return 0;
}

在这个例子中,我们使用了 >=(大于或等于)运算符。当number1为35或更大时,条件为真。程序会执行花括号内的所有语句:先打印信息,然后递增number1,最后打印递增后的值。

重要提示

  • 如果if后面只有一条语句,花括号是可选的。
  • 如果if后面有多条语句,必须使用花括号将它们组合成一个代码块。
  • if关键字后面不能直接跟分号,因为if本身不是一个完整的语句,而是一个控制流命令。例如 if (条件); 是错误的,分号会形成一个空语句,导致后续代码块永远与if无关。

总结

本节课我们一起学习了C语言中if语句的基础知识。我们了解了决策控制在编程中的重要性,掌握了if语句的两种基本格式:用于单条语句的无括号形式和用于多条语句的花括号形式。通过代码示例,我们实践了如何根据条件执行不同的操作,并注意了编写if语句时的常见注意事项。if语句是构建更复杂决策逻辑的基石,在接下来的课程中,我们将以此为基础,学习if-else等更强大的控制结构。

026:if语句练习 🧑‍💻

在本节课中,我们将通过一个具体的编程练习来学习如何使用 if 语句。我们将编写一个程序,根据用户输入的年龄判断其是否具有投票资格。

概述

我们将创建一个C语言程序。该程序会要求用户输入年龄,然后使用 if 语句判断该年龄是否大于或等于18岁。根据判断结果,程序会输出相应的信息,告知用户是否具备投票资格。

程序实现步骤

以下是实现该功能的具体步骤。

首先,我们需要包含必要的头文件并设置主函数。为了在程序结束后等待用户按键退出,我们会使用一个简单的循环。

#include <stdio.h>

int main() {
    // 程序主体将写在这里
    printf("按回车键退出程序...\n");
    getchar(); // 等待用户按回车键
    return 0;
}

接下来,在程序主体部分,我们需要声明一个变量来存储用户的年龄,并提示用户输入。

int age = 0; // 初始化年龄变量
printf("请输入您的年龄:\n");

为了接收用户的输入,我们需要使用 scanf 函数。

scanf("%d", &age); // 读取用户输入的整数并存储到age变量中

现在,我们拥有了用户的年龄数据。核心部分在于使用 if 语句进行条件判断。判断的逻辑是:如果年龄大于或等于18岁,则具备投票资格;否则不具备。

我们可以使用两个独立的 if 语句来实现这个逻辑。

if (age >= 18) {
    printf("您符合投票年龄要求,可以投票。\n");
}
if (age < 18) {
    printf("抱歉,您未达到投票年龄要求,无法投票。\n");
}

将以上所有部分组合起来,就得到了完整的程序。

完整代码示例

以下是整合后的完整程序代码。

#include <stdio.h>

int main() {
    int age = 0;

    printf("请输入您的年龄:\n");
    scanf("%d", &age);

    if (age >= 18) {
        printf("您符合投票年龄要求,可以投票。\n");
    }
    if (age < 18) {
        printf("抱歉,您未达到投票年龄要求,无法投票。\n");
    }

    printf("\n按回车键退出程序...\n");
    getchar();
    getchar(); // 使用两个getchar()来消耗scanf留下的换行符并等待用户按键
    return 0;
}

程序运行与测试

现在,让我们来测试这个程序。编译并运行后,程序会提示输入年龄。

  • 当输入 20 时,程序输出:您符合投票年龄要求,可以投票。
  • 当输入 16 时,程序输出:抱歉,您未达到投票年龄要求,无法投票。
  • 当输入 18 时,程序输出:您符合投票年龄要求,可以投票。

程序在所有三种情况下都能正确执行,并输出相应的提示信息。

总结

本节课中,我们一起完成了一个 if 语句的练习。我们学习了如何:

  1. 使用 scanf 接收用户的整数输入。
  2. 利用 if (条件) 结构进行条件判断。
  3. 根据不同的条件(age >= 18age < 18)执行不同的代码块,并向用户反馈结果。

这个练习演示了 if 语句在程序流程控制中的基础应用。在接下来的视频中,我们将进一步探讨 if-else 等更复杂的条件判断结构。

027:if 与 else 语句 🧠

在本节课中,我们将学习 C 语言中用于决策控制的核心结构之一:ifelse 语句。我们将了解其基本语法、执行逻辑,并通过具体示例演示如何使用它来简化条件判断。

概述

ifelse 语句允许程序根据特定条件表达式的真假,选择性地执行不同的代码块。这是实现程序分支逻辑的基础。

基本语法

if-else 语句有两种主要形式:单语句执行和多语句执行。

单语句执行

当条件成立或不成立时,只需执行一条语句,可以使用以下简洁形式:

if (expression)
    statement1;
else
    statement2;

多语句执行

如果需要在条件分支中执行多条语句,必须使用花括号 {} 将这些语句组合成一个代码块:

if (expression) {
    statement1;
    statement2;
    // ... 更多语句
} else {
    statement1;
    statement2;
    // ... 更多语句
}

执行逻辑

if-else 语句的执行遵循一个清晰的模式:

  1. 首先评估 if 后面的表达式
  2. 如果表达式的结果为 真(非零),则执行紧接在 if 后面的语句或代码块。
  3. 如果表达式的结果为 假(零),则跳过 if 部分,转去执行 else 后面的语句或代码块。
  4. 关键点在于,ifelse 两部分永远不会同时执行,程序会根据条件二选一。

实践示例

上一节我们介绍了 if 语句的基本用法,本节中我们来看看如何用 if-else 来优化逻辑。

示例1:比较数字大小

以下程序判断一个变量的值是否大于35。

#include <stdio.h>

int main() {
    int number1 = 30; // 可以尝试修改此值,例如改为40

    if (number1 > 35) {
        printf("变量值 %d 大于 35。\n", number1);
    } else {
        printf("变量值 %d 小于或等于 35。\n", number1);
    }

    return 0;
}
  • number1 为 30 时number1 > 35 为假,执行 else 块,输出:“变量值 30 小于或等于 35。”
  • number1 为 40 时number1 > 35 为真,执行 if 块,输出:“变量值 40 大于 35。”

示例2:投票资格检查

这个例子检查用户年龄是否达到投票标准。

#include <stdio.h>

int main() {
    int age;

    printf("请输入您的年龄:");
    scanf("%d", &age);

    if (age >= 18) {
        printf("您符合投票资格。\n");
    } else {
        printf("抱歉,您尚未达到投票年龄。\n");
    }

    return 0;
}
  • 输入年龄 18:条件 age >= 18 为真,输出:“您符合投票资格。”
  • 输入年龄 5:条件 age >= 18 为假,执行 else 块,输出:“抱歉,您尚未达到投票年龄。”

通过使用 if-else,我们避免了像之前课程中那样使用两个独立的 if 语句进行判断,使代码更简洁、逻辑更清晰。

总结

本节课中我们一起学习了 if-else 决策语句。我们掌握了它的两种语法形式,理解了其“二选一”的执行逻辑,并通过实例看到了它如何让条件判断代码更加高效和直观。在接下来的视频中,我们将通过一个练习来巩固所学:编写一个程序,接收用户输入的两个数字,并判断并输出其中较大的数;如果两数相等,则输出“两数相等”。

028:if与else练习实现 🧮

在本节课中,我们将通过一个具体的编程练习,学习如何使用 ifelse 语句来比较两个数字的大小。我们将从基础功能开始,逐步完善程序,使其能够处理用户可能输入的各种无效数据,从而构建一个健壮的应用程序。


概述

我们将创建一个程序,要求用户输入两个数字,然后比较它们的大小并输出结果。核心任务包括:

  1. 接收用户输入的两个数字。
  2. 使用 if-else 语句判断它们是否相等,或哪个更大。
  3. 处理用户输入非整数(如小数、字符)时可能出现的错误,确保程序不会意外崩溃。

第一步:基础比较功能

首先,我们实现程序的核心逻辑:比较两个整数。

  1. 声明变量并获取输入:我们需要两个整型变量来存储用户输入的数字。

    int number1, number2;
    printf(“请输入第一个整数:”);
    scanf(“%d”, &number1);
    printf(“请输入第二个整数:”);
    scanf(“%d”, &number2);
    
  2. 实现比较逻辑:使用 if-else 语句进行判断。

    • 首先检查两数是否相等。
    • 如果不相等,则检查 number1 是否小于 number2
    • 如果以上都不成立,则 number1 大于 number2
    if (number1 == number2) {
        printf(“数字相等。\n”);
    } else {
        if (number1 < number2) {
            printf(“%d 更大。\n”, number2);
        } else {
            printf(“%d 更大。\n”, number1);
        }
    }
    

此时,程序可以正确处理整数的比较。


第二步:处理小数输入

上一节我们实现了基础比较功能,但用户可能输入小数(浮点数),这会导致程序行为异常。本节中,我们将修改程序以处理这种情况。

我们的策略是:先以浮点数格式接收输入,然后只取其整数部分进行比较,并提示用户。

  1. 修改变量类型:将接收输入的变量改为 float 类型。
    float num1_float, num2_float;
    printf(“请输入第一个数字:”);
    scanf(“%f”, &num1_float);
    printf(“请输入第二个数字:”);
    scanf(“%f”, &num2_float);
    

  1. 提取整数部分:将浮点数转换为整数,仅比较整数部分。
    int n1 = (int)num1_float;
    int n2 = (int)num2_float;
    

  1. 添加警告提示:如果原始浮点数与转换后的整数不相等,说明用户输入了小数,我们需要给出警告。
    if (n1 != num1_float || n2 != num2_float) {
        printf(“警告:仅比较整数部分。\n”);
    }
    // 使用 n1 和 n2 进行比较
    if (n1 == n2) {
        printf(“数字相等。\n”);
    } else if (n1 < n2) {
        printf(“%d 更大。\n”, n2);
    } else {
        printf(“%d 更大。\n”, n1);
    }
    

现在,程序可以处理小数输入,不会崩溃,并会给出明确提示。


第三步:处理字符输入

上一节我们解决了小数输入的问题,但如果用户输入了字符(如 ‘a’),程序依然会异常结束。本节我们将利用 scanf 函数的返回值来检测并处理这种错误。

scanf 函数会返回成功读取的数据项数量。例如,scanf(“%f”, &num) 期望读取一个浮点数,成功则返回 1,失败(如输入字符)则返回 0

  1. 检查 scanf 返回值:在每次调用 scanf 后立即检查其返回值。

    printf(“请输入第一个数字:”);
    if (scanf(“%f”, &num1_float) != 1) {
        printf(“输入无效,程序退出。\n”);
        return 1; // 非零返回值通常表示错误退出
    }
    // 对第二个输入重复此检查
    
  2. 优化代码结构:将“等待用户按键并清空输入缓冲区”的代码封装成一个函数,以便在输入错误后调用,使程序更清晰。

    void clear_input_buffer() {
        int c;
        while ((c = getchar()) != ‘\n’ && c != EOF); // 清空缓冲区
        printf(“按任意键继续…\n”);
        getchar(); // 等待用户按键
    }
    

    在输入无效时,先打印错误信息,然后调用此函数,再退出程序。

  1. 整合最终逻辑:以下是整合了所有错误处理的完整输入逻辑示例。
    printf(“请输入第一个数字:”);
    if (scanf(“%f”, &num1_float) != 1) {
        printf(“输入无效!\n”);
        clear_input_buffer();
        return 1;
    }
    // 清空缓冲区中可能残留的换行符,为下一次输入做准备
    clear_input_buffer();
    // 重复上述过程获取第二个数字
    

经过以上步骤,我们的程序已经能够稳健地处理整数、小数和字符等各种输入情况。


总结

本节课中,我们一起学习了如何逐步构建一个健壮的数字比较程序。

  1. 核心逻辑:我们首先使用 if-else 语句实现了两个数字比较大小的基本功能。
  2. 错误处理:我们随后增强了程序的鲁棒性,通过将输入先作为浮点数处理来兼容小数输入,并给出友好提示。
  3. 输入验证:最后,我们利用 scanf 函数的返回值来检测非法字符输入,防止程序崩溃,并通过封装函数优化了代码结构。

这个练习演示了在实际编程中,除了实现核心功能,预见并处理潜在的用户输入错误是构建高质量、用户友好应用程序的关键步骤。

029:if-else-if 阶梯语句

在本节课中,我们将学习 C 语言中一种重要的多条件判断结构——if-else-if 阶梯语句。我们将通过一个计算个人所得税的实际例子,来理解它的工作原理和用法。

概述

if-else-if 阶梯语句用于在程序中检查多个条件。它按顺序从上到下评估每个条件,一旦某个条件为真,就执行对应的代码块,并跳过其余所有条件。如果所有条件都为假,则执行可选的 else 块。

if-else-if 阶梯语句的结构

以下是 if-else-if 阶梯语句的基本语法结构:

if (condition1) {
    // 如果 condition1 为真,执行这里的代码
} else if (condition2) {
    // 如果 condition1 为假,但 condition2 为真,执行这里的代码
} else if (condition3) {
    // 如果 condition1 和 condition2 都为假,但 condition3 为真,执行这里的代码
} else {
    // 如果所有条件都为假,执行这里的代码
}

它的执行流程是线性的:首先检查 condition1,如果为真,则执行其后的代码块,整个阶梯语句结束。如果为假,则继续检查 condition2,依此类推。else 块是可选的,用于处理所有条件都不满足的情况。

实践练习:计算个人所得税

为了更清晰地理解 if-else-if 阶梯语句,我们将完成一个编程练习。这个练习要求我们编写一个程序,根据用户输入的收入计算应缴的个人所得税。

以下是计算所依据的税率阶梯规则:

  • 如果收入 小于等于 9,525 美元,税率为 0%
  • 如果收入在 9,526 美元至 38,700 美元 之间,税率为 12%
  • 如果收入在 38,701 美元至 82,500 美元 之间,税率为 22%
  • 如果收入 大于 82,500 美元,税率为 32%,并额外固定加收 1000 美元

计算应缴税额的公式为:收入 * 税率 / 100。对于最高税率档,需在此基础上加上固定的 1000 美元。

在接下来的课程中,我们将动手编写代码来实现这个计算器,从而巩固对 if-else-if 阶梯语句的掌握。

总结

本节课我们一起学习了 if-else-if 阶梯语句。它是一种高效处理多个互斥条件的分支结构,通过顺序检查条件,确保只有第一个为真的条件对应的代码块会被执行。我们通过一个个人所得税计算器的例子,了解了其在实际编程中的应用场景。掌握这种结构,对于编写清晰的逻辑判断代码至关重要。

030:if-else-if 阶梯练习解决方案 💻

在本节课中,我们将学习如何解决一个关于 if-else-if 条件阶梯的编程练习。我们将编写一个C语言程序,根据用户输入的收入金额,按照给定的税率表计算应缴税款。通过这个练习,你将掌握 if-else-if 语句的嵌套使用、用户输入处理以及基本的错误检查。

概述

我们将创建一个函数,用于接收用户输入的收入,并根据以下规则计算税款:

  1. 收入 ≤ 9,525 美元:税率为 0%。
  2. 9,525 美元 < 收入 ≤ 38,700 美元:税率为 12%。
  3. 38,700 美元 < 收入 ≤ 82,500 美元:税率为 22%。
  4. 收入 > 82,500 美元:税率为 32%,并额外加收 1,000 美元。

此外,我们还将添加对负收入输入的简单错误处理。

代码实现步骤

以下是构建解决方案的详细步骤。

1. 包含头文件与函数声明

首先,我们需要包含必要的标准输入输出库,并声明我们的函数。

#include <stdio.h>
#include <stdint.h>

void calculateTax();

2. 主函数

在主函数中,我们只需调用计算税款的函数。

int main() {
    calculateTax();
    return 0;
}

3. 实现 calculateTax 函数

这是程序的核心部分。我们将在此函数中声明变量、获取用户输入、进行条件判断并计算税款。

void calculateTax() {
    uint64_t income;
    uint64_t tax;
    double temp_income;

    printf("Enter your income: ");
    scanf("%lf", &temp_income);

上一节我们设置了变量并获取了用户输入,本节中我们来看看如何进行数据验证和类型转换。

4. 输入验证与类型转换

在计算前,我们需要检查输入是否有效(非负数),并将双精度浮点数转换为无符号64位整数用于计算。

    if (temp_income < 0) {
        printf("Income cannot be negative.\n");
        calculateTax(); // 重新调用函数以获取新输入
        return;
    }

    income = (uint64_t)temp_income;

5. 使用 if-else-if 阶梯计算税款

现在,我们使用 if-else-if 语句根据收入范围计算税款。以下是核心逻辑:

    if (income <= 9525) {
        tax = 0;
    }
    else if (income > 9525 && income <= 38700) {
        tax = income * 0.12;
    }
    else if (income > 38700 && income <= 82500) {
        tax = income * 0.22;
    }
    else { // 收入 > 82500
        tax = income * 0.32;
        tax = tax + 1000; // 或写作 tax += 1000;
    }

6. 输出结果

计算完成后,我们将结果打印给用户。

    printf("Tax payable: %llu\n", tax);
}

程序测试

让我们通过几个测试用例来验证程序的正确性。

以下是几个测试输入及其预期输出:

  • 输入: 9524
    输出: Tax payable: 0
    (验证第一个条件分支)

  • 输入: 40000
    输出: Tax payable: 8800
    (验证第二个条件分支,40000 * 0.12 = 4800?这里视频中计算有误,40000属于第三个区间,应为40000*0.22=8800,程序输出正确)

  • 输入: 85000
    输出: Tax payable: 27200
    (验证第四个条件分支,(85000 * 0.32) + 1000 = 27200 + 1000 = 28200?这里视频中计算有误,应为85000*0.32=27200,再加1000是28200。程序输出27200,说明代码逻辑与描述不符,实际未加1000。需要检查代码 tax = income * 0.32; 之后是否执行了 tax = tax + 1000;

  • 输入: -100
    输出: Income cannot be negative. (然后程序会提示重新输入)
    (验证错误处理)

总结

本节课中我们一起学习了如何利用 if-else-if 条件阶梯结构来解决一个实际问题——计算累进税率。我们实现了从用户获取输入、进行数据验证、执行多条件判断到最终输出结果的全过程。这个练习巩固了条件语句的用法,并引入了基本的错误处理概念,这对于构建健壮的嵌入式系统或任何软件都至关重要。记住,清晰的逻辑结构和周全的输入验证是编程中的重要原则。

031:条件运算符

在本节课中,我们将学习C语言中的决策控制语句,并重点探讨条件运算符。条件运算符是一种三元运算符,用于根据条件进行求值。

首先,我们创建一个新项目来实践。创建一个C/C++项目,命名为 decision_making_conditional_operator,并添加一个名为 main.c 的源文件。

条件运算符语法

条件运算符是C语言中唯一的三元运算符,其操作符符号是 ?:。它之所以被称为三元运算符,是因为它需要三个操作数。

以下是条件运算符的基本语法结构:

表达式1 ? 表达式2 : 表达式3

在这个结构中:

  • 表达式1 是第一个操作数,通常是一个条件表达式。
  • 表达式2 是第二个操作数,在 ? 之后。
  • 表达式3 是第三个操作数,在 : 之后。

条件运算符的工作原理

上一节我们介绍了条件运算符的语法,本节中我们来看看它的具体执行逻辑。条件运算符的求值过程遵循一个简单的规则:

  1. 首先计算 表达式1 的值。
  2. 如果 表达式1 的求值结果为 (即非零),则整个条件运算符的结果是 表达式2 的值。
  3. 如果 表达式1 的求值结果为 (即零),则整个条件运算符的结果是 表达式3 的值。

让我们通过一个代码示例来加深理解。

#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t number1 = (5 + 4) ? (9 - 4) : 99;
    printf("Value is: %lu\n", number1);
    return 0;
}

在这段代码中:

  • 表达式1(5 + 4),结果为 9(非零,为真)。
  • 因为条件为真,所以计算 表达式2 (9 - 4),结果为 5
  • 因此,变量 number1 被赋值为 5。程序运行后会输出 Value is: 5

现在,我们修改一下条件,看看当条件为假时会发生什么。

#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t number1 = (-5 + 4) ? (9 - 4) : 99;
    printf("Value is: %lu\n", number1);
    return 0;
}

在这段修改后的代码中:

  • 表达式1(-5 + 4),结果为 -1(非零,为真)。注意,在C语言中,任何非零值都被视为真
  • 因为条件为真,所以计算 表达式2 (9 - 4),结果为 5
  • 因此,变量 number1 被赋值为 5。程序运行后会输出 Value is: 5

为了演示假条件,我们需要一个结果为0的表达式:

#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t number1 = (5 - 5) ? (9 - 4) : 99; // 表达式1结果为0
    printf("Value is: %lu\n", number1);
    return 0;
}

此时:

  • 表达式1 (5 - 5) 的结果为 0(假)。
  • 因为条件为假,所以跳过 表达式2,计算 表达式3 99
  • 因此,变量 number1 被赋值为 99。程序运行后会输出 Value is: 99

条件运算符的应用

理解了基本工作原理后,我们来看看它的一个常见用途。条件运算符可以作为一种简洁的替代方式,来实现简单的 if-else 语句逻辑。

例如,下面两段代码的功能是等价的:

使用 if-else 语句:

int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

使用条件运算符:

int max = (a > b) ? a : b;

可以看到,条件运算符的写法更加紧凑。但是,对于复杂的多分支判断或需要执行多条语句的情况,使用 if-elseswitch 语句通常更具可读性。

总结

本节课中我们一起学习了C语言中的条件运算符? :)。我们掌握了它的语法结构 表达式1 ? 表达式2 : 表达式3,并明确了其执行规则:根据表达式1的真假结果,选择执行并返回表达式2表达式3的值。这种运算符提供了一种简洁的方式来编写简单的条件赋值语句,可以作为基础 if-else 结构的一种替代。在后续课程中,我们将继续探讨其他决策控制语句。

构建嵌入式系统:ARM Cortex (STM32) 基础:P32:C语言中的switch case语句 🧠

在本节课中,我们将要学习C语言编程中的switch case语句。这是一种用于多路分支选择的控制结构,特别适合处理一个变量与多个可能值进行比较的情况。


语法结构

switch case语句的基本语法结构如下:

switch (expression) {
    case value1:
        // 当 expression 等于 value1 时执行的语句
        break;
    case value2:
        // 当 expression 等于 value2 时执行的语句
        break;
    // ... 可以有多个 case
    default:
        // 当 expression 与所有 case 值都不匹配时执行的语句
}

其中,expression是需要进行比较的表达式,其结果通常是一个整数或字符。case后面跟着一个常量值,用于与expression的结果进行比较。break语句用于跳出整个switch结构。default分支是可选的,用于处理所有case都不匹配的情况。


实践示例:判断元音字母

为了帮助理解,我们将通过一个判断输入字符是否为元音字母的程序来演示switch case的用法。

首先,我们创建一个新的C语言项目,并添加一个main.c源文件。程序的核心逻辑如下:

  1. 提示用户输入一个字符。
  2. 使用scanf函数读取用户输入的字符。
  3. 使用switch case语句判断该字符是否为元音字母(A, E, I, O, U)。
  4. 根据判断结果输出相应信息。

以下是实现此功能的代码:

#include <stdio.h>

int main() {
    char ch;
    printf("Enter a character: ");
    scanf("%c", &ch);

    switch (ch) {
        case 'A':
        case 'E':
        case 'I':
        case 'O':
        case 'U':
            printf("This is a vowel.\n");
            break;
        default:
            printf("This is not a vowel.\n");
    }
    return 0;
}

在这个例子中,我们使用了多个case标签(‘A’, ‘E’, ‘I’, ‘O’, ‘U’)共享同一段执行代码(打印“This is a vowel.”)。这是一种常见的技巧,可以简化代码。


执行与验证

编译并运行上述程序。当您输入字符‘A’、‘E’、‘I’、‘O’或‘U’(包括大小写,注意示例代码中是大写字母)时,程序会输出“This is a vowel.”。输入其他任何字符,程序则会执行default分支,输出“This is not a vowel.”。

您也可以为每个元音字母编写独立的输出语句,例如:

switch (ch) {
    case 'A':
        printf("This is vowel A.\n");
        break;
    case 'E':
        printf("This is vowel E.\n");
        break;
    // ... 其他元音字母
    default:
        printf("This is not a vowel.\n");
}

选择哪种方式取决于您的具体需求。共享代码块的方式更简洁,而独立代码块的方式可以为每个值提供更具体的反馈。


总结

本节课中我们一起学习了C语言的switch case语句。我们了解了它的基本语法结构,并通过一个判断元音字母的实例程序掌握了其使用方法。关键点包括:

  • switch语句根据一个表达式的值进行多路分支。
  • case标签用于指定匹配的值,并执行其后的代码块。
  • break语句用于在匹配成功后跳出switch结构。
  • default分支用于处理所有case都不匹配的情况。
  • 多个case标签可以指向同一段执行代码,这有助于简化逻辑。

在接下来的课程中,我们将通过更多练习来巩固对switch case语句的理解。

033:switch-case 语句练习 🧮

在本节中,我们将通过一个编程练习来巩固对 switch-case 决策语句的理解。我们将编写一个程序,根据用户输入的不同代码,计算并输出相应几何图形的面积。

概述

我们将创建一个名为“决策制定:switch-case练习”的C++项目。程序的核心功能是:提示用户输入一个代表特定几何图形的代码(例如,T代表三角形,C代表圆形),然后根据该代码请求必要的尺寸参数(如半径、底边、高),最后计算并显示面积。程序还需包含基本的错误处理,例如检查输入的参数是否为负值。

练习详解

以下是该练习的具体要求和实现步骤。

程序需要支持计算多种几何图形的面积。用户首先输入一个代表图形的字母代码。

根据用户输入的代码,程序将引导用户输入计算该图形面积所需的参数。

  • 圆形 (C):需要半径 radius。面积公式为:area = PI * radius * radius
  • 三角形 (T):需要底边 base 和高 height。面积公式为:area = 0.5 * base * height
  • 梯形 (Z):需要上底 base1、下底 base2 和高 height。面积公式为:area = 0.5 * (base1 + base2) * height
  • 正方形 (S):需要边长 side。面积公式为:area = side * side
  • 长方形 (A):需要长度 length 和宽度 width。面积公式为:area = length * width

程序必须对用户输入的参数进行验证。例如,如果用户为半径输入了-1,程序应输出错误信息“半径不能为负数”,而不是进行错误计算。

实现思路

上一节我们介绍了 switch-case 语句的语法,本节中我们来看看如何将其应用于实际编程问题。我们将使用 switch-case 结构来根据用户输入的字符代码,分流到不同的计算逻辑中。

以下是实现这个程序的基本逻辑流程:

  1. 定义必要的变量来存储用户输入的代码和图形参数。
  2. 提示用户输入图形代码。
  3. 使用 switch 语句,根据输入的代码进入不同的 case 分支。
  4. 在每个 case 分支中:
    • 提示用户输入计算该图形面积所需的参数。
    • 检查输入的参数是否有效(如是否为正数)。
    • 如果有效,则根据公式计算面积并显示结果。
    • 如果无效,则显示错误信息。
  5. 使用 default 分支处理用户输入了未定义代码的情况。

总结

本节课中我们一起学习了如何运用 switch-case 语句来解决一个多分支选择问题。我们设计了一个计算不同几何图形面积的程序,它不仅能根据用户选择执行相应计算,还加入了参数验证等基础错误处理功能。这个练习很好地演示了 switch-case 在组织清晰、结构化的决策逻辑时的实用性。在接下来的视频中,我们将动手实现这个程序。

034:Switch Case 练习解答 第1部分

概述

在本节课中,我们将学习如何使用C语言中的switch-case语句来构建一个简单的几何图形面积计算程序。我们将通过一个具体的编程练习,实现根据用户输入的不同代码来计算圆形、三角形和梯形的面积。

程序结构搭建

首先,我们需要搭建程序的基本框架。这包括包含必要的头文件、声明函数原型以及定义主函数。

#include <stdio.h>
#include <stdint.h>

void user_input(void);

int main(void) {
    user_input();
    return 0;
}

上一节我们搭建了程序的基本框架,本节中我们来看看如何实现用户交互部分。

用户交互与变量定义

以下是实现用户交互的步骤。我们首先向用户展示一个菜单,然后接收用户输入的图形代码。

void user_input(void) {
    uint8_t code;
    float radius, base, height, area;
    float base1, base2; // 用于梯形

    printf("Area Calculation Program\n");
    printf("For Circle, enter C\n");
    printf("For Triangle, enter T\n");
    printf("For Trapezoid, enter Z\n");
    printf("For Square, enter S\n");
    printf("For Rectangle, enter R\n");
    printf("Enter the code here: ");
    scanf("%c", &code);

实现Switch-Case逻辑

获取用户输入后,我们使用switch-case语句来根据不同的代码执行相应的面积计算逻辑。

    switch(code) {
        case 'C':
            printf("Circle Area Calculation\n");
            printf("Enter radius value: ");
            scanf("%f", &radius);
            area = 3.1415 * radius * radius;
            printf("Area: %f\n", area);
            break;

计算三角形面积

如果用户输入的是T,程序将计算三角形的面积。三角形的面积公式是 面积 = (底 * 高) / 2

        case 'T':
            printf("Triangle Area Calculation\n");
            printf("Enter base value: ");
            scanf("%f", &base);
            printf("Enter height value: ");
            scanf("%f", &height);
            area = (base * height) / 2;
            printf("Area: %f\n", area);
            break;

计算梯形面积

如果用户输入的是Z,程序将计算梯形的面积。梯形的面积公式是 面积 = ((上底 + 下底) / 2) * 高

        case 'Z':
            printf("Trapezoid Area Calculation\n");
            printf("Enter base1 value: ");
            scanf("%f", &base1);
            printf("Enter base2 value: ");
            scanf("%f", &base2);
            printf("Enter height value: ");
            scanf("%f", &height);
            area = ((base1 + base2) / 2) * height;
            printf("Area: %f\n", area);
            break;

总结

本节课中我们一起学习了如何利用switch-case控制流结构来创建一个多分支的程序。我们实现了根据用户输入选择不同几何图形并计算其面积的功能,涵盖了圆形、三角形和梯形的计算。在下一部分,我们将继续完成正方形和矩形的面积计算,并进一步完善这个程序。

035:switch-case 练习解答 第二部分

概述

在本节中,我们将继续完成一个使用 switch-case 语句的编程练习。我们将为计算不同几何图形面积的程序添加输入验证功能,确保用户输入的数值(如半径、边长等)是有效的非负数。通过这个练习,你将学习如何在 switch-case 结构中结合条件判断,以构建更健壮的程序。

继续编写代码

上一节我们介绍了 switch-case 的基本结构并处理了部分图形。本节中,我们来看看如何为每个分支添加输入验证,并完善整个程序。

以下是继续编写代码的步骤:

  1. 处理正方形面积计算
    对于正方形,我们需要接收边长并计算面积。同时,需要验证边长是否为非负数。

    case 'S': // 正方形
        printf("请输入边长值: ");
        scanf("%f", &a);
        if (a < 0) {
            printf("边长不能为负数。\n");
            area = -1;
        } else {
            area = a * a; // 面积公式:边长 * 边长
        }
        break;
    
  2. 处理矩形面积计算
    对于矩形,我们需要接收宽度和长度。同样,需要验证这两个值。

    case 'R': // 矩形
        printf("请输入宽度和长度: ");
        scanf("%f %f", &a, &b);
        if (a < 0 || b < 0) {
            printf("宽度或长度不能为负数。\n");
            area = -1;
        } else {
            area = a * b; // 面积公式:宽度 * 长度
        }
        break;
    

  1. 添加默认情况
    当用户输入无效选项时,程序应给出提示。
    default:
        printf("无效输入。\n");
        area = -1;
        break;
    

完善主逻辑

switch-case 语句结束后,我们需要根据 area 变量的值来决定是否输出结果。如果 area 不小于 0(即计算成功),则打印面积;否则不打印。

// switch-case 语句结束

if (!(area < 0)) { // 如果 area 不小于 0
    printf("面积为: %.2f\n", area);
}

printf("按任意键继续...\n");
getchar(); // 等待用户输入,以便查看结果

测试与验证

现在,让我们构建并运行程序,测试各种情况。

  1. 测试有效输入:选择三角形(T),输入合法的底和高,程序应正确计算并显示面积。
  2. 测试无效图形代码:输入一个未定义的字母,程序应显示“无效输入”。
  3. 测试负值输入
    • 选择圆形(C),输入负的半径,程序应提示“半径不能为负数”。
    • 选择矩形(R),输入负的宽度或长度,程序应提示“宽度或长度不能为负数”。
    • 选择梯形(Z),输入负的底或高,程序应提示“底或高不能为负数”。

经过测试,程序现在能够正确处理各种有效和无效的输入,确保了健壮性。

总结

本节课中我们一起学习了如何扩展 switch-case 结构的功能。我们不仅用其来根据用户选择执行不同的代码分支,还在每个分支内部结合了 if 条件语句来进行输入验证。这种组合是编写可靠、用户友好程序的关键。通过完成这个练习,你掌握了使用控制结构处理复杂逻辑的基本方法,这是嵌入式系统乃至所有编程领域的重要技能。

036:C语言中的位运算符

在本节课中,我们将学习C语言中的位运算符。位运算符在嵌入式系统编程中至关重要,常用于操作内存地址、外设寄存器内容和状态寄存器等。掌握位运算符是嵌入式C编程的重要一环。

逻辑运算符与位运算符的区别

首先,我们需要区分逻辑运算符和位运算符。逻辑运算符关注操作数的真假(非零为真,零为假),而位运算符则对操作数的每一位进行逐位运算。

例如,逻辑与运算符是 &&,位与运算符是 &。假设有两个变量:

int A = 40;  // 二进制: 0010 1000
int B = 30;  // 二进制: 0001 1110

逻辑运算 A && B 的结果为 1(真),因为A和B都是非零值。

位运算符详解

以下是C语言中可用的六种位运算符。

1. 位与运算符 &

位与运算符对两个操作数的每一位执行逻辑与操作。规则是:只有两个对应位都为1时,结果位才为1,否则为0。

对于 A & B

A: 0010 1000 (40)
B: 0001 1110 (30)
&: 0000 1000 (8)

因此,C = A & B 的结果是 8

2. 位或运算符 |

位或运算符对两个操作数的每一位执行逻辑或操作。规则是:只要两个对应位中有一个为1,结果位就为1。

对于 A | B

A: 0010 1000 (40)
B: 0001 1110 (30)
|: 0011 1110 (62)

因此,C = A | B 的结果是 62

3. 位非运算符 ~

位非运算符是单目运算符,它对操作数的每一位执行逻辑非操作(取反)。规则是:1变为0,0变为1。

对于 ~A

A:  0010 1000 (40)
~A: 1101 0111 (215,假设为8位无符号整数)

因此,C = ~A 的结果是 215(8位情况下)。

4. 位异或运算符 ^

位异或运算符对两个操作数的每一位执行异或操作。规则是:两个对应位不同时,结果位为1;相同时,结果位为0。

异或运算的真值表如下:

A  B | A ^ B
0  0 |   0
0  1 |   1
1  0 |   1
1  1 |   0

对于 A ^ B

A: 0010 1000 (40)
B: 0001 1110 (30)
^: 0011 0110 (54)

因此,C = A ^ B 的结果是 54

5. 左移运算符 <<

左移运算符将操作数的所有位向左移动指定的位数。右侧空出的位用0填充。

例如,A << 2 表示将A的二进制位向左移动2位。

A:    0010 1000 (40)
A<<2: 1010 0000 (160)

6. 右移运算符 >>

右移运算符将操作数的所有位向右移动指定的位数。对于无符号数,左侧空出的位用0填充;对于有符号数,行为可能依赖于编译器(通常进行符号扩展)。

例如,A >> 2 表示将A的二进制位向右移动2位。

A:    0010 1000 (40)
A>>2: 0000 1010 (10)

位运算符在嵌入式编程中的应用

在嵌入式C程序中,位运算符常用于以下操作:

  • 测试位:检查某个特定位是1还是0。
  • 设置位:将某个特定位设为1。
  • 清除位:将某个特定位设为0。
  • 切换位:将某个特定位从0变为1,或从1变为0。

例如,控制一个连接到微控制器端口的LED:

  • 点亮LED可能需要设置端口寄存器的某个位。
  • 关闭LED可能需要清除该位。
  • 读取外设状态寄存器时,需要测试特定的状态位。

实践练习

为了巩固理解,我们将在下一节中完成一个简单的练习。我们将编写一个C程序,该程序从用户处获取两个整数,然后计算并打印这两个数的位与、位或、位异或和位非的结果。

本节课中,我们一起学习了C语言中的六种位运算符(&|~^<<>>),理解了它们与逻辑运算符的区别,并探讨了它们在嵌入式系统编程中的基本应用。下一节我们将通过编程练习来实际应用这些知识。

037:按位与和按位或运算

在本节课中,我们将学习如何在C语言中实现基本的按位运算符,包括按位与、按位或、按位异或和按位非。我们将通过一个具体的编程练习来演示这些运算符的用法和效果。

概述

我们将创建一个新的C语言项目,编写一个程序,该程序接收用户输入的两个整数,然后计算并输出这两个数的按位与、按位或、按位异或以及第一个数的按位非结果。通过这个练习,你将直观地理解这些位运算的工作原理。

项目创建与代码编写

首先,我们需要创建一个新的项目并添加一个源文件。

以下是创建项目的步骤:

  1. 创建一个新的C项目。
  2. 在项目中添加一个名为 main.c 的源文件。

接下来,我们开始编写 main.c 文件中的代码。程序的核心逻辑是包含必要的头文件,定义主函数,接收用户输入,进行计算,并打印结果。

以下是 main.c 文件的完整代码:

#include <stdint.h>
#include <stdio.h>

int main() {
    // 声明两个整数变量
    int32_t number1, number2;

    // 提示用户输入两个数字
    printf("请输入两个数字,用空格分隔:\n");
    // 读取用户输入
    scanf("%d %d", &number1, &number2);

    // 计算并打印按位与的结果
    printf("按位与 (number1 & number2): %d\n", number1 & number2);
    // 计算并打印按位或的结果
    printf("按位或 (number1 | number2): %d\n", number1 | number2);
    // 计算并打印按位异或的结果
    printf("按位异或 (number1 ^ number2): %d\n", number1 ^ number2);
    // 计算并打印number1的按位非结果
    printf("按位非 (~number1): %d\n", ~number1);

    return 0;
}

代码解析

上一节我们列出了完整的程序代码,本节中我们来详细解析每一部分的作用。

以下是代码中关键部分的解释:

  • #include <stdint.h>#include <stdio.h>: 引入标准库,分别用于使用固定宽度整数类型(如int32_t)和标准输入输出函数(如printfscanf)。
  • int32_t number1, number2;: 声明两个32位有符号整数变量。
  • printf(“请输入两个数字,用空格分隔:\n”);: 向用户显示输入提示信息。
  • scanf(“%d %d”, &number1, &number2);: 从标准输入读取两个整数,并分别存储到number1number2变量中。
  • printf(“按位与 (number1 & number2): %d\n”, number1 & number2);: 计算number1number2的按位与(&),并打印结果。
  • printf(“按位或 (number1 | number2): %d\n”, number1 | number2);: 计算number1number2的按位或(|),并打印结果。
  • printf(“按位异或 (number1 ^ number2): %d\n”, number1 ^ number2);: 计算number1number2的按位异或(^),并打印结果。
  • printf(“按位非 (~number1): %d\n”, ~number1);: 计算number1的按位非(~),并打印结果。按位非是一元运算符,只对number1进行操作。

运行与验证

代码编写完成后,我们需要编译并运行程序来验证其功能。

以下是运行示例:

  1. 编译并构建项目。
  2. 运行生成的可执行文件。
  3. 程序会提示:“请输入两个数字,用空格分隔:”。
  4. 假设输入 4050,然后按回车键。
  5. 程序将输出以下结果:
    • 按位与 (number1 & number2): 32
    • 按位或 (number1 | number2): 58
    • 按位异或 (number1 ^ number2): 26
    • 按位非 (~number1): -41

这个输出展示了针对输入值40和50,各个按位运算符的具体计算结果。

总结

本节课中我们一起学习了C语言中基本按位运算符的实践应用。我们创建了一个完整的程序,该程序能够接收用户输入,并演示了按位与 (&)按位或 (|)按位异或 (^)按位非 (~) 运算符的使用方法及运算结果。通过这个动手练习,你应该对如何在嵌入式系统编程中操作数据的单个比特位有了更直观的认识。在接下来的课程中,我们将继续深入探讨位运算的更多高级应用。

038:位运算符的位测试适用性 🧪

在本节课中,我们将学习位运算符在嵌入式C编程中的一个核心应用:位测试。我们将通过编写一个判断整数奇偶性的程序,来具体理解如何使用位掩码技术来测试特定位的状态。

上一节我们介绍了位运算符的基本概念。本节中我们来看看如何利用这些运算符进行实际的位操作。

位测试的应用场景

在嵌入式C程序中,位操作非常常见。以下是几种典型操作及其对应的运算符:

  • 测试位:使用 按位与 (&) 运算符。
  • 设置位:使用 按位或 (|) 运算符。
  • 清除位:使用 按位取反 (~)按位与 (&) 运算符的组合。
  • 翻转位:使用 按位异或 (^) 运算符。

我们将通过一个练习来理解位测试。这个练习的目标是:编写一个程序,判断用户输入的整数是奇数还是偶数,并将结果打印到控制台。我们将使用测试位的逻辑来实现这个功能。

判断奇偶性的位逻辑原理

判断一个数字是奇数还是偶数有很多方法,但我们将使用基于位测试的逻辑。其原理非常简单。

让我们考虑一个数字,例如整数 46。它的二进制形式是 00101110。这是一个偶数,其最低有效位 (LSB)0

再以 47 为例,其二进制形式是 00101111。这是一个奇数,其LSB是 1

这表明:

  • 当LSB为 0 时,数字是偶数
  • 当LSB为 1 时,数字是奇数

因此,通过测试一个数字的最低有效位,我们就可以判断它是奇数还是偶数。这就是我们理解的基本逻辑。

位掩码技术

为了测试特定位,我们将使用一种称为位掩码的技术。位掩码是编程中用于测试或修改给定数据中特定位状态的一种方法。

我们需要创建一个掩码值,然后对该掩码值和原始数据执行位运算,以获得我们想要的结果。

现在让我们分析具体如何操作。假设我们有一个输入数字,以二进制格式表示。我们以数字 46 (00101110) 为例。

我们可以将这个数字的位分为两个区域:

  1. 区域1:除了我们关心的最低有效位(LSB)之外的所有高位。
  2. 区域2:我们关心的最低有效位(LSB)。

在这个例子中,我们的目标是测试LSB以判断奇偶性,因此区域1对我们没有意义。这就是为什么我们在该区域使用掩码值 0

对于区域2,我们创建一个掩码值。整个掩码值除了LSB位置为 1 外,其余位都是 0。对于8位数,这个掩码就是 00000001(即十进制的 1)。

现在,我们将原始数据 (00101110) 与掩码 (00000001) 进行按位与 (&) 运算:

数据:   0 0 1 0 1 1 1 0
掩码: & 0 0 0 0 0 0 0 1
结果:   0 0 0 0 0 0 0 0

运算结果是 0。因为数据LSB是 0,与掩码的 1 相与得到 0。我们可以得出结论:该数字是偶数

如果数字是奇数,例如 47 (00101111):

数据:   0 0 1 0 1 1 1 1
掩码: & 0 0 0 0 0 0 0 1
结果:   0 0 0 0 0 0 0 1

运算结果是 1(非零)。我们可以得出结论:该数字是奇数

这就是掩码值的重要性。通过这种技术,我们可以测试任何我们想要的位位置。在这个例子中,我们检查的是第1位(LSB),所以我们将掩码中除了该位设为 1 外,其余所有位都设为 0

程序实现思路

根据以上逻辑,我们可以设计程序的步骤:

  1. 获取用户输入的一个整数。
  2. 将该整数与掩码值 1 进行按位与 (&) 运算。
  3. 检查运算结果:
    • 如果结果为 0,则数字为偶数
    • 如果结果为 1(非零),则数字为奇数
  4. 打印相应的结果信息。

本节课中我们一起学习了如何使用位运算符进行位测试,并深入理解了位掩码技术的原理和应用。我们通过判断整数奇偶性的具体例子,掌握了通过按位与 (&) 运算和掩码值来测试特定位状态的方法。在下一节中,我们将动手编写代码来实现这个程序。

构建嵌入式系统:ARM Cortex (STM32) 基础:第39章:利用位测试判断奇偶性

在本节课中,我们将学习如何编写一个C语言程序,利用位运算来判断一个整数是奇数还是偶数。这是嵌入式系统编程中处理底层数据的基础技能。

上一节我们介绍了位运算的基本概念,本节中我们来看看如何具体应用位与运算进行奇偶性判断。

程序编写步骤

以下是创建一个判断奇偶性程序的完整步骤。

  1. 创建新项目:在集成开发环境中创建一个新的C项目,命名为“bitwise_example”。
  2. 添加源文件:在项目中添加一个新的源文件,命名为 main.c
  3. 包含头文件:在 main.c 文件中,包含必要的标准输入输出库。
    #include <stdio.h>
    #include <stdint.h>
    
  4. 定义主函数:创建 main 函数作为程序入口。
    int main(void) {
        return 0;
    }
    
  5. 声明变量:在主函数中,声明一个32位无符号整数变量 number1 来存储用户输入。
    uint32_t number1;
    
  6. 获取用户输入:使用 printf 提示用户输入,并使用 scanf 读取整数。
    printf("Enter a number: ");
    scanf("%d", &number1);
    
  7. 判断奇偶性:使用 if 语句和位与运算符 & 进行判断。核心逻辑是检查数字的最低位(LSB)。
    if (number1 & 1) {
        printf("%d is an odd number.\n", number1);
    } else {
        printf("%d is an even number.\n", number1);
    }
    
    公式解释(number1 & 1) 的结果等于 number1 二进制表示的最低位值。如果该位为1,则数字是奇数;如果为0,则数字是偶数。

程序运行与验证

完成代码编写后,需要编译并运行程序以验证其功能。

  1. 编译项目:在IDE中构建项目,确保没有语法错误。
  2. 运行程序:打开调试器或直接运行生成的可执行文件。
  3. 测试输入:程序会提示输入一个数字。分别输入奇数和偶数进行测试。
    • 输入偶数(如 4),程序输出:“4 is an even number.”
    • 输入奇数(如 7),程序输出:“7 is an odd number.”

程序运行结果符合预期,证明利用位与运算判断奇偶性的方法是正确的。关键在于使用掩码 1 与目标数字进行按位与操作。

下节预告

本节课中我们一起学习了如何通过位测试判断数字的奇偶性。在下一节视频中,我们将进行一个更深入的练习:编写程序,将给定数字的第四位和第七位(从最低位0开始计数)设置为1,然后计算并输出结果。我们将先手动计算二进制值,再通过程序验证。我们下节课再见。

040:位运算符清除位的适用性

在本节中,我们将通过一个练习来学习如何使用位运算符来清除(清零)数据中的特定位。我们将编写一个程序,将给定数字的第4、5、6位清零,并打印结果。

理解清除位的概念

清除位意味着将特定位的状态重置为0。我们的目标是让一个字节(8位)中的第4、5、6位变为0,同时确保第0到3位以及第7位的值不受影响。

那么,应该使用哪种位运算来清除位呢?答案是位与(AND) 运算。位或(OR)运算用于设置位(置1),而位与运算既可以用于测试位,也可以用于清除位。

清除位的两种方法

核心思想是使用一个掩码(mask) 与原始数据进行位与运算。以下是两种常用的方法。

方法一:掩码中需清除的位设为0

在这种方法中,我们创建一个掩码,其中需要保持不变的位设为1,需要清零的位设为0。然后,将原始数据与此掩码进行位与运算。

公式:
result = data & mask

例如,假设原始数据是 0b10111110(二进制),我们需要清零第4、5、6位(从第0位开始计数)。那么掩码应为 0b10001111(二进制),即 0x8F(十六进制)。

运算过程:

  数据 (data):   1 0 1 1 1 1 1 0
  掩码 (mask):   1 0 0 0 1 1 1 1  (0x8F)
位与 (&) 结果:   1 0 0 0 1 1 1 0

可以看到,第4、5、6位被成功清零,其他位保持不变。

方法二:掩码中需清除的位设为1,然后取反

另一种方法是先创建一个掩码,其中需要清零的位设为1,其他位设为0。然后,对这个掩码进行位非(NOT) 运算取反,再将取反后的掩码与原始数据进行位与运算。

公式:
result = data & (~mask)

例如,要清零第4、5、6位,我们先创建掩码 0b01110000(这些位为1)。然后对其取反得到 ~0b01110000 = 0b10001111,这个结果与方法一的掩码完全相同。

代码示例:

uint8_t data = 0xBE; // 二进制 10111110
uint8_t mask = 0x70; // 二进制 01110000,对应第4、5、6位
uint8_t result = data & (~mask); // 结果与方法一相同

练习:编写清除位程序

现在,让我们动手实践。我们将创建一个名为 bit_wise_clearing_exercise 的C++项目,并编写代码。

以下是实现步骤:

  1. 定义一个8位无符号整数变量,并赋予一个初始值(例如 0xBE)。
  2. 选择上述任一方法创建合适的掩码。
  3. 使用位与运算符执行清除操作。
  4. 打印原始数据和操作后的结果,以验证第4、5、6位是否被清零。

示例代码:

#include <stdio.h>
#include <stdint.h>

int main() {
    // 原始数据
    uint8_t data = 0xBE; // 二进制 1011 1110
    printf("原始数据: 0x%02X\n", data);

    // 方法一:直接使用掩码 0x8F (二进制 1000 1111)
    uint8_t mask1 = 0x8F;
    uint8_t result1 = data & mask1;
    printf("使用方法一清除第4、5、6位后: 0x%02X\n", result1);

    // 方法二:使用掩码取反
    uint8_t mask2 = 0x70; // 二进制 0111 0000,对应第4、5、6位
    uint8_t result2 = data & (~mask2);
    printf("使用方法二清除第4、5、6位后: 0x%02X\n", result2);

    return 0;
}

运行此程序,两种方法将输出相同的结果,确认特定位已被成功清零。

总结与展望

在本节中,我们一起学习了如何使用位与运算符来清除数据中的特定位。我们掌握了两种创建掩码的方法:直接构造清零位为0的掩码,或先构造清零位为1的掩码再取反。这两种方法都能有效地实现位的清零操作,是嵌入式编程中处理寄存器或数据包位域的基础技能。

在下一讲中,我们将介绍位移运算符,它们能帮助我们更灵活、高效地进行位的设置、清除和切换操作,敬请期待。

041:异或位运算符的适用性 🧩

在本节课中,我们将学习异或位运算符,并了解如何利用它来切换(或称为“翻转”)数据中的特定位。这对于控制嵌入式系统中的硬件(如LED)状态非常有用。

上一节我们介绍了位运算符的基础知识,本节中我们来看看异或运算符的具体应用。

异或运算符原理

异或运算符需要两个操作数。其运算规则是:当两个操作数相等时,结果为0;当两个操作数不相等时,结果为1。

以下是异或运算的真值表:

操作数 A 操作数 B 结果 (A XOR B)
0 0 0
0 1 1
1 0 1
1 1 0

这个特性可以用公式表示为:
结果 = (A != B)

异或运算符的应用:切换位状态

我们可以利用异或运算的特性来切换某个变量的特定位。例如,假设我们有一个变量 led_state 用于表示LED的状态(0表示熄灭,1表示点亮)。

如果不使用异或运算符,切换LED状态的代码可能如下:

if (led_state == 0) {
    led_state = 1;
} else {
    led_state = 0;
}

或者使用更简洁的三元运算符:

led_state = (led_state == 0) ? 1 : 0;

然而,使用异或运算符,我们可以用一行代码实现相同的功能:

led_state = led_state ^ 1;

或者使用更常见的简写形式:

led_state ^= 1;

其工作原理是:

  • 如果 led_state0,则 0 ^ 1 = 1,状态变为点亮。
  • 如果 led_state1,则 1 ^ 1 = 0,状态变为熄灭。

实践前的知识准备

为了在目标开发板上实践这个例子,你需要掌握以下知识:

  • 指针:你已经学习过相关内容。
  • 位运算:理解位运算符的工作原理。
  • 硬件连接:了解你所使用的微控制器端口与外部组件(如LED)的连接方式。

从下一个视频开始,我们将逐步涵盖这些硬件相关的知识。


本节课中我们一起学习了异或位运算符的独特性质及其在嵌入式编程中的一个典型应用——切换硬件状态。通过使用 ^= 运算符,我们可以用非常简洁的代码实现状态的翻转,这比传统的条件判断语句更加高效和优雅。在后续的实践中,我们将把这一知识应用到真实的硬件控制中。

042:P42 03_02_03_位运算符的比特位设置适用性 💻

在本节课中,我们将学习如何使用位运算符来设置一个给定数字的特定位。我们将通过一个具体的编程练习来理解位运算符的适用性,特别是按位或按位与在“设置”操作中的区别。

练习目标 🎯

本次练习的目标是编写一个程序,将给定数字的第4位第7位设置为1,同时不影响其他任何位。程序需要打印出操作后的结果。

核心概念解析 🔍

为了设置特定位,我们需要使用一个掩码。掩码是一个二进制数,其中我们想要设置的位为1,其余位为0。

上一节我们介绍了位运算符的基本概念,本节中我们来看看如何应用它们来设置比特位。

选择正确的位运算符

以下是决定使用哪个运算符的关键分析:

假设我们有一个8位数据:0011 1110(二进制)。
我们的目标是设置第4位(从右向左,从0开始计数)和第7位。

  • 掩码值应为:1001 0000(二进制),即第7位和第4位为1。
  • 我们需要决定使用按位与还是按位或

让我们通过示例来测试:

使用按位与 (&) 操作:

数据:   0011 1110
掩码:   1001 0000
结果:   0001 0000

分析:按位与操作将数据中与掩码中0对应的位都清零了,这并非我们想要的结果。

使用按位或 (|) 操作:

数据:   0011 1110
掩码:   1001 0000
结果:   1011 1110

分析:按位或操作成功地将掩码中为1的位(第7位和第4位)在数据中设置为1,而其他位保持不变。

结论:

  • 按位与运算符主要用于测试特定位是否为1,而不是用于设置位。
  • 按位或运算符用于设置特定位为1,而不是用于测试。

编程实现 🛠️

现在,让我们将理论转化为代码。我们将编写一个C语言程序来实现上述功能。

以下是实现步骤:

  1. 从用户输入获取一个整数。
  2. 定义一个掩码,其十六进制值为0x90(对应二进制1001 0000,即设置了第7位和第4位)。
  3. 使用按位或运算符将输入数字与掩码结合,生成结果。
  4. 以十六进制格式打印原始数字和结果。
#include <stdio.h>

int main() {
    int number;
    int output;

    printf("Enter a number: ");
    scanf("%d", &number);

    output = number | 0x90; // 使用按位或设置第4位和第7位

    printf("Your input:  0x%x\n", number);
    printf("Your output: 0x%x\n", output);

    return 0;
}

运行示例 📟

让我们运行程序并输入一个测试值(例如56)。

程序输出可能类似于:

Enter a number: 56
Your input:  0x38
Your output: 0xb8
  • 输入56的十六进制是0x38(二进制0011 1000)。
  • 与掩码0x90(二进制1001 0000)进行按位或操作后,结果为0xB8(二进制1011 1000)。
  • 可以看到,第7位和第4位被成功设置为1,其他位保持不变。

总结 📝

本节课中我们一起学习了位运算符在嵌入式编程中的一项关键应用:设置特定位。

  • 我们明确了按位或是用于设置位的正确运算符。
  • 我们通过一个完整的编程练习,演示了如何定义掩码并使用|运算符来设置一个数字的第4位和第7位,同时不影响其他位。
  • 理解并正确使用位运算符是进行底层硬件操作和优化内存使用的核心技能。

通过掌握这些概念,你将能够更有效地控制微控制器的寄存器和硬件状态,这是构建高效嵌入式系统的基础。

043:编码点亮LED 🔧

在本节课中,我们将开始进行点亮LED的实践练习。这个练习并非直接编写代码,而是需要我们先理解硬件连接。首先,我们需要了解外部硬件(即LED)是如何连接到微控制器的。

理解硬件连接 🔌

为了理解硬件连接,我们必须参考开发板的原理图。原理图展示了板上所有电子元件的连接方式。

以下是获取原理图的步骤:

  1. 打开浏览器,访问搜索引擎。
  2. 输入你的开发板型号(例如:STM32F407G-DISC1)和“原理图”或“schematic”进行搜索。
  3. 从搜索结果中找到并下载原理图文件(通常为PDF格式)。

对于本教程使用的STM32F407G-DISC1开发板,我们已经下载了原理图文件,并会将其附在课程资源中。如果你使用不同的开发板,请下载对应型号的原理图。

在原理图中定位LED 💡

打开原理图文件后,我们需要找到LED的部分。通常可以通过搜索“LED”来快速定位。

在STM32F407G-DISC1的原理图中,我们可以找到四个LED,它们的标识和颜色如下:

  • LD3:绿色LED
  • LD4:橙色LED
  • LD5:红色LED
  • LD6:蓝色LED

每个LED旁边会标注其型号(Part Number),例如绿色LED的型号为“LED GREEN”。

在用户手册和实物板上确认LED 📖

为了在实物开发板上找到这些LED,我们需要参考用户手册。同样,可以通过搜索“板卡型号 + user manual”来下载用户手册。

在用户手册的板卡布局图中,可以清晰地看到标记为LD3、LD4、LD5、LD6的LED位置。你可以对照实物板卡,仔细查看这些标识(它们通常印刷在LED旁边)。

分析LED与微控制器的连接 🧠

回到原理图,查看LED的具体连接。我们发现:

  • 绿色LED(LD4)连接到了 PD12
  • 蓝色LED(LD6)连接到了 PD15

这里的“PD12”代表 Port D(端口D)的第12号引脚。这引出了我们的下一个核心概念:什么是端口?

理解GPIO端口 ⚙️

微控制器(MCU)拥有许多用于连接外部设备的引脚。在STM32系列MCU中,这些引脚被组织成不同的端口(Port),例如Port A、Port B、Port C、Port D等。

查看原理图中MCU的引脚图,可以看到所有引脚都被引出到板载的排针上。每个端口通常包含16个引脚(PA0-PA15, PB0-PB15, 以此类推)。

这些端口引脚被称为 GPIO(General Purpose Input/Output, 通用输入输出)。之所以称为“通用”,是因为它们功能灵活,可以用于多种目的,例如:

  • 连接简单的输入输出设备(如LED、按钮)。
  • 实现通信协议(如UART、I2C、SPI)。

控制引脚的核心问题 ❓

我们的目标是:通过软件控制PD12这个GPIO引脚,使其输出高电平(High)或低电平(Low),从而点亮或熄灭与之连接的绿色LED。

那么,如何通过软件来控制一个具体的硬件引脚呢?这正是我们嵌入式编程的关键。在下一节视频中,我们将深入探讨STM32的GPIO编程模型,学习如何配置和控制引脚。

本节课中,我们一起学习了如何查阅硬件文档(原理图和用户手册),定位了LED在板卡上的位置,并理解了LED是通过GPIO端口连接到微控制器的。我们还引入了GPIO端口的概念,为接下来的实际编程打下了基础。

044:通过软件控制IO引脚

在本节课中,我们将学习如何通过软件来控制微控制器的输入输出引脚。我们将了解一个名为“G”的外设,它用于控制特定端口上的引脚,并探讨如何通过访问其寄存器来实现控制。

通过软件控制引脚

上一节我们介绍了微控制器的基本引脚功能。本节中我们来看看如何通过软件来具体控制这些引脚。

在微控制器内部,有一个被称为“G”的外设。这个外设专门用于控制端口D上的引脚。它拥有一组自己的寄存器,这些寄存器用于配置引脚的模式、状态以及其他功能。

软件通过向这些寄存器写入特定值,即可控制引脚的工作模式、发送数据或从端口读取数据。以下是可以通过寄存器完成的主要活动:

  • 控制引脚的模式(如输入、输出)。
  • 控制通过引脚发送的数据。
  • 向端口写入数据。
  • 从端口读取数据。

如何访问寄存器

那么,如何访问这些寄存器呢?答案是使用内存地址。

每个寄存器都有其唯一的内存地址。通过这个内存地址,你可以访问对应的寄存器,从而控制特定的引脚。因此,我们也可以说“G”外设的寄存器是内存映射的。

这引出了我们下一个要讨论的话题:什么是内存映射输入输出。

什么是内存映射输入输出

内存映射输入输出是指,输入输出引脚通过外设寄存器进行控制,而这些寄存器被映射到处理器可寻址的内存位置上。

至于什么是处理器可寻址的内存位置,我们将在下一个视频中详细讲解。

本节课中,我们一起学习了通过软件控制IO引脚的基本原理,了解了外设寄存器的作用及其通过内存地址进行访问的方式。我们还引入了内存映射输入输出的概念,为后续深入学习奠定了基础。

045:处理器可寻址内存区域

概述

在本节课中,我们将学习处理器可寻址内存区域的概念。我们将了解基于ARM Cortex-Mx/M4 CPU的微控制器中,处理器如何通过系统总线与内存和外设通信,以及32位地址总线如何定义4GB的线性地址空间。理解这一点是掌握微控制器内存映射和外设编程的基础。

系统总线与地址空间

上一节我们介绍了嵌入式系统的基本组成。本节中,我们来看看连接处理器、内存和外设的核心通道——系统总线。

在基于ARM Cortex-Mx/M4 CPU的微控制器中,系统总线是连接处理器、内存和外设的中央通道。这条总线基于ARM公司设计的AHB规范。AHB代表高级高性能总线

该系统总线包含两个主要通道:

  • 一个32位地址通道
  • 一个32位数据通道

这意味着地址总线可以承载 2^32 个不同的地址,用于寻址不同的外设和内存。例如,若想将数据从内存传输到GPIO外设,处理器需要将目标GPIO寄存器的特定地址放到地址总线上,并通过数据总线发送数据。

处理器可寻址位置

既然地址总线宽度为32位,处理器能够生成的地址范围是从 0x0000_00000xFFFF_FFFF。这相当于 4GB 的不同地址位置。

处理器内部有一个地址生成单元。当指令被解码后,该单元会被激活,并将目标地址放置到地址总线上。这个地址决定了总线是与微控制器的代码内存、数据内存还是某个外设寄存器进行通信。

内存映射

上一节我们明确了处理器可以访问4GB的地址空间。本节中,我们来看看这些地址是如何具体分配给不同硬件资源的。

处理器将地址放置在地址总线上时,根据地址值所属的特定区域,总线会与相应的硬件模块对话。这种将程序内存、数据内存以及各种外设寄存器组织在同一个4GB线性地址空间内的安排,就称为处理器的内存映射

这个内存映射是由ARM Cortex-Mx架构固定的。任何采用该处理器的微控制器设计者都必须遵循此映射规则。该图表在ARM Cortex-Mx技术参考手册中有详细说明。

总结与应用

本节课中,我们一起学习了处理器可寻址内存区域的核心概念。我们了解到:

  1. 系统总线(基于AHB)是处理器与内存、外设通信的通道。
  2. 32位地址总线定义了 2^32 = 4GB 的线性地址空间。
  3. 处理器的内存映射将这4GB空间划分为不同区域,分别对应代码区、数据区和各个外设。
  4. 例如,GPIO外设寄存器的地址就位于这个地址空间内的某个特定范围。

掌握内存映射后,我们的工作就变得简单了:一旦知道了某个外设寄存器(如GPIO)的确切地址,我们就可以在C语言中将其视为一个指针,通过读写该指针变量来直接控制外设。在接下来的视频中,我们将探索STM32微控制器的实际内存映射,这将使你的理解更加清晰。


核心概念公式与代码表示:

  • 可寻址位置总数地址总数 = 2^(地址总线宽度)
  • 对于32位地址总线:可寻址位置 = 2^32 = 4,294,967,296 个地址 (4GB)
  • 地址范围:从 0x000000000xFFFFFFFF
  • 访问外设寄存器(概念示例)
    // 假设 GPIOA 数据输出寄存器的地址是 0x40020000
    volatile uint32_t *pGPIOA_OUT = (volatile uint32_t *)0x40020000;
    // 通过指针写入数据,控制外设
    *pGPIOA_OUT = 0x00000001;
    

046:STM32存储器映射 📖

在本节课中,我们将要学习STM32微控制器的存储器映射。理解存储器映射是进行底层寄存器编程的基础,它定义了不同功能模块(如GPIO、ADC、定时器等)在微控制器地址空间中的位置。

上一节我们介绍了嵌入式系统的基本概念,本节中我们来看看STM32如何组织其内部地址空间。

存储器映射概述

存储器映射是微控制器设计的一部分,它规定了内部所有存储器(如Flash、RAM)和外设寄存器在统一地址空间中的分配情况。对于STM32 F407这款微控制器,其具体的存储器映射信息可以在其参考手册中找到。

大多数情况下,存储器映射信息会列在微控制器的参考手册中。在开始编程之前,必须查阅这个映射表,否则你将无法知道各个外设寄存器的具体地址。

如何查找外设地址

以下是查找外设基地址的步骤:

  1. 打开STM32微控制器的参考手册。
  2. 在目录中找到“内存和总线架构”或类似章节。
  3. 进入“存储器映射”部分,你会看到一个详细的地址分配表格。

例如,如果我们想控制LED,就需要配置GPIO外设的寄存器。通过查阅手册,我们可以找到GPIO D外设的基地址。

在参考手册中浏览,你可以找到GPIO D外设,其第一个寄存器的地址是 0x40020C00。这个地址就是GPIO D外设寄存器的基地址。

如果你想查找ADC寄存器的基地址,只需向下滚动手册,找到ADC相关的部分。对于STM32F407,它有三个ADC模块(ADC1, ADC2, ADC3),它们的寄存器地址范围会在手册中明确给出。

同样地,手册中也会列出定时器(如TIM1, TIM9, TIM10)、串口(UART)、CAN控制器、以太网MAC等所有外设的寄存器地址空间。

当处理器需要访问某个外设时,它会将对应的地址放到地址总线上,地址总线随后会与目标外设(如CAN控制器)的寄存器进行通信。

STM32 F407的存储器布局

STM32微控制器基于ARM Cortex-M处理器内核设计。在其存储器映射表中,你可以看到不同内存区域的划分。

  • Flash存储器:用于存储程序代码,起始于特定的地址。
  • 嵌入式SRAM:用于存储数据,起始于另一个特定的地址。
  • 系统存储器:包含Bootloader等系统代码。
  • 外设寄存器区域:所有外设(如GPIO、ADC、定时器)的寄存器都映射到这个连续的地址空间内。

此外,还有重映射区域和FMC(可变静态存储控制器)等区域。重要的是,无论你使用哪款STM32微控制器,都需要打开对应的用户手册来定位其存储器映射图。没有这个映射图,你将无法进行外设的编程。

关键地址示例与注意事项

我们之前找到了GPIO D外设的地址范围。对于STM32 F407G微控制器,GPIO D的地址范围是 0x40020C00 起始的一段空间。

如果你使用的是不同的STM32微控制器,请务必参考该设备的参考手册以获取正确的地址范围。不要盲目地使用教程中的地址进行编码,因为不同型号的芯片其外设地址可能不同。在接下来的视频中,我们将使用这个地址,请提前核对确认。

本节课中我们一起学习了STM32存储器映射的概念和查阅方法。我们了解到存储器映射是连接软件代码与硬件外设的桥梁,通过参考手册中的映射表,我们可以找到任何外设寄存器的准确地址,这是进行底层驱动开发的第一步。下一节,我们将深入探讨外设寄存器本身的结构与功能。

047:内存映射外设寄存器与IO访问

在本节课中,我们将要学习处理器可寻址内存位置的概念,以及如何通过内存映射来访问微控制器的外设和存储器。理解这些概念是进行嵌入式系统编程的基础。

处理器可寻址内存位置

上一节我们介绍了系统总线的基本概念,本节中我们来看看处理器如何通过地址总线来定位不同的硬件资源。

考虑一个基于 ARM Cortex-Mx/M4 CPU 的微控制器系统。系统总线(也称为系统 P 总线,基于 ARM 的 AHB 规范)连接着处理器、存储器和外设。这条总线包含两个通道:

  • 一个 32 位地址通道
  • 一个 32 位数据通道

由于地址总线宽度为 32 位,这意味着处理器可以在地址总线上放置 2^32 个不同的地址,即 4 GB 的地址空间,以此来定位不同的存储器和外设。

例如,如果你想将数据从数据存储器传输到 GPIO 外设以输出到外部引脚,你需要通过系统总线将数据发送到该外设的某个寄存器中。为此,你必须将正确的地址放置在地址总线上,以“瞄准”这个特定的外设寄存器。

内存映射

上一节我们了解了地址总线的能力,本节中我们来看看这些地址是如何被组织起来,以对应微控制器内部的不同资源的。

处理器不能随意使用地址来访问外设。例如,要读取 ADC 外设的数据,必须在地址总线上放置正确的地址。这个将 4 GB 线性地址空间划分为不同区域,并分配给代码存储器、数据存储器和各个外设寄存器的安排,就称为处理器的内存映射

这个内存映射是由 ARM Cortex-Mx 架构定义的。微控制器设计者在使用该处理器时,必须遵循这个映射规则。该图表可以在 ARM Cortex-Mx 技术参考手册中找到。

以下是核心结论:

  • 程序存储器、数据存储器和各种外设的寄存器都被组织在同一个线性的 4 GB 地址空间内。
  • GPIO 作为一个外设,其寄存器的地址必须落在为外设保留的地址区域内。

一旦你知道了某个外设寄存器的具体地址(例如 GPIO 的某个寄存器地址),你的工作就变得简单了。你可以将这个地址视为一个指针,通过读写指向这个地址的指针变量,就可以控制该外设。

探索实际内存映射

在下一节视频中,我们将探索 STM32 微控制器的实际内存映射图。通过查看具体实例,你的疑问将会得到进一步澄清。

本节课中我们一起学习了处理器可寻址内存空间和内存映射的核心概念。我们了解到,处理器通过 32 位地址总线可以访问 4 GB 的空间,这个空间被预先划分并映射到了芯片内部的存储器和外设寄存器上。掌握内存映射是直接通过 C 语言指针操作硬件寄存器的基础。

048:点亮LED程序教程 🚀

在本节课中,我们将学习如何为STM32微控制器编写代码来点亮一个LED。这是一个嵌入式系统编程的经典入门练习,涉及硬件配置、寄存器操作等核心概念。

点亮LED的过程并非简单地写一行代码,它要求我们理解微控制器的内存映射、外设寄存器、硬件连接等知识。这正是嵌入式编程的独特之处。因此,请耐心跟随我们,逐步完成点亮LED所需的所有步骤。

点亮LED的步骤概述 💡

以下是点亮一个LED所需遵循的五个核心步骤。我们将逐一进行详细说明。

  1. 识别连接LED的GPIO端口:首先,需要确定LED连接到了哪个GPIO端口。在本例中,我们使用的是 GPIOD 端口。
  2. 识别连接LED的GPIO引脚:接着,需要确定LED具体连接到了该GPIO端口的哪个引脚。在本例中,我们使用的是 引脚2
  3. 激活GPIO外设:在大多数STM32微控制器中,外设默认是关闭的。因此,我们需要通过启用其时钟来激活GPIO外设。这是关键的第一步,因为只有激活后,外设才能接收我们的配置指令。
  4. 配置GPIO引脚模式为输出:由于我们要驱动LED(输出高/低电平),因此必须将对应GPIO引脚的工作模式配置为输出模式
  5. 向GPIO引脚写入数据:最后,通过向该引脚写入逻辑 1(高电平,约3.3V)来点亮LED,或写入逻辑 0(低电平,0V)来熄灭LED。

步骤详解 🔍

上一节我们概述了点亮LED的五个步骤,本节中我们来详细看看每个步骤的含义和背后的原理。

1. 识别GPIO端口与引脚

这是硬件连接的基础。您需要根据您的开发板原理图或文档,找到LED所连接的端口和引脚。例如,我们的示例中使用的是 GPIOD 端口的 Pin 2

2. 激活GPIO外设(启用时钟)

这是嵌入式编程中一个非常重要的概念。在STM32中,为了降低功耗,大多数外设的时钟默认是关闭的。一个未被激活(时钟未开启)的外设是“死”的,它不会工作,也不会接受您的任何配置。

核心概念:必须通过配置微控制器的外设时钟寄存器来为特定外设(如GPIOD)启用时钟。只有时钟开启后,该外设才能正常工作并响应配置。

注意:不同厂商的微控制器设计可能不同。对于ST公司的产品(如STM32),外设通常默认关闭。请务必参考您所用芯片的《参考手册》来确认。

3. 配置引脚模式为输出

GPIO引脚可以配置为多种模式,如输入、输出、复用功能等。要驱动LED,我们需要控制引脚输出高电平或低电平,因此必须将其设置为通用推挽输出模式。这通过配置GPIO端口模式寄存器来实现。

4. 写入数据控制LED

配置完成后,我们就可以通过向GPIO端口输出数据寄存器中的特定位写入 10 来控制对应引脚的电平,从而控制LED的亮灭。

总结与预告 📚

本节课中,我们一起学习了点亮STM32上LED的完整流程。我们了解到,这个过程需要五个步骤:识别端口和引脚、激活外设时钟、配置引脚为输出模式,最后写入数据。其中,启用外设时钟是让硬件“活”起来的关键前提。

在接下来的视频中,我们将深入探讨如何通过寄存器操作来启用GPIO外设的时钟,这是将理论转化为实践代码的核心环节。

祝您学习愉快!

049:使能外设时钟

概述

在本节课程中,我们将学习如何为STM32微控制器上的外设(例如GPIO端口)使能时钟。这是配置和使用任何外设前必须完成的关键步骤,因为未使能时钟的外设将不会响应任何配置操作。

使能外设时钟的必要性

上一节我们介绍了外设的基本概念。本节中我们来看看如何激活它们。在配置外设之前,必须首先使能其时钟。否则,外设将不会接受任何配置值。这是因为时钟信号驱动着外设内部的逻辑电路,没有时钟,外设就无法工作。

时钟控制寄存器

我们可以通过微控制器的控制寄存器来使能外设时钟。在STM32微控制器中,时钟控制寄存器被映射到内存映射中的一个特定地址范围。

这个地址范围是:0x4002 38000x4002 3BFF

负责管理时钟的模块称为RCC,即复位与时钟控制模块。RCC模块负责控制微控制器各个部分的时钟,包括处理器、不同外设、总线和存储器等。

定位正确的RCC寄存器

RCC模块有自己的一组寄存器来控制时钟,这些寄存器就位于上述地址范围内。我们需要找到并操作RCC中正确的寄存器来为目标外设使能时钟。

以下是定位和操作RCC寄存器的步骤:

  1. 查阅参考手册:打开STM32的参考手册,找到RCC(复位与时钟控制)章节。
  2. 确定外设所在的总线:不同的外设挂载在不同的总线上。例如,GPIO端口D(GPIOD)挂载在AHB1总线上。这可以通过查阅数据手册中的内部架构图或用户手册来确认。
  3. 选择对应的时钟使能寄存器:在RCC章节中,找到控制AHB1总线上外设时钟的寄存器,即 RCC AHB1外设时钟使能寄存器
  4. 计算寄存器地址:该寄存器有一个偏移地址(例如 0x30)。将此偏移地址加到RCC模块的基地址(0x4002 3800)上,即可得到该寄存器的完整地址。
  5. 设置正确的位:在RCC AHB1外设时钟使能寄存器中,每个位控制一个特定外设的时钟。例如,要使能GPIOD的时钟,需要将该寄存器中对应的位(例如位3)设置为 1。当前该位为 0,表示时钟被禁用。

总结

本节课中我们一起学习了使能STM32外设时钟的完整流程。核心步骤是:首先通过参考手册确定外设所在的总线,然后在RCC模块中找到对应的总线时钟使能寄存器,最后通过设置该寄存器中特定的位来开启时钟。这个过程是后续所有外设配置和操作的基础。

050:计算外围寄存器地址

在本节课中,我们将学习如何计算STM32微控制器中外围寄存器的地址。这是直接通过内存地址操作硬件寄存器、控制GPIO引脚等外设的基础。

上一节我们介绍了如何通过RCC寄存器来启用GPIO端口的时钟。本节中,我们来看看如何具体计算所需操作寄存器的内存地址。

计算RCC寄存器地址

首先,我们需要计算用于启用GPIO时钟的RCC寄存器地址。具体是AHB1ENR寄存器。

计算地址需要两个部分:基地址偏移量。地址计算公式为:
寄存器地址 = 基地址 + 偏移量

以下是计算步骤:

  1. 查找基地址:根据芯片的内存映射图,RCC外设的基地址是0x4002 3800
  2. 查找偏移量:从参考手册可知,AHB1ENR寄存器相对于RCC基地址的偏移量是0x30
  3. 进行计算:将基地址与偏移量相加。
    0x40023800 + 0x30 = 0x40023830

因此,AHB1ENR寄存器的地址是0x40023830

计算GPIO模式寄存器地址

接下来,为了控制GPIO引脚的工作模式,我们需要计算GPIO模式寄存器(MODER)的地址。我们以GPIOD为例。

以下是计算GPIOD MODER地址的步骤:

  1. 查找基地址:根据内存映射,GPIOD外设的基地址是0x4002 0C00
  2. 查找偏移量:从GPIO寄存器手册中可以看到,模式寄存器(MODER)的偏移量是0x00
  3. 进行计算
    0x40020C00 + 0x00 = 0x40020C00

所以,GPIOD模式寄存器的地址就是其基地址本身,即0x40020C00

关于模式寄存器:这是一个32位寄存器,每2位控制一个引脚的模式(例如,引脚0由位[1:0]控制)。模式编码为:

  • 00:输入模式
  • 01:通用输出模式
  • 10:复用功能模式
  • 11:模拟模式

在本练习中,我们需要将引脚12设置为输出模式,因此需要将位[25:24]设置为01

计算GPIO输出数据寄存器地址

最后,为了控制引脚输出高电平或低电平,我们需要计算GPIO输出数据寄存器(ODR)的地址。

以下是计算GPIOD ODR地址的步骤:

  1. 使用相同的基地址:GPIOD的基地址仍然是0x4002 0C00
  2. 查找偏移量:输出数据寄存器(ODR)的偏移量是0x14
  3. 进行计算
    0x40020C00 + 0x14 = 0x40020C14

因此,GPIOD输出数据寄存器的地址是0x40020C14

关于输出数据寄存器:该寄存器的低16位(位[15:0])分别对应端口的16个引脚。将某一位设置为1,对应的引脚输出高电平;设置为0则输出低电平。例如,要控制引脚12,就需要操作位12。

总结

本节课中我们一起学习了STM32寄存器地址计算的核心方法。我们掌握了通过 基地址 + 偏移量 的公式来计算特定外设寄存器的绝对内存地址。我们具体计算了三个关键寄存器的地址:

  1. RCC的AHB1ENR寄存器(0x40023830),用于启用时钟。
  2. GPIOD的MODER寄存器(0x40020C00),用于设置引脚模式。
  3. GPIOD的ODR寄存器(0x40020C14),用于控制引脚输出电平。

有了这些地址,我们就可以在接下来的课程中,通过创建指针变量来访问它们,并通过设置相应的位字段来启用时钟、配置引脚为输出模式,最终点亮LED。

051:LED 驱动程序设计编码部分 第1集

概述

在本节课中,我们将开始动手编写LED驱动程序的代码。我们将学习如何在STM32项目中创建新工程,并初始化用于控制GPIO外设的指针变量。


创建新STM32项目

上一节我们讨论了LED驱动的基本原理,本节中我们来看看如何开始编码。首先,我们需要在集成开发环境中创建一个新的STM32项目。

  1. 打开你的IDE,并选择一个合适的工作空间。
  2. 点击 File -> New,然后选择创建 STM32 Project
  3. 项目创建向导启动后,需要一些时间加载。

选择开发板型号

项目初始化完成后,需要选择你所使用的具体开发板型号。

  1. 在目标选择器中,找到并选择你的开发板。例如,教程中使用的是 STM32F407G
  2. 点击 Next 进入下一步。
  3. 为项目命名,例如 LED_Control
  4. 在项目设置中,选择 Empty 模板,其他选项保持默认。
  5. 点击 Finish 完成项目创建。IDE需要一些时间来生成项目文件。

处理初始警告

项目创建后,你可能会在 main.c 文件中看到一个警告或错误。

以下是解决方法:

  1. 右键点击项目,选择 Properties
  2. 导航到 C/C++ Build -> Settings -> Tool Settings 选项卡。
  3. MCU GCC Compiler -> Preprocessor 部分,将 Define symbols 中的内容清空或设置为 none
  4. 点击 Apply and Close。之前的警告就会消失。

初始化指针变量

现在项目已设置完毕,我们可以开始编写代码。首先,我们需要创建指针变量来存储GPIO外设寄存器的内存地址。所有外设寄存器都是32位的。

我们将使用以下数据类型和步骤:

  • 数据类型uint32_t,用于确保变量是32位无符号整数。
  • 指针声明:使用星号 * 来声明指针变量。

以下是创建和初始化第一个指针变量的代码示例:

uint32_t *pRCC;
pRCC = (uint32_t*)0x40023830;

代码解释:

  • uint32_t *pRCC; 声明了一个名为 pRCC 的指针变量。
  • 0x40023830 是RCC(复位和时钟控制)寄存器中某个特定寄存器的内存地址。
  • 由于编译器将 0x40023830 视为一个普通数字,而我们需要的是一个内存地址,因此使用 (uint32_t*) 进行强制类型转换,将其转换为指向 uint32_t 类型的指针。

按照同样的方法,我们需要再创建两个指针变量。为了代码清晰,建议使用有意义的名称。

以下是创建另外两个寄存器的指针变量:

uint32_t *pMODER;
pMODER = (uint32_t*)0x40020C00;

uint32_t *pOUTPUT;
pOUTPUT = (uint32_t*)0x40020C14;

变量说明:

  • pMODER:指向GPIO模式寄存器的指针。
  • pOUTPUT:指向GPIO输出数据寄存器的指针。

至此,我们已经成功创建并初始化了三个控制LED所需的关键寄存器指针变量。


总结

本节课中,我们一起学习了LED驱动程序编码的初始步骤。我们首先创建了一个新的STM32项目,并正确配置了开发环境。接着,我们声明并初始化了三个重要的指针变量(pRCCpMODERpOUTPUT),它们分别对应着时钟配置、引脚模式设置和输出控制的寄存器地址。这为下一步实际配置引脚和点亮LED打下了基础。

052:LED运动编码第二部分

在本节课中,我们将学习如何为STM32微控制器的GPIO端口启用时钟。这是配置外设、使其正常工作的关键步骤。我们将重点介绍如何安全地操作寄存器中的特定位,而不影响其他位。

上一节我们介绍了如何查找和定义寄存器的内存地址。本节中我们来看看如何通过位操作来启用GPIO端口的时钟。

启用GPIO时钟

我们的第四步是启用GPIO端口的时钟。这需要操作RCC_AHB1ENR寄存器。具体来说,我们需要将该寄存器的第三位设置为1。

操作寄存器时,必须非常小心。只能修改我们需要的特定位,而不能影响寄存器中的其他位。如果错误地更改了其他位,可能会导致程序出现严重问题,尤其是在大型项目中。因此,我们将使用位操作技术来精确控制。

使用位操作设置特定位

为了安全地设置寄存器的第三位,我们需要遵循“读-改-写”的操作流程。以下是具体步骤:

  1. 读取:首先,将寄存器的当前值读取到一个临时变量中。
  2. 修改:然后,使用位操作(此处为按位或运算)修改临时变量中的特定位。
  3. 写入:最后,将修改后的值写回寄存器。

这样就能确保只改变目标位,而其他位保持不变。

代码实现

以下是实现上述步骤的C语言代码示例。我们假设已经定义了一个指向RCC_AHB1ENR寄存器地址的指针变量 RCC_AHB1ENR_PTR

// 步骤1:读取寄存器的当前值到一个临时变量
uint32_t temp1 = *RCC_AHB1ENR_PTR;

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-embsys/img/470b92bde461b83eda49abbb51782a03_4.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-embsys/img/470b92bde461b83eda49abbb51782a03_6.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-embsys/img/470b92bde461b83eda49abbb51782a03_7.png)

// 步骤2:使用按位或运算设置第三位(位2,因为从0开始计数)
// 掩码值 0x08 的二进制为 0000 1000,即第三位为1
temp1 = temp1 | 0x08;

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-embsys/img/470b92bde461b83eda49abbb51782a03_9.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-embsys/img/470b92bde461b83eda49abbb51782a03_11.png)

// 步骤3:将修改后的值写回寄存器
*RCC_AHB1ENR_PTR = temp1;

代码解释

  • uint32_t temp1 = *RCC_AHB1ENR_PTR;:这行代码通过解引用指针,将寄存器当前的值复制到临时变量temp1中。
  • temp1 = temp1 | 0x08;:这行代码对temp1和掩码0x08进行按位或运算。无论temp1的第三位原来是0还是1,运算后该位都会被设置为1,其他位则保持不变。
  • *RCC_AHB1ENR_PTR = temp1;:最后,将已设置好第三位的temp1值写回寄存器,完成时钟启用操作。

通过这三步操作,我们精确地启用了GPIO端口的时钟,而不会干扰到RCC_AHB1ENR寄存器中可能被其他功能使用的其他位。

本节课中我们一起学习了如何为STM32的GPIO端口启用时钟。关键在于理解并应用“读-改-写”的寄存器操作流程,并使用位运算来安全、精确地控制特定位。这是嵌入式编程中一项基础且重要的技能。

053:LED控制

在本节课中,我们将学习如何通过直接操作寄存器来控制STM32微控制器上的GPIO引脚,以实现LED的点亮。我们将完成三个核心任务:启用GPIO外设时钟、配置引脚为输出模式、以及设置引脚输出为高电平。

启用GPIO外设时钟

首先,我们需要启用GPIO端口的时钟。在STM32中,外设需要通过AHB1总线使能时钟后才能使用。我们将通过设置RCC_AHB1ENR寄存器的相应位来完成此操作。

以下是具体步骤:

  1. 首先,我们需要找到控制GPIO端口(例如GPIOD)时钟的位。对于GPIOD,这是第3位。
  2. 然后,我们通过位或(|=)操作将该位置1,同时不影响寄存器中的其他位。
// 启用 GPIOD 的时钟 (AHB1ENR 寄存器的第3位)
RCC->AHB1ENR |= (1 << 3);

上一节我们介绍了启用时钟的基本原理,本节中我们来看看如何配置GPIO引脚的模式。

配置GPIO引脚为输出模式

时钟启用后,下一步是配置目标引脚(例如PD12)为通用输出模式。这需要通过配置GPIOx_MODER(模式寄存器)来实现。每个引脚由该寄存器中的连续2个位控制。

以下是具体步骤:

  1. 首先,我们需要清除目标引脚对应的两个模式位(例如PD12对应位24和位25),将它们设置为00(输入模式)。
  2. 然后,我们将这两个位设置为01,即将引脚配置为通用输出模式。
// 假设 pGPIOx_MODER 是指向 GPIOD->MODER 寄存器的指针
// 1. 清除 PD12 对应的模式位 (位24和位25)
*pGPIOx_MODER &= ~(0x3 << 24);
// 2. 将 PD12 配置为通用输出模式 (01)
*pGPIOx_MODER |= (0x1 << 24);

通过以上操作,我们完成了引脚的输出模式配置。接下来,我们将学习如何控制这个引脚的输出电平。

设置GPIO引脚输出为高电平

引脚模式配置完成后,我们就可以通过GPIOx_ODR(输出数据寄存器)来控制其输出电平。将对应引脚的位置1,即可输出高电平(点亮LED)。

以下是具体步骤:

  1. 找到控制目标引脚(PD12)的位,即第12位。
  2. 使用位或(|=)操作将该位置1。
// 将 PD12 引脚输出设置为高电平
GPIOD->ODR |= (1 << 12);

代码整合与测试

现在,我们将以上三个步骤的代码整合到主函数中。完整的初始化顺序应为:先启用时钟,再配置模式,最后设置输出电平。

int main(void)
{
    // 1. 启用 GPIOD 时钟
    RCC->AHB1ENR |= (1 << 3);

    // 2. 配置 PD12 为输出模式
    // 清除模式位
    GPIOD->MODER &= ~(0x3 << 24);
    // 设置为通用输出模式
    GPIOD->MODER |= (0x1 << 24);

    // 3. 设置 PD12 输出高电平,点亮LED
    GPIOD->ODR |= (1 << 12);

    while(1)
    {
        // 主循环
    }
}

编写完代码后,将其编译并下载到STM32开发板进行测试。如果连接正确,对应的LED应该被点亮。

总结

本节课中我们一起学习了STM32 GPIO编程的三个核心步骤:

  1. 启用外设时钟:通过设置RCC_AHB1ENR寄存器来激活GPIO端口的时钟。
  2. 配置引脚模式:通过操作GPIOx_MODER寄存器,将特定引脚设置为通用输出模式。
  3. 控制输出电平:通过读写GPIOx_ODR寄存器,可以设置引脚输出高电平或低电平,从而控制外部设备如LED。

通过直接操作寄存器,我们掌握了最底层的硬件控制方法,这是理解和使用STM32 HAL库或LL库的重要基础。

054:LED 运动编码部分四

在本节课中,我们将学习如何编译一个嵌入式项目,并使用调试器实时观察微控制器寄存器的变化,以验证我们的代码是否正确配置了硬件。

上一节我们介绍了如何通过指针操作寄存器来配置GPIO。本节中,我们来看看如何编译项目,并使用调试工具验证寄存器的配置过程。

编译项目

首先,我们需要编译整个项目。在集成开发环境中,找到编译或构建的选项并执行。

以下是编译项目的步骤:

  1. 在IDE中定位到构建菜单。
  2. 选择“全部构建”选项以编译整个项目。

观察寄存器内容

编译完成后,我们可以使用调试器来观察微控制器内部寄存器的实时状态。这有助于监控微控制器的各种实时活动。

为了观察寄存器,我们需要打开相应的调试视图。

以下是打开寄存器观察窗口的步骤:

  1. 在IDE的菜单栏中,找到“窗口”选项。
  2. 选择“显示视图” -> “其他...”。
  3. 在弹出的对话框中,搜索并选择“寄存器”视图,然后打开它。

打开后,你将看到一个寄存器窗口。这里显示的是微控制器的通用寄存器。

如果你想观察特定外设的寄存器,可以打开另一个名为“SFRs”(特殊功能寄存器)的视图。

以下是观察外设寄存器的步骤:

  1. 在“显示视图”中,找到并打开“SFRs”视图。
  2. 在这个视图中,你可以找到并展开特定的外设,例如 GPIOA,来查看其相关寄存器。

调试与验证

现在,让我们开始调试程序,并逐步执行代码,观察寄存器的变化。

首先,我们需要重置芯片,使其恢复到初始状态。在调试工具栏中,找到并点击“重置”按钮。

重置后,我们可以看到所有寄存器的值都恢复为默认值(通常是0)。例如,模式寄存器的值现在是0。

接下来,我们开始逐行执行代码。使用“单步跳过”功能(通常是F6键)来执行每一条C语言语句。

我们的第一行代码是编程RCC模块中的 AHB1ENR 寄存器,以启用GPIO端口的时钟。

在SFRs视图中,找到 RCC 寄存器组,然后定位到 AHB1ENR 寄存器。执行单步操作后,观察该寄存器的值是否从默认值变为我们设定的值(例如,某一位从0变为1)。这证明我们成功启用了GPIO端口的时钟。

如果这一步没有成功,寄存器的值将保持不变。因此,跟踪寄存器的变化对于验证代码是否正确操作硬件至关重要。

启用时钟后,下一步是配置GPIO的模式寄存器。在SFRs视图中,找到 GPIOA 下的模式寄存器。

继续单步执行代码,配置模式寄存器。观察寄存器中特定位置(例如第24位和25位)的值是否按照我们的程序从 00 变为 01。这个变化意味着我们成功将对应的GPIO引脚配置为输出模式。

如果配置正确,相应的LED应该被点亮。你可以在调试器的图形化界面中查看引脚的状态图进行确认。

核心概念总结

这种方法的核心是利用指针访问内存映射的寄存器。其通用公式可以表示为:

*(volatile uint32_t *) (寄存器地址) = 要写入的值;

通过这种方式,我们可以配置任何微控制器的任何外设寄存器。

本节课中我们一起学习了如何编译嵌入式项目,并使用调试器逐步跟踪和验证寄存器配置的过程。我们看到了如何启用外设时钟、配置GPIO模式,并通过观察寄存器值的变化来确认操作是否成功。掌握这种调试方法对于嵌入式开发至关重要。

055:按位右移运算符

概述

在本节课程中,我们将学习C语言中的按位右移运算符。我们将了解其语法、工作原理,并通过代码示例演示如何对一个数值进行右移操作。


按位右移运算符介绍

上一节我们介绍了按位运算符的基本概念。本节中,我们来看看按位右移运算符。

按位右移运算符(>>)用于将一个数的所有二进制位向右移动指定的位数。其语法如下:

语法: 操作数1 >> 操作数2

其中,操作数1 是要被移位的值,操作数2 指定了要右移的位数。


代码示例与解析

以下是使用按位右移运算符的一个具体示例。

#include <stdio.h>

int main() {
    // 定义一个字符型变量并赋值
    char a = 111;
    // 将变量a的二进制位向右移动4位,结果赋值给b
    char b = a >> 4;

    // 打印移位后的结果
    printf("The value of b is: %d\n", b);
    return 0;
}

运行上述代码,变量 b 的值将是 6。这是因为十进制数 111 的二进制形式(以8位表示)向右移动了4位,空出的高位用 0 填充,从而得到了新的二进制值,其对应的十进制数就是 6


使用整数变量进行移位

为了更清晰地展示,我们可以使用整数变量进行同样的操作。

以下是另一个示例,演示了对整数 81 进行右移操作。

#include <stdio.h>

int main() {
    // 定义一个整型变量并赋值
    int x = 81;
    // 将变量x的二进制位向右移动4位,结果赋值给y
    int y = x >> 4;

    // 打印移位后的结果
    printf("The value of y is: %d\n", y);
    return 0;
}

运行此代码,变量 y 的值将是 5。这同样是因为 81 的二进制位右移4位后,产生了新的二进制序列,其对应的十进制值为 5


核心概念总结

按位右移运算符的核心行为可以总结为以下两点:

  1. 移位:将数值的所有二进制位整体向右移动。
  2. 补位:在左侧(高位)空出的位置上填充 0

公式化描述: 对于一个数 A 右移 n 位,可以近似理解为执行 A / (2^n) 的整数除法运算。


总结

本节课中我们一起学习了按位右移运算符(>>)。我们了解了它的语法,并通过具体的代码示例,观察了它对字符型和整型变量进行移位操作的结果。关键点在于,右移操作会将二进制位向右移动,并在高位补零,从而得到一个新的数值。下一节,我们将继续学习按位左移运算符。

构建嵌入式系统:ARM Cortex (STM32) 基础:第56章:位左移运算符

在本节课中,我们将要学习C语言中的位左移运算符。上一节我们介绍了位右移运算符,本节中我们来看看它的对称操作——位左移运算符。

位左移运算符的语法与右移类似,它需要两个操作数。其基本形式为:操作数1 << 操作数2。它的功能是将操作数1的二进制位向左移动,移动的位数由操作数2决定。

以下是位左移运算的一个具体示例。

int a = 806;
int c = a << 4;
printf(“c的值为:%d”, c);

在这个例子中,变量a的值为806。执行a << 4操作后,结果被赋值给变量c。运行程序后,c的值将变为12896。

现在,让我们理解二进制位是如何移动的。当执行左移操作时,数值的二进制位整体向左移动指定的位数。左侧(高位)溢出的位将被丢弃,而右侧(低位)空出的位则用0来填充。

正是由于比特位的移动和填充,导致了数值发生了改变。在嵌入式编程中,理解和掌握位左移操作至关重要。

本节课中我们一起学习了位左移运算符的语法、功能及其运算过程。在下一节视频中,我们将探讨位移动运算在嵌入式编程代码中的具体应用。

057:位运算移位运算符的适用性 🧮

在本节课中,我们将学习位运算中的移位运算符在嵌入式系统开发中的具体应用。我们将重点探讨如何使用移位运算符来简化设置和清除特定位的操作,这对于硬件寄存器编程至关重要。

上一节我们介绍了位运算的基本概念,本节中我们来看看移位运算符如何在实际编程中发挥作用。

概述

在嵌入式编程中,我们经常需要操作硬件寄存器的特定位,例如开启或关闭某个外设功能。位运算,特别是移位运算符,是实现这种“位掩码”操作的核心工具。它们能极大地简化代码,尤其是在处理多位宽数据时。

设置特定位

假设我们需要设置一个8位数据的第4位(从0开始计数,即二进制从右往左数的第5位)。传统方法是直接使用位或(|)运算和一个手动计算的掩码值。

以下是传统方法:

data = data | 0x10; // 0x10的二进制是0001 0000,用于设置第4位

这种方法在数据位宽较小时(如8位)是可行的。但当处理32位或64位数据时,手动计算和书写掩码值(如0x000000100x0000000000000010)会变得繁琐且容易出错。

因此,我们推荐使用移位运算符来动态生成掩码。其核心思想是:数字1的二进制形式(0000 0001)在经过左移后,可以精确地在指定位生成一个“1”。

以下是使用移位运算符的方法:

data = data | (1 << 4); // 将数字1左移4位,生成掩码0001 0000

公式目标掩码 = 1 << n,其中 n 是需要设置的位的位置(从0开始计数)。

这种方法更加直观和灵活。你只需要记住“1左移n位”,而无需记忆具体的十六进制掩码值。

清除特定位

与设置位类似,清除特定位也可以借助移位运算符来简化。传统方法是使用位与(&)运算和一个在特定位为0、其余位为1的掩码。

以下是清除第4位的传统方法:

data = data & 0xEF; // 0xEF的二进制是1110 1111

使用移位运算符,我们可以先通过左移生成一个只在目标位为1的掩码,然后对其取反,得到所需的清除掩码。

以下是使用移位运算符的方法:

data = data & ~(1 << 4); // 生成0001 0000后取反,得到1110 1111

公式清除掩码 = ~(1 << n)

方法对比与总结

以下是两种方法的对比:

  • 传统掩码法:直接、快速,但掩码值需要预先计算,在数据位宽很大或位位置变化时不够灵活。
  • 移位运算法:动态生成掩码,代码意图更清晰(“左移4位”明确指出了操作的是第4位),灵活性强,是嵌入式编程中的推荐做法。

本节课中我们一起学习了移位运算符在设置和清除数据特定位时的应用。我们了解到,相比于直接使用硬编码的掩码值,使用 (1 << n) 来动态生成掩码能使代码更易读、更易维护,尤其是在处理复杂或位宽较大的数据时。

在下一节,我们将把学到的知识付诸实践,使用移位运算符来优化之前控制LED的代码。

ARM Cortex (STM32) 基础:构建嵌入式系统 p58 04_01_04:使用位运算与移位操作符修改LED灯运动 🚀

在本节课中,我们将学习如何使用位运算和移位操作符来修改微控制器的寄存器,从而控制LED灯的运动。我们将通过改写之前的代码,用更简洁的位操作替代直接使用掩码值,使代码更易读和维护。


上一节我们介绍了如何通过直接赋值来配置寄存器。本节中,我们来看看如何使用位运算和移位操作符实现同样的功能,这种方法在嵌入式开发中更为常见和高效。

首先,我们需要使能GPIOD端口的时钟。这通过设置RCC->AHB1ENR寄存器的第3位来实现。

以下是修改步骤:

  1. 使能GPIOD时钟:原代码使用了一个掩码值。我们可以使用移位操作来设置特定位。

    // 原代码:RCC->AHB1ENR |= 0x08; // 设置第3位
    // 新代码:使用移位操作
    RCC->AHB1ENR |= (1 << 3); // 将数字1左移3位,等同于设置第3位为1
    
  2. 配置引脚模式:我们需要清除GPIOD->MODER寄存器的第24和25位(用于配置引脚12的模式),然后将第24位设置为1(配置为输出模式)。

    • 清除位:原代码使用&= ~操作和掩码。我们可以组合使用移位和按位取反操作。
      // 原代码:GPIOD->MODER &= ~(0x03000000); // 清除24、25位
      // 新代码:使用移位和取反
      GPIOD->MODER &= ~(3 << 24); // 数字3(二进制11)左移24位,然后取反,将24、25位清零
      
    • 设置位:同样,使用移位操作来设置特定位。
      // 原代码:GPIOD->MODER |= 0x01000000; // 设置第24位为1
      // 新代码:使用移位操作
      GPIOD->MODER |= (1 << 24); // 将数字1左移24位,设置第24位为1
      
  3. 配置输出类型:我们需要设置GPIOD->OTYPER寄存器的第12位为0(推挽输出)。

    // 原代码:GPIOD->OTYPER &= ~(0x1000); // 清除第12位
    // 新代码:使用移位操作
    GPIOD->OTYPER &= ~(1 << 12); // 将数字1左移12位后取反,清除第12位
    

修改完成后,重新编译代码以确保没有错误。如果编译成功,说明我们的位操作代码功能正确。你也可以通过调试器查看寄存器窗口,观察这些位的值是否按预期变化。


本节课中我们一起学习了如何运用位运算与移位操作符来高效地读写微控制器寄存器。相比直接使用十六进制掩码,这种方法逻辑更清晰,更容易理解每一位的作用。我们通过使能时钟、配置引脚模式和输出类型三个步骤实践了这一技巧。

在下一个视频中,我们将探讨另一个重要的主题:位提取操作。敬请期待!


059:位提取

在本节课程中,我们将学习如何从一个数据中提取特定的位段。这个过程被称为位提取,是嵌入式系统编程中处理硬件寄存器数据的常用技术。

概述

位提取是指从一个较大的数据值中,分离出我们感兴趣的连续几位。例如,我们可能需要从一个16位的寄存器值中,提取第9位到第14位(共6位)的数据,并将其存储到一个新的变量中。我们将通过位右移和位与运算来实现这一目标。

问题定义

假设我们有一个16位的变量 data,其二进制格式如下。我们的目标是从中提取位[14:9](即第9位到第14位,包含两端),并将这6位数据保存到另一个变量中。

解决方案步骤

位提取可以通过两个核心步骤完成:右移掩码

第一步:右移

首先,我们需要将目标位段移动到数据的最右侧(最低有效位,LSB)。这样,我们感兴趣的位就占据了从第0位开始的位置。

具体操作是,将原始数据向右移动9位。这样,原来的第9位就移动到了第0位的位置,原来的第14位则移动到了第5位的位置。

公式表示:
shifted_data = data >> 9

第二步:掩码

经过右移后,数据的低6位(第0位到第5位)就是我们想要提取的原始数据的[14:9]位。然而,数据的高位(第6位到第15位)现在包含的是无关信息(可能是0或原始数据的高位)。

为了只保留低6位,我们需要使用一个掩码。掩码是一个二进制数,在我们想要保留的位上为1,在其他位上为0。对于6位数据,掩码是二进制的 0000 0000 0011 1111,即十进制的 0x3F

通过将移位后的数据与这个掩码进行位与运算,我们可以将高位置零,只保留低6位。

公式表示:
extracted_bits = shifted_data & 0x3F

代码示例

以下是使用C语言实现上述位提取过程的示例代码。

#include <stdint.h>
#include <stdio.h>

int main() {
    // 定义一个16位的原始数据
    uint16_t data = 0xB410;

    // 定义一个8位的变量来存放提取结果
    uint8_t output_var;

    // 执行位提取操作:先右移9位,再与掩码0x3F进行与运算
    output_var = (uint8_t)((data >> 9) & 0x3F);

    // 打印提取结果
    printf("提取的值为:%u\n", output_var);

    return 0;
}

代码解析

  1. 变量定义data 是16位的原始数据,output_var 是8位的变量,用于存储提取结果。
  2. 位提取操作
    • (data >> 9):将 data 右移9位,使目标位段[14:9]移动到低6位。
    • & 0x3F:使用掩码 0x3F(二进制 0011 1111)进行位与运算,清除高10位,只保留低6位。
    • (uint8_t):进行类型转换,将结果存入8位变量。
  3. 输出结果:运行上述代码,output_var 的值将是26,这就是从 0xB410 中提取位[14:9]的结果。

总结

在本节课中,我们一起学习了位提取的概念和实现方法。我们了解到,通过结合位右移运算符(>>)位与运算符(&),可以轻松地从数据中分离出任何连续的位段。关键步骤是先将目标位段移动到数据最右侧,然后用一个合适的掩码过滤掉不需要的位。这项技能对于直接操作微控制器中的硬件寄存器位字段至关重要。

构建嵌入式系统:ARM Cortex (STM32) 基础:第04章:第02节:C语言中的循环结构

在本节课中,我们将要学习C语言编程中一个非常重要的概念:循环结构。循环允许我们重复执行一段代码,从而避免编写大量重复的语句,使程序更加简洁高效。我们将通过一个具体的例子来理解循环的必要性,并重点介绍while循环的工作原理。

为什么需要循环?

假设我们需要在程序中打印从1到10的所有数字。一种方法是编写10条独立的打印语句,但这会非常繁琐且低效。

以下是解决此问题的更优方法:使用循环。通过循环,我们只需编写一次打印语句,并让它重复执行10次。

while循环示例

让我们通过一个具体的程序来理解while循环。我们将创建一个项目,使用while循环打印从1到10的数字。

首先,我们初始化一个变量 num,其值为1。我们的目标是当 num 小于或等于10时,重复执行打印和递增操作。

代码示例:

#include <stdio.h>

int main() {
    int num = 1; // 初始化计数器

    while (num <= 10) { // 循环条件:当num小于等于10时执行
        printf("%d\n", num); // 打印当前num的值
        num++; // 将num的值增加1(后置递增)
    }

    return 0;
}

代码解释:

  1. int num = 1;:声明并初始化一个整型变量 num,起始值为1。
  2. while (num <= 10):这是循环的条件。只要 num 的值小于或等于10,花括号 {} 内的代码块就会一直执行。
  3. printf("%d\n", num);:打印变量 num 的当前值。
  4. num++;:这是后置递增操作。它先使用 num 的当前值,然后再将其增加1。因此,第一次循环打印的是1,然后 num 变为2,依此类推。

执行流程:

  • 第一次循环:num 为1,满足 num <= 10 的条件,打印1,然后 num 递增为2。
  • 第二次循环:num 为2,满足条件,打印2,然后 num 递增为3。
  • ...
  • 第十次循环:num 为10,满足条件,打印10,然后 num 递增为11。
  • 第十一次检查:num 为11,不满足 num <= 10 的条件,循环结束。

通过这种方式,我们仅用几行代码就完成了需要十条打印语句才能完成的任务。如果我们需要打印到50,只需将循环条件改为 num <= 50 即可,无需修改循环体内的代码。

C语言中的循环类型

C语言主要提供了三种循环结构,它们功能相似,主要在语法上有所区别:

  1. while循环:本节介绍的类型。先检查条件,条件为真则执行循环体。
  2. for循环:另一种常用的循环,将初始化、条件判断和更新操作集中在一行。
  3. do...while循环:先执行一次循环体,然后再检查条件。保证循环体至少执行一次。

在接下来的课程中,我们将继续学习 for 循环和 do...while 循环。

总结

本节课中我们一起学习了循环结构的基础知识。我们理解了使用循环可以避免代码重复,提高编程效率。我们重点掌握了 while循环 的语法和工作原理:它会在每次迭代前检查一个条件,只要条件为真,就会重复执行其代码块中的语句。通过一个打印数字的示例,我们直观地看到了循环如何简化我们的程序。

061:while循环练习第一部分 🔢

在本节课中,我们将学习如何使用 while 循环来编写一个程序,该程序能够根据用户输入的数字,自动生成并打印出其乘法表。这是一个理解循环控制流和用户交互的绝佳练习。

概述

我们将创建一个简单的C++项目,通过 while 循环结构,实现从用户处获取一个整数,然后打印出该数字从1到12的乘法表。通过这个练习,你将掌握循环的基本应用和如何将用户输入与程序逻辑结合。

项目创建与准备

首先,我们需要创建一个新的C++项目并添加源文件。我们将项目命名为 whileLoopExercise1

#include <stdio.h>
int main() {
    // 程序主体将在这里编写
    return 0;
}

程序逻辑设计

上一节我们创建了项目的基本框架,本节中我们来看看程序的具体逻辑。我们的目标是:

  1. 提示用户输入一个数字。
  2. 使用 while 循环计算并打印该数字的乘法表(例如,从1乘到12)。

以下是实现该逻辑的核心步骤:

  1. 声明变量:我们需要两个整数变量。一个(例如 num)用于存储用户输入的数字,另一个(例如 cnt)作为计数器,从1开始递增。
  2. 获取用户输入:使用 printf 提示用户,并使用 scanf 将输入值存储到 num 变量中。
  3. 打印表头:在开始循环前,先打印一行说明,例如“Multiplication table of [num]:”。
  4. 构建循环:使用 while 循环,条件是计数器 cnt 小于等于12。在循环体内,执行计算和打印。
  5. 循环体内操作:在每次循环中,打印 numcnt 以及它们的乘积 num * cnt。然后,将计数器 cnt 的值增加1(cnt++)。
  6. 循环结束:当 cnt 增加到13时,不满足循环条件(cnt <= 12),循环终止,程序结束。

代码实现

根据上述设计,完整的程序代码如下:

#include <stdio.h>

int main() {
    int num;    // 存储用户输入的数字
    int cnt = 1; // 计数器,从1开始

    // 提示用户输入
    printf("Enter a number for multiplication table: \n");
    scanf("%d", &num);

    // 打印乘法表标题
    printf("Multiplication table of %d:\n", num);

    // while循环开始
    while (cnt <= 12) {
        // 打印每一行: num x cnt = result
        printf("%d * %d = %d\n", num, cnt, num * cnt);
        cnt++; // 计数器递增
    }

    return 0;
}

程序运行示例

假设用户输入数字 57,程序将输出以下内容:

Enter a number for multiplication table:
57
Multiplication table of 57:
57 * 1 = 57
57 * 2 = 114
57 * 3 = 171
...
57 * 12 = 684

扩展与调整

这个程序的灵活性很高。如果你想打印到20而不是12,只需将 while 循环的条件从 cnt <= 12 改为 cnt <= 20 即可。这展示了循环如何让我们用几行代码轻松处理重复性任务,而无需手动编写大量重复的语句。

总结

本节课中我们一起学习了 while 循环的一个实际应用。我们创建了一个程序,它能够接受用户输入,并利用 while 循环自动生成该数字的乘法表。你掌握了如何:

  • 使用 scanf 获取用户输入。
  • 使用 while 循环控制重复执行流程。
  • 在循环体内进行计算和输出。
  • 通过修改循环条件来控制循环次数。

通过这个练习,你应该对循环在简化代码、处理重复任务方面的强大能力有了更直观的理解。

062:while循环练习 第二部分

在本节课程中,我们将学习如何编写一个使用 while 循环的程序。该程序会持续提示用户输入整数,直到用户输入数字0为止,然后计算并输出所有输入的正整数之和。


项目创建与目标

首先,我们需要创建一个新的项目。我们将项目命名为 while_loop_exercise_2

接下来,我们明确程序的目标:编写一个程序,提示用户输入一系列整数,直到用户输入0为止。程序需要使用 while 循环来实现持续输入,并在循环结束后,计算并打印所有已输入的正整数的总和。


程序设计与实现

为了实现上述功能,我们将遵循以下步骤:

  1. 包含必要的头文件。
  2. main 函数中声明变量。
  3. 向用户显示操作说明。
  4. 使用 while 循环持续接收用户输入。
  5. 在循环内部判断输入值,以决定是结束循环还是累加正数。
  6. 循环结束后,输出正整数的累加和。

以下是具体的代码实现步骤。

首先,我们需要包含标准输入输出头文件,并设置 main 函数。

#include <stdio.h>

int main() {
    // 变量声明与初始化
    int number;
    int sum = 0;

    // 程序逻辑将在这里编写

    return 0;
}

main 函数内部,我们首先声明两个变量:number 用于存储用户的每次输入,sum 用于累加正整数的和,并将其初始化为0。


用户提示与循环逻辑

在程序开始执行时,我们需要告知用户如何操作。我们将打印一条说明信息。

    printf("请输入一系列整数,输入0以停止。\n");

接下来,我们使用一个 while(1) 循环来创建一个无限循环。循环的退出条件将在循环体内部进行判断。

    while(1) {
        // 循环体内的代码将不断执行
    }

在循环体内,我们首先提示用户输入一个数字,并使用 scanf 函数读取这个值。

        printf("请输入一个数字: ");
        scanf("%d", &number);

读取用户输入后,我们需要进行判断。以下是循环内部的核心判断逻辑:

  • 如果用户输入的数字是0,则使用 break 语句跳出循环。
  • 如果用户输入的数字大于0,则将其累加到 sum 变量中。
        if (number == 0) {
            break; // 输入0,跳出循环
        }

        if (number > 0) {
            sum += number; // 等价于 sum = sum + number;
        }

输出结果与屏幕停留

当用户输入0,break 语句执行后,程序将跳出 while 循环。此时,所有正整数的和已经存储在 sum 变量中。我们将其打印出来。

    printf("所有正整数的和为: %d\n", sum);

为了让运行窗口在显示结果后不会立即关闭(特别是在某些集成开发环境中),我们可以在 return 0; 语句前添加一个 getchar() 函数调用,等待用户按下一个键。

    getchar(); // 等待用户按键,保持窗口
    return 0;


完整代码与执行示例

将以上所有部分组合起来,就得到了完整的程序代码。

#include <stdio.h>

int main() {
    int number;
    int sum = 0;

    printf("请输入一系列整数,输入0以停止。\n");

    while(1) {
        printf("请输入一个数字: ");
        scanf("%d", &number);

        if (number == 0) {
            break;
        }

        if (number > 0) {
            sum += number;
        }
    }

    printf("所有正整数的和为: %d\n", sum);

    getchar(); // 保持窗口
    return 0;
}

编译并运行此程序,你将看到类似以下的交互过程:

请输入一系列整数,输入0以停止。
请输入一个数字: 5
请输入一个数字: -3
请输入一个数字: 10
请输入一个数字: 0
所有正整数的和为: 15

程序会持续接收输入,忽略负数(如-3),只累加正数(5和10)。当输入0时,循环终止,并输出累加结果15。


总结

在本节课中,我们一起学习了 while 循环的一个实际应用。我们创建了一个程序,它能够:

  1. 使用 while(1) 构建一个无限循环。
  2. 在循环内通过 scanf 获取用户输入。
  3. 使用 if 条件判断和 break 语句来控制循环的退出条件。
  4. 使用 += 运算符累加符合条件的数值。
  5. 在循环结束后输出最终的计算结果。

这个练习巩固了循环控制、条件判断和用户交互输入的综合运用,是嵌入式系统编程中处理连续事件或数据的常见模式基础。

063:do-while 循环 🔄

在本节课中,我们将学习 do-while 循环。我们将通过一个具体的编程练习来理解其语法和工作原理,即编写一个程序,持续接收用户输入的数字,直到输入一个负数为止,然后计算并打印所有输入数字的总和。


上一节我们介绍了 while 循环,本节中我们来看看 do-while 循环。这两种循环的主要区别在于条件检查的时机。do-while 循环会先执行一次循环体,然后再检查条件是否满足。

以下是 do-while 循环的基本语法结构:

do {
    // 循环体语句
} while (条件);

现在,让我们开始动手实践。我们将创建一个名为 DoWhileLoopExercise1 的新项目,并添加一个源文件。

首先,我们需要声明变量。我们将需要一个变量来存储用户输入的数字,以及一个变量来存储总和。

int num;
int sum = 0;

接下来,我们编写 do-while 循环。循环的逻辑是:先提示用户输入一个数字,然后读取这个数字。如果这个数字是非负数(大于或等于0),就将其加到总和上。这个过程会一直重复,直到用户输入一个负数为止。

以下是循环部分的代码:

do {
    printf("输入一个数字(输入负数以停止): ");
    scanf("%d", &num);
    if (num >= 0) {
        sum += num; // 等价于 sum = sum + num
    }
} while (num >= 0);

循环结束后,我们需要打印出计算得到的总和。

printf("所有输入数字的总和是: %d\n", sum);

最后,非常重要的一点是,不要忘记在 main 函数中调用我们编写的功能函数。一个常见的错误是编写了函数却忘记调用它。

让我们保存代码,构建项目,并运行它来测试功能。

程序运行后,我们可以输入一系列数字进行测试。例如,输入 102030,最后输入 -1。程序应该会计算出总和 60 并正确输出。

通过这个练习,我们清晰地看到了 do-while 循环的执行流程:它至少会执行一次循环体,然后再判断条件以决定是否继续循环。这与先判断条件再决定是否执行循环体的 while 循环形成了对比。


本节课中我们一起学习了 do-while 循环的语法和应用。我们通过一个计算数字总和的练习,实践了如何用 do-while 循环处理需要至少执行一次的任务。记住其核心特点是先执行,后判断

064:04_03_01_for循环练习 第1部分

在本节课中,我们将通过一个编程练习来巩固对 for 循环的理解。我们将编写一个程序,根据用户输入的数字,生成并打印从1到该数字的乘法表,每个数字的乘法表将计算到12倍。

项目创建与初始化

首先,我们需要创建一个新的项目。我们将项目命名为 forLoopExercise1

接下来,在项目中添加一个新的源文件。

同样地,我们还需要配置项目设置,确保编译环境正确。

我们将编写一个主函数,并在其中调用我们的乘法表生成方法,以避免忘记执行。主函数最终返回0。

理解练习要求

现在,让我们明确本次练习的具体要求。

我们需要编写一个使用 for 循环的程序。该程序将打印一个乘法表,范围从1开始,直到用户输入的数字为止。每个数字的乘法表将计算到12倍。

例如,如果用户输入数字50,程序将生成并打印从1到50的乘法表,每个表都包含从1乘到12的结果。

程序设计与变量声明

为了实现这个功能,我们首先需要声明几个变量。

我们将创建一个变量来接收用户输入的数字。此外,我们还需要两个循环变量。

以下是变量声明的示例代码:

int num, var1, var2;

首先,我们将提示用户输入一个数字,作为乘法表的结束值。

例如,我们可以提示:“请输入乘法表的结束数字(从1开始):”。

然后,使用 scanf 语句将用户输入的值存储到变量 num 中。

接着,我们可以打印一条消息,告知用户将生成从1到 num 的乘法表。

使用嵌套循环生成乘法表

为了打印出矩阵形式的乘法表,我们需要使用嵌套的 for 循环。

第一个循环(外层循环)控制生成哪个数字的乘法表。由于表格需要从1开始,我们将循环变量 var1 初始化为1。循环条件是 var1 小于等于用户输入的数字 num。每次循环后,var1 递增。

但是请注意,根据要求,每个数字的乘法表需要计算到12倍。因此,在外层循环内部,我们还需要一个内层循环来计算每个 var1 的1到12倍。

内层循环的变量 var2 也从1开始,循环条件是 var2 小于等于12。每次循环后,var2 递增。

在内层循环的循环体内,我们可以计算并打印 var1 * var2 的结果。

预期输出格式

为了更直观地理解输出,可以想象一个Excel表格。

第一列是基数(从1到用户输入的数字,例如10)。第一行是乘数(从1到12)。表格中的每个单元格是对应基数与乘数的乘积。

程序的目标就是以清晰的格式打印出这样一个“矩阵”。


本节课中,我们一起学习了如何设计一个使用嵌套 for 循环的程序来生成乘法表。我们讨论了项目初始化、理解需求、变量声明以及嵌套循环的逻辑结构。在下一部分,我们将具体实现代码并查看运行结果。

065:for循环练习第二部分

概述

在本节课程中,我们将继续学习for循环的实践应用。我们将通过编写一个程序来生成乘法表,并探讨如何格式化输出,使其在终端中整齐地显示。我们将使用C语言在嵌入式开发环境中实现这一功能。


代码实现与解析

上一节我们介绍了for循环的基本结构。本节中,我们来看看如何利用嵌套的for循环来打印一个乘法表。

以下是实现乘法表的核心代码逻辑:

for (int var1 = 1; var1 <= limit; var1++) {
    for (int var2 = 1; var2 <= 12; var2++) {
        printf("%d x %d = %d\t", var1, var2, var1 * var2);
    }
    printf("\n");
}
  • 外层循环 (var1):控制乘法表的行数,即从1乘到用户输入的limit值。
  • 内层循环 (var2):控制每一行中的列数,这里固定为从1乘到12。
  • printf函数:用于格式化输出。%d是整数占位符,\t是制表符,用于对齐各列,\n用于在每行结束后换行。

输出格式化技巧

直接使用空格进行输出,在数字位数变化时容易导致列对齐混乱。为了使表格整齐,我们使用制表符\t来代替多个空格。

以下是两种输出方式的对比说明:

  • 使用空格:当乘积结果的位数不同时,列无法对齐。
  • 使用制表符 (\t):能自动适应内容长度,保持各列基本对齐,使表格更美观。

你可以尝试将代码中的\t替换为多个空格,然后输入一个较大的数字(例如19),观察输出格式的变化,以理解制表符的作用。

程序运行示例

当我们运行程序并输入数字9时,终端将显示从1到9的乘法表。每一行显示一个数字(1到9)与1到12的乘积结果。

例如,第一行是 1 x 1 = 1 1 x 2 = 2 ... 1 x 12 = 12,然后是2的乘法行,依此类推,直到9。

总结

本节课中我们一起学习了for循环的一个经典应用案例——生成乘法表。我们实践了嵌套循环的使用,并掌握了利用制表符\t来格式化输出、提升显示效果的重要技巧。这个练习综合运用了for循环的初始化、条件检查和变量更新三个部分。

在下一个视频中,我们将探索另一个for循环的练习。

066:for循环练习2 ⭐

在本节课中,我们将学习如何使用C++编写一个程序,通过嵌套的for循环来打印一个由星号(*)组成的直角三角形图案。用户将能够输入图案的行数,程序会根据输入动态生成相应大小的图案。

上一节我们介绍了基本的for循环结构,本节中我们来看看如何利用嵌套循环来生成更复杂的输出模式。

项目创建与设置

首先,我们需要创建一个新的C++项目。项目名称为“for loop and score Exercise 2”。创建完成后,添加一个源文件,并将基础代码框架粘贴进去。

以下是项目的基础代码结构:

#include <iostream>
using namespace std;

int main() {
    // 程序主体将在这里编写
    return 0;
}

程序逻辑设计

本程序的目标是打印一个直角三角形星号图案。程序的核心逻辑包含两个主要部分:获取用户输入和使用嵌套循环进行打印。

1. 获取用户输入

程序首先需要提示用户输入希望打印的星号行数,并将这个值存储在一个变量中。

以下是实现此功能的代码:

int rows;
cout << "Input Number of rows: ";
cin >> rows;

2. 使用嵌套循环打印图案

接下来,我们使用嵌套的for循环来生成图案。外层循环控制行数,内层循环控制每行打印的星号数量。

以下是嵌套循环的代码结构:

for (int row1 = 1; row1 <= rows; row1++) {
    for (int row2 = 1; row2 <= row1; row2++) {
        cout << "*";
    }
    cout << endl;
}

代码解释:

  • 外层循环 (for (int row1 = 1; row1 <= rows; row1++)): 变量 row1 从1开始,一直递增到用户输入的 rows 值。每次迭代代表处理新的一行。
  • 内层循环 (for (int row2 = 1; row2 <= row1; row2++)): 变量 row2 从1开始,一直递增到当前 row1 的值。这意味着第1行打印1个星号,第2行打印2个星号,依此类推。内层循环的每次迭代打印一个星号 (cout << "*")。
  • 换行 (cout << endl): 在内层循环结束后,使用 cout << endl 输出一个换行符,将光标移动到下一行,以便开始打印新的一行。这一步至关重要,它确保了星号按行排列,而不是全部打印在同一行。

完整代码示例

将以上两部分组合起来,得到完整的程序代码如下:

#include <iostream>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-embsys/img/04060b051964cbfce62c71011e24b36b_1.png)

int main() {
    int rows;
    cout << "Input Number of rows: ";
    cin >> rows;

    for (int row1 = 1; row1 <= rows; row1++) {
        for (int row2 = 1; row2 <= row1; row2++) {
            cout << "*";
        }
        cout << endl;
    }
    return 0;
}

程序运行与验证

构建并运行程序。当程序提示时,输入一个数字,例如 5。程序将输出一个5行的直角三角形星号图案。

预期输出:

*
**
***
****
*****

如果输入 14,则会生成一个14行的星号图案。

关键点验证:换行符的作用

为了理解 cout << endl 的重要性,可以尝试将其注释掉,然后重新编译运行程序。

// cout << endl;

再次输入 5,观察输出。

注释掉换行后的输出:

***************

可以看到,所有的星号都打印在了同一行。这证明了 cout << endl 语句的作用是结束当前行,使后续输出从新的一行开始。外层循环控制行(row),内层循环控制列(column),换行操作是区分不同行的关键。

总结

本节课中我们一起学习了如何编写一个C++程序,利用嵌套的for循环根据用户输入打印直角三角形星号图案。我们掌握了以下核心要点:

  1. 使用 cin 获取用户输入,并用变量存储。
  2. 使用嵌套的 for 循环,其中外层循环控制总行数,内层循环控制每行的星号数量。
  3. 理解并正确使用 cout << endl 在每行结束后进行换行,这是形成规整图案的必要步骤。

通过这个练习,你加深了对循环控制流,特别是嵌套循环逻辑的理解,这是进行复杂控制台输出和未来嵌入式编程中处理重复任务的基础。

构建嵌入式系统:04_03_04:通过软件延时实现LED切换 🔄

在本节课程中,我们将学习如何修改一个简单的LED点亮程序,使其变为LED切换(闪烁)程序。核心方法是引入一个软件延时,在LED点亮和熄灭之间制造一个可观察的时间间隔。

上一节我们介绍了如何点亮一个LED。本节中,我们来看看如何让LED自动地、周期性地在亮与灭之间切换。

为了实现LED切换,我们需要在控制LED状态变化的指令之间插入一段延时。延时可以通过软件或硬件方式实现。软件延时是让处理器在一个循环中“空转”以消耗时间;硬件延时则利用定时器等外设来产生精确的时间间隔。对于本练习,我们将使用软件延时方法。

以下是软件延时的核心概念:通过编写一个不做任何实质性工作的循环,让处理器保持忙碌,从而消耗掉特定的时钟周期,达到延时的目的。这种方法虽然不精确,但对于创建人眼可观察的LED闪烁效果已经足够。

代码示例:一个简单的延时循环

for(int i = 0; i < DELAY_VALUE; i++) {
    // 空循环,消耗时间
}

我们将创建一个新的项目,并基于之前的LED点亮程序进行修改。主要修改步骤是在点亮LED和熄灭LED的指令之间,各插入一段软件延时。循环结构可以使用 while 循环或 for 循环来实现。

以下是实现LED切换程序的基本步骤列表:

  1. 初始化LED引脚:将控制LED的GPIO引脚配置为输出模式。
  2. 进入主循环:使用一个无限循环(如 while(1))来持续运行切换逻辑。
  3. 点亮LED并延时:将LED引脚置为高电平(或低电平,取决于电路设计)以点亮LED,然后调用软件延时函数。
  4. 熄灭LED并延时:将LED引脚置为相反电平以熄灭LED,然后再次调用相同的软件延时函数。
  5. 重复循环:程序将不断重复步骤3和4,从而实现LED的持续闪烁。

请注意,软件延时的时间长度取决于 DELAY_VALUE 的大小和处理器的主频。你需要通过实验调整这个值,以获得满意的闪烁频率。

本节课中我们一起学习了如何利用软件延时,将一个静态的LED点亮程序改造为动态的LED切换(闪烁)程序。我们理解了软件延时的基本原理——通过空循环消耗CPU周期,并掌握了将其嵌入到主程序循环中的方法。在下一节中,我将展示这个练习的具体代码解决方案。

068:软件延迟控制的LED切换 第1部分 🚦💡

在本节课中,我们将学习如何在STM32微控制器上,通过编写软件延迟循环,实现LED的周期性闪烁。我们将创建一个新项目,配置GPIO引脚,并使用简单的for循环来产生延迟,从而控制LED的亮灭节奏。


创建STM32项目

首先,我们需要在开发环境中创建一个新的STM32项目。

  1. 启动STM32CubeIDE或您使用的开发环境。
  2. 选择“创建新项目”。
  3. 在项目配置向导中,根据您使用的具体STM32开发板型号进行选择。
  4. 将项目命名为 LED_toggle
  5. 完成项目创建向导,这可能需要一些时间。

项目创建完成后,我们将进入主代码编辑界面。


配置项目属性与复制基础代码

为了确保项目正确编译并避免常见错误(如FPU相关错误),我们需要检查项目属性。

上一节我们创建了项目,本节中我们来看看如何配置并准备基础代码。

  1. 在项目资源管理器中,右键点击项目名称,选择“属性”。
  2. 在属性窗口中,确保编译器和链接器设置正确,特别是与硬件浮点单元相关的选项。
  3. 应用更改并关闭属性窗口。

接下来,为了节省时间并减少错误,我们将从一个已有的、功能正确的基础项目中复制代码框架。请将以下GPIO初始化的核心代码复制到您项目的主源文件(通常是main.c)中。

// 初始化GPIO引脚(例如,将PA5配置为输出模式以驱动LED)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

实现软件延迟与LED控制逻辑

现在,我们有了一个可以控制LED的GPIO引脚。接下来,我们将编写主循环代码,实现LED的闪烁。关键在于使用一个for循环来产生人为可观察的延迟。

以下是实现无限循环中LED切换的步骤:

  1. 开启LED:首先,我们设置引脚输出高电平来点亮LED。

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 点亮LED
    
  2. 引入延迟:随后,我们通过一个执行空操作的for循环来让处理器“等待”一段时间,从而产生延迟。

    for(uint32_t i = 0; i < 10000; i++); // 软件延迟循环
    

    注意:循环次数10000是一个初始值,实际所需的延迟时间需要通过试验来调整。

  3. 关闭LED:延迟结束后,我们将引脚输出设置为低电平来关闭LED。

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 关闭LED
    
  4. 再次延迟并形成循环:在关闭LED后,再次使用相同的延迟循环,然后将所有这些操作放入一个while(1)无限循环中,使LED能够持续闪烁。

    while (1)
    {
        // 点亮LED -> 延迟 -> 关闭LED -> 延迟
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
        for(uint32_t i = 0; i < 10000; i++);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
        for(uint32_t i = 0; i < 10000; i++);
    }
    

编译、下载与调试

代码编写完成后,即可进行编译。

  1. 点击IDE中的“构建”或“编译”按钮。如果代码无误,控制台将显示“构建成功”。
  2. 使用ST-Link或其他调试器将编译好的程序下载到STM32开发板。
  3. 为了深入理解程序执行,可以进入调试模式。在调试视图中,您可以单步执行代码,并在反汇编窗口观察每条C语句对应的机器指令是如何被执行的。

本节课中我们一起学习了如何通过纯软件方式控制硬件。我们创建了一个STM32项目,配置了GPIO输出,并利用for循环制造延迟,最终实现了一个LED闪烁的应用程序。这是理解嵌入式系统中时序控制的基础。在下一部分,我们将探索如何优化延迟精度以及使用硬件定时器来替代软件循环。

069:软件延时控制的LED开关 第二部分 🔄

在本节中,我们将继续学习如何使用软件延时来控制LED的开关。我们将通过调试器观察延时循环的执行,并学习如何调整延时参数来改变LED的闪烁频率。

上一节我们介绍了如何编写一个简单的延时函数来控制LED。本节中,我们来看看如何在调试环境中观察和分析这个延时循环的实际执行过程。

观察延时循环 🔍

以下是使用调试器(Disassembler)窗口观察程序执行流程的步骤。

  1. 启动调试会话,进入主循环。
  2. 在反汇编窗口中,使用“单步跳过”(Step Over)功能执行程序。
  3. 观察程序计数器在延时循环代码段中的停留,以确认延时正在发生。

调整延时参数 ⚙️

初始设置的循环次数较少,难以观察到明显的延时效果。为了更清晰地观察,我们需要增加循环次数。

  • 修改代码:将延时循环中的计数值从较小的数字(例如1000)增加到更大的数字(例如30000)。
  • 核心代码
    for(int i = 0; i < 30000; i++); // 软件延时循环
    
  • 重新编译:保存修改后的代码,并重新构建(Build)整个项目。

验证延时效果 ✅

重新启动调试过程,并再次观察反汇编窗口。

  1. 运行程序至主循环。
  2. 使用“单步跳过”进入延时循环部分。
  3. 此时可以清晰地看到,程序计数器会在循环对应的汇编指令处停留较长时间,这表明延时正在按预期工作。
  4. 通过增加循环次数,延时效果变得非常容易识别。程序会消耗若干个时钟周期来完成循环,然后才继续执行后续的LED切换操作。

通过这种方式,我们完成了LED闪烁的练习,并学会了如何利用简单的软件循环实现延时,以及如何在调试环境中验证其执行。

本节课中我们一起学习了如何调整和验证软件延时函数。通过增加循环次数并在调试器中观察执行流程,我们确认了延时机制的有效性,从而能够更精确地控制LED的闪烁行为。

构建嵌入式系统:课程总结:回顾与展望

在本节课中,我们将对《构建嵌入式系统:ARM Cortex (STM32) 基础》这门课程进行全面的回顾与总结,梳理所学核心知识,并展望未来的学习方向。


回顾整个课程,你已深入掌握了使用C语言进行嵌入式系统开发的基础。从基本数据类型到高级硬件控制,你构建了坚实的编程知识体系。

以下是你在本课程中掌握的核心技能概览:

  • 基础概念:理解了C语言的基本数据类型、变量和用户输入处理。
  • 流程控制:掌握了使用条件语句(如if-else)进行决策,以及利用循环结构(如forwhile)重复执行代码。
  • 高效运算:学会了运用位运算符等工具进行高效的数据操作与代码执行。
  • 内存管理:深入理解了指针的机制,获得了内存管理的关键洞察。
  • 硬件交互:磨练了通过嵌入式C编程直接控制硬件外设的技能。

上一节我们回顾了所学的主要技能模块,本节中我们来看看这些知识如何构成你未来发展的基石。

你所构建的C语言编程基础,为多个领域的发展铺平了道路。无论你未来致力于软件开发、嵌入式系统工程,还是任何需要编程技能的领域,本课程所传授的知识都将成为你坚实的起点。


本节课中我们一起学习了C语言嵌入式编程的核心知识体系。你的旅程并未结束,这只是一个坚实的开始。展望未来,请持续练习、不断尝试、勇于探索知识的边界。

感谢你参与这段学习旅程,我们祝愿你在未来的探索中取得最佳成就。

本课程到此全部结束,衷心感谢你的学习。

posted @ 2026-03-29 09:13  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报