项目介绍
本项目参与 Funpack 3-1 活动,使用 Silicon Labs 的 EFR32xG24 Explorer Kit: XG24-EK2703A 开发板与 OLED 显示屏、DHT11 传感器等外设,实现了基于 FreeRTOS 的具有环境监测与警报(主机温度、环境温湿度)的电子相框. 透过 MCU 支持的 Bluetooth LE 蓝牙低功耗协议栈与 Flutter 编写的 Android 应用进行通信,实现了绘制实时数据折线、配置警报阈值、上传电子相框图像的上位机.
硬件介绍
EFR32xG24 Explorer Kit: XG24-EK2703A 是一款基于 EFR32MG24 SoC 的小型开发和评估平台 [1]. 该套件专注于 2.4 GHz 无线协议,包括 Bluetooth LE、Bluetooth Mesh、Zigbee、Thread 和 Matter,用于快速原型设计和概念验证的物联网应用.
该开发板携带USB Type-C 端口,可连接板载 SEGGER J-Link 调试器、虚拟串口、PTI 调试接口. 除此以外,板上还配有 LED 与按键,并预留了 mikroBUS 插槽 和 Qwiic 的扩展口,具体的资源外设布局如下图所示.
准备工作
SDK 安装
使用 Silicon Labs 官方的开发工具 Simplicity Studio 进行开发.
外设清单
除了XG24-EK2703A开发板外,为了实现核心功能,还需要准备这些外设:
· DHT11温湿度传感器
· 无源蜂鸣器
· OLED显示屏
连接方式
由于板子上预留了mikroBUS插槽,项目使用双面喷锡洞洞板来制作简易扩展板.
将有关部件组装后就可以装载到 mikroBUS 的插槽上了. 相比于杜邦线连接,这样做增加了连接的稳定性.
设计实现
嵌入式设计
由于项目的场景中涉及的行为较多,步调不一定一致,便使用 FreeRTOS 而非直接编写裸机程序,这样对不同行为的调度更加方便. 但相比裸机程序而言,FreeRTOS 编写任务过程中也更加要关注潜在的数据竞争问题. 如下图所示,采集任务通过 FreeRTOS跨任务通信Queue API 将消息传递给 Bluetooth LE 数据外发任务以及输出任务.
Android 应用设计
采用非常流行的以 Dart 编程语言为底色的跨平台应用框架 Flutter 开发上位机,用到了 fl_chart, flutter_blue_plus 等组件库.
实现过程
DHT 11 移植
官方未提供 DHT 11 的样例程序,因此需要手动移植. 此处参考了 Adafruit 编写的库程序 [2],移植后的部分代码如下所示(仅展示核心的脉冲计数部分,更多请参见附件源代码).
GPIO_PinModeSet(DHT11_PORT, DHT11_PIN, gpioModeInputPull, 1);
sl_udelay_wait(55);
if (dht11_expect_pulse(0) == TIMEOUT)
{
app_log_debug("DHT timeout low pulse prelude.");
result->temperature = last_result.temperature;
result->humidity = last_result.humidity;
return SL_STATUS_TIMEOUT;
}
if (dht11_expect_pulse(1) == TIMEOUT)
{
app_log_debug("DHT timeout high pulse prelude.");
result->temperature = last_result.temperature;
result->humidity = last_result.humidity;
return SL_STATUS_TIMEOUT;
}
for (int i = 0; i < 80; i += 2)
{
cycles[i] = dht11_expect_pulse(0);
cycles[i + 1] = dht11_expect_pulse(1);
}
OLED 驱动
选用的 OLED 显示屏驱动芯片为 SSD 1306,Silicon Labs 官方有提供 SSD 1306 的驱动,但由于尺寸不一样,除了需要修改宏定义的屏幕大小之外,还需要修改初始化的命令以及页数据传输的偏移.
diff --git a/album/ssd1306.c b/oled_ssd1306_i2c/src/ssd1306.c
index fc71968..9e4523a 100755
--- a/album/ssd1306.c
+++ b/oled_ssd1306_i2c/src/ssd1306.c
@@ -53,7 +53,7 @@ const uint8_t cmd_buff[] = {
0x80, /* the suggested ratio 0x80 */
SSD1306_SETMULTIPLEX, /* 0xA8 Set Multiplex Ratio */
- 0x3F, /* = oled_height/8 - 1 */
+ 0x2F, /* = oled_height/8 - 1 */
SSD1306_SETDISPLAYOFFSET, /* 0xD3 Set Display Offset */
0x00,
@@ -79,7 +79,7 @@ const uint8_t cmd_buff[] = {
0x12,
SSD1306_SETCONTRAST, /* 0x81 Set Contrast Control */
- 0xCF,
+ 0x8F,
SSD1306_SETPRECHARGE, /* 0xD9 Set Pre-Charge Period */
0xF1,
@@ -143,7 +143,7 @@ sl_status_t ssd1306_draw(const void *data)
sc += ssd1306_send_command(&cmd, 1);
cmd = 0x00; /* Set Lower Column Start Address for Page Addressing Mode */
sc += ssd1306_send_command(&cmd, 1);
- cmd = 0x10; /* Set Higher Column Start Address for Page Addressing Mode */
+ cmd = 0x12; /* Set Higher Column Start Address for Page Addressing Mode */
sc += ssd1306_send_command(&cmd, 1);
/* Send pixels for this page */
BLE 协议服务实现
利用 Simplicity Studio 对 GATT 的配置可以自定义 BLE 协议的服务,我们定义了三个特征,分别用于传感器数据的传出、传感器警报阈值的写入、以及让设备进入电子相框模式的命令流.
其中 Sensors Data 和 Thresholds 均为 9 字节,字节布局为一个 uint8_t 和两个 float 分别对应 CPU 温度与环境温湿度.
typedef struct __attribute__ ((packed)) {
uint8_t cpu;
float temperature;
float humidity;
} app_sensors_t;
特征 Command Stream 是给图像数据封包使用的,因为设备的 MTU 受限而且 long write 特性的通用性比较差,我们就用了一个简单的无状态协议来从手机往设备上传输图像,在发送完若干 IMAGE_DATA 封包之后紧接着发一个 IMAGE_DISPLAY 封包即可显示刚刚接受完的图像.
typedef enum uint32_t {
UNSPECIFIED,
IMAGE_DATA,
IMAGE_DISPLAY,
} app_ble_command_t;
typedef struct __attribute__ ((packed)) {
uint16_t offset;
uint16_t size;
uint8_t payload[0];
} app_ble_image_data_t;
typedef struct __attribute__ ((packed)) {
uint32_t type;
uint8_t payload[0];
} app_ble_packet_t;
安卓应用实现
笔者此前从未使用过 Flutter,但之前有过 React Native 的经历,也想趁着这次机会熟悉一下 Dart 语言. 下面挑一些有意思的部分着重展示一下代码,有兴趣的可以直接在附件中查看 App 源代码.
图像传输
手机端向手机传输图像数据的核心部分,包含了图像二值化(这里采用了 0.8 的灰度阈值,其实一般用 0.5 这种中间值就行)以及将单色图像 pack 成 Uint8List 然后分 chunk 带上元数据头(app_ble_packet_t 和 app_ble_image_data_t)发送出去. 要特别注意数据传输时的大小端序,一般如果相应的传输标准有推荐或者强制规范,就按传输标准的来,比如 GATT 规范很明显对小端序偏爱有加. 否则没要求的情况下,我一般使用大端序作通讯,到主机后有必要的话再转换为相应的主机字节序.
Future<void> sendImageBytes(Uint8List bytes) async {
const int chunkSize = 200;
var cursor = 0;
while (cursor < bytes.length)
{
final chunk = bytes.sublist(cursor, min(cursor + chunkSize, bytes.length));
final header = Uint8List(8);
final headerView = ByteData.sublistView(header);
headerView.setUint32(0, 1, Endian.little); // TYPE: IMAGE_DATA
headerView.setUint16(4, cursor, Endian.little); // OFFSET
headerView.setUint16(6, chunk.length, Endian.little); // SIZE
await commandStream?.write([...header, ...chunk]);
cursor += chunkSize;
}
final header = Uint8List(4);
final headerView = ByteData.sublistView(header);
headerView.setUint32(0, 2, Endian.little); // TYPE: IMAGE_DISPLAY
await commandStream?.write(header);
}
Uint8List packBinaryImage(Iterable<bool> stream) {
var list = Uint8List(128 * (64 ~/ 8));
for (final (index, pixel) in stream.indexed) {
final x = (index % 128);
final y = (index ~/ 128);
if (pixel) {
list[x + (y ~/ 8) * 128] |= (1 << (y % 8));
}
}
return list;
}
void chooseImageAndSend() async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.gallery);
if (picked != null) {
final image = (await parseImage(picked))!;
final resized = img.copyResize(image, width: 128, height: 64);
final bytes = packBinaryImage(resized.map((pixel) => pixel.luminanceNormalized < 0.8));
debugPrint(bytes.toString());
await sendImageBytes(bytes);
}
}
图表显示
fl_chart 的示例比较庞大,需要耐心地改一下才能用,这里我做了一个小技巧:图表显示 12 个历史数据,但事实上记录了有 30 个,30 个的范围更大可以更好地为坐标轴最大值和最小值提供参考,让折线在坐标轴变化时更自然.
minY: history.map((sensor) => sensor.humidity).reduce(min).floorToDouble() - 2.0,
maxY: history.map((sensor) => sensor.humidity).reduce(max).ceilToDouble() + 2.0,
lineBarsData: [
LineChartBarData(
spots: validHistory.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.humidity);
}).toList(),
isCurved: true,
color: Colors.teal[200],
dotData: FlDotData(show: true),
belowBarData: BarAreaData(show: false),
),
],
设备 BLE 事件处理实现
这里的核心是要对 Thresholds 和 Command Stream 两个特征的外部写操作进行处理,对阈值的处理其实比较简单好懂.
case sl_bt_evt_gatt_server_attribute_value_id:
if (gattdb_command_stream == evt->data.evt_gatt_server_attribute_value.attribute)
{
uint8_t buffer[256];
size_t length;
sc = sl_bt_gatt_server_read_attribute_value(gattdb_command_stream, 0, sizeof(buffer), &length, &buffer);
app_ble_packet_t *packet = buffer;
switch (packet->type)
{
case IMAGE_DATA:
app_ble_image_data_t *image_data = packet->payload;
uint8_t *dest = state.active_image;
for (int i = 0; i < image_data->size; i++)
{
dest[image_data->offset + i] = image_data->payload[i];
}
break;
case IMAGE_DISPLAY:
xTaskNotify(state.output_task, 0, eNoAction);
break;
}
app_log_status_error(sc);
}
else if (gattdb_thresholds == evt->data.evt_gatt_server_attribute_value.attribute)
{
size_t length;
sc = sl_bt_gatt_server_read_attribute_value(gattdb_thresholds, 0, sizeof(state.threshold), &length, &state.threshold);
app_log_status_error(sc);
app_log_info("Set threshold: %d %.2f %.2f\n", state.threshold.cpu, state.threshold.temperature, state.threshold.humidity);
}
这里的封包处理对应了之前的 BLE 服务实现,对于 IMAGE_DATA 封包我们将 payload 拷贝到图像缓冲区对应的偏移上,在 IMAGE_DISPLAY 封包来到之后,我们利用 FreeRTOS 自带的通知原语,通知「输出任务」是时候输出图像了.
效果展示
参考文献
[1] EFR32xG24 Explorer Kit User's Guide: https://www.silabs.com/documents/public/user-guides/ug533-xg24-ek2703a.pdf
[2] Arduino library for DHT11: https://github.com/adafruit/DHT-sensor-library
[3] Silicon Labs Platform Hardware Drivers: https://github.com/SiliconLabs/platform_hardware_drivers
意见、建议与展望
活动的任务其实还可以分难度层次进行设计,并给予不同的奖励.
Simplicity Studio 设计的本意挺好的,但是实际使用过程中打开某些属性对话框偶有闪退现象.
因为报名的时间比较晚,其实如果有闲暇时光的话倒是挺希望把某些新型语言的工具链给移植到这款硬件上的.