跳到主要内容

2 - 小程序与自定义通信协议

课程定位: 5天IoT实训 · 第二天
主题: AI 辅助开发工具链 · VibeCoding 小程序 · 自定义二进制数据帧 · STM32 串口接收与 LED 控制
项目: WXMP-IOT — 微信小程序中控系统
目标硬件: STM32F407ZET6 + BLE DX-BT24 + ESP8266 + DHT11


一、AI 工具链概览

1.1 现代开发者的 AI 工具链

AI工具链1

AI工具链2

当前主流 AI 辅助开发工具:

工具定位适用场景
VibeCodingAI 驱动的全栈代码生成(小程序/Web)微信小程序、前端页面快速原型
CursorAI 增强的代码编辑器(IDE)日常编码、代码重构、解释代码
Claude / ChatGPT对话式 AI 助手需求分析、协议设计、文档生成
GitHub Copilot行内代码补全嵌入已有 IDE,逐行辅助

核心思路: AI 工具不替代程序员,而是把重复性代码生成交给 AI,让开发者专注在架构设计调试验证上。


1.2 嵌入式开发 vs 纯软件开发

嵌入式软件和纯软件项目开发1

嵌入式软件和纯软件项目开发2

维度纯软件项目嵌入式项目
运行环境PC / 服务器 / 手机 OS裸机 MCU(无 OS 或 RTOS)
调试方式printf / IDE 断点 / 浏览器 DevToolsUART 打印 / Keil 仿真器 / 示波器
AI 生成代码直接运行,秒级验证需编译烧录,分钟级验证
迭代速度极快(保存即生效)较慢(编译 + 烧录 + 复位)
调试难点逻辑错误为主硬件时序、电平、外设配置

结论: 嵌入式项目用 AI 生成驱动代码模板效率最高;涉及硬件时序的代码仍需人工精调。


1.3 嵌入式系统层级与数据流

嵌入式系统层级与数据流解析示意图

嵌入式软件架构

WXMP-IOT 项目采用三端架构

微信小程序(上位机)
│ BLE / WiFi

STM32F407(下位机)
├── BLE DX-BT24 ← UART2
├── ESP8266 WiFi ← UART3
├── DHT11 温湿度 ← GPIO
└── LED x4 ← GPIO

数据流方向:小程序发送控制帧 → BLE 透传到 STM32 UART → 解析帧 → 执行动作(开/关 LED)→ 上报结果。


二、VibeCoding 小程序开发

2.1 什么是 VibeCoding

VibeCoding 是一种以自然语言描述需求,由 AI 自动生成完整项目代码的开发方式。用于微信小程序时,只需描述页面功能和交互逻辑,AI 即可生成:

  • 页面结构(WXML)
  • 样式(WXSS)
  • 逻辑(JavaScript)
  • 蓝牙/网络调用代码(wx.createBLEConnection 等)

2.2 工程目录结构

用 VibeCoding 生成的微信小程序工程标准结构:

wxmp-iot/
├── app.js ← 全局 App 实例,初始化蓝牙权限
├── app.json ← 全局配置:页面注册、导航栏样式
├── app.wxss ← 全局样式
├── pages/
│ ├── index/ ← 首页(设备列表 + 扫描)
│ │ ├── index.wxml
│ │ ├── index.js
│ │ └── index.wxss
│ └── control/ ← 控制页(LED 开关 + 传感器显示)
│ ├── control.wxml
│ ├── control.js
│ └── control.wxss
└── utils/
├── ble.js ← 蓝牙通信封装(连接/发送/接收)
└── protocol.js ← 数据帧编解码

2.3 关键代码片段

小程序发送控制帧(protocol.js)

/**
* 计算 SUM 校验值(所有字节求和保留低8位)
*/
function calcSum(bytes) {
return bytes.reduce((acc, b) => acc + b, 0) & 0xFF;
}

/**
* 构建 LED 单独控制帧
* @param {number} ledNum LED 编号 1-4
* @param {number} status 0xFF=亮, 0x00=灭, 0x55=翻转
* @returns {ArrayBuffer}
*/
function buildLedFrame(ledNum, status) {
const devId = 0x10 + ledNum; // 0x11~0x14
const frame = [0xAA, 0x05, devId, status];
frame.push(calcSum(frame)); // 追加校验和
return new Uint8Array(frame).buffer;
}

/**
* 构建 LED 批量控制帧
* @param {number[]} leds 4个LED状态 [L1, L2, L3, L4]
*/
function buildLedBatchFrame(leds) {
const frame = [0xAA, 0x08, 0x10, ...leds];
frame.push(calcSum(frame));
return new Uint8Array(frame).buffer;
}

