发作品签到
标准版

【训练营_进阶班】卷帘大将

工程标签

5.4k
0
0
6

简介

该项目旨在开发一款电动窗帘设备。 设备使用STM32单片机做主控,通过控制编码器电机正反转实现窗帘的电动开合, 并通过EMW3080接入阿里云物联网平台,实现本地/云端控制。

简介:该项目旨在开发一款电动窗帘设备。 设备使用STM32单片机做主控,通过控制编码器电机正反转实现窗帘的电动开合, 并通过EMW3080接入阿里云物联网平台,实现本地/云端控制。

开源协议

CC-BY-NC 3.0

创建时间:2020-08-08 01:39:57更新时间:2022-01-10 06:49:36

描述

卷帘大将

注:

该工程描述文件同说明文档,可下载附件“卷帘大将_电动窗帘_说明文档.pdf”查看。

工程代码可下载附件 “Code_RollerBlindGeneral.zip” 查看。

配网、配网过程意外断电重启、联网控制演示视频已上传,可下拉至文档下方查看。

PDF文件内置超链接,可直接点击跳转Datasheet等文件。

作者:OpticalMoe

日期:2020/08/23

高清靓照:

高清靓照
高清靓照
高清靓照


前 言

该项目旨在开发一款电动窗帘设备。
设备使用STM32单片机做主控,通过控制编码器电机正反转实现窗帘的电动开合,并通过EMW3080接入阿里云物联网平台,实现本地/云端控制。

  • 电源:采用LP6498芯片,设计输出5V@1A。LDO采用HX9193,设计输出3.3V。
  • WIFI:采用EMW3080(阿里飞燕固件),可接入阿里云生活物联网平台。
  • 电机驱动:配有AB相编码器接口,可实现电机的位置环、速度环PID控制;驱动芯片采用RZ7899,支持25V@3A(最大5A);另配置了一限位开关接口,防止窗帘超限位运行损坏。
  • 外设:配有无源蜂鸣器,通过TIM定时器控制,可奏乐;两个LED指示灯用于指示WIFI联网状态和设备运行状态;光敏电阻和NTC热敏电阻,可反馈光照和温度;两枚按键,单键可手动控制窗帘的上下行,双键可进入配网模式。

使用方法

一、配网

  1. 上电后任意时刻,同时按动两按键两秒(先后按动两按键也可识别)。
  2. 设备奏乐“ Bi~~~ Pu ”,开始配网流程。蓝色、绿色指示灯快闪。
  3. 手机打开“云智慧”,扫码后,按提示开始配网。(扫码可下载APP)。
  4. 等待5s后设备奏乐“嘀嘀嘀”三声,设备开启AP热点。
  5. 手机端输入设备将要连接的家庭路由器名称和密码,点击“开始连接”,开始配网。(低版本安卓系统可直接接入adh_xxx热点,高版本安卓需手动连接该热点,热点连接成功后会断开,返回APP界面即可)。
  6. 耐心等待设备上云。若过程中设备奏乐“ 滴~~~~ ”表示出错,可以断电重启后从第2步重试。
  7. 当设备配网成功后,奏乐“ 1234567 ”,蓝色、绿色指示灯慢闪。
    注:在配网过程中任意时刻意外断电导致设备配网失败,下次上电自动进入配网模式。从第2步开始依次执行配网操作直至配网完成。

二、联网控制

  • 设备配网后可实现联网控制。
  • 设备联网成功后奏乐“123”,蓝色指示灯开始慢闪。
  • 按动上键或下键,窗帘匀速上行或下行,松手后,APP端自动刷新窗帘位置。
  • APP端滑动窗帘位置滑块,窗帘运行到指定位置停止并锁定。
  • 点击APP端“快捷操作”,可直接控制窗帘到达预设位置。
  • 点击APP端“状态信息”,可查看设备温度、电压、光强等数据。
  • 配网二维码:(未安装APP可扫码下载)

配网二维码


目 录

一 器件选型

  • 电源
  • MCU
  • WIFI
  • 电机驱动
  • 外设

二 原理图设计

  • 电源
  • MCU
  • EMW3080
  • 电机驱动
  • 外设

三 PCB设计

  • 电源&电机驱动
  • MCU
  • EMW3080
  • 外设

四 焊接

五 APP设计

六 程序调试

  • 代码移植&上传数据
  • ADC&DMA采样
  • 蜂鸣器驱动
  • PID及参数整定
  • 按键控制
  • 任务/消息调度器改写
  • 配网模式

结论


一 器件选型

本次活动要求设计一款物联网设备。为了控制成本,器件选型尽可能地选择性价比高的器件。

1.电源

  • 电源输入插座 采用DC005插座,设计可承受30V@3A。

Datasheet:DC005-30A

商城编号:C111573

封装:DC005-T25

输入:30V(最大)

电流:3A(最大)

注意选用A级插座,并注意可承受的电压电流是否满足。

  • DCDC 采用LP6498AB6F芯片,设计输出5.12V@1A。

Datasheet:LP6498

商城编号:C387722

封装:SOT23-6

输入:4.5 ~ 30V

输出:4.8 ~ 12V

电流:1200mA(最大)

该芯片耐压高,输入、输出电压范围宽,电流大。体积小,外围电路简单,输出电压可调。便宜皮实,性价比高。

  • LDO 采用HX9193-33GB,设计输出3.3V@600mA。

Datasheet:HX9193-33GB

商城编号:C296123

封装:SOT-23-5

输入:6V(最大)

输出:3.3V(固定)

电流:600mA(最大)

压降:480mV(最大)

该芯片电流大,压降小。体积小,外围电路简单。便宜皮实,性价比高。


2. MCU

MCU采用 STM32F030K6 单片机。

Datasheet:STM32F030K6T6

商城编号:C88446

封装:LQFP32

