嵌入式人工智能实践项目——天气预测¶
〇. 简介¶
众所周知,沆州天气变换莫测,天气预报似乎无能为力。为了成为杭州的天气之子,掌握实时天气动向,快快拿起手头的单片机,结合紫金港气象观测站的数据与你的智能,相信你也能成为 浙大阳菜。
一. 实验目的¶
- 设计并实现嵌入式人工智能天气预测系统,根据温湿度信息对当前晴雨天气进行判断,并通过改变LED灯的状态进行输出
- 通过对硬件系统的实现,了解并体验STM32单片机编程的基本方法,加深对嵌入式编程的理解
- 通过对模型的设计与训练,巩固之前学习的人工智能知识,并将其运用于实战中
二. 实验材料¶
- stm32f103c8t6最小系统板,ST-Link下载器
- USB-串口转换模块
- DHT11温湿度传感器
- LED灯珠
- 面包板
- 杜邦线若干
Info
感兴趣的同学可购置更多模块以实现一个更为完整的系统,推荐购买江科大套件(上述材料均含在套件中),后续可配合相关课程进一步学习。
三. 实验步骤¶
硬件部分¶
0 环境配置¶
目前主流的单片机开发平台有Keil、IAR、STM32CubeIDE等,本实验出于便捷性考虑,采用VScode+PlatformIO的形式进行开发,配置方法如下:
-
安装PlatformIO扩展:

-
下载项目模板代码
- 打开示例项目文件夹,打开platformio.ini,等待必要组件下载完成
一个platformIO工程的项目结构如下:
├─.pio
│ └─build
├─.vscode
├─include
├─lib
├─src
│ ├─CORE
│ ├─FWLIB
│ │ ├─inc
│ │ └─src
│ ├─SYSTEM
│ └─main.c
├─test
├─.gitignore
└─platformio.ini
- .pio/build:程序编译结果
- .vscode:VScode工作区配置(对C/C++插件的配置,用于头文件寻找等)
- include:stm32全局宏定义
- src:用于存放用户代码
- CORE:Cortex-M3芯片外设宏定义
- FWLIB:stm32固件库,存放stm32片上外设驱动
- SYSTEM:用于存放其它外设驱动
- main.c:程序入口
- platformio.ini:项目配置文件
故在main.c中书写处理的主要逻辑,将其它驱动放置在SYSTEM目录下。若想在项目中创建其它文件夹,请同步修改platformio.ini,将其添加到build_flags中:-Idir_name.
main.c的结构如下:
单片机在进入程序后先进行初始化设定,设定完毕后进入事件循环。Info
由于platformIO的启动代码中并没有对全局时钟进行配置,进入main函数后需手动配置全局时钟,调用RCC_Configuration()函数对全局时钟进行配置。否则内置时钟不能正常工作,单片机将使用外部时钟,导致定时器走慢9倍。
模板中已经在main.c中对该函数进行了调用,无需做额外修改。
按照以下接线图,使用ST-Link将单片机连接电脑

使用底部功能按键完成代码编译与烧写

烧写成功,完成配置。
1 系统搭建¶
接线图如图所示

实物图如下,仅供参考

