有谁能拒绝一个10块钱包邮的ESP8266呢——
有段时间没碰芯片了有些想念
业余的我要折腾嵌入式了~
多灾多难的HelloWorld
规则怪谈
嵌入式,当然是用PlatformIO啦~方便
这次的Hello World我们基于esp8266-rtos-sdk 来写。
在espressif\esp_common.h 中有如下怪谈:
void user_init(void)是应用程序的入口函数。建议用户将定时器设置为周期模式,用于定期检查。
在 FreeRTOS 定时器或 os_timer 中,不要通过
while(1)或其他会阻塞线程的方式来延时。定时器回调函数不应占用 CPU 超过 15ms。
os_timer_t不应定义为局部变量,它必须是全局变量或者是通过malloc获取的内存。函数默认存储在 CACHE 中,不需要添加
ICACHE_FLASH_ATTR。中断函数也可以存储在 CACHE 中。如果用户希望将一些频繁调用的函数存储在 RAM 中,请在函数名前加上IRAM_ATTR。网络编程使用 socket,请不要绑定到相同的端口。
如果用户想要创建 3 个或更多的 TCP 连接,请在
user_init中添加"TCP_WND = 2 x TCP_MSS;"。RTOS SDK 的优先级是 15。
xTaskCreate是 FreeRTOS 的接口,关于 FreeRTOS 和系统 API 的详细信息,请访问 http://www.freertos.org。使用
xTaskCreate创建任务时,任务堆栈的范围是 [176, 512]。如果任务中使用的数组长度超过 60 字节,建议用户使用
malloc和free,而不是使用局部变量来分配数组,大型局部变量可能导致任务堆栈溢出。RTOS SDK 占用了一些优先级。pp 任务的优先级是 13;精确定时器(ms)线程的优先级是 12;TCP/IP 任务的优先级是 10;FreeRTOS 定时器的优先级是 2;空闲任务的优先级是 0。
用户可以使用优先级为 1 到 9 的任务。
请不要修改
FreeRTOSConfig.h,配置是由 RTOS SDK 中的源代码决定的,用户无法更改它。
Hello World
#include "esp_common.h"
extern "C"
{
void user_init()
{
printf("HelloWorld!\n");
}
void user_rf_cal_sector_set(void)
{
}
}虽然user_rf_cal_sector_set 目前没啥用,但是因为缺少定义不加不能编译。
(这个HelloWorld是不能用的,因为这个HelloWord写错了就有了后文)
库提供的头文件都是给C++可用的头(里面都有extern C),函数声明实现都是C写的,所以像user_init 这种函数的定义需要extern C 。
默认波特率是 74880。
打开串口,复位,静候HelloWorld,我得到了如下结果:
ets Jan 8 2013,rst cause:2, boot mode:(3,6)
load 0x40100000, len 31360, room 16
tail 0
chksum 0x89
load 0x3ffe8000, len 2128, room 8
tail 8
chksum 0x3c
load 0x3ffe8850, len 484, room 0
tail 4
chksum 0xa1
csum 0xa1
csum err又进行了一些测试,循环输出Helloworld,发现,烧录完之后立即连接串口能看到信息,等一会儿就会卡住,复位就是上面的内容。
重新上电就是干巴巴一行ets Jan 8 2013,rst cause:2, boot mode:(1,7)
嗯,HelloWorld并不容易呢。
排除问题
排除设备选择的问题
为了确定是不是PIO的板子选错了,我用Arduino框架试了一下,跑起来了···
于是我猜测是Flash大小可能没对上,因为Arduino比RTOS框架小,然后···