module.exports = { buildLedFrame, buildLedBatchFrame };

小程序通过 BLE 发送帧(ble.js)

// 已连接到 DX-BT24,serviceId/characteristicId 从扫描时获取
function sendFrame(buffer) {
wx.writeBLECharacteristicValue({
deviceId: app.globalData.deviceId,
serviceId: app.globalData.serviceId,
characteristicId: app.globalData.writeCharId,
value: buffer,
success: () => console.log('帧发送成功'),
fail: (err) => console.error('发送失败', err)
});
}

// 点击"LED1 亮"按钮
onLed1On() {
const buf = protocol.buildLedFrame(1, 0xFF);
sendFrame(buf);
}

三、自定义数据帧协议

3.1 帧结构设计

┌─────────┬───────────┬──────────┬──────────────────────┬──────────┐
│ 包头 │ 数据个数 │ DevID │ 数据内容 │ SUM │
│ 0xAA │ N │ DEV(1B) │ DATA(N-2) │ SUM(1B) │
└─────────┴───────────┴──────────┴──────────────────────┴──────────┘
字段字节数说明
包头1固定 0xAA,帧起始标识
数据个数 N1帧总长度(含 DevID + 数据内容 + SUM,不含包头和N本身)
DevID1设备/功能编号
数据内容N-2控制参数,字节数 = N - 2
SUM1前面所有字节(含包头)求和的低8位

SUM 计算公式:

SUM = (0xAA + N + DevID + DATA[0] + DATA[1] + ...) & 0xFF
为什么 N 不含包头?

包头 0xAA 是固定的帧起始标识,解析时直接查找 0xAA 即可定位帧头。N 从自身开始计数,方便 MCU 按长度读取剩余字节。


3.2 DevID 速查表

DevID功能数据内容示例帧
0x00系统 PING(触发温湿度上报)1B: 0x00AA 05 00 00 AF
0x01系统复位1B: 0x00AA 05 01 00 B0
0x10LED 批量控制4B: [L1][L2][L3][L4]AA 08 10 FF FF FF FF BF
0x11LED1 控制1B: FF=亮 00=灭 55=翻转AA 05 11 FF BF
0x12LED2 控制同上AA 05 12 FF C0
0x13LED3 控制同上AA 05 13 FF C1
0x14LED4 控制同上AA 05 14 FF C2
0x21DHT11 温湿度上报JSON 字符串AA 17 21 7B...7D C6
0xFF错误响应[原DevID][错误码]AA 06 FF 11 01 C1

3.3 LED 控制帧示例

单独控制 LED

操作SUM 计算
LED1 亮AA 05 11 FF BFAA+05+11+FF = 0x1BF → 0xBF
LED1 灭AA 05 11 00 C0AA+05+11+00 = 0xC0
LED1 翻转AA 05 11 55 15AA+05+11+55 = 0x115 → 0x15
LED2 亮AA 05 12 FF C0AA+05+12+FF = 0x1C0 → 0xC0

批量控制 LED

L1L2L3L4
FF000000AA 08 10 FF 00 00 00 C1
00FF0000AA 08 10 00 FF 00 00 C1
FFFFFFFFAA 08 10 FF FF FF FF BF
00000000AA 08 10 00 00 00 00 C2
55555555AA 08 10 55 55 55 55 16

四、STM32 串口通信实现

4.1 UART 接口分配

UART波特率连接设备用途
UART1115200PC / 串口助手调试输出 + 帧测试
UART29600BLE DX-BT24小程序透传帧数据
UART3115200ESP8266WiFi AT 指令 / 帧数据

4.2 UART 初始化(bsp_uart.c)

在 Day 1 的 UART1(printf 重定向)基础上,新增 UART2 初始化用于 BLE 通信:

/* bsp/uart/bsp_uart.c */

#include "bsp_uart.h"

/* ===== UART1 115200 调试口 ===== */
void UART1_Init(uint32_t baudrate)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

/* PA9 = TX, PA10 = RX */
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStruct);

GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);

USART_InitStruct.USART_BaudRate = baudrate;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStruct);

/* 使能接收中断 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);

USART_Cmd(USART1, ENABLE);
}

/* printf 重定向 */
int fputc(int ch, FILE *f)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}

4.3 数据帧解析(状态机)

接收串口数据帧采用状态机解析,避免轮询等待整帧到来:

/* bsp/uart/frame_parser.c */

#include "frame_parser.h"

/* 帧缓冲区(最大64字节) */
#define FRAME_BUF_MAX 64

