主控:stm32f103芯片;
通信板:涂鸦VWXR2 Wi-Fi通信板;
物联网云平台:接入涂鸦IoT云平台;
喂食器执行电机:42微型步进电机;
电机驱动:RQSM240驱动器;
余粮和出粮检测模块:电阻应变片式压力传感器+HX711AD模块;
小夜灯:普通LED灯;
手机控制端:涂鸦智能APP;
外壳:自主DIY.
硬件框架
操作系统:rt-thread-3.1.3-nano;
云端对接方式:涂鸦标准模组MCU SDK;
通信模组固件:涂鸦通用Wi-Fi通信模块固件;
MCU与模组通信方式:USART;
语音平台:涂鸦小智管家;\
线程:
time_sys_thread: 时间系统线程,为计划喂食功能提供当前时间,通过涂鸦IoT平台校准时间;
wifi_usart_service_thread: 串口通信服务线程,用于处理MCU与同学模组之间的通信数据;
granary_weight_thread: 余粮检测线程,检测当前粮桶余粮情况,每变化50g向云端上报一次;
export_weight_thread: 出粮检测线程,检测当前已出粮的剩余重量,每变化50g向云端上报一次;
quick_feed_thread: 快速喂食线程,提供快速喂食服务,支持本地按键控制和手机APP控制,按一次或点击一次喂食一份;
key_scan_thread: 按键检测线程,检测快速喂食控制按钮是否被按下;
信号量:
Quick_feed_sem: 记录按键按下次数,或APP点击次数,为快速喂食提供信号,实现连续多份快速喂食;
软件框架
产品型号:TYDE5-VWXR2-MCU-1
产品型号:TYDE5-H-BRIDGE-1
产品型号:TYDE5-POWER-DC_DC-1
参考涂鸦官方教程产品
参考涂鸦官方教程
时间系统主要服务于计划喂食功能,而喂食计划由星期、时、分、喂食量构成。因此时间系统不需要过于细致的功能,能提供所需的数据即可,必要时可通过涂鸦云平台进行更新校准。通过rt-thread提供的ms级延时函数rt_thread_mdeay()实现时间秒针,进而实现时间系统功能。每分钟检查一次喂食计划,每小时校准一下时间。具体实现方式如下:
/**
* @brief 时间系统
* @param Null
* @return Null
* @note Null
*/
void time_sys(void* parameter)
{
time_now.year = 2020;
time_now.month = 1;
time_now.day = 1;
time_now.hour = 0;
time_now.min = 0;
time_now.sec = 0;
time_now.week = 1;
time_now.updata_state = ERROR;
while(1)
{
rt_thread_mdelay(1000);
time_now.sec ++;
if(time_now.updata_state != SUCCESS)
{
mcu_get_system_time();//更新时间日期
get_nearly_meal_plan();
}
if(time_now.sec >= 60)
{
time_now.sec = 0;
time_now.min ++;
meal_plan_check();//喂食计划检测
if(time_now.min >= 60)
{
time_now.min = 0;
time_now.hour ++;
time_now.updata_state = ERROR;//日期的更新靠联网实现
if(time_now.hour >= 24)
{
time_now.hour = 0;
time_now.week++;
if(time_now.week >= 8)
{
time_now.week = 1;
}
time_now.updata_state = ERROR;//日期的更新靠联网实现
}
}
}
快速喂食功能提供两种控制方式,手动按键与APP控制。APP通过云平台下发数据后进入功能点下发处理函数:
函数名称 : dp_download_quick_feed_handle
功能描述 : 针对DPID_QUICK_FEED的处理函数
输入参数 : value:数据源数据
: length:数据长度
返回参数 : 成功返回:SUCCESS/失败返回:ERROR
使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至app
*****************************************************************************/
static unsigned char dp_download_quick_feed_handle(const unsigned char value[], unsigned short length)
{
//示例:当前DP类型为BOOL
unsigned char ret;
//0:关/1:开
unsigned char quick_feed;
quick_feed = mcu_get_dp_download_bool(value,length);
if(quick_feed == 0)
{
//开关关
}else {
//开关开
feed(1);
}
//处理完DP数据后应有反馈
ret = mcu_dp_bool_update(DPID_QUICK_FEED,quick_feed);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
本地按键按下后被按键检测线程捕获进而向快速喂食线程释放信号量:
/**
* @brief 按键1扫描
* @param Null
* @return Null
* @note Null
*/
void key1_scan(void* parameter)
{
while(1)
{
rt_thread_delay(50);
if( GPIO_ReadInputDataBit(KEY1_INT_GPIO_PORT, KEY1_INT_GPIO_PIN) == KEY_ON )
{
// 松手检测
while( GPIO_ReadInputDataBit(KEY1_INT_GPIO_PORT, KEY1_INT_GPIO_PIN) == KEY_ON );
rt_sem_release(quick_feed_sem);
}
}
}
功能点下发处理函数和快速喂食线程最终都调用喂食执行函数void feed(uint8_t n)通过步进电机驱动实现喂食。
/**
* @brief 反转n圈
* @param n:转动圈数
* @retval 无
*/
void step_motor_reverse(uint8_t n)
{
step_motor_enable();
set_dir_reverse();
for(uint8_t i=0; i<n; i++)
{
step_motor_rotate_1();
}
}
步进电机执行喂食需要一定的时间,如果多次按下按键可能会被遗漏执行,这里采用信号量决绝该问题。 步进电机初始化如下所示:
/**
* @brief 初始化控制步进电机的IO
* @param 无
* @retval 无
*/
void STEP_MOTOR_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启步进电机相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd( STEP_MOTOR_DIR_GPIO_CLK | STEP_MOTOR_PLUSE_GPIO_CLK | STEP_MOTOR_OFFLINE_GPIO_CLK, ENABLE);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = STEP_MOTOR_DIR_GPIO_PIN;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(STEP_MOTOR_DIR_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = STEP_MOTOR_PLUSE_GPIO_PIN;
/*调用库函数,初始化GPIO*/
GPIO_Init(STEP_MOTOR_PLUSE_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = STEP_MOTOR_OFFLINE_GPIO_PIN;
/*调用库函数,初始化GPIOF*/
GPIO_Init(STEP_MOTOR_OFFLINE_GPIO_PORT, &GPIO_InitStructure);
/* 使能步进电机 */
GPIO_SetBits(STEP_MOTOR_OFFLINE_GPIO_PORT, STEP_MOTOR_OFFLINE_GPIO_PIN);
/* 设置转动方向 */
GPIO_SetBits(STEP_MOTOR_DIR_GPIO_PORT, STEP_MOTOR_DIR_GPIO_PIN);
}
HX711AD模块初始化:
* @brief 初始化控制hx711的IO
* @param 无
* @retval 无
*/
void HX711_GRANARY_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启hx711相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd( HX711_GRANARY_DOUT_GPIO_CLK | HX711_GRANARY_SCK_GPIO_CLK, ENABLE);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = HX711_GRANARY_DOUT_GPIO_PIN;
/*设置引脚模式为浮空输入*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(HX711_GRANARY_DOUT_GPIO_PORT, &GPIO_InitStructure);
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = HX711_GRANARY_SCK_GPIO_PIN;
/*调用库函数,初始化GPIO*/
GPIO_Init(HX711_GRANARY_SCK_GPIO_PORT, &GPIO_InitStructure);
}
读取压力传感器值:
/**
* @brief 读取HX711
* @param Null
* @return 压力传感器值
* @note Null
*/
uint16_t hx711_granary_read(void) //增益128
{
uint32_t count;
uint16_t granary_weight;
uint8_t i;
set_hx711_dout();
rt_thread_delay(5);
reset_hx711_sck();
count=0;
while(read_dout());
for(i=0;i<24;i++)
{
set_hx711_sck();
count=count<<1;
reset_hx711_sck();
if(read_dout())
count++;
}
set_hx711_sck();
count=count^0x800000;//第25个脉冲下降沿来时,转换数据
rt_thread_delay(5);
reset_hx711_sck();
granary_weight = (unsigned long)((float)count/gapvalue_granary);
return(granary_weight);
}
在线程中实现重量检测,余粮剩余情况采用百分制表示,定义3Kg,full_granary=3000为满箱状态。余粮每变化1% 向云端上报一次。
/**
* @brief 粮桶余粮重量检测
* @param Null
* @return Null
* @note Null
*/
void granary_weight(void* parameter)
{
uint32_t tem, granary_weight, diff;
uint8_t granary_weight_percent;
while(1)
{
rt_thread_delay(50);
tem = hx711_granary_read() - granary_peel;
if(tem > granary_weight)
{
diff = tem - granary_weight;
}
else
{
diff = granary_weight - tem;
}
if(diff > full_granary/100)
{
granary_weight = tem;
if( granary_weight >= full_granary )
{
granary_weight_percent = 100;
mcu_dp_value_update(DPID_SURPLUS_GRAIN, granary_weight_percent);
}
else
{
granary_weight_percent = (granary_weight) / (full_granary/100);
mcu_dp_value_update(DPID_SURPLUS_GRAIN, granary_weight_percent);
}
}
}
}
HX711初始化和压力传感器值读取方法与余粮检测功能中相似,在此不再赘述。已出粮剩余情况采用重量表示,没变化50g向云端上报一次。
* @brief 已出粮剩余重量检测
* @param Null
* @return Null
* @note Null
*/
void export_weight(void* parameter)
{
uint32_t tem, export_weight, diff;
while(1)
{
rt_thread_delay(50);
tem = hx711_export_read() - export_peel;
if(tem > export_weight)
{
diff = tem - export_weight;
}
else
{
diff = export_weight - tem;
}
if(diff > 50)
{
export_weight = tem;
mcu_dp_value_update(DPID_WEIGHT, export_weight);
}
}
}
喂食计划下发后进入功能点下发处理函数,在此处根据通讯协议对喂食计划进行解析提取并存入meal_plan结构体中
/*****************************************************************************
函数名称 : dp_download_meal_plan_handle
功能描述 : 针对DPID_MEAL_PLAN的处理函数
输入参数 : value:数据源数据
: length:数据长度
返回参数 : 成功返回:SUCCESS/失败返回:ERROR
使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至app
*****************************************************************************/
static unsigned char dp_download_meal_plan_handle(const unsigned char value[], unsigned short length)
{
uint8_t i, k=0;
//示例:当前DP类型为RAW
unsigned char ret;
//RAW类型数据处理
meal_plan_amount = 0;
for(i=0; i<length/5; i++)
{
if((uint16_t)value[5*(i+1)-1] != 0)
{
meal_plan_amount++;
meal_plan[k].week = value[5*i+0];
meal_plan[k].hour = value[5*i+1];
meal_plan[k].min = value[5*i+2];
meal_plan[k].amount = value[5*i+3];
k++;
}
}
// for(i=0; i<meal_plan_amount; i++)
// {
// rt_kprintf("meal plan %d: %d %d %d %d\n", i, meal_plan[i].week, meal_plan[i].hour, meal_plan[i].min,meal_plan[i].amount);
// }
// rt_kprintf("\n");
get_nearly_meal_plan();
//处理完DP数据后应有反馈
ret = mcu_dp_raw_update(DPID_MEAL_PLAN,value,length);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
喂食计划需要每分钟都与当前时间进行一次比较以确保喂食计划的执行不被遗漏。然后喂食计划最多可设置10 个,若每次都将10个喂食计划一一与当前时间进行比较将占用较多系统资源。因此在获取全部喂食计划后对最近的一次喂食计划进行提取,这样每次只需将当前时间与进行喂食计划进行对比,大大节约系统资源。最近喂食计划获取方法如下所示:
/**
* @brief 获取最近的喂食计划
* @param Null
* @return Null
* @note Null
*/
void get_nearly_meal_plan(void)
{
uint16_t distance, nearly_distance = 24*60;
//将当前时间的星期数转换为one-hot型表示方法
uint8_t week_day_form = 0x01;
week_day_form = week_day_form << (7 - time_now.week);
nearly_meal_plan.week = 0x80;//确保日期不是当天的nearly_meal_plan不生效
for(uint8_t i=0; i<meal_plan_amount; i++)
{
if( ((week_day_form & meal_plan[i].week) != 0) && (meal_plan[i].hour >= time_now.hour) && (meal_plan[i].min > time_now.min))
{
distance = (meal_plan[i].hour - time_now.hour)*60 + (meal_plan[i].min - time_now.min);
if(distance < nearly_distance)
{
nearly_distance = distance;
nearly_meal_plan = meal_plan[i];
}
}
}
}
分别计算喂食计划于当前时间相隔的分钟数,最小的即为最近喂食计划。这里需要注意,当前时间的星期数采用十进制表示而喂食计划的星期数采用one-hot编码,因此在比较时需先进行转化。同时,为避免混淆将星期数最高为初始化为1,表该计划不生效。因为one-hot编码仅适用低7位,十进制表示方法显然也用不到最高位。因此将最高位用于表示喂食计划是否生效不会造成干涉。获取最近喂食后只需在每分钟检查一次是否到达喂食时间即可,当到达喂食计划的时间则调用喂食执行函数进行喂食,实现方法如下:
/**
* @brief 喂食计划检查
* @param Null
* @return Null
* @note Null
*/
static void meal_plan_check(void)
{
if(((nearly_meal_plan.week & 0x80) == 0) && time_now.min == nearly_meal_plan.min && time_now.hour == nearly_meal_plan.hour)
{
feed(nearly_meal_plan.amount);
get_nearly_meal_plan();
}
}
/*****************************************************************************
函数名称 : dp_download_manual_feed_handle
功能描述 : 针对DPID_MANUAL_FEED的处理函数
输入参数 : value:数据源数据
: length:数据长度
返回参数 : 成功返回:SUCCESS/失败返回:ERROR
使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至app
*****************************************************************************/
static unsigned char dp_download_manual_feed_handle(const unsigned char value[], unsigned short length)
{
//示例:当前DP类型为VALUE
unsigned char ret;
unsigned long manual_feed;
manual_feed = mcu_get_dp_download_value(value,length);
//VALUE类型数据处理
feed(manual_feed);
//处理完DP数据后应有反馈
ret = mcu_dp_value_update(DPID_MANUAL_FEED,manual_feed);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
对小夜灯的控制相当于普通GPIO的控制,引脚初始化这里不再赘述。具体控制在功能点下发处理函数 unsigned char dp_download_light_handle(const unsigned char value[], unsigned short length) 实现,其中 LIGHT_ON 和 LIGHT_OFF 为宏定义,实现引脚的高低电平控制。
/*****************************************************************************
函数名称 : dp_download_light_handle
功能描述 : 针对DPID_LIGHT的处理函数
输入参数 : value:数据源数据
: length:数据长度
返回参数 : 成功返回:SUCCESS/失败返回:ERROR
使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至app
*****************************************************************************/
static unsigned char dp_download_light_handle(const unsigned char value[], unsigned short length)
{
//示例:当前DP类型为BOOL
unsigned char ret;
//0:关/1:开
unsigned char light;
light = mcu_get_dp_download_bool(value,length);
if(light == 0)
{
LIGHT_OFF;
}else
{
LIGHT_ON;
}
//处理完DP数据后应有反馈
ret = mcu_dp_bool_update(DPID_LIGHT,light);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
涂鸦 VWXR2 Wi-Fi 模组自带两个mic和一个扬声器,只需在涂鸦云平台完成语音配置,使语音与对应的功能点对应起来即可实现喂食器部分功能的语音控制。配置方法如下图所示: 这里配置了三项语音功能,包括小夜灯的开关控制、快速喂食和粮桶余粮查询。 语音模块收到小夜灯的开关控制和快速喂食指令后会按照设置向MCU下发对应的指令,与APP控制相同,语音控制最终也是进入到功能点下发处理函数执行对应的操作。具体实现方式前面已经介绍过。 粮桶余粮为查询类指令,模块收到指令后直接读取粮桶余粮功能点的数据。
![20210329163012829.png] (https://images.tuyacn.com/developer/community/16170955326e394baa81f.png)