项目介绍
这次项目基于 Silicon Labs 的 XG24-EK2703A 开发板,其 EFR32MG24 SOC 内置了温度传感器和蓝牙功能。我们将通过蓝牙把检测到的温度上报给上位机,上位机以折线图的形式可视化温度信息。
XG24-EK2703A
XG24-EK2703A 开发板基于 EFR32MG24 SOC,专注于 IoT 项目的快速原型和概念验证。
EFR32MG24 SOC 提供了丰富的无线网络连接方式,支持 2.4 GHz、Bluetooth LE、Bluetooth Mesh、Zigbee、Thread 和 Matter。
板载了 SEGGER J-Link 调试器,通过一个 USB Type-C 接口即可完成程序烧录和调试。
SOC 功能实现
这次项目使用了 Silicon Labs 的 Simplicity Studio IDE,它提供了非常方便的组件安装、代码生成功能,其中很多生产可用的组件都是可以直接使用的。
项目搭建
我们从官方提供的 Bluetooth SoC Empty 项目模板开始搭建,主要使用到以下组件:
- TEMPDRV:温度传感器驱动,可以通过它访问 SoC 内部的温度传感器
- Health Thermometer API:实现了封装蓝牙标准的 Health Thermometer 服务,安装组件的时候会自动生产相关代码(例如插入到大循环里处理蓝牙连接事件),我们只需要在回调里根据蓝牙事件实现获取温度和发送的逻辑就可以了
- Timer:定时器,用来定时发送温度信息
- Log:日志组件,方便调试
GATT 配置
Simplicity Studio 提供了图形化的 GATT 配置。在安装 Health Thermometer API 组件的时候会自动配置好 Health Thermometer 的 GATT。
我们改一下设备名称即可,上位机扫描的时候可以方便区分其他蓝牙设备。
代码逻辑
项目没有使用 RTOS, 所以是大循环的模式,除了中断外的代码都在一个大循环里处理。
上电之后,先在 app_init
回调初始化温度传感器驱动:
#include "tempdrv.h"
SL_WEAK void app_init(void)
{
app_log_info("app_init");
TEMPDRV_Init();
}
然后就进入了循环,在 Health Thermometer API 提供的 sl_bt_ht_temperature_measurement_indication_changed_cb
回调里,根据蓝牙事件进行处理。
void sl_bt_ht_temperature_measurement_indication_changed_cb(
uint8_t connection,
sl_bt_gatt_client_config_flag_t client_config)
{
sl_status_t sc;
app_connection = connection;
// Indication or notification enabled.
if (sl_bt_gatt_disable != client_config) {
// Start timer used for periodic indications.
sc = app_timer_start(&app_periodic_timer,
1000,
app_periodic_timer_cb,
NULL,
true);
app_assert_status(sc);
// Send first indication.
app_periodic_timer_cb(&app_periodic_timer, NULL);
}
// Indications disabled.
else {
// Stop timer used for periodic indications.
(void)app_timer_stop(&app_periodic_timer);
}
}
当上位机建立连接并开启订阅的时候,我们使用 app_timer_start
开启了一个定时器,每秒发送一次温度。当上位机停止订阅的时候,我们就停掉定时器。
定时任务用 TEMPDRV_GetTemp()获取温度,然后用 sl_bt_ht_temperature_measurement_indicate 发送温度:
static void app_periodic_timer_cb(app_timer_t *timer, void *data)
{
int32_t temperature = TEMPDRV_GetTemp() * 1000;
float tmp_c = (float)temperature / 1000;
app_log_info("Temperature: %5.2f C\n", tmp_c);
// Send temperature measurement indication to connected client.
sl_bt_ht_temperature_measurement_indicate(app_connection, temperature, false);
}
这里需要注意消息的格式,蓝牙标准要求用 32 位的 IEEE 11073-20601 浮点数进行编码,它以 10 为基数,包含 8 位的指数(Exponent)和24的尾数(Mantissa):。
Health Thermometer API 的实现里,把 Exponent 固定为 -3 了,相当于除 1000, 所以调用时温度要先乘 1000。
static void temperature_measurement_val_to_buf(int32_t value,
bool fahrenheit,
uint8_t *buffer)
{
uint32_t tmp_value = ((uint32_t)value & 0x00ffffffu) \
| ((uint32_t)(-3) << 24); // Exponent 固定为 -3 了
buffer[0] = fahrenheit ? TEMPERATURE_MEASUREMENT_FLAG_UNITS : 0;
buffer[1] = tmp_value & 0xff;
buffer[2] = (tmp_value >> 8) & 0xff;
buffer[3] = (tmp_value >> 16) & 0xff;
buffer[4] = (tmp_value >> 24) & 0xff;
}
上位机实现
上位机使用 Rust 实现,主要使用两个库:
- btleplug:封装了各个平台的 Bluetooth LE 接口
- egui:跨平台的即时模式 GUI 库
蓝牙处理和 UI 跑在独立的线程里,两者通过 channel 进行通信,不会相互阻塞。
蓝牙处理
首先打开蓝牙设备,然后对周边的蓝牙设备进行扫描,找到我们的开发版。然后建立连接,订阅温度特性。
// get the first bluetooth adapter
let adapters = manager.adapters().await?;
let central = adapters
.into_iter()
.nth(0)
.ok_or(btleplug::Error::DeviceNotFound)?;
// start scanning for devices
central.start_scan(ScanFilter::default()).await?;
tokio::time::sleep(Duration::from_secs(2)).await;
// find the sensor
let sensor = self.find_sensor(¢ral).await?;
info!("connecting to sensor: {}", sensor.address());
sensor.connect().await?;
info!("discovering services");
sensor.discover_services().await?;
info!("findind temperature characteristic");
let chars = sensor.characteristics();
let notify_char = chars
.iter()
.find(|c| c.uuid == uuid_from_u16(0x2a1c))
.ok_or(btleplug::Error::NoSuchCharacteristic)?;
info!("subscribing to characteristic");
sensor.subscribe(notify_char).await?;
订阅之后我们会得到一个消息流,循环处理消息即可:
let mut stream = sensor.notifications().await?;
while let Some(data) = stream.next().await {
if let Some(temp) = self.decode(&data.value) {
self.tx.send(temp)?;
egui_ctx.request_repaint()
}
}
在循环里我们解析数据,然后发送到 channel,并请求 UI 进行更新。
UI
UI 也是一个循环,先尝试接收消息然后进行 UI 更新。
先定义一个 UI 结构体,里面包含 channel 的接收端和一个存温度数据的 VecDeque。接着实现 eframe::App 接口,里面的 update 方法会在需要 UI 更新时自动调用。
struct UI {
rx: Receiver<f32>,
measures: VecDeque<f32>,
}
impl eframe::App for UI {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// receive temperature
if let Ok(temp) = self.rx.try_recv() {
if self.measures.len() >= self.measures.capacity() {
self.measures.pop_front();
}
self.measures.push_back(temp);
}
// 画图在下面 ...
}
我们先尝试从 channel 里接收数据,如果有数据就加到 VecDeque 里。然后我们用 egui_plot 画折线就可以了:
plot.show(ui, |plot_ui| {
let points: PlotPoints = self
.measures
.iter()
.enumerate()
.map(|(i, x)| [i as f64, *x as f64])
.collect();
let line = Line::new(points)
.color(Color32::from_rgb(100, 200, 100))
.style(Solid)
.highlight(true)
.name("Tempereture");
plot_ui.line(line);
})
功能展示
上位机:
常温:
加热:
总结
XG24-EK2703A 是一款非常适合快速原型的 IoT 开发板,搭配 Simplicity Studio 和丰富的组件库,可以快速验证概念。
参考资料
- EFR32xG24 Explorer Kit - Silicon Labs
- Personal Health Devices Transcoding - Bluetooth® White Paper: 包含体温计消息协议和IEEE 11073-20601 浮点数的介绍。