typedef enum {
STATE_WAIT_HEADER = 0, /* 等待包头 0xAA */
STATE_GET_LEN, /* 读取长度 N */
STATE_GET_DATA, /* 读取 N 字节数据 */
} ParseState;

static ParseState s_state = STATE_WAIT_HEADER;
static uint8_t s_buf[FRAME_BUF_MAX];
static uint8_t s_len = 0; /* 期望接收的总字节数(含N本身) */
static uint8_t s_cnt = 0; /* 已接收字节数 */

/* 校验和计算 */
static uint8_t calc_sum(const uint8_t *frame, uint8_t len)
{
uint8_t sum = 0;
for (uint8_t i = 0; i < len; i++) sum += frame[i];
return sum;
}

/**
* 单字节输入状态机,帧完整时回调 on_frame()
* @param byte 收到的单字节
*/
void frame_parser_feed(uint8_t byte)
{
switch (s_state) {
case STATE_WAIT_HEADER:
if (byte == 0xAA) {
s_buf[0] = 0xAA;
s_state = STATE_GET_LEN;
}
break;

case STATE_GET_LEN:
s_len = byte; /* N = 数据个数(含DevID+Data+SUM) */
s_buf[1] = byte;
s_cnt = 2; /* 已存 AA + N */
s_state = STATE_GET_DATA;
break;

case STATE_GET_DATA:
s_buf[s_cnt++] = byte;
/* 收满 2+N 字节(AA + N + N字节数据) */
if (s_cnt >= (uint8_t)(s_len + 2)) {
/* 验证校验和:前(s_cnt-1)字节求和 == 最后1字节 */
uint8_t expect = calc_sum(s_buf, s_cnt - 1);
if (expect == s_buf[s_cnt - 1]) {
/* 帧合法,回调处理 */
on_frame(s_buf, s_cnt);
} else {
printf("SUM ERR: expect 0x%02X got 0x%02X\r\n",
expect, s_buf[s_cnt - 1]);
}
/* 复位状态机 */
s_state = STATE_WAIT_HEADER;
s_cnt = 0;
}
break;
}
}

/* USART1 中断处理 */
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
uint8_t data = (uint8_t)USART_ReceiveData(USART1);
frame_parser_feed(data);
}
}

五、串口数据帧控制 LED

5.1 帧处理回调函数

/* app/frame_handler.c */

#include "frame_handler.h"
#include "bsp_led.h" /* LED_On(), LED_Off(), LED_Toggle() */

/* LED 状态值 */
#define LED_ON 0xFF
#define LED_OFF 0x00
#define LED_TOGGLE 0x55

/**
* 应用层帧处理:根据 DevID 执行动作
* @param frame 完整帧缓冲区(含包头~SUM)
* @param len 帧长度
*/
void on_frame(const uint8_t *frame, uint8_t len)
{
uint8_t dev_id = frame[2]; /* DevID 在第3字节 */

switch (dev_id) {

/* ---- LED 单独控制 0x11~0x14 ---- */
case 0x11: case 0x12: case 0x13: case 0x14:
{
uint8_t led_num = dev_id - 0x10; /* 1~4 */
uint8_t status = frame[3];
apply_led(led_num, status);
printf("LED%d %s\r\n", led_num,
status == LED_ON ? "ON" :
status == LED_OFF ? "OFF" : "TOGGLE");
break;
}

/* ---- LED 批量控制 0x10 ---- */
case 0x10:
for (uint8_t i = 0; i < 4; i++) {
apply_led(i + 1, frame[3 + i]);
}
printf("LED batch: L1=%02X L2=%02X L3=%02X L4=%02X\r\n",
frame[3], frame[4], frame[5], frame[6]);
break;

/* ---- 系统 PING ---- */
case 0x00:
printf("PING received\r\n");
/* TODO: 触发 DHT11 上报 */
break;

default:
printf("Unknown DevID: 0x%02X\r\n", dev_id);
break;
}
}

/* LED 动作执行(封装亮/灭/翻转) */
static void apply_led(uint8_t led_num, uint8_t status)
{
switch (status) {
case LED_ON: LED_On(led_num); break;
case LED_OFF: LED_Off(led_num); break;
case LED_TOGGLE: LED_Toggle(led_num); break;
}
}

5.2 LED BSP 驱动(bsp_led.c)

/* bsp/led/bsp_led.c — LED1=PG14, LED2=PG13, LED3=PG6, LED4=PG7 */

#include "bsp_led.h"

