【瑞萨BLE/WIFI模块测评】无线家居智能监测
本帖最后由 eefocus_3914144 于 2026-9-11 16:42 编辑# 基于RA4M2的无线家居智能监测
> 此次提供了RA4M2开发板一块,蓝牙开发板DA14531一块,wifi开发板DA16200一块。我将使用DA14531获取家里的小米温湿度计的信息,通过DA16200连接到家里的无线路由器,通过MQTT实现家里的温度湿度智能监测。
## 1. 开发板介绍
### 1.1 RA-Eco-RA4M2-100PIN-V2

**型号** R7FA4M2AD3CFP,LQFP-100,Cortex-M33,512KB Flash / 128KB RAM。
### 1.2 DA14531蓝牙开发板

### 1.3 DA16200 wifi开发板

### 1.4 总览
!(https://www.eefocus.com/forum/data/attachment/forum/202609/11/151550famtmzci5zaselxp.png)
## 2. 开发环境
### 2.1 操作系统
* Zephyr RTOS 4.4.0
* Zephyr SDK 1.0.1
### 2.2 调试下载
Jlink Ultra +
!(https://www.eefocus.com/forum/data/attachment/forum/202609/11/151904h920nwx9mmis2vm4.jpg)
## 3. 已发布的文章
由于我是阶段开发,因此前面已经将环境、OLED、蓝牙的部分做了详细的分享
* [【瑞萨BLE/WIFI模块测评】Zephyr 驱动OLED - 瑞萨电子 - 与非网](https://www.eefocus.com/forum/thread-236382-1-1.html)
* [【瑞萨BLE/WIFI模块测评】Zephyr 驱动OLED - 瑞萨电子 - 与非网](https://www.eefocus.com/forum/thread-236382-1-1.html)
* [【瑞萨BLE/WIFI模块测评】小米温湿度计接入 RA4M2 全记录 - 瑞萨电子 - 与非网](https://www.eefocus.com/forum/thread-236389-1-1.html)
* (https://www.eefocus.com/forum/thread-236396-1-1.html)
## 4. 程序框架
!(https://www.eefocus.com/forum/data/attachment/forum/202609/11/161014bodi0i80swrlz9ww.png)
## 5. 主要的技术要点
此次我的项目可以说是一个大工程,集成了zephyr rtos 、BLE、WIFI、MQTT、显示等三个大环路。按子系统梳理如下:
### 5.1 外置 HCI 蓝牙控制器(DA14531)—— 全项目最难的一环
* **MCU 无蓝牙射频**,蓝牙能力完全靠 DA14531 模块刷 2-wire UART HCI 固件后充当 Zephyr BLE host 的外置控制器(`<span>renesas,bt-hci-da1453x</span>`,SCI0 P410/P411 @115200,无流控)。
* **固件要自行裁剪编译**(SDK6 + e2studio headless):必须 `<span>ARCH_SLEEP_OFF</span>`、关闭 `<span>CFG_TRNG</span>` —— 空白 OTP 没有出厂 trim 数据,否则 `<span>TRNG_ACQUIRE</span>` 直接断言。
* **烧录格式坑**:ROM booter 要的是 8 字节头(`<span>70 50 00 00 00 00</span>` + 大端长度)+ raw bin,而不是 AN-B-001 文档里的 64 字节二级引导头。
* **自研烧录工具** `<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">tools/spi_flash_program.py</button>`:J-Link SWD 寄存器级操作,ClrRESET 让芯片停在 POR 保证连接可靠,先发 0xAB 唤醒 P25Q40 flash 再页编程。
### 5.2. BLE 扫描与 MiBeacon 解密
* **线程解耦**:扫描回调运行在中断/BT RX 线程上下文,栈有限,而 AES-CCM 解密栈消耗大 —— 用 `<span>k_mem_slab</span>`(4 块帧池)+ `<span>k_fifo</span>` 把广播帧转交主线程处理,回调里只做解析和投递(`<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">src/main.c</button><span>:90-120</span>`)。
* **MAC 白名单过滤**:环境里有邻居的 0xFE95 设备,必须按目标 MAC(A4:C1:38:99:9A:07)过滤,否则日志和显示被污染- **MiBeacon V4 协议 + PSA 加密**:帧解析、bindkey 导入 PSA、AES-CCM-128 解密(AAD=0x11,tag=4,nonce = MAC+PID+帧计数+扩展计数器),从 STM32L5 工程移植(`<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">src/mibeacon.c</button>`)。
* **数据分帧合并**:小米把温度/湿度/电量分散在不同事件帧里广播,需要缓存 `<span>last</span>` 跨帧合并,OLED 显示和 MQTT 应答都依赖这份。
### 5.3. OLED 显示(SSD1306)
* 走 RA 的 SCI3 simple-I2C 控制器(`<span>renesas,ra-i2c-sci</span>`,P408/P409),不是原生 I2C 外设。
* **驱动坑**:该 I2C 驱动对零长度写会挂死,总线扫描必须改用 1 字节读(`<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">src/main.c</button><span>:51-65</span>`)。
* 自研可复用驱动模块 `<span>oled_ssd1306.c</span>`,提供 init/clear/print/flush 的 4 行文本接口。
### 5.4. WiFi + MQTT(DA16200)
* **AT 指令并发管理**:主线程(发布)和 conn-mgr 线程(连接管理)都会发 AT,所有事务用 `<span>at_mutex</span>` 串行化(`<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">src/drivers/da16200.c</button><span>:48</span>`)。
* **非请求响应处理**:UART 接收进 ring buffer,每次排空时做行,捕捉 `<span>+NWMQMSG:<payload>,<topic>,<len></span>` 实现订阅消息的异步接收。
* **连接状态机**:conn-mgr 线程独占模块生命周期 —— 复位、AT 握手、加 AP、起 MQTT(**必须先 `<span>AT+NWMQTS</span>` 订阅再 `<span>AT+NWMQCL</span>` 连接**)、轮询链路状态、掉线自动重连。
* **查询-应答模式**: `<span>sensor/ra4m2/query</span>` 消息 → 把缓存的小米读数组成 JSON 发布到 `<span>sensor/ra4m2/temperature</span>`(已端到端验证)。
* 模块 RESETn(P403)/RTC\_WAKE\_UP(P402用自定义 dts binding(`<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">dts/bindings/wifi-da16200.yaml</button>`)管理。
### 5.5. 系统集成与工程层面
* **三个执行上下文**:BT RX 线程、main 主线程、da16200 conn-mgr 线程,靠 FIFO/互斥锁/状态标志协作,没有共享数据裸奔。
* **第三方板卡适配**:RA-Eco-RA4M2 与官方 EK-RA4M2 的 LED 引脚不同,overlay 定义;24MHz 晶振确认后直接沿用官方时钟配置。
* **调试手段**:控制台走 SEGGER RTT(J-Link),UART 全部留给模组;还有 PC 端测试工具 `<button type="button" class="hover:text-zinc-900 dark:hover:text-white hover:underline cursor-pointer break-all text-left">tools/mqtt_query_test.py</button>`。
* **构建**:Zephyr 4.4 + SDK 1.0.1,west workspace 共享 `<span>build/</span>` 目录,切换应用必须 `<span>-p</span>` pristine。
## 6. 工程代码简介
> 本次工程大约1100行 我分别介绍如下:
### 6.1 prj.con
```
CONFIG_GPIO=y
CONFIG_I2C=y
CONFIG_TICKLESS_KERNEL=y
# DA16200 WiFi 模块(UART AT 指令,中断驱动接收)
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# 蓝牙 host(外接 DA14531 HCI 控制器,observer 角色)
CONFIG_BT=y
CONFIG_BT_OBSERVER=y
CONFIG_BT_DEVICE_NAME="ra4m2-ble"
# AES-CCM(PSA crypto)用于 MiBeacon bindkey 解密
CONFIG_PSA_WANT_ALG_CCM=y
CONFIG_PSA_WANT_KEY_TYPE_AES=y
CONFIG_MAIN_STACK_SIZE=8192
CONFIG_HEAP_MEM_POOL_SIZE=2048
# RTT 调试输出(经 J-Link SWD,不占串口;SCI0 留给 BLE 模块)
CONFIG_PRINTK=y
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=n
```
配置文件,我们按模块进行加载,特别是如何加裁wifi ble host RTT调试等。
### 6.2 板级设备树
我是使用ek_ra4m2官方开发板进行开发板,由于与官方的一些不同,我在ek_ra4m2.overlay中按照原理图进行了定义,这一块最花时间的。
```
/*
* RA-Eco-RA4M2-100PIN(第三方板)适配。
*
* 时钟:本板已焊 24MHz 主晶振,直接用官方 ek_ra4m2.dts 的默认时钟配置
* (xtal 24MHz -> PLL -> ICLK 100MHz),无需改动。
* (注意:同系列 RA-Eco-RA4L1 等板子未贴晶振,那种情况需关闭 xtal/PLL
* 改 HOCO,参考 app/RA4L1_led/boards/ek_ra4l1.overlay。)
*
* LED 引脚与官方 EK-RA4M2 不同,按 RA-Eco 板原理图重定义:
* LED1=P405, LED2=P404, LED3=P002,均高电平点亮。
* 官方板 led1=P415、led2=P404、led3=P405,先删再建,alias led0..2 不变。
*/
#include <zephyr/dt-bindings/gpio/gpio.h>
#include <zephyr/dt-bindings/i2c/i2c.h>
#include <zephyr/dt-bindings/pinctrl/renesas/pinctrl-ra.h>
/* I2C 插座(SCI3 简易 I2C):SCL3=P408, SDA3=P409,板载 1.5K 上拉,
* 挂 SSD1306 OLED(0x3C)。驱动:i2c_renesas_ra_sci(FSP r_sci_i2c)。 */
&pinctrl {
sci3_i2c_default: sci3_i2c_default {
group1 {
/* SCL3 SDA3 */
psels = <RA_PSEL(RA_PSEL_SCI_3, 4, 8)>,
<RA_PSEL(RA_PSEL_SCI_3, 4, 9)>;
drive-open-drain;
};
};
};
&sci3 {
pinctrl-0 = <&sci3_i2c_default>;
pinctrl-names = "default";
status = "okay";
i2c {
clock-frequency = <I2C_BITRATE_FAST>;
status = "okay";
};
};
/ {
aliases {
oled-i2c = &{/soc/sci3@40118300/i2c};
};
chosen {
zephyr,bt-hci = &bt_hci_uart;
};
};
/* DA14531 HCI 控制器:SCI0(P410 RX / P411 TX),115200 8N1,无流控。
* 模块复位脚悬空、上电自启,renesas,bt-hci-da1453x 的 reset-gpios 可选,不接。 */
&uart0 {
bt_hci_uart: bt_hci_uart {
compatible = "zephyr,bt-hci-uart";
status = "okay";
da1453x {
compatible = "renesas,bt-hci-da1453x";
status = "okay";
};
};
};
/delete-node/ &led1;
/delete-node/ &led2;
/delete-node/ &led3;
/ {
leds {
led1: led1 {
gpios = <&ioport4 5 GPIO_ACTIVE_HIGH>;
label = "LED1";
};
led2: led2 {
gpios = <&ioport4 4 GPIO_ACTIVE_HIGH>;
label = "LED2";
};
led3: led3 {
gpios = <&ioport0 2 GPIO_ACTIVE_HIGH>;
label = "LED3";
};
};
};
&ioport0 {
status = "okay";
};
&ioport2 {
status = "okay";
};
&ioport4 {
status = "okay";
};
/* DA16200 WiFi 模块插 PMOD1(SCI4):pin2 P205(TXD4)-> 模块 RXD,
* pin3 P206(RXD4)<- 模块 TXD,115200 8N1,无流控。
* 控制脚用杜邦线:pin8 P403 -> 模块 RESETn(低有效),
* pin9 P402 -> 模块 RTC_WAKE_UP(高有效脉冲)。 */
&pinctrl {
sci4_default: sci4_default {
group1 {
/* TXD4 RXD4 */
psels = <RA_PSEL(RA_PSEL_SCI_4, 2, 5)>,
<RA_PSEL(RA_PSEL_SCI_4, 2, 6)>;
};
};
};
&sci4 {
pinctrl-0 = <&sci4_default>;
pinctrl-names = "default";
status = "okay";
uart4: uart {
current-speed = <115200>;
status = "okay";
};
};
/ {
wifi_da16200: wifi-da16200 {
compatible = "renesas,da16200";
/* PMOD1 pin8P403 -> module RESETn (active low) */
reset-gpios = <&ioport4 3 GPIO_ACTIVE_LOW>;
/* PMOD1 pin9P402 -> module RTC_WAKE_UP (active high pulse) */
wakeup-gpios = <&ioport4 2 GPIO_ACTIVE_HIGH>;
};
};
```
### 6.3 da16200 绑定文件
为了配置wifi模块的复位、中断、唤醒,我在项目dts\bindings下面创建了wif-da16200.yaml 规定的PMOD1的规则
```
# SPDX-License-Identifier: Apache-2.0
description: DA16200 WiFi module control GPIOs (jumper-wired to PMOD1)
compatible: "renesas,da16200"
properties:
reset-gpios:
type: phandle-array
required: true
description: MCU -> module RESETn (active low)
wakeup-gpios:
type: phandle-array
required: true
description: MCU -> module RTC_WAKE_UP (active high pulse)
irq-gpios:
type: phandle-array
required: false
description: module INTn -> MCU (active low, input only)
```
### 6.4 oled显示
由于zephyr的display在驱动ssd1306上有问题,因此我使用i2c_write来直接写
```
/* ctrl: 0x00=命令,0x40=显存数据 */
static int oled_write(uint8_t ctrl, const uint8_t *data, size_t len)
{
uint8_t buf;
if (len > OLED_WIDTH) {
return -EINVAL;
}
buf = ctrl;
memcpy(&buf, data, len);
return i2c_write(i2c_bus, buf, len + 1, SSD1306_ADDR);
}
```
### 6.5 wifi驱动
我在src/drivers下面创建da16200.c,来实现AT命令的打包与解包,这是由官方提供的代码,修改为适配zephyr而来的。
```
/* SPDX-License-Identifier: Apache-2.0 */
/*
* Minimal DA16200 AT-command driver (UART host interface, no flow control).
*
* Ported from the RA6M4 project (app ra6m4_sht30_da16200), which in turn
* ported the AT sequence, response matching and retry/timeout policy from
* the Renesas FSP example RA6M4_DA16200_MQTT_CLIENT (da16200_AT.c). The RX
* path uses the Zephyr UART interrupt-driven API (ISR -> ring buffer).
*
* RA4M2 differences vs the RA6M4 original:
* - UART moved to sci4/uart4 (PMOD1: P205 TXD4 / P206 RXD4 @115200).
* - SNTP time sync dropped (not needed here).
* - MQTT subscription added: AT+NWMQTS before AT+NWMQCL, and every byte
* pulled from the RX ring buffer is passed through a line scanner that
* catches the unsolicited +NWMQMSG:<payload>,<topic>,<len> response.
* - All UART transactions are serialized with a mutex: the connection
* manager thread and the application thread (publish) share the module.
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <stdio.h>
#include <string.h>
#include "da16200.h"
#define DA16200_UART_NODEDT_NODELABEL(uart4) /* sci4: P206 RX / P205 TX @115200 */
#define DA16200_GPIO_NODEDT_NODELABEL(wifi_da16200)
#define RX_RB_SIZE 512
#define RESP_SIZE 256
static const struct device *uart_dev = DEVICE_DT_GET(DA16200_UART_NODE);
static const struct gpio_dt_spec reset_gpio =
GPIO_DT_SPEC_GET(DA16200_GPIO_NODE, reset_gpios);
static const struct gpio_dt_spec wakeup_gpio =
GPIO_DT_SPEC_GET(DA16200_GPIO_NODE, wakeup_gpios);
RING_BUF_DECLARE(rx_rb, RX_RB_SIZE);
/* Serializes every UART transaction (send_cmd / link_check / RX polling):
* the connection manager thread and the application thread both talk to
* the module, and the ring buffer plus resp buffer are not thread-safe. */
static K_MUTEX_DEFINE(at_mutex);
/* Last response, kept so da16200_connect_ap() can parse the IP address. */
static char resp;
/* --------------------------------------------------------------------------
* Unsolicited +NWMQMSG scanning
*
* Every byte taken out of the RX ring buffer (by read_resp, rx_flush or
* the idle poll) is fed through scan_bytes(), which assembles lines and
* catches "+NWMQMSG:<payload>,<topic>,<len>". Only one message is kept;
* the application polls it with da16200_mqtt_incoming().
* ------------------------------------------------------------------------ */
static char scan_line;
static size_t scan_line_len;
static volatile bool incoming_pending;
static char incoming_payload;
static void scan_line_process(const char *line, size_t len)
{
static const char prefix[] = "+NWMQMSG:";
const char *p, *end, *c1, *c2, *q;
size_t plen;
if (len <= sizeof(prefix) - 1 ||
strncmp(line, prefix, sizeof(prefix) - 1) != 0) {
return;
}
p = line + sizeof(prefix) - 1;
end = line + len;
/* The payload itself may contain commas; topic and length are the
* last two comma-separated fields. */
c1 = NULL;
c2 = NULL;
for (q = end; q > p; q--) {
if (q[-1] == ',') {
if (c2 == NULL) {
c2 = q - 1;
} else {
c1 = q - 1;
break;
}
}
}
if (c1 == NULL || c2 == NULL) {
return;
}
plen = c1 - p;
if (plen >= sizeof(incoming_payload)) {
plen = sizeof(incoming_payload) - 1;
}
memcpy(incoming_payload, p, plen);
incoming_payload = '\0';
incoming_pending = true;
}
static void scan_bytes(const uint8_t *data, size_t len)
{
for (size_t i = 0; i < len; i++) {
char c = (char)data;
if (c == '\r') {
continue;
}
if (c == '\n') {
if (scan_line_len > 0) {
scan_line_process(scan_line, scan_line_len);
}
scan_line_len = 0;
continue;
}
if (scan_line_len < sizeof(scan_line) - 1) {
scan_line = c;
} else {
scan_line_len = 0; /* overlong line: drop it */
}
}
}
bool da16200_mqtt_incoming(char *payload, size_t cap)
{
bool pending;
k_mutex_lock(&at_mutex, K_FOREVER);
pending = incoming_pending;
if (pending) {
incoming_pending = false;
if (payload != NULL && cap > 0) {
strncpy(payload, incoming_payload, cap - 1);
payload = '\0';
}
}
k_mutex_unlock(&at_mutex);
return pending;
}
/* --------------------------------------------------------------------------
* UART low level
* ------------------------------------------------------------------------ */
static void uart_isr(const struct device *dev, void *user_data)
{
ARG_UNUSED(user_data);
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
uint8_t buf;
int len = uart_fifo_read(dev, buf, sizeof(buf));
if (len > 0) {
ring_buf_put(&rx_rb, buf, len);
}
}
}
}
static void rx_flush(void)
{
uint8_t tmp;
uint32_t got;
while ((got = ring_buf_get(&rx_rb, tmp, sizeof(tmp))) > 0) {
scan_bytes(tmp, got); /* don't lose a +NWMQMSG in the flush */
}
}
/* Collect a response into buf. Returns as soon as both expected substrings
* have been seen (early exit keeps fast commands fast), or when the timeout
* expires. expect2 may be NULL. */
static size_t read_resp(char *buf, size_t cap, uint32_t timeout_ms,
const char *expect1, const char *expect2)
{
size_t len = 0;
int64_t deadline = k_uptime_get() + timeout_ms;
buf = '\0';
while (len < cap - 1) {
uint32_t got = ring_buf_get(&rx_rb, (uint8_t *)buf + len, cap - 1 - len);
if (got > 0) {
scan_bytes((uint8_t *)buf + len, got);
}
len += got;
buf = '\0';
if ((expect1 == NULL || strstr(buf, expect1) != NULL) &&
(expect2 == NULL || strstr(buf, expect2) != NULL)) {
break;
}
if (k_uptime_get() >= deadline) {
break;
}
if (got == 0) {
k_msleep(2);
}
}
buf = '\0';
return len;
}
/*
* Send one AT command and wait for the expected response substring(s).
* Ported from the FSP AT_cmd_send_ok / AT_cmd_send_data pair: up to
* `retries` attempts, `timeout_ms` of RX collection per attempt and
* `retry_delay_ms` between attempts. expect2 may be NULL.
*/
static int send_cmd(const char *cmd, const char *expect1, const char *expect2,
uint32_t timeout_ms, int retries, uint32_t retry_delay_ms)
{
int rc = -ETIMEDOUT;
k_mutex_lock(&at_mutex, K_FOREVER);
for (int i = 0; i < retries; i++) {
rx_flush();
for (const char *p = cmd; *p != '\0'; p++) {
uart_poll_out(uart_dev, *p);
}
read_resp(resp, sizeof(resp), timeout_ms, expect1, expect2);
bool ok1 = (expect1 == NULL) || (strstr(resp, expect1) != NULL);
bool ok2 = (expect2 == NULL) || (strstr(resp, expect2) != NULL);
if (ok1 && ok2) {
rc = 0;
break;
}
k_msleep(retry_delay_ms);
}
k_mutex_unlock(&at_mutex);
if (rc < 0) {
printf("da16200: no match for %s", cmd);
printf("da16200: got: %s\n", resp);
}
return rc;
}
static int da16200_init(void)
{
if (!device_is_ready(uart_dev)) {
printf("da16200: uart4 not ready\n");
return -ENODEV;
}
if (!device_is_ready(reset_gpio.port) || !device_is_ready(wakeup_gpio.port)) {
printf("da16200: control GPIOs not ready\n");
return -ENODEV;
}
/* Wake + hardware reset. The module auto-enters DPM sleep (UART off)
* when it has nothing to do; RTC_WAKE_UP pulls it out of sleep, and
* the RESETn pulse then guarantees a clean boot from any state. */
gpio_pin_configure_dt(&wakeup_gpio, GPIO_OUTPUT_ACTIVE);
gpio_pin_configure_dt(&reset_gpio, GPIO_OUTPUT_ACTIVE);
k_msleep(200);
gpio_pin_set_dt(&reset_gpio, 0); /* release reset */
k_msleep(50);
gpio_pin_set_dt(&wakeup_gpio, 0);/* end wake-up pulse */
k_msleep(1500);
uart_irq_callback_set(uart_dev, uart_isr);
uart_irq_rx_enable(uart_dev);
/* Give the boot banner (+INIT:DONE,0) time to arrive, then drop it. */
k_msleep(500);
rx_flush();
/* Handshake with plain "AT" - NOT "ATZ": ATZ reboots the module, and
* any command sent during the reboot window arrives as garbled bytes
* (module answers ERROR:-1). The hardware reset above already gave us
* a clean boot. */
int rc = send_cmd("AT\r\n", "OK", NULL, 500, 10, 500);
if (rc < 0) {
return rc;
}
/* Echo toggle. DA16200 only accepts bare "ATE" (ATE0/ATE1 are rejected
* with ERROR:-1), and failure is harmless anyway: response parsing
* uses strstr, so the echo state does not matter. */
(void)send_cmd("ATE\r\n", "OK", NULL, 500, 3, 200);
return 0;
}
static int da16200_connect_ap(const char *ssid, const char *pw, char *ip, size_t ip_len)
{
char cmd;
int rc;
/* Clear NVRAM profiles; module reboots into Soft-AP default mode. */
rc = send_cmd("AT+DEFAP\r\n", "OK", "+INIT:DONE,1", 4000, 5, 200);
if (rc < 0) {
return rc;
}
/* Country code step removed: this module's firmware rejects codes
* outside its regulatory table ("CN" -> ERROR:-4), and the default
* covers 2.4 GHz channels 1-11 anyway. */
rc = send_cmd("AT+WFMODE=0\r\n", "OK", NULL, 200, 5, 200); /* station mode */
if (rc < 0) {
return rc;
}
rc = send_cmd("AT+RESTART\r\n", "OK", "+INIT:DONE,0", 3000, 5, 1000);
if (rc < 0) {
return rc;
}
/* Restart turns echo back on; toggle it off again (non-fatal). */
(void)send_cmd("ATE\r\n", "OK", NULL, 200, 3, 200);
/* DPM keepalive every 5 s (default is 30 s): the module wakes more
* often to keep the AP association alive, so the AP does not age out
* the sleeping station. Non-fatal if the firmware rejects it. */
if (send_cmd("AT+DPMKA=500000\r\n", "OK", NULL, 500, 3, 200) < 0) {
printf("da16200: DPMKA not accepted, continue anyway\n");
}
rc = send_cmd("AT+NWDHC=1\r\n", "OK", NULL, 200, 5, 200); /* DHCP client */
if (rc < 0) {
return rc;
}
snprintf(cmd, sizeof(cmd), "AT+WFJAPA='%s','%s'\r\n", ssid, pw);
rc = send_cmd(cmd, "OK", "+WFJAP:1", 4000, 5, 1000);
if (rc < 0) {
return rc;
}
/* Query the assigned IP via AT+NWIP=? -> +NWIP:0,<ip>,<netmask>,<gw>.
* The +WFJAP reply format varies between firmware builds (this module
* answers +WFJAP:'<ssid>',<sec>,<enc>,'<key>' with no IP field), so
* never parse the IP from the join response. */
if (ip != NULL && ip_len > 0) {
ip = '\0';
if (send_cmd("AT+NWIP=?\r\n", "+NWIP:", "OK", 500, 3, 200) == 0) {
const char *p = strstr(resp, "+NWIP:");
if (p != NULL) {
p = strchr(p, ','); /* skip the interface id */
}
if (p != NULL) {
p++;
size_t n = strcspn(p, ",\r\n");
if (n >= ip_len) {
n = ip_len - 1;
}
memcpy(ip, p, n);
ip = '\0';
}
}
}
return 0;
}
/* --------------------------------------------------------------------------
* Connection manager thread
*
* State machine: while offline -> reset + handshake + join AP (retry with
* backoff); while online -> poll the link state and drop back to offline
* on loss. The application only reads the state via da16200_is_connected().
* ------------------------------------------------------------------------ */
#define CHECK_PERIOD_MS 5000 /* link-state poll interval when online */
#define POLL_SLICE_MS 100 /* unsolicited-message poll slice when online */
#define RETRY_DELAY_MS 10000/* backoff between join attempts when offline */
static volatile bool link_up;
static volatile bool mqtt_ready;
bool da16200_is_connected(void)
{
return link_up;
}
/* Link check: AT+WFJAP=? answers with a +WFJAP: line. The connected format
* varies by firmware: the manual says +WFJAP:1,'<ssid>',<ip>, this module
* answers +WFJAP:'<ssid>',<sec>,<enc>,'<key>'. The only reliable common
* rule is: +WFJAP: present and NOT +WFJAP:0 (disconnected). Two attempts
* so a single transient UART glitch doesn't drop the link. */
static bool link_check(void)
{
static const char query[] = "AT+WFJAP=?\r\n";
bool up = false;
k_mutex_lock(&at_mutex, K_FOREVER);
for (int i = 0; i < 2; i++) {
rx_flush();
for (const char *p = query; *p != '\0'; p++) {
uart_poll_out(uart_dev, *p);
}
read_resp(resp, sizeof(resp), 1000, "OK", NULL);
if (strstr(resp, "+WFJAP:") != NULL &&
strstr(resp, "+WFJAP:0") == NULL) {
up = true;
break;
}
k_msleep(200);
}
k_mutex_unlock(&at_mutex);
if (!up) {
printf("da16200: link check failed, got: %s\n", resp);
}
return up;
}
/* Bring up the MQTT client on the module: broker address, QoS 0, login,
* client ID, subscription topic, then enable. Mirrors the FSP example's
* mqtt_con_routine() plus AT+NWMQTS from UM-WI-003 9.1.1.
* MQTT config is not auto-started by the module, so this runs after every
* (re)connect. */
static int mqtt_setup(void)
{
char cmd;
int rc;
snprintf(cmd, sizeof(cmd), "AT+NWMQBR=%s,%s\r\n",
DA16200_MQTT_BROKER, DA16200_MQTT_PORT);
rc = send_cmd(cmd, "OK", NULL, 500, 3, 200);
if (rc < 0) {
return rc;
}
rc = send_cmd("AT+NWMQQOS=0\r\n", "OK", NULL, 200, 3, 200);
if (rc < 0) {
return rc;
}
snprintf(cmd, sizeof(cmd), "AT+NWMQLI=%s,%s\r\n",
DA16200_MQTT_USER, DA16200_MQTT_PW);
rc = send_cmd(cmd, "OK", NULL, 200, 3, 200);
if (rc < 0) {
return rc;
}
snprintf(cmd, sizeof(cmd), "AT+NWMQCID=%s\r\n", DA16200_MQTT_CLIENT_ID);
rc = send_cmd(cmd, "OK", NULL, 200, 3, 200);
if (rc < 0) {
return rc;
}
/* Subscribe BEFORE enabling the client: the topic list is read by
* the module when AT+NWMQCL=1 opens the broker connection. */
snprintf(cmd, sizeof(cmd), "AT+NWMQTS=1,%s\r\n", DA16200_MQTT_SUB_TOPIC);
rc = send_cmd(cmd, "OK", NULL, 500, 3, 200);
if (rc < 0) {
return rc;
}
/* Connecting to the broker can take a moment - allow longer waits. */
return send_cmd("AT+NWMQCL=1\r\n", "OK", NULL, 1000, 5, 500);
}
int da16200_mqtt_publish(const char *topic, const char *payload)
{
if (!link_up || !mqtt_ready) {
return -ENOTCONN;
}
char cmd;
/* JSON payloads contain commas/double quotes: the AT spec requires
* the message to be wrapped in single quotes. */
snprintf(cmd, sizeof(cmd), "AT+NWMQMSG='%s',%s\r\n", payload, topic);
return send_cmd(cmd, "OK", NULL, 1000, 2, 200);
}
static void conn_mgr_thread(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
char ip;
for (;;) {
if (!link_up) {
if (da16200_init() == 0 &&
da16200_connect_ap(DA16200_WIFI_SSID, DA16200_WIFI_PW,
ip, sizeof(ip)) == 0) {
link_up = true;
printf("da16200: connected, IP %s\n", ip);
if (mqtt_setup() == 0) {
mqtt_ready = true;
printf("da16200: MQTT ready (sub %s)\n",
DA16200_MQTT_SUB_TOPIC);
} else {
/* Retry the whole cycle (with backoff). */
printf("da16200: MQTT setup failed\n");
link_up = false;
}
} else {
printf("da16200: offline, retry in %d s\n",
RETRY_DELAY_MS / 1000);
k_msleep(RETRY_DELAY_MS);
}
} else {
/* Sleep in slices and drain the RX ring buffer in
* between, so an unsolicited +NWMQMSG is seen within
* ~POLL_SLICE_MS instead of waiting for the next
* link_check(). */
for (int t = 0; t < CHECK_PERIOD_MS / POLL_SLICE_MS; t++) {
uint8_t tmp;
uint32_t got;
k_msleep(POLL_SLICE_MS);
k_mutex_lock(&at_mutex, K_FOREVER);
while ((got = ring_buf_get(&rx_rb, tmp,
sizeof(tmp))) > 0) {
scan_bytes(tmp, got);
}
k_mutex_unlock(&at_mutex);
}
if (!link_check()) {
link_up = false;
mqtt_ready = false;
printf("da16200: connection lost\n");
}
}
}
}
K_THREAD_DEFINE(da16200_mgr, 2048, conn_mgr_thread, NULL, NULL, NULL,
12, 0, -1);
int da16200_start(void)
{
k_thread_start(da16200_mgr);
return 0;
}
```
### 6.6 小米温湿度计解包
根据网上的资料,我编写了mibeacon.c用来解包
```
/*
* 小米 MiBeacon 帧解析 + AES-CCM 解密,接口见 mibeacon.h
*/
#include <errno.h>
#include <string.h>
#include <psa/crypto.h>
#include <zephyr/sys/printk.h>
#include "mibeacon.h"
#define MI_PAYLOAD_MAX 32
/* 小米 MiBeacon 事件对象 ID(小端) */
#define MI_EVT_TEMP 0x1004U /* s16, 0.1 ℃ */
#define MI_EVT_HUM 0x1006U /* u16, 0.1 % */
#define MI_EVT_BATT_PCT0x100AU /* u8, % */
#define MI_EVT_TEMP_HUM0x100DU /* s16 0.1℃ + u16 0.1% */
/* FC 首字节标志位 */
#define MI_FC_ENCRYPTED0x08U
#define MI_FC_CAPABILITY 0x20U
#define MI_FC_HAS_DATA 0x40U
#define MI_TAG_LEN 4U
#define MI_AAD 0x11U
/* 设备 bindkey:A4:C1:38:99:9A:07(TelinkMiFlasher 读取,
* 注意 flasher 页面每次"登录/激活"都会轮换密钥,轮换后必须更新这里) */
static const uint8_t mi_bindkey = {
0x93, 0xbb, 0x3b, 0x15, 0xd5, 0x90, 0x82, 0xe3,
0x34, 0xe4, 0x28, 0x32, 0x7f, 0x9a, 0x63, 0x69,
};
static psa_key_id_t mi_key_id;
int mibeacon_init(void)
{
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_status_t st;
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, 128);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attr,
PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM,
MI_TAG_LEN));
st = psa_import_key(&attr, mi_bindkey, sizeof(mi_bindkey), &mi_key_id);
if (st != PSA_SUCCESS) {
printk("mibeacon: bindkey import failed %d\n", st);
return (int)st;
}
return 0;
}
static void decode_events(const uint8_t *p, uint8_t len,
struct mi_sensor_data *sd)
{
uint8_t off = 0;
while (off + 3 <= len) {
uint16_t eid = (uint16_t)p | ((uint16_t)p << 8);
uint8_t elen = p;
off += 3;
if (off + elen > len) {
return;
}
switch (eid) {
case MI_EVT_TEMP:
if (elen == 2) {
sd->temp_x10 = (int16_t)((uint16_t)p |
((uint16_t)p << 8));
sd->has_temp = true;
}
break;
case MI_EVT_HUM:
if (elen == 2) {
sd->hum_x10 = (uint16_t)p |
((uint16_t)p << 8);
sd->has_hum = true;
}
break;
case MI_EVT_BATT_PCT:
if (elen == 1) {
sd->batt_pct = p;
sd->has_batt = true;
}
break;
case MI_EVT_TEMP_HUM:
if (elen == 4) {
sd->temp_x10 = (int16_t)((uint16_t)p |
((uint16_t)p << 8));
sd->hum_x10 = (uint16_t)p |
((uint16_t)p << 8);
sd->has_temp = true;
sd->has_hum = true;
}
break;
default:
break;
}
off = (uint8_t)(off + elen);
}
}
static int decrypt_frame(const uint8_t *payload, uint8_t payload_len,
struct mi_sensor_data *sd)
{
uint8_t enc_off = 11;
uint8_t ct_len;
uint8_t nonce;
uint8_t ct_tag;
uint8_t plain;
size_t plain_len = 0;
static const uint8_t aad = MI_AAD;
psa_status_t st;
if (payload & MI_FC_CAPABILITY) {
enc_off++;
}
if (payload_len < enc_off + MI_TAG_LEN + 3 + 1) {
return -EINVAL;
}
ct_len = (uint8_t)(payload_len - enc_off - 3 - MI_TAG_LEN);
/* nonce = MAC(广播序) + PID + frame_cnt + ext_counter */
memcpy(nonce, payload + 5, 6);
memcpy(nonce + 6, payload + 2, 2);
nonce = payload;
memcpy(nonce + 9, payload + payload_len - 7, 3);
/* PSA AEAD 输入为 ciphertext || tag */
memcpy(ct_tag, payload + enc_off, ct_len);
memcpy(ct_tag + ct_len, payload + payload_len - MI_TAG_LEN,
MI_TAG_LEN);
st = psa_aead_decrypt(mi_key_id,
PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM,
MI_TAG_LEN),
nonce, sizeof(nonce), &aad, sizeof(aad),
ct_tag, (size_t)ct_len + MI_TAG_LEN,
plain, sizeof(plain), &plain_len);
if (st != PSA_SUCCESS) {
return (int)st;
}
decode_events(plain, (uint8_t)plain_len, sd);
return 0;
}
int mibeacon_process(const uint8_t *payload, uint8_t len,
struct mi_sensor_data *sd)
{
uint8_t off = 11;
memset(sd, 0, sizeof(*sd));
if (len < 11) {
return -EINVAL;
}
if (!(payload & (MI_FC_ENCRYPTED | MI_FC_HAS_DATA))) {
return -ENODATA; /* 周期无数据信标帧,忽略 */
}
if (payload & MI_FC_ENCRYPTED) {
return decrypt_frame(payload, len, sd);
}
if (payload & MI_FC_CAPABILITY) {
off++;
}
if (off < len) {
decode_events(payload + off, (uint8_t)(len - off), sd);
}
return 0;
}
```
### 6.7 主程序
做为总的调度 main.c 主要实现各个功能模块的总装
```
/*
* RA-Eco-RA4M2-100PIN:BLE observer 读小米温湿度计(LYWSD03MMC,MiBeacon)
* - DA14531 HCI 控制器挂 SCI0(P410/P411,115200,无流控)
* - 被动扫描;MiBeacon(0xFE95)帧经 FIFO 交主线程解密(STACK 消耗大)
* - 温湿度/电量显示在 SSD1306 OLED(SCI3 I2C)上,LED1 心跳
* 调试输出走 SEGGER RTT(J-Link)
*/
#include <stdio.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/sys/byteorder.h>
#include "oled_ssd1306.h"
#include "mibeacon.h"
#include "drivers/da16200.h"
#define MI_SVC_UUID 0xFE95U
#define MI_PAYLOAD_MAX 32
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
struct mi_frame {
void *fifo_reserved;
int8_t rssi;
uint8_t len;
uint8_t payload;
};
static K_FIFO_DEFINE(mi_frame_fifo);
K_MEM_SLAB_DEFINE_STATIC(mi_frame_slab, sizeof(struct mi_frame), 4, 4);
struct mi_ctx {
bool found;
uint8_t len;
uint8_t payload;
};
static uint32_t adv_count;
static uint32_t mi_count;
/* 保留最近读数:小米把温/湿/电分散在不同事件帧里;MQTT 查询应答也用它 */
static struct mi_sensor_data last;
/* I2C 总线扫描:打印所有应答的 7 位地址(诊断用;注意该驱动不支持零长度写) */
static void i2c_scan(const struct device *bus)
{
uint8_t dummy;
bool found = false;
for (uint16_t addr = 0x08; addr < 0x78; addr++) {
if (i2c_read(bus, &dummy, 1, addr) == 0) {
printk("i2c scan: 0x%02x ACK\n", addr);
found = true;
}
}
if (!found) {
printk("i2c scan: no device found\n");
}
}
static bool parse_ad(struct bt_data *data, void *user_data)
{
struct mi_ctx *ctx = user_data;
uint16_t uuid;
uint8_t len;
if (data->type != BT_DATA_SVC_DATA16 || data->data_len < 3) {
return true;
}
uuid = sys_get_le16(data->data);
if (uuid != MI_SVC_UUID) {
return true;
}
len = (uint8_t)(data->data_len - 2);
if (len > sizeof(ctx->payload)) {
return true;
}
memcpy(ctx->payload, data->data + 2, len);
ctx->len = len;
ctx->found = true;
return true;
}
static void scan_cb(const bt_addr_le_t *addr, int8_t rssi, uint8_t adv_type,
struct net_buf_simple *buf)
{
struct mi_ctx ctx = {0};
struct mi_frame *frame;
adv_count++;
ARG_UNUSED(addr);
ARG_UNUSED(adv_type);
bt_data_parse(buf, parse_ad, &ctx);
if (!ctx.found) {
return;
}
mi_count++;
/* 只处理目标传感器,过滤环境中其它 0xFE95 设备 */
if (addr->a.val != 0xa4 || addr->a.val != 0xc1 ||
addr->a.val != 0x38 || addr->a.val != 0x99 ||
addr->a.val != 0x9a || addr->a.val != 0x07) {
return;
}
if (k_mem_slab_alloc(&mi_frame_slab, (void **)&frame, K_NO_WAIT) != 0) {
printk("frame pool exhausted, dropped\n");
return;
}
frame->rssi = rssi;
frame->len = ctx.len;
memcpy(frame->payload, ctx.payload, ctx.len);
k_fifo_put(&mi_frame_fifo, frame);
}
static void show_frame(const struct mi_frame *frame)
{
struct mi_sensor_data sd;
char line;
int rc = mibeacon_process(frame->payload, frame->len, &sd);
if (rc != 0) {
if (rc != -ENODATA) {
printk("mibeacon: process failed %d(wrong bindkey?)\n", rc);
}
return;
}
printk("PKT #%u RSSI=%d", frame->payload, frame->rssi);
int frac;
if (sd.has_temp) {
last.temp_x10 = sd.temp_x10;
last.has_temp = true;
printk(" TEMP=%d.%dC", sd.temp_x10 / 10,
sd.temp_x10 % 10 < 0 ? -(sd.temp_x10 % 10) : sd.temp_x10 % 10);
}
if (sd.has_hum) {
last.hum_x10 = sd.hum_x10;
last.has_hum = true;
printk(" HUM=%u.%u%%", sd.hum_x10 / 10, sd.hum_x10 % 10);
}
if (sd.has_batt) {
last.batt_pct = sd.batt_pct;
last.has_batt = true;
printk(" BATT=%u%%", sd.batt_pct);
}
printk("\n");
oled_clear();
oled_print(da16200_is_connected() ? "RA4M2 MiBeacon W" : "RA4M2 MiBeacon", 0, 0);
if (last.has_temp) {
frac = last.temp_x10 % 10;
if (frac < 0) {
frac = -frac;
}
snprintf(line, sizeof(line), "T: %d.%d C",
(int)(last.temp_x10 / 10), frac);
oled_print(line, 0, 1);
}
if (last.has_hum) {
snprintf(line, sizeof(line), "H: %u.%u %%",
(unsigned)(last.hum_x10 / 10), (unsigned)(last.hum_x10 % 10));
oled_print(line, 0, 2);
}
if (last.has_batt) {
snprintf(line, sizeof(line), "B:%u%% %ddBm #%u",
last.batt_pct, frame->rssi, frame->payload);
} else {
snprintf(line, sizeof(line), "%ddBm #%u",
frame->rssi, frame->payload);
}
oled_print(line, 0, 3);
oled_flush();
}
int main(void)
{
bool bt_ok = false;
printk("app start\n");
if (!gpio_is_ready_dt(&led)) {
printk("led device not ready\n");
return 0;
}
if (gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE) != 0) {
printk("led config failed\n");
return 0;
}
int ret = oled_init();
printk("oled init ret=%d\n", ret);
i2c_scan(oled_i2c_bus());
ret = mibeacon_init();
printk("mibeacon init ret=%d\n", ret);
/* 蓝牙初始化:模块没刷好 HCI 固件时会超时断言,属预期,先看 OLED */
ret = bt_enable(NULL);
if (ret != 0) {
printk("bt enable failed ret=%d(模块未就绪或未刷 HCI 固件)\n", ret);
} else {
struct bt_le_scan_param scan_param = {
.type = BT_LE_SCAN_TYPE_PASSIVE,
.options = BT_LE_SCAN_OPT_NONE,
.interval = BT_GAP_SCAN_FAST_INTERVAL,
.window = BT_GAP_SCAN_FAST_WINDOW,
};
bt_ok = true;
ret = bt_le_scan_start(&scan_param, scan_cb);
printk("scan start ret=%d\n", ret);
}
oled_clear();
oled_print("RA4M2 MiBeacon", 0, 0);
oled_print(bt_ok ? "scanning..." : "BT INIT FAIL", 0, 3);
oled_flush();
/* WiFi 后台任务:连 AP、起 MQTT(含订阅),掉线自动重连 */
da16200_start();
while (1) {
struct mi_frame *frame = k_fifo_get(&mi_frame_fifo, K_MSEC(10));
if (frame != NULL) {
show_frame(frame);
k_mem_slab_free(&mi_frame_slab, (void *)frame);
}
/* 收到订阅主题(查询)消息 -> 把当前温湿度发布出去 */
char query;
if (da16200_mqtt_incoming(query, sizeof(query))) {
printk("MQTT query: %s\n", query);
if (last.has_temp || last.has_hum || last.has_batt) {
char payload;
int len = 0;
/* 只发已收到的字段(小米把温/湿/电分散在不同帧里) */
len += snprintf(payload + len, sizeof(payload) - len, "{");
if (last.has_temp) {
int tfrac = last.temp_x10 % 10;
if (tfrac < 0) {
tfrac = -tfrac;
}
len += snprintf(payload + len, sizeof(payload) - len,
"\"temperature\":%d.%d",
(int)(last.temp_x10 / 10), tfrac);
}
if (last.has_hum) {
len += snprintf(payload + len, sizeof(payload) - len,
"%s\"humidity\":%u.%u",
last.has_temp ? "," : "",
(unsigned)(last.hum_x10 / 10),
(unsigned)(last.hum_x10 % 10));
}
if (last.has_batt) {
len += snprintf(payload + len, sizeof(payload) - len,
"%s\"battery\":%u",
(last.has_temp || last.has_hum) ? "," : "",
last.batt_pct);
}
snprintf(payload + len, sizeof(payload) - len, "}");
int rc = da16200_mqtt_publish(DA16200_MQTT_TOPIC, payload);
printk("MQTT publish %s rc=%d\n", payload, rc);
} else {
printk("MQTT skip: no sensor data yet\n");
}
}
gpio_pin_toggle_dt(&led);
}
return 0;
}
```
## 工程总结
经过一个星期的工作,终于是把整个工程给完成了,这里主要是我用zephyr 可以轻松的把我原来在ra6m4上的oled、da16200的工程给移植过来。因此体会到了zephyr rtos的功能强大,相比使用e2studio或者keil,不需要进行复杂的fsp工程配置,依据官方开发板ek-ra4m2的工程包进行少量人修改即可以快速驱动外设。
当然这次试用,官方提供了驱动资料包,找资料也非常的方便。再次感谢与非网与瑞萨电子!
页:
[1]