M-Design设计竞赛-基于树莓派5-STM32的汽车仪表盘
该项目使用了该项目使用了树莓派5作为主控平台、STM32F103C8T6作为协处理器,结合多种传感器模块和Qt框架,,实现了一个智能车载仪表盘系统的设计,它的主要功能为:1.通过STM32协处理器实现了多传感器数据采集与融合: 使用MPU6050获取车辆姿态数据(横滚角/俯仰角/偏航角) 通过TJA1051T CAN收发器解析OBD-II标准协议,实时获取发动机转速、车速、水温等车辆状态 采用ATGM332D GPS模块实现卫星定位(经纬度/海拔/速度) 通过SHT30监测车内温湿度环境参数 2.基于树莓派5的Qt人机交互界面实现了: 实时显示车辆动态数据的三维仪表盘。
标签
嵌入式系统
树莓派5
M-Design设计竞赛
QT仪表界面
ODB-II
空耳-
更新2025-04-01
127

基于树莓派5-STM32的汽车仪表盘

一,项目名称

本次项目完成的是M-Design的任务1:边缘智能、智能设备基于树莓派5-stm32的汽车仪表盘。

二,项目概述

本项目基于树莓派5与STM32,开发了一套集车辆状态监测、环境感知和智能交互于一体的车载仪表盘系统。系统通过多传感器融合技术,实现了对车辆运行状态的全面监控和可视化展示。在技术架构方面,项目采用模块化设计思想,以树莓派5作为主控平台负责图形界面和数据处理,STM32F103C8T6作为协处理器专精于实时数据采集。两者通过串口通信建立高效数据通道,构建了层次分明的硬件体系。

三,系统框架

四,硬件部分

主控:

  • 树莓派5
    image-20250330143039043.png
  • STM32F103C8T6
    {E9BAFFFA-0EA4-48DF-A63F-A75F698A094B}.png

传感器:

  • GPS模块-ATGM332D-5N-11-0
  • mpu6050
  • 蜂鸣器
  • sht30温湿度传感器
  • NXP的TJA1051T/3/1j,高速can收发器

实物图如下:

整体实物图.jpg

详细原理图如下:

  • 电源部分
    采用矽力杰的SY8089A1AAC高效1.5MHz 2A同步降压调节器。
    image-20250302225624673.png
  • 充电部分
    采用wsp4056,输出电压 4.2V 输入电压 4.0-8.0V 最大充电电流 1000ma
    image-20250330140048521.png
  • 蜂鸣器部分
    image-20250330140100956.png
  • mpu6050
    image-20250330140116505.png
  • GNSS
    image-20250330140133757.png
  • CAN
    采用NXP的TJA1051T/3/1j,高速can收发器High-speed CAN transceiver
    image-20250330135957779.png
  • 温湿度计
    image-20250330140155424.png

pcb预览如下:

image.png

{B0C8BAA9-6D92-4797-9BBB-D3768CDA2FC2}.png

五,软件部分

(1) STM32数据采集部分

系统流程图:

  • GNSS数据解析(NMEA0183协议解析):

我这里使用到的GNSS模组为:ATGM332D-5N31GPS数据包类型:(GP :GPS;BD:北斗;GN:多星联合定位;GL:GLONASS)GPGSV:可见卫星信息 GPGLL:地理定位信息 GPRMC:推荐最小定位信息 GPVTG:地面速度信息 GPGGA:GPS定位信息 GPGSA:当前卫星信息我这里主要使用的是GNGGA,其标准协议为:标准格式:$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh<CR><LF>

