惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Y
Y Combinator Blog
博客园 - Franky
D
Docker
B
Blog RSS Feed
M
MIT News - Artificial intelligence
雷峰网
雷峰网
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
S
SegmentFault 最新的问题
GbyAI
GbyAI
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
B
Blog
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
Vercel News
Vercel News
The Cloudflare Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News

峰峰的小窝

逆向提取软件数据库 正射影像拼接和AI实例分割系统设计实现 照片高斯建模实现 从零开始的自动驾驶(六)无刷电调硬件改了又改 从零开始的自动驾驶(五)整体设计|峰峰的小窝 从零开始的自动驾驶(四)无刷电调硬件修改|峰峰的小窝 从零开始的自动驾驶(三)ROS2与雷达|峰峰的小窝 从零开始的自动驾驶(二)编译开发板Linux|峰峰的小窝 从零开始的自动驾驶(一)无刷电调硬件|峰峰的小窝 搭建代理服务器跳板|峰峰的小窝 迎接新的时代|峰峰的小窝 一段故事的结束|峰峰的小窝 AI全自动写小说|峰峰的小窝 桌面直播通话APP设计实现|峰峰的小窝 2025 年总结|峰峰的小窝 SPI串并转换模块|峰峰的小窝 一种另类的数据标注方法|峰峰的小窝 GPS定位模块设计|峰峰的小窝 STM32硬件和软件设计指南|峰峰的小窝 基于Docker的多实例管理系统|峰峰的小窝 Hello PCB|峰峰的小窝 实时语音智能体快速搭建|峰峰的小窝 Git自动化测试部署|峰峰的小窝 Docker镜像构建和部署|峰峰的小窝 Web应用私有化|峰峰的小窝 虚幻FFMPEG解码视频|峰峰的小窝 Fish-Speech的编译优化|峰峰的小窝 阶段总结|峰峰的小窝
Hello ESP8266|峰峰的小窝
FFeng 我能想到的,最大的成功就是无愧于自己的心。 · 2025-08-19 · via 峰峰的小窝

有谁能拒绝一个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 字节,建议用户使用 mallocfree,而不是使用局部变量来分配数组,大型局部变量可能导致任务堆栈溢出。

  • 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 是罪魁祸首。

  1. 他和Flash有关系

  2. 它是我写的代码

解决问题

我发现这种找不到资料的东西真的特别适合上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,通过我设计的协议通信呢)

d3e4418302d13cd4ec559ac829371222.png

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端口。