简介:基于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 处理状态机和日志绰绰有余,低成本低功耗才是长期运维的刚需。核心器件选型一览表如下,后面二次开发时可以按这个表去替换:器件选用要点可选替代方案主控STM32F103C8T6,72MHz,64KB Flash,工业级F103RCT6(Flash 翻倍)微波感应模块数字输出,探测距离 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.c:CRC16 计算 */ 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 控制激光开关。写完以后先在手动模式测舵机范围,确认没有阻挡再切回自动模式。我一直有个习惯:改完执行层代码后,先做三次断电重启测试,再做两次看门狗复位测试,确认复位后不会误动作,最后才拿到现场跑。机场环境试错成本太高,宁可多测一轮。拿到这份源码包后,也建议你先跑通自动模式再叠加激光改造,希望帮到你。本文还有配套的精品资源,点击获取
简介:基于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 处理状态机和日志绰绰有余,低成本低功耗才是长期运维的刚需。核心器件选型一览表如下,后面二次开发时可以按这个表去替换:器件选用要点可选替代方案主控STM32F103C8T6,72MHz,64KB Flash,工业级F103RCT6(Flash 翻倍)微波感应模块数字输出,探测距离 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.c:CRC16 计算 */ 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 控制激光开关。写完以后先在手动模式测舵机范围,确认没有阻挡再切回自动模式。我一直有个习惯:改完执行层代码后,先做三次断电重启测试,再做两次看门狗复位测试,确认复位后不会误动作,最后才拿到现场跑。机场环境试错成本太高,宁可多测一轮。拿到这份源码包后,也建议你先跑通自动模式再叠加激光改造,希望帮到你。本文还有配套的精品资源,点击获取