格式解析:
<1> UTC 时间,hhmmss(时分秒)格式
<2> 纬度ddmm.mmmm(度分)格式(前面的0 也将被传输)
<3> 纬度半球N(北半球)或S(南半球
<4> 经度dddmm.mmmm(度分)格式(前面的0 也将被传输)
<5> 经度半球E(东经)或W(西经)
<6> GPS 状态:0=未定位,1=非差分定位,2=差分定位,6=正在估算
<7> 正在使用解算位置的卫星数量(00~12)(前面的0 也将被传输)
<8> HDOP 水平精度因子(0.5~99.9
<9> 海拔高度(-9999.9~99999.9
<10> 地球椭球面相对大地水准面的高度
<11> 差分时间(从最近一次接收到差分信号开始的秒数,如果不是差分定位将为空)
<12> 差分站ID0000~1023(前面的0 也将被传输,如果不是差分定位将为空

核心代码如下:

#include "gps.h"
#include <string.h>
nmea_msg gpsx;
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] NMEA_Comma_pos 从buf里面得到第n个逗号所在的位置
* @argument 数组
* @argument 地n个','
* [url=home.php?mod=space&uid=784970]@return[/url] 逗号的位置
*/

uint8_t NMEA_Comma_Pos(uint8_t *buf,uint8_t n)
{
uint8_t *ptr = buf;
while(n)
{
//遇到'*'或者非法字符,则不存在第cx个逗号
if(*ptr == '*' || *ptr < ' '|| *ptr > 'z')return 0xFF;
if(*ptr == ',')n--;
ptr++;
}
return ptr-buf;
}
/**
* @brief NMEA_Pow m^n次方
*/
uint32_t NMEA_Pow(uint8_t m,uint8_t n)
{
uint32_t result =1;
while(n--)result *= m;
return result;
}
/**
* @brief 字符串转数字
*/
int NMEA_Str2Num(uint8_t *buf,uint8_t*dx)
{
uint8_t *p = buf;
uint32_t ires=0,fres=0;
uint8_t ilen = 0,flen =0,i;
uint8_t mask=0;
int res;
/*********得到个位十位***************/
while(1)
{
if(*p == '-')
{
mask |= 0x02;
p++;
}
if(*p == ',' || *p == '*')break;
if(*p == '.')
{
mask |= 0x01;
p++;
}
else if((*p >'9') || (*p < '0'))
{
ilen=0;
flen=0;
break;
}
if(mask & 0x01)flen++;//小数
else ilen++;//整数
p++;
}
if(mask&0x02)buf++;
for(i = 0;i<ilen;i++)
{
ires += NMEA_Pow(10,ilen-1-i)*(buf[i]-'0');
//LTPrintf("buf[%d]:%d ,ires:%d\n",i,(buf[i]-'0'),ires);
}
if(flen>5)flen = 5;
*dx = flen;
for(i=0;i<flen;i++)
{
fres += NMEA_Pow(10,flen-1-i)*(buf[ilen+1+i]-'0');
}
//LTPrintf("ilen:%d flen%d ires:%d fres:%d\n",ilen,flen,ires,fres);
res = ires*NMEA_Pow(10,flen)+fres;
if(mask&0x02)res = -res;
return res;
}
/**
* @brief 分析GPGSV信息信息
* @argument nmea信息结构体
* @argument buf接收数据的缓存区地址
* @return 无?
* @note GSV表示可视的GNSS卫星。本语句包含可视的卫星数、卫星标识号、仰角、方位角和信噪比。每次传送,
* 一个GSV语句只能包含最多4颗卫星的数据,
* 因此可能需要多个语句才能获得完整的信息。由于GSV包含的卫星不用于定位解决方案,所以GSV语句指示的卫星可能比GGA多。
* @note “GN”标识符不可用于该语句。如果可以多个卫星系统可视,则设备输出多条GSV语句,用不同的发送设备标识符表示相应的卫星。
**/
void NMEA_GPGSV_Analysis(nmea_msg *gpsx,uint8_t *buf)
{
uint8_t *p,*p1,dx;
uint8_t len,i,j,slx = 0;
uint8_t posx;
p = buf;
p1 = (uint8_t*)strstr((const char*)p,"$GPGSV");
len = p1[7] - '0'; //得到GPGSV的条数语句总数。范围:1~9。
posx=NMEA_Comma_Pos(p1,3); //得到可见卫星总数
if(len!=0xFF)gpsx->svnum = NMEA_Str2Num(p1+posx,&dx);
for(i=0;i<len;i++)
{
p1 = (uint8_t*)strstr((const char*)p,"$GPGSV");
for(j=0;j<4;j++)
{
posx=NMEA_Comma_Pos(p1,4+j*4);
if(posx!=0xFF)gpsx->slmsg[slx].num = NMEA_Str2Num(p1+posx,&dx);//得到卫星编号
else break;
posx=NMEA_Comma_Pos(p1,5+j*4);
if(posx!=0xFF)gpsx->slmsg[slx].eledeg= NMEA_Str2Num(p1+posx,&dx);//得到卫星仰角
else break;
posx=NMEA_Comma_Pos(p1,6+j*4);
if(posx!=0xFF)gpsx->slmsg[slx].azideg = NMEA_Str2Num(p1+posx,&dx);//得到卫星方位角
else break;
posx=NMEA_Comma_Pos(p1,7+j*4);
if(posx!=0xFF)gpsx->slmsg[slx].sn = NMEA_Str2Num(p1+posx,&dx);//得到卫星信噪比
else break;
slx++;
}
}
}
/**
* @brief 分析GPGGA信息
* @argument nmea信息结构体
* @argument buf接收数据的缓存区地址
* @return 无?
* @note GGA提供全球定位系统定位数据。本语句包含GNSS接收机提供的时间、位置和定位相关数据
* 1.QZSS和GPS星系配置下<TalkerID>均为GP;有关卫星标识符的详情,请参考表16:GNSS标识符。
* 2. NMEA 0183协议指示GGA消息为GPS系统特有;但当接收器配置为多星系时,GGA消息的内容将从多星系解决方案中生成。
* 3. 1) NMEA 0183协议定义的使用中卫星数量范围为00~12,然而,在多星系解决方案中,使用的卫星数量可能超过12颗。
**/
void NMEA_GPGGA_Analysis(nmea_msg *gpsx,uint8_t *buf)
{
uint8_t *p1,dx;
uint8_t posx;
p1 = (uint8_t*)strstr((const char*)buf,"$GNGGA");
posx = NMEA_Comma_Pos(p1,1);//得到日期
if(posx != 0xFF)
{
int temp = NMEA_Str2Num(p1+posx,&dx);
gpsx->utc.sec = (temp / 1000) % 10;
gpsx->utc.min = (temp / 100000) %100;
gpsx->utc.hour = temp / 10000000;
}

posx = NMEA_Comma_Pos(p1,2);
if(posx!=0xFF)
{
int temp = NMEA_Str2Num(p1+posx,&dx);
//LTPrintf("temp:%d dx:%d\n",temp,dx);
gpsx->latitude = temp / NMEA_Pow(10,dx+2);//得到°
float rs = temp%NMEA_Pow(10,dx+2); //得到'
gpsx->latitude=gpsx->latitude*NMEA_Pow(10,5)+(rs*NMEA_Pow(10,5-dx))/60;//转换为°
}
posx = NMEA_Comma_Pos(p1,3);//南纬还是北纬
if(posx!=0xFF)gpsx->nshemi = *(p1+posx);
posx = NMEA_Comma_Pos(p1,4);//得到经度
if(posx!=0xFF)
{
int temp = NMEA_Str2Num(p1+posx,&dx);
gpsx->longitude = temp / NMEA_Pow(10,dx+2);
float rs = temp%NMEA_Pow(10,dx+2);
gpsx->longitude = gpsx->longitude*NMEA_Pow(10,5)+(rs*NMEA_Pow(10,5-dx))/60;
}
posx = NMEA_Comma_Pos(p1,5);//东经还是西经
if(posx!=0xFF)gpsx->ewhemi = *(p1+posx);

posx = NMEA_Comma_Pos(p1,6); //GPS状态:0,未定位;1,非差分定位;2,差分定位;6,正在估算.
if(posx!=0XFF)gpsx->gpssta = NMEA_Str2Num(p1+posx,&dx);
posx = NMEA_Comma_Pos(p1,7); //使用的卫星数。
if(posx!=0xFF)gpsx->posslnum = NMEA_Str2Num(p1+posx,&dx);
posx = NMEA_Comma_Pos(p1,9); //得到海拔高度
if(posx != 0xFF)gpsx->altitude = NMEA_Str2Num(p1+posx,&dx);
}
/**
* @brief 分析GPGSA信息
* @argument nmea信息结构体
* @argument buf接收数据的缓存区地址
* @return 无?
* @note GSA表示GNSS精度因子(DOP)与有效卫星。本语句包含GNSS接收机工作模式,GGA或GNS
语句报告的导航解算中用到的卫星以及精度因子的值。
**/
void NMEA_GPGSA_Analysis(nmea_msg *gpsx,uint8_t *buf)
{
uint8_t *p1,dx;
uint8_t posx,i;
//LTPrintf("===============NMEA_GPGSA_Analysis=======================\r\n");
p1 = (uint8_t*)strstr((const char*)buf,"$GNGSA");
//LTPrintf("p1:%p buf:%p\r\n",p1,buf);
posx = NMEA_Comma_Pos(p1,2); //得到定位类型
//LTPrintf("posx:%d\r\n",posx);
if(posx!=0xFF)gpsx->fixmode = NMEA_Str2Num(p1+posx,&dx);
for(i=0;i<12;i++)//得到定位卫星编号
{
posx = NMEA_Comma_Pos(p1,3+i);
if(posx!=0xFF)gpsx->possl[i] = NMEA_Str2Num(p1+posx,&dx);
else break;
}
posx = NMEA_Comma_Pos(p1,15);//位置精度因子
if(posx != 0xFF)gpsx->pdop = NMEA_Str2Num(p1+posx,&dx);
posx = NMEA_Comma_Pos(p1,16);//水平精度因子
if(posx != 0xFF)gpsx->hdop = NMEA_Str2Num(p1+posx,&dx);
posx = NMEA_Comma_Pos(p1,17);//垂直精度因子
if(posx != 0xFF)gpsx->vdop = NMEA_Str2Num(p1+posx,&dx);
}
/**
* @brief 分析GPRMC信息
* @argument nmea信息结构体
* @argument buf接收数据的缓存区地址
* @return 无?
* @note RMC表示推荐的最少专用GNSS数据。本语句包含GNSS接收机提供的时间、日期、位置、航迹向
和速度数据。
**/
void NMEA_GPRMC_Analysis(nmea_msg *gpsx,uint8_t *buf)
{
uint8_t *p1,dx,posx;
uint32_t temp;
float rs;
p1 = (uint8_t *)strstr((const char*)buf,"GNRMC");//"$GPRMC",经常有&和GPRMC分开的情况,故只判断GPRMC.
//LTPrintf("buf:%p p1:%p\n",buf,p1);
//LTPrintf("buf:%s p1:%s\n",buf,p1);
posx = NMEA_Comma_Pos(p1,1);//获取时间,不要ms
//LTPrintf("posx:%d ",posx);
if(posx!=0xFF)
{
temp = NMEA_Str2Num(p1+posx,&dx) / NMEA_Pow(10,dx);
//LTPrintf("temp:%d dx:%d\n",temp,dx);
gpsx->utc.hour = temp/10000;
gpsx->utc.min = (temp /100) %100;
gpsx->utc.sec = temp % 100;
}
posx = NMEA_Comma_Pos(p1,3);
if(posx!=0xFF)
{
temp = NMEA_Str2Num(p1+posx,&dx);
//LTPrintf("temp:%d dx:%d\n",temp,dx);
gpsx->latitude = temp / NMEA_Pow(10,dx+2);//得到°
rs = temp%NMEA_Pow(10,dx+2); //得到'
gpsx->latitude=gpsx->latitude*NMEA_Pow(10,5)+(rs*NMEA_Pow(10,5-dx))/60;//转换为°
}
posx = NMEA_Comma_Pos(p1,4);//南纬还是北纬
if(posx!=0xFF)gpsx->nshemi = *(p1+posx);
posx = NMEA_Comma_Pos(p1,5);//得到经度
if(posx!=0xFF)
{
temp = NMEA_Str2Num(p1+posx,&dx);
gpsx->longitude = temp / NMEA_Pow(10,dx+2);
rs = temp%NMEA_Pow(10,dx+2);
gpsx->longitude = gpsx->longitude*NMEA_Pow(10,5)+(rs*NMEA_Pow(10,5-dx))/60;
}
posx = NMEA_Comma_Pos(p1,6);//东经还是西经
if(posx!=0xFF)gpsx->ewhemi = *(p1+posx);
posx = NMEA_Comma_Pos(p1,9);//得到日期
if(posx != 0xFF)
{
temp = NMEA_Str2Num(p1+posx,&dx);
gpsx->utc.date = temp / 10000;
gpsx->utc.month = (temp / 100) %100;
gpsx->utc.year = 2000 + temp % 100;
}
}
/**
* @brief 分析GPVTG信息
* @argument nmea信息结构体
* @argument buf接收数据的缓存区地址
* @return 无?
* @note VTG语句包含相对于地面的实际航向和速度
**/
void NMEA_GPVTG_Analysis(nmea_msg *gpsx,uint8_t *buf)
{
uint8_t *p1,dx,posx;
p1 = (uint8_t*)strstr((const char*)buf,"$GNVTG");
posx = NMEA_Comma_Pos(p1,1);//<COGT 对地航向(真北)
if(posx!=0xFF)gpsx->cogt = NMEA_Str2Num(p1+posx,&dx) / NMEA_Pow(10,dx);
posx = NMEA_Comma_Pos(p1,5);//对地速度 节
if(posx!=0xFF)
{
gpsx->SOGN = NMEA_Str2Num(p1+posx,&dx) / NMEA_Pow(10,dx);

}
posx = NMEA_Comma_Pos(p1,7);//对地速度 km/h
gpsx->SOGK = NMEA_Str2Num(p1+posx,&dx) / NMEA_Pow(10,dx);
}
/**
* @brief 提取NMEA-0183信息
* @argument nmea信息结构体
* @argument buf接收数据的缓存区地址
* @return 无?
* @note
**/

void GPS_Analysis(nmea_msg *gpsx,uint8_t *buf)
{
NMEA_GPGSV_Analysis(gpsx,buf); //GPGSV解析
NMEA_GPGGA_Analysis(gpsx,buf); //GPGGA解析
NMEA_GPGSA_Analysis(gpsx,buf); //GPGSA解析
NMEA_GPRMC_Analysis(gpsx,buf); //GPRMC解析
NMEA_GPVTG_Analysis(gpsx,buf); //GPVTG解析
}
/**
* @brief GPS校验和计算
* @argument
* @argument
* @return 无?
* @note
**/
void GPS_CheckSum(uint8_t*buf,uint8_t*checksum)
{
*checksum = 0;
uint8_t *ptr = buf;
const char *start = strchr(ptr,'$');
const char *end = strchr(ptr,'*');
if(start == NULL || end == NULL || (start >= end))return;
for(ptr = (uint8_t *)(start + 1);ptr < (uint8_t *)end;ptr++)
{
*checksum ^= *ptr;
}
}
  • OBD协议解析

本次代码解析的协议是ISO15765-4。ISO 15765-4 是一种用于汽车诊断的协议,特别是针对排放系统的要求。它是 ISO 15765 系列的一部分,该系列定义了在 CAN 总线上实现统一诊断服务(UDS)的标准。ISO 15765-4 主要涉及排放相关的诊断要求和数据传输。一般的家用车使用的都是这种协议。

核心代码:

#include "odb.h"
ODB_info g_odb_message;
CAN_TxHeaderTypeDef TxMessage;
CAN_RxHeaderTypeDef RxMessage;
uint8_t ODB_PID[]={PID_RPM,PID_SPEED,PID_DISTANCE,PID_CONTROL_MODULE_VOLTAGE,PID_ENGINE_FUEL_RATE,PID_EVAP_SYS_VAPOR_PRESSURE,PID_COOLANT_TEMP};
void send_pid(uint8_t *buf,uint8_t len)
{
uint8_t send_data[8]={0x02, 0x01 ,0x00 ,0x00 ,0x00, 0x00, 0x00, 0x00};
uint32_t CAN_TX_BOX=0;
static uint8_t i = 0;
if(i>=len) i = 0;
send_data[2] = buf[i];
//发送请求
TxMessage.IDE= CAN_ID_STD;
TxMessage.StdId = ODB_STA_ID;
TxMessage.RTR =CAN_RTR_DATA;
TxMessage.TransmitGlobalTime = DISABLE;
TxMessage.DLC = 8;
if (HAL_CAN_AddTxMessage(&hcan,&TxMessage, send_data,&CAN_TX_BOX) != HAL_OK){
Error_Handler();
}
i++;
}
void ODB_Data_Analysis(uint8_t* rx_message,ODB_info *odb_message)
{
uint8_t pid = 0,data1 = 0,data2 = 0;
int value = 0;
pid = rx_message[2];
data1 = rx_message[3];
data2 = rx_message[4];
switch(pid)
{
case PID_RPM://转速
value = (data2 | data1 << 8) / 4;
g_odb_message.rpm = value ;
break;
case PID_SPEED://车速
value = data1 ;
g_odb_message.speed = value ;
break;
case PID_DISTANCE: // km//表示总行驶里程
value = (data2 | data1 << 8) ;
g_odb_message.Distance = value;
break;
case PID_CONTROL_MODULE_VOLTAGE: // 表示控制模块的电压,通常用于监控电源状态。 v
value = (data2 | data1 << 8) / 1000;
g_odb_message.Control_Module_Voltage = value;
break;
case PID_ENGINE_FUEL_RATE: // L/h //表示单位时间内的燃油消耗量(通常以升每小时或加仑每小时为单位)。
value = (data2 | data1 << 8) / 20;
g_odb_message.engine_fuel_rate = value;
break;
case PID_EVAP_SYS_VAPOR_PRESSURE: // kPa 表示蒸发系统内的气压,通常用于监测油箱的密封性。
value = (data2 | data1 << 8) / 4;
g_odb_message.map = value;
break;
case PID_COOLANT_TEMP: // 水温
value = data1 - 40;
g_odb_message.coolant_temp = value;
break;
defalut:
break;
}
}
void CANFilter_Config(void)
{
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;

if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
{
/* Filter configuration Error */
Error_Handler();
}

}

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
uint8_t data[8];
HAL_StatusTypeDef status;
if (hcan->Instance == NULL){
return;
}
status = HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxMessage, data);
if (status == HAL_OK) {
ODB_Data_Analysis(data,&g_odb_message);
} else {
}
}
  • SHT30数据解析

这里我使用的是模拟量的SHT30 ,我是通过dam触发adc的方式去采集的。

转换关系如下:

1743344070926.png

{53A5C4ED-C40F-4319-97A7-D4CC9B261569}.png

核心代码如下:

#include "sht30.h"

SHT30_INFO sht30_info;
uint16_t my_adc_data[20];
void Get_SHT30_Data(void)
{
memset(&sht30_info,0,sizeof(sht30_info));
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)my_adc_data,20);//因为你选择的软件触发,所以每次采集都需要开启一次
static int i = 0;
for( i = 0; i < 10;i++)
{
if(i != 0 && i != 9)
{
sht30_info.humidity += my_adc_data[2*i] * 330 / 4096;
sht30_info.temperature += my_adc_data[1 + 2*i]* 330 / 4096;
}
}
sht30_info.humidity /= 8.0;
sht30_info.temperature /= 8.0;
sht30_info.temperature = -12.50 + 125.00*(sht30_info.temperature/330.00);
sht30_info.humidity = -66.875+218.75*(sht30_info.humidity / 330.00);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) //DMA采集完成中断服务函数
{
HAL_ADC_Stop_DMA(&hadc1);//关闭DMA的ADC采集
}
  • mpu6050数据解析

