拉电流和灌电流
想象一下你的单片机(或者其他芯片)的引脚(Pin)是一个水龙头。
-
拉电流 (Source Current):
-
动作: 你把这个引脚设置成高电平(比如输出 3.3V)。
-
比喻: 就像你打开了水龙头(拉),水(电流)从这个引脚流出来,流向外面的电路(比如一个 LED 灯,灯的另一端接地)。
-
总结: “拉” 就是芯片的引脚往外输出电流,像“拉”着外面的设备工作。此时引脚是电源。
-
-
灌电流 (Sink Current):
-
动作: 你把这个引脚设置成低电平(比如输出 0V,也就是接地)。
-
比喻: 就像你打开了下水道的盖子(灌),外面电路(比如 VCC 接了电阻,再接 LED,灯的另一端接到这个引脚)的水(电流)流进来,通过这个引脚流向芯片内部的“地”(GND)。
-
总结: “灌” 就是芯片的引脚吸收电流,像“灌”水一样把外面的电流“喝”进去。此时引脚是地。
-
如何读手册

以这段代码为例子。首先要明白systemcoreclock是系统时钟的意思。





提高CPU利用率

像这个代码就有点问题了,每次都要去计算一次strlen(str),所以一般推荐

当然,也可以选择别的方法。因为反斜杠0是每一个数组最后一位

串口一般默认使用高电平
所以要给它一个上拉电阻,对应的IO口上。
串口的波特率
一般不要太高的波特率,比如2Mbps,会有很多问题,这个具体可以问问AI。
stm32还可以自己修改栈大小
你的工程里一定有一个汇编文件,通常叫 startup_stm32f4xx.s(后缀是 .s)。这个文件负责单片机上电后的第一次初始化。

RAM
STM32(以及大多数单片机)中,RAM(随机存取存储器)就像是一个**“大仓库”**。
当你的程序跑起来时,这个仓库里主要堆放了 4 类东西。理解了这 4 类,你就知道你的 RAM 到底被谁吃掉了。
1. 全局变量区 (Global / Static) —— “钉子户”
2. 栈区 (Stack) —— “临时办事处”
3. 堆区 (Heap) —— “自由租赁区”
这块区域是给程序员手动申请用的。
-
谁在用?
-
malloc()、calloc()、realloc()这些 C 标准库函数。 -
某些第三方库(如 cJSON、某些 MQTT 库)内部可能会用到。
-
-
特点:
-
如果你代码里从来不写
malloc,这块消耗通常为 0(或者非常小,取决于启动文件里的Heap_Size设置)。 -
注意:有些工程默认
Heap_Size开了 0x200 (512字节),如果你不用,可以在启动文件里把它改成 0,省点 RAM。
-
4. 所谓的“常数” (Const) —— 注意!这通常不占 RAM
-
误区:很多新手以为
const int a = 10;占 RAM。 -
真相:在 STM32 中,加了
const修饰的只读变量(尤其是大的数组、字符串常量),编译器通常会把它们扔到 Flash(硬盘) 里,而不是 RAM 里。 -
优化技巧:多用
const!-
char msg[] = "Hello";-> 占 RAM(因为你可以修改它)。 -
const char *msg = "Hello";-> 字符串本体在 Flash,RAM 只占一个指针(4字节)。
-
如何省 RAM?(优化建议)
-
大数组尽量别做局部变量:
-
void func() { char buf[1024]; }-> 危险! 容易爆栈。 -
改为全局变量
static char buf[1024];-> 安全,移到了 BSS 区,虽然占总量,但不挤占栈。
-
-
字符串加
const:-
打印菜单、AT指令字符串,全部加上
const。
-
-
调整 Stack 和 Heap:
-
如果完全不用
malloc,把Heap_Size设为 0。 -
Stack_Size够用就行,不要盲目开太大(除非 RAM 很富裕)。
-
ESP 自动重连”是指当 Wi-Fi 模块(ESP8266/ESP32)因为路由器断电、信号太弱或干扰导致断网后,如何恢复网络连接的机制。
这通常分为两种情况:一种是 ESP 模块自己偷偷干的(固件自带功能),另一种是 STM32 命令它干的(应用层逻辑)。
我们来详细拆解一下这两种方式,以及推荐你怎么做。
方式一:ESP 模块内部的“傻瓜式”自动重连
ESP 的官方 AT 固件里内置了一个功能。只要你曾经连上过一次 Wi-Fi,它就会把账号密码记住(保存在 Flash 里)。
-
机制:一旦断网,ESP 内部的底层代码会不断尝试去连接刚才保存的那个路由器。
-
指令:
AT+CWAUTOCONN=<enable>-
AT+CWAUTOCONN=1:开启自动重连(出厂默认通常是开启的)。 -
AT+CWAUTOCONN=0:关闭。
-
-
优点:STM32 代码写起来简单。STM32 只需要在开机时发一次连接命令,后面如果断网了,STM32 甚至都不用管,ESP 会自己拼命连。
-
缺点:STM32 失去了控制权。STM32 不知道现在到底连没连上(除非不断查询),如果这时候 STM32 发送 HTTP 请求,必然会报错。
方式二:STM32 控制的“智能”重连(强烈推荐)
这是更稳健的做法。STM32 不依赖 ESP 的自动功能,而是通过代码逻辑来监控状态。
核心逻辑是: STM32 充当“监工”,每隔几秒钟问一下 ESP:“你还连着吗?”
-
如果连着,就正常干活(比如获取天气)。
-
如果断了,STM32 再次发送
AT+CWJAP命令让它重新连接。
为什么推荐这种方式?
因为你的代码里实现了 wifi_is_connected() 和 esp_at_http_get()。如果不做判断:
-
假设断网了,ESP 正在尝试内部重连。
-
此时你的 STM32 不知道,还在不停地发
AT+HTTPCLIENT...。 -
ESP 会返回
ERROR或busy。 -
你的程序可能会卡住,或者得到错误的数据。
RTC
本身就是一个计数器,单片机一般都是32位,而且是无符号类型的,所以计数会非常舒服。当通过VBAT引脚给单片机供电,stm32芯片就可以继续维持其芯片上的一块叫后备区域的地方继续执行。
由于其耗电少,所以一般是可以由纽扣电池为其供电。一般都是低速外部时钟提供时钟信号。
中断清除