选用这款单片机的主要原因是 便宜 性价比高。同时STM32芯片可以使用 ST-Link 连接 Keil 在线DEBUG,也可以使用 STM32CubeMonitor 软件打印内部变量变化曲线,方便PID调试。

这款单片拥有32KB FLASH,4KB RAM,48MHz的主频,LQFP-32的封装,一个串口,5个定时器,一个10通道12bit AD,26个IO。可谓是小巧精悍,实力不凡。

原本计划上RTOS的,但是4KB的RAM跑OS有点勉强,稍稍加点东西就超,最后没跑上。

自己做的ST-Link V2-1,成本低,性能强,比某宝盗版J-Link采样频率高。


3. WIFI

WIFI 选用的是EMW3080V2(阿里云飞燕固件)。WIFI选型没有经验,全跟课程走。


4.电机驱动

  • 电机驱动 部分采用RZ7899驱动芯片。

Datasheet:RZ7899

商城编号:C92373

封装:SOP-8_150mil

输入:3 ~ 25V

电流:3A

峰值电流:5A

内建刹车功能、内置过温保护、内置短路保护、内置过流保护。

  • 电流传感器 采用CC6900SO-5A芯片。

Datasheet:CC6900SO-5A

商城编号:C350864

封装:SOP-8

增益:400mV/A

电流:5A


5.外设

外设部分设计有:一个无源蜂鸣器,两个按键,两个LED指示灯。编码器接口,限位开关接口。


二 原理图设计

根据个人的设计习惯,原理图按功能划分,设计在5张A4图纸上。下面依次介绍。

1.电源

电源部分主要分为四块。分别是:电源输入插座、DCDC降压、LDO降压、测试点

电源输入插座 正极先通过SS54二极管,再接入设备。

DCDCLDO 部分按照官方手册绘制就可。

原理图-电源

                                        原理图-电源

2. MCU

MCU部分主要设计 晶振电路,复位电路,SWD下载接口

晶振 采用SMD-3225封装的8MHz无源晶振,该封装对烙铁焊接不友好。晶振电路主要由晶振和两个22pF无极性陶瓷电容构成。

复位电路 由10k上拉电阻和0.1uF电容构成,主要完成上电复位功能。

SWD接口 用于调试和下载程序,引出了SWCLK、SWDIO、NRST,采用XH2.5-4P端子接口。引出NRST引脚,即使程序中未使能SWD调试接口仍能下载、调试程序。

原理图-MCU

                                        原理图-MCU

3. EMW3080

EMW3080电路主要包括 电源滤波、串口、BOOT、测试点

电源滤波 采用0.1uF和10uF组合的形式;根据手册要求,电源采用3.3V。

串口 通过0R电阻交叉连接到MCU串口;GPIO23根据手册要求通过10k上拉;

BOOT 引脚预留0R电阻接地,但不焊接;EN引脚通过0R电阻连接到MCU和按键,主要完成WIFI的复位工作。

测试点 包括串口的TXD和RXD接口。调试时连接串口,可监视MCU和WIFI间交换的所有数据。

连接外部串口监视数据时,MCU串口需设置为开漏+上拉模式,否则会导致MCU与WIFI间数据乱码。

原理图-EMW3080

                                        原理图-EMW3080

4. 电机驱动

电机驱动部分主要完成 电流传感器电流采样、电源电压采样、电机驱动、测试点

电源电压采样 采用分压电阻结构,通过100k和10k电阻获得低的采样电压送入MCU-ADC接口。

电流采样 按照CC6900SO-5A官方手册绘制即可。

电机驱动芯片 按照官方手册绘制,注意输入和输出接口走线宽度。同时在电机接口上设计四个二极管钳位。电机采用5.0-2P接口,方便拆装。

测试点 主要有电流采样点、电压采样点、电机驱动正反转信号点,便于调试时确定状态。

注意功率地与信号地分开,并连接

原理图-电机驱动

                                        原理图-电机驱动

5. 外设

外设主要设计 无源蜂鸣器、光敏电阻、热敏电阻、编码器接口、按键、LED指示灯、测试点、机械孔

无源蜂鸣器 需连接到TIM-PWM输出引脚,可以通过调整TIM装载值和比较值控制蜂鸣器音调和音量。

热敏电阻光敏电阻 需要串联一个已知阻值的电阻接入电路,通过MCU-ADC测量中间点电压反向推算出外部温度和光强。

热敏电阻和光敏电阻测量精度非常有限,即使程序中加入修正,采样值仍可能和实际值偏差较大。对温度和光强精度要求高的场所慎用。

编码器接口 是为了电机的位置环、速度环PID设计,可连接AB相编码器。接口内已设计上拉电阻和硬件消抖电路,编码器电源通过两个0R电阻在5V和3.3V间选择,注意不可同时连接5V和3.3V电阻。

按键 用于控制窗帘的上拉、下拉动作,同时在必要时刻充当配网开关。

蓝色LED指示灯 用于指示WIFI连接状态,绿色LED 用于指示设备运行状态。

测试点 可测量光敏电阻和热敏电阻输出电压。

机械孔 是四个M3螺丝孔,方便设备通过螺丝安装在需要的地方。

原理图-外设

                                        原理图-外设

三 PCB设计

PCB设计经验不足,在此抛砖引玉。如有错误之处,还望大佬不惜赐教。

PCB效果图

                                        PCB效果图

1. 电源&电机驱动

电源电机驱动 主要注意走线宽度、功率地和信号地分开、端子下面挖空防止接地短路等。

PCB-电源&电机驱动

            PCB-电源&电机驱动

左侧为 LDO,右侧为 DCDC。注意电感离DCDC芯片近一些,电感下面不要走信号线。(此图为错误示范)

PCB-DCDC&LDO

                            PCB-DCDC&LDO

2. MCU