这一块开始我是使用互补滤波去处理的,但是发现得到的姿态角漂移太严重了,故采用调DMP库的方式实现的,该说不说是真的稳。

这里的代码我就不多赘述了,太多了,有兴趣的朋友可以下载附件。dmp库的代码虽然多,但是实现功能却很简单,只需要给两个接口函数就行,代码如下:

uint8_t MPU_Write_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
return HAL_I2C_Mem_Write(&hi2c1,(addr<<1)|1,reg,1,buf,len,1000);
}
uint8_t MPU_Read_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
return HAL_I2C_Mem_Read(&hi2c1,(addr<<1)|0,reg,1,buf,len,1000);
}
#define i2c_write MPU_Write_Len
#define i2c_read MPU_Read_Len
  • 数据帧打包

数据帧:[5A A5] [转速] [车速] [水温] ... [校验和] (总长度36字节)

代码如下:

typedef struct {
uint16_t FH;
uint16_t rpm; //表示发动机的转速(RPM),对于监控发动机性能至关重要。
uint16_t speed; //时速
int16_t coolant_temp; //水温
uint8_t Control_Module_Voltage; //控制模块的电压
uint32_t Distance; //总里程
uint32_t Trip_Distance; //小计里程
int16_t map; // //进气压力

float temperature;
float humidity;

float Angle_of_pitch; //俯仰角
float Roll_angle; //横滚角
float Yaw_angle; //偏航角

uint32_t latitude; //纬度 分扩大100000倍,实际要除以100000
uint32_t longitude; //经度 分扩大100000倍,实际要除以100000

uint8_t CHECK_SUM;
}Little_CarPlay;
Little_CarPlay m_carplay;
//数据copy
void Get_CarPlay_Info(void)
{
m_carplay.rpm = g_odb_message.rpm;
m_carplay.speed = g_odb_message.speed;
m_carplay.coolant_temp = g_odb_message.coolant_temp ;
m_carplay.Control_Module_Voltage = g_odb_message.Control_Module_Voltage;
m_carplay.Distance = g_odb_message.Distance;
m_carplay.Trip_Distance = g_odb_message.Trip_Distance;
m_carplay.map = g_odb_message.map;

m_carplay.temperature = sht30_info.temperature;
m_carplay.humidity = sht30_info.humidity;

m_carplay.Angle_of_pitch = mpu6050_data.Angle_of_pitch;
m_carplay.Roll_angle = mpu6050_data.Roll_angle;
m_carplay.Yaw_angle = mpu6050_data.Yaw_angle;

m_carplay.latitude = gpsx.latitude;
m_carplay.longitude = gpsx.longitude;
}
uint8_t get_check_sum(uint8_t* buf,uint8_t len)
{
uint8_t i = 0;
uint8_t check_sum = 0;
for(i = 0;i<len - 1;i++)
{
check_sum += buf[i];
}
return check_sum;
}
//序列化
void serialize_sensor_data( Little_CarPlay *data, uint8_t *buffer, size_t buffer_size)
{
Get_CarPlay_Info();
if (buffer_size < sizeof(Little_CarPlay)) {
return;
}
data->FH = 0x5AA5;
memcpy(buffer, data, sizeof(Little_CarPlay));
data->CHECK_SUM = get_check_sum(buffer,sizeof(Little_CarPlay));
for(int i = 0;i< sizeof(Little_CarPlay);i++)
{
printf("%c",buffer[i]);
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
serialize_sensor_data(&m_carplay,USART1_TX_BUF,sizeof(Little_CarPlay));
}

(2) 树莓派5上位机部分

系统流程图:

  • 数据解析

该部分的代码流程图:

{A0F32078-1E22-45C9-9FA3-6246923A581E}.png


核心代码:

bool validateChecksum(const QByteArray &frame)
{
const uint8_t* data = reinterpret_cast<const uint8_t*>(frame.constData());

// 完全复制下位机逻辑:包括帧头在内的所有字节参与计算
uint8_t checksum = 0;
for(size_t i = 0; i < sizeof(Little_CarPlay) - 1; ++i) {
checksum += data[i];
}

// 最终校验位是前面所有字节的累加和
return (checksum == data[sizeof(Little_CarPlay) - 1]);
}
void MainWindow::processValidFrame(const QByteArray &frame)
{
// 确保内存对齐(重要!)
#pragma pack(push, 1)
struct AlignedCarPlay : public Little_CarPlay {
// 保证与下位机完全相同的内存布局
};
#pragma pack(pop)

AlignedCarPlay carData;
memcpy(&carData, frame.constData(), sizeof(AlignedCarPlay));

// 调试输出原始数据
qDebug() << "Raw frame data:";
for(int i = 0; i < frame.size(); ++i) {
qDebug() << QString("Byte %1: 0x%2")
.arg(i, 2, 10, QChar(' '))
.arg(static_cast<uint8_t>(frame.at(i)), 2, 16, QChar('0'));
}

// 更新数据
updateVehicleData(carData);
}
void MainWindow::usartReadData()
{
static QByteArray buffer;
buffer += usart->readAll();

// 帧头匹配模式(0x5A 0xA5)
static const char HEADER_PATTERN[] = "\xA5\x5A";
static const int HEADER_SIZE = 2;

while(buffer.size() >= FRAME_SIZE)
{
// 查找帧头(二进制精确匹配)
int headerPos = buffer.indexOf(HEADER_PATTERN);

// 未找到有效帧头
if(headerPos < 0) {
buffer.clear();
return;
}

// 丢弃帧头前的无效数据
if(headerPos > 0) {
buffer.remove(0, headerPos);
continue;
}

// 检查完整帧
if(buffer.size() < FRAME_SIZE) {
return; // 等待更多数据
}

// 提取帧数据(零拷贝方式)
QByteArray frame = QByteArray::fromRawData(
buffer.constData(),
FRAME_SIZE
);

// 移动缓冲区
buffer.remove(0, FRAME_SIZE);

// // 校验和验证
// if(!validateChecksum(frame)) {
// qDebug() << "Checksum failed. Frame discarded.";
// continue;
// }

// 处理有效帧
processValidFrame(frame);
}

// 防止缓冲区膨胀(最大保留2帧数据)
if(buffer.size() > FRAME_SIZE * 2) {
buffer.clear();
qWarning() << "Buffer overflow protection triggered";
}
}

// 单独的数据更新函数
void MainWindow::updateVehicleData(const Little_CarPlay &data)
{
// 原子操作更新数据
QMutexLocker locker(&dataMutex);

currentRpmValue = data.rpm;
currentSpeedValue = data.speed;
rollAngle = data.Roll_angle;
pitchAngle = data.Angle_of_pitch;
yawAngle = data.Yaw_angle;
latitude = data.latitude / 100000.0; // 转换单位
longitude = data.longitude / 100000.0;
temperature = data.temperature;
humidity = data.humidity;

// 触发界面更新(跨线程安全)
QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
}
  • 仪表绘制

该部分流程图如下:

{76B2E817-28BD-4FD5-AC24-337FB7901F96}.png

主要代码:

void MainWindow::paintEvent(QPaintEvent *)
{
QPainter painter(this);
initCanvas(painter);

int rad = std::min(width(), height()) / 3;
int verticalOffset = height() * 0.2; // 下移20%高度

// 绘制速度表(左侧)
painter.save();
painter.translate(width() * 0.25, height() / 2 + verticalOffset);
drawSpeedMeter(painter, 0, 0, rad);
painter.restore();

// 绘制转速表(右侧)
painter.save();
painter.translate(width() * 0.75, height() / 2 + verticalOffset);
drawRpmMeter(painter, 0, 0, rad);
painter.restore();

// 计算传感器数据显示区域
int infoWidth = width() * 0.9;
int infoHeight = height() * 0.15;
QRect infoRect((width() - infoWidth)/2, height() * 0.05, infoWidth, infoHeight);
// 绘制传感器数据
drawSensorData(painter, infoRect);
}

六,功能展示图及说明

开发板及各模块连接的效果图如下:

整体实物图.jpg

可以实时显示汽车的转速,车速,车身姿态,和车内的温湿度以及车辆的定位信息。

车速仪表:动态显示车辆的车速信息,当车辆速度到达180,会进入警示区域

{72B8833E-2D88-4955-9E60-290B558AF87A}.png

转速仪表:动态显示车辆的转速信息,当车辆转度到达12000,会进入警示区域

{63F1A2CF-A4AC-4D74-8A1F-969795EBE7B7}.png

姿态仪表:实时显示车辆的姿态信息,红色的线会随着ROLL的值上下浮动,蓝色的十字同样也会随着PITCH值左右摆荡。

{F1EF0C17-A2EA-46D4-A8AD-7FE1178D5D7D}.png

GPS信息栏:主要显示经纬度信息,和偏航角信息,不过这里显示的偏航角信息来自MPU6050。

{F07F8020-0182-4387-8955-77DCCBDDA9C8}.png

温湿度信息栏:数字化显示温湿度,同样下方还有一个温湿度进度条用于图形化显示,提高逼格。

{A36C3624-5F35-437D-A08C-DC4641CEFDC8}.png

六,设计中遇到的难题和解决方法

  • 问题一:can会环模式正常,正常模式就是无法收发数据。
    • 解决方法:通过查阅资料发现TJA1051的VIO角是必须接参考电平的,而TJA1050是可以悬空的。

{F4DCB56F-E51A-4EB0-95D4-1981796A27FC}.png

七,心得体会

在本次活动中,我深入学习并掌握了许多关键技术和技能,从原理图的制作到 PCB 的焊接,再到 STM32 数据的采集、处理与打包,以及在树莓派 5 上编写 Qt 上位机程序。每个环节都让我对电子系统的整体工作流程有了更系统的理解,并进一步提高了我的动手能力和技术素养。在这个过程中,我深刻体会到理论与实践相结合的重要性。在此,我衷心感谢电子森林和贸泽电子的大力支持,使我能够顺利完成这一系列学习与实践,积累宝贵的经验。这次活动不仅提升了我的技术水平,也让我对未来的项目充满信心。

附件下载
06_odb-上位机.zip
树莓派5Qt上位机
01_can-下位机.7z
stm32下位机
团队介绍
独立完成
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号