我就往里塞了本小说~
但是并没有出问题,所以问题就出在RTOS框架上了。
排除是串口干扰
我发现一个问题,即使是Arduino框架写的,重新上电之后程序也不运行,显示ets Jan 8 2013,rst cause:2, boot mode:(1,7)
查资料得,mode1就是不运行,下载用的,mode3才会跑程序。
一顿尝试后,我给它写了个LED闪烁,我发现上电后并非不执行,而是只要连了串口就不执行。
不知道是板子设计缺陷,还是软件BUG,还是我有BUG,聪明的我拿了一根线把 IO0 接到了 3V3 上,IO0脚高电平就不会进入烧录模式。
暂定这个问题是硬件设计问题,对一开始的问题不构成影响。
降低问题复杂度
有一点在我脑海中徘徊,我测试了循环输出HelloWorld,烧录好后立即连接串口就能看到,过一会儿就输出乱码,然后卡住,复位重启就Check Sum Error了。
出现乱码卡住肯定就是程序跑飞了,为什么呢?
我不知道捏~
和程序一起卡住的我,决定要做点儿什么,于是写了这样的程序:
#include "esp_common.h"
void ICACHE_FLASH_ATTR init_light_task_main(void *)
{
while (true)
{
vTaskDelay(500 / portTICK_RATE_MS);
GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, BIT2);
vTaskDelay(500 / portTICK_RATE_MS);
GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, BIT2);
}
}
void ICACHE_FLASH_ATTR init_light()
{
GPIO_REG_WRITE(GPIO_ENABLE_W1TS_ADDRESS, BIT2);
uint32_t pin_reg = GPIO_REG_READ(GPIO_PIN2_ADDRESS);
pin_reg &= ~GPIO_PIN2_CONFIG;
pin_reg |= (0 << GPIO_PIN2_CONFIG_S);
GPIO_REG_WRITE(GPIO_PIN2_ADDRESS, pin_reg);
xTaskCreate(init_light_task_main, (const signed char *)"init_light_task", 256, NULL, 2, NULL);
}据测试(我去Arduino那边扒拉了下代码)ESP8266模块上的那个小灯是IO2 D4 寄存器是第二个。
求着AI写了上面的闪烁代码。
我发现这个程序烧录完,只要不复位就可以一直跑。(这个程序不会跑飞)
说是我根据上面的信息得出的结论有些牵强,不如说是我猜的,因为有个坏家伙(一句代码)写了代码所在的FLASH,所以校验和会失败,又因为写的不多,所以有偶发性程序跑飞。
确定目标
这个坏家伙是谁呢?RTOS吗?操作系统会写Flash似乎并不奇怪,但我就算代码全都删掉也会有问题,总不能默认就是跑不起来的吧。
沿着这个思路,看我没几行的代码,我觉得user_rf_cal_sector_set 是罪魁祸首。
他和Flash有关系
它是我写的代码
解决问题
我发现这种找不到资料的东西真的特别适合上Github,我在Github搜user_rf_cal_sector_set 的名字,找到了,我的签名直接就错了,这个函数签名是uint32 user_rf_cal_sector_set(void) 我写的void user_rf_cal_sector_set(void) ,C语言啊你学学人家C++类型对不上至少会给报错。
在我的认知中,有返回值和无返回值的函数编译成汇编后区别很大,不像Js,无返回值就是undefined,程序不会直接完全跑飞真的奇迹呀。
参考(Copy)Github上的实现:
uint32 user_rf_cal_sector_set(void)
{
auto size_map = system_get_flash_size_map();
uint32 rf_cal_sec = 0;
switch (size_map)
{
case FLASH_SIZE_4M_MAP_256_256:
rf_cal_sec = 128 - 5;
break;
case FLASH_SIZE_8M_MAP_512_512:
rf_cal_sec = 256 - 5;
break;
case FLASH_SIZE_16M_MAP_512_512:
case FLASH_SIZE_16M_MAP_1024_1024:
rf_cal_sec = 512 - 5;
break;
case FLASH_SIZE_32M_MAP_512_512:
case FLASH_SIZE_32M_MAP_1024_1024:
rf_cal_sec = 1024 - 5;
break;
case FLASH_SIZE_64M_MAP_1024_1024:
rf_cal_sec = 2048 - 5;
break;
case FLASH_SIZE_128M_MAP_1024_1024:
rf_cal_sec = 4096 - 5;
break;
default:
rf_cal_sec = 0;
break;
}
return rf_cal_sec;
}于是,HelloWorld,完成!

