基于STM32F103的机场智能驱鸟系统:传感器联锁与Modbus上报
简介基于STM32F103的机场驱鸟装置是一套完整的嵌入式工程包面向STM32入门学习者、电子设计竞赛参赛者以及机场鸟击防治的技术人员。整个系统以ARM Cortex-M3内核的STM32F103微控制器为核心通过脉宽调制信号精确控制舵机旋转带动超声波模块对起降区域持续扫测当超声波回波时间表明有鸟类进入警戒距离时控制器会立即停止舵机转动并对准目标连续发射高频超声波脉冲将其驱离完整覆盖测距、方位调整、动作执行三个环节。压缩包共159个文件容量约5.12MB包含C源文件、头文件、Keil工程配置文件、STM32CubeMX硬件初始化文件、编译生成的HEX固件以及MAP、LST、AXF等调试辅助文件目录划分清楚可用Keil或STM32CubeIDE直接编译下载也可对照源码理解定时器输出PWM、超声波回波捕获、外部中断响应等典型外设用法。已有272人学习下载。该工程尤其适合做课程设计或竞赛作品代码注释较完整GPIO、定时器、串口等驱动段可直接复用调试映射文件还能辅助新手排查链接告警明白程序在存储器中的分布若想添加远程监控串口通信代码也已给出可在此基础上快速改造。1. 项目定位为什么机场驱鸟要选 STM32F103一台老式驱鸟器只会按固定时间放炮鸟群反而学会了踩着炮响节奏觅食。当它被换成基于STM32F103的机场驱鸟装置后情况完全不同微波和红外双通道实时探测鸟群接近毫秒级响应先高频声波警告再频闪灯最后才触发煤气炮。整个过程不用人工盯每一条事件都通过 RS485 上报到驱鸟管理平台。这套项目适合机场鸟防技术人员做现场试点也适合嵌入式开发者学习状态机、外设中断驱动和 Modbus 协议。它的价值不在硬件贵而在于用很少的器件把“检测-确认-分级驱离-上报”闭环起来替换掉只会定时响的老设备。2. 系统架构与硬件选型传感器、执行器与通信链路的取舍2.1 整机分层感知、决策、执行、上报这套装置不是一块开发板跑几个灯而是一台需要 24 小时无人值守、还要应付机场供电波动的嵌入式设备。常见做法是拆成四层感知层、决策层、执行层和上报层。感知层把微波感应模块、红外热释电、气压开关和温度探头接到 STM32 的 GPIO 与 ADC决策层在 F103 内部完成信号滤波、双通道确认、状态机和联锁判断执行层通过光耦隔离的继电器和 MOSFET 输出控制超声波驱鸟器、频闪灯、煤气炮和音频模块上报层用 RS485 总线把设备状态、鸟情计数和事件日志送给管理主机。源码目录按 bsp、driver、app 三层拆分是我比较习惯的组织方式。这样某一路传感器换型只需要改对应 driver不用在 main 函数里挖洞。参考目录结构如下f103_bird_deterrent/ bsp/ clk.c gpio.c uart.c tim.c adc.c driver/ microwave.c pir.c gas_cannon.c ultrasonic.c flash_light.c audio_player.c app/ bird_detect.c bird_state_machine.c modbus_rtu.c event_log.c main.c stm32f1xx_it.cbsp 只负责芯片底层外设初始化driver 是对传感器和执行器的具体操作app 是业务状态机。拿到这份源码的人第一眼就能知道哪里是改协议、哪里是改设备逻辑也方便后续做自动化测试。2.2 关键器件参数对比与选型理由STM32F103C8T6 是这套装置的主控72MHz 主频、64KB Flash、20KB RAM带 3 路 USART、2 路 SPI、2 路 ADC工业级温度范围覆盖机场户外的冬夏温差。机场围界驱鸟点数量多、分布广单个控制板成本很低比 PLC 方案便宜也更容易做成自己的控制逻辑。有人问为什么不用更高主频的芯片答案是现场 485 总线 9600 波特率F103 处理状态机和日志绰绰有余低成本低功耗才是长期运维的刚需。核心器件选型一览表如下后面二次开发时可以按这个表去替换器件选用要点可选替代方案主控STM32F103C8T672MHz64KB Flash工业级F103RCT6Flash 翻倍微波感应模块数字输出探测距离 5-8m灵敏度可调24GHz 微波雷达模组红外热释电覆盖 110 度带菲涅尔透镜双元红外探测器超声波驱鸟器15-25kHz 变频需要功放驱动大功率压瓷喇叭煤气炮执行器12V 电磁阀加点火线圈受安全开关控制液化气爆鸣管485 收发器支持 1200m 总线带 TVS 防护国产等效半双工收发芯片音频播放模块串口触发播放鹰叫音频喇叭外接语音芯片加 DAC这里要特别说明选型逻辑微波和红外是两个互补通道红外确认热源微波确认移动目标两者同时触发才能过滤树叶摇晃、气温变化带来的误报。虽然每加一个传感器都会增加成本和接线但在机场场景里误报比漏报更危险误触发一次煤气炮就可能造成事故。所以我宁可多一路检测也不让单信号直接放炮。2.3 引脚规划与接线建议F103 引脚不算宽裕但刚好够这组设备。接线顺序有讲究强电侧继电器、点火线圈和弱电侧GPIO、串口要隔开否则雷击时浪涌会顺着 PCB 爬到单片机里。下面是我验证过的一版引脚分配功能引脚外设配置说明微波感应输入PA0GPIO 下降沿中断带 10k 下拉红外热释电输入PA1GPIO 上升沿中断带滤波电容煤气炮点火PB0推挽输出高有效受 PB1 联锁燃气电磁阀PB1推挽输出高有效先开阀后点火超声波驱鸟PA8/PA9TIM1_CH1/CH2 PWM用于 15-25kHz 扫频频闪灯PB9开漏输出外部 12V 电路音频模块控制USART2_TX串口命令异步播放鹰叫RS485 ModbusUSART19600-8N1使用收发器芯片接线时继电器线圈两端必须并联续流二极管不然关断瞬间的反向电动势会在 12V 母线上打出尖峰轻则让 F103 复位重则烧 GPIO。上电时序也要处理先让传感器和 485 稳定 500ms再初始化执行器输出并默认全部为禁止状态避免复位瞬间高压侧动作。2.4 供电与现场防护机场驱鸟点一般有 220V 市电但户外箱电压波动和雷击浪涌很常见。常见设计是 220V 经 AC-DC 模块转为 12V 中间母线继电器、电磁阀、频闪灯都从 12V 取电再用降压模块转 5V 给传感器和音频模块3.3V 由 LDO 单独供给 STM32。这样强电到弱电逐级降压每一级都加滤波电容抗干扰能力比直接 5V 供单片机好很多。防雷和防浪涌不能只在总闸做要在板上入口放压敏电阻和 TVS 管485 接口 A/B 线也要各对地加 TVS。另外加一颗外部电源监控复位芯片当电压跌到阈值以下时自动拉低 NRST比单纯依赖内部 BOR 更可靠。我一般还会把备用电池接在 5V 母线上断电后至少撑 10 秒保证事件日志写完再掉电。3. 核心模块拆解一个鸟情从检测到驱离的完整链路3.1 双通道鸟情确认从传感器信号到软件决策驱鸟系统最难的不是驱动煤气炮而是判断“确实有鸟”。微波感应模块的探测范围是一个扇形空间飞机灯光、巡场车、草地的气流都可能让它跳电平红外热释电在太阳底下也会误报。常见做法是把两路信号放进同一个时间窗口只有窗口内两个通道都有触发才认为是一次有效鸟情。下面这段代码是双通道确认的核心理论上可以放到 1ms 任务里轮询/* bird_detect.c双通道窗口确认 */ #define DETECT_WINDOW_MS 200 static uint32_t window_start; static uint8_t radar_ok; static uint8_t pir_ok; uint8_t bird_detection_poll(void) { uint32_t now HAL_GetTick(); if (now - window_start DETECT_WINDOW_MS) { uint8_t result (radar_ok pir_ok) ? 1 : 0; radar_ok 0; pir_ok 0; window_start now; if (result) return 1; } if (read_sensor(SENSOR_RADAR)) radar_ok 1; if (read_sensor(SENSOR_PIR)) pir_ok 1; return 0; }逻辑说明每次进入函数先检查 200ms 窗口是否到期到期就把窗口内两路标志取与后清零窗口未到期时持续采集传感器电平并置位。这种设计不会在中断里做除法或延时所有运算都在主循环轮询中完成。参数上DETECT_WINDOW_MS是现场最需要调的值。机场风大、草多窗口加大到 500ms 能明显降低误报但鸟群飞行速度快窗口太大又会漏掉快速掠过的小鸟。我建议把它做成配置项存到片上 Flash现场用上位机改而不是每次重新烧录固件。3.2 驱离执行与安全联锁执行层最容易翻车的是煤气炮。如果传感器把影子或气流当目标炮口又没有安全确认就会在检修人员靠近时点火这种事故一出就是大麻烦。所以我在煤气炮驱动函数里加了三层联锁本地钥匙开关置到自动、气压开关正常、检测区域无人员进入。任一条件不满足点火被抑制并记录一条事件。/* gas_cannon.c带安全联锁的煤气炮触发 */ uint8_t trigger_gas_cannon(void) { if (!read_safety_key_switch()) { log_event(gas suppressed: key not armed); return 0; } if (!read_pressure_ok()) { log_event(gas suppressed: pressure low); return 0; } if (read_presence_zone()) { log_event(gas suppressed: person in zone); return 0; } HAL_GPIO_WritePin(GAS_VALVE_PORT, GAS_VALVE_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(GAS_IGNITE_PORT, GAS_IGNITE_PIN, GPIO_PIN_SET); delay_ms(120); HAL_GPIO_WritePin(GAS_IGNITE_PORT, GAS_IGNITE_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(GAS_VALVE_PORT, GAS_VALVE_PIN, GPIO_PIN_RESET); log_event(gas cannon fired); return 1; }逻辑说明函数开头三个 if 依次检查硬件输入不满足就直接返回不执行任何输出。满足条件后先开电磁阀再开点火线圈保持 120ms 后按先关点火、后关阀门的顺序复位。这个顺序不能颠倒否则燃气堆积可能形成二次点火。120ms 是一个常用经验值点火太长会烧线圈太短可能点不着。连续点火失败两次以后我建议程序主动停止自动触发并上报避免浪费燃气也避免现场人员以为是设备有效。相比一失败就不断重试这种“可失败并记录”的行为更容易获得信任。3.3 主状态机静默、预警、强驱离整个业务用状态机串起来避免散落一堆 if。状态包含静默、预警、强驱离和手动从静默进入预警只需要双通道确认预警持续 3 秒仍检测到鸟升级到强驱离如果预警期间没有持续鸟情就回到静默。这样就不会有一看附近有鸟就立刻放炮的毛躁行为。/* bird_state_machine.c主控状态机 */ typedef enum { ST_IDLE, ST_WARNING, ST_DISPERSAL, ST_MANUAL } bird_state_t; static bird_state_t g_state ST_IDLE; static uint32_t warn_enter_ms 0; void bird_state_task_10ms(void) { switch (g_state) { case ST_IDLE: if (bird_detection_poll()) { g_state ST_WARNING; warn_enter_ms HAL_GetTick(); start_ultrasonic_sweep(); start_flash_light(); } break; case ST_WARNING: if ((HAL_GetTick() - warn_enter_ms 2000) !bird_detection_poll()) { stop_ultrasonic_sweep(); stop_flash_light(); g_state ST_IDLE; } else if (HAL_GetTick() - warn_enter_ms 3000) { g_state ST_DISPERSAL; trigger_gas_cannon(); start_audio_playback(); } break; case ST_DISPERSAL: if (HAL_GetTick() - warn_enter_ms 12000) { stop_audio_playback(); stop_flash_light(); g_state ST_IDLE; } break; case ST_MANUAL: /* 手动模式由上位机接管状态机不干预 */ break; default: g_state ST_IDLE; break; } }逻辑说明这个状态机每 10ms 执行一次预警到达 2 秒后如果当前没有持续鸟情就认为鸟已经飞过不升级不放炮持续到 3 秒仍确认才进入强驱离。从预警进入开始统一计时12 秒后整个流程结束强驱离实际运行约 9 秒避免执行器长时间空转。为什么不用 RTOS这套逻辑单链路、状态少裸机协作式调度完全够用而且调试时不需要处理任务切换带来的随机时序问题。真到了要接云台、GPS 和本地触摸屏的阶段再移植 RTOS 也不迟状态机代码依然能复用。3.4 事件日志让每一次点火都有据可查机场管理者最关心的不是设备响不响而是“鸟情来了设备做了什么”。这个装置需要把每次状态变化、成功或失败的点火、每次被安全逻辑抑制的事件写进日志。常见做法是在 RAM 里维护环形缓冲区定期刷到备份 Flash避免频繁擦写导致 Flash 提前报废。/* event_log.c简易事件记录 */ void log_event(const char *msg) { static log_entry_t buf[EVENT_RING_SIZE]; static uint8_t head; log_entry_t *entry buf[head]; entry-tick HAL_GetTick(); entry-type 1; snprintf(entry-text, sizeof(entry-text), %s, msg); head (head 1) % EVENT_RING_SIZE; flash_lazy_write(entry); }逻辑说明所有事件先写 RAM 环形队列再由慢速任务在系统空闲时刷到 Flash保证log_event不会阻塞状态机。snprintf限制最大长度避免长消息把缓冲区撑爆。读取时通过 Modbus 寄存器把队列尾部的事件逐条发给上位机运维人员就能看到到底是被安全抑制还是真的触发了。4. 通信与远程管理Modbus RTU 协议栈和状态上报4.1 为什么选 Modbus RTU 而不是私有协议机场围界已经敷设了很多双绞线驱鸟管理平台大多支持 Modbus RTU。选择 RS485 加 Modbus最直接的好处是调试成本低万用表量 A/B 线压差能判断物理层CRC 判断链路层监控软件按照规范解析就能上线。私有协议看起来简洁但换一个上位机工程师就头疼一次尤其在机场这种需要长期运维的场景标准协议反而更高效。4.2 寄存器映射与功能码设计Modbus 从站在这个项目里只需要两个功能码03H 读保持寄存器、06H 写单个保持寄存器。设备地址由拨码开关决定默认 1。寄存器表设计如下寄存器地址读写含义0x0000读设备运行状态bit0 自动模式bit1 预警中bit2 强驱离中0x0001读当日鸟情次数掉电保存0x0002读12V 母线电压单位 mV0x0010写手动驱离指令写 1 触发0x0011读写工作模式0 自动1 手动2 单测0x0020读固件版本号地址分配有个细节只读状态放在前段写命令放在后段中间留空避免现场人员误操作读到不存在的地址。上位机组态时把 0x0010 做成一个按钮点击写 1 就能远程单次驱离比到现场按开关高效得多。4.3 CRC16 校验和帧解析Modbus RTU 每条报文末尾都有 2 字节 CRC采用标准多项式反射算法。为了在 F103 上保持足够的余量我通常用查表法但为了让你看明白原理下面给出逐位计算的版本/* modbus_rtu.cCRC16 计算 */ uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i; for (i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }说明0xA001是标准 Modbus 多项式 0x8005 反射后的结果。发送时要注意字节序低字节在前、高字节在后上位机才能验过 CRC。我第一次调这个项目时就是在这里翻了车收到上位机一直报 CRC 错误查了半天发现寄存器值本身是对的高低字节却反了。帧接收用串口中断加超时判断。9600 波特率下3.5 个字符时间约 4ms常见做法是每收一字节就重置一次定时器4ms 没等到下一字节就认为一帧结束。简化版接收代码如下void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buf[rx_cnt] rx_byte; if (rx_cnt 8) { process_modbus_request(rx_buf, rx_cnt); rx_cnt 0; } HAL_UART_Receive_IT(huart, rx_byte, 1); } }代码说明每收到一字节就判断长度达到 8 字节后进入解析函数。这只是演示实际 Modbus 请求长度随功能码变化应改成按功能码从rx_buf[1]推算长度同时配置一个超时定时器防止丢字节后卡死。4.4 心跳上报与远程复位设备除了响应主站轮询还要主动上报存活状态。常见做法是每 5 秒向管理主机发一条读保持寄存器请求内容包含设备状态和鸟情计数主机连续 3 次收不到就让设备在界面上变红。下面这段代码把心跳帧拼好发出/* heartbeat_task.c心跳帧组装 */ void heartbeat_task_1000ms(void) { uint8_t frame[8]; static uint32_t counter; counter; if (counter % 5 ! 0) return; frame[0] SLAVE_ADDR; frame[1] 0x03; frame[2] 0x00; frame[3] 0x00; frame[4] 0x00; frame[5] 0x01; uint16_t crc modbus_crc16(frame, 6); frame[6] (uint8_t)(crc 0xFF); frame[7] (uint8_t)(crc 8); HAL_UART_Transmit(huart1, frame, 8, 50); }逻辑说明每 5 次定时任务触发一次读取设备自身 0x0000 寄存器主机据此判断设备在线。严格来说 Modbus 是主从协议从站不应该主动发请求但不少现场设备都这么用。如果管理平台不支持主动上报就把心跳改成被动轮询由主机定时来读状态字。远程复位用看门狗实现写 0x0010 寄存器后设备先记录复位原因再调用NVIC_SystemReset()而不是主站远程断电。这样可以在复位日志里找到谁在什么时候发过复位命令后续排查设备反复重启时能省很多时间。5. 常见问题与避坑机场电磁环境下的死机、误触发与联锁5.1 误触发微波模块把气流和影子当目标现象没有鸟预警灯和频闪灯却在闪偶尔还会触发强驱离。原因微波感应模块探测范围太大机场空旷区域的气流扰动、围界外汽车灯光扫过都会产生电平变化红外热释电在中午阳光直射时也容易误报。单靠一路信号根本分不清目标和伪信号。解决把微波模块灵敏度调到最低档安装仰角抬高到 15 度以上避开车道和强反射面同时维持双通道确认的窗口让两路信号在 200ms 内同时被判有效才采取动作。现场调试时要在早中晚各跑一轮误报记录只在晴天测一遍属于交作业式验证等翻车了没有后悔药吃。注意双通道确认不是万能的两路传感器都被强光或雨水干扰时依然可能同时误报所以安装方向和遮挡设计是硬功夫软件只能兜底。5.2 复位陷阱看门狗喂了也死现象设备运行一两天后串口无响应看门狗超时却没有触发复位手动断电重启又恢复正常。原因看门狗虽然使能但主循环中某处阻塞超过喂狗时间I2C 等待、串口阻塞发送都会造成软件卡死喂狗代码永远执行不到。这一类问题最典型就是给阻塞操作没加超时HAL 库函数默认最长等待时间可以到几秒。解决喂狗固定放在主循环末尾不要放在中断里所有阻塞式的读写接口都加超时参数超时后返回错误并继续主循环。从那以后每次调硬件我都要确认一遍是否有函数可能无限等待这种问题复现概率低排查成本却极高。5.3 煤气炮联锁安全开关没闭合就点火现象检修人员在电控箱旁工作时设备突然放炮好在没有伤人。原因煤气炮触发条件只检查了传感器信号没有检查本地钥匙开关、气压和人员探测。检修时传感器仍在工作检修人员走过去被红外识别成鸟情触发了输出。这种事故在机场是不能容忍的。解决在trigger_gas_cannon()开头依次检查钥匙开关、气压、人员存在任一条件不满足就抑制点火并记录事件。上位机收到 suppressed 事件后要能区分是安全逻辑挡住而不是设备坏了。安全逻辑要在纸面架构里画出联锁链再落到代码不能临时在某个中断里改。5.4 485 总线的玄学现象两套设备接同一条总线一个上报正常另一个偶发超时排查半天发现换台从站就正常了。原因多半是终端电阻没匹配或者设备地址冲突。也见过把屏蔽层两端都接地的情况地电位差在屏蔽层形成回流干扰了 A/B 线信号。这类问题在实验室连一根短线上测不出来到了机场围界几百米线路上才暴露。解决总线首尾各并 120Ω 终端电阻中间设备不要加设备地址从 1 开始分配确保每条总线唯一485 屏蔽层单点接大地。调试时用示波器看 A-B 压差静止状态应大于 200mV否则要么地址冲突要么终端电阻缺失。5.5 掉电后重启状态恢复现象现场短时掉电恢复后设备自动进入强驱离状态煤气炮直接点火。原因复位后全局变量清零状态机默认从静默开始但执行器输出的初始化顺序有误某个 GPIO 配置成默认高电平挂在它后面的中间继电器立刻吸合相当于强驱离生效。解决上电后延迟 2 秒再初始化执行器输出并统一写禁止电平同时读取复位标志若检测到看门狗复位或外部复位让设备进入手动模式要求人工确认后才切回自动。这个从一次无人机测试里踩出来的经验后来成了所有设备固件的默认启动流程。6. 二次开发实战增加激光驱鸟模式并保持原有架构如果要把这套装置升级成带激光扫掠的驱鸟站我的做法是从状态机扩展而不是推翻重写。先给状态枚举增加一个激光扫掠阶段然后在强驱离分支里判断激光使能位启用后通过 TIM2 的 PWM 通道控制云台舵机让激光在 120 度扇区内来回扫掠。static uint8_t sweep_step; static void laser_sweep_100ms(void) { uint16_t level 0; static const uint16_t pos[6] {512, 1023, 512, 0, 512, 900}; if (sweep_step 6) { level pos[sweep_step]; __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, level); } sweep_step (sweep_step 1) % 6; }代码说明__HAL_TIM_SET_COMPARE直接修改比较寄存器动态改变舵机 PWM 占空比使舵机按 6 个位置点往复转动。PWM 比较值 0-1023 对应舵机 0 到 180 度上电时必须先把舵机归中防止开机瞬间扫到极限位置撞坏齿轮。改造步骤可以按四步走先在配置区加一个laser_enable位再初始化 TIM2 的 PWM 输出在状态机的强驱离分支调用laser_sweep_100ms最后通过新增寄存器 0x0012 控制激光开关。写完以后先在手动模式测舵机范围确认没有阻挡再切回自动模式。我一直有个习惯改完执行层代码后先做三次断电重启测试再做两次看门狗复位测试确认复位后不会误动作最后才拿到现场跑。机场环境试错成本太高宁可多测一轮。拿到这份源码包后也建议你先跑通自动模式再叠加激光改造希望帮到你。本文还有配套的精品资源点击获取