引言:从停车场抬杆说起

你每天开车进出小区或商场停车场,道闸抬杆落杆,习以为常。但你有没有想过,这套系统背后是怎么工作的?摄像头识别车牌、后台校验权限、道闸自动抬起、延时自动落下——一套完整的智能道闸系统涉及传感器、嵌入式、网络通信、云平台等多个领域。

这次实验,我们用 STM32L431 + 双舵机 + WiFi 模块 + LiteOS 操作系统,从零搭了一套迷你版智能道闸。麻雀虽小,五脏俱全——PWM 舵机控制、多任务调度、云平台通信、二维码车牌,一个不落。

一、系统架构:端管云三层

智能道闸不是一个单片机程序那么简单,它是典型的「端-管-云」物联网架构:

层级

角色

本实验对应

设备端,执行物理动作

STM32 + 双舵机道闸 + LCD + WiFi 模块

网络层,数据传输通道

ESP8285 WiFi + OC 协议

平台层,业务逻辑处理

华为 OceanConnect 物联网平台

设备端负责道闸的物理控制和状态显示;网络层负责设备和云之间的双向通信;云平台负责车牌识别、权限校验、指令下发等业务逻辑。三者各司其职,协同工作。

二、舵机 PWM 控制:20ms 里的角度魔法

2.1 为什么是 50Hz

舵机的控制信号是周期 20ms(频率 50Hz)的 PWM 脉冲。这个频率不是随便定的——它是航模舵机的工业标准,源自早期遥控模型的信号规范。脉冲宽度决定了舵机转动到哪个角度:

脉冲宽度

舵机角度

CCR 比较值

0.5 ms

-90°

50

1.0 ms

-45°

100

1.5 ms

150

2.0 ms

+45°

200

2.5 ms

+90°

250

2.2 PWM 参数怎么算

STM32 的定时器时钟是 80MHz,要输出 50Hz 的 PWM,怎么配?

公式很简单:PWM 频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))

我们选 PSC = 799,ARR = 1999:

频率 = 80,000,000 / ((799+1) × (1999+1))

     = 80,000,000 / (800 × 2000)

     = 80,000,000 / 1,600,000

     = 50 Hz  ✓

完美。定时器计数频率 = 80MHz / 800 = 100kHz,每个计数值 = 10 微秒。所以 CCR = 50 对应 50 × 10μs = 0.5ms,CCR = 250 对应 2.5ms,正好覆盖舵机的 -90° 到 +90°。

2.3 双舵机反向运动

道闸的抬落靠两个舵机协同实现——舵机0 接 TIM1_CH4(PA11),舵机1 接 TIM2_CH1(PA15)。开闸时,舵机0 转到 +90°,舵机1 转到 -90°,两个舵机反向旋转,带动闸杆抬起:

void Servo_Open(void) {

    Set_Servo_PWM(DEGREE_90, 0);      // 舵机0: +90°

    Set_Servo_PWM(DEGREE_MINOR_90, 1); // 舵机1: -90°

}

关闸时两个舵机都回到 0° 位置,闸杆落下。这种双舵机反向驱动的设计,比单舵机驱动力矩更大,也更接近真实道闸的机械结构。

三、LiteOS 双任务:控制和显示的分工

整个系统跑在 Huawei LiteOS 上,两个任务并行:

任务

优先级

做什么

control_task

0(最高)

OC 协议处理、WiFi 通信、道闸控制、5 秒落杆计时、LED 心跳

view_task

1

LCD 显示、二维码生成、按键扫描、网络状态显示

3.1 为什么要分两个任务?

这是嵌入式系统设计的经典问题。单任务不是不能做,但把业务逻辑和界面显示混在一起,代码会变得又臭又长,而且显示卡顿可能影响控制响应。

分成两个任务的好处:

  • 职责清晰:控制任务专注于业务逻辑和网络通信,显示任务专注于用户界面
  • 优先级合理:控制任务优先级高,保证道闸动作及时响应;显示任务优先级低,偶尔慢一帧不影响功能
  • 便于维护:改界面不会动到控制逻辑,改控制逻辑也不会影响显示

3.2 任务间怎么传数据?