MCU主要注意晶振连线短一些,滤波电容靠近MCU电源引脚。

PCB-MCU

                            PCB-MCU

3. EMW3080

EMW3080按照官方手册要求,1、2、24、25脚不接,天线前方、左右留16mm净空区。搜索EDA中所有的封装都不完全满足官方手册要求,我自己画了一个。

PCB-EMW3080

                        PCB-EMW3080

4. 外设

热敏 避开发热区域;光敏 避开LED区域;编码器接口 放在电机端子旁边,方便连接;按键LED指示灯 放在板子下方,方便操作。

PCB-外设

                             PCB-外设

四 焊接

拿到PCB,准备焊接工具,开始焊接。

PCB

                        PCB

因为部分PCB中有一些封装对烙铁十分不友好。所以,上风枪。

工具

                    工具

下面简述下焊接步骤和注意事项。

  • 首先,准备0.5mm左右的焊锡丝、焊锡膏、助焊剂。清理烙铁头,烙铁温度350℃。尖嘴镊子。提前释放身上静电。
  • 第一步,焊接DCDC芯片及外围电路。电感封装问题,只能用风枪和焊锡膏焊。焊接完成后,焊接DC005接口。接入12V电源,使用万用表电压档测量5V测试点电压是否在5.12V左右。若电压不正确,需核对反馈电阻阻值是否正确。
  • 第二步,焊接LDO电路。焊完后上电测试输出电压是否在3.3V左右。
  • 第三步,电源没有问题后,焊接其他元件。顺序没有要求,一般由高度低的元件开始焊接。

单片机可以使用针管挤焊锡膏在焊盘上,摆好单片机,烙铁走一遍就能焊好,不连锡,贼好用。

电容焊盘也较短,需要焊锡膏和烙铁配合焊接。

PCB注意有几个元件不能焊接。分别是:编码器电源5V处0R电阻,EMW3080的BOOT接地电阻。

焊接完成后,效果如下。

实物图

                                        实物图

五 APP设计

APP设计采用阿里云物联网平台。具体过程可参考B站课程回放

一些属性参数如下:(部分功能未使用)

属性参数

                                    属性参数

三元组信息

                                    三元组信息

APP主界面

                        APP主界面

六 程序调试

程序调试主要按功能块调试。调试日志按以下顺序依次进行:代码移植、上传数据、蜂鸣器驱动、ADC&DMA采样、PID&参数整定、按键控制、任务调度器改写、配网模式

1. 代码移植&上传数据

零妖大佬给的例程是基于51单片机。51程序和STM32不兼容,需要移植一些底层代码。代码平台 CubeMX&HAL 库,MDK-ARM 5.27

移植需要一定的软件操作基础,回忆的过程,不完整。

工程代码可移步附件下载查看。

移植前可通过串口先让WIFI上云,减小移植难度。

首先打开 CubeMX 软件,选择 STM32F030K6T6 ,使能外部晶振,使能SWD接口。勾选必要的外设。设置时钟48MHz。填写工程名称,保存位置。选择使用的IDE为MDK-ARM 5.27,勾选“为每一个外设生成.c/.h文件”。点击“生成代码”。

引脚配置

                            引脚配置

时钟树

                                        时钟树

代码生成后,点击“打开工程”,自动调用Keil软件。

首先,在keil左侧Project中添加一个文件夹,用来存放我们的.c文件。双击该文件夹,加入Code_User文件夹下所有文件。

然后,点击 魔术棒 ,点击“C/C++”选项卡,点击“Include Paths”后面三点,添加“Code_User”文件夹路径。

编译文件,不出意外,您会收获 error(s)

下面开始解决这些错误:

  1. 大多数错误是由于 *#include * 造成的,这是51的头文件,32不使用,删除 所有该语句。
  2. Button.c 文件实现按键的短按、长按识别。该设备只需识别单按键和双按键,修改 其中的Button_Loop 函数。
  3. DeviceName.c 文件操作三元组数据。我们把三元组数据移植到 usart.c 文件下,删除 该文件。
  4. DS18B20.c 文件用于读取温度传感器数据。我们温度采用热敏电阻配合ADC,不涉及该文件,删除
  5. IAP_EEPROM.c 文件操作 EEPROM。32没有,删除 文件。
  6. main.c 文件是设备主逻辑实现。复制内容到CubeMX创建的 main.c 文件中,删除 该文件。
  7. Mode.c 文件操作LED和继电器。我们没有继电器,LED灯移植到 WIFI.c 文件下,删除 该文件。
  8. Relay.c 文件操作继电器,删除
  9. Timer0.c 文件实现任务调度器,需要修改定时器底层。删除 定时器初始化结尾前代码,并添加“HAL_TIM_Base_Start_IT(&htim17);”启动定时器。
  10. Uart_1.c 文件用于转发串口2数据到电脑。STM32F030K6 只有一个串口,删除 该文件。
  11. Uart2.c 文件主要和WIFI交换数据。修改 底层代码,使用STM32的 DMA+空闲中断 接收不定长数据。
  12. WDT.c 文件实现看门狗。未使用,删除

其他错误可双击编译结果跳转至指定位置。具体问题自行查阅资料修改,不赘述。
首次移植可只删除和移植串口代码,其他无关紧要的稍后移植。
如果一切顺利,编译没有错误,可下载到MCU。WIFI能够上云。

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC_Init();
  MX_TIM3_Init();
  MX_TIM14_Init();
  MX_USART1_UART_Init();
  MX_TIM17_Init();
  MX_TIM1_Init();
  MX_TIM16_Init();
  /* USER CODE BEGIN 2 */
//******** Init ***************//
    AdcInit();

    MotorSpeedInit();
    MotorPositionInit();

//******** OS Init ***************//
    Init_Uart2();
    Timer0_Init();
    Button_Init();
    WIFI_Init();