2 成为点灯大师¶
下面正式开始单片机代码编写。复制模板以创建新项目,或直接在模板上进行修改。
第一个任务为点亮A0脚上的LED灯。由于LED长脚接电源正极,故短脚输出低电平时电路接通,LED灯点亮。
请尝试根据以上实验原理,点亮该LED灯。具体步骤为
- 使能APB2外设GPIOA时钟
- 通过GPIO_Init结构体初始化对应引脚
- 向该引脚写入低电平
参考函数:
// APB2外设时钟控制
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
// 根据给定结构体初始化
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
// 向指定端口写入值
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
进一步的,通过调用给定的延时函数,可实现LED灯闪烁的效果,以下是一些使用例
参考代码:
// 使能APB2外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 构造GPIO_Init结构体初始化引脚
GPIO_InitTypeDef GPIO_InitStructure;
// 设置输出模式、引脚与速度
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// LED灯闪烁
while (1) {
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
Delay_ms(500);
}
3 串口通信¶
Info
实验中涉及的库代码下载
由于单片机上运行的程序不像直接运行在PC上的程序,可以简单的通过printf将结果输出到stdout上可直接在控制台观察,我们需要通过串口将数据从单片机上发回PC,从而通过串口监视器观察输出结果。
先下载所需的库文件,并参照上述项目结构说明把它们放到指定的目录下。先对串口进行初始化,然后再调用相应的输出函数,具体请阅读Serial.h.
使用示例:
打开串口监视器,并烧写程序,观察实验现象。
Warning
如果你没有按照接线图要求对usb转串口模块进行接线,请自行修改Serial.c,使之与实际的接线保持一致。
为了让串口能够发送浮点数,需要修改编译指令。将下列编译指令加到platformio.ini文件的build_flags中:
这样便可利用串口发送浮点数。4 获取温湿度信息¶
将DHT11模块驱动代码放到指定的位置。
调用库函数获取温湿度,示例如下:
#include "dht11.h"
uint16_t _temperature, _humidity;
double temperature, humidity;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 不要忘记使能DHT所在GPIO口的时钟 !!!
DHT11_Init();
DHT11_Read_Data(&_temperature, &_humidity);
temperature = (_temperature >> 8) + (_temperature & 0xff) * 0.1; // 单位℃
humidity = (_humidity >> 8) + (_humidity & 0xff) * 0.1; // 单位%
Warning
若没有按照接线图将DHT11的Data脚接到B13处,则需修改dht11.h与dht11.c保证接线与代码的一致性。
学有余力的同学可尝试阅读dht驱动代码,尝试理解dht通信时序。
软件部分¶
本部分请大家结合前面课程所学,自由发挥,设计模型并在PC端训练,并尝试将训练好的模型推理部署到单片机上,结合上述硬件与外设操作完成实验任务。
1 数据处理¶
紫金港观测站观测数据下载
Info
鼓励尝试使用爬虫对网络数据进行爬取
主要利用到数据表中的列Ta_Avg(平均温度), Ta_Max(最大温度), Ta_Min(最低温度), RH_Avg(平均湿度), RH_Max(最大湿度), RH_Min(最低湿度), rain_Tot(降水量).
可以使用pandas库对数据表进行读取,下面给出一个示例
import pandas as pd
import numpy as np
df = pd.read_csv('data.csv')
# df.head() # 查看数据表前5行数据
temps = np.array(df["Ta_Avg"][2:], dtype=float)
humids = np.array(df["RH_Avg"][2:], dtype=float)
is_rain = np.array(df["rain_Tot"][2:], dtype=float) > 0
更多使用方法请参考官方文档.
2 模型训练与部署¶
由于stm32f10x系列芯片存储、运算能力受限,模型训练应选择在PC端完成。可以利用pytorch在PC端完成数据的训练,得到模型参数,再在stm32上实现该模型,并填入模型参数,实现推理函数,并利用推理结果实现对LED灯的控制.
也可以尝试TensorFlow Lite,具体内容请参考文档。
下面给出一个简单的示例,效果并不是很好,仅供参考,请勿照搬。
使用单层线性神经网络 + sigmoid激活函数,采用交叉熵损失函数,并使用随机梯度下降算法
import torch
from torch import nn
model = nn.Sequential(nn.Linear(2, 1), nn.Sigmoid())
loss_fn = nn.BCELoss()
opt = torch.optim.SGD(model.parameters(), lr = 0.0001)
划分数据集与训练集
from sklearn.model_selection import train_test_split
X = np.concatenate((temps[:,np.newaxis], humids[:,np.newaxis]), axis=1)
Y = is_rain[:np.newaxis]
train_x, test_x, train_y, test_y = train_test_split(X, Y)
train_x = torch.from_numpy(train_x).type(torch.FloatTensor)
test_x = torch.from_numpy(test_x).type(torch.FloatTensor)
train_y = torch.from_numpy(train_y).type(torch.FloatTensor)
test_y = torch.from_numpy(test_y).type(torch.FloatTensor)
train_ds = TensorDataset(train_x, train_y)
train_dl = DataLoader(train_ds, batch_size=64, shuffle=True)
valid_ds = TensorDataset(test_x, test_y)
valid_dl = DataLoader(valid_ds, batch_size=128)
模型训练
def loss_batch(model, loss_func, xb, yb, opt=None):
loss = loss_func(model(xb).reshape(-1), yb.reshape(-1))
if opt is not None:
loss.backward()
opt.step()
opt.zero_grad()
return loss.item(), len(xb)
def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
for epoch in range(epochs):
model.train()
for xb, yb in train_dl:
loss_batch(model, loss_func, xb, yb, opt)
model.eval()
with torch.no_grad():
losses, nums = zip(
*[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
)
val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)
print(epoch, val_loss)
fit(100, model, loss_fn, opt, train_dl, valid_dl)
在单片机上实现该模型
double sigmoid(double x) { return 1.0 / (1 + exp(-x)); }
uint8_t predict(double temperature, double humidity) {
double A = ..., B = ..., C = ...;
return sigmoid(A * temperature + B * humidity + C) > 0.5;
}
运用该模型进行推理,并反馈在LED灯上
四 评分标准¶
本项目涉及软硬件协同,具有一定的挑战性,推荐1-4人一组,请在5.25日23:59分前完成分组与组长登记。评分标准如下(具体验收展示方式待定):
- 系统完整性 80%:按要求实现完整系统
- 展示与互评 20%:各组组长对其它小组的展示进行打分,从硬件系统的完整性、创新性与数据处理的合理性、正确性等方面进行评估
- Bonus:
- 使用额外的模块构建系统可获得加分(20%)
- 解释dht驱动实现原理可获得加分(10%)
- 使用TensorFlow Lite部署模型(30%)
- 使用更加复杂的模型进行推理(如考虑数据时序、构造特征等)(25%)