连接Wifi
写HelloWorld的时候写了一段连接Wifi的代码,没想到Hello World写完了Wifi连接记录居然还在。
感觉Wifi的连接信息似乎没记录在Flash里,因为我清空过Flash,但是Wifi还在。
void ICACHE_FLASH_ATTR init_wifi_task_main(void *)
{
do
{
printf("Connecting to Wi-Fi...\n");
wifi_set_opmode(STATION_MODE);
struct station_config stationConf;
memset(&stationConf, 0, sizeof(stationConf));
sprintf((char *)stationConf.ssid, "CU_Fkk4");
sprintf((char *)stationConf.password, "13280041987");
wifi_station_set_config_current(&stationConf);
} while (!wifi_station_connect());
printf("Wi-Fi connected successfully!\n");
vTaskDelete(NULL);
}
void ICACHE_FLASH_ATTR init_wifi()
{
xTaskCreate(init_wifi_task_main, (const signed char *)"init_wifi_task", 256, NULL, 2, NULL);
}说是不能在init的时候连接wifi,于是就在线程里面连接Wifi了。
这里就一切顺利咯。并不顺利,没用过FreeRTOS的我没写vTaskDelete导致反复重启报错。
广播轰炸
除了能够连接Wifi,还能够作为软AP。
软AP有个选项叫做广播发送间距,最小设置100毫秒。
既然是软AP,也就是说所有的广播、连接管理、数据包构造、收发都是软件完成的,理论是不会被限制广播发送间隔时间的。
反骨一下,我要以最大速率发送Wifi广播!
我先打开混杂模式、进入连接模式但断开连接、将收到的数据包通过串口发送给电脑。
挑一个广播包,复制到代码里面,用wifi_send_pkt_freedom 重放,据测试,手机能够收到满格的这个Wifi。
接下来,广播包里面的SSID和MAC地址每次都发送不一样的,就能达到广播轰炸的目的。
sint32 send_custom_beacon(const char *ssid)
{
// 基础帧结构长度(不含SSID部分)
#define BEACON_FRAME_BASE_SIZE 42
uint8_t ssid_len = strlen(ssid);
uint16_t frame_length = BEACON_FRAME_BASE_SIZE + ssid_len + 10; // +10 用于其他标签
if (frame_length > 1400)
{
return sint32(-1);
}
uint8_t *frame = (uint8_t *)malloc(frame_length);
if (!frame)
return sint32(-1);
int pos = 0;
// 1. 帧控制字段 (0x80 表示管理帧/信标帧)
frame[pos++] = 0x80; // Type/Subtype: Beacon
frame[pos++] = 0x00; // Flags
// 2. 持续时间 (设为0)
frame[pos++] = 0x00;
frame[pos++] = 0x00;
// 3. 目的地址 (广播地址)
uint8_t broadcast_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
memcpy(&frame[pos], broadcast_addr, 6);
pos += 6;
// 4. 源地址 (伪造的MAC地址)
uint8_t fake_mac[6];
for (int i = 0; i < sizeof(fake_mac); i++)
fake_mac[i] = os_random() & 0xFF;
memcpy(&frame[pos], fake_mac, 6);
pos += 6;
// 5. BSSID (与源地址相同)
memcpy(&frame[pos], fake_mac, 6);
pos += 6;
// 6. 序列号 (随机值,高4位为片段号,低12位为序列号)
frame[pos++] = 0xC0; // 片段号 0xC,序列号低位
frame[pos++] = 0x00; // 序列号高位
// 7. 帧主体 - 固定参数部分
// 时间戳 (8字节,设为0)
memset(&frame[pos], 0, 8);
pos += 8;
// 信标间隔 (100 TU = 102.4 ms)
frame[pos++] = 0x64; // 100 in hex
frame[pos++] = 0x00;
// 能力信息 (0x2104 表示标准AP)
frame[pos++] = 0x21;
frame[pos++] = 0x04;
// 8. 标签部分
// SSID 标签 (0x00)
frame[pos++] = 0x00; // SSID 标签号
frame[pos++] = ssid_len; // SSID 长度
memcpy(&frame[pos], ssid, ssid_len); // SSID 内容
pos += ssid_len;
// 支持速率标签 (0x01)
uint8_t rates[] = {0x82, 0x84, 0x8b, 0x96, 0x24, 0x30, 0x48, 0x6c}; // 典型速率
frame[pos++] = 0x01; // 速率标签号
frame[pos++] = 8; // 8种速率
memcpy(&frame[pos], rates, 8);
pos += 8;
// 9. 发送帧
sint32 ret = wifi_send_pkt_freedom(frame, pos, false);
free(frame);
return ret;
}(从理论上来说,两个ESP8266可以不用Wifi,通过我设计的协议通信呢)