两个任务通过全局结构体共享数据:TranView 存显示数据(道闸状态、校验结果、清屏标志等),Scene 存控制标志(state 状态变化标志、event_sendflag 上报标志等)。

关键的同步机制是 Scene.state 这个标志位。控制任务检测到道闸状态变化时,把 Scene.state 置 1。显示任务每次循环检查这个标志,如果是 1 就刷新界面,然后清零:

// control_task 中,道闸状态变化时

TranView.GateOutput = gate_state + 1;

Scene.state = 1;           // 通知显示任务刷新

// view_task 中,每次循环检查

if(Scene.state == 1) {

    // 刷新道闸图标和文字...

    Scene.req_state = 1;

}

这种「生产者-消费者」模式非常简洁。控制任务是生产者(产生状态变化事件),显示任务是消费者(消费事件刷新界面),中间用一个标志位做同步。不需要信号量、不需要消息队列,一个标志位搞定。

四、云平台指令解析:功能码驱动的状态机

设备和云平台之间通过 OC 协议通信,用功能码(Funcode)区分不同指令。本实验涉及两条核心指令:

功能码

含义

方向

数据字节

0x31

道闸控制

云 → 设备

第6字节:0=关闸,1=开闸

0x33

车牌校验结果

云 → 设备

第10字节:0=失败,1=成功;含车牌信息

解析逻辑很直接——读功能码,switch 判断,执行对应操作:

void ptl_Receive_OCData_Handle(char* data)

{

    char Funcode = *(pRecData + 3);

    if(Funcode == 0x31)              // 道闸控制

    {

        unsigned char result = *(pRecData + 5) - 0x30;

        gate_deal(result);             // 开闸或关闸

        TranView.GateOutput = gate_state + 1;

        Scene.state = 1;               // 通知刷新界面

        Scene.event_sendflag = 1;      // 触发状态上报

    }

    else if(Funcode == 0x33)         // 车牌校验

    {

        unsigned char result = *(pRecData + 9) - 0x30;

        check_result_deal(result);

        TranView.CheckResult = result + 1;

        Scene.state = 1;

    }

}

注意数据字节里的 - 0x30。这是因为协议里的数字是 ASCII 码形式传输的,ASCII '0' = 0x30,所以减去 0x30 就得到整数值 0 或 1。这个小细节如果没注意到,会出现「收到 0x31 却判断为开闸失败」的诡异 bug。

五、5 秒自动落杆:一个变量搞定非阻塞计时

道闸打开后 5 秒自动落下,怎么实现?你可能想到用 HAL_Delay(5000)——但在多任务系统里,阻塞 5 秒是不可接受的。control_task 还要处理心跳、网络、按键这些事情呢。

优雅的解决方案:利用已有的 1 秒 LED 心跳计数器,加一个倒计时变量:

// 开闸时

void gate_deal(uint8_t state) {

    gate_state = state;

    if(state == 1) {

        close_gate_time_cnt = 5;   // 设置 5 秒倒计时

        Servo_Open();

    }

}

// 主循环中,每秒执行一次

if(HAL_GetTick() - sys_time > 1000) {

    sys_time = HAL_GetTick();

    led_toggle(LED1_PORT);          // LED 心跳

   

    if(close_gate_time_cnt) {       // 倒计时非零则递减

        close_gate_time_cnt--;

        if(0 == close_gate_time_cnt) {

            gate_deal(0);            // 倒计时到 0,自动关闸

            Scene.state = 1;

            Scene.event_sendflag = 1;

        }

    }

}

一个变量 close_gate_time_cnt,开闸时设为 5,每秒减 1,减到 0 就自动关闸。没有额外定时器,没有阻塞延时,复用已有的心跳计时,代码简洁得令人舒服。

这种设计模式在嵌入式里太常见了——如果系统里已经有一个周期性的时钟节拍(比如 1ms 的 SysTick、或者这里 1s 的 LED 心跳),尽量复用它做软件计时,而不是动不动就开新的硬件定时器。硬件定时器资源是有限的,软件计时变量想加多少加多少。

六、二维码车牌:一个巧妙的交互设计

这个实验里有个很有意思的设计——用二维码模拟车牌识别。屏幕上显示一个二维码,内容是 URL + 车牌号:

https://iot.zj-huawei.com/wx/#/device?carPlate=A123

