文章目录
前言
本文的初衷是总结一些在学习STM32编程中的问题和疑惑。
一、函数
1. fputc() 函数
fputc是C语言中的一个标准库函数,用于将一个字符写入到指定的文件流中。这个函数通常用于写入文本文件或者向设备发送数据。
fputc函数的原型如下:
int fputc(int char, FILE *stream);
- 这个函数接受
两个参数
:
int char:这是你想要写入的字符。虽然参数名为char,但实际上它是一个int类型,这是因为C语言没有真正的字符类型,所有的字符都是通过整数表示的。
FILE *streamr:这是一个指向FILE结构的指针,用于指定要写入的文件流。- 如果函数成功,它会返回写入的字符。如果发生错误,它会返回EOF(一个特殊的值,表示文件结束或者错误)。
例如,下面的代码将字符’a’写入到标准输出(屏幕):fputc(‘a’, stdout);
在STM32嵌入式系统中,常规的输入/输出函数(如printf,scanf等)可能无法直接使用,或者效率不高,为了使这些函数能够在STM32的环境中使用,我们通常需要重写(或者说,重定义)它们。例如,将字符发送到USART设备。
fputc函数的重定义
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
- int ch:这是你想要发送的字符。它被定义为整数,因为C语言没有真正的字符类型,所有的字符都是通过整数表示的。
- FILE *f:这是一个指向FILE结构的指针,通常用于指定要写入的文件。在这个重定义的版本中,这个参数并没有被使用,因为正在发送数据到USART,而不是写入文件。
- while((USART1->SR&0X40)==0):这是一个等待循环,它会一直运行,直到USART1的SR寄存器的第6位(0x40:0100 0000)被设置。
SR寄存器是串口的状态寄存器,用于表示USART的各种状态。SR寄存器的第6位,即
TC(Transmission Complete)标志
。这个标志用于表示上一次的数据传输是否已经完成。如果TC标志被设置(值为1),那么这表示上一次的数据传输已经完成,USART设备已经准备好发送新的数据。如果TC标志没有被设置(值为0),那么这表示上一次的数据传输还未完成,USART设备还不能发送新的数据。while((USART1->SR&0X40)==0)循环就是在等待上一次的数据传输完成。只有当TC标志被设置时,循环才会结束,新的数据才会被发送。
- USART1->DR = (u8) ch:这行代码将字符ch发送到USART设备。字符首先被转换为u8(无符号8位整数),然后被写入DR(数据寄存器)。
- return ch:这个函数返回发送的字符。这是fputc函数的标准行为,如果成功,它应该返回发送的字符,如果失败,它应该返回一个错误指示符(通常是EOF)。
2. sscanf() 函数
sscanf()函数是C语言中用于从字符串读取输入的函数。它的名字代表"string scanf",即"从字符串读取"。这个函数的原型如下:
int sscanf(const char *str, const char *format, ...);
这个函数接受一个字符串str作为输入,并尝试根据提供的格式字符串format来解析这个字符串。格式字符串可以包含一些格式说明符,例如%d(用于整数)、%f(用于浮点数)、%s(用于字符串)等。函数的返回值是成功匹配和赋值的项目数。
例1:从一个字符串中读取一个整数和一个浮点数:
char str[] = "123 456.789";
int i;
float f;
sscanf(str, "%d %f", &i, &f);
在这个例子中,sscanf()函数会将str中的"123"解析为一个整数,并赋值给i,然后将"456.789"解析为一个浮点数,并赋值给f。
例2:使用sscanf()函数从字符串temp中解析格式为+EVENT:MQTT_SUB,<topic>,<payload size>,<payload>的字符串,字符串分别被存储在recvMsg.topic, &size和recvMsg.payLoad中。
sscanf(temp, "+EVENT:MQTT_SUB,%49[^,],%d,%99[^\n]", recvMsg.topic, &size, recvMsg.payLoad);
- %49[^,]表示读取最多49个字符,直到遇到逗号(,)为止。读取的字符串被存储在>- recvMsg.topic中,表示MQTT主题。
- %d表示读取一个整数,存储在size变量中,表示负载的大小。
- %99[^\n]表示读取最多99个字符,直到遇到换行符(\n)为止。读取的字符串被存储在recvMsg.payLoad中,表示MQTT消息的实际负载。
例3:使用sscanf()函数从字符串temp1中解析出两个以逗号分隔的字符串。这两个字符串分别被存储在recvwifiInfo.topic和recvwifiInfo.payLoad中。
sscanf(temp1, "+WJAP:3,%31[^,],%31[^,]", recvwifiInfo.topic, recvwifiInfo.payLoad);
函数期望temp1的格式为+WJAP:3,<string1>,<string2>
。
%31[^,]表示读取最多31个字符,直到遇到逗号(,)为止。第一个读取的字符串被存储在recvwifiInfo.topic中。第二个%31[^,]同样表示读取最多31个字符,直到遇到逗号(,)为止。第二个读取的字符串被存储在recvwifiInfo.payLoad中。
注意
,sscanf()函数不会在读取的字符串末尾自动添加空字符(\0),因此你需要确保recvwifiInfo.topic和recvwifiInfo.payLoad在使用前已经被初始化为零,或者在sscanf()函数调用后手动添加空字符。
二、如何给寄存器某个位赋值
举个例子:uint16_t temp=0;给位6赋值为1
方法一:
temp &= 0xFFBF;
第一步先将变量temp的位6清零:
16位数据二进制表示: 1111 1111 1111 1111
16位数据十六进制表示: 0xFFFF
0xFFBF二进制表示为 1111 1111 1011 1111
所以根据按位与的规则,可以保留temp其他位的值不变,清除位6的值
temp |= 0x0040;
第二步将变量temp的位6置1:
0x0040二进制表示为 0000 0000 0100 0000
所以根据按位或的规则,可以不改变temp其他位的值,将位6置1
总结下来就是先将位X清零,再置1。
方法二:
temp &= ~(1<<6);
第一步先将变量temp的位6清零:
1<<6就相当于 100 0000
再根据数据对齐原则 0000 0000 0100 0000再取反就是 1111 1111 1011 1111 (本质也是0xFFBF的另一种表达,但相比前面少了二进制的计算,所以更方便易用)
根据按位与的规则,可以保留temp其他位的值不变,清除位6的值
temp |= 1<<6;
第二步将变量temp的位6置1:
1<<6就相当于 100 0000
再根据数据对齐原则 0000 0000 0100 0000所以根据按位或的规则,可以不改变temp其他位的值,将位6置1
总结下来就是方法一的另一种简便的表达方式,但由于不用频繁的进行二进制计算,更加方便。
口诀:与等非移为清零
或等左移为置一
扩展:如何同时对位6位7置1:
temp &= ~(3<<6);
temp |= 3<<6;
3<<6就相当于 1100 0000
再根据数据对齐原则 0000 0000 1100 0000
三、按位异或控制某个位翻转
异或运算符"∧"
也称XOR运算符。它的规则是若参加运算的两个二进位同号,则结果为0(假);异号则为1(真)。即 0∧0=0,0∧1=1, 1^0=1,1∧1=0。
方法:
temp ^= 1<<6
1<<6就相当于 100 0000
再根据数据对齐原则 0000 0000 0100 0000将temp与0000 0000 0100 0000进行异或时,位6的值就会在置1和清零来回变化,实现翻转。而其他位与0异或的结果还是本身。
三、STM32头文件含义
1 #include <stdlib.h>
#include <stdlib.h>
是C和C++编程语言中的一个预处理指令,用于包含(或者说引入)stdlib.h头文件。这个头文件是标准库的一部分,它包含了一些常用的函数和宏,例如内存分配函数(如malloc和free),数学函数(如abs和div),环境操作函数(如system和getenv),以及一些其他的实用功能。
下面是一些stdlib.h头文件中常用的函数:
void* malloc(size_t size)
: 分配给定大小的内存,并返回指向该内存的指针。void free(void* ptr)
: 释放之前由malloc分配的内存。int abs(int n)
: 返回给定整数的绝对值。div_t div(int numer, int denom)
: 对两个整数进行除法,并返回商和余数。int system(const char* command)
: 执行一个系统命令。char* getenv(const char* name)
: 获取环境变量的值。
所以,如果你的程序中需要使用这些函数或宏,你就需要在程序的开始处包含stdlib.h头文件。例如:
#include <stdlib.h>
int main() {
char* buffer = (char*)malloc(100); // 分配100字节的内存
free(buffer); // 释放内存
return 0;
}
需要注意
的是,虽然#include "stdlib.h"
和include <stdlib.h>
在大多数情况下都可以正常工作,但它们的含义略有不同。使用双引号(" ")的形式通常会先在当前目录中
查找头文件,而使用尖括号(< >)的形式则会在标准库目录中
查找头文件。因此,如果你要包含的是标准库的头文件,通常推荐使用尖括号的形式,即#include <stdlib.h>
。
1 #include <stdio.h>
  #include <stdio.h>
