
【训练营_进阶班】卷帘大将
简介
该项目旨在开发一款电动窗帘设备。 设备使用STM32单片机做主控,通过控制编码器电机正反转实现窗帘的电动开合, 并通过EMW3080接入阿里云物联网平台,实现本地/云端控制。
简介:该项目旨在开发一款电动窗帘设备。 设备使用STM32单片机做主控,通过控制编码器电机正反转实现窗帘的电动开合, 并通过EMW3080接入阿里云物联网平台,实现本地/云端控制。开源协议
:CC-BY-NC 3.0
描述
卷帘大将
注:
该工程描述文件同说明文档,可下载附件“卷帘大将_电动窗帘_说明文档.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热敏电阻,可反馈光照和温度;两枚按键,单键可手动控制窗帘的上下行,双键可进入配网模式。
使用方法
一、配网
- 上电后任意时刻,同时按动两按键两秒(先后按动两按键也可识别)。
- 设备奏乐“ Bi~~~ Pu ”,开始配网流程。蓝色、绿色指示灯快闪。
- 手机打开“云智慧”,扫码后,按提示开始配网。(扫码可下载APP)。
- 等待5s后设备奏乐“嘀嘀嘀”三声,设备开启AP热点。
- 手机端输入设备将要连接的家庭路由器名称和密码,点击“开始连接”,开始配网。(低版本安卓系统可直接接入adh_xxx热点,高版本安卓需手动连接该热点,热点连接成功后会断开,返回APP界面即可)。
- 耐心等待设备上云。若过程中设备奏乐“ 滴~~~~ ”表示出错,可以断电重启后从第2步重试。
- 当设备配网成功后,奏乐“ 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二极管,再接入设备。
DCDC 和 LDO 部分按照官方手册绘制就可。
原理图-电源
2. MCU
MCU部分主要设计 晶振电路,复位电路,SWD下载接口。
晶振 采用SMD-3225封装的8MHz无源晶振,该封装对烙铁焊接不友好。晶振电路主要由晶振和两个22pF无极性陶瓷电容构成。
复位电路 由10k上拉电阻和0.1uF电容构成,主要完成上电复位功能。
SWD接口 用于调试和下载程序,引出了SWCLK、SWDIO、NRST,采用XH2.5-4P端子接口。引出NRST引脚,即使程序中未使能SWD调试接口仍能下载、调试程序。
原理图-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
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效果图
1. 电源&电机驱动
电源 和 电机驱动 主要注意走线宽度、功率地和信号地分开、端子下面挖空防止接地短路等。
PCB-电源&电机驱动
左侧为 LDO,右侧为 DCDC。注意电感离DCDC芯片近一些,电感下面不要走信号线。(此图为错误示范)
PCB-DCDC&LDO
2. MCU
MCU主要注意晶振连线短一些,滤波电容靠近MCU电源引脚。
PCB-MCU
3. EMW3080
EMW3080按照官方手册要求,1、2、24、25脚不接,天线前方、左右留16mm净空区。搜索EDA中所有的封装都不完全满足官方手册要求,我自己画了一个。
PCB-EMW3080
4. 外设
热敏 避开发热区域;光敏 避开LED区域;编码器接口 放在电机端子旁边,方便连接;按键 和 LED指示灯 放在板子下方,方便操作。
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主界面
六 程序调试
程序调试主要按功能块调试。调试日志按以下顺序依次进行:代码移植、上传数据、蜂鸣器驱动、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) 。
下面开始解决这些错误:
- 大多数错误是由于 *#include * 造成的,这是51的头文件,32不使用,删除 所有该语句。
- Button.c 文件实现按键的短按、长按识别。该设备只需识别单按键和双按键,修改 其中的Button_Loop 函数。
- DeviceName.c 文件操作三元组数据。我们把三元组数据移植到 usart.c 文件下,删除 该文件。
- DS18B20.c 文件用于读取温度传感器数据。我们温度采用热敏电阻配合ADC,不涉及该文件,删除。
- IAP_EEPROM.c 文件操作 EEPROM。32没有,删除 文件。
- main.c 文件是设备主逻辑实现。复制内容到CubeMX创建的 main.c 文件中,删除 该文件。
- Mode.c 文件操作LED和继电器。我们没有继电器,LED灯移植到 WIFI.c 文件下,删除 该文件。
- Relay.c 文件操作继电器,删除 。
- Timer0.c 文件实现任务调度器,需要修改定时器底层。删除 定时器初始化结尾前代码,并添加“HAL_TIM_Base_Start_IT(&htim17);”启动定时器。
- Uart_1.c 文件用于转发串口2数据到电脑。STM32F030K6 只有一个串口,删除 该文件。
- Uart2.c 文件主要和WIFI交换数据。修改 底层代码,使用STM32的 DMA+空闲中断 接收不定长数据。
- 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(); //奏乐
}
结论
该项目参考零妖代码框架,删除了一些未使用的功能,修改了一些底层功能,增加了一些定制化功能。
设备基本实现了电动窗帘的本地/云端控制功能,能够按一定频率上传设备运行状态。当设备发生故障时,具有一定的处理能力。有着较为友好的交互方式。成本控制得当。
程序设计采用任务/消息调度器模式,可方便地添加、删减功能而不影响其他功能运行。
由于时间、精力、能力有限,设备还存在着诸多问题待修复,已知问题罗列如下,如您有时间、有精力、有能力,可尝试修复。
- 软硬限位未实现,窗帘存在超限位运行损坏风险。窗帘超限位后APP无法显示实际位置;
- PID参数设定需要手动完成。
- 当外接电源为开关电源时,电机反转会触发开关电源过压保护。加入SS54二极管防倒灌可解决。但电机反转时设备电压会被拉高,当使用24V电源时可能会导致电机驱动芯片过压保护.
- 光敏电阻和热敏电阻测量数值不准确。
致 谢
感谢立创EDA开设活动,提供一个 白嫖 学习的机会。
感谢零妖源码,学习了任务/消息调度器、环形缓存区、JSON字符串比较等知识,受益匪浅。
感谢客编:481978A大佬上传的文件,解决了令人头秃的配网流程。
设计图
BOM
ID | Name | Designator | Footprint | Quantity |
---|---|---|---|---|
1 | LED-0603_R | LED1 | LED0603_RED | 1 |
2 | 8MHz | X1 | OSC-SMD_4P-L3.2-W2.5-BL | 1 |
3 | 0R | R1 | R0603 | 1 |
4 | 10k | R2 | R0603 | 1 |
5 | 1.5k | R3 | R0603 | 1 |

评论