手机扫这个二维码,就能跳转到一个包含车牌信息的页面,模拟摄像头识别车牌的效果。

为什么用二维码而不是直接显示文字?因为这是一个物联网实验,二维码是设备和手机之间最方便的「离线通信」方式——不需要蓝牙配对、不需要 WiFi 直连,手机扫一扫就拿到了设备想传达的信息。在真实场景中,设备的 SN 码、配网信息、故障二维码…… 都是用这种方式和用户交互的。

二维码的生成用了 QR_Encode 开源库——前面 B06 实验详细讲过,这里就不重复了。在智能道闸这个场景下,二维码不再是一个独立的功能,而是整个业务流程中的一环:车牌信息 → 二维码编码 → 屏幕显示 → 手机扫码 → 云平台校验 → 道闸抬杆。技术还是那个技术,但放在业务场景里,价值就完全不一样了。

七、一个值得讨论的设计取舍

7.1 为什么用 OC 协议而不是 MQTT

MQTT 是现在物联网最主流的协议,为什么这个实验用 OC 协议?答案很实际——OC 协议是华为 OceanConnect 平台的原生协议,设备接入华为云时天然支持。而且 OC 协议是二进制格式,比 JSON 格式的 MQTT 消息更紧凑,适合 NB-IoT 这种低带宽场景。

当然,如果你用阿里云、腾讯云或者自建平台,MQTT + JSON 是更通用的选择。协议本身没有绝对的好坏,关键是看场景和平台。

7.2 为什么道闸状态变化要上报?

你可能注意到了,每次道闸状态变化,代码里都会置位 Scene.event_sendflag = 1 触发状态上报。为什么不能云平台自己知道道闸开了没?

因为云平台下发指令后,并不能确定设备是否真的执行了——网络可能丢包,设备可能掉线,舵机可能卡住。所以设备执行完动作后,必须主动把当前状态上报回去,云平台才能确认「哦,道闸确实打开了」。

这是物联网通信的一个基本原则:**指令下发是云到端,状态上报是端到云,两条链路是独立的**。云不能假设发了指令设备就一定执行,设备也不能假设上报了云就一定收到。双向确认才能保证数据一致性。

八、从实验室到真实停车场的距离

这个实验实现了智能道闸的核心流程,但距离真实的停车场道闸系统还有不小的差距。几个可以继续深入的方向:

8.1 车辆检测

真实道闸不会开闸后傻等 5 秒,而是用地感线圈或超声波检测车辆是否通过。车过了才落杆,车没过就一直抬着。5 秒固定延时只是最简单的教学实现。可以结合 HC-SR04 超声波传感器实现「车到抬杆、车过落杆」的智能检测。

8.2 车牌识别

二维码模拟车牌识别只是教学手段,真实系统用的是摄像头 + OCR。在嵌入式端跑 OCR 算力可能不够,通常是设备把图片传到云端识别,或者用专门的车牌识别摄像头模组(内置 OCR 芯片,直接输出车牌号)。

8.3 防砸车安全

真实道闸必须有防砸功能——下落过程中如果检测到下方有车或人,立即停止并抬起。常见方案有红外对射、压力电波、地感线圈等。这是安全相关的强制功能,实验里省略了,但做产品时绝对不能省。

8.4 断电保护

停电了道闸还能不能抬起来?真实道闸都有手动摇把或备用电池,确保紧急情况下可以手动开启。物联网设备的可用性设计里,断电/断网场景的处理非常重要。

写在最后

做这个实验最大的收获,不是学会了舵机怎么转、PWM 怎么配——这些是单点技术。真正有价值的是第一次完整体验了「端-管-云」三层架构的协同工作:设备端采集和执行、网络层传输数据、云平台处理业务。

以前做传感器实验,代码都在单片机里跑,逻辑闭环也在单片机里。这个实验不一样——道闸开不开,不是单片机自己说了算,而是云平台下发指令决定的。设备从「自主决策」变成了「执行终端」,这是物联网和传统嵌入式最大的区别。

理解了这个转变,再看任何物联网设备——智能音箱、智能门锁、智能路灯——你都能立刻拆解出它的三层架构,知道每一层在做什么、数据怎么流转。这才是这个实验真正的价值。

更多推荐