Wifi轰炸的一些小结论
MAC地址不是随便选的,第一个字节必须是偶数。
SSID长度必须小于等于32字节。
SSID可以是Unicode字符。(一个中文3字节)
MAC地址相同的不同SSID会被合并。
MAC地址不同的相同SSID也会被合并。
Wifi广告
在广播轰炸的基础上,我希望显示一段连续的信息,每行一个Wifi,我把它们写在了一个TXT里面:

感觉手机扫描Wifi的时间窗口是1秒,据测试1秒能够发出去1600个广播包,理论上来说,能够支持1500行的内容不丢失。
如果是最开始写单片机的我,肯定把这个txt里面的每行单独复制,放C++代码里面。
而现在的我,哼哼哼,不仅让常量放Flash里面不占内存,还能写工具脚本,免去复制。
import os
Import("env") # PlatformIO 自动传入的环境对象
TXT_FILE = "src/wifi_banner.txt"
OUTPUT_H = "include/wifi_banner.h"
ARRAY_NAME = "wifi_banner_txt"
MAX_LINE_LENGTH = 28
def txt_to_header(source, target, env):
print(f"[extra_script] 正在处理 {TXT_FILE}...")
line_pos = [0]
content_bytes = bytes()
with open(TXT_FILE, "rt", encoding="utf-8") as f:
content = f.read()
content = content.replace("\r\n", "\n").split("\n")
for i, line in enumerate(content):
bytes_line = line.encode("utf-8")
line_pos.append(line_pos[-1] + len(bytes_line) + 1)
if len(bytes_line) > MAX_LINE_LENGTH:
raise ValueError(
f"行{i + 1}长度超过限制: {line} (长度: {len(bytes_line)}),最大长度: {MAX_LINE_LENGTH}"
)
content_bytes += bytes_line + b'\0'
with open(OUTPUT_H, "w", encoding="utf-8") as f:
f.write(f"// 自动生成的文件,不要手动修改\n")
f.write(f"#include \"esp_system.h\"\n")
f.write(
f"const ICACHE_RODATA_ATTR unsigned char {ARRAY_NAME}[] = {{\n")
for i, b in enumerate(content_bytes):
f.write(f"0x{b:02x},")
if (i + 1) % 16 == 0:
f.write("\n")
f.write(f"\n}};\n")
f.write(
f"const unsigned int {ARRAY_NAME}_len = {len(content_bytes)};\n")
f.write(
f"const unsigned int {ARRAY_NAME}_count = {len(content)};\n")
f.write(
f"const ICACHE_RODATA_ATTR unsigned int {ARRAY_NAME}_line_pos[] = {{{', '.join(map(str, line_pos))}}};\n")
print(f"[extra_script] {OUTPUT_H} 已生成")
return None
txt_to_header(None, None, env)
# 注册在编译前执行
env.AddPreAction("buildprog", txt_to_header)
最终项目已经开源,鬼能想到我把HelloWorld写成了什么,见Github。
总结
Hello ESP8266!
Hello Free RTOS!
如果所有功能,所有技术都封装好了的话,学习底层还有什么用呢?
当然有!就像这次违规广播轰炸,还有之前用Go写的RTMP和HTTP复用TCP端口。