static GPIO_TypeDef* LED_PORT[] = {GPIOG, GPIOG, GPIOG, GPIOG};
static uint16_t LED_PIN[] = {GPIO_Pin_14, GPIO_Pin_13,
GPIO_Pin_6, GPIO_Pin_7};

void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;

for (int i = 0; i < 4; i++) {
GPIO_InitStruct.GPIO_Pin = LED_PIN[i];
GPIO_Init(LED_PORT[i], &GPIO_InitStruct);
GPIO_SetBits(LED_PORT[i], LED_PIN[i]); /* 默认关闭(高电平灭) */
}
}

void LED_On(uint8_t n) { GPIO_ResetBits(LED_PORT[n-1], LED_PIN[n-1]); }
void LED_Off(uint8_t n) { GPIO_SetBits(LED_PORT[n-1], LED_PIN[n-1]); }
void LED_Toggle(uint8_t n) { LED_PORT[n-1]->ODR ^= LED_PIN[n-1]; }

5.3 main.c 整合

/* app/main.c */

#include "board.h"
#include "bsp_uart.h"
#include "bsp_led.h"

int main(void)
{
Board_Init(); /* SysTick + 时钟初始化 */
LED_Init(); /* 初始化 4 个 LED */
UART1_Init(115200);/* 调试口,接收帧数据 */

printf("WXMP-IOT Ready. Waiting for frames...\r\n");

while (1) {
/* 帧解析在 USART1_IRQHandler 中异步处理,主循环保持空闲 */
Delay_ms(500);
}
}

六、调试验证

6.1 用串口助手发送测试帧

打开串口助手(如 SSCOM / 友善串口),选择正确 COM 口,波特率 115200,发送十六进制帧:

测试用例发送帧(HEX)预期现象
LED1 亮AA 05 11 FF BFLED1 亮起
LED1 灭AA 05 11 00 C0LED1 熄灭
全部亮AA 08 10 FF FF FF FF BF4 个 LED 全亮
全部灭AA 08 10 00 00 00 00 C24 个 LED 全灭
全部翻转AA 08 10 55 55 55 55 164 个 LED 状态取反
系统 PINGAA 05 00 00 AF串口打印 "PING received"

6.2 SUM 校验验证

以 "LED1亮" 帧为例:
帧内容:AA 05 11 FF
SUM = 0xAA + 0x05 + 0x11 + 0xFF = 0x1BF
取低8位 → 0xBF
完整帧:AA 05 11 FF BF ✓
快速验证 SUM

可以在 网站课程页校验和计算器中,输入 AA 05 11 FF 点击"计算",即可自动得出 BF


七、BLE AT 调试(DX-BT24)

DX-BT24 模块支持通过串口发送 AT 指令进行配置:

指令功能示例
AT+NAME=xxxx修改蓝牙广播名AT+NAME=IOT-BOARD
AT+BAUD=4修改波特率(4=9600)AT+BAUD=4
AT+RST重启模块AT+RST
AT+VERSION查询固件版本

调试步骤:

  1. DX-BT24 的 TX/RX 直接连接到 PC 的 USB 串口(不经过 STM32
  2. 串口助手发送 AT 指令(末尾需加 \r\n
  3. 模块回复 OK 表示成功
  4. 修改名称后,手机蓝牙列表中设备名即更新

八、验收标准

完成 Day 2 学习后,能够独立完成以下操作:

  • 使用 VibeCoding 生成一个包含 BLE 连接的微信小程序页面框架
  • 手工计算任意控制帧的 SUM 校验值
  • 用串口助手发送十六进制帧,控制 STM32 上的 LED 亮灭
  • 在 Keil 中阅读并运行帧解析状态机代码,理解状态转移逻辑
  • 修改 DX-BT24 蓝牙广播名称

常见问题

Q: 发送帧后 LED 没有反应?
A: 检查以下几点:① SUM 是否计算正确;② 串口助手是否选择了"HEX 发送"模式(不要发 ASCII 字符);③ UART 波特率是否匹配(115200)。

Q: 串口有输出但 SUM ERR?
A: 重新计算 SUM,常见错误是漏算包头 0xAA 或包头后的长度字节 N

Q: VibeCoding 生成的代码能直接用吗?
A: 作为代码框架可以直接用,但蓝牙 UUID、数据帧格式需要根据实际硬件调整。AI 生成的代码一定要在真实设备上验证。

Q: DX-BT24 AT 指令没有回应?
A: 确认模块处于命令模式(未与手机连接时),部分版本需要在发送前先断开已有 BLE 连接。波特率默认为 9600,确认串口助手设置正确。

📲 扫码联系
微信二维码微信咨询
关注公众号关注公众号