就比如这个EXTI_ClearITPendingBit(EXTI_LineX);
FPU

有这个就说明了打开了FPU了。
电容消抖

如何理解呢?
就是电容它要么吸收电流要么放出电流,不会同时进行。
当打开开关的时候,电容一直吃电流,直到满了,这个时候打开开关,吃的电流就全部往外吐了。吐给gnd,速度非常快。虽然会有抖动,但是抖动的时间,就比如时关时开,但是这个时间太短了,无法把电容充满电,也就无法让KEY3这个端口读到高电平。
我们拆解一下这“精彩的一瞬”:
-
动作: 你按下按键,电容里的电瞬间吐光了(变成 0V)。
-
抖动发生: 0.1毫秒后,金属片因为弹性**“意外断开”**了一下(电路变成了充电状态)。
-
电容的反应:
-
这时候,电源 (3.3V) 确实想赶紧给电容“喂饭”(充电)。
-
但是! 因为有一个 10kΩ 的大电阻 (R22) 卡在中间,喂饭的速度太慢了!
-
抖动只持续了极短的时间(比如 0.1ms)。
-
在这 0.1ms 里,电容只来得及“吃”进一口饭,电压可能才从 0V 涨到 0.1V。
-
-
结果:
-
对于单片机 (KEY3) 来说,0.1V 依然算作 低电平。
-
单片机根本没发现中间断开过。它觉得你一直按得死死的。
-
电容就是一个“反应迟钝的仓库”。
-
放电快(因为没电阻挡着,直接吐给GND)。
-
充电慢(因为有 10kΩ 电阻挡着)。
编译和链接
编译:将文件变为可执行的二进制机器指令。保存这些二进制机器指令的文件叫做目标文件。每一个源文件对应目标文件。
3. 所谓的“残缺”长什么样?
既然不知道地址,编译器也不能罢工啊。于是,它决定**“留个坑”**(或者填个假地址)。
它生成的目标文件(机器指令)大概是这样的:
这就轮到链接器出场了:
这时候,指令才变成了完整的:“跳转到 0x08000555”。
这就是为什么说它是“残缺”的——如果你现在强行运行这个文件,CPU 执行到这就跳转到 0x00000000 去了,程序直接崩溃。
4. 链接器的修补(生成可执行文件)
-
此时的“目标文件”(Object File)虽然是二进制指令,但这时的指令是**“甚至是残缺的”**(比如函数地址还是空的),所以它还不能运行,只能躺在硬盘里。如何理解残缺呢?
-
假设你的代码是这样的:
// main.c 文件 void main() { LED_ON(); // 调用点灯函数 }但是!
LED_ON()这个函数的具体代码,是写在另一个文件led.c里的。2. 编译器的尴尬(生成目标文件 .o)
当你编译
main.c时,编译器是一个很死板的翻译官,它从上往下翻译: -
看到
void main()-> 翻译成机器码0101...(意思是建立函数栈帧)。 -
看到
LED_ON();-> 编译器知道这是“跳转”指令。-
它的心里活动是:“我知道你要跳到
LED_ON那里去执行,但是!LED_ON到底在哪?” -
因为
LED_ON在led.c里,编译器现在只盯着main.c看,它根本不知道LED_ON在内存的第几号房间。
-
-
指令意思:
CALL(呼叫/跳转) -
目标地址:
0x00000000(这是一个假地址/占位符) -
附带一张小纸条(重定位表): “注意!这里原本要填
LED_ON的地址,但我不知道,先空着,麻烦后面来的人(链接器)帮我填一下。” -
它手里拿着
main.o(有一张小纸条说缺地址)。 -
它手里还拿着
led.o(里面有LED_ON的真实代码)。 -
它经过计算,发现
LED_ON最终会被放在内存的0x08000555这个位置。 -
回填: 它跑回
main.o的那个坑里,把0x00000000涂改掉,填上0x08000555。
这就轮到链接器出场了:
-
它手里拿着
main.o(有一张小纸条说缺地址)。 -
它手里还拿着
led.o(里面有LED_ON的真实代码)。 -
它经过计算,发现
LED_ON最终会被放在内存的0x08000555这个位置。 -
回填: 它跑回
main.o的那个坑里,把0x00000000涂改掉,填上0x08000555。
这时候,指令才变成了完整的:“跳转到 0x08000555”。

