有谁能拒绝一个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端口。

我能想到的,最大的成功就是无愧于自己的心。