2025交互标牌 - 基于Seeed XIAO ESP32S3 Sense开发板实现行车辅助灯牌
一、 项目介绍
本项目使用Seeed XIAO ESP32S3 Sense开发板为主控,并结合多个外部传感器模块实现一个多功能行车辅助灯。
辅助灯的功能是通过两个按键切换灯板可显示的内容,灯板可以根据需要显示本车速度、目标车辆速度、后方碰撞预警和转向指示等功能。
二、 简短的使用到的硬件介绍
硬件围绕主控Seeed XIAO ESP32S3 Sense开发板并增加了传感器、及交互组件。
1. Seeed XIAO ESP32S3 Sense开发板
Seeed Studio XIAO ESP32S3 Sense 采用高集成度的 Xtensa 处理器 ESP32-S3R8 SoC,支持 2.4GHz WiFi 和低功耗蓝牙® BLE 5.0 双模,芯片上有 8MB PSRAM 和 8MB FLASH,板载一个 SD 卡插槽、一个插入式 OV2640 摄像头传感器和一个数字麦克风,具有锂电池充电管理功能(我这里单独外挂了一个充放电一体化模块),整体尺寸非常小巧。
2. MPU6050
集成了3轴加速度计和3轴陀螺仪的6轴运动处理组件,使用I2C通讯接口。
3. GPS模块
使用的GPS模块是一个4G+GPS模块,使用串口通讯,可实现常规网络协议通讯及GPS定位功能。这里只使用了GPS定位功能。
4. 24GHz移动车辆状态感应模组
这个模块是各车辆检测模块,能检测靠近或远离的车辆,探测距离100m,探测速度120KM/H,使用串口通讯。
5. WS2812灯板
WS2812灯珠组成的灯板,用于显示交互信息。
6. 为方便接线,这里制作了一个转接板用来连接主控及传感器附件,接口都是2.54的排针或者排母。
蜂鸣器为有源蜂鸣器,连接在D0引脚上,使用三极管进行驱动,作为警报提示音。
设计了两个按键用于交互,分别连接在D7和D8引脚上。
充电使用了模块化的充放电一体模块直接焊接在连接板上。
4G模块及24GHZ模块分别连接D9、D10及D3、D4引脚使用串口与主控通讯,WS2812灯板通过D6引脚与主控通讯。
MPU6050通过I2C接口与主控进行通讯,连接在D1、D2引脚,同时预留了接口,可以扩展连接OLED屏幕或者地磁传感器模块。
为了实现转向提示功能,我还使用了ESP32开发板作为辅助,其上也配置了MPU6050和TFT屏幕。MPU6050用于获取角度,屏幕用于指示当前角度,该开发板用于模拟方向盘并数据回传。
三、 方案框图和项目设计思路介绍
为实现测速、预警以及显示等功能,需要从外部通过不同的协议引入对应的外设,同时还需要考虑外设对引脚占用的数量,尽量考虑IIC或者UART这种两线设置一线的设备。
1. 为实现后方碰撞预警这里引入了24GHZ雷达模块,该模块使用串口通讯,Seeed XIAO ESP32S3 Sense开发板使用硬件串口2来与之交互。通过对接收数据进行解析后来实现周期性检测后方靠近车辆的速度及距离。
2. 加入GPS模块来测量本车的速度,该模块同样使用串口通讯,Seeed XIAO ESP32S3 Sense开发板通过串口1来与之交互。
3. MPU6050为加速度计和陀螺仪,这里主要使用来检测车辆的加速度实现对车辆急刹车的检测。MPU6050使用IIC接口与MCU进行通讯。
4. WS2812灯板通过单IO与单片机进行通讯,主要实现信息交互的功能,将速度、警报等信息反馈给用户。
5. 通过控制IO的高低电平来控制有源蜂鸣器的开关及节奏,增强警报效果。
6. 灯板同一时间可显示信息有限,通过按键来切换灯板可显示的信息,同时加入按键中断来提高响应速度。
整体硬件框图如下:
四、 软件流程图和关键代码介绍
软件流程大体如下:初始化串口通信(雷达、碰撞预警),设置GPIO引脚模式,启动I2C总线并初始化MPU6050传感器,初始化LED灯板和显示矩阵,创建信号量和任务,注册中断服务程序(ISR)--用于按键输入检测,发送GPS启动指令并等待响应,初始化ESP-NOW并注册接收回调函数—用于从辅助设备获取方向盘数据,创建RTOS任务(雷达解析—获取后方目标的速度及距离、GPS数据解析—获取本车速度、MPU6050数据解析—获取本车加速度、转向动画—满足条件时触发、警告动画—满足条件时触发),主循环中更新MPU6050数据。软件启动后主要数据的获取及更新均在RTOS中进行,动画及显示界面通过按键进行控制和切换。
因为使用arduino开开发,引入了外部库来处理GPS、MPU6050及WS2812的数据解析或驱动,整体代码较为简单,重点在于对24GHZ雷达模块反馈数据的解析及灯板动画。
对于24GHZ雷达模块的数据解析主要拆分成了两部分来实现,一部分是RTOS的任务,一部分是对数据按照手册格式进行拆分对用。
24GHZ雷达模块RTOS任务部分,通过循环调用和延时来周期性的读取雷达发来的数据,通过状态机控制,如果未找到数据头且收到的数据长度比数据头长度短则一直接收,当收到的数数据大于或等于数据头的长度时与设定的数据头进行比对,如果是则进行第二部分的解析,如果不是则丢弃第一个数据再读取尾部数据,以此来循环比对,直至找到数据头。
if (!parserState.headerFound) {
// 搜索帧头
parserState.buffer[parserState.idx++] = byte;
// 防止溢出
if (parserState.idx >= sizeof(parserState.buffer)) {
Serial.println("[WARN] Buffer overflow, resetting...");
parserState.idx = 0;
return;
}
// 检查是否匹配帧头(至少需要4字节)
if (parserState.idx >= 4) {
if (memcmp(parserState.buffer, DATA_HEADER, 4) == 0) {
parserState.headerFound = true;
parserState.idx = 4; // 从长度字段开始存储
Serial.println("[INFO] Header found");
} else {
// 滑动窗口:丢弃最早字节,继续搜索
memmove(parserState.buffer, parserState.buffer + 1, 3);
parserState.idx = 3;
}
}
}
找到数据头后改变状态机值,开始读取数据正常读取数据后续字节,当读取到长度字节则按照规则计算实际数据长度,并判断是否收到足够的数据,收到足够的数据后则进行数据位的比对,如果数据尾等于指定的内容则进行数据处理。
parserState.buffer[parserState.idx++] = byte;
// 检查是否收到足够数据
if (parserState.idx >= 6) { // 已经收到长度字段
uint16_t dataLen = parserState.buffer[4] | (parserState.buffer[5] << 8);
uint16_t expectedLength = 4 + 2 + dataLen + 4;
// 检查是否完整接收
if (parserState.idx >= expectedLength) {
// 验证帧尾
if (memcmp(&parserState.buffer[expectedLength - 4], DATA_FOOTER, 4) == 0) {
processRadarFrame(parserState.buffer, expectedLength);
} else {
Serial.println("[ERROR] Footer mismatch");
}
// 重置状态
parserState.idx = 0;
parserState.headerFound = false;
}
}
24GHZ雷达模块数据处理部分先判断提供的数据长度是否满足最小数据长度,不满足则退出。之后使用长度字节对实际收到的有效数据长度进行验证,验证一致则继续。之后处理数据目标数量部分,如果数据的有效长度是0,则认为未检测到目标,进行无目标处理,否则则按照数据规则顺序进行数据对应。
void processRadarFrame(const uint8_t* frame, uint16_t length) {
if (length < 10) { // 基础校验(长度至少包含帧头4 + 长度2 + 帧尾4)
Serial.println("[ERROR] Frame too short");
return;
}
uint16_t dataLen = frame[4] | (frame[5] << 8);// 提取数据长度(小端格式)
if (length != (4 + 2 + dataLen + 4)) { // 验证长度一致性
Serial.printf("[ERROR] Length mismatch. Expected:%d Actual:%d\n",
4 + 2 + dataLen + 4, length);
return;
}
if (dataLen == 0) {// 处理无目标情况
Serial.println("No targets");
targetDistance = 100 ;
targetSpeed = 0;
alarmOth = false; //如果未检测到目标警报关闭
if(menu_index == OTHERSPEED_MODE){
showText("NT"); //如果是显示对方速度模式且未获得目标则显示NT(无目标)
}
return;
}
uint8_t targetCount = frame[6]; // 提取目标信息
uint8_t alarmStatus = frame[7];
// 验证目标数量合法性
if (targetCount > (dataLen - 2) / 5) { // dataLen包含targetCount(1) + alarmStatus(1)
Serial.printf("[ERROR] Invalid target count:%d\n", targetCount);
return;
}
Serial.printf("Targets:%d Alarm:0x%02X\n", targetCount, alarmStatus);
float minDistance = INFINITY;
int nearestTargetIndex = -1;
for (uint8_t i = 0; i < targetCount; ++i) {// 解析每个目标(每个目标占5字节)
uint16_t offset = 8 + i * 5;
if (offset + 4 >= length) break; // 防止越界
int angle = frame[offset] - 0x80; // 角度(有符号)
targetDistance = frame[offset + 1]; // 距离
uint8_t speedDir = frame[offset + 2]; // 方向位
targetSpeed = frame[offset + 3]; // 速度绝对值
uint8_t snr = frame[offset + 4]; // 信噪比
if(menu_index == OTHERSPEED_MODE){
showText(String(targetSpeed)); //如果是显示对方速度模式且获得了当前速度则显示当前速度
}
if(old_Speed!=-1 && speedDir == 0x01){
float relativeSpeed_mps = (targetSpeed - old_Speed) / 3.6f; // km/h -> m/s
if (relativeSpeed_mps > 0.1f) { // 避免除以0或极低速情况
float timeToCollision = (float)targetDistance / relativeSpeed_mps;
if (timeToCollision < 3.0f) { // 3秒内可能发生碰撞
alarmOth = true; //启动报警
} else {
alarmOth = false; //关闭报警
}
Serial.printf("Time to collision: %.2fs, Alarm: %s\n",
timeToCollision, alarmOth ? "ON" : "OFF");
} else {
alarmOth = false;
}
}
Serial.printf("Target%d: %d°, %dm, %dkm/h %s SNR:%d\n",
i + 1, angle, targetDistance, targetSpeed,
speedDir ? "Approach" : "Depart", snr);
}
}
灯板的动画问题其实比较类似,这里拿一个警报的部分说下。收到警报指示后灯板显示动画,如果不做处理这个动画很可能就闪一下就结束了,我这里用了一个for循环和一个常量进行控制。修改了之后变成收到警报指示后动画执行20次方结束,确保能看到明显的动画。
if(menu_index == COLLISION_WARNING && alarmAcc || alarmOth){
if(alarmAcc){
for(int i = 0; i<20; i++){
top_x = center_x;
top_y = center_y - 0.6 * size; // 使顶部顶点随size增加而上移,调整三角形的高矮
left_x = center_x - size;
left_y = center_y + size - 1; // 底部顶点随size增加而下移
right_x = center_x + size;
right_y = left_y;
if(!colorRev){
color = TFT_RED;
}else{
color = TFT_ORANGE;
}
matrix.clear();
matrix.fillTriangle(top_x, top_y, left_x, left_y, right_x, right_y, color);
matrix.show();
digitalWrite(BUZZ_PIN, colorRev ? HIGH : LOW); //反转蜂鸣器,
colorRev=!colorRev;
size = (size % 7) + 1;
// 模拟动画延迟
vTaskDelay(pdMS_TO_TICKS(50));
}
matrix.clear();
matrix.show();
}
五、 功能展示图及说明
按键:按键1显示当前目录,按键2切换目录。
按键说明
按键切换目录
功能0:显示当前本车速度。设计的是连接和解析成功则显示实际速度,如果未连接则显示NC。
未获取到有效GPS速度时显示NC
获取到GPS速度后显示当前速度
功能1:检测靠近的目标速度。这个模块可以同时检测靠近和远离目标的速度,我这里只检测靠近目标的速度,如果检测到目标则显示目标速度,如果未检测到目标则显示NT。
功能1,未检测到目标时显示NT
有目标时测量并显示目标速度
有目标时测量并显示目标速度
功能2:碰撞预警和急刹车预警。碰撞预警是将GPS速度、雷达检测速度和距离进行碰撞时间计算小于3秒则发出警报,这里碰撞预警就不演示了,暂时没那条件。急刹车预警比较简单,就是检测陀螺仪的Y轴减速度值,大于1则报警。为了区分两种效果急刹车使用的警示灯的颜色是橙色和红色,碰撞预警的警示灯颜色是紫色和粉色。
切换至功能2
急刹车警示,同时蜂鸣器周期性鸣响
急刹车警示,同时蜂鸣器周期性鸣响
碰撞预警,同时蜂鸣器周期性鸣响
功能3:转向提示。这里需要一个从机模拟安装再方向盘上的传感器,通过MPU6050侦测方向盘的转动,将转动数据通过ESPNOW向主机发送方向盘数据,主机根据方向盘数据做出动画提示。
切换至功能3
左转向提示动画
右转向动画提示
六、 项目中遇到的难题和解决方法
使用arduino来实现项目极大的降低了项目的难度,但是在进行24GHZ模块返回数据解析时有点蒙圈。模块返回的数据没有空格,长度不定长,之前只做过定长或以空格结尾的数据。刚开始使用arduino的readString函数,虽然实现了对数据的读取和解析,但是读取速度较慢,之后将通过各找找资料查案例,发现使用循环比对数据头,执行起来效率果然很快。
第二个问题就是这个ESPNOW协议问题。之前有使用arduino和ESP实现过多次ESPNOW的数据传输,所以就直接抄过来用了,但是测试发现不能正常接收。之后又专门将ESPNOW相关代码移到空白程序中单独一步步验证进行测试方才解决。
七、 对本次活动的心得体会
Seeed XIAO ESP32S3 Sense开发板正如其名字非常小巧,可以较容易的集成到项目中去,非常荣幸有机会使用这个性能强大、体积小巧的开发板来进行学习。借助此次机会使我对RTOS的使用有了进一步的提升(虽然只是在arduino中),对串口数据解析也有了进一步掌握。非常感谢电子森林举办的这次活动,希望后面多多组织,让我有机会多多参与。