是C和C++编程语言中的一个预处理指令,用于包含(或者说引入)stdio.h头文件。这个头文件是标准库的一部分,它包含了一些用于输入和输出的函数,例如printf, scanf, puts, gets, fopen, fclose等。
下面是一些stdio.h头文件中常用的函数:
int printf(const char* format, ...)
: 打印格式化的输出到标准输出(通常是屏幕)。int scanf(const char* format, ...)
: 从标准输入(通常是键盘)读取格式化的输入。FILE* fopen(const char* filename, const char* mode)
: 打开一个文件,并返回一个文件指针。int fclose(FILE* stream)
: 关闭一个文件。
所以,如果你的程序中需要进行输入/输出操作,或者需要操作文件,你就需要在程序的开始处包含stdio.h头文件。例如:
#include <stdio.h>
int main() {
printf("Hello, world!\n"); // 打印"Hello, world!"到屏幕
return 0;
}
需要注意
的是,虽然#include "stdio.h"
和#include <stdio.h>
在大多数情况下都可以正常工作,但它们的含义略有不同。使用双引号(" ")的形式通常会先在当前目录
中查找头文件,而使用尖括号(< >)的形式则会在标准库目录
中查找头文件。因此,如果你要包含的是标准库的头文件,通常推荐使用尖括号的形式,即#include <stdio.h>
。
1 #include<string.h>
#include <string.h>
是C和C++编程语言中的一个预处理指令,用于包含(或者说引入)string.h头文件。这个头文件是标准库的一部分,它包含了一些常用的字符串操作函数,例如strcpy, strcat, strlen, strcmp等。
下面是一些string.h头文件中常用的函数:
char* strcpy(char* dest, const char* src)
: 将src字符串复制到dest。char* strcat(char* dest, const char* src)
: 将src字符串追加到dest字符串的末尾。size_t strlen(const char* str)
: 返回str字符串的长度。int strcmp(const char* str1, const char* str2)
: 比较str1和str2两个字符串。如果相等,返回0;如果str1小于str2,返回负数;如果str1大于str2,返回正数。
所以,如果你的程序中需要进行字符串操作,你就需要在程序的开始处包含string.h头文件。例如:
#include "string.h"
int main() {
char str[20] = "Hello";
strcat(str, ", world!"); // 将", world!"追加到str的末尾
printf("%s\n", str); // 打印"Hello, world!"
return 0;
}
需要注意
的是,虽然#include "string.h"
和#include <string.h>
在大多数情况下都可以正常工作,但它们的含义略有不同。使用双引号(" ")的形式通常会先在当前目录
中查找头文件,而使用尖括号(< >)的形式则会在标准库目录
中查找头文件。因此,如果你要包含的是标准库的头文件,通常推荐使用尖括号的形式,即#include <string.h>
。
1 #include<stdarg.h>
#include <stdarg.h>
是C和C++编程语言中的一个预处理指令,用于包含(或者说引入)stdarg.h头文件。这个头文件是标准库的一部分,它定义了一套宏,这些宏用于处理函数的可变参数列表。可变参数列表使得函数可以接受任意数量和类型的参数。
下面是一些stdarg.h头文件中定义的常用宏:
va_start(va_list ap, last_arg)
: 初始化ap参数指针,使其指向最后一个固定参数last_arg之后的第一个参数。va_arg(va_list ap, type)
: 返回当前的参数,并将ap指针指向下一个参数。type是当前参数的类型。va_end(va_list ap)
: 清理ap参数指针。
例如,下面的代码定义了一个接受可变参数的函数,并将所有的参数相加:
#include "stdarg.h"
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(ap, int);
}
va_end(ap);
return total;
}
int main() {
printf("%d\n", sum(3, 1, 2, 3)); // 打印6
return 0;
}
需要注意
的是,虽然#include "stdarg.h"
和#include <stdarg.h>
在大多数情况下都可以正常工作,但它们的含义略有不同。使用双引号(" ")的形式通常会先在当前目录
中查找头文件,而使用尖括号(< >)的形式则会在标准库目录
中查找头文件。因此,如果你要包含的是标准库的头文件,通常推荐使用尖括号的形式,即#include <stdarg.h>
。
四、C/C++语言(八股文)
1 C语言中变量的定义
在C语言中,一个完整的变量定义可能包括以下部分:
存储类型 特征修饰 数据类型 变量名
eg:
static volatile int value;
- 存储类型 决定变量的存储位置
- 特征修饰 决定变量的特征属性
- 数据类型 决定变量的存储空间及数据范围
- 变量名字 决定变量的引用标识
存储类型:C语言变量的存储类型一共有四种
- ①
auto
这是所有局部变量的默认存储类型。自动变量只在其定义所在的函数或代码块中存在,一旦离开这个范围,这个变量就会被销毁,它的值也会丢失。自动变量在每次函数或代码块被调用时都会被重新创建。生命周期随着该变量所在函数结束而结束。
例:
int a;
等价于
auto int a;
-
②
static
生命周期:静态变量在整个程序执行期间都存在,即使它们的定义所在的函数或代码块执行完毕,它们仍然存在。静态变量在程序执行过程中只被初始化一次。静态局部变量的作用域仅限于它们被定义的函数或代码块,静态全局变量的作用域仅限于定义它们的文件。 -
③
extern
外部变量可以在整个程序(所有的文件)中访问。外部变量在程序执行过程中只被初始化一次,即使在其他文件中使用extern关键字声明。 -
④
register
这种类型的变量被存储在寄存器中,而不是RAM中。这意味着访问它们的速度非常快,但空间有限。寄存器变量的生命周期和作用域与自动变量相同。
特征修饰:在C语言中,变量的特征修饰主要涉及到以下两个关键字:const 和 volatile。
- ①
const
const 关键字用于声明常量,也就是说,一旦定义了 const 变量,其值就不能被修改。 - ②
volatile
volatile 关键字用于告诉编译器,变量的值可能会在外部被改变,因此编译器不应对这种变量进行优化。
这两个关键字可以组合使用,
例如:
const volatile int port = 0x300;
这行代码定义了一个 const volatile 整数变量 port,这意味着 port 的值不能在程序中被修改,但可能会在外部被改变(例如,由硬件改变)。
2 变量的读写操作
在计算机中,变量的读写操作涉及到内存和CPU之间的数据交换。
读变量
:内存—>寄存器(CPU)
当你在程序中访问一个变量的值时,这个值会从内存中读取到CPU的寄存器中。这是因为CPU不能直接从内存中操作数据,它需要将数据读取到其寄存器中才能进行操作。写变量
:寄存器(CPU)—>内存
你在程序中更改一个变量的值时,这个值会从CPU的寄存器写入到内存中。这是因为变量的值需要存储在内存中,以便在之后的程序执行中使用。
3 代码优化
在计算机工作时,内存的访问速度远不及CPU的处理速度,为了提升计算机的整体性能,在软硬件层面都有相应的机制去优化内存的访问
- 硬件层面:引入高速缓存(Cache)
- 软件层面:
1.编码优化(程序员)
2.编译优化(编译器)优化的结果:减少内存访问的次数
4 关键字
5 指针
1. 函数指针
在C语言中,函数指针是一种特殊类型的指针,它指向一个函数,而不是一个数据对象。你可以使用函数指针来调用函数,或
者将函数作为参数传递给其他函数。
函数指针的声明包含了函数的返回类型和参数类型。
例如,下面是一个指向接受两个整数参数并返回整数的函数的函数指针的声明:
int (*function_ptr)(int, int);
function_ptr是一个函数指针,它指向一个接受两个int参数并返回int的函数。
应用1:将一个函数的地址赋值给函数指针,然后使用函数指针来调用这个函数。
例如:
int add(int x, int y) //定义一个名为`add`的函数
{
return x + y;
}
int main()
{
int (*add_ptr)(int, int) = add;
int sum = add_ptr(1, 2);
printf("%d\n", sum);
return 0;
}
创建一个名为add_ptr
的函数指针,并将函数add
的地址赋值给此函数指针,此时函数指针就指向了函数add
的地址,所以可以
用add_ptr
来调用函数add
2. 结构体指针
在C语言中,结构体是一种可以包含多个不同类型的数据的复合数据类型。结构体指针则是存储结构体变量地址的指针。
例如,定义一个名为Person的结构体,它包含一个字符串(用于存储名字)和一个整数(用于存储年龄):
struct Person {
char name[100];
int age;
};
然后,我们可以创建一个Person类型的变量,并创建一个指向这个变量的指针:
struct Person john;
struct Person *p = &john;
在这个例子中,p是一个指向Person结构体的指针。我们可以使用*运算符来访问p指向的结构体,例如:
strcpy(john.name, "John");
john.age = 30;
不过,更常见的做法是使用->运算符来访问结构体指针指向的结构体的成员,例如:
strcpy(p->name, "John");
p->age = 30;
这两段代码实现的功能是一样的:它们都将p指向的结构体的name成员设置为"John",并将age成员设置为30。
总结
边学习边总结,所以会持续写入新内容。如果觉得有用的话可以一起交流探讨心得体会。