//******** PID ***************//
    Timer_0_Add_Fun(10, MotorPositionLoop);     //位置环 
//  Timer_0_Pas_Fun(MotorPositionLoop);         //位置环   暂停

    Timer_0_Add_Fun(10, MotorSpeedLoop);        //速度环
    Timer_0_Pause_Fun(MotorSpeedLoop);          //速度环   暂停

//******** OS ***************//
    Timer_0_Add_Fun(50, UserButton);            //按键检测底层业务
    Timer_0_Add_Fun(5, Uart2_CheckMessageLoop); //帧处理函数
    Timer_0_Add_Fun(500, WIFI_LED_Loop);        //网络状态指示灯 1Hz
    Timer_0_Add_Fun(500, Mode_LED_Loop);        //设备状态指示灯 1Hz

    Timer_0_Add_Fun(60 * 1000, WIFI_SubTemp);       //上报一次 温度 信息
    Timer_0_Add_Fun(61 * 1000, WIFI_SubLlluminance);//上报一次 光强 信息
    Timer_0_Add_Fun(62 * 1000, WIFI_SubVoltage);    //上报一次 电压 信息

//  Timer_0_Add_Fun(32 * 1000, WIFI_SubMotorMode);  //上报一次 电机运行模式 信息
//  Timer_0_Add_Fun(33 * 1000, WIFI_SubLimitStatus);//上报一次 限位状态 信息
//  Timer_0_Add_Fun(34 * 1000, WIFI_SubAction);     //上报一次 电机动作 信息
//  Timer_0_Add_Fun(35 * 1000, WIFI_SubMode);       //上报一次 窗帘模式 信息
//  Timer_0_Add_Fun(36 * 1000, WIFI_SubPosition);   //上报一次 窗帘位置 信息

//******** Message ***************//
    Timer0_Add_MessageFun('A', DistributionNetwork);    //上次AP配网不成功,开机会自动进 “配网模式” 
    Timer0_Add_MessageFun('F', DistributionNetwork);    //上下按键同时长按2S  配网
    Timer0_Add_MessageFun('U', MotorUp);                //上键  上行
    Timer0_Add_MessageFun('D', MotorDown);              //下键  下行
    Timer0_Add_MessageFun('S', LetGo);	                //松手检测
    Timer0_Add_MessageFun('S', WIFI_SubPosition);       //上传位置

//******** Buzzer ***************//
    Timer0_Add_MessageFun('C', Buzzer_DJI);             //连接网络
    Timer0_Add_MessageFun('U', Buzzer_Di);              //上键  上行
    Timer0_Add_MessageFun('D', Buzzer_Di);              //下键  下行

//******** Pause ***************// 
    PauseUpload();                                      //未联网时,所有上传动作暂停

