项目链接
https://github.com/chengjing11/Dance-Robot.git
项目说明
基于STC12C5A60S2 + CDS5516 + nRF24L01 + LCD1602 + openmv等设计的舞蹈机器人。
-
整个项目包含一个主控台,三个舞蹈机器人;其中,一个机器人是主机器人,其余两个机器人为从机器人。
-
三个机器人开机之后,为了实现舞蹈同步与机器人状态实时查看,在主控台使用按键开启/结束任务,并通过无线通信发送指令给主机器人;LCD2406液晶显示屏提示字幕可实时查看交互过程。
-
文件中有小青、小粉、小蓝,小青为主机器人。
-
比赛开始控制台通过无线发送指令,可保证三个机器人同时跳舞。跳舞结束后,主机器人等待进行人机交互,主要包含语音交互和视觉交互。
-
歌曲使用《强军战歌》
各模块通信方式
串口1:相当于“print”的功能,用来打印字符、传递消息。
串口2:语音交互
串口3数字舵机控制
串口4视觉交互(openmv)
SPI通信:NRF24L01无线通信使用SPI通信方式
核心代码
主程序–控制台
# include "narfl24l01.h"
# include "LCD2406.h"
sbit key = P0^4;
unsigned char a;
void three(unsigned int date)
{
TF_senddat(date);
TF_senddat(date);
TF_senddat(date);
}
void main ()
{
wagh();
Uart1234_Init();
Init_NRF24L01_MA();
Set_TxMode_MA();
delay_ms(200);
CLR_Buf1();
while (1)
{
if(key==0)
{
delay_ms(200);
if(key==0)
{
a++;
}
}
switch(a)
{
case 0 : LCD2406("Power up! Waitting cmd! ");break;//等待
case 1 : three(0x02);a++;LCD2406("Start Dancing........ ");delay_ms(1000);break;//启动舞蹈
case 2 : LCD2406("Dancing........ ");break;//舞蹈
case 3 : three(0x01);a++;LCD2406("Emergency stop button ... ");delay_ms(1000);break;//紧急停止
case 4 : LCD2406("Stop dancing - Waitting cmd..... ");break;//等待
case 5 : a=0;break;
}
}
}
主程序–主机器人
# include "uart.h"
# include "NARFL2401.h"
#include "asr.h"
# include "open.H"
#include "duoji.h"
extern unsigned char cmd;
void main ()
{
wagh(); //IO初始化
Uart1234_Init(); //四串口初始化
Init_NRF24L01_MA(); //初始化
Set_RxMode_MA(); //配置nRF24L01为接收模式
zhongduanyi(); //中断初始化
P2=0X00;
start();
while(1)
{
switch (cmd)
{
case 0 :SendStringByUart1("0x00 ");break; //等待启动
case 1 :SendStringByUart1("0x01 ");break; //紧急停止
case 2 :SendStringByUart1("0x02 ");dancing(); //舞蹈启动
case 3 :SendStringByUart1("0x03 "); //人机交互
human_computer();//语音交互
case 4 :SendStringByUart1("0x04 ");
human_computer2();//视觉交互
break;
default :cmd=0;break;
}
delay_ms(200);
}
}
主程序–2个从机器人
# include "uart.h"
# include "NARFL2401.h"
#include "asr.h"
# include "open.H"
#include "duoji.h"
extern unsigned char cmd;
void main ()
{
wagh(); //IO初始化
Uart1234_Init(); //四串口初始化
Init_NRF24L01_MA(); //初始化
Set_RxMode_MA(); //配置nRF24L01为接收模式
zhongduanyi(); //中断初始化
P2=0X00;
start();
while(1)
{
switch (cmd)
{
case 0 :SendStringByUart1("0x00 ");break; //等待启动
case 1 :SendStringByUart1("0x01 ");break; //紧急停止
case 2 :SendStringByUart1("0x02 "); dancing();
break;
default :cmd=0;break;
}
delay_ms(200);
}
}
从机器人没有设置交互功能,其他部分都和主机器人一样。
主程序中的分块程序
外部中断
void zhongduanyi()
{
ES=1;
IE2 |= 0x01;
IE2 |= 0x08;
IE2 |= 0x10;
IE1 = 0; //外部中断1标志位
EX1 = 1; //INT1 Enable
IT1 = 1; //INT1 下降沿中断
EA = 1; //允许总中断
}
开启外部中断1接收无线信号。
舵机函数
使用数字舵机CDS5516。通过通信协议设置每个舵机的ID号,设置速度(速度过慢过快会使得舞蹈过程不够优美等;并且根据大赛要求需要控制舞蹈时长)、最大旋转角度(比如仿真人体结构,头部关节不可超过180度)
void dancing()
{
while(cmd!=0x01)
{
static unsigned char y=0;
switch(y)
{
case 0:part_one();break; //part_部分就是我们设置的舞蹈动作
case 1:part_two();break;
case 2:part_three();
case 3:cmd=0x01;break;
}
y++;
}
}
//舵机驱动程序
void a(unsigned char ID,unsigned short int speed,unsigned short int position)
{
unsigned char temp_sum=0;
if (position>=0x3ff)
position=0x3ff;
if (speed>=0x3ff)
speed=0x3ff;
temp_sum=ID + 0x07 + 0x04 + 0x1E + position%256+position/256+speed%256+speed/256;
temp_sum=~temp_sum;
IE2 &= 0xF7; // 串口3中断关闭
OE2=0;
OE1=1;
SendDataByUart3(0XFF);
SendDataByUart3(0XFF);
SendDataByUart3(ID);
SendDataByUart3(0X07);
SendDataByUart3(0X04);
SendDataByUart3(0X1E);
SendDataByUart3(position%256);
SendDataByUart3(position/256);
SendDataByUart3(speed%256);
SendDataByUart3(speed/256);
SendDataByUart3(temp_sum);
IE2 |= 0x08; // 串口3中断打开
OE2=1;
OE1=0;
delay_ms(100);
}
语音识别函数
void Voice_interaction()
{
ASR_init(); //语音识别模块初始化
ASR_init1(); //灵敏度初始化
ASR_init2(); //音量输入初始化
start_repeatedly_Speech_Recognition(); //启动连续识别
while(cmd!=0x01)
{
if(Rec_Buf2[0]==0x5B)
{
delay_ms(500); //等待接收三位数数据
EA=0; //关闭总中断
SendDataByUart1(Rec_Buf2[0]) ;
SendDataByUart1(Rec_Buf2[1]) ;
switch (Rec_Buf2[1])
{
case 1:P2=~0X11;
Delay2500ms();
P2=0X00 ;
break; //可以
case 2:P2=~0X12;
Delay5000ms();
P2=0X00;
break; //我是17年入伍,目前为止,已经有两年军龄
case 3:P2=~0X13;
Delay10000ms();
P2=0X00;
break; //当兵首先对自己是一种锻炼,使我更加的自律,但更深层次是对国旗,对国家,对人民的信仰。
case 4:P2=~0X14;
Delay6000ms();
P2=0X00;
break; //我的老班长,当兵过程中,老班长就像一面旗帜,引领我们前进。
case 5:P2=~0X15;
Delay3500ms();
P2=0X00;
cmd=0x05; //准备退出语音模块
break; //好好学习,为国争光。
}
delay_ms(200);
CLR_Buf2();
EA=1; //打开总中断
}
}
ASR_end(); //退出语音识别
}
//启动连续语音识别函数start_repeatedly_Speech_Recognition()
void start_repeatedly_Speech_Recognition(void)
{
IE2 &= 0xFE; // 串口2中断关闭
SendDataByUart2(0xAB);
SendDataByUart2(0xAB);
SendDataByUart2(0x00);
IE2 |= 0x01; // 串口2中断打开
delay_ms(1000);
}
无线发送/接收数据
//无线发送数据
Set_TxMode_MA();
void TF_senddat(unsigned char i)
{
TxPayload[0] =i;
if(NRF24L01_TxPacket_MA(TxPayload) == TX_OK) //如果发送成功
{
led=~led; //无线发射成功
delay_ms(200);
}
}
//无线接收数据
Set_RxMode_MA();
void RF_ReceDat()
{
if(NRF24L01_RxPacket_MA(RxPayload) == RX_OK) //如果接收到数据
{
cmd=RxPayload[0];
RxPayload[0]=0;
delay_ms(200);
}
}
设置IO口
void wagh()
{
// P0 端口模式寄存器 1,设置为 0 表示准双向口
// P0 端口模式寄存器 0,设置为 0 表示准双向口
P0M1 = 0; P0M0 = 0; //设置P0.0~P0.7为准双向口
P1M1 = 0; P1M0 = 0; //设置P1.0~P1.7为准双向口
P2M1 = 0; P2M0 = 0; //设置P2.0~P2.7为准双向口
P3M1 = 0; P3M0 = 0; //设置P3.0~P3.7为准双向口
P4M1 = 0; P4M0 = 0; //设置P4.0~P4.7为准双向口
P5M1 = 0; P5M0 = 0; //设置P5.0~P5.7为准双向口
}
使用for循环完成毫秒级别的延迟:
优点:简单易实现,不需要使用专门的硬件定时器或时钟;
缺点:1、精度不高(依赖于编译器的优化和 MCU 的时钟频率);2、使用阻塞式延迟,延迟期间,CPU 无法执行其他任务。
for循环延迟适用于需要简单延迟的嵌入式应用。
初始化UART四个串口
void Uart1234_Init(void)
{
// 选择 P1.6 和 P1.7 为 UART1 的引脚
P_SW1 |= 0x00; // P_SW1 的低 2 位控制 UART1 引脚选择,0x00 表示选择 P1.6 和 P1.7
P_SW1 &= 0xFF; // 确保 P1.6 和 P1.7 被正确选择为 UART1
// 选择 P4.6 和 P4.7 为 UART2 的引脚
P_SW2 |= S2_S; // P_SW2 的某一位控制 UART2 引脚选择,S2_S 是宏定义,表示选择 P4.6 和 P4.7
// 初始化 UART1
PCON &= 0x3F; // 清除 SMOD 和 SMOD0 位,选择 UART1 普通波特率
SCON = 0x50; // 设置 UART1 为 8 位可变波特率模式,允许接收
AUXR |= 0x01; // 选择 Timer 2 作为 UART1 的波特率发生器
// 初始化 UART2
S2CON = 0x50; // 设置 UART2 为 8 位可变波特率模式,允许接收
// 初始化 UART3
S3CON |= 0x50; // 设置 UART3 为 8 位可变波特率模式,允许接收
S3CON &= 0x70; // 选择 Timer 2 作为 UART3 的波特率发生器
// 初始化 UART4
S4CON |= 0x10; // 设置 UART4 为 8 位可变波特率模式,允许接收
S4CON &= 0x30; // 选择 Timer 2 作为 UART4 的波特率发生器
S4CON |= 0x40; // 选择 Timer 4 作为 UART4 的波特率发生器
// 配置 Timer 2 作为波特率发生器
AUXR |= 0x04; // 设置 Timer 2 时钟为 Fosc,1T 模式
T2L = (65536 - (MAIN_Fosc / 4 / BAUD));
T2H = (65536 - (MAIN_Fosc / 4 / BAUD)) >> 8; // 设置 Timer 2 的重装值
AUXR |= 0x10; // 启动 Timer 2
// 配置 Timer 3 和 Timer 4 作为波特率发生器
T4T3M |= 0x22; // 设置 Timer 3 和 Timer 4 时钟为 Fosc,1T 模式
T3L = (65536 - (MAIN_Fosc / 4 / BAUD3));
T3H = (65536 - (MAIN_Fosc / 4 / BAUD3)) >> 8;
T4L = (65536 - (MAIN_Fosc / 4 / BAUD4));
T4H = (65536 - (MAIN_Fosc / 4 / BAUD4)) >> 8;
T4T3M |= 0x88; // 启动 Timer 3 和 Timer 4
}
串口1、3是选择定时器2作为波特率发生器,串口4选择定时器4作为波特率发生器。
串口2没有设置定时器作为波特率发生器的原因可能是因为串口2的波特率生成方式不同于串口1、3和4。在这段代码中,串口2的波特率可能是通过其他方式生成的,而不是依赖于定时器。因此,在初始化串口2时,并没有设置定时器作为波特率发生器。
串口1发送字符串函数
//串口1发送字符函数
void SendDataByUart1(uint8 dat)
{
SBUF = dat; //写数据到UART1数据寄存器
while(TI == 0); //在停止位没有发送时,TI为0即一直等待
TI = 0; //清除TI位(该位必须软件清零)
}
//串口1发送字符串函数
void SendStringByUart1(uint8 *s)
{
while(*s)
{
SendDataByUart1(*s++); //将字符串中的字符一个一个发送
}
}
SBUF是芯片头文件中定义好的。
sfr(Special Function Register)是一个关键字,即特殊功能寄存器。在单片机中,特殊功能寄存器是用于控制各种外设和功能的寄存器,如串口数据传输、定时器控制等。通过定义sfr,程序员可以直接访问。
//串行口特殊功能寄存器
sfr SCON = 0x98; //0000,0000 串口1控制寄存器
sbit SM0 = SCON^7;
sbit SM1 = SCON^6;
sbit SM2 = SCON^5;
sbit REN = SCON^4;
sbit TB8 = SCON^3;
sbit RB8 = SCON^2;
sbit TI = SCON^1;
sbit RI = SCON^0;
sfr SBUF = 0x99; //xxxx,xxxx 串口1数据寄存器
sfr S2CON = 0x9A; //0000,0000 串口2控制寄存器
sfr S2BUF = 0x9B; //xxxx,xxxx 串口2数据寄存器
sfr S3CON = 0xAC; //0000,0000 串口3控制寄存器
sfr S3BUF = 0xAD; //xxxx,xxxx 串口3数据寄存器
sfr S4CON = 0x84; //0000,0000 串口4控制寄存器
sfr S4BUF = 0x85; //xxxx,xxxx 串口4数据寄存器
握手函数
bit Hand1(unsigned char *a)
{
if(strstr(Rec_Buf1, a) != NULL) // 判断字符串 a 是否在 Rec_Buf1 中
return 1; // 如果字符串 a 是 Rec_Buf1 的子串,返回 1
else
return 0; // 如果字符串 a 不是 Rec_Buf1 的子串,返回 0
}
bit
类型通常在 8051 系列单片机中使用,表示一个二进制位(0 或 1)
strstr
是标准 C 库函数,strstr
是标准 C 库函数
串口中断函数
//定义了一个 UART1 的中断服务程序.interrupt UART1_VECTOR 指定这个函数是 UART1 的中断处理程序,UART1_VECTOR 是 UART1 中断向量,using 1 表示使用寄存器组 1 来处理这个中断。
void Uart1() interrupt UART1_VECTOR using 1
{
ES = 0; // 串口1中断关闭,防止在处理过程中再次触发中断,确保数据处理的完整性。
if (RI) //串行接收到停止位的中间时刻时,该位置1
{
RI = 0; //清除RI位 (该位必须软件清零)
Rec_Buf1[i] = SBUF; //把串口1缓存SBUF寄存器数据依次存放到数组Rec_Buf1中
i++;
if(i>Buf_Max) //接收数大于定义接收数组最大个数时,覆盖接收数组之前值
{
i = 0;
}
}
if (TI) //在停止位开始发送时,该位置1
{
TI = 0; //清除TI位(该位必须软件清零)
}
ES = 1; // 串口1中断打开
}