【智能窗帘扩展板】资料
题目与要求
基于梁山派的智能窗帘系统的设计与实现
1.题目的背景
随着科学技术的发展和人们生活水平的提高,智能化家居系统越来越多的出现在人们的生活中,带给人们快捷、舒适的体验。对于家居必需品来讲,窗帘占有着重要的地位。由于手动窗帘需要手动开关,电动窗帘无法根据光照实现自动控制且需要特定遥控器,具有一定的局限性。因此设计了一款基于梁山派微控制器的智能窗帘控制系统。
2.设计要求与指标
技术要求
(1)雨滴传感器模块:用于检测雨滴;
(2)光线强度传感器模块:用于检测光线强度;
(3)红外接收模块,用于遥控控制;
(4)步进电机模块:用于窗户窗帘驱动。
技术指标
(1)可以通过红外遥控与语音设置是否打开自动模式;
(2)在自动模式下,当光照强度很高时,自动展开窗帘;当光照强度很低时,自动关闭窗帘;
(3)在自动模式下,当检测到大量雨滴时,自动展开窗帘,此优先级比光照检测高;
(4)在任何时候,可以通过红外遥控或语音命令展开与关闭窗帘,并关闭自动模式;
(5)主控芯片接收到数据后,驱动电机运转,现实智能窗帘窗户系统。
一、雨滴与光照检测
1. 雨滴检测原理
雨滴传感器常见的工作原理是通过检测水滴的导电性来判断是否下雨。它是利用两个电极之间的电导性变化来测量水滴的存在。这两个电极之间会有一个空气间隙,正常状态下是断路状态。当水滴接触到电极上时,水滴的导电性会导致电流通过水滴形成电流回路,从而改变电极之间的电阻值。通过测量电阻值的变化,就可以判断是否有水滴存在。
2. 雨滴传感器接口设计
雨滴传感器购买链接:https://detail.tmall.com/item.htm?abbucket=0&id=41266204564&ns=1&spm=a21n57.1.0.0.4c52523cd1r9Zc
我们知道了雨滴传感器会根据雨滴进行电阻阻值变化。那我们如何通过梁山派去检测雨滴呢?
我们给雨滴传感器接入电源,当雨滴传感器的电阻发生变化时,其电压也会产生变化。此时我们可以把雨滴传感器看作一个开关。当没有雨滴时,雨滴传感器没有因为雨滴短路,所以处于断路状态。
当有雨滴时,雨滴传感器因为雨滴导电,导致两个电极短路,相当于按键按下。
因此我们可以测量雨滴传感器的电压变化就可以知道是否有雨滴。
扩展板雨滴传感器接口如下:
我们需要给雨滴传感器的一极接正,一极接负,才可以产生回路测量电压变化。但是当传感器的雨滴过多时会导致两极彻底短路。为了防止雨滴传感器彻底短路,需要增加一个电阻R1作为负载,防止3.3V直接接入GND,形成电源短路。
3. 光照检测原理
扩展板的光照检测功能是通过光敏电阻进行识别。光敏电阻是一种特殊的电阻器,它随着光照强度的升高,电阻值会迅速降低,其在无光照时,几乎呈高阻状态,因此暗时电阻很大。
市场上最常见的用于检测光照的模块就是以光敏电阻作为主要识别器件。它的原理图见右方,原理图中的U2.1是一个电压比较器。当电压比较器的2脚电压大于3脚电压时,1脚输出低电平;当电压比较器的2脚电压小于3脚电压时,1脚输出高电平;R3为光敏电阻,当光敏电阻周围环境很亮时,其阻值会变小,AO的电压也会变小;当光敏电阻周围环境很暗时,其阻值会变大,AO的电压也会变大;因此通过调整R4滑动电阻的阻值,就可以调整DO输出的灵敏度。
4. 光照传感器接口设计
我们将模块原理图简化,直接使用一个分压电阻加上一个光敏电阻即可。我们扩展板直接检测的是光敏电阻的电压变化,见模块原理图中的AO。
光照传感器购买链接:因为只使用到了光敏电阻一个器件,可直接在立创商城购买;
5. ADC介绍
ADC是一种用于将模拟信号转换为数字信号的模拟数字转换器。我们知道,模拟信号是连续的,其取值可以在一定范围内任意变化。而数字信号则是离散的,仅能取有限的值。ADC的工作原理是将模拟信号通过采样转换为离散的数字信号,然后再通过量化、编码等处理,最终得到对应的数字表示。简单来说就是将电压转换为数字量,例如测量3.3V的电压时数字量是4095,测量1.65V电压时数字量是2047,测量0V电压时数字量是0。
我们两个传感器都需要采集它们的电压变化,其中雨滴传感器接入的引脚是PF8,光敏电阻传感器接入的是PF6。要开启ADC功能,必须确保引脚有ADC的采集通道,它们对应的ADC通道如下:
PF6的附加功能有ADC2的通道4,PF8的附加功能有ADC2的通道6。
/* 雨滴传感器接口 PF8 ADC2_IN6 */
#define BSP_RAINDROP_GPIO_RCU RCU_GPIOF
#define BSP_RAINDROP_GPIO_PORT GPIOF
#define BSP_RAINDROP_GPIO_PIN GPIO_PIN_8
/* 光照传感器接口 PF6 ADC2_IN4 */
#define BSP_LIGHT_GPIO_RCU RCU_GPIOF
#define BSP_LIGHT_GPIO_PORT GPIOF
#define BSP_LIGHT_GPIO_PIN GPIO_PIN_6
#define BSP_LIGHT_ADC_RCU RCU_ADC2
#define BSP_LIGHT_ADC ADC2
#define BSP_LIGHT_ADC_CHANNEL ADC_CHANNEL_4
#define BSP_RAINDROP_ADC_CHANNEL ADC_CHANNEL_6
2
3
4
5
6
7
8
9
10
11
12
13
14
6. ADC配置
当前的ADC配置为软件触发方式的单次扫描模式。
软件触发方式意思很简单,就是通过我们编写代码的方式,去触发ADC的采集。
单次扫描模式指的是,触发采集之后,采集一次配置的通道数据,然后停止,等待下一次采集触发。
void raindrop_and_light_config(void)
{
//使能引脚时钟
rcu_periph_clock_enable(BSP_RAINDROP_GPIO_RCU);
rcu_periph_clock_enable(BSP_LIGHT_GPIO_RCU);
//使能ADC时钟
rcu_periph_clock_enable(BSP_ADC_RCU);
//配置ADC时钟
adc_clock_config(ADC_ADCCK_PCLK2_DIV4);
//配置引脚为模拟输入模式
gpio_mode_set(BSP_RAINDROP_GPIO_PORT, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, BSP_RAINDROP_GPIO_PIN);
gpio_mode_set(BSP_LIGHT_GPIO_PORT, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, BSP_LIGHT_GPIO_PIN);
//配置ADC为独立模式
adc_sync_mode_config(ADC_SYNC_MODE_INDEPENDENT);
//使能扫描模式
adc_special_function_config(BSP_ADC, ADC_SCAN_MODE, ENABLE);
//数据右对齐
adc_data_alignment_config(BSP_ADC, ADC_DATAALIGN_RIGHT);
//ADC0设置为12位分辨率
adc_resolution_config(BSP_ADC, ADC_RESOLUTION_12B);
//ADC0设置为规则组 一共使用 1 个通道
adc_channel_length_config(BSP_ADC,ADC_REGULAR_CHANNEL, 1);
//ADC外部触发禁用, 即只能使用软件触发
adc_external_trigger_config(BSP_ADC, ADC_REGULAR_CHANNEL, EXTERNAL_TRIGGER_DISABLE);
//ADC0使能
adc_enable(BSP_ADC);
//开启ADC自校准
adc_calibration_enable(BSP_ADC);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
7. ADC采集
我们之前使用的是扫描模式并使用的是软件触发方式,因此当我们要采集信号时,需要配置好采集通道并开启软件转换。开启转换后,等待EOC标志位置1。其中EOC为转换完成标志位,我们可以通过判断其是否置1,确定是否转换完成。转换完成后将数据从16位寄存器中取出。
/**********************************************************
*函 数 名 称:Get_ADC_Value
*函 数 功 能:读取ADC值
*传 入 参 数:ADC_CHANNEL_x=要采集的通道
*函 数 返 回:测量到的值
*作 者:LC
*备 注:无
**********************************************************/
unsigned int Get_ADC_Value(uint8_t ADC_CHANNEL_x)
{
unsigned int adc_value = 0;
//设置采集通道
adc_regular_channel_config(BSP_ADC, 0, ADC_CHANNEL_x, ADC_SAMPLETIME_15);
//软件触发方式,开始常规通道组软件转换
adc_software_trigger_enable(BSP_ADC, ADC_REGULAR_CHANNEL);
//等待 ADC 采样完成
while ( adc_flag_get(BSP_ADC, ADC_FLAG_EOC) == RESET )
{
;
}
//读取采样值
adc_value = adc_regular_data_read(BSP_ADC);
//返回采样值
return adc_value;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
8. 功能验证
在main.c中,不断采集雨滴值和光照值,再通过串口输出。
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_usart.h"
#include "bsp_adc.h"
int main(void)
{
uint16_t value = 0;
//滴答定时器初始化 1ms
systick_config();
//串口0初始化 调试
usart_gpio_config(9600U);
//ADC接口初始化
raindrop_and_light_config();
while(1)
{
//读取光照值
value = Get_ADC_Value(BSP_LIGHT_ADC_CHANNEL);
//串口输出光照值
printf("illumination = %d\r\n",value );
//读取雨滴值
value = Get_ADC_Value(BSP_RAINDROP_ADC_CHANNEL);
//串口输出雨滴值
printf("raindrop = %d\r\n",value );
delay_1ms(500);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
验证结果:
INFO
验证代码工程文件
二、步进电机驱动
1. 步进电机介绍
步进电机是将电脉冲信号,转变为角位移或线位移的开环控制电机,又称为脉冲电机。步进电机组成最主要的就是转子和定子部分。
- 定子,就是由电流控制磁场方向,通电时就会产生磁力;
- 转子,被定子环绕在中间受定子磁场变化产生转动(下方示意图中转动的指针)
通过给定子通电,产生磁力,将转子吸附过来,那转子就会转一小格;通过给定子连续的通电,就可以实现让转子转动。
在非超载的情况下,电机的转速、停止的位置只取决于脉冲信号的频率和脉冲数。当步进驱动器接收到一个脉冲信号时,它就可以驱动步进电机按设定的方向转动一个固定的角度。因此:
- 可以通过控制脉冲个数来控制角位移量,从而达到准确定位的目的;
- 可以通过控制脉冲频率来控制电机转动的速度和加速度,从而达到调速的目的;
- 可以通过控制绕组通电顺序,达到控制电机正反转的目的。
2. 步进电机的转动
我们采购的是二相四线式步进电机,二相指的是有两个线圈,四线指的是每一个线圈有两根线。其中A+与A-为一相,B+与B-为一相。
采购链接:https://item.taobao.com/item.htm?spm=a1z09.2.0.0.4be02e8dpoBqfX&id=642594293054&_u=72t4uge55e33 买带滑块加转换座的,调试时就可以清楚看到电机在转;加转换座可以方便连接;
要让它转动起来,需要给线圈连续通电。而转动方式有四拍方式、八拍方式。
四拍方式的转动顺序:【A+】->【B+】->【A-】->【B-】。
八拍方式的转动顺序:【A+】->【A+B+】->【B+】->【B+A-】->【A-】->【A-B-】->【B-】->【B-A+】。
使用磁性电机,电流越大,磁力越强。虽然直接使用开发板的GPIO去控制步进电机也可以,但是会有损坏开发板引脚的风险。因此我们需要考虑一个合适的步进电机驱动。
3. 步进电机硬件驱动设计
案例使用L9110S作为步进电机的驱动,在其数据手册中有应用电路。我们根据数据手册的说明,进行设计即可。
其中BAK控制X4引脚的电平输出;FOR控制X3引脚的电平输出。图中的电机A,接入步进电机的一相即可。按照思路则需要两个L9110S才可以驱动二相四线步进电机。
4. 步进电机软件驱动设计
要想让步进电机动起来,我们需要先配置好引脚。引脚的配置直接配置为推挽输出模式即可。
#define AP_RCU RCU_GPIOG
#define AP_PORT GPIOG
#define AP_PIN GPIO_PIN_12
#define AM_RCU RCU_GPIOG
#define AM_PORT GPIOG
#define AM_PIN GPIO_PIN_10
#define BP_RCU RCU_GPIOB
#define BP_PORT GPIOB
#define BP_PIN GPIO_PIN_9
#define BM_RCU RCU_GPIOB
#define BM_PORT GPIOB
#define BM_PIN GPIO_PIN_6
#define AP(X) gpio_bit_write(AP_PORT, AP_PIN, X?SET:RESET)//A+
#define AM(X) gpio_bit_write(AM_PORT, AM_PIN, X?SET:RESET)//A-
#define BP(X) gpio_bit_write(BP_PORT, BP_PIN, X?SET:RESET)//B+
#define BM(X) gpio_bit_write(BM_PORT, BM_PIN, X?SET:RESET)//B-
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/******************************************************************
* 函 数 名 称:stepper_motor_config
* 函 数 说 明:对步进电机引脚初始化
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void stepper_motor_config(void)
{
rcu_periph_clock_enable(AP_RCU); // 开启时钟
rcu_periph_clock_enable(AM_RCU); // 开启时钟
rcu_periph_clock_enable(BP_RCU); // 开启时钟
rcu_periph_clock_enable(BM_RCU); // 开启时钟
/* 配置A+推挽输出模式 上拉模式 */
gpio_mode_set(AP_PORT,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,AP_PIN);
gpio_output_options_set(AP_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,AP_PIN);
/* 配置A-推挽输出模式 上拉模式 */
gpio_mode_set(AM_PORT,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,AM_PIN);
gpio_output_options_set(AM_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,AM_PIN);
/* 配置B+推挽输出模式 上拉模式 */
gpio_mode_set(BP_PORT,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,BP_PIN);
gpio_output_options_set(BP_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,BP_PIN);
/* 配置B-推挽输出模式 上拉模式 */
gpio_mode_set(BM_PORT,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,BM_PIN);
gpio_output_options_set(BM_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,BM_PIN);
AP(0);
BP(0);
AM(0);
BM(0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
本案例使用的是八拍方式驱动步进电机,则顺时针的控制方式为:
将以上控制方式的顺序,转为16进制,得到顺时针旋转的控制码。
uint8_t phasecw[] = {0x08, 0x0c, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09};
最终实现顺时针旋转的代码:
//顺时针,转动顺序:a+ b+ a- b-
void motor_cw(void)
{
static uint8_t i=0;
//开启了顺时针动作
if( motor_cw_flag == 1 )
{
AP ( ( phasecw[] >> 3 ) & 0x01 );
BP ( ( phasecw[] >> 2 ) & 0x01 );
AM ( ( phasecw[] >> 1 ) & 0x01 );
BM ( ( phasecw[] >> 0 ) & 0x01 );
//拍数增加
i = ( i + 1 ) % 8;
//记录当前步数
step_count++;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
逆时针旋转同理。
uint8_t phaseccw[] = {0x09, 0x01, 0x03, 0x02, 0x06, 0x04, 0x0c, 0x08};
//逆时针,转动顺序:b- a- b+ a+
void motor_ccw( void )
{
static uint8_t i=0;
//如果开启了逆时针动作
if( motor_ccw_flag == 1 )
{
AP ( ( phaseccw[] >> 3 ) & 0x01 );
BP ( ( phaseccw[] >> 2 ) & 0x01 );
AM ( ( phaseccw[] >> 1 ) & 0x01 );
BM ( ( phaseccw[] >> 0 ) & 0x01 );
i=(i+1)%8;
//记录当前步数
if( step_count <= 1 ) step_count = 1;
step_count--;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
但是,此时我们还不能够将其应用,如果现在下载程序,会发现步进电机只是在震动,没有旋转。是因为我们给的速度太快了,给完一个脉冲后,直接给下一个脉冲,步进电机还没有被吸附过去,这个脉冲就结束了。
所以我们需要给脉冲一定的延时,等待转子被吸附过去之后,再启动下一次脉冲。本案例开启了定时器5计时中断,作为步进电机的脉冲频率,每隔 2 Ms,更新一次脉冲。
/******************************************************************
* 函 数 名 称:stepper_motor_timer_config
* 函 数 说 明:步进电机脉冲更新频率定时器初始化
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:设置为2ms,即每2ms更新一次脉冲。速度太快只会震动,时间太慢速度也很慢
******************************************************************/
void stepper_motor_timer_config(void)
{
/* 一个周期的时间T = 1/f, 定时时间time = T * 周期
设预分频值位pre,周期位per
time = (pre + 1) * (per + 1) / psc_clk
*/
timer_parameter_struct timere_initpara; // 定义定时器结构体
/* 开启时钟 */
rcu_periph_clock_enable(RCU_TIMER5); // 开启定时器时钟
/* CK_TIMERx = 4 x CK_APB1 = 4x50M = 200MHZ */
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4); // 配置定时器时钟
timer_deinit(TIMER5); // 复位定时器
/* 配置定时器参数 */
timere_initpara.prescaler = 2000-1; // 时钟预分频值 0-65535
timere_initpara.alignedmode = TIMER_COUNTER_EDGE; // 边缘对齐
timere_initpara.counterdirection = TIMER_COUNTER_UP; // 向上计数
timere_initpara.period = 200-1; // 周期
timere_initpara.clockdivision = TIMER_CKDIV_DIV1; // 分频因子
timere_initpara.repetitioncounter = 0; // 重复计数器 0-255
timer_init(TIMER5,&timere_initpara); // 初始化定时器
/* 配置中断优先级 */
nvic_irq_enable(TIMER5_DAC_IRQn,1,2); // 设置中断优先级为 3,2
/* 使能中断 */
timer_interrupt_enable(TIMER5,TIMER_INT_UP); // 使能更新事件中断
/* 使能定时器 */
timer_enable(TIMER5);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在定时器5中断中,将步进电机的动作放入,让其每隔2ms更新一次脉冲。
/************************************************
函数名称 : BSP_TIMER_IRQHandler
功 能 : 基本定时器中断服务函数
参 数 : 无
返 回 值 : 无
作 者 : LC
*************************************************/
void TIMER5_DAC_IRQHandler(void)
{
/* 这里是定时器中断 */
if(timer_interrupt_flag_get(TIMER5,TIMER_INT_FLAG_UP) == SET)
{
// 清除中断标志位
timer_interrupt_flag_clear(TIMER5,TIMER_INT_FLAG_UP);
//顺时针旋转
motor_cw();
//逆时针旋转
motor_ccw();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可能会有朋友,在这里感到迷惑,为什么顺时针和逆时针放在一起,那不是就不动了吗?
我们在顺时针和逆时针函数中,加入了一个开启标志位,只有当对应标志位为1时,才会开启动作。
例如,我要让它顺时针旋转,那么可以这样写:
motor_cw_flag = 1; //开启顺时针旋转
motor_ccw_flag = 0; //关闭逆时针旋转
2
而逆时针旋转,可以这样写:
motor_cw_flag = 0; //关闭顺时针旋转
motor_ccw_flag = 1; //开启逆时针旋转
2
5. 功能验证
在main.c中编写以下代码:
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_usart.h"
#include "bsp_adc.h"
#include "bsp_stepper_motor.h"
int main(void)
{
uint8_t motor_flag = 0;
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
//滴答定时器初始化 1ms
systick_config();
//串口0初始化 调试
usart_gpio_config(9600U);
//步进电机初始化
stepper_motor_config();
stepper_motor_timer_config();
while(1)
{
//状态每隔500ms,取反一次
motor_flag = !motor_flag;
//如果当前状态为0
if( motor_flag == 0 )
{
//开启顺时针旋转
motor_cw_flag = 1;
motor_ccw_flag = 0;
}
else//如果当前状态为0
{
//开启逆时针旋转
motor_cw_flag = 0;
motor_ccw_flag = 1;
}
delay_1ms(500);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
验证结果:
验证结果
验证代码工程文件:
三、红外接收驱动
1. 红外线协议介绍
在光谱中波长自760nm至400um的电磁波称为红外线,它是一种不可见光。红外线通信的例子我们每个人应该都很熟悉,目前常用的家电设备几乎都可以通过红外遥控的方式进行遥控,比如电视机、空调、投影仪等,都可以见到红外遥控的影子。这种技术应用广泛,相应的应用器件都十分廉价,因此红外遥控是我们日常设备控制的理想方式。
2. 红外线的通讯原理
红外光是以特定的频率脉冲形式发射,接收端收到到信号后,按照约定的协议进行解码,完成数据传输。在消费类电子产品里,脉冲频率普遍采用 30KHz 到 60KHz 这个频段,NEC协议的频率就是38KHZ。 这个以特定的频率发射其实就可以理解为点灯,不要被复杂的词汇难住了,就是控制灯的闪烁频率(亮灭),和刚学单片机完成闪烁灯一样的意思,只不过是灯换了一种类型,都是灯。
接收端的原理: 接收端的芯片对这个红外光比较敏感,可以根据有没有光输出高低电平,如果发送端的闪烁频率是有规律的,接收端收到后输出的高电平和低电平也是有规律对应的,这样发送端和接收端只要约定好,那就可以做数据传输了。
红外线传输协议可以说是所有无线传输协议里成本最低,最方便的传输协议了,但是也有缺点,距离不够长,速度不够快;当然,每个传输协议应用的环境不一样,定位不一样,好坏没法比较,具体要看自己的实际场景选择合适的通信方式。
3. NEC协议介绍
NEC协议是众多红外线协议中的一种(这里说的协议就是他们数据帧格式定义不一样,数据传输原理都是一样的),我们购买的外能遥控器、淘宝买的mini遥控器、电视机、投影仪几乎都是NEC协议。 像格力空调、美的空调这些设备使用的就是其他协议格式,不是NEC协议,但是只要学会一种协议解析方式,明白了红外线传输原理,其他遥控器协议都可以解出来。
NEC协议一次完整的传输包含: 引导码、8位地址码、8位地址反码、8位命令码、8位命令反码。这里我们主要讲解如何接收红外发送端发送的NEC协议内容。
引导码: 由9ms的低电平+4.5ms的高电平组成。
4个字节的数据: 地址码+地址反码+命令码+命令反码。 这里的反码可以用来校验数据是否传输正确,有没有丢包。
重点: NEC协议传输数据位的时候,0和1的区分是依靠收到的高、低电平的持续时间来进行区分的。这是解码关键。
数据发送0码:0.56m低电平+ 0.56ms的高电平。
数据发送1码:0.56ms低电平+1.68ms的高电平
所以,收到一个数据位的完整时间表示方法是这样的:
收到数据位0: 0.56m低电平+ 0.56ms的高电平
收到数据位1: 0.56ms低电平+1.68ms的高电平
还有一个重复码,它是由一个 9ms 的低电平和一个 2.5ms 的高电平组成。当一个红外信号连续发送时,可以通过发送重复码的方式快速发送。
4. 红外接收硬件接口设计
红外线接收头模块输出电平的原理: 红外线接收头感应到有红外光就输出低电平,没有感应到红外光就输出高电平。因此,我们只要检测OUT端,是否输出低电平,就可以知道是否有接收到红外数据。
这里选择接入PF7,没有什么特殊要求,使用普通的GPIO即可。
5. 红外接收软件遥控解码
关于红外遥控,是采购的淘宝的一款标准红外遥控。使用前,要记得拔出尾巴的塑料薄片,让遥控器通电。这个遥控器内部已经集成了NEC红外协议,我们可以直接使用。
购买链接:https://detail.tmall.com/item.htm?_u=72t4uge51318&id=548393997684&skuId=4361372496386
引脚配置
当红外线接收头感应到有红外光就输出低电平,没有感应到红外光就输出高电平。因此我们配置红外引脚为外部中断下降沿触发方式,当红外引脚有下降沿时,我们马上进入中断处理并接收红外信号。
#define IR_RCU RCU_GPIOF
#define IR_PORT GPIOF
#define IR_PIN GPIO_PIN_7
#define EXTI_X EXTI_7
#define EXTI_IRQ EXTI5_9_IRQn
#define EXTI_SOURCE_PORT EXTI_SOURCE_GPIOF
#define EXTI_SOURCE_PIN EXTI_SOURCE_PIN7
2
3
4
5
6
7
8
引脚配置如下:
//红外引脚初始化
void infrared_goio_config(void)
{
//开启引脚时钟
rcu_periph_clock_enable(IR_RCU);
//开启系统配置时钟
rcu_periph_clock_enable(RCU_SYSCFG);
//配置引脚为上拉输入模式
gpio_mode_set(IR_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, IR_PIN);
/* 使能NVIC中断 中断分组为2位抢占优先级,2位子优先级 */
nvic_irq_enable(EXTI_IRQ,2U,2U); // 抢占优先级2,子优先级2
/* 连接中断线到GPIO */
syscfg_exti_line_config(EXTI_SOURCE_PORT,EXTI_SOURCE_PIN);
/* 初始化中断线下降沿触发 */
exti_init(EXTI_X,EXTI_INTERRUPT,EXTI_TRIG_FALLING);
/* 使能中断 */
exti_interrupt_enable(EXTI_X);
/* 清除中断标志位 */
exti_interrupt_flag_clear(EXTI_X);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
红外信号的数据,全部是以时间长度来确定数据是0还是1,而最小的单位要求有560us,已经达到了us级的测量。我们将滴答定时器的时间设置为1us中断一次。这样我们就有了us级延时。修改完之后,要记得将delay*1us的函数在systick.h中进行定义。
获取高低电平时间
获取低电平时间的实现代码如下:
//获取红外低电平时间
//以微秒us作为时间参考
void get_infrared_low_time( uint32_t *low_time )
{
uint32_t time_val = 0;
//如果引脚为低电平
while( gpio_input_bit_get(IR_PORT, IR_PIN) == 0 )
{
//如果超过 500 * 20 = 10000us = 10ms 还是低电平,则退出
if( time_val>= 500 )
{
*low_time = time_val;
return;
}
delay_1us(20);
time_val++;
}
*low_time = time_val;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当引脚为低电平时,将进入 while 循环,直到不为低电平时就结束循环。在循环之中不断的让时间变量time_val累加, 每加一次需要经过20us。当time_val变量累加时间大于 500 * 20 = 10000us = 10ms时,判断为超时,强行结束该函数,防止阻碍系统运行。
获取高电平时间的代码同理:
//获取红外高电平时间
//以微秒us作为时间参考
void get_infrared_high_time(uint32_t *high_time)
{
uint32_t time_val = 0;
//如果引脚为高电平
while( gpio_input_bit_get(IR_PORT, IR_PIN) == 1 )
{
//如果超过 250 * 20 = 5000us = 5ms 还是高电平,则退出
if( time_val >= 250 )
{
*high_time = time_val;
return;
}
delay_1us(20);
time_val++;
}
*high_time = time_val;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
引导码与重复码判断
引导码是由一个 9ms 的低电平和一个 4.5ms 的高电平组成。每当接收到一个红外信号时,第一个数据就是引导码。我们通过判断红外信号的第一个数据是否是引导码,来决定是否要进行后面的数据接收处理。
重复码是由一个 9ms 的低电平和一个 2.5ms 的高电平组成。当我们的红外遥控一直按住按键时,就会发出重复码,我们可以检测重复码,来确定是否要连续触发重复动作,比如长按开机,长按加速等等。
/******************************************************************
* 函 数 名 称:guide_and_repeat_code_judgment
* 函 数 说 明:引导 和 重复 码 判断
* 函 数 形 参:无
* 函 数 返 回:1:不是引导码 2:重复码 0:引导码
* 作 者:LC
* 备 注:以20微秒us作为时间参考
引导码:由一个 9ms 的低电平和一个 4.5ms 的高电平组成
重复码:由一个 9ms 的低电平和一个 2.5ms 的高电平组成
******************************************************************/
uint8_t guide_and_repeat_code_judgment(void)
{
uint32_t out_time=0;
get_infrared_low_time(&out_time);
//time>10ms time <8ms
if((out_time > 500) || (out_time < 400))
{
return 1;
}
get_infrared_high_time(&out_time);
// x>5ms 或者 x<2ms
if((out_time > 250) || (out_time < 100))
{
return 1;
}
//如果是重复码 2ms < time < 3ms
if((out_time > 100) && (out_time < 150))
{
return 2;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
完整红外数据接收
具体接收流程:【判断是否接收到引导码】->【接收数据】->【判断数据是否正确】。
//接收红外数据
void receiving_infrared_data(void)
{
uint16_t group_num = 0,data_num = 0;
uint32_t time=0;
uint8_t bit_data = 0;
uint8_t ir_value[5] = {0};
uint8_t guide_and_repeat_code = 0;
//等待引导码
guide_and_repeat_code = guide_and_repeat_code_judgment();
//如果不是引导码则结束解析
if( guide_and_repeat_code == 1 ) return;
//共有4组数据
//地址码+地址反码+命令码+命令反码
for(group_num = 0; group_num < 4; group_num++ )
{
//接收一组8位的数据
for( data_num = 0; data_num < 8; data_num++ )
{
//接收低电平
get_infrared_low_time(&time);
//如果不在0.56ms内的低电平,数据错误
if((time > 60) || (time < 20))
{
return ;
}
time = 0;
//接收高电平
get_infrared_high_time(&time);
//如果是在1200us<t<2000us范围内则判断为1
if((time >=60) && (time < 100))
{
bit_data = 1;
}
//如果是在200us<t<1000us范围内则判断为0
else if((time >=10) && (time < 50))
{
bit_data = 0;
}
//groupNum表示第几组数据
ir_value[ group_num ] <<= 1;
//接收的第1个数为高电平;在第二个for循环中,数据会向右移8次
ir_value[ group_num ] |= bit_data;
//用完时间要重新赋值
time=0;
}
}
//判断数据是否正确,正确则保存数据
infrared_data_true_judgment(ir_value);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
判断数据是否正确,可以通过将正常数据取反,与反码比较。如果不一致说明数据不对。
typedef struct INFRARED_DATA{
uint8_t AddressCode; //地址码
uint8_t AddressInverseCode; //地址反码
uint8_t CommandCode; //命令码
uint8_t CommandInverseCode; //命令反码
}_INFRARED_DATA_STRUCT_;
_INFRARED_DATA_STRUCT_ InfraredData;
//红外数据是否正确判断
uint8_t infrared_data_true_judgment(uint8_t *value)
{
//判断地址码是否正确
if( value[0] != (uint8_t)(~value[1]) ) return 0;
//判断命令码是否正确
if( value[2] != (uint8_t)(~value[3]) ) return 1;
//串口输出查看接收到的数据
printf("%x %x %x %x\r\n",value[0],value[1],value[2],value[3]);
//保存正确数据
InfraredData.AddressCode = value[0];
InfraredData.AddressInverseCode = value[1];
InfraredData.CommandCode = value[2];
InfraredData.CommandInverseCode = value[3];
}
//获取红外发送过来的命令
uint8_t get_infrared_command(void)
{
return InfraredData.CommandCode;
}
//清除红外发送过来的数据
void clear_infrared_command(void)
{
InfraredData.CommandCode = 0x00;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
最后,记得在外部中断服务函数中,调用红外接收函数。
void EXTI5_9_IRQHandler(void)
{
if(exti_interrupt_flag_get(EXTI_X) == SET) // 中断标志位为1
{
if(gpio_input_bit_get(IR_PORT,IR_PIN) == RESET) // 如果是低电平
{
//接收一次红外数据
receiving_infrared_data();
}
exti_interrupt_flag_clear(EXTI_X); // 清中断标志位
}
}
2
3
4
5
6
7
8
9
10
11
12
6. 功能验证
在main.c中编写以下代码:
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_usart.h"
#include "bsp_adc.h"
#include "bsp_stepper_motor.h"
#include "bsp_ir_receiver.h"
int main(void)
{
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
//滴答定时器初始化 1us
systick_config();
//串口0初始化 调试
usart_gpio_config(9600U);
//红外接收初始化
infrared_goio_config();
while(1)
{
//如果按下遥控的【1】键
if( get_infrared_command() == 0xA2 )
{
clear_infrared_command();
printf("Press the 1 button \r\n");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
验证视频:
视频
验证代码工程文件:
四、语音识别驱动
1. 语音模块介绍
HLK-V20是海凌科电子针对大量纯离线控制场景和产品推出的高性能纯离线语音识别模块,可广泛且快速的应用于智能家居、各类智能小家电、86盒、玩具、灯具、工业、 医疗、物联网、汽车、安防与照明等需要语音操控的产品。 HLK-V20支持150条本地指令离线识别,可自由定制唤醒词、命令词与应答播报词,具有丰富的外围接口。
离线语音识别指的是只能识别固定命令词条,不需要连接网络。
购买链接:AI智能语音模块V20 海凌科离线语音开关控制 语音识别控制开发板
模块的命令词、播报词等配置,可以通过在线配置平台进行配置,因篇幅过长,另起了一份文档,具体语音产品的配置案例见下方链接。
海凌科语音识别模块的语音固件创建与下载
该案例来自模块移植手册中4.13章节的 HLK-V20语音识别模块。
参考案例的配置流程,本案例的语音模块引脚设置同样选择的是B2P3作为串口1。
唤醒词设置:
命令词与控制:
设置的控制参数如下。当串口接收到 0XAA 0X01 0X55 时,说明语音模块触发了开窗帘命令。其它同理。为了方便接收并解析多条命令,设置成为了帧头帧尾的格式。当串口接收到0X55说明语音识别模块触发了命令,分析上一个接收的数据,则可以知道是识别的哪一个命令。
if( RX_BUFF[DATA_LEN] == 0X55 )//接收到帧尾
{
if( RX_BUFF[DATA_LEN-2] == 0XAA)//接收到帧头,确定数据格式正确
{
V20_data = cRX_BUFF[DATA_LEN-1];//接收数据
}
}
2
3
4
5
6
7
例如:语音识别模块发来了命令:0XAA 0X03 0X55,则说明触发了打开自动模式命令。
帧头 | 数据 | 帧尾 | 说明 |
---|---|---|---|
0XAA | 0X01 | 0X55 | 识别到开窗帘命令 |
0XAA | 0X02 | 0X55 | 识别到关窗帘命令 |
0XAA | 0X03 | 0X55 | 识别到自动模式命令 |
0XAA | 0X04 | 0X55 | 识别到手动模式命令 |
其他设置:
2. 语音识别模块硬件设计
该语音识别模块是可以由我们自由设计命令词的,可以通过在线配置平台进行配置。配置完成之后会生成一个语音识别固件,我们要将固件下载到模块里,通过模块的B6,B7引脚进行下载。所以下方通过排针的方式引出了B6,B7引脚方便我们下载。还需要注意的是,在下载固件时,要先将模块断电。待下载工具识别到模块之后,再给模块通电,才可以正常下载。这个通断电操作,由原理图中的SW1开关进行控制。
语音固件烧入步骤:
视频
3. 语音识别模块软件设计
语音识别模块的软件设计,参考模块移植手册中4.13章节的 4.13 HLK-V20 语音识别模块。
语音识别模块我们配置为了串口通信方式,在原理图中我们接入的是PA2和PA3,它们是串口1的复用引脚。复用通道是AF7。其中PA2是串口1的TX,PA3是串口1的RX。
引脚与串口1相关参数宏定义如下:
#define RCU_HLK_TX RCU_GPIOA // 串口TX的端口时钟
#define RCU_HLK_RX RCU_GPIOA // 串口RX的端口时钟
#define RCU_HLK_USART RCU_USART1 // 串口1的时钟
#define PORT_HLK_TX GPIOA // 串口TX的端口
#define PORT_HLK_RX GPIOA // 串口RX的端口
#define GPIO_HLK_TX GPIO_PIN_2 // 串口TX的引脚
#define GPIO_HLK_RX GPIO_PIN_3 // 串口RX的引脚
#define BSP_HLK_AF GPIO_AF_7 // 串口1的复用功能
#define HLK_USART USART1 // 串口1
#define HLK_USART_IRQ USART1_IRQn // 串口1中断
#define HLK_USART_IRQHandler USART1_IRQHandler // 串口1中断服务函数
#define HLK_RX_LEN_MAX 200 //串口接收最大长度
2
3
4
5
6
7
8
9
10
11
12
13
14
15
串口1的初始化配置代码如下:
/************************************************************
* 函数名称:hlk_usart_init
* 函数说明:语音识别模块的初始化
* 型 参:bund=串口波特率
* 返 回 值:无
* 备 注:波特率根据你设置的内容来决定
*************************************************************/
void hlk_usart_init(unsigned int bund)
{
/* 使能 HLK_USART 的时钟 */
rcu_periph_clock_enable(RCU_HLK_USART);
/* 使能时钟 */
rcu_periph_clock_enable(RCU_HLK_TX);
rcu_periph_clock_enable(RCU_HLK_RX);
/* 配置引脚为复用功能 */
gpio_af_set(PORT_HLK_TX, BSP_HLK_AF, GPIO_HLK_TX);
/* 配置引脚为复用功能 */
gpio_af_set(PORT_HLK_RX, BSP_HLK_AF, GPIO_HLK_RX);
/* 配置TX引脚为复用上拉模式 */
gpio_mode_set(PORT_HLK_TX, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_HLK_TX);
/* 配置RX引脚为复用上拉模式 */
gpio_mode_set(PORT_HLK_RX, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_HLK_RX);
/* 配置PA2引脚为为输出模式 */
gpio_output_options_set(PORT_HLK_TX, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_HLK_TX);
/* 配置PA3引脚为为输出模式 */
gpio_output_options_set(PORT_HLK_RX, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_HLK_RX);
/* 设置HLK_USART的波特率为115200 */
usart_baudrate_set(HLK_USART, bund);
/* 设置HLK_USART的校验位为无 */
usart_parity_config(HLK_USART, USART_PM_NONE);
/* 设置HLK_USART的数据位为8位 */
usart_word_length_set(HLK_USART, USART_WL_8BIT);
/* 设置HLK_USART的停止位为1位 */
usart_stop_bit_set(HLK_USART, USART_STB_1BIT);
/* 使能串口1 */
usart_enable(HLK_USART);
/* 使能HLK_USART传输 */
usart_transmit_config(HLK_USART, USART_TRANSMIT_ENABLE);
/* 使能HLK_USART接收 */
usart_receive_config(HLK_USART, USART_RECEIVE_ENABLE);
/* 使能HLK_USART接收中断标志位 */
usart_interrupt_enable(HLK_USART, USART_INT_RBNE);
/* 使能HLK_USART空闲中断标志位 */
usart_interrupt_enable(HLK_USART, USART_INT_IDLE);
/* 配置中断优先级 */
nvic_irq_enable(HLK_USART_IRQ, 2, 2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
串口中断服务函数:
当接收到我们自己设置的格式, 0XAA 数据 0X55 时,rx_flag就设置为1。我们通过判断rx_flag是否为1,确定是否有接收到语音识别模块发送过来的数据。当然,大家也可以自己设计一个格式,或者直接不要格式,直接发送数据0x01,0x02,0x03等等。
/******************************************************************
* 函 数 名 称:HLK_USART_IRQHandler
* 函 数 说 明:连接HLK的串口中断服务函数
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void HLK_USART_IRQHandler(void)
{
if(usart_interrupt_flag_get(HLK_USART,USART_INT_FLAG_RBNE) != RESET) // 接收缓冲区不为空
{
//接收数据
hlk_rx_buff[ hlk_rx_len ] = usart_data_receive(HLK_USART);
#if DEBUG
//测试,查看接收到了什么数据
printf("%c", hlk_rx_buff[ hlk_rx_len ]);
#endif
//0XAA X 0X55
if( hlk_rx_buff[hlk_rx_len] == 0X55 )//接收到帧尾
{
if( hlk_rx_buff[hlk_rx_len-2] == 0XAA)//接收到帧头,确定数据格式正确
{
rx_data = hlk_rx_buff[hlk_rx_len-1];//接收数据
rx_flag = 1;
}
}
//接收长度限制
hlk_rx_len = ( hlk_rx_len + 1 ) % HLK_RX_LEN_MAX;
}
if(usart_interrupt_flag_get(HLK_USART,USART_INT_FLAG_IDLE) == SET) // 检测到空闲中断
{
usart_data_receive(HLK_USART); // 必须要读,读出来的值不能要
hlk_rx_buff[hlk_rx_len] = '\0'; //字符串结尾补 '\0'
hlk_rx_flag = 1; // 接收完成
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
分析语音识别模块发送过来的指令,并完成对应操作:
/**********************************************************
* 函 数 名 称:voice_anakysis_data
* 函 数 功 能:解析语音识别模块发送过来的数据
* 传 入 参 数:无
* 函 数 返 回:1=接收到语音数据 0=没有接收到语音数据
* 作 者:LC
* 备 注:假设 AA 03 55 则 printf("automatic mode\r\n");
**********************************************************/
unsigned char voice_anakysis_data(void)
{
unsigned char ret = 0;
if( rx_flag == 1 )//接收到语音命令
{
rx_flag = 0;
switch( rx_data )//根据语音命令确定对应的动作
{
case 0x01://开窗帘
printf("open curtain\r\n");
break;
case 0x02://关窗帘
printf("close curtain\r\n");
break;
case 0x03://自动模式
printf("automatic mode\r\n");
break;
case 0x04://手动模式
printf("manual mode\r\n");
break;
}
ret = 1;
//清除当前数据
clear_hlk_rx_buff();
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
4. 功能验证
在main.c中编写以下代码:
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_usart.h"
#include "bsp_adc.h"
#include "bsp_stepper_motor.h"
#include "bsp_ir_receiver.h"
#include "bsp_ir_receiver.h"
#include "bsp_voice.h"
int main(void)
{
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
//滴答定时器初始化 1us
systick_config();
//串口0初始化 调试
usart_gpio_config(9600U);
//语音识别模块引脚初始化
hlk_usart_init(9600U);
while(1)
{
//语音识别操作
voice_anakysis_data();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
验证视频:
验证代码工程文件:
五、完整案例设计
按照设计要求,完成完整的窗帘控制案例。
(1)可以通过红外遥控与语音设置是否打开自动模式;
(2)在自动模式下,当光照强度很高时,自动展开窗帘;当光照强度很低时,自动关闭窗帘;
(3)在自动模式下,当检测到大量雨滴时,自动展开窗帘,此优先级比光照检测高;
(4)在任何时候,可以通过红外遥控或语音命令展开与关闭窗帘,并关闭自动模式;
(5)主控芯片接收到数据后,驱动电机运转,现实智能窗帘窗户系统。
1.窗帘控制部分
上电第一步,先让步进电机复位到最开始的位置,这样方便我们后续的定位。在调试时,已经测量出步进电机从左尽头到右尽头一共执行了600步,以下窗帘复位代码中,最大步数 MAX_STEPS = 600 步。
//窗帘复位
void curtain_reset(void)
{
//如果步数没有到达最大步数
while( get_step_count() < MAX_STEPS )
{
//步进电机顺时针旋转,步数累加
motor_cw_flag = 1;
motor_ccw_flag = 0;
}
//停止步进电机动作
motor_cw_flag = 0;
motor_stop();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
展开窗帘与关闭窗帘
通过步数的判断,确定我们步进电机是否到达了另一端。因为在我们编写步进电机的驱动时,设置了顺时针步数累加,逆时针步数累减。当步数累加超过最大步数时,我们就知道已经到达了顺时针旋转的尽头。当步数累减为0或者比0还小时,我们就知道已经到达了逆时针旋转的尽头。
为什么采用的是if判断?是为了不阻碍到其他任务的执行。例如你在步进电机动作的过程中,红外信号发送过来停止电机的信号,但是因为我们还在执行步进电机的运动过程,导致我们不能正常接收并处理红外信号。
//展开窗帘
void open_curtain(void)
{
//如果步数没有到达0步
if( get_step_count() > 0 )
{
//步进电机逆时针旋转,步数累减
motor_ccw_flag = 1;
motor_cw_flag = 0;
}
else
{
//停止步进电机动作
motor_ccw_flag = 0;
motor_cw_flag = 0;
motor_stop();
}
}
//关闭窗帘
void close_curtain(void)
{
//如果步数没有到达最大步数
if( get_step_count() < MAX_STEPS )
{
//步进电机顺时针旋转,步数累加
motor_cw_flag = 1;
motor_ccw_flag = 0;
}
else
{
//停止步进电机动作
motor_cw_flag = 0;
motor_ccw_flag = 0;
motor_stop();
}
}
//限位判断
//num=当前步数
void limit_judgment(int num)
{
//如果当前是顺时针旋转
if( motor_cw_flag == 1 )
{
//如果顺时针旋转的步数已经累加超过最大步数
if( num >= MAX_STEPS )
{
//停止旋转
motor_cw_flag = 0;
motor_stop();
}
}
//如果当前是逆时针旋转
if( motor_ccw_flag == 1 )
{
//如果逆时针旋转的步数已经累减到0步
if( num <= 0 )
{
//停止旋转
motor_ccw_flag = 0;
motor_stop();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2.自动模式部分
在自动模式下,我们需要检测光照强度值与雨滴值,当光照强度很高时,自动展开窗帘;当光照强度很低时,自动关闭窗帘。当检测到大量雨滴时,优先自动展开窗帘;
//自动模式
void automatic_mode(void)
{
uint32_t light_value = 0;
uint32_t raindrop_value = 0;
//读取雨滴值
raindrop_value = get_adc_value( BSP_RAINDROP_ADC_CHANNEL );
//读取光照值
light_value = get_adc_value( BSP_LIGHT_ADC_CHANNEL );
//如果检测到大量雨滴
if( raindrop_value <= RAINDROP_THRESHOLD )
{
//展开窗帘
open_curtain();
//结束函数运行,使该任务优先级高于后面的任务
return;
}
//如果检测到光照很亮
if( light_value < ILLUMINATION_THRESHOLD )
{
//展开窗帘
open_curtain();
//结束函数运行,使该任务优先级高于后面的任务
return;
}
//如果检测到光照很暗
if( light_value >= ILLUMINATION_THRESHOLD )
{
//关闭窗帘
close_curtain();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
在设计要求中,我们要判断雨滴情况,红外接收情况,语音识别情况。在执行任务的同时,又不可以阻塞其他任务的运行,所以我们要考虑好各个模块间的调度问题。
3.红外接收部分
通过按键【*】【#】【>】【<】来进行控制。其中【*】为自动模式键,【#】为非自动模式键,【>】为展开窗帘键,【<】为关闭窗帘键。 这里需要注意的是在模式切换的过程中,需要将正在动作的电机停止,否则就停不下来了。
/******************************************************************
* 函 数 名 称:set_mode_switch_flag
* 函 数 说 明:设置模式
* 函 数 形 参:flag = 1:自动模式 flag = 0:非自动模式
* 函 数 返 回:无
* 作 者:LCKFB
* 备 注:(自动模式)AUTO_MODE == 1
(非自动模式) NO_AUTO_MODE == 0
******************************************************************/
void set_mode_switch_flag(uint8_t flag)
{
mode_switch_flag = flag;
//防止电机正在动作时,切换模式,导致电机死机
motor_stop();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//红外命令判断
void infrared_command_judgment(void)
{
//播报【当前为自动模式】语音
unsigned char auto_mode_report[5] = {0XAA, 0X55, 0X01, 0X55, 0XAA};
//播报【当前为手动模式】语音
unsigned char manual_mode_report[5] = {0XAA, 0X55, 0X02, 0X55, 0XAA};
//播报【窗帘已打开】语音
unsigned char curtain_open_report[5] = {0XAA, 0X55, 0X03, 0X55, 0XAA};
//播报【窗帘已关闭】语音
unsigned char curtain_clear_report[5] = {0XAA, 0X55, 0X04, 0X55, 0XAA};
//如果按下【*】键,进入自动模式
if( get_infrared_command() == 0X68 )
{
clear_infrared_command();
set_mode_switch_flag(AUTO_MODE);
//播报【当前为自动模式】语音
hlk_usart_send_string(auto_mode_report, 5);
}
//如果按下【#】键,进入非自动模式
if( get_infrared_command() == 0Xb0 )
{
clear_infrared_command();
set_mode_switch_flag(NO_AUTO_MODE);
//播报【当前为手动模式】语音
hlk_usart_send_string(manual_mode_report, 5);
}
//如果按下【<】键,关闭窗帘
if( get_infrared_command() == 0X10 )
{
clear_infrared_command();
close_curtain();
set_mode_switch_flag(NO_AUTO_MODE);
//播报【窗帘已关闭】语音语音
hlk_usart_send_string(curtain_clear_report, 5);
}
//如果按下【>】键,展开窗帘
if( get_infrared_command() == 0X5a )
{
clear_infrared_command();
open_curtain();
set_mode_switch_flag(NO_AUTO_MODE);
//播报【窗帘已打开】语音语音
hlk_usart_send_string(curtain_open_report, 5);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
4.语音识别部分
/**********************************************************
* 函 数 名 称:voice_anakysis_data
* 函 数 功 能:解析语音识别模块发送过来的数据
* 传 入 参 数:无
* 函 数 返 回:1=接收到语音数据 0=没有接收到语音数据
* 作 者:LC
* 备 注:假设 AA 03 55 则 设置为自动模式
**********************************************************/
unsigned char voice_anakysis_data(void)
{
unsigned char ret = 0;
if( rx_flag == 1 )//接收到语音命令
{
rx_flag = 0;
switch( rx_data )//根据语音命令确定对应的动作
{
case 0x01://开窗帘命令
open_curtain();
//设置非自动模式
set_mode_switch_flag(NO_AUTO_MODE);
break;
case 0x02://关窗帘命令
close_curtain();
//设置非自动模式
set_mode_switch_flag(NO_AUTO_MODE);
break;
case 0x03://自动模式命令
set_mode_switch_flag(AUTO_MODE);
break;
case 0x04://手动模式命令
set_mode_switch_flag(NO_AUTO_MODE);
break;
}
ret = 1;
//清除当前数据
clear_hlk_rx_buff();
}
//步进电机限位判断
limit_judgment(get_step_count());
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
5.完整案例验证
在main.c中编写以下代码:
/********************************************************************************
* 测试硬件:立创·梁山派开发板GD32F470ZGT6 使用主频200Mhz 晶振25Mhz
* 版 本 号: V1.0
* 修改作者: LCKFB
* 修改日期: 2023年08月04日
* 功能介绍:
******************************************************************************
* 梁山派软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:club.szlcsc.com
* 其余模块移植手册:https://dri8c0qdfb.feishu.cn/docx/EGRVdxunnohkrNxItYTcrwAnnHe
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
*********************************************************************************/
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_usart.h"
#include "bsp_adc.h"
#include "bsp_stepper_motor.h"
#include "bsp_ir_receiver.h"
#include "bsp_ir_receiver.h"
#include "bsp_voice.h"
#include "bsp_mode_control.h"
int main(void)
{
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
//滴答定时器初始化 1us
systick_config();
//串口0初始化 调试
usart_gpio_config(9600U);
//语音识别模块引脚初始化
hlk_usart_init(9600U);
//雨滴与光照识别引脚初始化
raindrop_and_light_config();
//红外接收引脚初始化
infrared_goio_config();
//步进电机初始化
stepper_motor_config();
stepper_motor_timer_config();
//步进电机复位
curtain_reset();
while(1)
{
//语音命令识别操作:窗帘控制与模式切换
voice_anakysis_data();
//红外命令识别操作:窗帘控制与模式切换
infrared_command_judgment();
//模式选择与对应模式的操作
mode_select();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
视频验证:
视频
验证代码工程文件: