2025贸泽电子M-Design创意设计竞赛,方向三:无线通信、物联网——蓝牙。
项目介绍
设计一款便捷的汽车 CAN 信号交互设备,基于 STM32F103 主控芯片,结合纳芯微 CAN 收发器和蓝牙模块,实现汽车 OBD 口与手机小程序之间的双向通信。通过蓝牙模块与手机小程序的数据交互,用户可实时查看汽车状态并发送指令。该系统通过高效的电源管理和精确的信号处理,为汽车维修与诊断提供便捷的解决方案。项目涵盖硬件设计、软件开发及蓝牙通信、3D建模及打印等多个领域。
项目结构
- 硬件开发-原理图、PCB设计
- 软件开发-STM32代码开发
- 小程序开发-蓝牙BLE信号收发小程序
- 外壳制作-3D建模及打印
系统原理图
设计思路
电源管理:汽车 OBD 口提供的 12V 电源通过 12V 转 5V 芯片和 5V 转 3.3V 芯片,为 STM32F103 和其他模块提供稳定的工作电压,确保设备正常运行。
信号接收与处理:CAN 收发器负责接收汽车 OBD 口的 CAN 信号,并将其传输给 STM32F103。主控芯片对信号进行解析和处理,然后通过 UART 接口将数据发送给蓝牙模块。
无线通信:蓝牙模块将接收到的数据以无线方式传输给手机蓝牙,手机蓝牙再将数据传递给小程序进行显示,方便用户实时了解汽车状态。
信号交互流程:用户在手机小程序上输入指令,指令通过手机蓝牙发送给设备的蓝牙模块,再由蓝牙模块传输给 STM32F103。主控芯片将指令处理后通过 CAN 收发器发送给车载CAN总;车载CAN总线上的CAN报文,也可以通过CAN收发器捕捉后传递给STM32F103,然后通过串口发送到蓝牙模块。蓝牙模块再将信号发送给小程序进行显示。
硬件开发
原理图
除了主控芯片外,最重要的就是蓝牙透传模块和CAN收发器。蓝牙模块选用的是大夏龙雀DX-BT37蓝牙模块,具有超低功耗的特点。CAN收发器选用的是纳芯微的1044CAN芯片,是一款车规级的低功耗CAN芯片。另外,电路中的CP2012是DEBUG过程中使用的,完工之后作用就不大了。
PCB
这里特别鸣谢我们团队的硬件工程师戴老板,他layout水平在我看来是非常高的,板子做出来让人感觉是一个艺术品。
实物展示
这里的焊接全是戴老板手工完成,十年老硬件工程师的功力还是值得信赖的。
软件开发
代码流程图
流程图说明:
- 初始化 STM32F103 主控:
- 配置 IO 引脚(用于控制各类外设)。
- 配置 UART(用于串口通讯)。
- 配置 CAN 收发器(用于 CAN 总线通讯)。
- CAN 收发器操作:
- 在 CAN 收到报文 后进入 CAN 接收中断,并 读取 CAN 报文。
- 读取的 CAN 报文会通过 UART 发送到蓝牙模块,然后由蓝牙模块透传给手机 APP。
- UART 中断操作:
- 当 UART 接收到来自蓝牙芯片的报文后,首先会 解析报文格式。
- 如果 报文格式符合 CAN 发送格式,则将该报文通过 CAN 发送出去;如果格式不符合,则 丢弃报文或给出错误提示。
驱动相关代码
可以使用STM32CubeMX进行关键引脚的配置;这里不展开介绍在2025寒假练的项目中有多优秀的项目/教程大家可以去参考。
然后点击Generate COde生成基础代码,然后再修改部分用户代码。
关键函数:
- UART 和 CAN 的配置与中断处理确保了STM32F103主控可以实现双向数据通信。
- CAN 中断与UART 中断的实时数据处理确保了蓝牙与CAN设备的高效通讯。
主函数
int main(void)
{
LED_Config(); // 配置LED灯
PC_USART_Config(230400);
TIM3_Config(); // LED控制
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
ISO15765_Config(CAN_ID_STD, CAN_500K);
Task_Manager_Init();
// 调用任务管理器运行函数,启动任务调度
Task_Manager_Run();
// 返回0表示程序正常退出
return 0;
}
CAN信号接收中断函数
//CAN信号接收中断
void USB_LP_CAN1_RX0_IRQHandler(void)
{
CanRxMsg CANMessaage;
CanTxMsg CmdDeviceContrl101 = {0x101, 0x18DB33F1, CAN_ID_STD, CAN_RTR_DATA, 3, 0xFE, 0x01, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00};
CAN_Receive(CAN1, CAN_FIFO0, &CANMessaage);
gCAN_u8MessageReveived = 1;
if((CANMessaage.StdId >> 8) & 0x07 == 1)
{
printf("time=%lu msg= %4x %2x %2x %2x %2x %2x %2x %2x %2x\n ", gSMT_u32localtimems,CANMessaage.StdId,
CANMessaage.Data[0],
CANMessaage.Data[1],
CANMessaage.Data[2],
CANMessaage.Data[3],
CANMessaage.Data[4],
CANMessaage.Data[5],
CANMessaage.Data[6],
CANMessaage.Data[7]);
}
else if(TESTMODE == 1)
{
printf("time=%lu msg= %4x %2x %2x %2x %2x %2x %2x %2x %2x\n ", gSMT_u32localtimems,CANMessaage.StdId,
CANMessaage.Data[0],
CANMessaage.Data[1],
CANMessaage.Data[2],
CANMessaage.Data[3],
CANMessaage.Data[4],
CANMessaage.Data[5],
CANMessaage.Data[6],
CANMessaage.Data[7]);
}
}
UART信号接收中断函数
void PC_USART_IRQHandler(void)
{
u8 data;
u8 index, i;
ErrorStatus err;
data = data;
if (USART_GetFlagStatus(PC_USART, USART_FLAG_ORE) != RESET)
{
data = PC_USART->DR;
printf("USART Overrun Error occurred.\n"); // WJ ADD
}
if (USART_GetFlagStatus(PC_USART, USART_IT_RXNE) != RESET)
{
ATCmd[ATLEN++] = PC_USART->DR;
USART_ITConfig(PC_USART, USART_IT_IDLE, ENABLE); // WJ 注释:使能 IDLE 中断(空闲线检测中断),这样当接收缓冲器为空时,会触发一次中断,表示接收的数据块已经结束。
}
if (USART_GetFlagStatus(PC_USART, USART_FLAG_IDLE) != RESET)
{
data = PC_USART->DR;
USART_ITConfig(PC_USART, USART_IT_IDLE, DISABLE); // 关闭IDLE中断
if (!strncmp((const char *)ATCmd, "AT+HWVERSION", 12))
{
// ISO15765_Config(CAN_ID_STD, CAN_500K);
// TESTMODE = 0;
printf((const char *)HWVersion);
ClearRAM((u8 *)ATCmd, 100);
}
//收到报文后通过CAN发送出去
else if (hexncmp((const char *)ATCmd, MSGCMD1, 11)){
SENDMESG1FLAG =1;
SENDMESG1.StdId = ATCmd[0]<<8 | ATCmd[1];
SENDMESG1.DLC = 8;
SENDMESG1.Data[0] = ATCmd[3];
SENDMESG1.Data[1] = ATCmd[4];
SENDMESG1.Data[2] = ATCmd[5];
SENDMESG1.Data[3] = ATCmd[6];
SENDMESG1.Data[4] = ATCmd[7];
SENDMESG1.Data[5] = ATCmd[8];
SENDMESG1.Data[6] = ATCmd[9];
SENDMESG1.Data[7] = ATCmd[10];
ClearRAM((u8 *)ATCmd, 100);
ATLEN = 0;
}
else if (!strncmp((const char *)ATCmd, "AT+READALL", 10)){
TESTMODE = 1;
ClearRAM((u8 *)ATCmd, 100);
ATLEN = 0;
}
else if (!strncmp((const char *)ATCmd, "AT+CLOSEALL", 11)){
TESTMODE = 1;
ClearRAM((u8 *)ATCmd, 100);
ATLEN = 0;
}
else
{
if (ATLEN >= 20)
{
ClearRAM((u8 *)ATCmd, 100);
ATLEN = 0;
}
}
}
}
CAN报文发送函数
void SEND_CAN_MESSAGE(CanTxMsg *TxMessage, u8 CANStype, ErrorStatus *err)
{
u8 TransmitMailbox;
u32 lCAN_u32StartSendTime;
RxFlay = SUCCESS;
TxMessage->IDE = CANStype;
TransmitMailbox = CAN_Transmit(CAN1, TxMessage);
lCAN_u32StartSendTime = gSMT_u32localtimems;
while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CANTXOK)
{
if (gSMT_u32localtimems > lCAN_u32StartSendTime + 10)
{
RxFlay = ERROR;
break;
}
__WFI(); // 等待中断,进入低功耗模式
}
*err = RxFlay;
if(RxFlay == SUCCESS)
{
printfTxMessage(TxMessage);
}
else
{
printf("SENT FAIL");
}
}
小程序开发
进入小程序官网:https://mp.weixin.qq.com/?token=&lang=zh_CN
注册账号,并下载 “微信开发者工具”,创建自己的小程序并进行开发,官方提供了很多函数接口可以直接调用。
小程序的代码核心是以下两个页面。
在Page文件夹下创建所需的页面。每个页面开发时,主要有四种文件类型,每种文件都有其特定的作用。这些文件类型分别是:js
、json
、wxml
、wxss
。
1. js 文件 (JavaScript):
- 用途:用于处理小程序的逻辑功能,如页面的交互、数据处理、事件响应等。
- 主要功能:
- 包含页面的事件处理函数,例如点击按钮、获取数据、与后端接口交互等。
- 通过
js
文件可以控制页面的显示、数据绑定、视图更新等操作。 - 例如,可以通过
wx.request
发送请求,获取接口数据,并更新页面内容。
2. json 文件 (配置文件):
- 用途:用于配置小程序的全局或页面级别的设置。
- 主要功能:
- 配置页面路径、窗口设置、页面标题、导航栏、背景色等。
app.json
配置文件用于配置整个小程序的页面路由、窗口样式、底部导航等。- 每个页面的
page.json
可以配置该页面的特定属性,比如是否启用下拉刷新、是否开启分享等。 - 通过
json
文件的配置,可以控制小程序的外观、功能等特性。
3. wxml 文件 (WeiXin Markup Language):
- 用途:用于定义页面的结构和布局,类似于 HTML 文件。
- 主要特点:
wxml
中的语法结构与 HTML 相似,但有一些差异。例如,使用{{}}
来进行数据绑定,将 JavaScript 数据渲染到页面上。- 支持小程序的原生组件,如
<view>
,<text>
,<image>
,<button>
等,它们对应了小程序的基本界面元素。 - 支持条件渲染(
wx:if
、wx:for
)和事件绑定(如bindtap
等)。 wxml
文件决定了页面的外观和组件布局,描述了页面中的元素、标签、容器等。
4. wxss 文件 (WeiXin Style Sheets):
- 用途:用于设置页面的样式,类似于 CSS 文件。
- 主要特点:
wxss
基本语法和 CSS 非常相似,但有一些特定的规则和差异:- 支持单位:
rpx
(响应式像素)是小程序特有的单位,适用于不同屏幕尺寸的自适应布局。rpx
单位的设计使得页面在各种设备上能保持良好的显示效果。 - 样式的作用范围:
wxss
是作用于小程序的页面级别的样式定义,但如果想要全局共享样式,通常需要在app.wxss
中进行配置。
- 支持单位:
- 可以使用额外的功能,比如条件样式、透明度、背景图片等,控制页面元素的视觉效果,如颜色、字体、边距、布局等。
小程序核心代码
getBLEDeviceCharacteristics(deviceId, serviceId) {
const that = this
wx.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
var ismy_service = false
console.log("compute ", serviceId, this.serviceu)
if (serviceId == this.serviceu) {
ismy_service = true
console.warn("this is my service ")
}
console.log('getBLEDeviceCharacteristics success', res.characteristics)
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i]
if (ismy_service){
console.log("-----------------------")
}
console.log("this properties = ", item.properties)
if (item.properties.read) {
console.log("[Read]", item.uuid)
wx.readBLECharacteristicValue({
deviceId,
serviceId,
characteristicId: item.uuid,
})
}
if (item.properties.write) {
this.setData({
canWrite: true
})
console.log("[Write]",item.uuid)
this._deviceId = deviceId
if (ismy_service && (this.txdu == item.uuid)){
console.warn("find write uuid ready to ", item.uuid)
this._characteristicId = item.uuid
this._serviceId = serviceId
// this.showModalTips(this.txdu+ "\r找到发送特征值")
}
//this.writeBLECharacteristicValue()
}
if (item.properties.notify || item.properties.indicate) {
console.log("[Notify]", item.uuid)
if (ismy_service && (this.rxdu == item.uuid)){
console.warn("find notity uuid try enablec....", item.uuid)
// this.showModalTips(this.rxdu + "\r正在开启通知...")
wx.notifyBLECharacteristicValueChange({ //开启通知
deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
success(res) {
console.warn('notifyBLECharacteristicValueChange success', res.errMsg)
that.setData({
connectState: "连接成功"
})
// that.showModalTips(that.rxdu + "\r开启通知成功")
that.data.readyRec=true
}
})
}
}
}
},
fail(res) {
console.error('getBLEDeviceCharacteristics', res)
}
})
// 操作之前先监听,保证第一时间获取数据
wx.onBLECharacteristicValueChange((characteristic) => {
var buf = new Uint8Array(characteristic.value)
var nowrecHEX = ab2hex(characteristic.value)
console.warn("rec: ", nowrecHEX, characteristic.characteristicId)
var recStr = ab2Str(characteristic.value)
console.warn("recstr: ", recStr, characteristic.characteristicId)
if (this.rxdu != characteristic.characteristicId){
console.error("no same : ", this.rxdu, characteristic.characteristicId)
return
}
if (!this.data.readyRec)return
var mrecstr
if (this.data.hexRec){
mrecstr = nowrecHEX
}else{
mrecstr = recStr
}
if (this.data.recdata.length>3000){
this.data.recdata = this.data.recdata.substring(mrecstr.length, this.data.recdata.length)
}
console.warn("RXlen: ", buf.length)
this.setData({
recdata: this.data.recdata + mrecstr,
rxCount: this.data.rxCount + buf.length,
timRX: this.data.timRX+buf.length
})
})
},
外壳制作
3D建模
这里使用CATIA进行3D建模
完成后点击另存为将图片保存为stl格式格式,后续3D打印软件会使用到。
3D打印
使用拓竹的 LAB A1mini3D打印机,软件是官方的 Bambu Studio。Bambu Studio是一款开源、尖端、功能丰富的切片软件。
打印过程可以导出,非常的方便。
设计中遇到的难题和解决方法
1. 多次打印板子测试问题
- 问题:第一次打印板子简易板卡,测试 CAN 收发器,蓝牙时,焊接STM32的时候好几个引脚都短路了,调试了很久。
- 解决方法:回流焊的時候要控制好锡膏。
2. 正式板子 OBD 口孔大小错误及走线问题
- 问题:第一次打印的正式板子 OBD 口孔大小打错,钻孔时弄断走线。
- 解决方法:在设计文件输出前,安排专人进行审核,对关键尺寸进行再次确认。对于已经出现的走线问题,采用飞线的方式进行临时修复,同时对设计文件进行修改,避免在后续打印中再次出现类似问题。
3. 3D 打印外壳尺寸误差问题
- 问题:3D 打印外壳时,由于缺乏精密测试工具,打印尺寸差距较大。
- 解决方法:购买或借用精密的测量工具,如卡尺、千分尺等,在打印前对设计尺寸进行精确测量和校准。同时,根据第一次打印的结果,对设计文件进行调整,然后进行第二次打印,以提高外壳尺寸的准确性。
4. 小程序蓝牙连接调试问题
- 问题:小程序开发时蓝牙连接调试时间长。
- 解决方法:深入学习蓝牙通信协议和小程序开发框架,查阅相关文档和资料,了解常见的蓝牙连接问题及解决方法。同时,使用调试工具对蓝牙通信过程进行监控和分析,逐步排查问题,最终实现稳定的蓝牙连接。
对本次竞赛的心得体会
在本次任务里,我收获满满,积累了大量宝贵经验,掌握了不少实用技能。起初,我从零基础起步,学习绘制原理图,接着精心设计 PCB,切实将硬件功能予以实现。这一全过程,加深了我对电路设计原理、布局规划以及元件选型的认知,让我理解得更为透彻。到了焊接和调试环节,我的手工操作精准度大幅提升,还积累了丰富的电路故障处理经验,学会运用合理手段排查问题,快速完成修复。
在这个过程中,我对蓝牙模块的认识也达到了新高度,成功搭建起蓝牙模块与主控板之间的数据传输桥梁。在实际操作中,我掌握了蓝牙通信协议,熟练运用数据处理方法,为后续投身项目开发筑牢根基。
值得一提的是,这次我首次运用 3D 打印机制作产品外壳。从设计到打印,我深度钻研 3D 建模技巧,熟悉 3D 打印技术的实际应用,顺利将脑海中的设计理念转化为实实在在的产品,有力保障了项目的完整性。
另外,我初次涉足微信小程序开发领域,不仅学会开发流程,还实现了蓝牙设备与小程序的联动。经此一举,产品互动性增强,用户体验显著提升。借助小程序,用户能便捷地与设备通信,轻松完成设备控制、数据查看等操作。
不过,复盘整个项目,我也察觉到存在一些短板。就拿目前的小程序来说,功能相对简单,仅能完成基础操作,缺少一些实用的扩展功能。往后规划加入故障读取、故障清除、实时物流数据查询等常用功能,既能提高工程师使用时的工作效率,又能优化用户体验,让操作流程更流畅,产品使用更具智能化。