//******** ReStart ***************//
    Timer0_Add_MessageFun('C', ReStartUpload);          //开机 连接网络成功

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {		
      Timer0_SYS_APP_LOOP();
      Timer0_SYS_APP_LOOP_Message();
      Timer0_SYS_APP_LOOP_Once();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

上传数据基于零妖代码结构。把所有上传项目独立,可配置不同项目不同上传频率。

//******************* 上传数据 **************************//
void WIFI_SubTemp(void)
{
    WIFI_SubStation(0);
}

void WIFI_SubLlluminance(void)
{
    WIFI_SubStation(1);
}

void WIFI_SubMotorMode(void)
{
    WIFI_SubStation(2);
}

void WIFI_SubLimitStatus(void)
{
    WIFI_SubStation(3);
}

void WIFI_SubMode(void)
{
    WIFI_SubStation(5);
}

void WIFI_SubPosition(void)
{
    WIFI_SubStation(6);
}

void WIFI_SubVoltage(void)
{
    WIFI_SubStation(7);
}

2. ADC&DMA采样

ADC采用DMA多通道不连续采集。使用二维数组缓存数据,每次获取ADC测量值时均采样10次求平均后上传。

//0:Current; 1:Voltage; 2:Temp; 3:Photo; 4:Vref
uint16_t AdcValue[10][5];
float AdcActualValue[10][5];
uint8_t AdcValuePosition = 0;
void AdcInit(void)
{
    //校准ADC
    HAL_ADCEx_Calibration_Start(&hadc);
    //开DMA
    AdcValuePosition = 0;
//  HAL_ADC_Start_DMA(&hadc, (uint32_t *)AdcValue[AdcValuePosition], 5);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* AdcHandle)
{
    HAL_ADC_Stop_DMA(&hadc);
    if(++AdcValuePosition >= 10)
        AdcValuePosition = 0;
    HAL_ADC_Start_DMA(&hadc, (uint32_t *)AdcValue[AdcValuePosition], 5);
}

温度和光强公式根据元件手册给出的温度-阻值、光强-阻值曲线拟合而成,辅以修正因子修正。

float AdcGetOneChannel(uint8_t channel)
{
    uint8_t i;
    float AdcReturn;
    float PowerVoltage;

    //校准ADC
    HAL_ADCEx_Calibration_Start(&hadc);
    //开DMA
    AdcValuePosition = 0;
    HAL_ADC_Start_DMA(&hadc, (uint32_t *)AdcValue[AdcValuePosition], 5);
    HAL_Delay(1);
    HAL_ADC_Stop_DMA(&hadc);

    for(i = 0; i < 10; i++)
    {
        PowerVoltage = 1.2 * 4096 / AdcValue[i][4];        //电源电压,内部参考电压1.2V
        switch(channel)
        {
            case 0: //电流,中点2.5v,增益100mV/A
                AdcActualValue[i][0] = ( 2.5 - ( AdcValue[i][0] * PowerVoltage / 4096 )) / 0.1;
                break;

            case 1: //电压,1/11
                AdcActualValue[i][1] = ( AdcValue[i][1] * PowerVoltage / 4096 ) * 11;
                break;

            case 2: //温度
                //-10~50℃: y = -33.186 * x + 80.268  (R^2 = 0.998)  //修正:-5
                AdcActualValue[i][2] = -33.186 * ((float)AdcValue[i][2] * PowerVoltage / 4096) + 80.268 - 5;
                break;

            case 3: //光强: [<400lx] y = 30.24 * x^(-3.54) (R^2 = 0.9653); [>400lx] y = -16691 * x + 8262.9 (R^2 = 0.9764);
                if(AdcValue[i][3] > (0.47 * 4096 / PowerVoltage))
                    AdcActualValue[i][3] = 30.24 * pow(( (float)AdcValue[i][3] * PowerVoltage / 4096 ), -3.54);
                else
                    AdcActualValue[i][3] = -16691 * ((float)AdcValue[i][3] * PowerVoltage / 4096 ) + 8262.9;
                break;

            default:
                break;
        }
        if(i == 0)
            AdcReturn = AdcActualValue[i][channel];
        AdcReturn += AdcActualValue[i][channel];
        AdcReturn /= 2;
    }
    return AdcReturn;
}

3. 蜂鸣器驱动

蜂鸣器通过TIM14-1通道驱动。内置25个环形缓存区数组实现蜂鸣器音调、音量、延时功能。

#define BuzzerParameterMax  25

//蜂鸣器参数:频率(0-65535Hz),音量(0-100),时长(0-65535ms)
uint16_t BuzzerParameter[BuzzerParameterMax][3] = {{0xFFFF, 0, 0}};
uint8_t BuzzerPositionOut = 0, BuzzerPositionIn = 0, BuzzerCount = 0;
float BuzzerReload = 0.0, BuzzerTime = -1.0, BuzzerBeat = 0.0;
uint8_t BuzzerWorking = 0;
void BuzzerInterrupt(void)    //蜂鸣器中断
{
    if(BuzzerTime > 0)  //延时中...
    {
        BuzzerTime -= BuzzerBeat;
    }
    else                //切换
    {
        if(BuzzerCount == 0)    //OVER
        {
            BuzzerWorking = 0;
            HAL_TIM_PWM_Stop(&htim14, TIM_CHANNEL_1);
            HAL_TIM_Base_Stop_IT(&htim14);
            HAL_GPIO_WritePin(Buzzer_GPIO_Port, Buzzer_Pin, GPIO_PIN_RESET);
        }
        else            //NEXT
        {
            BuzzerReload = 1000000.0 / BuzzerParameter[BuzzerPositionOut][0];
            __HAL_TIM_SET_AUTORELOAD(&htim14, (uint16_t)BuzzerReload - 1);
            __HAL_TIM_SET_COMPARE(&htim14, TIM_CHANNEL_1, (uint16_t)(BuzzerReload * BuzzerParameter[BuzzerPositionOut][1] * 0.01 * 0.9));

            BuzzerBeat = BuzzerReload / 1000;       //ms
            BuzzerTime = BuzzerParameter[BuzzerPositionOut][2];
            HAL_TIM_PWM_Start(&htim14, TIM_CHANNEL_1);
            HAL_TIM_Base_Start_IT(&htim14);

            BuzzerPositionOut = (BuzzerPositionOut + 1) % BuzzerParameterMax;
            BuzzerWorking = 1;
            BuzzerCount--;
        }
    }
}
uint8_t BuzzerSetParameter(uint16_t frequncy, uint8_t volume, uint16_t time)    //设置蜂鸣器参数
{
    BuzzerParameter[BuzzerPositionIn][0] = frequncy;
    BuzzerParameter[BuzzerPositionIn][1] = volume;
    BuzzerParameter[BuzzerPositionIn][2] = time;

    BuzzerPositionIn = (BuzzerPositionIn + 1) % BuzzerParameterMax;

    if((++BuzzerCount == 1) && (!BuzzerWorking))      //从停止状态启动
    {
        BuzzerTime = -1.0;
        BuzzerInterrupt();
    }

    if(BuzzerCount == BuzzerParameterMax)
        return 0;
    else 
        return 1;
}

4. PID及参数整定

PID使用增量式和位置式,分别用于速度环和位置环。

PID参数因电机而异,需要自行耐心调整。

void MotorIncrementPID(struct PID *pid, int16_t pidInput)
{
    pid->PidInput = pidInput;
    //Pid
    pid->PidEt = pid->PidSetPoint - pid->PidInput;
    pid->PidOutput += pid->PidKp * (pid->PidEt - pid->PidLastErr) \
                    + pid->PidKi * pid->PidEt \
                    + pid->PidKd * (pid->PidEt - 2 * pid->PidLastErr + pid->PidLastTwoErr);
    //Pid限幅
    pid->PidOutput = pid->PidOutput > pid->PidLimitUp ? pid->PidLimitUp : pid->PidOutput;
    pid->PidOutput = pid->PidOutput < pid->PidLimitDown ? pid->PidLimitDown : pid->PidOutput;
    //覆写
    pid->PidLastTwoErr = pid->PidLastErr;
    pid->PidLastErr = pid->PidEt;
}
void MotorPositionPID(struct PID *pid, int32_t pidInput)
{
    pid->PidInput = pidInput;
    //Pid
    pid->PidEt = pid->PidSetPoint - pid->PidInput;
    pid->PidEtSum += pid->PidEt;
    pid->PidOutput = pid->PidKp * pid->PidEt + pid->PidKi * pid->PidEtSum
                    + pid->PidKd * (pid->PidEt - pid->PidLastErr);
    //Pid限幅
    pid->PidOutput = pid->PidOutput > pid->PidLimitUp ? pid->PidLimitUp : pid->PidOutput;
    pid->PidOutput = pid->PidOutput < pid->PidLimitDown ? pid->PidLimitDown : pid->PidOutput;
    //覆写
    pid->PidLastErr = pid->PidEt;
}

因为使用的编码器电机阻尼大,大约5V电压才能启动,为了避免电机从停止状态退出过程时间过长,在PID输出和电机间添加MotorCurve函数。保证低速时呈对数变化,高速时线性变化。该函数已在我的遥控车项目验证,效果非常棒。

float MotorCurve(float inPut)
{
    float outPut = 0;
    outPut = (inPut < 0 ? -1 : 1);
    inPut = inPut > 265 ? 265 : inPut;
    inPut = inPut < -265 ? -265 : inPut;
    //输出曲线。类似对数曲线。
    //分界点 15:266.2; 0.002425; -0.155; use
    //可快速从驻车状态退出,且低速时刹车距离变短。
    //f(x) = 256.7[227.6, 285.8] * exp(0.002453[0.001927, 0.002979] * x)
    //				+ (-256.7[-297.8, -215.6]) * exp(-0.09653[-0.1635, -0.02957] * x);
    /*下面两行可合并为一行。此处改写是因为MarkDown不识别*/
    outPut *= 266.2 * (exp(0.002425 * (inPut < 0 ? - inPut : inPut));
    output -= exp(-0.155 * (inPut < 0 ? - inPut : inPut))) + 0.5;
    return outPut;
}

速度环:

void MotorSpeedInit(void)
{
    //PID: 5, 0.6, 1
    MotorInit(&PidSpeed);
    MotorSetPidParameter(&PidSpeed, 5, 0.6, 1);
    PidSpeed.PidLimitUp = 150;
    PidSpeed.PidLimitDown = -150;
}

从速度环曲线可以看出,调整时间大约0.3s。过程存在一点超调,正常现象,可以保证更快速的调整。

速度环调试曲线

                                    速度环调试曲线

位置环:

void MotorPositionInit(void)
{
    //0.6, 0, 13
    MotorInit(&PidPosition); 
    MotorSetPidParameter(&PidPosition, 0.6, 0, 13);     //微调该参数
    PidPosition.PidLimitUp = 200;
    PidPosition.PidLimitDown = -200;
}

位置环中间线性段受限于电机输出限幅。后段波动是施加扰动和释放扰动造成。
位置环曲线

                                    位置环曲线

5. 按键控制

按键部分删除了原来的Button_Loop实现函数。添加了User_Button函数,用于识别单按键按下,双按键按下,按键松开等动作。并向系统发布消息。

void UserButton(void)
{
    if( ((Button_ReadIO(0)) && (Button_ReadIO(1))) \
        && (ButtonTwoTime || Button_Timer[0] || Button_Timer[1]) )        //松手检测
    {
        Timer0_SendMessage('S');          //松手
    }

    if((!Button_ReadIO(0)) && (!Button_ReadIO(1)))      //同时按下
    {
        ButtonTwoTime++;
        Button_Timer[0] = 0;
        Button_Timer[1] = 0;
    }
    else
    {
        ButtonTwoTime = 0;

        if(Button_ReadIO(0) == 0)
            Button_Timer[0]++;
        else
            Button_Timer[0] = 0;

        if(Button_ReadIO(1) == 0)
            Button_Timer[1]++;
        else
            Button_Timer[1] = 0;
    }

    if(ButtonTwoTime > 65500)
        ButtonTwoTime = 65500;
    if(Button_Timer[0] > 65500)
        Button_Timer[0] = 65500;
    if(Button_Timer[1] > 65500)
        Button_Timer[1] = 65500;


    if(ButtonTwoTime > Button_L_Time)
    {
        Timer0_SendMessage('F');          //恢复出场设置
        return;
    }
    if(ButtonTwoTime == 0)              //按键未同时按下
    {
        if((Button_Timer[0] > Button_G_Time) && (Button_Timer[0] < Button_L_Time))
        {
            Timer0_SendMessage('U');          //上
            Button_Timer[0] = 65500;
            return;
        }
        if((Button_Timer[1] > Button_G_Time) && (Button_Timer[1] < Button_L_Time))
        {
            Timer0_SendMessage('D');          //下
            Button_Timer[1] = 65500;
            return;
        }
    }
}

为了实现一个消息可以对应多个功能函数,我们修改了Message循环部分代码。修改后的代码,可以实现:按下上键,电机速度环模式上行,同时蜂鸣器“滴”提示音。下键同理。

//******** Message ***************//
    Timer0_Add_MessageFun('A', DistributionNetwork);    //上次AP配网不成功,开机会自动进 “配网模式” 
    Timer0_Add_MessageFun('F', DistributionNetwork);    //上下按键同时长按2S  配网
    Timer0_Add_MessageFun('U', MotorUp);                //上键  上行
    Timer0_Add_MessageFun('D', MotorDown);              //下键  下行
    Timer0_Add_MessageFun('S', LetGo);                  //松手检测
    Timer0_Add_MessageFun('S', WIFI_SubPosition);       //上传位置

//******** Buzzer ***************//
    Timer0_Add_MessageFun('C', Buzzer_DJI);             //连接网络
    Timer0_Add_MessageFun('U', Buzzer_Di);              //上键  上行
    Timer0_Add_MessageFun('D', Buzzer_Di);              //下键  下行
//系统循环执行-邮箱处理
void Timer0_SYS_APP_LOOP_Message(void)
{
    signed char i = 0, j = 0;
    if(Timer0_Handler_Flag_Message == 0)
        return;
    Timer0_Handler_Flag_Message = 0;
	
    for(i = 0; i < Timer_0_List_Count; i++) //调用消息队列中的函数
    {
        if(Timer0_Message_Struct.MessageQueue[i])
        {
            for(j = 0; j < Timer_0_List_Count; j++)
            {
                if(Timer0_Message_Struct.Flag[j] == 1)
                {
                    if(Timer0_Message_Struct.MessageQueue[i] == Timer0_Message_Struct.MessageList[j]) 
                    {
                        Timer0_Message_Struct.MessageFun_Point_List[j]();
//                      j = Timer_0_List_Count + 10;      //注释则可以一个消息对应多个函数
                    }
                }
            }
            Timer0_Message_Struct.MessageQueue[i] = 0;
        }
    }
}

6. 任务/消息调度器改写

为了实现一些功能,我们在零妖的任务调度器基础上进行了修改。

对Flag部分重新规划。

unsigned char Flag[Timer_0_List_Count]; //0:空;1:运行;2:暂停;10:暂停所有

修改后的任务调度器添加了:删除、暂停、恢复、暂停所有、恢复所有功能。

//************** 任务 *****************//
//添加
unsigned char Timer_0_Add_Fun(unsigned long Time,void (*Fun)(void))
{
	signed char i = 0;
	for(i = 0; i < Timer_0_List_Count; i++)
	{
		if(Timer0_Struct.Flag[i] == 0)      //空的
		{
			Timer0_Struct.Flag[i] = 1;
			Timer0_Struct.Counter[i] = 0;
			Timer0_Struct.Fun_Point_List[i] = Fun;
			Timer0_Struct.Timer[i] = Time-1;
			return 1;
		}
	}
	return 0;
}

//删除
unsigned char Timer_0_Del_Fun(void (*Fun)(void))
{
    signed char i=0;
    for(i = (Timer_0_List_Count- 1); i >= 0; i--)     //从后往前
	{
        if(Fun == Timer0_Struct.Fun_Point_List[i])
        {
            Timer0_Struct.Flag[i] = 0;
			Timer0_Struct.Counter[i] = 0;
			return 1;
        }
	}
	return 0;
}

//暂停
unsigned char Timer_0_Pause_Fun(void (*Fun)(void))
{
    signed char i=0;
    for(i = (Timer_0_List_Count- 1); i >= 0; i--)     //从后往前
	{
        if((Fun == Timer0_Struct.Fun_Point_List[i]) 
            && (Timer0_Struct.Flag[i] == 1))
        {
            Timer0_Struct.Flag[i] = 2;
			Timer0_Struct.Counter[i] = 0;
        }
	}
	return 0;
}

//恢复
unsigned char Timer_0_ReStart_Fun(void (*Fun)(void))
{
    signed char i=0;
    for(i = (Timer_0_List_Count- 1); i >= 0; i--)     //从后往前
	{
        if(Fun == Timer0_Struct.Fun_Point_List[i])
        {
            Timer0_Struct.Flag[i] = 1;
			Timer0_Struct.Counter[i] = 0;
        }
	}
	return 0;
}

//暂停所有
unsigned char Timer_0_Pause_All(void)
{
    signed char i=0;
    for(i = 0; i < Timer_0_List_Count; i++)
	{
        if(Timer0_Struct.Flag[i] == 1)
        {
            Timer0_Struct.Flag[i] = 10;
            Timer0_Struct.Counter[i] = 0;
        }
	}
	return 0;
}

//恢复 通过“暂停所有”暂停的任务
unsigned char Timer_0_ReStart_All(void)
{
    signed char i=0;
    for(i = 0; i < Timer_0_List_Count; i++)
	{
        if(Timer0_Struct.Flag[i] == 10)
        {
            Timer0_Struct.Flag[i] = 1;
            Timer0_Struct.Counter[i] = 0;
        }
	}
	return 0;
}

为了实现一些功能,我们添加了Flag,并对其重新规划。

unsigned char Flag[Timer_0_List_Count]; //0:空;1:运行;2:暂停;10:暂停所有

修改后的消息调度器添加了:删除、暂停、恢复、暂停所有、恢复所有功能。

//************** 消息 *****************//
//添加
unsigned char Timer0_Add_MessageFun(unsigned char Message,void (*Fun)(void))
{
	signed char i;
	for(i = 0; i < Timer_0_List_Count; i++)
	{
		if(Timer0_Message_Struct.Flag[i] == 0)
		{
			Timer0_Message_Struct.Flag[i] = 1;
			Timer0_Message_Struct.MessageList[i] = Message;
			Timer0_Message_Struct.MessageFun_Point_List[i] = Fun;
			return 1;
		}
	}
	return 0;
}

//删除
unsigned char Timer0_Del_MessageFun(void (*Fun)(void))
{
	signed char i;
	for(i = (Timer_0_List_Count - 1); i >= 0; i--)     //从后往前
	{
		if(Fun == Timer0_Message_Struct.MessageFun_Point_List[i])
		{
			Timer0_Message_Struct.Flag[i] = 0;
			Timer0_Message_Struct.MessageList[i] = 0x00;
			return 1;
		}
	}
	return 0;
}

//暂停
unsigned char Timer0_Pause_MessageFun(void (*Fun)(void))
{
	signed char i;
	for(i = (Timer_0_List_Count - 1); i >= 0; i--)     //从后往前
	{
		if((Fun == Timer0_Message_Struct.MessageFun_Point_List[i]) 
            && (Timer0_Message_Struct.Flag[i] == 1))
		{
			Timer0_Message_Struct.Flag[i] = 2;
			return 1;
		}
	}
	return 0;
}

//恢复
unsigned char Timer0_ReStart_MessageFun(void (*Fun)(void))
{
	signed char i;
	for(i = (Timer_0_List_Count- 1); i >= 0; i--)     //从后往前删
	{
		if(Fun == Timer0_Message_Struct.MessageFun_Point_List[i])
		{
			Timer0_Message_Struct.Flag[i] = 1;
			return 1;
		}
	}
	return 0;
}

//暂停 所有
unsigned char Timer0_Pause_MessageAll(void)
{
	signed char i;
	for(i = (Timer_0_List_Count - 1); i >= 0; i--)     //从后往前
	{
		if(Timer0_Message_Struct.Flag[i] == 1)
		{
			Timer0_Message_Struct.Flag[i] = 10;
		}
	}
	return 0;
}

//恢复 所有
unsigned char Timer0_ReStart_MessageAll(void)
{
	signed char i;
	for(i = (Timer_0_List_Count- 1); i >= 0; i--)     //从后往前删
	{
		if(Timer0_Message_Struct.Flag[i] == 10)
		{
			Timer0_Message_Struct.Flag[i] = 1;
		}
	}
	return 0;
}

7. 配网模式

同时长按上下两按键2S,进入AP热点配网模式。

//配网模式
void DistributionNetwork(void)
{
    MotorPause();
    Timer_0_Pause_All();        //暂停所有任务
    Timer0_Pause_MessageAll();  //暂停所有消息
    Buzzer_BiPu();              //奏乐

    //开始配网流程
    Timer_0_Add_Fun_Once(1000, WIFI_CloseRTE);   //关闭回显
    Timer_0_Add_Fun_Once(1500, WIFI_ResetAuthor);//解除绑定关系,
    Timer_0_Add_Fun_Once(2000, WIFI_SendAT);     //AT
    Timer_0_Add_Fun_Once(2500, WIFI_SetILOP);    //设置三元组
    Timer_0_Add_Fun_Once(3000, WIFI_StartILOP);  //开启ILOP
//  Timer_0_Add_Fun_Once(5000, WIFI_StartAWS);   //路由器配网
    Timer_0_Add_Fun_Once(5000, WIFI_StartAP);    //热点配网

    //其他操作
    Timer_0_Add_Fun(100, WIFI_LED_Loop);            //WIFI指示灯快闪 5Hz
    Timer_0_Add_Fun(100, Mode_LED_Loop);            //快闪 5Hz
    Timer_0_ReStart_Fun(Uart2_CheckMessageLoop);    //帧处理函数 

//  Timer0_Add_MessageFun('C', WIFI_SubPosition);   //连接成功 主动上报一次位置
    Timer0_Add_MessageFun('C', NetworkConnected);   //连接成功
    Timer0_Add_MessageFun('E', Buzzer_DiLong);      //Error
    Timer0_Add_MessageFun('A', Buzzer_DiDiDi);      //AP已开启
}

配网成功,则执行NetworkConnected函数。删除配网过程创建的任务和消息,启用电机,恢复配网前所有任务和消息。最后奏乐,返回原来任务。无需重启设备,节省了重启&联网时间。
这点和零妖代码有较大区别。宋工实现过程是:进入配网过程,重新初始化所有任务、消息。创建配网需要的任务和消息,配网成功后调用软件重启指令,重新进入正常工作模式。这样会多一次重启WIFI上云过程。

//配网成功 已连接
void NetworkConnected(void)
{
   //删除 配网 过程创建的所有任务
   Timer_0_Del_Fun(WIFI_LED_Loop);             //WIFI指示灯
   Timer_0_Del_Fun(Mode_LED_Loop);             //
   Timer0_Del_MessageFun(NetworkConnected);    //连接成功
   Timer0_Del_MessageFun(Buzzer_DiLong);       //Error
   Timer0_Del_MessageFun(Buzzer_DiDiDi);       //AP已开启

   MotorReStart();

   Timer_0_ReStart_All();                      //恢复 配网前所有任务
   Timer0_ReStart_MessageAll();                //恢复 配网前所有消息
   Buzzer_DoToXi();                            //奏乐
}

结论

该项目参考零妖代码框架,删除了一些未使用的功能,修改了一些底层功能,增加了一些定制化功能。

设备基本实现了电动窗帘的本地/云端控制功能,能够按一定频率上传设备运行状态。当设备发生故障时,具有一定的处理能力。有着较为友好的交互方式。成本控制得当。

程序设计采用任务/消息调度器模式,可方便地添加、删减功能而不影响其他功能运行。

由于时间、精力、能力有限,设备还存在着诸多问题待修复,已知问题罗列如下,如您有时间、有精力、有能力,可尝试修复。

  1. 软硬限位未实现,窗帘存在超限位运行损坏风险。窗帘超限位后APP无法显示实际位置;
  2. PID参数设定需要手动完成。
  3. 当外接电源为开关电源时,电机反转会触发开关电源过压保护。加入SS54二极管防倒灌可解决。但电机反转时设备电压会被拉高,当使用24V电源时可能会导致电机驱动芯片过压保护.
  4. 光敏电阻和热敏电阻测量数值不准确。

致 谢

感谢立创EDA开设活动,提供一个 白嫖 学习的机会。

感谢零妖源码,学习了任务/消息调度器、环形缓存区、JSON字符串比较等知识,受益匪浅。

感谢客编:481978A大佬上传的文件,解决了令人头秃的配网流程。

设计图

原理图(1 / 5)
PCB

BOM

IDNameDesignatorFootprintQuantity
1LED-0603_RLED1LED0603_RED1
28MHzX1OSC-SMD_4P-L3.2-W2.5-BL1
30RR1R06031
410kR2R06031
51.5kR3R06031

附件

序号文件名称下载次数
1
BOM_RollerBlindGeneral.pdf
11
2
点位_RollerBlindGeneral.pdf
11
3
原理图_RollerBlindGeneral.pdf
14
4
BOM_RollerBlindGeneral.xls
13
5
Gerber_RollerBlindGeneral_V1.0_NoSilk.zip
12
6
卷帘大将_电动窗帘_说明文档.pdf
17
7
配网.mp4
95
8
联网控制.mp4
82
9
配网过程意外断电重启.mp4
110
10
Code_RollerBlindGeneral.zip
19
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

评论

全部评论(1)
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航