简介:一套基于STM32F103的机场驱鸟装置完整工程项目,面向嵌入式爱好者、STM32初学者及安防设备开发者,针对机场鸟群干扰飞行安全的实际场景,实现超声波探测与舵机云台自动驱离。压缩包内共159个文件、大小5.12MB,以C源码、H头文件、Keil工程配置为主,另含hex固件、CubeMX初始化文件及编译中间文件,覆盖从源码到烧录验证的完整链路。已有272人学习下载,适合希望从零搭建智能驱鸟系统或参考STM32外设综合应用的读者。代码基于HAL库编写,包含定时器PWM舵机控制、超声波模块驱动、串口通信及中断处理等关键模块,并有独立配置文件与工程文档,便于直接编译、烧录和二次开发。通过该资源可掌握STM32多外设协同设计思路,为类似测距与运动控制项目提供可复用模板。
1. 基于STM32F103的机场驱鸟装置:不止是吓鸟,更是一套实时检测与威慑系统
机场驱鸟一直是个硬骨头。鸟类撞击飞机造成的损失,轻则航班延误,重则机毁人亡。传统的驱鸟方式靠人工巡逻、播放录音、甚至用猎枪,效率低且不持续。而这个基于STM32F103的驱鸟装置,解决的是“自动检测+智能威慑”的问题:它通过超声波和红外传感器感知鸟类靠近,再由STM32F103核心板根据预设策略,控制音频、强光和激光等执行器,在鸟群进入跑道前就把它们赶走。整套系统成本不高、部署灵活,适合机场周界、农田果园、风力发电站等需要驱赶鸟类的场景。对于嵌入式开发者来说,这也是一个典型的“传感器+决策+执行”综合项目,涉及定时器、ADC、PWM、串口通信和状态机设计,值得拆一拆。
2. 系统架构与硬件选型:从传感器到执行器的全链路设计
2.1 整体架构:三层结构的驱鸟装置
整个装置的架构可以分成三层:感知层、决策层和执行层。感知层负责收集环境信息,常见的传感器包括超声波测距模块(如HC-SR04)、红外热释电模块、微波雷达等。决策层就是STM32F103,它读取传感器数据,通过判断逻辑识别“是否有鸟接近”,然后输出控制信号。执行层则是各种威慑手段,比如大功率音频功放(播放鹰叫或超声波驱鸟)、强光频闪灯、激光发射模块,甚至可以接水炮或煤气炮的继电器控制。
我比较推荐将系统设计成模块化结构。主控板固定通信接口,传感器和执行器都通过标准接插件连接,这样后续更换传感器或增加执行器时不需要改动主控板。项目源码里通常会把驱动代码按模块分离,比如sensor/、driver/、app/这三级目录,便于移植和复用。特别是执行器部分,如果把音频、激光、强光灯都封装成同一个接口,那决策层代码就不用关心具体接了哪个设备,后期维护会轻松很多。
2.2 核心硬件选型:STM32F103的外设资源分配
STM32F103系列有多种型号,驱鸟装置一般使用C8T6或ZET6。如果只是驱动几个传感器和音频模块,C8T6(64KB Flash、20KB RAM)够用;但如果你要跑FreeRTOS并做复杂策略,建议直接上ZET6(512KB Flash、64KB RAM)。我一般会预留至少40%的Flash和60%的RAM,为后续功能升级留空间。
外设资源分配上,常规做法是:超声波测距模块使用定时器输入捕获或普通GPIO模拟时序。我习惯用TIM4的输入捕获通道来测回波宽度,比GPIO轮询更精确。红外热释电模块用EXTI外部中断,鸟一进入区域立刻触发回调。音频功放和强光灯用PWM输出控制,分别接到TIM2和TIM3的通道上。激光模块通过GPIO开关控制,也可以用PWM调节功率。通信方面,USART1用于和上位机调试,USART2或SPI用于接无线模块。
电源设计是容易忽视的地方。音频功放峰值电流可能达到2~3A,如果和主控共用LDO,瞬时压降就会导致STM32复位。所以我会把电源分为数字3.3V和功率12V两路,主控板用隔离电源模块,功放单独用开关电源。下面给出一份典型的引脚分配表,实际接线时可以以此为基础微调。
| 功能模块 | 接口类型 | 对应外设 | 说明 |
|---|---|---|---|
| 超声波HC-SR04 | GPIO/定时器 | TIM4_CH1 | 测量回波时间,分辨率约0.1cm |
| 红外热释电 | GPIO+EXTI | PA0 | 上升沿触发中断,检测运动 |
| 音频功放 | PWM->低通 | TIM2_CH1 | 载波频率20kHz可调 |
| 强光频闪 | GPIO | PB5 | 低电平触发,光耦隔离 |
| 激光模块 | GPIO/PWM | PB6/TIM3_CH1 | 恒流驱动,PWM调功率 |
| 无线模块 | USART2 | PA2/PA3 | 与上位机双向通信,9600bps |
表里的PB5和PB6如果和JTag调试口冲突,可以用STM32的软件映射功能调整,或者改用PE口。开发阶段调试器和外设争用IO是常见问题,最好一开始就留出备用引脚。
2.3 执行器驱动方案:音频、强光、激光
音频驱鸟是最常见的方式。播放鹰类叫声或电子驱鸟声,通常用语音模块,或者直接用STM32内部的DAC播放WAV文件。不过要注意,单纯靠声音容易被鸟适应。所以实际项目里会把声、光、激光组合起来,随机切换威慑策略。
驱动大功率音频功放时,STM32的PWM输出经过低通滤波器变成音频信号,再送入功放芯片。这样既能播放音频,又能调节音量。PWM载波频率选20kHz,高于人耳听觉上限,低通截止频率设计在15kHz左右,避免把载波噪声放出来。功放增益控制在20dB以内,音量太大反而会让传感器误报。
激光模块需要恒流驱动电路,不能直接把IO接激光二极管。最省钱的方案是用一个三极管和采样电阻组成恒流源,更稳定的做法是用LM317加电阻构成恒流。强光频闪灯则是用可控硅或MOS管隔离,STM32只给出TTL电平触发信号。如果直接在PB5上驱动继电器,触点火花会在电源线上产生很大的毛刺,导致ADC采集漂移。所以所有执行器模块最好都加光耦隔离,这是做这类设备的一个血泪经验。
2.4 开发环境与工程结构:怎么把工程跑起来
这个项目一般用Keil MDK开发,也可以用STM32CubeMX生成HAL库工程后再移植代码。如果拿到的是标准外设库版本,直接打开Project.uvprojx编译即可。目录结构通常包含:
project/ ├── Core/ # 启动文件、中断处理 ├── Hardware/ # 传感器、执行器驱动(hc_sr04.c, pir.c, audio.c, laser.c) ├── App/ # 应用层逻辑(state_machine.c, strategy.c) ├── ThirdParty/ # FreeRTOS源码或操作系统接口 └── Doc/ # 原理图、引脚描述、通信协议烧录时用ST-Link或J-Link连接SWD口,Keil里配置一下Flash下载算法就行。注意一定不要把板子上的串口1占满,因为调试日志通常走串口1,烧录和排障都靠它。我一般会在编译后先做一次静态检查,确认没有未分配的中断优先级,再下载到板子。
3. 核心模块拆解:一个驱鸟指令从检测到触发的完整链路
3.1 鸟类检测:超声波测距与红外触发
超声波测距用的是HC-SR04,发送一个10us的触发脉冲,然后测量回波高电平时间。高电平时间越长,距离越远。我用定时器输入捕获来精准测量,代码是这样的:
// 超声波测距:发送10us触发脉冲,读取回波高电平时间 void HC_SR04_Trigger(GPIO_TypeDef* port, uint16_t pin) { GPIO_SetBits(port, pin); delay_us(10); GPIO_ResetBits(port, pin); } volatile uint32_t echo_time_ticks = 0; // TIM4_CH1 输入捕获中断:上升沿开始计时,下降沿保存计数值 void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_CC1)) { TIM_ClearITPendingBit(TIM4, TIM_IT_CC1); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)) { TIM_SetCounter(TIM4, 0); // 回波开始,清零计数 } else { echo_time_ticks = TIM_GetCapture1(TIM4); // 回波结束,保存计数值 } } } float GetDistanceCm(void) { // 定时器频率为1MHz,声速340m/s,距离 = 时间 * 340 / 2 / 1e6 = 0.017cm/us return echo_time_ticks * 0.017f; }逻辑说明:TIM4的计数频率设为1MHz,所以每个tick等于1us。上升沿到来时清零计数器,下降沿到来时读取计数值,这个计数值就是回波持续时间。距离公式里的系数0.017,对应20°C空气中的声速340m/s。实际使用中如果温度变化大,这个系数会有明显偏差,可以用DS18B20做温度补偿,公式改成c = 331.5 + 0.6 * temp。
红外热释电模块比较简单,检测到运动后输出高电平。我把它接到PA0,配置为EXTI外部中断,代码如下:
volatile uint8_t pir_flag = 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0)) { EXTI_ClearITPendingBit(EXTI_Line0); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 1) { pir_flag = 1; } } }逻辑说明:红外中断一旦触发,就置位pir_flag。主循环或任务中检测到这个标志后,会去读取超声波距离做二次确认。这里有个关键参数:热释电传感器上电后需要预热20到30秒,期间不能判断有鸟,否则会不断误报。经验做法是在初始化后延时30秒再使能中断。
3.2 决策逻辑:状态机与优先级调度
有了传感器数据还不行,直接驱动执行器很容易被一根树枝吓到就乱开激光。我设计了一个四状态状态机:IDLE(空闲)、DETECT(确认)、DETER(驱赶)、RELAX(间歇)。核心代码如下:
typedef enum {IDLE, DETECT, DETER, RELAX} State_t; State_t state = IDLE; static uint16_t deter_count = 0; void Decision_Tick(void) { switch (state) { case IDLE: if (ultrasonic_distance < 30.0f || pir_flag) { state = DETECT; deter_count = 0; } break; case DETECT: // 连续检测到距离小于30cm且红外触发,才进入驱赶 if (ultrasonic_distance < 30.0f && pir_flag) { state = DETER; } else if (deter_count++ > 3) { // 超过30ms未确认,回到空闲 deter_count = 0; state = IDLE; } break; case DETER: // 执行驱赶动作,持续5秒后进入放松 Execute_Deterrent_Action(); if (deter_count++ >= 500) { // 每10ms一个tick deter_count = 0; state = RELAX; } break; case RELAX: // 停止驱赶,5秒后回到检测 Stop_Deterrent_Action(); if (deter_count++ >= 500) { deter_count = 0; state = IDLE; } break; } }逻辑说明:IDLE状态下,只要超声波距离小于30cm或者红外检测到运动,就进入DETECT。DETECT状态要求距离和红外同时满足,才会真正驱赶,否则超时回到IDLE。DETER持续500个tick,如果tick周期为10ms,就是5秒。RELAX也是5秒,给鸟一个逃走的时间,避免驱赶动作一直开着。
这里距离阈值30cm是不是太小了?对于一个驱鸟装置来说,30cm连鸟都撞到杆子了。实际项目中这个阈值要根据安装位置调整。如果安装在跑道边,可以把阈值改成5米甚至10米,因为鸟一旦进入跑道就需要马上驱离。所以这个参数不要写死,最好通过串口指令动态配置。
3.3 执行驱动:PWM波形与随机化策略
音频驱鸟用PWM输出,初始化代码大致如下:
void TIM2_PWM_Init(uint16_t period, uint16_t pulse) { TIM_TimeBaseInitTypeDef timer; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); timer.TIM_Prescaler = 720 - 1; // 72MHz / 720 = 100kHz计数 timer.TIM_Period = period; // 决定PWM频率 timer.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &timer); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_Pulse = pulse; // 占空比 oc.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM2, &oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }参数说明:预分频720,计数频率100kHz。如果period设为5,PWM频率就是100kHz/5=20kHz。pulse设为period的一半,得到50%占空比。驱动音频低通滤波器时,占空比决定输出音频信号的幅度。你可以把pulse理解为音量旋钮,想要更大音量就加大pulse,但不能超过period。
驱赶策略的随机化是个容易被忽视的点。如果每次都是先响5秒、停5秒,鸟很快就会学会规律,等声音停了再回来。我用一个简单的线性反馈移位寄存器来做伪随机选择:
uint16_t lfsr = 0xACE1; uint8_t GetRandom(void) { lfsr ^= lfsr << 13; lfsr ^= lfsr >> 17; lfsr ^= lfsr << 5; return (uint8_t)(lfsr & 0x0F); }根据随机值,决定这一轮是用强光、激光、还是超声波脉冲。如果连续两次用到同一个设备,第二次就跳过,强制切换。这样让鸟永远摸不清驱赶节奏。
4. 软件实现细节:RTOS任务划分与关键代码
4.1 任务划分:检测、决策、执行、通信
这个项目有实时性要求,所以我直接用FreeRTOS跑多任务。任务划分成四个:SensorTask负责读取传感器,DecisionTask跑状态机,ActuatorTask更新执行器,CommTask处理上位机指令。任务参数和优先级如下表。
| 任务名 | 优先级 | 周期 | 职责 |
|---|---|---|---|
| SensorTask | 3 | 50ms | 读取超声波和红外,发布消息 |
| DecisionTask | 2 | 10ms | 订阅传感器消息,运行状态机 |
| ActuatorTask | 4 | 5ms | 接收决策结果,更新PWM和GPIO |
| CommTask | 1 | 事件驱动 | 解析上位机指令,回报状态 |
任务间用消息队列传递数据,避免全局变量满天飞。SensorTask把超声波距离和红外触发打包成结构体,发给DecisionTask。DecisionTask计算出的动作指令再发给ActuatorTask。核心代码:
typedef struct { float distance; uint8_t pir; uint32_t timestamp; } SensorData_t; QueueHandle_t sensor_q; QueueHandle_t action_q; void SensorTask(void *arg) { SensorData_t data; for (;;) { data.distance = GetDistanceCm(); data.pir = pir_flag; pir_flag = 0; xQueueSend(sensor_q, &data, 0); vTaskDelay(pdMS_TO_TICKS(50)); } } void DecisionTask(void *arg) { SensorData_t data; for (;;) { if (xQueueReceive(sensor_q, &data, 10)) { ultrasonic_distance = data.distance; pir_flag = data.pir; Decision_Tick(); // 发送动作指令给执行任务 action_t act = GetCurrentAction(); xQueueOverwrite(action_q, &act); } } } void ActuatorTask(void *arg) { action_t act; for (;;) { if (xQueuePeek(action_q, &act, 10)) { ApplyAction(act); } vTaskDelay(pdMS_TO_TICKS(5)); } }逻辑说明:SensorTask周期50ms,正好避开HC-SR04的40ms最小测量间隔。DecisionTask使用带超时的接收,避免队列为空时永久阻塞。ActuatorTask用xQueuePeek而不是Receive,因为动作要持续执行,不能被取走。
任务优先级上,我把ActuatorTask设为最高,因为PWM输出和激光控制不能有抖动。SensorTask次之,DecisionTask再次,CommTask最低。如果CommTask收到了上位机指令比如“强制驱赶”,可以通过直接修改state变量来插队。这在生产环境里很有用,鸟群已经进入跑道时,现场人员可以一键驱赶。
4.2 关键代码:多路捕获与PWM同步
超声波和PWM如果共用一个定时器,很容易出现测量途中定时器被重装,导致回波时间错误。所以我坚持用两个独立定时器:TIM4做超声波输入捕获,TIM2做音频PWM输出。中断优先级上,TIM4中断优先级高于TIM2的比较事件,防止捕获丢失。
在多传感器场景下,如果加入多个超声波模块,需要逐个触发并依次读取。位置不同,回波可能重叠。我给每个探头固定一个触发时序:第一个探头在0ms触发,第二个在20ms触发,避免互相干扰。这个时序放在SensorTask里统一调度,不然并发触发会让回波混乱。
4.3 通信协议:与上位机的串口指令
上位机通过串口下发参数和指令,格式是自定义的帧结构:帧头0xAA 0x55,第3字节是长度,后面跟命令字和数据,最后两个字节是CRC16。串口中断接收代码:
uint8_t uart_buf[64]; uint8_t uart_len = 0; void USART1_IRQHandler(void) { uint8_t byte = USART_ReceiveData(USART1); if (uart_len == 0 && byte != 0xAA) return; uart_buf[uart_len++] = byte; // 前两字节是帧头,第三字节是长度 if (uart_len == 3) { if (uart_buf[2] > 60) { uart_len = 0; return; } } if (uart_len == uart_buf[2] + 2) { if (CheckCRC(uart_buf, uart_len)) { ProcessCommand(uart_buf, uart_len); } uart_len = 0; } }逻辑说明:先检查帧头,再按长度字段收完整帧,最后校验CRC。ProcessCommand里根据命令字执行不同操作,比如设置距离阈值、切换工作模式、读取触发日志。这样不用重新烧录固件,现场就能调参数。
通信波特率我选9600bps,不是不能更快,但无线透传模块在机场这种复杂电磁环境下,低速通信更稳。如果在环境好的地方,可以改115200bps,但要注意中断里处理不完会有丢包风险。
5. 避坑指南:STM32F103驱鸟装置开发中的常见问题与排查方法
5.1 超声波传感器间歇性误触发
现象:没有鸟经过时,距离读数突然从3米跳到30米,然后恢复,状态机被频繁触发。
原因:机场跑道附近有无线电台和雷达信号,加上金属围栏对超声波的反射,会造成虚假回波。另一个常见原因是超声波传感器安装太靠近声源,自己的音频驱鸟声反而被传感器自己接收到。
解决:对数据做中值滤波,连续采集5次,去掉最大值和最小值,取剩下三个值的平均值。并且在状态机里增加“双确认”:只有红外先触发,超声波再读到近距离,才进入驱赶状态。单独超声波读数不作为唯一判定依据。
5.2 音频功放一启动,主控就复位
现象:声音一加大,STM32自动重启,OLED屏花屏,串口日志中断。
原因:功放瞬间从12V电源抽取大量电流,电源轨被拉低到8V以下。如果主控板和功放共用同一个12V转5V再转3.3V的电源链路,主控的供电低压导致复位电路动作。
解决:把功放电源完全独立出来,用单独的12V开关电源模块。主控板用7805或者降压模块从另一个12V取电,然后在主控电源入口并联一个1000uF电解电容和100nF陶瓷电容。地线采用单点连接,星型布线。这样功放再怎么波动,也不会拉死主控。
5.3 激光二极管光衰严重甚至烧毁
现象:激光模块用了不到一个月,亮度明显下降,输出功率只有刚装时的一半。
原因:直接通过GPIO控制三极管开关激光管,没有恒流电路。激光二极管是电流敏感器件,温度升高后阻抗降低,电流反而增大,形成热失控。
解决:把激光模块换成带恒流驱动的成品,或者自己用LM317做恒流源。假设激光管额定电流200mA,LM317的输出电流I=Vref/R,Vref是1.25V,所以R取6.2欧姆,再用10k可调电阻微调。加一个10nF电容跨接在激光管两端,吸收开关噪声。
5.4 FreeRTOS下系统硬死机
现象:设备正常运行几个小时或几天后,所有任务停止,看门狗也救不回来,只能断电重启。
原因:某个中断里调用了xQueueSendFromISR,但没有处理返回的pxHigherPriorityTaskWoken参数。如果队列满了,这个调用会丢弃数据或者返回errQUEUE_FULL,但因为没有触发任务切换,更高优先级的执行任务永远拿不到新指令,系统卡死。
解决:所有FromISR函数必须接收pxHigherPriorityTaskWoken指针,并在发送成功后立即调用portYIELD_FROM_ISR(pxHigherPriorityTaskWoken)。另外,普通任务之间的队列发送要设置超时时间,我用0表示“发不进去就丢弃”,绝不在运行主线上阻塞等待。
5.5 上位机收到的日志乱码
现象:串口助手能收到数据,但显示全是乱码,波特率怎么调都调不对。
原因:板卡上的USB转串口芯片晶振精度不够,或者主控的HSE启动后有点微小的频偏,长时间传输会累积错误。另一个常见原因是上位机软件把回车换行解析错了。
解决:使用晶振精度为0.1%的USB转串口芯片,或者在主控端输出调试日志时,每条都加上一个固定的时间戳帧头,方便上位机做同步。调试时先用逻辑分析仪抓串口波形,确认每个字节的位宽是16us(9600波特率),再怀疑软件问题。
6. 二次开发:如何把驱鸟装置改造成通用智能驱赶设备
6.1 扩展视觉识别模块
如果把超声波和红外换成摄像头,就可以从“有鸟就赶”升级为“认出鸟种再赶”。STM32F103本身的算力跑不了YOLO,但可以挂一个独立的摄像头串口模块。模块识别到鸟类后,通过USART发送一个字节,比如0x01表示麻雀、0x02表示老鹰。主控根据鸟种决定驱赶方式:麻雀用强光吓走,老鹰这种猛禽可以不用驱赶,它本身就是驱鸟工具。
如果你不想挂额外模块,还有一个折中方案:用红外对射栅栏。把一对对射红外放在跑道侧面,鸟穿过时遮断光线,触发驱赶。这个方案对飞鸟的检出率比超声波更稳定,成本也低。
6.2 调整驱赶策略:分时段布防
机场鸟害主要集中在黎明和傍晚,其余时间驱赶噪音会扰民。我给系统加了一个RTC芯片,按小时切换工作模式:
void ApplyTimeStrategy(void) { uint8_t hour = GetRTCHour(); if (hour >= 5 && hour < 9) { // 清晨:强光+音频,鸟群刚开始活动 SetDeterrentMode(MODE_DAYTIME); } else if (hour >= 16 && hour < 19) { // 傍晚:激光+音频,光线暗时激光更显眼 SetDeterrentMode(MODE_DUSK); } else if (hour >= 22 || hour < 4) { // 深夜:关闭声光,只做超声波脉冲,避免扰民 SetDeterrentMode(MODE_NIGHT); } else { // 白天:低功耗待机,红外检测到异常才触发 SetDeterrentMode(MODE_IDLE); } }逻辑说明:这个函数放在DecisionTask里,每分钟调用一次。MODE_DAYTIME、MODE_DUSK、MODE_NIGHT是三个配置结构体,里面保存了不同执行器的启停组合。比如MODE_NIGHT下,音频PWM不输出,激光也关闭,只让超声波传感器以低频脉冲工作。
6.3 加入远程监控与自动上报
在USART2接口挂一个4G或WiFi透传模块,设备就能把“触发次数”“当前状态”“电池电量”定时上报到云平台。修改现有的串口协议时,要注意保留帧头校验,同时在帧里加入设备ID,方便多台设备组网。
我的经验是,每次加新功能前先画一个状态机流程,再动手写代码。之前翻车过一次,在裸机上直接加了一个电调,导致PWM信号毛刺太大,功放出现杂音。后来所有执行器驱动都加了光耦或逻辑门隔离。从那以后,我每次做嵌入式扩展都会先检查输出驱动能力是否足够,再考虑算法。希望这篇文章能帮到你,少走这些坑。
本文还有配套的精品资源,点击获取