链接器将目标文件打包成可执行程序。编译器除了打包这个还会打包标准库等现成库。链接器(Linker)就像装修工,不仅要把你自己造的家具(目标文件)摆好,还得把买来的现成家电(标准库,比如 printf 的实现代码)也接好电线。
拿helloworld来说,编译器在遇到printf的时候,它不知道这个是啥玩意,并不是完全不知道,这不是编译器关心的东西,编译器只能看到局部,只能看到一个原文件内部,它虽然不管 printf 的代码在哪里,但它必须知道 printf 长什么样(这就需要写 #include <stdio.h>)。
-
如果你不写头文件,编译器会抱怨:“我都没见过这个人的‘名片’,你让我怎么给你打包?”
编译器只关心**“名片”(声明),不关心“真人”**(定义)。
链接器才是关心printf哪里来的。它是真的要去把 printf 的**“真人”**(二进制代码)找出来,塞进你的程序里。
结合extern
房间 A (sensor.c) —— 有实物
// 这里真正定义了变量,分配了内存空间
int temperature = 25;
房间 B (main.c) —— 想借用
void main() {
// 我想打印 temperature,但是编译器不知道它是谁!
printf("%d", temperature);
}
2. 这里的矛盾(编译报错)
当你尝试编译 main.c 时,编译器(那个死板的翻译官)会立马报错:
Error: 'temperature' undeclared (first use in this function). 翻译: “你不讲武德!在
main.c这个房间里,我压根没看到你定义temperature,你凭什么用它?”
因为编译器是**“单文件主义者”**,它编译 main.c 时,完全看不到 sensor.c 里有什么。
为了让编译器闭嘴,你需要用 extern 给它写一张“承诺书”。
我们修改 房间 B (main.c):
// 这就是 extern 的作用!
extern int temperature;
void main() {
printf("%d", temperature); // 现在编译器放行了
}
这句话的意思是:
“亲爱的编译器,请不要报错。虽然在这个文件里没看到
temperature的定义,但我承诺:它一定在别的什么文件里定义过了。你先给我生成一个‘残缺’的目标文件,回头让链接器去找它。”
2858

被折叠的 条评论
为什么被折叠?



